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/browse_media.py
homeassistant/components/kodi/media_player.py homeassistant/components/kodi/media_player.py
homeassistant/components/kodi/notify.py homeassistant/components/kodi/notify.py
homeassistant/components/komfovent/__init__.py
homeassistant/components/komfovent/climate.py
homeassistant/components/konnected/__init__.py homeassistant/components/konnected/__init__.py
homeassistant/components/konnected/panel.py homeassistant/components/konnected/panel.py
homeassistant/components/konnected/switch.py homeassistant/components/konnected/switch.py

View File

@@ -259,8 +259,6 @@ build.json @home-assistant/supervisor
/tests/components/denonavr/ @ol-iver @starkillerOG /tests/components/denonavr/ @ol-iver @starkillerOG
/homeassistant/components/derivative/ @afaucogney /homeassistant/components/derivative/ @afaucogney
/tests/components/derivative/ @afaucogney /tests/components/derivative/ @afaucogney
/homeassistant/components/devialet/ @fwestenberg
/tests/components/devialet/ @fwestenberg
/homeassistant/components/device_automation/ @home-assistant/core /homeassistant/components/device_automation/ @home-assistant/core
/tests/components/device_automation/ @home-assistant/core /tests/components/device_automation/ @home-assistant/core
/homeassistant/components/device_tracker/ @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 /tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund /homeassistant/components/kodi/ @OnFreund
/tests/components/kodi/ @OnFreund /tests/components/kodi/ @OnFreund
/homeassistant/components/komfovent/ @ProstoSanja
/tests/components/komfovent/ @ProstoSanja
/homeassistant/components/konnected/ @heythisisnate /homeassistant/components/konnected/ @heythisisnate
/tests/components/konnected/ @heythisisnate /tests/components/konnected/ @heythisisnate
/homeassistant/components/kostal_plenticore/ @stegm /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 ARG BUILD_FROM
FROM ${BUILD_FROM} FROM ${BUILD_FROM}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax", "documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["adax", "adax_local"], "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": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]" "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" DOMAIN: Final = "airq"
MANUFACTURER: Final = "CorantGmbH" MANUFACTURER: Final = "CorantGmbH"
TARGET_ROUTE: Final = "average"
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³" CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³" ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
UPDATE_INTERVAL: float = 10.0 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.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 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__) _LOGGER = logging.getLogger(__name__)
@@ -56,4 +56,6 @@ class AirQCoordinator(DataUpdateCoordinator):
hw_version=info["hw_version"], 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", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairq"], "loggers": ["aioairq"],
"requirements": ["aioairq==0.3.1"] "requirements": ["aioairq==0.2.4"]
} }

View File

@@ -16,7 +16,7 @@
"device_path": "Device Path" "device_path": "Device Path"
}, },
"data_description": { "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)" "port": "The port on which AlarmDecoder is accessible (for example, 10000)"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,10 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"description": "Connect to the device", "title": "Connect to the device",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]" "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})", "flow_title": "{name} ({host})",
"step": { "step": {
"user": { "user": {
"description": "Set up an Axis device", "title": "Set up Axis device",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]" "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: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan.""" """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 self._device.fan_mode = OffOnAuto.AUTO
async def async_set_direction(self, direction: str) -> None: async def async_set_direction(self, direction: str) -> None:

View File

@@ -2,12 +2,9 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"description": "Connect to the Balboa Wi-Fi device", "title": "Connect to the Balboa Wi-Fi device",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "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 .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
from .coordinator import BlinkUpdateCoordinator from .coordinator import BlinkUpdateCoordinator
from .services import setup_services from .services import async_setup_services
_LOGGER = logging.getLogger(__name__) _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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blink.""" """Set up Blink."""
setup_services(hass) await async_setup_services(hass)
return True return True

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,6 @@
"passkey": "Passkey string", "passkey": "Passkey string",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "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 hass: HomeAssistant, client: caldav.DAVClient, component: str
) -> list[caldav.Calendar]: ) -> list[caldav.Calendar]:
"""Get all calendars that support the specified component.""" """Get all calendars that support the specified component."""
calendars = await hass.async_add_executor_job(client.principal().calendars)
def _get_calendars() -> list[caldav.Calendar]:
return client.principal().calendars()
calendars = await hass.async_add_executor_job(_get_calendars)
components_results = await asyncio.gather( components_results = await asyncio.gather(
*[ *[
hass.async_add_executor_job(calendar.get_supported_components) hass.async_add_executor_job(calendar.get_supported_components)

View File

@@ -2,10 +2,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import date, datetime, timedelta from datetime import timedelta
from functools import partial from functools import partial
import logging import logging
from typing import Any, cast from typing import cast
import caldav import caldav
from caldav.lib.error import DAVError, NotFoundError from caldav.lib.error import DAVError, NotFoundError
@@ -21,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 .api import async_get_calendars, get_attr_value
from .const import DOMAIN 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 or (summary := get_attr_value(todo, "summary")) is None
): ):
return 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( return TodoItem(
uid=uid, uid=uid,
summary=summary, summary=summary,
@@ -85,28 +78,9 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
get_attr_value(todo, "status") or "", get_attr_value(todo, "status") or "",
TodoItemStatus.NEEDS_ACTION, 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): class WebDavTodoListEntity(TodoListEntity):
"""CalDAV To-do list entity.""" """CalDAV To-do list entity."""
@@ -115,9 +89,6 @@ class WebDavTodoListEntity(TodoListEntity):
TodoListEntityFeature.CREATE_TODO_ITEM TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_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: 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.""" """Add an item to the To-do list."""
try: try:
await self.hass.async_add_executor_job( 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: except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err raise HomeAssistantError(f"CalDAV save error: {err}") from err
@@ -162,7 +139,10 @@ class WebDavTodoListEntity(TodoListEntity):
except (requests.ConnectionError, DAVError) as err: except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
vtodo = todo.icalendar_component # type: ignore[attr-defined] 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: try:
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
partial( partial(

View File

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

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/comelit", "documentation": "https://www.home-assistant.io/integrations/comelit",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiocomelit"], "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%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]", "port": "[%key:common::config_flow::data::port%]",
"pin": "[%key:common::config_flow::data::pin%]" "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: if device_area is None:
return None return None
return {"area": device_area.id} return {"area": device_area.name}
def _get_error_text( def _get_error_text(
self, response_type: ResponseType, lang_intents: LanguageIntents | None self, response_type: ResponseType, lang_intents: LanguageIntents | None

View File

@@ -7,5 +7,5 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal", "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": { "config": {
"step": { "step": {
"user": { "user": {
"description": "Set up your CoolMasterNet connection details.", "title": "Set up your CoolMasterNet connection details.",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"off": "Can be turned off", "off": "Can be turned off",
@@ -12,9 +12,6 @@
"dry": "Support dry mode", "dry": "Support dry mode",
"fan_only": "Support fan only mode", "fan_only": "Support fan only mode",
"swing_support": "Control swing 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, LightColorMode.XY: ColorMode.XY,
} }
XMAS_LIGHT_EFFECTS = [ TS0601_EFFECTS = [
"carnival", "carnival",
"collide", "collide",
"fading", "fading",
@@ -200,8 +200,8 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
if device.effect is not None: if device.effect is not None:
self._attr_supported_features |= LightEntityFeature.EFFECT self._attr_supported_features |= LightEntityFeature.EFFECT
self._attr_effect_list = [EFFECT_COLORLOOP] self._attr_effect_list = [EFFECT_COLORLOOP]
if device.model_id in ("HG06467", "TS0601"): if device.model_id == "TS0601":
self._attr_effect_list = XMAS_LIGHT_EFFECTS self._attr_effect_list += TS0601_EFFECTS
@property @property
def color_mode(self) -> str | None: def color_mode(self) -> str | None:

View File

@@ -11,14 +11,11 @@
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]" "port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your deCONZ host."
} }
}, },
"link": { "link": {
"title": "Link with deCONZ", "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": { "hassio_confirm": {
"title": "deCONZ Zigbee gateway via Home Assistant add-on", "title": "deCONZ Zigbee gateway via Home Assistant add-on",

View File

@@ -9,9 +9,6 @@
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]", "port": "[%key:common::config_flow::data::port%]",
"web_port": "Web port (for visiting service)" "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: def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
self._preset_mode = preset_mode if self.preset_modes and preset_mode in self.preset_modes:
self._percentage = None self._preset_mode = preset_mode
self.schedule_update_ha_state() self._percentage = None
self.schedule_update_ha_state()
else:
raise ValueError(f"Invalid preset mode: {preset_mode}")
def turn_on( def turn_on(
self, self,
@@ -227,6 +230,10 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """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._preset_mode = preset_mode
self._percentage = None self._percentage = None
self.async_write_ha_state() 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)) 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: def get_gravatar_for_email(email: str) -> str:
"""Return an 80px Gravatar for the given email address. """Return an 80px Gravatar for the given email address.

View File

@@ -8,9 +8,6 @@
"user": { "user": {
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "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 description in sensors
for value_key in {description.key, *description.alternative_keys} for value_key in {description.key, *description.alternative_keys}
if description.value_fn(coordinator.data, value_key, description.scale) if description.value_fn(coordinator.data, value_key, description.scale)
is not None
) )
async_add_entities(entities) async_add_entities(entities)

View File

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

View File

@@ -17,11 +17,8 @@
"data": { "data": {
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"name": "Device name", "name": "Device Name",
"username": "[%key:common::config_flow::data::username%]" "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": { "user": {
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "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] PLATFORMS = [Platform.SENSOR]
CONF_DSMR_VERSION = "dsmr_version" CONF_DSMR_VERSION = "dsmr_version"
CONF_PROTOCOL = "protocol" CONF_PROTOCOL = "protocol"
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_PRECISION = "precision" CONF_PRECISION = "precision"
CONF_TIME_BETWEEN_UPDATE = "time_between_update" CONF_TIME_BETWEEN_UPDATE = "time_between_update"
@@ -28,7 +29,6 @@ DATA_TASK = "task"
DEVICE_NAME_ELECTRICITY = "Electricity Meter" DEVICE_NAME_ELECTRICITY = "Electricity Meter"
DEVICE_NAME_GAS = "Gas Meter" DEVICE_NAME_GAS = "Gas Meter"
DEVICE_NAME_WATER = "Water Meter"
DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}

View File

@@ -34,7 +34,6 @@ from homeassistant.const import (
UnitOfVolume, UnitOfVolume,
) )
from homeassistant.core import CoreState, Event, HomeAssistant, callback 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.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
@@ -48,6 +47,7 @@ from .const import (
CONF_DSMR_VERSION, CONF_DSMR_VERSION,
CONF_PRECISION, CONF_PRECISION,
CONF_PROTOCOL, CONF_PROTOCOL,
CONF_RECONNECT_INTERVAL,
CONF_SERIAL_ID, CONF_SERIAL_ID,
CONF_SERIAL_ID_GAS, CONF_SERIAL_ID_GAS,
CONF_TIME_BETWEEN_UPDATE, CONF_TIME_BETWEEN_UPDATE,
@@ -57,7 +57,6 @@ from .const import (
DEFAULT_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE,
DEVICE_NAME_ELECTRICITY, DEVICE_NAME_ELECTRICITY,
DEVICE_NAME_GAS, DEVICE_NAME_GAS,
DEVICE_NAME_WATER,
DOMAIN, DOMAIN,
DSMR_PROTOCOL, DSMR_PROTOCOL,
LOGGER, LOGGER,
@@ -74,7 +73,6 @@ class DSMRSensorEntityDescription(SensorEntityDescription):
dsmr_versions: set[str] | None = None dsmr_versions: set[str] | None = None
is_gas: bool = False is_gas: bool = False
is_water: bool = False
obis_reference: str obis_reference: str
@@ -376,138 +374,28 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
) )
def create_mbus_entity( def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
mbus: int, mtype: int, telegram: dict[str, DSMRObject] """Return correct entity for 5B Gas meter."""
) -> DSMRSensorEntityDescription | None: ref = None
"""Create a new MBUS Entity.""" if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
if ( ref = obis_references.BELGIUM_MBUS1_METER_READING2
mtype == 3 elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
and ( ref = obis_references.BELGIUM_MBUS2_METER_READING2
obis_reference := getattr( elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2" ref = obis_references.BELGIUM_MBUS3_METER_READING2
) elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
) ref = obis_references.BELGIUM_MBUS4_METER_READING2
in telegram elif ref is None:
): ref = obis_references.BELGIUM_MBUS1_METER_READING2
return DSMRSensorEntityDescription( return DSMRSensorEntityDescription(
key=f"mbus{mbus}_gas_reading", key="belgium_5min_gas_meter_reading",
translation_key="gas_meter_reading", translation_key="gas_meter_reading",
obis_reference=obis_reference, obis_reference=ref,
is_gas=True, dsmr_versions={"5B"},
device_class=SensorDeviceClass.GAS, is_gas=True,
state_class=SensorStateClass.TOTAL_INCREASING, 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
async def async_setup_entry( async def async_setup_entry(
@@ -527,10 +415,25 @@ async def async_setup_entry(
add_entities_handler() add_entities_handler()
add_entities_handler = None 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": if dsmr_version == "5B":
mbus_entities = create_mbus_entities(hass, telegram, entry) all_sensors += (add_gas_sensor_5B(telegram),)
for mbus_entity in mbus_entities:
entities.append(mbus_entity)
entities.extend( entities.extend(
[ [
@@ -540,7 +443,7 @@ async def async_setup_entry(
telegram, telegram,
*device_class_and_uom(telegram, description), # type: ignore[arg-type] *device_class_and_uom(telegram, description), # type: ignore[arg-type]
) )
for description in SENSORS for description in all_sensors
if ( if (
description.dsmr_versions is None description.dsmr_versions is None
or dsmr_version in description.dsmr_versions or dsmr_version in description.dsmr_versions
@@ -646,7 +549,9 @@ async def async_setup_entry(
update_entities_telegram(None) update_entities_telegram(None)
# throttle reconnect attempts # 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): except (serial.serialutil.SerialException, OSError):
# Log any error while establishing connection and drop to retry # Log any error while establishing connection and drop to retry
@@ -660,7 +565,9 @@ async def async_setup_entry(
update_entities_telegram(None) update_entities_telegram(None)
# throttle reconnect attempts # throttle reconnect attempts
await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL) await asyncio.sleep(
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
)
except CancelledError: except CancelledError:
# Reflect disconnect state in devices state by setting an # Reflect disconnect state in devices state by setting an
# None telegram resulting in `unavailable` states # None telegram resulting in `unavailable` states
@@ -711,8 +618,6 @@ class DSMREntity(SensorEntity):
telegram: dict[str, DSMRObject], telegram: dict[str, DSMRObject],
device_class: SensorDeviceClass, device_class: SensorDeviceClass,
native_unit_of_measurement: str | None, native_unit_of_measurement: str | None,
serial_id: str = "",
mbus_id: int = 0,
) -> None: ) -> None:
"""Initialize entity.""" """Initialize entity."""
self.entity_description = entity_description self.entity_description = entity_description
@@ -724,15 +629,8 @@ class DSMREntity(SensorEntity):
device_serial = entry.data[CONF_SERIAL_ID] device_serial = entry.data[CONF_SERIAL_ID]
device_name = DEVICE_NAME_ELECTRICITY device_name = DEVICE_NAME_ELECTRICITY
if entity_description.is_gas: if entity_description.is_gas:
if serial_id: device_serial = entry.data[CONF_SERIAL_ID_GAS]
device_serial = serial_id
else:
device_serial = entry.data[CONF_SERIAL_ID_GAS]
device_name = DEVICE_NAME_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: if device_serial is None:
device_serial = entry.entry_id device_serial = entry.entry_id
@@ -740,13 +638,7 @@ class DSMREntity(SensorEntity):
identifiers={(DOMAIN, device_serial)}, identifiers={(DOMAIN, device_serial)},
name=device_name, name=device_name,
) )
if mbus_id != 0: self._attr_unique_id = f"{device_serial}_{entity_description.key}"
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}"
@callback @callback
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None: 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) 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 return value
@staticmethod @staticmethod

View File

@@ -147,9 +147,6 @@
}, },
"voltage_swell_l3_count": { "voltage_swell_l3_count": {
"name": "Voltage swells phase L3" "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.", "description": "Ensure that your player is turned on.",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "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%]", "host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "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%]", "host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "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": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]" "port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Elgato device."
} }
}, },
"zeroconf_confirm": { "zeroconf_confirm": {

View File

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

View File

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

View File

@@ -317,11 +317,6 @@ class EnergyCostSensor(SensorEntity):
try: try:
energy_price = float(energy_price_state.state) energy_price = float(energy_price_state.state)
except ValueError: 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 return
energy_price_unit: str | None = energy_price_state.attributes.get( energy_price_unit: str | None = energy_price_state.attributes.get(

View File

@@ -8,9 +8,6 @@
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "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": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"name": "[%key:common::config_flow::data::name%]" "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, APIClient,
APIVersion, APIVersion,
BLEConnectionError, BLEConnectionError,
BluetoothConnectionDroppedError,
BluetoothProxyFeature, BluetoothProxyFeature,
DeviceInfo, DeviceInfo,
) )
@@ -31,6 +30,7 @@ from aioesphomeapi.core import (
BluetoothGATTAPIError, BluetoothGATTAPIError,
TimeoutAPIError, TimeoutAPIError,
) )
from async_interrupt import interrupt
from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.client import BaseBleakClient, NotifyCallback
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
@@ -68,25 +68,39 @@ def mac_to_int(address: str) -> int:
return int(address.replace(":", ""), 16) 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: def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
"""Define a wrapper throw esphome api errors as BleakErrors.""" """Define a wrapper throw esphome api errors as BleakErrors."""
async def _async_wrap_bluetooth_operation( async def _async_wrap_bluetooth_operation(
self: ESPHomeClient, *args: Any, **kwargs: Any self: ESPHomeClient, *args: Any, **kwargs: Any
) -> Any: ) -> Any:
# pylint: disable=protected-access
try: try:
return await func(self, *args, **kwargs) return await func(self, *args, **kwargs)
except TimeoutAPIError as err: except TimeoutAPIError as err:
raise asyncio.TimeoutError(str(err)) from 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: except BluetoothGATTAPIError as ex:
# If the device disconnects in the middle of an operation # If the device disconnects in the middle of an operation
# be sure to mark it as disconnected so any library using # 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. # before the callback is delivered.
if ex.error.error == -1: if ex.error.error == -1:
# pylint: disable=protected-access
_LOGGER.debug( _LOGGER.debug(
"%s: BLE device disconnected during %s operation", "%s: BLE device disconnected during %s operation",
self._description, self._description,
@@ -154,6 +169,7 @@ class ESPHomeClient(BaseBleakClient):
self._notify_cancels: dict[ self._notify_cancels: dict[
int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]] 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._device_info = client_data.device_info
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat( self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
client_data.api_version client_data.api_version
@@ -169,7 +185,24 @@ class ESPHomeClient(BaseBleakClient):
def __str__(self) -> str: def __str__(self) -> str:
"""Return the string representation of the client.""" """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: def _async_disconnected_cleanup(self) -> None:
"""Clean up on disconnect.""" """Clean up on disconnect."""
@@ -178,10 +211,12 @@ class ESPHomeClient(BaseBleakClient):
for _, notify_abort in self._notify_cancels.values(): for _, notify_abort in self._notify_cancels.values():
notify_abort() notify_abort()
self._notify_cancels.clear() 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) self._disconnect_callbacks.discard(self._async_esp_disconnected)
if self._cancel_connection_state: self._unsubscribe_connection_state()
self._cancel_connection_state()
self._cancel_connection_state = None
def _async_ble_device_disconnected(self) -> None: def _async_ble_device_disconnected(self) -> None:
"""Handle the BLE device disconnecting from the ESP.""" """Handle the BLE device disconnecting from the ESP."""
@@ -371,6 +406,7 @@ class ESPHomeClient(BaseBleakClient):
"""Get ATT MTU size for active connection.""" """Get ATT MTU size for active connection."""
return self._mtu or DEFAULT_MTU return self._mtu or DEFAULT_MTU
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def pair(self, *args: Any, **kwargs: Any) -> bool: async def pair(self, *args: Any, **kwargs: Any) -> bool:
"""Attempt to pair.""" """Attempt to pair."""
@@ -379,7 +415,6 @@ class ESPHomeClient(BaseBleakClient):
"Pairing is not available in this version ESPHome; " "Pairing is not available in this version ESPHome; "
f"Upgrade the ESPHome version on the {self._device_info.name} device." 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) response = await self._client.bluetooth_device_pair(self._address_as_int)
if response.paired: if response.paired:
return True return True
@@ -388,6 +423,7 @@ class ESPHomeClient(BaseBleakClient):
) )
return False return False
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def unpair(self) -> bool: async def unpair(self) -> bool:
"""Attempt to unpair.""" """Attempt to unpair."""
@@ -396,7 +432,6 @@ class ESPHomeClient(BaseBleakClient):
"Unpairing is not available in this version ESPHome; " "Unpairing is not available in this version ESPHome; "
f"Upgrade the ESPHome version on the {self._device_info.name} device." 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) response = await self._client.bluetooth_device_unpair(self._address_as_int)
if response.success: if response.success:
return True return True
@@ -419,6 +454,7 @@ class ESPHomeClient(BaseBleakClient):
dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs
) )
@verify_connected
async def _get_services( async def _get_services(
self, dangerous_use_bleak_cache: bool = False, **kwargs: Any self, dangerous_use_bleak_cache: bool = False, **kwargs: Any
) -> BleakGATTServiceCollection: ) -> BleakGATTServiceCollection:
@@ -426,7 +462,6 @@ class ESPHomeClient(BaseBleakClient):
Must only be called from get_services or connected Must only be called from get_services or connected
""" """
self._raise_if_not_connected()
address_as_int = self._address_as_int address_as_int = self._address_as_int
cache = self._cache cache = self._cache
# If the connection version >= 3, we must use the cache # If the connection version >= 3, we must use the cache
@@ -492,6 +527,7 @@ class ESPHomeClient(BaseBleakClient):
) )
return characteristic return characteristic
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def clear_cache(self) -> bool: async def clear_cache(self) -> bool:
"""Clear the GATT cache.""" """Clear the GATT cache."""
@@ -505,7 +541,6 @@ class ESPHomeClient(BaseBleakClient):
self._device_info.name, self._device_info.name,
) )
return True return True
self._raise_if_not_connected()
response = await self._client.bluetooth_device_clear_cache(self._address_as_int) response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
if response.success: if response.success:
return True return True
@@ -516,6 +551,7 @@ class ESPHomeClient(BaseBleakClient):
) )
return False return False
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def read_gatt_char( async def read_gatt_char(
self, self,
@@ -534,12 +570,12 @@ class ESPHomeClient(BaseBleakClient):
Returns: Returns:
(bytearray) The read data. (bytearray) The read data.
""" """
self._raise_if_not_connected()
characteristic = self._resolve_characteristic(char_specifier) characteristic = self._resolve_characteristic(char_specifier)
return await self._client.bluetooth_gatt_read( return await self._client.bluetooth_gatt_read(
self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT
) )
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray:
"""Perform read operation on the specified GATT descriptor. """Perform read operation on the specified GATT descriptor.
@@ -551,11 +587,11 @@ class ESPHomeClient(BaseBleakClient):
Returns: Returns:
(bytearray) The read data. (bytearray) The read data.
""" """
self._raise_if_not_connected()
return await self._client.bluetooth_gatt_read_descriptor( return await self._client.bluetooth_gatt_read_descriptor(
self._address_as_int, handle, GATT_READ_TIMEOUT self._address_as_int, handle, GATT_READ_TIMEOUT
) )
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def write_gatt_char( async def write_gatt_char(
self, self,
@@ -574,12 +610,12 @@ class ESPHomeClient(BaseBleakClient):
response (bool): If write-with-response operation should be done. response (bool): If write-with-response operation should be done.
Defaults to `False`. Defaults to `False`.
""" """
self._raise_if_not_connected()
characteristic = self._resolve_characteristic(characteristic) characteristic = self._resolve_characteristic(characteristic)
await self._client.bluetooth_gatt_write( await self._client.bluetooth_gatt_write(
self._address_as_int, characteristic.handle, bytes(data), response self._address_as_int, characteristic.handle, bytes(data), response
) )
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None:
"""Perform a write operation on the specified GATT descriptor. """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. handle (int): The handle of the descriptor to read from.
data (bytes or bytearray): The data to send. data (bytes or bytearray): The data to send.
""" """
self._raise_if_not_connected()
await self._client.bluetooth_gatt_write_descriptor( await self._client.bluetooth_gatt_write_descriptor(
self._address_as_int, handle, bytes(data) self._address_as_int, handle, bytes(data)
) )
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def start_notify( async def start_notify(
self, self,
@@ -619,7 +655,6 @@ class ESPHomeClient(BaseBleakClient):
callback (function): The function to be called on notification. callback (function): The function to be called on notification.
kwargs: Unused. kwargs: Unused.
""" """
self._raise_if_not_connected()
ble_handle = characteristic.handle ble_handle = characteristic.handle
if ble_handle in self._notify_cancels: if ble_handle in self._notify_cancels:
raise BleakError( raise BleakError(
@@ -674,6 +709,7 @@ class ESPHomeClient(BaseBleakClient):
wait_for_response=False, wait_for_response=False,
) )
@verify_connected
@api_error_as_bleak_error @api_error_as_bleak_error
async def stop_notify( async def stop_notify(
self, self,
@@ -687,7 +723,6 @@ class ESPHomeClient(BaseBleakClient):
specified by either integer handle, UUID or directly by the specified by either integer handle, UUID or directly by the
BleakGATTCharacteristic object representing it. BleakGATTCharacteristic object representing it.
""" """
self._raise_if_not_connected()
characteristic = self._resolve_characteristic(char_specifier) characteristic = self._resolve_characteristic(char_specifier)
# Do not raise KeyError if notifications are not enabled on this characteristic # Do not raise KeyError if notifications are not enabled on this characteristic
# to be consistent with the behavior of the BlueZ backend # to be consistent with the behavior of the BlueZ backend
@@ -695,11 +730,6 @@ class ESPHomeClient(BaseBleakClient):
notify_stop, _ = notify_cancel notify_stop, _ = notify_cancel
await notify_stop() 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: def __del__(self) -> None:
"""Destructor to make sure the connection state is unsubscribed.""" """Destructor to make sure the connection state is unsubscribed."""
if self._cancel_connection_state: 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_min_temp = static_info.visual_min_temperature
self._attr_max_temp = static_info.visual_max_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) features = ClimateEntityFeature(0)
if self._static_info.supports_two_point_target_temperature: if self._static_info.supports_two_point_target_temperature:
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
else: else:
features |= ClimateEntityFeature.TARGET_TEMPERATURE features |= ClimateEntityFeature.TARGET_TEMPERATURE
if self._static_info.supports_target_humidity:
features |= ClimateEntityFeature.TARGET_HUMIDITY
if self.preset_modes: if self.preset_modes:
features |= ClimateEntityFeature.PRESET_MODE features |= ClimateEntityFeature.PRESET_MODE
if self.fan_modes: if self.fan_modes:
@@ -238,14 +234,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
"""Return the current temperature.""" """Return the current temperature."""
return self._state.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 @property
@esphome_state_property @esphome_state_property
def target_temperature(self) -> float | None: 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 the highbound target temperature we try to reach."""
return self._state.target_temperature_high 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: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature (and operation mode if set).""" """Set new target temperature (and operation mode if set)."""
data: dict[str, Any] = {"key": self._key} 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] data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
await self._client.climate_command(**data) 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: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target operation mode.""" """Set new target operation mode."""
await self._client.climate_command( await self._client.climate_command(

View File

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

View File

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

View File

@@ -4,9 +4,6 @@
"user": { "user": {
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "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, SERVICE_TURN_ON,
STATE_ON, STATE_ON,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
@@ -78,19 +77,8 @@ ATTR_PRESET_MODES = "preset_modes"
# mypy: disallow-any-generics # mypy: disallow-any-generics
class NotValidPresetModeError(ServiceValidationError): class NotValidPresetModeError(ValueError):
"""Raised when the preset_mode is not in the preset_modes list.""" """Exception class when the preset_mode in 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,
)
@bind_hass @bind_hass
@@ -119,7 +107,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
), ),
vol.Optional(ATTR_PRESET_MODE): cv.string, 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_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") 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( component.async_register_entity_service(
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
{vol.Required(ATTR_PRESET_MODE): cv.string}, {vol.Required(ATTR_PRESET_MODE): cv.string},
"async_handle_set_preset_mode_service", "async_set_preset_mode",
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE], [FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
) )
@@ -249,30 +237,17 @@ class FanEntity(ToggleEntity):
"""Set new preset mode.""" """Set new preset mode."""
raise NotImplementedError() 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: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
await self.hass.async_add_executor_job(self.set_preset_mode, 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: def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
"""Raise NotValidPresetModeError on invalid preset_mode.""" """Raise NotValidPresetModeError on invalid preset_mode."""
preset_modes = self.preset_modes preset_modes = self.preset_modes
if not preset_modes or preset_mode not in preset_modes: if not preset_modes or preset_mode not in preset_modes:
preset_modes_str: str = ", ".join(preset_modes or [])
raise NotValidPresetModeError( raise NotValidPresetModeError(
f"The preset_mode {preset_mode} is not a valid preset_mode:" f"The preset_mode {preset_mode} is not a valid preset_mode:"
f" {preset_modes}", f" {preset_modes}"
translation_placeholders={
"preset_mode": preset_mode,
"preset_modes": preset_modes_str,
},
) )
def set_direction(self, direction: str) -> None: def set_direction(self, direction: str) -> None:
@@ -292,18 +267,6 @@ class FanEntity(ToggleEntity):
"""Turn on the fan.""" """Turn on the fan."""
raise NotImplementedError() 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( async def async_turn_on(
self, self,
percentage: int | None = None, percentage: int | None = None,

View File

@@ -144,10 +144,5 @@
"reverse": "Reverse" "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, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Fast.com sensor.""" """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 # pylint: disable-next=hass-invalid-inheritance # needs fixing
@@ -38,10 +38,9 @@ class SpeedtestSensor(RestoreEntity, SensorEntity):
_attr_icon = "mdi:speedometer" _attr_icon = "mdi:speedometer"
_attr_should_poll = False _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.""" """Initialize the sensor."""
self._speedtest_data = speedtest_data self._speedtest_data = speedtest_data
self._attr_unique_id = entry_id
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle entity which will be added.""" """Handle entity which will be added."""

View File

@@ -6,9 +6,6 @@
"name": "[%key:common::config_flow::data::name%]", "name": "[%key:common::config_flow::data::name%]",
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]" "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: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
command = PRESET_TO_COMMAND[preset_mode] if command := PRESET_TO_COMMAND.get(preset_mode):
async with self.coordinator.async_connect_and_update() as device: async with self.coordinator.async_connect_and_update() as device:
await device.send_command(command) await device.send_command(command)
else:
raise UnsupportedPreset(f"The preset {preset_mode} is unsupported")
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off.""" """Turn the entity off."""

View File

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

View File

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

View File

@@ -26,9 +26,6 @@
"port": "[%key:common::config_flow::data::port%]", "port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "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( FritzboxLight(
coordinator, coordinator,
ain, ain,
device.get_colors(),
device.get_color_temps(),
) )
for ain in coordinator.new_devices 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)) entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -55,10 +57,27 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
self, self,
coordinator: FritzboxDataUpdateCoordinator, coordinator: FritzboxDataUpdateCoordinator,
ain: str, ain: str,
supported_colors: dict,
supported_color_temps: list[int],
) -> None: ) -> None:
"""Initialize the FritzboxLight entity.""" """Initialize the FritzboxLight entity."""
super().__init__(coordinator, ain, None) 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]] = {} 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 @property
def is_on(self) -> bool: def is_on(self) -> bool:
@@ -154,28 +173,3 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""Turn the light off.""" """Turn the light off."""
await self.hass.async_add_executor_job(self.data.set_state_off) await self.hass.async_add_executor_job(self.data.set_state_off)
await self.coordinator.async_refresh() 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%]", "host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your FRITZ!Box router."
} }
}, },
"confirm": { "confirm": {

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,9 +13,6 @@
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]", "ssl": "[%key:common::config_flow::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_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: def is_on(self) -> bool:
"""Return the current state of the on/off zone. """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 ( return self._zone.data["mode"] == "override" and self._zone.data["setpoint"]
self._zone.data["mode"] in ["override", "timer"]
and self._zone.data["setpoint"]
)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the zone to Timer mode. """Send the zone to Timer mode.

View File

@@ -10,9 +10,6 @@
"version": "Glances API Version (2 or 3)", "version": "Glances API Version (2 or 3)",
"ssl": "[%key:common::config_flow::data::ssl%]", "ssl": "[%key:common::config_flow::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_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": { "reauth_confirm": {

View File

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

View File

@@ -686,12 +686,8 @@ class GoogleEntity:
return device return device
# Add Matter info # Add Matter info
if ( if "matter" in self.hass.config.components and (
"matter" in self.hass.config.components matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
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)
)
): ):
device["matterUniqueId"] = matter_info["unique_id"] device["matterUniqueId"] = matter_info["unique_id"]
device["matterOriginalVendorId"] = matter_info["vendor_id"] device["matterOriginalVendorId"] = matter_info["vendor_id"]

View File

@@ -1,7 +1,7 @@
"""Google Tasks todo platform.""" """Google Tasks todo platform."""
from __future__ import annotations from __future__ import annotations
from datetime import date, datetime, timedelta from datetime import timedelta
from typing import Any, cast from typing import Any, cast
from homeassistant.components.todo import ( from homeassistant.components.todo import (
@@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .api import AsyncConfigEntryAuth from .api import AsyncConfigEntryAuth
from .const import DOMAIN from .const import DOMAIN
@@ -36,31 +35,9 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]:
result["title"] = item.summary result["title"] = item.summary
if item.status is not None: if item.status is not None:
result["status"] = TODO_STATUS_MAP_INV[item.status] 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 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( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
@@ -91,8 +68,6 @@ class GoogleTaskTodoListEntity(
TodoListEntityFeature.CREATE_TODO_ITEM TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_TODO_ITEM | TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
) )
def __init__( def __init__(
@@ -113,7 +88,17 @@ class GoogleTaskTodoListEntity(
"""Get the current set of To-do items.""" """Get the current set of To-do items."""
if self.coordinator.data is None: if self.coordinator.data is None:
return 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: async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list.""" """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 .bridge import DiscoveryService
from .const import ( from .const import (
COORDINATORS, COORDINATORS,
DATA_DISCOVERY_INTERVAL,
DATA_DISCOVERY_SERVICE, DATA_DISCOVERY_SERVICE,
DISCOVERY_SCAN_INTERVAL, DISCOVERY_SCAN_INTERVAL,
DISPATCHERS, DISPATCHERS,
@@ -28,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
gree_discovery = DiscoveryService(hass) gree_discovery = DiscoveryService(hass)
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
hass.data[DOMAIN].setdefault(DISPATCHERS, [])
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def _async_scan_update(_=None): 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") _LOGGER.debug("Scanning network for Gree devices")
await _async_scan_update() await _async_scan_update()
entry.async_on_unload( hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval(
async_track_time_interval( hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
)
) )
return True 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """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: if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
hass.data.pop(DATA_DISCOVERY_SERVICE) hass.data.pop(DATA_DISCOVERY_SERVICE)

View File

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

View File

@@ -3,6 +3,7 @@
COORDINATORS = "coordinators" COORDINATORS = "coordinators"
DATA_DISCOVERY_SERVICE = "gree_discovery" DATA_DISCOVERY_SERVICE = "gree_discovery"
DATA_DISCOVERY_INTERVAL = "gree_discovery_interval"
DISCOVERY_SCAN_INTERVAL = 300 DISCOVERY_SCAN_INTERVAL = 300
DISCOVERY_TIMEOUT = 8 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.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 from .entity import GreeEntity
@@ -102,7 +102,7 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Gree HVAC device from a config entry.""" """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]: for coordinator in hass.data[DOMAIN][COORDINATORS]:
init_device(coordinator) init_device(coordinator)
entry.async_on_unload( hass.data[DOMAIN][DISPATCHERS].append(
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) 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