Compare commits

..

3 Commits

Author SHA1 Message Date
c0ffeeca7
83458d24c7 Implement review comments from #104658 2023-11-29 05:36:29 +00:00
c0ffeeca7
df6d43adc4 Fix style 2023-11-28 17:44:45 +00:00
c0ffeeca7
5cbfc1c224 Add info what to enter into host field 2023-11-28 17:35:21 +00:00
536 changed files with 3695 additions and 11831 deletions

View File

@@ -633,6 +633,8 @@ omit =
homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/media_player.py
homeassistant/components/kodi/notify.py
homeassistant/components/komfovent/__init__.py
homeassistant/components/komfovent/climate.py
homeassistant/components/konnected/__init__.py
homeassistant/components/konnected/panel.py
homeassistant/components/konnected/switch.py

View File

@@ -259,8 +259,6 @@ build.json @home-assistant/supervisor
/tests/components/denonavr/ @ol-iver @starkillerOG
/homeassistant/components/derivative/ @afaucogney
/tests/components/derivative/ @afaucogney
/homeassistant/components/devialet/ @fwestenberg
/tests/components/devialet/ @fwestenberg
/homeassistant/components/device_automation/ @home-assistant/core
/tests/components/device_automation/ @home-assistant/core
/homeassistant/components/device_tracker/ @home-assistant/core
@@ -663,6 +661,8 @@ build.json @home-assistant/supervisor
/tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund
/tests/components/kodi/ @OnFreund
/homeassistant/components/komfovent/ @ProstoSanja
/tests/components/komfovent/ @ProstoSanja
/homeassistant/components/konnected/ @heythisisnate
/tests/components/konnected/ @heythisisnate
/homeassistant/components/kostal_plenticore/ @stegm

View File

@@ -1,6 +1,3 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM
FROM ${BUILD_FROM}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
"loggers": ["adax", "adax_local"],
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
"requirements": ["adax==0.3.0", "Adax-local==0.1.5"]
}

View File

@@ -6,9 +6,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The IP address of the Agent DVR server."
}
}
},

View File

@@ -3,6 +3,7 @@ from typing import Final
DOMAIN: Final = "airq"
MANUFACTURER: Final = "CorantGmbH"
TARGET_ROUTE: Final = "average"
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
UPDATE_INTERVAL: float = 10.0

View File

@@ -13,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, MANUFACTURER, UPDATE_INTERVAL
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
_LOGGER = logging.getLogger(__name__)
@@ -56,4 +56,6 @@ class AirQCoordinator(DataUpdateCoordinator):
hw_version=info["hw_version"],
)
)
return await self.airq.get_latest_data()
data = await self.airq.get(TARGET_ROUTE)
return self.airq.drop_uncertainties_from_data(data)

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.3.1"]
"requirements": ["aioairq==0.2.4"]
}

View File

@@ -16,7 +16,7 @@
"device_path": "Device Path"
},
"data_description": {
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
"host": "The hostname or IP address of AlarDecoder device that is connected to your alarm panel.",
"port": "The port on which AlarmDecoder is accessible (for example, 10000)"
}
}

View File

@@ -7,9 +7,6 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The IP address of the device running the Android IP Webcam app. The IP address is shown in the app once you start the server."
}
}
},

View File

@@ -41,6 +41,7 @@ from homeassistant.exceptions import (
Unauthorized,
)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.aiohttp_compat import enable_compression
from homeassistant.helpers.event import EventStateChangedData
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.service import async_get_all_descriptions
@@ -217,11 +218,9 @@ class APIStatesView(HomeAssistantView):
if entity_perm(state.entity_id, "read")
)
response = web.Response(
body=f'[{",".join(states)}]',
content_type=CONTENT_TYPE_JSON,
zlib_executor_size=32768,
body=f'[{",".join(states)}]', content_type=CONTENT_TYPE_JSON
)
response.enable_compression()
enable_compression(response)
return response
@@ -391,14 +390,17 @@ class APIDomainServicesView(HomeAssistantView):
)
try:
# shield the service call from cancellation on connection drop
await shield(
hass.services.async_call(
domain, service, data, blocking=True, context=context
async with timeout(SERVICE_WAIT_TIMEOUT):
# shield the service call from cancellation on connection drop
await shield(
hass.services.async_call(
domain, service, data, blocking=True, context=context
)
)
)
except (vol.Invalid, ServiceNotFound) as ex:
raise HTTPBadRequest() from ex
except TimeoutError:
pass
finally:
cancel_listen()

View File

@@ -9,7 +9,7 @@ from dataclasses import asdict, dataclass, field
from enum import StrEnum
import logging
from pathlib import Path
from queue import Empty, Queue
from queue import Queue
from threading import Thread
import time
from typing import TYPE_CHECKING, Any, Final, cast
@@ -1010,8 +1010,8 @@ class PipelineRun:
self.tts_engine = engine
self.tts_options = tts_options
async def text_to_speech(self, tts_input: str) -> None:
"""Run text-to-speech portion of pipeline."""
async def text_to_speech(self, tts_input: str) -> str:
"""Run text-to-speech portion of pipeline. Returns URL of TTS audio."""
self.process_event(
PipelineEvent(
PipelineEventType.TTS_START,
@@ -1024,40 +1024,43 @@ class PipelineRun:
)
)
if tts_input := tts_input.strip():
try:
# Synthesize audio and get URL
tts_media_id = tts_generate_media_source_id(
self.hass,
tts_input,
engine=self.tts_engine,
language=self.pipeline.tts_language,
options=self.tts_options,
)
tts_media = await media_source.async_resolve_media(
self.hass,
tts_media_id,
None,
)
except Exception as src_error:
_LOGGER.exception("Unexpected error during text-to-speech")
raise TextToSpeechError(
code="tts-failed",
message="Unexpected error during text-to-speech",
) from src_error
try:
# Synthesize audio and get URL
tts_media_id = tts_generate_media_source_id(
self.hass,
tts_input,
engine=self.tts_engine,
language=self.pipeline.tts_language,
options=self.tts_options,
)
tts_media = await media_source.async_resolve_media(
self.hass,
tts_media_id,
None,
)
except Exception as src_error:
_LOGGER.exception("Unexpected error during text-to-speech")
raise TextToSpeechError(
code="tts-failed",
message="Unexpected error during text-to-speech",
) from src_error
_LOGGER.debug("TTS result %s", tts_media)
tts_output = {
"media_id": tts_media_id,
**asdict(tts_media),
}
else:
tts_output = {}
_LOGGER.debug("TTS result %s", tts_media)
self.process_event(
PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output})
PipelineEvent(
PipelineEventType.TTS_END,
{
"tts_output": {
"media_id": tts_media_id,
**asdict(tts_media),
}
},
)
)
return tts_media.url
def _capture_chunk(self, audio_bytes: bytes | None) -> None:
"""Forward audio chunk to various capturing mechanisms."""
if self.debug_recording_queue is not None:
@@ -1244,8 +1247,6 @@ def _pipeline_debug_recording_thread_proc(
# Chunk of 16-bit mono audio at 16Khz
if wav_writer is not None:
wav_writer.writeframes(message)
except Empty:
pass # occurs when pipeline has unexpected error
except Exception: # pylint: disable=broad-exception-caught
_LOGGER.exception("Unexpected error in debug recording thread")
finally:

View File

@@ -55,9 +55,7 @@ _LOGGER = logging.getLogger(__name__)
_AsusWrtBridgeT = TypeVar("_AsusWrtBridgeT", bound="AsusWrtBridge")
_FuncType = Callable[
[_AsusWrtBridgeT], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]
]
_FuncType = Callable[[_AsusWrtBridgeT], Awaitable[list[Any] | dict[str, Any]]]
_ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]]]
@@ -83,7 +81,7 @@ def handle_errors_and_zip(
if isinstance(data, dict):
return dict(zip(keys, list(data.values())))
if not isinstance(data, (list, tuple)):
if not isinstance(data, list):
raise UpdateFailed("Received invalid data type")
return dict(zip(keys, data))

View File

@@ -2,6 +2,7 @@
"config": {
"step": {
"user": {
"title": "AsusWRT",
"description": "Set required parameter to connect to your router",
"data": {
"host": "[%key:common::config_flow::data::host%]",
@@ -10,12 +11,10 @@
"ssh_key": "Path to your SSH key file (instead of password)",
"protocol": "Communication protocol to use",
"port": "Port (leave empty for protocol default)"
},
"data_description": {
"host": "The hostname or IP address of your ASUSWRT router."
}
},
"legacy": {
"title": "AsusWRT",
"description": "Set required parameters to connect to your router",
"data": {
"mode": "Router operating mode"
@@ -38,6 +37,7 @@
"options": {
"step": {
"init": {
"title": "AsusWRT Options",
"data": {
"consider_home": "Seconds to wait before considering a device away",
"track_unknown": "Track unknown / unnamed devices",

View File

@@ -2,13 +2,10 @@
"config": {
"step": {
"user": {
"description": "Connect to the device",
"title": "Connect to the device",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of the Atag device."
}
}
},

View File

@@ -3,16 +3,12 @@
"flow_title": "{name} ({host})",
"step": {
"user": {
"description": "Set up an Axis device",
"title": "Set up Axis device",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of the Axis device.",
"username": "The user name you set up on your Axis device. It is recommended to create a user specifically for Home Assistant."
}
}
},

View File

@@ -93,6 +93,8 @@ class BAFFan(BAFEntity, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode != PRESET_MODE_AUTO:
raise ValueError(f"Invalid preset mode: {preset_mode}")
self._device.fan_mode = OffOnAuto.AUTO
async def async_set_direction(self, direction: str) -> None:

View File

@@ -2,12 +2,9 @@
"config": {
"step": {
"user": {
"description": "Connect to the Balboa Wi-Fi device",
"title": "Connect to the Balboa Wi-Fi device",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "Hostname or IP address of your Balboa Spa Wifi Device. For example, 192.168.1.58."
}
}
},

View File

@@ -25,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
from .coordinator import BlinkUpdateCoordinator
from .services import setup_services
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
@@ -74,7 +74,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blink."""
setup_services(hass)
await async_setup_services(hass)
return True

View File

@@ -1,6 +1,8 @@
"""Services for the Blink integration."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -12,7 +14,7 @@ from homeassistant.const import (
CONF_PIN,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.device_registry as dr
@@ -25,67 +27,56 @@ from .const import (
)
from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_FILENAME): cv.string,
}
)
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_PIN): cv.string,
}
{vol.Required(ATTR_DEVICE_ID): cv.ensure_list, vol.Optional(CONF_PIN): cv.string}
)
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_FILE_PATH): cv.string,
}
)
def setup_services(hass: HomeAssistant) -> None:
async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Blink integration."""
def collect_coordinators(
async def collect_coordinators(
device_ids: list[str],
) -> list[BlinkUpdateCoordinator]:
config_entries: list[ConfigEntry] = []
config_entries = list[ConfigEntry]()
registry = dr.async_get(hass)
for target in device_ids:
device = registry.async_get(target)
if device:
device_entries: list[ConfigEntry] = []
device_entries = list[ConfigEntry]()
for entry_id in device.config_entries:
entry = hass.config_entries.async_get_entry(entry_id)
if entry and entry.domain == DOMAIN:
device_entries.append(entry)
if not device_entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device",
translation_placeholders={"target": target, "domain": DOMAIN},
raise HomeAssistantError(
f"Device '{target}' is not a {DOMAIN} device"
)
config_entries.extend(device_entries)
else:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"target": target},
f"Device '{target}' not found in device registry"
)
coordinators: list[BlinkUpdateCoordinator] = []
coordinators = list[BlinkUpdateCoordinator]()
for config_entry in config_entries:
if config_entry.state != ConfigEntryState.LOADED:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="not_loaded",
translation_placeholders={"target": config_entry.title},
)
raise HomeAssistantError(f"{config_entry.title} is not loaded")
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
return coordinators
@@ -94,36 +85,24 @@ def setup_services(hass: HomeAssistant) -> None:
camera_name = call.data[CONF_NAME]
video_path = call.data[CONF_FILENAME]
if not hass.config.is_allowed_path(video_path):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_path",
translation_placeholders={"target": video_path},
)
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
_LOGGER.error("Can't write %s, no access to path!", video_path)
return
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
all_cameras = coordinator.api.cameras
if camera_name in all_cameras:
try:
await all_cameras[camera_name].video_to_file(video_path)
except OSError as err:
raise ServiceValidationError(
str(err),
translation_domain=DOMAIN,
translation_key="cant_write",
) from err
_LOGGER.error("Can't write image to file: %s", err)
async def async_handle_save_recent_clips_service(call: ServiceCall) -> None:
"""Save multiple recent clips to output directory."""
camera_name = call.data[CONF_NAME]
clips_dir = call.data[CONF_FILE_PATH]
if not hass.config.is_allowed_path(clips_dir):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_path",
translation_placeholders={"target": clips_dir},
)
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
_LOGGER.error("Can't write to directory %s, no access to path!", clips_dir)
return
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
all_cameras = coordinator.api.cameras
if camera_name in all_cameras:
try:
@@ -131,15 +110,11 @@ def setup_services(hass: HomeAssistant) -> None:
output_dir=clips_dir
)
except OSError as err:
raise ServiceValidationError(
str(err),
translation_domain=DOMAIN,
translation_key="cant_write",
) from err
_LOGGER.error("Can't write recent clips to directory: %s", err)
async def send_pin(call: ServiceCall):
"""Call blink to send new pin."""
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
await coordinator.api.auth.send_auth_key(
coordinator.api,
call.data[CONF_PIN],
@@ -147,7 +122,7 @@ def setup_services(hass: HomeAssistant) -> None:
async def blink_refresh(call: ServiceCall):
"""Call blink to refresh info."""
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
await coordinator.api.refresh(force_cache=True)
# Register all the above services

View File

@@ -101,22 +101,5 @@
}
}
}
},
"exceptions": {
"invalid_device": {
"message": "Device '{target}' is not a {domain} device"
},
"device_not_found": {
"message": "Device '{target}' not found in device registry"
},
"no_path": {
"message": "Can't write to directory {target}, no access to path!"
},
"cant_write": {
"message": "Can't write to file"
},
"not_loaded": {
"message": "{target} is not loaded"
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected[china]==0.14.6"]
"requirements": ["bimmer-connected==0.14.3"]
}

View File

@@ -199,6 +199,10 @@ class BondFan(BondEntity, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode != PRESET_MODE_BREEZE or not self._device.has_action(
Action.BREEZE_ON
):
raise ValueError(f"Invalid preset mode: {preset_mode}")
await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON))
async def async_turn_off(self, **kwargs: Any) -> None:

View File

@@ -12,9 +12,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"access_token": "[%key:common::config_flow::data::access_token%]"
},
"data_description": {
"host": "The IP address of your Bond hub."
}
}
},

View File

@@ -6,9 +6,6 @@
"title": "SHC authentication parameters",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your Bosch Smart Home Controller."
}
},
"credentials": {

View File

@@ -5,9 +5,6 @@
"description": "Ensure that your TV is turned on before trying to set it up.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Sony Bravia TV to control."
}
},
"authorize": {

View File

@@ -3,13 +3,10 @@
"flow_title": "{name} ({model} at {host})",
"step": {
"user": {
"description": "Connect to the device",
"title": "Connect to the device",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"timeout": "Timeout"
},
"data_description": {
"host": "The hostname or IP address of your Broadlink device."
}
},
"auth": {

View File

@@ -6,9 +6,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"type": "Type of the printer"
},
"data_description": {
"host": "The hostname or IP address of the Brother printer to control."
}
},
"zeroconf_confirm": {

View File

@@ -60,7 +60,8 @@ async def async_setup_entry(
data.static,
entry,
)
]
],
True,
)

View File

@@ -11,9 +11,6 @@
"passkey": "Passkey string",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your BSB-Lan device."
}
}
},

View File

@@ -11,11 +11,7 @@ async def async_get_calendars(
hass: HomeAssistant, client: caldav.DAVClient, component: str
) -> list[caldav.Calendar]:
"""Get all calendars that support the specified component."""
def _get_calendars() -> list[caldav.Calendar]:
return client.principal().calendars()
calendars = await hass.async_add_executor_job(_get_calendars)
calendars = await hass.async_add_executor_job(client.principal().calendars)
components_results = await asyncio.gather(
*[
hass.async_add_executor_job(calendar.get_supported_components)

View File

@@ -2,10 +2,10 @@
from __future__ import annotations
import asyncio
from datetime import date, datetime, timedelta
from datetime import timedelta
from functools import partial
import logging
from typing import Any, cast
from typing import cast
import caldav
from caldav.lib.error import DAVError, NotFoundError
@@ -21,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .api import async_get_calendars, get_attr_value
from .const import DOMAIN
@@ -72,12 +71,6 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
or (summary := get_attr_value(todo, "summary")) is None
):
return None
due: date | datetime | None = None
if due_value := get_attr_value(todo, "due"):
if isinstance(due_value, datetime):
due = dt_util.as_local(due_value)
elif isinstance(due_value, date):
due = due_value
return TodoItem(
uid=uid,
summary=summary,
@@ -85,28 +78,9 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
get_attr_value(todo, "status") or "",
TodoItemStatus.NEEDS_ACTION,
),
due=due,
description=get_attr_value(todo, "description"),
)
def _to_ics_fields(item: TodoItem) -> dict[str, Any]:
"""Convert a TodoItem to the set of add or update arguments."""
item_data: dict[str, Any] = {}
if summary := item.summary:
item_data["summary"] = summary
if status := item.status:
item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
if due := item.due:
if isinstance(due, datetime):
item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ")
else:
item_data["due"] = due.strftime("%Y%m%d")
if description := item.description:
item_data["description"] = description
return item_data
class WebDavTodoListEntity(TodoListEntity):
"""CalDAV To-do list entity."""
@@ -115,9 +89,6 @@ class WebDavTodoListEntity(TodoListEntity):
TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
@@ -145,7 +116,13 @@ class WebDavTodoListEntity(TodoListEntity):
"""Add an item to the To-do list."""
try:
await self.hass.async_add_executor_job(
partial(self._calendar.save_todo, **_to_ics_fields(item)),
partial(
self._calendar.save_todo,
summary=item.summary,
status=TODO_STATUS_MAP_INV.get(
item.status or TodoItemStatus.NEEDS_ACTION, "NEEDS-ACTION"
),
),
)
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@@ -162,7 +139,10 @@ class WebDavTodoListEntity(TodoListEntity):
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
vtodo = todo.icalendar_component # type: ignore[attr-defined]
vtodo.update(**_to_ics_fields(item))
if item.summary:
vtodo["summary"] = item.summary
if item.status:
vtodo["status"] = TODO_STATUS_MAP_INV.get(item.status, "NEEDS-ACTION")
try:
await self.hass.async_add_executor_job(
partial(

View File

@@ -73,7 +73,7 @@
}
},
"get_events": {
"name": "Get events",
"name": "Get event",
"description": "Get events on a calendar within a time range.",
"fields": {
"start_date_time": {

View File

@@ -68,13 +68,13 @@ class ComelitSerialBridge(DataUpdateCoordinator):
async def _async_update_data(self) -> dict[str, Any]:
"""Update device data."""
_LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host)
try:
await self.api.login()
return await self.api.get_all_devices()
except exceptions.CannotConnect as err:
_LOGGER.warning("Connection error for %s", self._host)
await self.api.close()
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
except exceptions.CannotAuthenticate:
raise ConfigEntryAuthFailed
return await self.api.get_all_devices()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/comelit",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.6.2"]
"requirements": ["aiocomelit==0.5.2"]
}

View File

@@ -13,9 +13,6 @@
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"pin": "[%key:common::config_flow::data::pin%]"
},
"data_description": {
"host": "The hostname or IP address of your Comelit device."
}
}
},

View File

@@ -649,7 +649,7 @@ class DefaultAgent(AbstractConversationAgent):
if device_area is None:
return None
return {"area": device_area.id}
return {"area": device_area.name}
def _get_error_text(
self, response_type: ResponseType, lang_intents: LanguageIntents | None

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.12.05"]
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.11.17"]
}

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "Set up your CoolMasterNet connection details.",
"title": "Set up your CoolMasterNet connection details.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"off": "Can be turned off",
@@ -12,9 +12,6 @@
"dry": "Support dry mode",
"fan_only": "Support fan only mode",
"swing_support": "Control swing mode"
},
"data_description": {
"host": "The hostname or IP address of your CoolMasterNet device."
}
}
},

View File

@@ -67,7 +67,7 @@ DECONZ_TO_COLOR_MODE = {
LightColorMode.XY: ColorMode.XY,
}
XMAS_LIGHT_EFFECTS = [
TS0601_EFFECTS = [
"carnival",
"collide",
"fading",
@@ -200,8 +200,8 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
if device.effect is not None:
self._attr_supported_features |= LightEntityFeature.EFFECT
self._attr_effect_list = [EFFECT_COLORLOOP]
if device.model_id in ("HG06467", "TS0601"):
self._attr_effect_list = XMAS_LIGHT_EFFECTS
if device.model_id == "TS0601":
self._attr_effect_list += TS0601_EFFECTS
@property
def color_mode(self) -> str | None:

View File

@@ -11,14 +11,11 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your deCONZ host."
}
},
"link": {
"title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings > Gateway > Advanced\n2. Press \"Authenticate app\" button"
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
},
"hassio_confirm": {
"title": "deCONZ Zigbee gateway via Home Assistant add-on",

View File

@@ -9,9 +9,6 @@
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"web_port": "Web port (for visiting service)"
},
"data_description": {
"host": "The hostname or IP address of your Deluge device."
}
}
},

View File

@@ -161,9 +161,12 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
self._preset_mode = preset_mode
self._percentage = None
self.schedule_update_ha_state()
if self.preset_modes and preset_mode in self.preset_modes:
self._preset_mode = preset_mode
self._percentage = None
self.schedule_update_ha_state()
else:
raise ValueError(f"Invalid preset mode: {preset_mode}")
def turn_on(
self,
@@ -227,6 +230,10 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if self.preset_modes is None or preset_mode not in self.preset_modes:
raise ValueError(
f"{preset_mode} is not a valid preset_mode: {self.preset_modes}"
)
self._preset_mode = preset_mode
self._percentage = None
self.async_write_ha_state()

View File

@@ -1,31 +0,0 @@
"""The Devialet integration."""
from __future__ import annotations
from devialet import DevialetApi
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Devialet from a config entry."""
session = async_get_clientsession(hass)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = DevialetApi(
entry.data[CONF_HOST], session
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Devialet config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
del hass.data[DOMAIN][entry.entry_id]
return unload_ok

View File

@@ -1,104 +0,0 @@
"""Support for Devialet Phantom speakers."""
from __future__ import annotations
import logging
from typing import Any
from devialet.devialet_api import DevialetApi
import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
LOGGER = logging.getLogger(__package__)
class DevialetFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Devialet."""
VERSION = 1
def __init__(self) -> None:
"""Initialize flow."""
self._host: str | None = None
self._name: str | None = None
self._model: str | None = None
self._serial: str | None = None
self._errors: dict[str, str] = {}
async def async_validate_input(self) -> FlowResult | None:
"""Validate the input using the Devialet API."""
self._errors.clear()
session = async_get_clientsession(self.hass)
client = DevialetApi(self._host, session)
if not await client.async_update() or client.serial is None:
self._errors["base"] = "cannot_connect"
LOGGER.error("Cannot connect")
return None
await self.async_set_unique_id(client.serial)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=client.device_name,
data={CONF_HOST: self._host, CONF_NAME: client.device_name},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user or zeroconf."""
if user_input is not None:
self._host = user_input[CONF_HOST]
result = await self.async_validate_input()
if result is not None:
return result
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=self._errors,
)
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle a flow initialized by zeroconf discovery."""
LOGGER.info("Devialet device found via ZEROCONF: %s", discovery_info)
self._host = discovery_info.host
self._name = discovery_info.name.split(".", 1)[0]
self._model = discovery_info.properties["model"]
self._serial = discovery_info.properties["serialNumber"]
await self.async_set_unique_id(self._serial)
self._abort_if_unique_id_configured()
self.context["title_placeholders"] = {"title": self._name}
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle user-confirmation of discovered node."""
title = f"{self._name} ({self._model})"
if user_input is not None:
result = await self.async_validate_input()
if result is not None:
return result
return self.async_show_form(
step_id="confirm",
description_placeholders={"device": self._model, "title": title},
errors=self._errors,
last_step=True,
)

View File

@@ -1,12 +0,0 @@
"""Constants for the Devialet integration."""
from typing import Final
DOMAIN: Final = "devialet"
MANUFACTURER: Final = "Devialet"
SOUND_MODES = {
"Custom": "custom",
"Flat": "flat",
"Night mode": "night mode",
"Voice": "voice",
}

View File

@@ -1,32 +0,0 @@
"""Class representing a Devialet update coordinator."""
from datetime import timedelta
import logging
from devialet import DevialetApi
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
class DevialetCoordinator(DataUpdateCoordinator[None]):
"""Devialet update coordinator."""
def __init__(self, hass: HomeAssistant, client: DevialetApi) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.client = client
async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.client.async_update()

View File

@@ -1,20 +0,0 @@
"""Diagnostics support for Devialet."""
from __future__ import annotations
from typing import Any
from devialet import DevialetApi
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
client: DevialetApi = hass.data[DOMAIN][entry.entry_id]
return await client.async_get_diagnostics()

View File

@@ -1,12 +0,0 @@
{
"domain": "devialet",
"name": "Devialet",
"after_dependencies": ["zeroconf"],
"codeowners": ["@fwestenberg"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/devialet",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["devialet==1.4.3"],
"zeroconf": ["_devialet-http._tcp.local."]
}

View File

@@ -1,212 +0,0 @@
"""Support for Devialet speakers."""
from __future__ import annotations
from devialet.const import NORMAL_INPUTS
from homeassistant.components.media_player import (
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, SOUND_MODES
from .coordinator import DevialetCoordinator
SUPPORT_DEVIALET = (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
)
DEVIALET_TO_HA_FEATURE_MAP = {
"play": MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP,
"pause": MediaPlayerEntityFeature.PAUSE,
"previous": MediaPlayerEntityFeature.PREVIOUS_TRACK,
"next": MediaPlayerEntityFeature.NEXT_TRACK,
"seek": MediaPlayerEntityFeature.SEEK,
}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Devialet entry."""
client = hass.data[DOMAIN][entry.entry_id]
coordinator = DevialetCoordinator(hass, client)
await coordinator.async_config_entry_first_refresh()
async_add_entities([DevialetMediaPlayerEntity(coordinator, entry)])
class DevialetMediaPlayerEntity(
CoordinatorEntity[DevialetCoordinator], MediaPlayerEntity
):
"""Devialet media player."""
_attr_has_entity_name = True
_attr_name = None
def __init__(self, coordinator: DevialetCoordinator, entry: ConfigEntry) -> None:
"""Initialize the Devialet device."""
self.coordinator = coordinator
super().__init__(coordinator)
self._attr_unique_id = str(entry.unique_id)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=MANUFACTURER,
model=self.coordinator.client.model,
name=entry.data[CONF_NAME],
sw_version=self.coordinator.client.version,
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if not self.coordinator.client.is_available:
self.async_write_ha_state()
return
self._attr_volume_level = self.coordinator.client.volume_level
self._attr_is_volume_muted = self.coordinator.client.is_volume_muted
self._attr_source_list = self.coordinator.client.source_list
self._attr_sound_mode_list = sorted(SOUND_MODES)
self._attr_media_artist = self.coordinator.client.media_artist
self._attr_media_album_name = self.coordinator.client.media_album_name
self._attr_media_artist = self.coordinator.client.media_artist
self._attr_media_image_url = self.coordinator.client.media_image_url
self._attr_media_duration = self.coordinator.client.media_duration
self._attr_media_position = self.coordinator.client.current_position
self._attr_media_position_updated_at = (
self.coordinator.client.position_updated_at
)
self._attr_media_title = (
self.coordinator.client.media_title
if self.coordinator.client.media_title
else self.source
)
self.async_write_ha_state()
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the device."""
playing_state = self.coordinator.client.playing_state
if not playing_state:
return MediaPlayerState.IDLE
if playing_state == "playing":
return MediaPlayerState.PLAYING
if playing_state == "paused":
return MediaPlayerState.PAUSED
return MediaPlayerState.ON
@property
def available(self) -> bool:
"""Return if the media player is available."""
return self.coordinator.client.is_available
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = SUPPORT_DEVIALET
if self.coordinator.client.source_state is None:
return features
if not self.coordinator.client.available_options:
return features
for option in self.coordinator.client.available_options:
features |= DEVIALET_TO_HA_FEATURE_MAP.get(option, 0)
return features
@property
def source(self) -> str | None:
"""Return the current input source."""
source = self.coordinator.client.source
for pretty_name, name in NORMAL_INPUTS.items():
if source == name:
return pretty_name
return None
@property
def sound_mode(self) -> str | None:
"""Return the current sound mode."""
if self.coordinator.client.equalizer is not None:
sound_mode = self.coordinator.client.equalizer
elif self.coordinator.client.night_mode:
sound_mode = "night mode"
else:
return None
for pretty_name, mode in SOUND_MODES.items():
if sound_mode == mode:
return pretty_name
return None
async def async_volume_up(self) -> None:
"""Volume up media player."""
await self.coordinator.client.async_volume_up()
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self.coordinator.client.async_volume_down()
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
await self.coordinator.client.async_set_volume_level(volume)
async def async_mute_volume(self, mute: bool) -> None:
"""Mute (true) or unmute (false) media player."""
await self.coordinator.client.async_mute_volume(mute)
async def async_media_play(self) -> None:
"""Play media player."""
await self.coordinator.client.async_media_play()
async def async_media_pause(self) -> None:
"""Pause media player."""
await self.coordinator.client.async_media_pause()
async def async_media_stop(self) -> None:
"""Pause media player."""
await self.coordinator.client.async_media_stop()
async def async_media_next_track(self) -> None:
"""Send the next track command."""
await self.coordinator.client.async_media_next_track()
async def async_media_previous_track(self) -> None:
"""Send the previous track command."""
await self.coordinator.client.async_media_previous_track()
async def async_media_seek(self, position: float) -> None:
"""Send seek command."""
await self.coordinator.client.async_media_seek(position)
async def async_select_sound_mode(self, sound_mode: str) -> None:
"""Send sound mode command."""
for pretty_name, mode in SOUND_MODES.items():
if sound_mode == pretty_name:
if mode == "night mode":
await self.coordinator.client.async_set_night_mode(True)
else:
await self.coordinator.client.async_set_night_mode(False)
await self.coordinator.client.async_set_equalizer(mode)
async def async_turn_off(self) -> None:
"""Turn off media player."""
await self.coordinator.client.async_turn_off()
async def async_select_source(self, source: str) -> None:
"""Select input source."""
await self.coordinator.client.async_select_source(source)

View File

@@ -1,22 +0,0 @@
{
"config": {
"flow_title": "{title}",
"step": {
"user": {
"description": "Please enter the host name or IP address of the Devialet device.",
"data": {
"host": "Host"
}
},
"confirm": {
"description": "Do you want to set up Devialet device {device}?"
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}

View File

@@ -1,22 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured"
},
"error": {
"cannot_connect": "Failed to connect"
},
"flow_title": "{title}",
"step": {
"confirm": {
"description": "Do you want to set up Devialet device {device}?"
},
"user": {
"data": {
"host": "Host"
},
"description": "Please enter the host name or IP address of the Devialet device."
}
}
}
}

View File

@@ -1033,19 +1033,6 @@ def update_config(path: str, dev_id: str, device: Device) -> None:
out.write(dump(device_config))
def remove_device_from_config(hass: HomeAssistant, device_id: str) -> None:
"""Remove device from YAML configuration file."""
path = hass.config.path(YAML_DEVICES)
devices = load_yaml_config_file(path)
devices.pop(device_id)
dumped = dump(devices)
with open(path, "r+", encoding="utf8") as out:
out.seek(0)
out.truncate()
out.write(dumped)
def get_gravatar_for_email(email: str) -> str:
"""Return an 80px Gravatar for the given email address.

View File

@@ -8,9 +8,6 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your DirectTV device."
}
}
},

View File

@@ -183,7 +183,6 @@ async def async_setup_entry(
for description in sensors
for value_key in {description.key, *description.alternative_keys}
if description.value_fn(coordinator.data, value_key, description.scale)
is not None
)
async_add_entities(entities)

View File

@@ -9,7 +9,6 @@
"use_legacy_protocol": "Use legacy protocol"
},
"data_description": {
"host": "The hostname or IP address of your D-Link device",
"password": "Default: PIN code on the back."
}
},

View File

@@ -17,11 +17,8 @@
"data": {
"password": "[%key:common::config_flow::data::password%]",
"host": "[%key:common::config_flow::data::host%]",
"name": "Device name",
"name": "Device Name",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"host": "The hostname or IP address of your DoorBird device."
}
}
},

View File

@@ -4,9 +4,6 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your Dremel 3D printer."
}
}
},

View File

@@ -12,6 +12,7 @@ LOGGER = logging.getLogger(__package__)
PLATFORMS = [Platform.SENSOR]
CONF_DSMR_VERSION = "dsmr_version"
CONF_PROTOCOL = "protocol"
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_PRECISION = "precision"
CONF_TIME_BETWEEN_UPDATE = "time_between_update"
@@ -28,7 +29,6 @@ DATA_TASK = "task"
DEVICE_NAME_ELECTRICITY = "Electricity Meter"
DEVICE_NAME_GAS = "Gas Meter"
DEVICE_NAME_WATER = "Water Meter"
DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}

View File

@@ -34,7 +34,6 @@ from homeassistant.const import (
UnitOfVolume,
)
from homeassistant.core import CoreState, Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -48,6 +47,7 @@ from .const import (
CONF_DSMR_VERSION,
CONF_PRECISION,
CONF_PROTOCOL,
CONF_RECONNECT_INTERVAL,
CONF_SERIAL_ID,
CONF_SERIAL_ID_GAS,
CONF_TIME_BETWEEN_UPDATE,
@@ -57,7 +57,6 @@ from .const import (
DEFAULT_TIME_BETWEEN_UPDATE,
DEVICE_NAME_ELECTRICITY,
DEVICE_NAME_GAS,
DEVICE_NAME_WATER,
DOMAIN,
DSMR_PROTOCOL,
LOGGER,
@@ -74,7 +73,6 @@ class DSMRSensorEntityDescription(SensorEntityDescription):
dsmr_versions: set[str] | None = None
is_gas: bool = False
is_water: bool = False
obis_reference: str
@@ -376,138 +374,28 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
)
def create_mbus_entity(
mbus: int, mtype: int, telegram: dict[str, DSMRObject]
) -> DSMRSensorEntityDescription | None:
"""Create a new MBUS Entity."""
if (
mtype == 3
and (
obis_reference := getattr(
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2"
)
)
in telegram
):
return DSMRSensorEntityDescription(
key=f"mbus{mbus}_gas_reading",
translation_key="gas_meter_reading",
obis_reference=obis_reference,
is_gas=True,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
)
if (
mtype == 7
and (
obis_reference := getattr(
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING1"
)
)
in telegram
):
return DSMRSensorEntityDescription(
key=f"mbus{mbus}_water_reading",
translation_key="water_meter_reading",
obis_reference=obis_reference,
is_water=True,
device_class=SensorDeviceClass.WATER,
state_class=SensorStateClass.TOTAL_INCREASING,
)
return None
def device_class_and_uom(
telegram: dict[str, DSMRObject],
entity_description: DSMRSensorEntityDescription,
) -> tuple[SensorDeviceClass | None, str | None]:
"""Get native unit of measurement from telegram,."""
dsmr_object = telegram[entity_description.obis_reference]
uom: str | None = getattr(dsmr_object, "unit") or None
with suppress(ValueError):
if entity_description.device_class == SensorDeviceClass.GAS and (
enery_uom := UnitOfEnergy(str(uom))
):
return (SensorDeviceClass.ENERGY, enery_uom)
if uom in UNIT_CONVERSION:
return (entity_description.device_class, UNIT_CONVERSION[uom])
return (entity_description.device_class, uom)
def rename_old_gas_to_mbus(
hass: HomeAssistant, entry: ConfigEntry, mbus_device_id: str
) -> None:
"""Rename old gas sensor to mbus variant."""
dev_reg = dr.async_get(hass)
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
if device_entry_v1 is not None:
device_id = device_entry_v1.id
ent_reg = er.async_get(hass)
entries = er.async_entries_for_device(ent_reg, device_id)
for entity in entries:
if entity.unique_id.endswith("belgium_5min_gas_meter_reading"):
try:
ent_reg.async_update_entity(
entity.entity_id,
new_unique_id=mbus_device_id,
device_id=mbus_device_id,
)
except ValueError:
LOGGER.debug(
"Skip migration of %s because it already exists",
entity.entity_id,
)
else:
LOGGER.debug(
"Migrated entity %s from unique id %s to %s",
entity.entity_id,
entity.unique_id,
mbus_device_id,
)
# Cleanup old device
dev_entities = er.async_entries_for_device(
ent_reg, device_id, include_disabled_entities=True
)
if not dev_entities:
dev_reg.async_remove_device(device_id)
def create_mbus_entities(
hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry
) -> list[DSMREntity]:
"""Create MBUS Entities."""
entities = []
for idx in range(1, 5):
if (
device_type := getattr(obis_references, f"BELGIUM_MBUS{idx}_DEVICE_TYPE")
) not in telegram:
continue
if (type_ := int(telegram[device_type].value)) not in (3, 7):
continue
if (
identifier := getattr(
obis_references,
f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER",
)
) in telegram:
serial_ = telegram[identifier].value
rename_old_gas_to_mbus(hass, entry, serial_)
else:
serial_ = ""
if description := create_mbus_entity(idx, type_, telegram):
entities.append(
DSMREntity(
description,
entry,
telegram,
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
serial_,
idx,
)
)
return entities
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
"""Return correct entity for 5B Gas meter."""
ref = None
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS1_METER_READING2
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS2_METER_READING2
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS3_METER_READING2
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS4_METER_READING2
elif ref is None:
ref = obis_references.BELGIUM_MBUS1_METER_READING2
return DSMRSensorEntityDescription(
key="belgium_5min_gas_meter_reading",
translation_key="gas_meter_reading",
obis_reference=ref,
dsmr_versions={"5B"},
is_gas=True,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
)
async def async_setup_entry(
@@ -527,10 +415,25 @@ async def async_setup_entry(
add_entities_handler()
add_entities_handler = None
def device_class_and_uom(
telegram: dict[str, DSMRObject],
entity_description: DSMRSensorEntityDescription,
) -> tuple[SensorDeviceClass | None, str | None]:
"""Get native unit of measurement from telegram,."""
dsmr_object = telegram[entity_description.obis_reference]
uom: str | None = getattr(dsmr_object, "unit") or None
with suppress(ValueError):
if entity_description.device_class == SensorDeviceClass.GAS and (
enery_uom := UnitOfEnergy(str(uom))
):
return (SensorDeviceClass.ENERGY, enery_uom)
if uom in UNIT_CONVERSION:
return (entity_description.device_class, UNIT_CONVERSION[uom])
return (entity_description.device_class, uom)
all_sensors = SENSORS
if dsmr_version == "5B":
mbus_entities = create_mbus_entities(hass, telegram, entry)
for mbus_entity in mbus_entities:
entities.append(mbus_entity)
all_sensors += (add_gas_sensor_5B(telegram),)
entities.extend(
[
@@ -540,7 +443,7 @@ async def async_setup_entry(
telegram,
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
)
for description in SENSORS
for description in all_sensors
if (
description.dsmr_versions is None
or dsmr_version in description.dsmr_versions
@@ -646,7 +549,9 @@ async def async_setup_entry(
update_entities_telegram(None)
# throttle reconnect attempts
await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
await asyncio.sleep(
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
)
except (serial.serialutil.SerialException, OSError):
# Log any error while establishing connection and drop to retry
@@ -660,7 +565,9 @@ async def async_setup_entry(
update_entities_telegram(None)
# throttle reconnect attempts
await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
await asyncio.sleep(
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
)
except CancelledError:
# Reflect disconnect state in devices state by setting an
# None telegram resulting in `unavailable` states
@@ -711,8 +618,6 @@ class DSMREntity(SensorEntity):
telegram: dict[str, DSMRObject],
device_class: SensorDeviceClass,
native_unit_of_measurement: str | None,
serial_id: str = "",
mbus_id: int = 0,
) -> None:
"""Initialize entity."""
self.entity_description = entity_description
@@ -724,15 +629,8 @@ class DSMREntity(SensorEntity):
device_serial = entry.data[CONF_SERIAL_ID]
device_name = DEVICE_NAME_ELECTRICITY
if entity_description.is_gas:
if serial_id:
device_serial = serial_id
else:
device_serial = entry.data[CONF_SERIAL_ID_GAS]
device_serial = entry.data[CONF_SERIAL_ID_GAS]
device_name = DEVICE_NAME_GAS
if entity_description.is_water:
if serial_id:
device_serial = serial_id
device_name = DEVICE_NAME_WATER
if device_serial is None:
device_serial = entry.entry_id
@@ -740,13 +638,7 @@ class DSMREntity(SensorEntity):
identifiers={(DOMAIN, device_serial)},
name=device_name,
)
if mbus_id != 0:
if serial_id:
self._attr_unique_id = f"{device_serial}"
else:
self._attr_unique_id = f"{device_serial}_{mbus_id}"
else:
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
@callback
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
@@ -794,10 +686,6 @@ class DSMREntity(SensorEntity):
float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION)
)
# Make sure we do not return a zero value for an energy sensor
if not value and self.state_class == SensorStateClass.TOTAL_INCREASING:
return None
return value
@staticmethod

View File

@@ -147,9 +147,6 @@
},
"voltage_swell_l3_count": {
"name": "Voltage swells phase L3"
},
"water_meter_reading": {
"name": "Water consumption"
}
}
},

View File

@@ -5,9 +5,6 @@
"description": "Ensure that your player is turned on.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your Dune HD device."
}
}
},

View File

@@ -6,9 +6,6 @@
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your Duotecno device."
}
}
},

View File

@@ -6,9 +6,6 @@
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your Ecoforest device."
}
}
},

View File

@@ -7,9 +7,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Elgato device."
}
},
"zeroconf_confirm": {

View File

@@ -5,9 +5,6 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your SiteSage Emonitor device."
}
},
"confirm": {

View File

@@ -6,6 +6,7 @@ import logging
from aiohttp import web
import voluptuous as vol
from homeassistant.components.http import HomeAssistantAccessLogger
from homeassistant.components.network import async_get_source_ip
from homeassistant.const import (
CONF_ENTITIES,
@@ -100,7 +101,7 @@ async def start_emulated_hue_bridge(
config.advertise_port or config.listen_port,
)
runner = web.AppRunner(app)
runner = web.AppRunner(app, access_log_class=HomeAssistantAccessLogger)
await runner.setup()
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)

View File

@@ -317,11 +317,6 @@ class EnergyCostSensor(SensorEntity):
try:
energy_price = float(energy_price_state.state)
except ValueError:
if self._last_energy_sensor_state is None:
# Initialize as it's the first time all required entities except
# price are in place. This means that the cost will update the first
# time the energy is updated after the price entity is in place.
self._reset(energy_state)
return
energy_price_unit: str | None = energy_price_state.attributes.get(

View File

@@ -8,9 +8,6 @@
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your Enphase Envoy gateway."
}
}
},

View File

@@ -5,9 +5,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"host": "The hostname or IP address of your Epson projector."
}
}
},

View File

@@ -22,7 +22,6 @@ from aioesphomeapi import (
APIClient,
APIVersion,
BLEConnectionError,
BluetoothConnectionDroppedError,
BluetoothProxyFeature,
DeviceInfo,
)
@@ -31,6 +30,7 @@ from aioesphomeapi.core import (
BluetoothGATTAPIError,
TimeoutAPIError,
)
from async_interrupt import interrupt
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.client import BaseBleakClient, NotifyCallback
from bleak.backends.device import BLEDevice
@@ -68,25 +68,39 @@ def mac_to_int(address: str) -> int:
return int(address.replace(":", ""), 16)
def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
"""Define a wrapper throw BleakError if not connected."""
async def _async_wrap_bluetooth_connected_operation(
self: ESPHomeClient, *args: Any, **kwargs: Any
) -> Any:
# pylint: disable=protected-access
if not self._is_connected:
raise BleakError(f"{self._description} is not connected")
loop = self._loop
disconnected_futures = self._disconnected_futures
disconnected_future = loop.create_future()
disconnected_futures.add(disconnected_future)
disconnect_message = f"{self._description}: Disconnected during operation"
try:
async with interrupt(disconnected_future, BleakError, disconnect_message):
return await func(self, *args, **kwargs)
finally:
disconnected_futures.discard(disconnected_future)
return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation)
def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
"""Define a wrapper throw esphome api errors as BleakErrors."""
async def _async_wrap_bluetooth_operation(
self: ESPHomeClient, *args: Any, **kwargs: Any
) -> Any:
# pylint: disable=protected-access
try:
return await func(self, *args, **kwargs)
except TimeoutAPIError as err:
raise asyncio.TimeoutError(str(err)) from err
except BluetoothConnectionDroppedError as ex:
_LOGGER.debug(
"%s: BLE device disconnected during %s operation",
self._description,
func.__name__,
)
self._async_ble_device_disconnected()
raise BleakError(str(ex)) from ex
except BluetoothGATTAPIError as ex:
# If the device disconnects in the middle of an operation
# be sure to mark it as disconnected so any library using
@@ -97,6 +111,7 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
# before the callback is delivered.
if ex.error.error == -1:
# pylint: disable=protected-access
_LOGGER.debug(
"%s: BLE device disconnected during %s operation",
self._description,
@@ -154,6 +169,7 @@ class ESPHomeClient(BaseBleakClient):
self._notify_cancels: dict[
int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]]
] = {}
self._disconnected_futures: set[asyncio.Future[None]] = set()
self._device_info = client_data.device_info
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
client_data.api_version
@@ -169,7 +185,24 @@ class ESPHomeClient(BaseBleakClient):
def __str__(self) -> str:
"""Return the string representation of the client."""
return f"ESPHomeClient ({self._description})"
return f"ESPHomeClient ({self.address})"
def _unsubscribe_connection_state(self) -> None:
"""Unsubscribe from connection state updates."""
if not self._cancel_connection_state:
return
try:
self._cancel_connection_state()
except (AssertionError, ValueError) as ex:
_LOGGER.debug(
(
"%s: Failed to unsubscribe from connection state (likely"
" connection dropped): %s"
),
self._description,
ex,
)
self._cancel_connection_state = None
def _async_disconnected_cleanup(self) -> None:
"""Clean up on disconnect."""
@@ -178,10 +211,12 @@ class ESPHomeClient(BaseBleakClient):
for _, notify_abort in self._notify_cancels.values():
notify_abort()
self._notify_cancels.clear()
for future in self._disconnected_futures:
if not future.done():
future.set_result(None)
self._disconnected_futures.clear()
self._disconnect_callbacks.discard(self._async_esp_disconnected)
if self._cancel_connection_state:
self._cancel_connection_state()
self._cancel_connection_state = None
self._unsubscribe_connection_state()
def _async_ble_device_disconnected(self) -> None:
"""Handle the BLE device disconnecting from the ESP."""
@@ -371,6 +406,7 @@ class ESPHomeClient(BaseBleakClient):
"""Get ATT MTU size for active connection."""
return self._mtu or DEFAULT_MTU
@verify_connected
@api_error_as_bleak_error
async def pair(self, *args: Any, **kwargs: Any) -> bool:
"""Attempt to pair."""
@@ -379,7 +415,6 @@ class ESPHomeClient(BaseBleakClient):
"Pairing is not available in this version ESPHome; "
f"Upgrade the ESPHome version on the {self._device_info.name} device."
)
self._raise_if_not_connected()
response = await self._client.bluetooth_device_pair(self._address_as_int)
if response.paired:
return True
@@ -388,6 +423,7 @@ class ESPHomeClient(BaseBleakClient):
)
return False
@verify_connected
@api_error_as_bleak_error
async def unpair(self) -> bool:
"""Attempt to unpair."""
@@ -396,7 +432,6 @@ class ESPHomeClient(BaseBleakClient):
"Unpairing is not available in this version ESPHome; "
f"Upgrade the ESPHome version on the {self._device_info.name} device."
)
self._raise_if_not_connected()
response = await self._client.bluetooth_device_unpair(self._address_as_int)
if response.success:
return True
@@ -419,6 +454,7 @@ class ESPHomeClient(BaseBleakClient):
dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs
)
@verify_connected
async def _get_services(
self, dangerous_use_bleak_cache: bool = False, **kwargs: Any
) -> BleakGATTServiceCollection:
@@ -426,7 +462,6 @@ class ESPHomeClient(BaseBleakClient):
Must only be called from get_services or connected
"""
self._raise_if_not_connected()
address_as_int = self._address_as_int
cache = self._cache
# If the connection version >= 3, we must use the cache
@@ -492,6 +527,7 @@ class ESPHomeClient(BaseBleakClient):
)
return characteristic
@verify_connected
@api_error_as_bleak_error
async def clear_cache(self) -> bool:
"""Clear the GATT cache."""
@@ -505,7 +541,6 @@ class ESPHomeClient(BaseBleakClient):
self._device_info.name,
)
return True
self._raise_if_not_connected()
response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
if response.success:
return True
@@ -516,6 +551,7 @@ class ESPHomeClient(BaseBleakClient):
)
return False
@verify_connected
@api_error_as_bleak_error
async def read_gatt_char(
self,
@@ -534,12 +570,12 @@ class ESPHomeClient(BaseBleakClient):
Returns:
(bytearray) The read data.
"""
self._raise_if_not_connected()
characteristic = self._resolve_characteristic(char_specifier)
return await self._client.bluetooth_gatt_read(
self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT
)
@verify_connected
@api_error_as_bleak_error
async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray:
"""Perform read operation on the specified GATT descriptor.
@@ -551,11 +587,11 @@ class ESPHomeClient(BaseBleakClient):
Returns:
(bytearray) The read data.
"""
self._raise_if_not_connected()
return await self._client.bluetooth_gatt_read_descriptor(
self._address_as_int, handle, GATT_READ_TIMEOUT
)
@verify_connected
@api_error_as_bleak_error
async def write_gatt_char(
self,
@@ -574,12 +610,12 @@ class ESPHomeClient(BaseBleakClient):
response (bool): If write-with-response operation should be done.
Defaults to `False`.
"""
self._raise_if_not_connected()
characteristic = self._resolve_characteristic(characteristic)
await self._client.bluetooth_gatt_write(
self._address_as_int, characteristic.handle, bytes(data), response
)
@verify_connected
@api_error_as_bleak_error
async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None:
"""Perform a write operation on the specified GATT descriptor.
@@ -588,11 +624,11 @@ class ESPHomeClient(BaseBleakClient):
handle (int): The handle of the descriptor to read from.
data (bytes or bytearray): The data to send.
"""
self._raise_if_not_connected()
await self._client.bluetooth_gatt_write_descriptor(
self._address_as_int, handle, bytes(data)
)
@verify_connected
@api_error_as_bleak_error
async def start_notify(
self,
@@ -619,7 +655,6 @@ class ESPHomeClient(BaseBleakClient):
callback (function): The function to be called on notification.
kwargs: Unused.
"""
self._raise_if_not_connected()
ble_handle = characteristic.handle
if ble_handle in self._notify_cancels:
raise BleakError(
@@ -674,6 +709,7 @@ class ESPHomeClient(BaseBleakClient):
wait_for_response=False,
)
@verify_connected
@api_error_as_bleak_error
async def stop_notify(
self,
@@ -687,7 +723,6 @@ class ESPHomeClient(BaseBleakClient):
specified by either integer handle, UUID or directly by the
BleakGATTCharacteristic object representing it.
"""
self._raise_if_not_connected()
characteristic = self._resolve_characteristic(char_specifier)
# Do not raise KeyError if notifications are not enabled on this characteristic
# to be consistent with the behavior of the BlueZ backend
@@ -695,11 +730,6 @@ class ESPHomeClient(BaseBleakClient):
notify_stop, _ = notify_cancel
await notify_stop()
def _raise_if_not_connected(self) -> None:
"""Raise a BleakError if not connected."""
if not self._is_connected:
raise BleakError(f"{self._description} is not connected")
def __del__(self) -> None:
"""Destructor to make sure the connection state is unsubscribed."""
if self._cancel_connection_state:

View File

@@ -164,15 +164,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
)
self._attr_min_temp = static_info.visual_min_temperature
self._attr_max_temp = static_info.visual_max_temperature
self._attr_min_humidity = round(static_info.visual_min_humidity)
self._attr_max_humidity = round(static_info.visual_max_humidity)
features = ClimateEntityFeature(0)
if self._static_info.supports_two_point_target_temperature:
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
else:
features |= ClimateEntityFeature.TARGET_TEMPERATURE
if self._static_info.supports_target_humidity:
features |= ClimateEntityFeature.TARGET_HUMIDITY
if self.preset_modes:
features |= ClimateEntityFeature.PRESET_MODE
if self.fan_modes:
@@ -238,14 +234,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
"""Return the current temperature."""
return self._state.current_temperature
@property
@esphome_state_property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
if not self._static_info.supports_current_humidity:
return None
return round(self._state.current_humidity)
@property
@esphome_state_property
def target_temperature(self) -> float | None:
@@ -264,12 +252,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
"""Return the highbound target temperature we try to reach."""
return self._state.target_temperature_high
@property
@esphome_state_property
def target_humidity(self) -> int:
"""Return the humidity we try to reach."""
return round(self._state.target_humidity)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature (and operation mode if set)."""
data: dict[str, Any] = {"key": self._key}
@@ -285,10 +267,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
await self._client.climate_command(**data)
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self._client.climate_command(key=self._key, target_humidity=humidity)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target operation mode."""
await self._client.climate_command(

View File

@@ -15,7 +15,8 @@
"iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [
"aioesphomeapi==19.2.1",
"async-interrupt==1.1.1",
"aioesphomeapi==19.1.4",
"bluetooth-data-tools==1.15.0",
"esphome-dashboard-api==1.2.3"
],

View File

@@ -186,22 +186,16 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
data_to_send = {"text": event.data["tts_input"]}
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END:
assert event.data is not None
tts_output = event.data["tts_output"]
if tts_output:
path = tts_output["url"]
url = async_process_play_media_url(self.hass, path)
data_to_send = {"url": url}
path = event.data["tts_output"]["url"]
url = async_process_play_media_url(self.hass, path)
data_to_send = {"url": url}
if self.device_info.voice_assistant_version >= 2:
media_id = tts_output["media_id"]
self._tts_task = self.hass.async_create_background_task(
self._send_tts(media_id), "esphome_voice_assistant_tts"
)
else:
self._tts_done.set()
if self.device_info.voice_assistant_version >= 2:
media_id = event.data["tts_output"]["media_id"]
self._tts_task = self.hass.async_create_background_task(
self._send_tts(media_id), "esphome_voice_assistant_tts"
)
else:
# Empty TTS response
data_to_send = {}
self._tts_done.set()
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END:
assert event.data is not None

View File

@@ -4,9 +4,6 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your Evil Genius Labs device."
}
}
},

View File

@@ -18,8 +18,7 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_ON,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@@ -78,19 +77,8 @@ ATTR_PRESET_MODES = "preset_modes"
# mypy: disallow-any-generics
class NotValidPresetModeError(ServiceValidationError):
"""Raised when the preset_mode is not in the preset_modes list."""
def __init__(
self, *args: object, translation_placeholders: dict[str, str] | None = None
) -> None:
"""Initialize the exception."""
super().__init__(
*args,
translation_domain=DOMAIN,
translation_key="not_valid_preset_mode",
translation_placeholders=translation_placeholders,
)
class NotValidPresetModeError(ValueError):
"""Exception class when the preset_mode in not in the preset_modes list."""
@bind_hass
@@ -119,7 +107,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
),
vol.Optional(ATTR_PRESET_MODE): cv.string,
},
"async_handle_turn_on_service",
"async_turn_on",
)
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
@@ -168,7 +156,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service(
SERVICE_SET_PRESET_MODE,
{vol.Required(ATTR_PRESET_MODE): cv.string},
"async_handle_set_preset_mode_service",
"async_set_preset_mode",
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
)
@@ -249,30 +237,17 @@ class FanEntity(ToggleEntity):
"""Set new preset mode."""
raise NotImplementedError()
@final
async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
"""Validate and set new preset mode."""
self._valid_preset_mode_or_raise(preset_mode)
await self.async_set_preset_mode(preset_mode)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode)
@final
@callback
def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
"""Raise NotValidPresetModeError on invalid preset_mode."""
preset_modes = self.preset_modes
if not preset_modes or preset_mode not in preset_modes:
preset_modes_str: str = ", ".join(preset_modes or [])
raise NotValidPresetModeError(
f"The preset_mode {preset_mode} is not a valid preset_mode:"
f" {preset_modes}",
translation_placeholders={
"preset_mode": preset_mode,
"preset_modes": preset_modes_str,
},
f" {preset_modes}"
)
def set_direction(self, direction: str) -> None:
@@ -292,18 +267,6 @@ class FanEntity(ToggleEntity):
"""Turn on the fan."""
raise NotImplementedError()
@final
async def async_handle_turn_on_service(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Validate and turn on the fan."""
if preset_mode is not None:
self._valid_preset_mode_or_raise(preset_mode)
await self.async_turn_on(percentage, preset_mode, **kwargs)
async def async_turn_on(
self,
percentage: int | None = None,

View File

@@ -144,10 +144,5 @@
"reverse": "Reverse"
}
}
},
"exceptions": {
"not_valid_preset_mode": {
"message": "Preset mode {preset_mode} is not valid, valid preset modes are: {preset_modes}."
}
}
}

View File

@@ -24,7 +24,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Fast.com sensor."""
async_add_entities([SpeedtestSensor(entry.entry_id, hass.data[DOMAIN])])
async_add_entities([SpeedtestSensor(hass.data[DOMAIN])])
# pylint: disable-next=hass-invalid-inheritance # needs fixing
@@ -38,10 +38,9 @@ class SpeedtestSensor(RestoreEntity, SensorEntity):
_attr_icon = "mdi:speedometer"
_attr_should_poll = False
def __init__(self, entry_id: str, speedtest_data: dict[str, Any]) -> None:
def __init__(self, speedtest_data: dict[str, Any]) -> None:
"""Initialize the sensor."""
self._speedtest_data = speedtest_data
self._attr_unique_id = entry_id
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""

View File

@@ -6,9 +6,6 @@
"name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your FiveM server."
}
}
},

View File

@@ -131,9 +131,11 @@ class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
command = PRESET_TO_COMMAND[preset_mode]
async with self.coordinator.async_connect_and_update() as device:
await device.send_command(command)
if command := PRESET_TO_COMMAND.get(preset_mode):
async with self.coordinator.async_connect_and_update() as device:
await device.send_command(command)
else:
raise UnsupportedPreset(f"The preset {preset_mode} is unsupported")
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""

View File

@@ -6,9 +6,6 @@
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your Flo device."
}
}
},

View File

@@ -9,9 +9,6 @@
"password": "[%key:common::config_flow::data::password%]",
"rtsp_port": "RTSP port",
"stream": "Stream"
},
"data_description": {
"host": "The hostname or IP address of your Foscam camera."
}
}
},

View File

@@ -5,9 +5,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Freebox router."
}
},
"link": {

View File

@@ -26,9 +26,6 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your FRITZ!Box router."
}
}
},

View File

@@ -38,9 +38,11 @@ async def async_setup_entry(
FritzboxLight(
coordinator,
ain,
device.get_colors(),
device.get_color_temps(),
)
for ain in coordinator.new_devices
if (coordinator.data.devices[ain]).has_lightbulb
if (device := coordinator.data.devices[ain]).has_lightbulb
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -55,10 +57,27 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
self,
coordinator: FritzboxDataUpdateCoordinator,
ain: str,
supported_colors: dict,
supported_color_temps: list[int],
) -> None:
"""Initialize the FritzboxLight entity."""
super().__init__(coordinator, ain, None)
if supported_color_temps:
# only available for color bulbs
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
self._supported_hs: dict[int, list[int]] = {}
for values in supported_colors.values():
hue = int(values[0][0])
self._supported_hs[hue] = [
int(values[0][1]),
int(values[1][1]),
int(values[2][1]),
]
@property
def is_on(self) -> bool:
@@ -154,28 +173,3 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""Turn the light off."""
await self.hass.async_add_executor_job(self.data.set_state_off)
await self.coordinator.async_refresh()
async def async_added_to_hass(self) -> None:
"""Get light attributes from device after entity is added to hass."""
await super().async_added_to_hass()
supported_colors = await self.hass.async_add_executor_job(
self.coordinator.data.devices[self.ain].get_colors
)
supported_color_temps = await self.hass.async_add_executor_job(
self.coordinator.data.devices[self.ain].get_color_temps
)
if supported_color_temps:
# only available for color bulbs
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
for values in supported_colors.values():
hue = int(values[0][0])
self._supported_hs[hue] = [
int(values[0][1]),
int(values[1][1]),
int(values[2][1]),
]

View File

@@ -8,9 +8,6 @@
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your FRITZ!Box router."
}
},
"confirm": {

View File

@@ -8,9 +8,6 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your FRITZ!Box router."
}
},
"phonebook": {

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20231208.2"]
"requirements": ["home-assistant-frontend==20231030.2"]
}

View File

@@ -5,13 +5,10 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Frontier Silicon device."
}
},
"device_config": {
"title": "Device configuration",
"title": "Device Configuration",
"description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'",
"data": {
"pin": "[%key:common::config_flow::data::pin%]"

View File

@@ -19,14 +19,13 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize."""
self.use_ssl = entry.data.get(CONF_SSL, False)
self.fully = FullyKiosk(
async_get_clientsession(hass),
entry.data[CONF_HOST],
DEFAULT_PORT,
entry.data[CONF_PASSWORD],
use_ssl=self.use_ssl,
verify_ssl=entry.data.get(CONF_VERIFY_SSL, False),
use_ssl=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
super().__init__(
hass,
@@ -34,6 +33,7 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
name=entry.data[CONF_HOST],
update_interval=UPDATE_INTERVAL,
)
self.use_ssl = entry.data[CONF_SSL]
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""

View File

@@ -13,9 +13,6 @@
"password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"host": "The hostname or IP address of the device running your Fully Kiosk Browser application."
}
}
},

View File

@@ -68,12 +68,9 @@ class GeniusSwitch(GeniusZone, SwitchEntity):
def is_on(self) -> bool:
"""Return the current state of the on/off zone.
The zone is considered 'on' if the mode is either 'override' or 'timer'.
The zone is considered 'on' if & only if it is override/on (e.g. timer/on is 'off').
"""
return (
self._zone.data["mode"] in ["override", "timer"]
and self._zone.data["setpoint"]
)
return self._zone.data["mode"] == "override" and self._zone.data["setpoint"]
async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the zone to Timer mode.

View File

@@ -10,9 +10,6 @@
"version": "Glances API Version (2 or 3)",
"ssl": "[%key:common::config_flow::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"host": "The hostname or IP address of the system running your Glances system monitor."
}
},
"reauth_confirm": {

View File

@@ -6,9 +6,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"host": "The hostname or IP address of your Goal Zero Yeti."
}
},
"confirm_discovery": {

View File

@@ -686,12 +686,8 @@ class GoogleEntity:
return device
# Add Matter info
if (
"matter" in self.hass.config.components
and any(x for x in device_entry.identifiers if x[0] == "matter")
and (
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
)
if "matter" in self.hass.config.components and (
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
):
device["matterUniqueId"] = matter_info["unique_id"]
device["matterOriginalVendorId"] = matter_info["vendor_id"]

View File

@@ -1,7 +1,7 @@
"""Google Tasks todo platform."""
from __future__ import annotations
from datetime import date, datetime, timedelta
from datetime import timedelta
from typing import Any, cast
from homeassistant.components.todo import (
@@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .api import AsyncConfigEntryAuth
from .const import DOMAIN
@@ -36,31 +35,9 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]:
result["title"] = item.summary
if item.status is not None:
result["status"] = TODO_STATUS_MAP_INV[item.status]
if (due := item.due) is not None:
# due API field is a timestamp string, but with only date resolution
result["due"] = dt_util.start_of_local_day(due).isoformat()
if (description := item.description) is not None:
result["notes"] = description
return result
def _convert_api_item(item: dict[str, str]) -> TodoItem:
"""Convert tasks API items into a TodoItem."""
due: date | None = None
if (due_str := item.get("due")) is not None:
due = datetime.fromisoformat(due_str).date()
return TodoItem(
summary=item["title"],
uid=item["id"],
status=TODO_STATUS_MAP.get(
item.get("status", ""),
TodoItemStatus.NEEDS_ACTION,
),
due=due,
description=item.get("notes"),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@@ -91,8 +68,6 @@ class GoogleTaskTodoListEntity(
TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
def __init__(
@@ -113,7 +88,17 @@ class GoogleTaskTodoListEntity(
"""Get the current set of To-do items."""
if self.coordinator.data is None:
return None
return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)]
return [
TodoItem(
summary=item["title"],
uid=item["id"],
status=TODO_STATUS_MAP.get(
item.get("status"), # type: ignore[arg-type]
TodoItemStatus.NEEDS_ACTION,
),
)
for item in _order_tasks(self.coordinator.data)
]
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""

View File

@@ -11,6 +11,7 @@ from homeassistant.helpers.event import async_track_time_interval
from .bridge import DiscoveryService
from .const import (
COORDINATORS,
DATA_DISCOVERY_INTERVAL,
DATA_DISCOVERY_SERVICE,
DISCOVERY_SCAN_INTERVAL,
DISPATCHERS,
@@ -28,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
gree_discovery = DiscoveryService(hass)
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
hass.data[DOMAIN].setdefault(DISPATCHERS, [])
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def _async_scan_update(_=None):
@@ -37,10 +39,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Scanning network for Gree devices")
await _async_scan_update()
entry.async_on_unload(
async_track_time_interval(
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
)
hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval(
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
)
return True
@@ -48,6 +48,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if hass.data[DOMAIN].get(DISPATCHERS) is not None:
for cleanup in hass.data[DOMAIN][DISPATCHERS]:
cleanup()
if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None:
hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)()
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
hass.data.pop(DATA_DISCOVERY_SERVICE)

View File

@@ -47,6 +47,7 @@ from .bridge import DeviceDataUpdateCoordinator
from .const import (
COORDINATORS,
DISPATCH_DEVICE_DISCOVERED,
DISPATCHERS,
DOMAIN,
FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW,
@@ -87,7 +88,7 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@@ -100,7 +101,7 @@ async def async_setup_entry(
for coordinator in hass.data[DOMAIN][COORDINATORS]:
init_device(coordinator)
entry.async_on_unload(
hass.data[DOMAIN][DISPATCHERS].append(
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
)

View File

@@ -3,6 +3,7 @@
COORDINATORS = "coordinators"
DATA_DISCOVERY_SERVICE = "gree_discovery"
DATA_DISCOVERY_INTERVAL = "gree_discovery_interval"
DISCOVERY_SCAN_INTERVAL = 300
DISCOVERY_TIMEOUT = 8

View File

@@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN
from .entity import GreeEntity
@@ -102,7 +102,7 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@@ -119,7 +119,7 @@ async def async_setup_entry(
for coordinator in hass.data[DOMAIN][COORDINATORS]:
init_device(coordinator)
entry.async_on_unload(
hass.data[DOMAIN][DISPATCHERS].append(
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
)

Some files were not shown because too many files have changed in this diff Show More