Merge branch 'dev' into block_pyserial_asyncio

This commit is contained in:
J. Nick Koston 2024-05-05 10:06:24 -05:00 committed by GitHub
commit 1428ce4084
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
231 changed files with 3524 additions and 2621 deletions

View File

@ -519,6 +519,7 @@ omit =
homeassistant/components/guardian/util.py homeassistant/components/guardian/util.py
homeassistant/components/guardian/valve.py homeassistant/components/guardian/valve.py
homeassistant/components/habitica/__init__.py homeassistant/components/habitica/__init__.py
homeassistant/components/habitica/coordinator.py
homeassistant/components/habitica/sensor.py homeassistant/components/habitica/sensor.py
homeassistant/components/harman_kardon_avr/media_player.py homeassistant/components/harman_kardon_avr/media_player.py
homeassistant/components/harmony/data.py homeassistant/components/harmony/data.py

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.2 rev: v0.4.3
hooks: hooks:
- id: ruff - id: ruff
args: args:

View File

@ -31,7 +31,6 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherData from . import AccuWeatherData
@ -65,8 +64,6 @@ class AccuWeatherEntity(
CoordinatorWeatherEntity[ CoordinatorWeatherEntity[
AccuWeatherObservationDataUpdateCoordinator, AccuWeatherObservationDataUpdateCoordinator,
AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
] ]
): ):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""

View File

@ -17,15 +17,18 @@ from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP,
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import DOMAIN
from .helpers import create_api, get_enable_ime from .helpers import create_api, get_enable_ime
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE] PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE]
AndroidTVRemoteConfigEntry = ConfigEntry[AndroidTVRemote]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
) -> bool:
"""Set up Android TV Remote from a config entry.""" """Set up Android TV Remote from a config entry."""
api = create_api(hass, entry.data[CONF_HOST], get_enable_ime(entry)) api = create_api(hass, entry.data[CONF_HOST], get_enable_ime(entry))
@ -64,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# update the config entry data and reload the config entry. # update the config entry data and reload the config entry.
api.keep_reconnecting(reauth_needed) api.keep_reconnecting(reauth_needed)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api entry.runtime_data = api
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -77,17 +80,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
) )
entry.async_on_unload(entry.add_update_listener(update_listener)) entry.async_on_unload(entry.add_update_listener(update_listener))
entry.async_on_unload(api.disconnect)
return True return True
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 unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
api: AndroidTVRemote = hass.data[DOMAIN].pop(entry.entry_id)
api.disconnect()
return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -4,23 +4,20 @@ from __future__ import annotations
from typing import Any from typing import Any
from androidtvremote2 import AndroidTVRemote
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.const import CONF_HOST, CONF_MAC
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from . import AndroidTVRemoteConfigEntry
TO_REDACT = {CONF_HOST, CONF_MAC} TO_REDACT = {CONF_HOST, CONF_MAC}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
api: AndroidTVRemote = hass.data[DOMAIN].pop(entry.entry_id) api = entry.runtime_data
return async_redact_data( return async_redact_data(
{ {
"api_device_info": api.device_info, "api_device_info": api.device_info,

View File

@ -8,6 +8,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["androidtvremote2"], "loggers": ["androidtvremote2"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["androidtvremote2==0.0.14"], "requirements": ["androidtvremote2==0.0.15"],
"zeroconf": ["_androidtvremote2._tcp.local."] "zeroconf": ["_androidtvremote2._tcp.local."]
} }

View File

@ -14,12 +14,11 @@ from homeassistant.components.media_player import (
MediaPlayerState, MediaPlayerState,
MediaType, MediaType,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
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 .const import DOMAIN from . import AndroidTVRemoteConfigEntry
from .entity import AndroidTVRemoteBaseEntity from .entity import AndroidTVRemoteBaseEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -27,11 +26,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AndroidTVRemoteConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Android TV media player entity based on a config entry.""" """Set up the Android TV media player entity based on a config entry."""
api: AndroidTVRemote = hass.data[DOMAIN][config_entry.entry_id] api = config_entry.runtime_data
async_add_entities([AndroidTVRemoteMediaPlayerEntity(api, config_entry)]) async_add_entities([AndroidTVRemoteMediaPlayerEntity(api, config_entry)])
@ -53,7 +52,9 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
| MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
) )
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None: def __init__(
self, api: AndroidTVRemote, config_entry: AndroidTVRemoteConfigEntry
) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(api, config_entry) super().__init__(api, config_entry)

View File

@ -6,8 +6,6 @@ import asyncio
from collections.abc import Iterable from collections.abc import Iterable
from typing import Any from typing import Any
from androidtvremote2 import AndroidTVRemote
from homeassistant.components.remote import ( from homeassistant.components.remote import (
ATTR_ACTIVITY, ATTR_ACTIVITY,
ATTR_DELAY_SECS, ATTR_DELAY_SECS,
@ -19,11 +17,10 @@ from homeassistant.components.remote import (
RemoteEntity, RemoteEntity,
RemoteEntityFeature, RemoteEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from . import AndroidTVRemoteConfigEntry
from .entity import AndroidTVRemoteBaseEntity from .entity import AndroidTVRemoteBaseEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -31,11 +28,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AndroidTVRemoteConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Android TV remote entity based on a config entry.""" """Set up the Android TV remote entity based on a config entry."""
api: AndroidTVRemote = hass.data[DOMAIN][config_entry.entry_id] api = config_entry.runtime_data
async_add_entities([AndroidTVRemoteEntity(api, config_entry)]) async_add_entities([AndroidTVRemoteEntity(api, config_entry)])

View File

@ -651,11 +651,8 @@ def websocket_delete_all_refresh_tokens(
continue continue
try: try:
hass.auth.async_remove_refresh_token(token) hass.auth.async_remove_refresh_token(token)
except Exception as err: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
getLogger(__name__).exception( getLogger(__name__).exception("Error during refresh token removal")
"During refresh token removal, the following error occurred: %s",
err,
)
remove_failed = True remove_failed = True
if remove_failed: if remove_failed:

View File

@ -13,8 +13,10 @@ from .hub import AxisHub, get_axis_api
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AxisConfigEntry = ConfigEntry[AxisHub]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry) -> bool:
"""Set up the Axis integration.""" """Set up the Axis integration."""
hass.data.setdefault(AXIS_DOMAIN, {}) hass.data.setdefault(AXIS_DOMAIN, {})
@ -25,8 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
except AuthenticationRequired as err: except AuthenticationRequired as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
hub = AxisHub(hass, config_entry, api) hub = config_entry.runtime_data = AxisHub(hass, config_entry, api)
hass.data[AXIS_DOMAIN][config_entry.entry_id] = hub
await hub.async_update_device_registry() await hub.async_update_device_registry()
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hub.setup() hub.setup()
@ -42,7 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Axis device config entry.""" """Unload Axis device config entry."""
hass.data[AXIS_DOMAIN].pop(config_entry.entry_id)
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

View File

@ -17,11 +17,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from . import AxisConfigEntry
from .entity import AxisEventDescription, AxisEventEntity from .entity import AxisEventDescription, AxisEventEntity
from .hub import AxisHub from .hub import AxisHub
@ -177,11 +177,11 @@ ENTITY_DESCRIPTIONS = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a Axis binary sensor.""" """Set up a Axis binary sensor."""
AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisBinarySensor, ENTITY_DESCRIPTIONS async_add_entities, AxisBinarySensor, ENTITY_DESCRIPTIONS
) )

View File

@ -4,12 +4,12 @@ from urllib.parse import urlencode
from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import HTTP_DIGEST_AUTHENTICATION from homeassistant.const import HTTP_DIGEST_AUTHENTICATION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
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 . import AxisConfigEntry
from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE
from .entity import AxisEntity from .entity import AxisEntity
from .hub import AxisHub from .hub import AxisHub
@ -17,13 +17,13 @@ from .hub import AxisHub
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Axis camera video stream.""" """Set up the Axis camera video stream."""
filter_urllib3_logging() filter_urllib3_logging()
hub = AxisHub.get_hub(hass, config_entry) hub = config_entry.runtime_data
if ( if (
not (prop := hub.api.vapix.params.property_handler.get("0")) not (prop := hub.api.vapix.params.property_handler.get("0"))

View File

@ -32,6 +32,7 @@ from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from homeassistant.util.network import is_link_local from homeassistant.util.network import is_link_local
from . import AxisConfigEntry
from .const import ( from .const import (
CONF_STREAM_PROFILE, CONF_STREAM_PROFILE,
CONF_VIDEO_SOURCE, CONF_VIDEO_SOURCE,
@ -260,13 +261,14 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry): class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Axis device options.""" """Handle Axis device options."""
config_entry: AxisConfigEntry
hub: AxisHub hub: AxisHub
async def async_step_init( async def async_step_init(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Manage the Axis device options.""" """Manage the Axis device options."""
self.hub = AxisHub.get_hub(self.hass, self.config_entry) self.hub = self.config_entry.runtime_data
return await self.async_step_configure_stream() return await self.async_step_configure_stream()
async def async_step_configure_stream( async def async_step_configure_stream(

View File

@ -5,11 +5,10 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.const import CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .hub import AxisHub from . import AxisConfigEntry
REDACT_CONFIG = {CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME} REDACT_CONFIG = {CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME}
REDACT_BASIC_DEVICE_INFO = {"SerialNumber", "SocSerialNumber"} REDACT_BASIC_DEVICE_INFO = {"SerialNumber", "SocSerialNumber"}
@ -17,10 +16,10 @@ REDACT_VAPIX_PARAMS = {"root.Network", "System.SerialNumber"}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: AxisConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
hub = AxisHub.get_hub(hass, config_entry) hub = config_entry.runtime_data
diag: dict[str, Any] = hub.additional_diagnostics.copy() diag: dict[str, Any] = hub.additional_diagnostics.copy()
diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG)

View File

@ -2,11 +2,10 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
import axis import axis
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
@ -17,12 +16,15 @@ from .config import AxisConfig
from .entity_loader import AxisEntityLoader from .entity_loader import AxisEntityLoader
from .event_source import AxisEventSource from .event_source import AxisEventSource
if TYPE_CHECKING:
from .. import AxisConfigEntry
class AxisHub: class AxisHub:
"""Manages a Axis device.""" """Manages a Axis device."""
def __init__( def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice self, hass: HomeAssistant, config_entry: AxisConfigEntry, api: axis.AxisDevice
) -> None: ) -> None:
"""Initialize the device.""" """Initialize the device."""
self.hass = hass self.hass = hass
@ -37,13 +39,6 @@ class AxisHub:
self.additional_diagnostics: dict[str, Any] = {} self.additional_diagnostics: dict[str, Any] = {}
@callback
@staticmethod
def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> AxisHub:
"""Get Axis hub from config entry."""
hub: AxisHub = hass.data[AXIS_DOMAIN][config_entry.entry_id]
return hub
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Connection state to the device.""" """Connection state to the device."""
@ -63,7 +58,7 @@ class AxisHub:
@staticmethod @staticmethod
async def async_new_address_callback( async def async_new_address_callback(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: AxisConfigEntry
) -> None: ) -> None:
"""Handle signals of device getting new address. """Handle signals of device getting new address.
@ -71,7 +66,7 @@ class AxisHub:
This is a static method because a class method (bound method), This is a static method because a class method (bound method),
cannot be used with weak references. cannot be used with weak references.
""" """
hub = AxisHub.get_hub(hass, config_entry) hub = config_entry.runtime_data
hub.config = AxisConfig.from_config_entry(config_entry) hub.config = AxisConfig.from_config_entry(config_entry)
hub.event_source.config_entry = config_entry hub.event_source.config_entry = config_entry
hub.api.config.host = hub.config.host hub.api.config.host = hub.config.host

View File

@ -11,10 +11,10 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
LightEntityDescription, LightEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AxisConfigEntry
from .entity import TOPIC_TO_EVENT_TYPE, AxisEventDescription, AxisEventEntity from .entity import TOPIC_TO_EVENT_TYPE, AxisEventDescription, AxisEventEntity
from .hub import AxisHub from .hub import AxisHub
@ -45,11 +45,11 @@ ENTITY_DESCRIPTIONS = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Axis light platform.""" """Set up the Axis light platform."""
AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisLight, ENTITY_DESCRIPTIONS async_add_entities, AxisLight, ENTITY_DESCRIPTIONS
) )

View File

@ -10,11 +10,11 @@ from homeassistant.components.switch import (
SwitchEntity, SwitchEntity,
SwitchEntityDescription, SwitchEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AxisConfigEntry
from .entity import AxisEventDescription, AxisEventEntity from .entity import AxisEventDescription, AxisEventEntity
from .hub import AxisHub from .hub import AxisHub
@ -38,11 +38,11 @@ ENTITY_DESCRIPTIONS = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Axis switch platform.""" """Set up the Axis switch platform."""
AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisSwitch, ENTITY_DESCRIPTIONS async_add_entities, AxisSwitch, ENTITY_DESCRIPTIONS
) )

View File

@ -97,10 +97,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
matched_domains = self._integration_matcher.match_domains(service_info) matched_domains = self._integration_matcher.match_domains(service_info)
if self._debug: if self._debug:
_LOGGER.debug( _LOGGER.debug(
"%s: %s %s match: %s", "%s: %s match: %s",
self._async_describe_source(service_info), self._async_describe_source(service_info),
service_info.address, service_info,
service_info.advertisement,
matched_domains, matched_domains,
) )

View File

@ -16,10 +16,10 @@
"requirements": [ "requirements": [
"bleak==0.21.1", "bleak==0.21.1",
"bleak-retry-connector==3.5.0", "bleak-retry-connector==3.5.0",
"bluetooth-adapters==0.19.1", "bluetooth-adapters==0.19.2",
"bluetooth-auto-recovery==1.4.2", "bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.19.0", "bluetooth-data-tools==1.19.0",
"dbus-fast==2.21.1", "dbus-fast==2.21.1",
"habluetooth==2.8.1" "habluetooth==3.0.1"
] ]
} }

View File

@ -2,12 +2,17 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from boschshcpy import SHCSession from boschshcpy import SHCSession
from boschshcpy.device import SHCDevice from boschshcpy.device import SHCDevice
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -20,341 +25,207 @@ from homeassistant.const import (
) )
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.typing import StateType
from .const import DATA_SESSION, DOMAIN from .const import DATA_SESSION, DOMAIN
from .entity import SHCEntity from .entity import SHCEntity
@dataclass(frozen=True, kw_only=True)
class SHCSensorEntityDescription(SensorEntityDescription):
"""Describes a SHC sensor."""
value_fn: Callable[[SHCDevice], StateType]
attributes_fn: Callable[[SHCDevice], dict[str, Any]] | None = None
TEMPERATURE_SENSOR = "temperature"
HUMIDITY_SENSOR = "humidity"
VALVE_TAPPET_SENSOR = "valvetappet"
PURITY_SENSOR = "purity"
AIR_QUALITY_SENSOR = "airquality"
TEMPERATURE_RATING_SENSOR = "temperature_rating"
HUMIDITY_RATING_SENSOR = "humidity_rating"
PURITY_RATING_SENSOR = "purity_rating"
POWER_SENSOR = "power"
ENERGY_SENSOR = "energy"
COMMUNICATION_QUALITY_SENSOR = "communication_quality"
SENSOR_DESCRIPTIONS: dict[str, SHCSensorEntityDescription] = {
TEMPERATURE_SENSOR: SHCSensorEntityDescription(
key=TEMPERATURE_SENSOR,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda device: device.temperature,
),
HUMIDITY_SENSOR: SHCSensorEntityDescription(
key=HUMIDITY_SENSOR,
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.humidity,
),
PURITY_SENSOR: SHCSensorEntityDescription(
key=PURITY_SENSOR,
translation_key=PURITY_SENSOR,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
value_fn=lambda device: device.purity,
),
AIR_QUALITY_SENSOR: SHCSensorEntityDescription(
key=AIR_QUALITY_SENSOR,
translation_key="air_quality",
value_fn=lambda device: device.combined_rating.name,
attributes_fn=lambda device: {
"rating_description": device.description,
},
),
TEMPERATURE_RATING_SENSOR: SHCSensorEntityDescription(
key=TEMPERATURE_RATING_SENSOR,
translation_key=TEMPERATURE_RATING_SENSOR,
value_fn=lambda device: device.temperature_rating.name,
),
COMMUNICATION_QUALITY_SENSOR: SHCSensorEntityDescription(
key=COMMUNICATION_QUALITY_SENSOR,
translation_key=COMMUNICATION_QUALITY_SENSOR,
value_fn=lambda device: device.communicationquality.name,
),
HUMIDITY_RATING_SENSOR: SHCSensorEntityDescription(
key=HUMIDITY_RATING_SENSOR,
translation_key=HUMIDITY_RATING_SENSOR,
value_fn=lambda device: device.humidity_rating.name,
),
PURITY_RATING_SENSOR: SHCSensorEntityDescription(
key=PURITY_RATING_SENSOR,
translation_key=PURITY_RATING_SENSOR,
value_fn=lambda device: device.purity_rating.name,
),
POWER_SENSOR: SHCSensorEntityDescription(
key=POWER_SENSOR,
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
value_fn=lambda device: device.powerconsumption,
),
ENERGY_SENSOR: SHCSensorEntityDescription(
key=ENERGY_SENSOR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda device: device.energyconsumption / 1000.0,
),
VALVE_TAPPET_SENSOR: SHCSensorEntityDescription(
key=VALVE_TAPPET_SENSOR,
translation_key=VALVE_TAPPET_SENSOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.position,
attributes_fn=lambda device: {
"valve_tappet_state": device.valvestate.name,
},
),
}
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the SHC sensor platform.""" """Set up the SHC sensor platform."""
entities: list[SensorEntity] = []
session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION]
for sensor in session.device_helper.thermostats: entities: list[SensorEntity] = [
entities.append( SHCSensor(
TemperatureSensor( device,
device=sensor, SENSOR_DESCRIPTIONS[sensor_type],
parent_id=session.information.unique_id, session.information.unique_id,
entry_id=config_entry.entry_id, config_entry.entry_id,
)
)
entities.append(
ValveTappetSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
) )
for device in session.device_helper.thermostats
for sensor_type in (TEMPERATURE_SENSOR, VALVE_TAPPET_SENSOR)
]
for sensor in session.device_helper.wallthermostats: entities.extend(
entities.append( SHCSensor(
TemperatureSensor( device,
device=sensor, SENSOR_DESCRIPTIONS[sensor_type],
parent_id=session.information.unique_id, session.information.unique_id,
entry_id=config_entry.entry_id, config_entry.entry_id,
)
)
entities.append(
HumiditySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
) )
for device in session.device_helper.wallthermostats
for sensor_type in (TEMPERATURE_SENSOR, HUMIDITY_SENSOR)
)
for sensor in session.device_helper.twinguards: entities.extend(
entities.append( SHCSensor(
TemperatureSensor( device,
device=sensor, SENSOR_DESCRIPTIONS[sensor_type],
parent_id=session.information.unique_id, session.information.unique_id,
entry_id=config_entry.entry_id, config_entry.entry_id,
)
) )
entities.append( for device in session.device_helper.twinguards
HumiditySensor( for sensor_type in (
device=sensor, TEMPERATURE_SENSOR,
parent_id=session.information.unique_id, HUMIDITY_SENSOR,
entry_id=config_entry.entry_id, PURITY_SENSOR,
) AIR_QUALITY_SENSOR,
) TEMPERATURE_RATING_SENSOR,
entities.append( HUMIDITY_RATING_SENSOR,
PuritySensor( PURITY_RATING_SENSOR,
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
AirQualitySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
TemperatureRatingSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
HumidityRatingSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
PurityRatingSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
) )
)
for sensor in ( entities.extend(
session.device_helper.smart_plugs + session.device_helper.light_switches_bsm SHCSensor(
): device,
entities.append( SENSOR_DESCRIPTIONS[sensor_type],
PowerSensor( session.information.unique_id,
device=sensor, config_entry.entry_id,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
) )
entities.append( for device in (
EnergySensor( session.device_helper.smart_plugs + session.device_helper.light_switches_bsm
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
) )
for sensor_type in (POWER_SENSOR, ENERGY_SENSOR)
)
for sensor in session.device_helper.smart_plugs_compact: entities.extend(
entities.append( SHCSensor(
PowerSensor( device,
device=sensor, SENSOR_DESCRIPTIONS[sensor_type],
parent_id=session.information.unique_id, session.information.unique_id,
entry_id=config_entry.entry_id, config_entry.entry_id,
)
)
entities.append(
EnergySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
CommunicationQualitySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
) )
for device in session.device_helper.smart_plugs_compact
for sensor_type in (POWER_SENSOR, ENERGY_SENSOR, COMMUNICATION_QUALITY_SENSOR)
)
async_add_entities(entities) async_add_entities(entities)
class TemperatureSensor(SHCEntity, SensorEntity): class SHCSensor(SHCEntity, SensorEntity):
"""Representation of an SHC temperature reporting sensor.""" """Representation of a SHC sensor."""
_attr_device_class = SensorDeviceClass.TEMPERATURE entity_description: SHCSensorEntityDescription
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: def __init__(
"""Initialize an SHC temperature reporting sensor.""" self,
device: SHCDevice,
entity_description: SHCSensorEntityDescription,
parent_id: str,
entry_id: str,
) -> None:
"""Initialize sensor."""
super().__init__(device, parent_id, entry_id) super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_temperature" self.entity_description = entity_description
self._attr_unique_id = f"{device.serial}_{entity_description.key}"
@property @property
def native_value(self): def native_value(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._device.temperature return self.entity_description.value_fn(self._device)
class HumiditySensor(SHCEntity, SensorEntity):
"""Representation of an SHC humidity reporting sensor."""
_attr_device_class = SensorDeviceClass.HUMIDITY
_attr_native_unit_of_measurement = PERCENTAGE
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC humidity reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_humidity"
@property @property
def native_value(self): def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state of the sensor."""
return self._device.humidity
class PuritySensor(SHCEntity, SensorEntity):
"""Representation of an SHC purity reporting sensor."""
_attr_translation_key = "purity"
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC purity reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_purity"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.purity
class AirQualitySensor(SHCEntity, SensorEntity):
"""Representation of an SHC airquality reporting sensor."""
_attr_translation_key = "air_quality"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC airquality reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_airquality"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.combined_rating.name
@property
def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { if self.entity_description.attributes_fn is not None:
"rating_description": self._device.description, return self.entity_description.attributes_fn(self._device)
} return None
class TemperatureRatingSensor(SHCEntity, SensorEntity):
"""Representation of an SHC temperature rating sensor."""
_attr_translation_key = "temperature_rating"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC temperature rating sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_temperature_rating"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.temperature_rating.name
class CommunicationQualitySensor(SHCEntity, SensorEntity):
"""Representation of an SHC communication quality reporting sensor."""
_attr_translation_key = "communication_quality"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC communication quality reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_communication_quality"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.communicationquality.name
class HumidityRatingSensor(SHCEntity, SensorEntity):
"""Representation of an SHC humidity rating sensor."""
_attr_translation_key = "humidity_rating"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC humidity rating sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_humidity_rating"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.humidity_rating.name
class PurityRatingSensor(SHCEntity, SensorEntity):
"""Representation of an SHC purity rating sensor."""
_attr_translation_key = "purity_rating"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC purity rating sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_purity_rating"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.purity_rating.name
class PowerSensor(SHCEntity, SensorEntity):
"""Representation of an SHC power reporting sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC power reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_power"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.powerconsumption
class EnergySensor(SHCEntity, SensorEntity):
"""Representation of an SHC energy reporting sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_state_class = SensorStateClass.TOTAL_INCREASING
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC energy reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{self._device.serial}_energy"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.energyconsumption / 1000.0
class ValveTappetSensor(SHCEntity, SensorEntity):
"""Representation of an SHC valve tappet reporting sensor."""
_attr_translation_key = "valvetappet"
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = PERCENTAGE
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC valve tappet reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_valvetappet"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.position
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {
"valve_tappet_state": self._device.valvestate.name,
}

View File

@ -43,21 +43,21 @@ SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = {
"smartplug": SHCSwitchEntityDescription( "smartplug": SHCSwitchEntityDescription(
key="smartplug", key="smartplug",
device_class=SwitchDeviceClass.OUTLET, device_class=SwitchDeviceClass.OUTLET,
on_key="state", on_key="switchstate",
on_value=SHCSmartPlug.PowerSwitchService.State.ON, on_value=SHCSmartPlug.PowerSwitchService.State.ON,
should_poll=False, should_poll=False,
), ),
"smartplugcompact": SHCSwitchEntityDescription( "smartplugcompact": SHCSwitchEntityDescription(
key="smartplugcompact", key="smartplugcompact",
device_class=SwitchDeviceClass.OUTLET, device_class=SwitchDeviceClass.OUTLET,
on_key="state", on_key="switchstate",
on_value=SHCSmartPlugCompact.PowerSwitchService.State.ON, on_value=SHCSmartPlugCompact.PowerSwitchService.State.ON,
should_poll=False, should_poll=False,
), ),
"lightswitch": SHCSwitchEntityDescription( "lightswitch": SHCSwitchEntityDescription(
key="lightswitch", key="lightswitch",
device_class=SwitchDeviceClass.SWITCH, device_class=SwitchDeviceClass.SWITCH,
on_key="state", on_key="switchstate",
on_value=SHCLightSwitch.PowerSwitchService.State.ON, on_value=SHCLightSwitch.PowerSwitchService.State.ON,
should_poll=False, should_poll=False,
), ),

View File

@ -12,9 +12,10 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import DOMAIN
from .coordinator import BraviaTVCoordinator from .coordinator import BraviaTVCoordinator
BraviaTVConfigEntry = ConfigEntry[BraviaTVCoordinator]
PLATFORMS: Final[list[Platform]] = [ PLATFORMS: Final[list[Platform]] = [
Platform.BUTTON, Platform.BUTTON,
Platform.MEDIA_PLAYER, Platform.MEDIA_PLAYER,
@ -22,7 +23,9 @@ PLATFORMS: Final[list[Platform]] = [
] ]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(
hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
host = config_entry.data[CONF_HOST] host = config_entry.data[CONF_HOST]
mac = config_entry.data[CONF_MAC] mac = config_entry.data[CONF_MAC]
@ -40,26 +43,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) config_entry.runtime_data = coordinator
hass.data[DOMAIN][config_entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
config_entry, PLATFORMS
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: async def update_listener(
hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> None:
"""Handle options update.""" """Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)

View File

@ -10,12 +10,11 @@ from homeassistant.components.button import (
ButtonEntity, ButtonEntity,
ButtonEntityDescription, ButtonEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
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 .const import DOMAIN from . import BraviaTVConfigEntry
from .coordinator import BraviaTVCoordinator from .coordinator import BraviaTVCoordinator
from .entity import BraviaTVEntity from .entity import BraviaTVEntity
@ -45,12 +44,12 @@ BUTTONS: tuple[BraviaTVButtonDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: BraviaTVConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Bravia TV Button entities.""" """Set up the Bravia TV Button entities."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id unique_id = config_entry.unique_id
assert unique_id is not None assert unique_id is not None

View File

@ -3,21 +3,19 @@
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_PIN from homeassistant.const import CONF_MAC, CONF_PIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from . import BraviaTVConfigEntry
from .coordinator import BraviaTVCoordinator
TO_REDACT = {CONF_MAC, CONF_PIN, "macAddr"} TO_REDACT = {CONF_MAC, CONF_PIN, "macAddr"}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: BraviaTVCoordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
device_info = await coordinator.client.get_system_info() device_info = await coordinator.client.get_system_info()

View File

@ -15,22 +15,22 @@ from homeassistant.components.media_player import (
MediaType, MediaType,
) )
from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.media_player.browse_media import BrowseMedia
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 .const import DOMAIN, SourceType from . import BraviaTVConfigEntry
from .const import SourceType
from .entity import BraviaTVEntity from .entity import BraviaTVEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: BraviaTVConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Bravia TV Media Player from a config_entry.""" """Set up Bravia TV Media Player from a config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id unique_id = config_entry.unique_id
assert unique_id is not None assert unique_id is not None

View File

@ -6,22 +6,21 @@ from collections.abc import Iterable
from typing import Any from typing import Any
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity
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 .const import DOMAIN from . import BraviaTVConfigEntry
from .entity import BraviaTVEntity from .entity import BraviaTVEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: BraviaTVConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Bravia TV Remote from a config entry.""" """Set up Bravia TV Remote from a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id unique_id = config_entry.unique_id
assert unique_id is not None assert unique_id is not None

View File

@ -24,8 +24,10 @@ PLATFORMS: list[Platform] = [Platform.TODO]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
"""Set up Bring! from a config entry.""" """Set up Bring! from a config entry."""
email = entry.data[CONF_EMAIL] email = entry.data[CONF_EMAIL]
@ -57,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = BringDataUpdateCoordinator(hass, bring) coordinator = BringDataUpdateCoordinator(hass, bring)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -66,7 +68,4 @@ 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 unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -15,7 +15,6 @@ from homeassistant.components.todo import (
TodoListEntity, TodoListEntity,
TodoListEntityFeature, TodoListEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
@ -23,6 +22,7 @@ from homeassistant.helpers.config_validation import make_entity_service_schema
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 . import BringConfigEntry
from .const import ( from .const import (
ATTR_ITEM_NAME, ATTR_ITEM_NAME,
ATTR_NOTIFICATION_TYPE, ATTR_NOTIFICATION_TYPE,
@ -34,11 +34,11 @@ from .coordinator import BringData, BringDataUpdateCoordinator
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: BringConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the sensor from a config entry created in the integrations UI.""" """Set up the sensor from a config entry created in the integrations UI."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id unique_id = config_entry.unique_id

View File

@ -373,8 +373,11 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
start_time = dt_util.utcnow() start_time = dt_util.utcnow()
while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT: while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT:
await asyncio.sleep(1) await asyncio.sleep(1)
found = await device.async_request(device.api.check_frequency)[0] is_found, frequency = await device.async_request(
if found: device.api.check_frequency
)
if is_found:
_LOGGER.info("Radiofrequency detected: %s MHz", frequency)
break break
else: else:
await device.async_request(device.api.cancel_sweep_frequency) await device.async_request(device.api.cancel_sweep_frequency)

View File

@ -2,29 +2,23 @@
from __future__ import annotations from __future__ import annotations
from asyncio import timeout from brother import Brother, SnmpError
from datetime import timedelta
import logging
from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.const import CONF_HOST, CONF_TYPE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP from .const import DOMAIN, SNMP
from .coordinator import BrotherDataUpdateCoordinator
from .utils import get_snmp_engine from .utils import get_snmp_engine
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(seconds=30) BrotherConfigEntry = ConfigEntry[BrotherDataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
"""Set up Brother from a config entry.""" """Set up Brother from a config entry."""
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
printer_type = entry.data[CONF_TYPE] printer_type = entry.data[CONF_TYPE]
@ -40,48 +34,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = BrotherDataUpdateCoordinator(hass, brother) coordinator = BrotherDataUpdateCoordinator(hass, brother)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = coordinator
hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {}) hass.data.setdefault(DOMAIN, {SNMP: snmp_engine})
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator
hass.data[DOMAIN][SNMP] = snmp_engine
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: loaded_entries = [
hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) entry
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY]: for entry in hass.config_entries.async_entries(DOMAIN)
hass.data[DOMAIN].pop(SNMP) if entry.state == ConfigEntryState.LOADED
hass.data[DOMAIN].pop(DATA_CONFIG_ENTRY) ]
# We only want to remove the SNMP engine when unloading the last config entry
if unload_ok and len(loaded_entries) == 1:
hass.data[DOMAIN].pop(SNMP)
return unload_ok return unload_ok
class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]): # pylint: disable=hass-enforce-coordinator-module
"""Class to manage fetching Brother data from the printer."""
def __init__(self, hass: HomeAssistant, brother: Brother) -> None:
"""Initialize."""
self.brother = brother
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self) -> BrotherSensors:
"""Update data via library."""
try:
async with timeout(20):
data = await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModelError) as error:
raise UpdateFailed(error) from error
return data

View File

@ -2,12 +2,13 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from typing import Final from typing import Final
DATA_CONFIG_ENTRY: Final = "config_entry"
DOMAIN: Final = "brother" DOMAIN: Final = "brother"
PRINTER_TYPES: Final = ["laser", "ink"] PRINTER_TYPES: Final = ["laser", "ink"]
SNMP: Final = "snmp" SNMP: Final = "snmp"
UPDATE_INTERVAL = timedelta(seconds=30)

View File

@ -0,0 +1,37 @@
"""Coordinator for Brother integration."""
from asyncio import timeout
import logging
from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL
_LOGGER = logging.getLogger(__name__)
class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]):
"""Class to manage fetching Brother data from the printer."""
def __init__(self, hass: HomeAssistant, brother: Brother) -> None:
"""Initialize."""
self.brother = brother
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
)
async def _async_update_data(self) -> BrotherSensors:
"""Update data via library."""
try:
async with timeout(20):
data = await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModelError) as error:
raise UpdateFailed(error) from error
return data

View File

@ -5,20 +5,16 @@ from __future__ import annotations
from dataclasses import asdict from dataclasses import asdict
from typing import Any from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import BrotherDataUpdateCoordinator from . import BrotherConfigEntry
from .const import DATA_CONFIG_ENTRY, DOMAIN
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: BrotherConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: BrotherDataUpdateCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ coordinator = config_entry.runtime_data
config_entry.entry_id
]
return { return {
"info": dict(config_entry.data), "info": dict(config_entry.data),

View File

@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -25,8 +24,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BrotherDataUpdateCoordinator from . import BrotherConfigEntry, BrotherDataUpdateCoordinator
from .const import DATA_CONFIG_ENTRY, DOMAIN from .const import DOMAIN
ATTR_COUNTER = "counter" ATTR_COUNTER = "counter"
ATTR_REMAINING_PAGES = "remaining_pages" ATTR_REMAINING_PAGES = "remaining_pages"
@ -318,11 +317,12 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: BrotherConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add Brother entities from a config_entry.""" """Add Brother entities from a config_entry."""
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] coordinator = entry.runtime_data
# Due to the change of the attribute name of one sensor, it is necessary to migrate # Due to the change of the attribute name of one sensor, it is necessary to migrate
# the unique_id to the new one. # the unique_id to the new one.
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)

View File

@ -7,21 +7,21 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.start import async_at_started from homeassistant.helpers.start import async_at_started
from .const import DOMAIN
from .coordinator import CertExpiryDataUpdateCoordinator from .coordinator import CertExpiryDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
CertExpiryConfigEntry = ConfigEntry[CertExpiryDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) -> bool:
"""Load the saved entities.""" """Load the saved entities."""
host: str = entry.data[CONF_HOST] host: str = entry.data[CONF_HOST]
port: int = entry.data[CONF_PORT] port: int = entry.data[CONF_PORT]
coordinator = CertExpiryDataUpdateCoordinator(hass, host, port) coordinator = CertExpiryDataUpdateCoordinator(hass, host, port)
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
if entry.unique_id is None: if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}") hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}")

View File

@ -12,7 +12,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
) )
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -22,7 +22,7 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CertExpiryDataUpdateCoordinator from . import CertExpiryConfigEntry, CertExpiryDataUpdateCoordinator
from .const import DEFAULT_PORT, DOMAIN from .const import DEFAULT_PORT, DOMAIN
SCAN_INTERVAL = timedelta(hours=12) SCAN_INTERVAL = timedelta(hours=12)
@ -62,15 +62,13 @@ async def async_setup_platform(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: CertExpiryConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add cert-expiry entry.""" """Add cert-expiry entry."""
coordinator: CertExpiryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
sensors = [ sensors = [SSLCertificateTimestamp(coordinator)]
SSLCertificateTimestamp(coordinator),
]
async_add_entities(sensors, True) async_add_entities(sensors, True)

View File

@ -142,6 +142,9 @@ async def websocket_list_agents(
agent = manager.async_get_agent(agent_info.id) agent = manager.async_get_agent(agent_info.id)
assert agent is not None assert agent is not None
if isinstance(agent, ConversationEntity):
continue
supported_languages = agent.supported_languages supported_languages = agent.supported_languages
if language and supported_languages != MATCH_ALL: if language and supported_languages != MATCH_ALL:
supported_languages = language_util.matches( supported_languages = language_util.matches(

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components.notify import DOMAIN, NotifyEntity from homeassistant.components.notify import DOMAIN, NotifyEntity, NotifyEntityFeature
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -33,12 +33,15 @@ class DemoNotifyEntity(NotifyEntity):
) -> None: ) -> None:
"""Initialize the Demo button entity.""" """Initialize the Demo button entity."""
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
self._attr_supported_features = NotifyEntityFeature.TITLE
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)}, identifiers={(DOMAIN, unique_id)},
name=device_name, name=device_name,
) )
async def async_send_message(self, message: str) -> None: async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to a user.""" """Send a message to a user."""
event_notitifcation = {"message": message} event_notification = {"message": message}
self.hass.bus.async_fire(EVENT_NOTIFY, event_notitifcation) if title is not None:
event_notification["title"] = title
self.hass.bus.async_fire(EVENT_NOTIFY, event_notification)

View File

@ -18,19 +18,15 @@ from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import DeviceEntry
from .const import ( from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, GATEWAY_SERIAL_PATTERN, PLATFORMS
CONF_MYDEVOLO,
DEFAULT_MYDEVOLO, DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
DOMAIN,
GATEWAY_SERIAL_PATTERN,
PLATFORMS,
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(
hass: HomeAssistant, entry: DevoloHomeControlConfigEntry
) -> bool:
"""Set up the devolo account from a config entry.""" """Set up the devolo account from a config entry."""
hass.data.setdefault(DOMAIN, {})
mydevolo = configure_mydevolo(entry.data) mydevolo = configure_mydevolo(entry.data)
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
@ -47,11 +43,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
uuid = await hass.async_add_executor_job(mydevolo.uuid) uuid = await hass.async_add_executor_job(mydevolo.uuid)
hass.config_entries.async_update_entry(entry, unique_id=uuid) hass.config_entries.async_update_entry(entry, unique_id=uuid)
def shutdown(event: Event) -> None:
for gateway in entry.runtime_data:
gateway.websocket_disconnect(
f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}"
)
# Listen when EVENT_HOMEASSISTANT_STOP is fired
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
)
try: try:
zeroconf_instance = await zeroconf.async_get_instance(hass) zeroconf_instance = await zeroconf.async_get_instance(hass)
hass.data[DOMAIN][entry.entry_id] = {"gateways": [], "listener": None} entry.runtime_data = []
for gateway_id in gateway_ids: for gateway_id in gateway_ids:
hass.data[DOMAIN][entry.entry_id]["gateways"].append( entry.runtime_data.append(
await hass.async_add_executor_job( await hass.async_add_executor_job(
partial( partial(
HomeControl, HomeControl,
@ -66,31 +73,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
def shutdown(event: Event) -> None:
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]:
gateway.websocket_disconnect(
f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}"
)
# Listen when EVENT_HOMEASSISTANT_STOP is fired
hass.data[DOMAIN][entry.entry_id]["listener"] = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, shutdown
)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: DevoloHomeControlConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await asyncio.gather( await asyncio.gather(
*( *(
hass.async_add_executor_job(gateway.websocket_disconnect) hass.async_add_executor_job(gateway.websocket_disconnect)
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] for gateway in entry.runtime_data
) )
) )
hass.data[DOMAIN][entry.entry_id]["listener"]()
hass.data[DOMAIN].pop(entry.entry_id)
return unload return unload

View File

@ -9,12 +9,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
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 .const import DOMAIN from . import DevoloHomeControlConfigEntry
from .devolo_device import DevoloDeviceEntity from .devolo_device import DevoloDeviceEntity
DEVICE_CLASS_MAPPING = { DEVICE_CLASS_MAPPING = {
@ -28,12 +27,14 @@ DEVICE_CLASS_MAPPING = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Get all binary sensor and multi level sensor devices and setup them via config entry.""" """Get all binary sensor and multi level sensor devices and setup them via config entry."""
entities: list[BinarySensorEntity] = [] entities: list[BinarySensorEntity] = []
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: for gateway in entry.runtime_data:
entities.extend( entities.extend(
DevoloBinaryDeviceEntity( DevoloBinaryDeviceEntity(
homecontrol=gateway, homecontrol=gateway,

View File

@ -13,17 +13,18 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitOfTemperature from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitOfTemperature
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 .const import DOMAIN from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Get all cover devices and setup them via config entry.""" """Get all cover devices and setup them via config entry."""
@ -33,7 +34,7 @@ async def async_setup_entry(
device_instance=device, device_instance=device,
element_uid=multi_level_switch, element_uid=multi_level_switch,
) )
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property for multi_level_switch in device.multi_level_switch_property
if device.device_model_uid if device.device_model_uid

View File

@ -9,16 +9,17 @@ from homeassistant.components.cover import (
CoverEntity, CoverEntity,
CoverEntityFeature, CoverEntityFeature,
) )
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 .const import DOMAIN from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Get all cover devices and setup them via config entry.""" """Get all cover devices and setup them via config entry."""
@ -28,7 +29,7 @@ async def async_setup_entry(
device_instance=device, device_instance=device,
element_uid=multi_level_switch, element_uid=multi_level_switch,
) )
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property for multi_level_switch in device.multi_level_switch_property
if multi_level_switch.startswith("devolo.Blinds") if multi_level_switch.startswith("devolo.Blinds")

View File

@ -4,24 +4,19 @@ from __future__ import annotations
from typing import Any from typing import Any
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from . import DevoloHomeControlConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: DevoloHomeControlConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
gateways: list[HomeControl] = hass.data[DOMAIN][entry.entry_id]["gateways"]
device_info = [ device_info = [
{ {
"gateway": { "gateway": {
@ -38,7 +33,7 @@ async def async_get_config_entry_diagnostics(
for device_id, properties in gateway.devices.items() for device_id, properties in gateway.devices.items()
], ],
} }
for gateway in gateways for gateway in entry.runtime_data
] ]
return { return {

View File

@ -8,16 +8,17 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
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 .const import DOMAIN from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Get all light devices and setup them via config entry.""" """Get all light devices and setup them via config entry."""
@ -27,7 +28,7 @@ async def async_setup_entry(
device_instance=device, device_instance=device,
element_uid=multi_level_switch.element_uid, element_uid=multi_level_switch.element_uid,
) )
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property.values() for multi_level_switch in device.multi_level_switch_property.values()
if multi_level_switch.switch_type == "dimmer" if multi_level_switch.switch_type == "dimmer"

View File

@ -10,12 +10,11 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.const import PERCENTAGE, EntityCategory
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 .const import DOMAIN from . import DevoloHomeControlConfigEntry
from .devolo_device import DevoloDeviceEntity from .devolo_device import DevoloDeviceEntity
DEVICE_CLASS_MAPPING = { DEVICE_CLASS_MAPPING = {
@ -39,12 +38,14 @@ STATE_CLASS_MAPPING = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Get all sensor devices and setup them via config entry.""" """Get all sensor devices and setup them via config entry."""
entities: list[SensorEntity] = [] entities: list[SensorEntity] = []
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: for gateway in entry.runtime_data:
entities.extend( entities.extend(
DevoloGenericMultiLevelDeviceEntity( DevoloGenericMultiLevelDeviceEntity(
homecontrol=gateway, homecontrol=gateway,

View File

@ -6,16 +6,17 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature
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 .const import DOMAIN from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Get all binary sensor and multi level sensor devices and setup them via config entry.""" """Get all binary sensor and multi level sensor devices and setup them via config entry."""
@ -25,7 +26,7 @@ async def async_setup_entry(
device_instance=device, device_instance=device,
element_uid=multi_level_switch, element_uid=multi_level_switch,
) )
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property for multi_level_switch in device.multi_level_switch_property
if multi_level_switch.startswith("devolo.SirenMultiLevelSwitch") if multi_level_switch.startswith("devolo.SirenMultiLevelSwitch")

View File

@ -8,16 +8,17 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
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 .const import DOMAIN from . import DevoloHomeControlConfigEntry
from .devolo_device import DevoloDeviceEntity from .devolo_device import DevoloDeviceEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Get all devices and setup the switch devices via config entry.""" """Get all devices and setup the switch devices via config entry."""
@ -27,7 +28,7 @@ async def async_setup_entry(
device_instance=device, device_instance=device,
element_uid=binary_switch, element_uid=binary_switch,
) )
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] for gateway in entry.runtime_data
for device in gateway.binary_switch_devices for device in gateway.binary_switch_devices
for binary_switch in device.binary_switch_property for binary_switch in device.binary_switch_property
# Exclude the binary switch which also has multi_level_switches here, # Exclude the binary switch which also has multi_level_switches here,

View File

@ -12,16 +12,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import get_async_client
from .const import DOMAIN
from .coordinator import DiscovergyUpdateCoordinator from .coordinator import DiscovergyUpdateCoordinator
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
DiscovergyConfigEntry = ConfigEntry[list[DiscovergyUpdateCoordinator]]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -> bool:
"""Set up Discovergy from a config entry.""" """Set up Discovergy from a config entry."""
hass.data.setdefault(DOMAIN, {})
client = Discovergy( client = Discovergy(
email=entry.data[CONF_EMAIL], email=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD], password=entry.data[CONF_PASSWORD],
@ -53,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
coordinators.append(coordinator) coordinators.append(coordinator)
hass.data[DOMAIN][entry.entry_id] = coordinators entry.runtime_data = coordinators
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry)) entry.async_on_unload(entry.add_update_listener(async_reload_entry))
@ -63,11 +62,7 @@ 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."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -6,11 +6,9 @@ from dataclasses import asdict
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from . import DiscovergyConfigEntry
from .coordinator import DiscovergyUpdateCoordinator
TO_REDACT_METER = { TO_REDACT_METER = {
"serial_number", "serial_number",
@ -22,14 +20,13 @@ TO_REDACT_METER = {
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: DiscovergyConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
flattened_meter: list[dict] = [] flattened_meter: list[dict] = []
last_readings: dict[str, dict] = {} last_readings: dict[str, dict] = {}
coordinators: list[DiscovergyUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id]
for coordinator in coordinators: for coordinator in entry.runtime_data:
# make a dict of meter data and redact some data # make a dict of meter data and redact some data
flattened_meter.append( flattened_meter.append(
async_redact_data(asdict(coordinator.meter), TO_REDACT_METER) async_redact_data(asdict(coordinator.meter), TO_REDACT_METER)

View File

@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
EntityCategory, EntityCategory,
UnitOfElectricPotential, UnitOfElectricPotential,
@ -25,6 +24,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
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 . import DiscovergyConfigEntry
from .const import DOMAIN, MANUFACTURER from .const import DOMAIN, MANUFACTURER
from .coordinator import DiscovergyUpdateCoordinator from .coordinator import DiscovergyUpdateCoordinator
@ -163,13 +163,13 @@ ADDITIONAL_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DiscovergyConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Discovergy sensors.""" """Set up the Discovergy sensors."""
coordinators: list[DiscovergyUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id]
entities: list[DiscovergySensor] = [] entities: list[DiscovergySensor] = []
for coordinator in coordinators: for coordinator in entry.runtime_data:
sensors: tuple[DiscovergySensorEntityDescription, ...] = () sensors: tuple[DiscovergySensorEntityDescription, ...] = ()
# select sensor descriptions based on meter type and combine with additional sensors # select sensor descriptions based on meter type and combine with additional sensors

View File

@ -2,27 +2,27 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN, PLATFORMS from .const import PLATFORMS
from .coordinator import DwdWeatherWarningsCoordinator from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(
hass: HomeAssistant, entry: DwdWeatherWarningsConfigEntry
) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
coordinator = DwdWeatherWarningsCoordinator(hass) coordinator = DwdWeatherWarningsCoordinator(hass)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: DwdWeatherWarningsConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -19,11 +19,13 @@ from .const import (
from .exceptions import EntityNotFoundError from .exceptions import EntityNotFoundError
from .util import get_position_data from .util import get_position_data
DwdWeatherWarningsConfigEntry = ConfigEntry["DwdWeatherWarningsCoordinator"]
class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]): class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
"""Custom coordinator for the dwd_weather_warnings integration.""" """Custom coordinator for the dwd_weather_warnings integration."""
config_entry: ConfigEntry config_entry: DwdWeatherWarningsConfigEntry
api: DwdWeatherWarningsAPI api: DwdWeatherWarningsAPI
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:

View File

@ -14,7 +14,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -40,7 +39,7 @@ from .const import (
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
) )
from .coordinator import DwdWeatherWarningsCoordinator from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
@ -55,10 +54,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: DwdWeatherWarningsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up entities from config entry.""" """Set up entities from config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
[ [
@ -80,7 +81,7 @@ class DwdWeatherWarningsSensor(
def __init__( def __init__(
self, self,
coordinator: DwdWeatherWarningsCoordinator, coordinator: DwdWeatherWarningsCoordinator,
entry: ConfigEntry, entry: DwdWeatherWarningsConfigEntry,
description: SensorEntityDescription, description: SensorEntityDescription,
) -> None: ) -> None:
"""Initialize a DWD-Weather-Warnings sensor.""" """Initialize a DWD-Weather-Warnings sensor."""

View File

@ -85,6 +85,6 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity):
f"{self.thermostat["identifier"]}_notify_{thermostat_index}" f"{self.thermostat["identifier"]}_notify_{thermostat_index}"
) )
def send_message(self, message: str) -> None: def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message.""" """Send a message."""
self.data.ecobee.send_message(self.thermostat_index, message) self.data.ecobee.send_message(self.thermostat_index, message)

View File

@ -17,7 +17,7 @@ from homeassistant.components.light import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
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
@ -94,7 +94,7 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
name=device.sku, name=device.sku,
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=device.sku, model=device.sku,
connections={(CONNECTION_NETWORK_MAC, device.fingerprint)}, serial_number=device.fingerprint,
) )
@property @property

View File

@ -6,5 +6,5 @@
"dependencies": ["network"], "dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local", "documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["govee-local-api==1.4.4"] "requirements": ["govee-local-api==1.4.5"]
} }

View File

@ -1,7 +1,9 @@
"""The habitica integration.""" """The habitica integration."""
from http import HTTPStatus
import logging import logging
from aiohttp import ClientResponseError
from habitipy.aio import HabitipyAsync from habitipy.aio import HabitipyAsync
import voluptuous as vol import voluptuous as vol
@ -16,6 +18,7 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -30,9 +33,12 @@ from .const import (
EVENT_API_CALL_SUCCESS, EVENT_API_CALL_SUCCESS,
SERVICE_API_CALL, SERVICE_API_CALL,
) )
from .coordinator import HabiticaDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"] SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"]
INSTANCE_SCHEMA = vol.All( INSTANCE_SCHEMA = vol.All(
@ -104,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: HabiticaConfigEntry) -> bool:
"""Set up habitica from a config entry.""" """Set up habitica from a config entry."""
class HAHabitipyAsync(HabitipyAsync): class HAHabitipyAsync(HabitipyAsync):
@ -120,7 +126,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api = None api = None
for entry in entries: for entry in entries:
if entry.data[CONF_NAME] == name: if entry.data[CONF_NAME] == name:
api = hass.data[DOMAIN].get(entry.entry_id) api = entry.runtime_data.api
break break
if api is None: if api is None:
_LOGGER.error("API_CALL: User '%s' not configured", name) _LOGGER.error("API_CALL: User '%s' not configured", name)
@ -139,24 +145,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data} EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
) )
data = hass.data.setdefault(DOMAIN, {})
config = entry.data
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
url = config[CONF_URL]
username = config[CONF_API_USER] url = entry.data[CONF_URL]
password = config[CONF_API_KEY] username = entry.data[CONF_API_USER]
name = config.get(CONF_NAME) password = entry.data[CONF_API_KEY]
config_dict = {"url": url, "login": username, "password": password}
api = HAHabitipyAsync(config_dict) api = HAHabitipyAsync(
user = await api.user.get() {
if name is None: "url": url,
"login": username,
"password": password,
}
)
try:
user = await api.user.get(userFields="profile")
except ClientResponseError as e:
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
) from e
raise ConfigEntryNotReady(e) from e
if not entry.data.get(CONF_NAME):
name = user["profile"]["name"] name = user["profile"]["name"]
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
entry, entry,
data={**entry.data, CONF_NAME: name}, data={**entry.data, CONF_NAME: name},
) )
data[entry.entry_id] = api
coordinator = HabiticaDataUpdateCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): if not hass.services.has_service(DOMAIN, SERVICE_API_CALL):
@ -169,10 +191,6 @@ 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."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.config_entries.async_entries(DOMAIN)) == 1: if len(hass.config_entries.async_entries(DOMAIN)) == 1:
hass.services.async_remove(DOMAIN, SERVICE_API_CALL) hass.services.async_remove(DOMAIN, SERVICE_API_CALL)
return unload_ok return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,56 @@
"""DataUpdateCoordinator for the Habitica integration."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
from aiohttp import ClientResponseError
from habitipy.aio import HabitipyAsync
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@dataclass
class HabiticaData:
"""Coordinator data class."""
user: dict[str, Any]
tasks: list[dict]
class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
"""Habitica Data Update Coordinator."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, habitipy: HabitipyAsync) -> None:
"""Initialize the Habitica data coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=30),
)
self.api = habitipy
async def _async_update_data(self) -> HabiticaData:
user_fields = set(self.async_contexts())
try:
user_response = await self.api.user.get(userFields=",".join(user_fields))
tasks_response = []
for task_type in ("todos", "dailys", "habits", "rewards"):
tasks_response.extend(await self.api.tasks.user.get(type=task_type))
except ClientResponseError as error:
raise UpdateFailed(f"Error communicating with API: {error}") from error
return HabiticaData(user=user_response, tasks=tasks_response)

View File

@ -4,13 +4,9 @@ from __future__ import annotations
from collections import namedtuple from collections import namedtuple
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from enum import StrEnum from enum import StrEnum
from http import HTTPStatus
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, cast
from aiohttp import ClientResponseError
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -22,14 +18,15 @@ from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HabiticaConfigEntry
from .const import DOMAIN, MANUFACTURER, NAME from .const import DOMAIN, MANUFACTURER, NAME
from .coordinator import HabiticaDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
@dataclass(kw_only=True, frozen=True) @dataclass(kw_only=True, frozen=True)
class HabitipySensorEntityDescription(SensorEntityDescription): class HabitipySensorEntityDescription(SensorEntityDescription):
@ -122,14 +119,14 @@ SENSOR_DESCRIPTIONS: dict[str, HabitipySensorEntityDescription] = {
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
TASKS_TYPES = { TASKS_TYPES = {
"habits": SensorType( "habits": SensorType(
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"] "Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habit"]
), ),
"dailys": SensorType( "dailys": SensorType(
"Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["dailys"] "Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["daily"]
), ),
"todos": SensorType("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todos"]), "todos": SensorType("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todo"]),
"rewards": SensorType( "rewards": SensorType(
"Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["rewards"] "Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["reward"]
), ),
} }
@ -163,79 +160,26 @@ TASKS_MAP = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: HabiticaConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the habitica sensors.""" """Set up the habitica sensors."""
name = config_entry.data[CONF_NAME] name = config_entry.data[CONF_NAME]
sensor_data = HabitipyData(hass.data[DOMAIN][config_entry.entry_id]) coordinator = config_entry.runtime_data
await sensor_data.update()
entities: list[SensorEntity] = [ entities: list[SensorEntity] = [
HabitipySensor(sensor_data, description, config_entry) HabitipySensor(coordinator, description, config_entry)
for description in SENSOR_DESCRIPTIONS.values() for description in SENSOR_DESCRIPTIONS.values()
] ]
entities.extend( entities.extend(
HabitipyTaskSensor(name, task_type, sensor_data, config_entry) HabitipyTaskSensor(name, task_type, coordinator, config_entry)
for task_type in TASKS_TYPES for task_type in TASKS_TYPES
) )
async_add_entities(entities, True) async_add_entities(entities, True)
class HabitipyData: class HabitipySensor(CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity):
"""Habitica API user data cache."""
tasks: dict[str, Any]
def __init__(self, api) -> None:
"""Habitica API user data cache."""
self.api = api
self.data = None
self.tasks = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update(self):
"""Get a new fix from Habitica servers."""
try:
self.data = await self.api.user.get()
except ClientResponseError as error:
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
_LOGGER.warning(
(
"Sensor data update for %s has too many API requests;"
" Skipping the update"
),
DOMAIN,
)
else:
_LOGGER.error(
"Count not update sensor data for %s (%s)",
DOMAIN,
error,
)
for task_type in TASKS_TYPES:
try:
self.tasks[task_type] = await self.api.tasks.user.get(type=task_type)
except ClientResponseError as error:
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
_LOGGER.warning(
(
"Sensor data update for %s has too many API requests;"
" Skipping the update"
),
DOMAIN,
)
else:
_LOGGER.error(
"Count not update sensor data for %s (%s)",
DOMAIN,
error,
)
class HabitipySensor(SensorEntity):
"""A generic Habitica sensor.""" """A generic Habitica sensor."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -243,15 +187,14 @@ class HabitipySensor(SensorEntity):
def __init__( def __init__(
self, self,
coordinator, coordinator: HabiticaDataUpdateCoordinator,
entity_description: HabitipySensorEntityDescription, entity_description: HabitipySensorEntityDescription,
entry: ConfigEntry, entry: ConfigEntry,
) -> None: ) -> None:
"""Initialize a generic Habitica sensor.""" """Initialize a generic Habitica sensor."""
super().__init__() super().__init__(coordinator, context=entity_description.value_path[0])
if TYPE_CHECKING: if TYPE_CHECKING:
assert entry.unique_id assert entry.unique_id
self.coordinator = coordinator
self.entity_description = entity_description self.entity_description = entity_description
self._attr_unique_id = f"{entry.unique_id}_{entity_description.key}" self._attr_unique_id = f"{entry.unique_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
@ -263,25 +206,27 @@ class HabitipySensor(SensorEntity):
identifiers={(DOMAIN, entry.unique_id)}, identifiers={(DOMAIN, entry.unique_id)},
) )
async def async_update(self) -> None: @property
"""Update Sensor state.""" def native_value(self) -> StateType:
await self.coordinator.update() """Return the state of the device."""
data = self.coordinator.data data = self.coordinator.data.user
for element in self.entity_description.value_path: for element in self.entity_description.value_path:
data = data[element] data = data[element]
self._attr_native_value = data return cast(StateType, data)
class HabitipyTaskSensor(SensorEntity): class HabitipyTaskSensor(
CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity
):
"""A Habitica task sensor.""" """A Habitica task sensor."""
def __init__(self, name, task_name, updater, entry): def __init__(self, name, task_name, coordinator, entry):
"""Initialize a generic Habitica task.""" """Initialize a generic Habitica task."""
super().__init__(coordinator)
self._name = name self._name = name
self._task_name = task_name self._task_name = task_name
self._task_type = TASKS_TYPES[task_name] self._task_type = TASKS_TYPES[task_name]
self._state = None self._state = None
self._updater = updater
self._attr_unique_id = f"{entry.unique_id}_{task_name}" self._attr_unique_id = f"{entry.unique_id}_{task_name}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
@ -292,14 +237,6 @@ class HabitipyTaskSensor(SensorEntity):
identifiers={(DOMAIN, entry.unique_id)}, identifiers={(DOMAIN, entry.unique_id)},
) )
async def async_update(self) -> None:
"""Update Condition and Forecast."""
await self._updater.update()
all_tasks = self._updater.tasks
for element in self._task_type.path:
tasks_length = len(all_tasks[element])
self._state = tasks_length
@property @property
def icon(self): def icon(self):
"""Return the icon to use in the frontend, if any.""" """Return the icon to use in the frontend, if any."""
@ -313,26 +250,29 @@ class HabitipyTaskSensor(SensorEntity):
@property @property
def native_value(self): def native_value(self):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return len(
[
task
for task in self.coordinator.data.tasks
if task.get("type") in self._task_type.path
]
)
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes of all user tasks.""" """Return the state attributes of all user tasks."""
if self._updater.tasks is not None: attrs = {}
all_received_tasks = self._updater.tasks
for element in self._task_type.path:
received_tasks = all_received_tasks[element]
attrs = {}
# Map tasks to TASKS_MAP # Map tasks to TASKS_MAP
for received_task in received_tasks: for received_task in self.coordinator.data.tasks:
if received_task.get("type") in self._task_type.path:
task_id = received_task[TASKS_MAP_ID] task_id = received_task[TASKS_MAP_ID]
task = {} task = {}
for map_key, map_value in TASKS_MAP.items(): for map_key, map_value in TASKS_MAP.items():
if value := received_task.get(map_value): if value := received_task.get(map_value):
task[map_key] = value task[map_key] = value
attrs[task_id] = task attrs[task_id] = task
return attrs return attrs
@property @property
def native_unit_of_measurement(self): def native_unit_of_measurement(self):

View File

@ -59,6 +59,11 @@
} }
} }
}, },
"exceptions": {
"setup_rate_limit_exception": {
"message": "Currently rate limited, try again later"
}
},
"services": { "services": {
"api_call": { "api_call": {
"name": "API name", "name": "API name",

View File

@ -35,7 +35,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -99,7 +103,7 @@ async def async_setup_entry(
heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE) heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE)
data: HoneywellData = hass.data[DOMAIN][entry.entry_id] data: HoneywellData = hass.data[DOMAIN][entry.entry_id]
_async_migrate_unique_id(hass, data.devices)
async_add_entities( async_add_entities(
[ [
HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp) HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp)
@ -109,6 +113,21 @@ async def async_setup_entry(
remove_stale_devices(hass, entry, data.devices) remove_stale_devices(hass, entry, data.devices)
def _async_migrate_unique_id(
hass: HomeAssistant, devices: dict[str, SomeComfortDevice]
) -> None:
"""Migrate entities to string."""
entity_registry = er.async_get(hass)
for device in devices.values():
entity_id = entity_registry.async_get_entity_id(
"climate", DOMAIN, device.deviceid
)
if entity_id is not None:
entity_registry.async_update_entity(
entity_id, new_unique_id=str(device.deviceid)
)
def remove_stale_devices( def remove_stale_devices(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
@ -161,7 +180,7 @@ class HoneywellUSThermostat(ClimateEntity):
self._away = False self._away = False
self._retry = 0 self._retry = 0
self._attr_unique_id = device.deviceid self._attr_unique_id = str(device.deviceid)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.deviceid)}, identifiers={(DOMAIN, device.deviceid)},

View File

@ -75,7 +75,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_FOLDER, default="INBOX"): str,
vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str,
# The default for new entries is to not include text and headers # The default for new entries is to not include text and headers
vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): cv.ensure_list, vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): EVENT_MESSAGE_DATA_SELECTOR,
} }
) )
CONFIG_SCHEMA_ADVANCED = { CONFIG_SCHEMA_ADVANCED = {

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["imgw_pib==1.0.0"] "requirements": ["imgw_pib==1.0.1"]
} }

View File

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
from homeassistant.components.notify import NotifyEntity from homeassistant.components.notify import NotifyEntity, NotifyEntityFeature
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -25,6 +25,12 @@ async def async_setup_entry(
device_name="MyBox", device_name="MyBox",
entity_name="Personal notifier", entity_name="Personal notifier",
), ),
DemoNotify(
unique_id="just_notify_me_title",
device_name="MyBox",
entity_name="Personal notifier with title",
supported_features=NotifyEntityFeature.TITLE,
),
] ]
) )
@ -40,15 +46,19 @@ class DemoNotify(NotifyEntity):
unique_id: str, unique_id: str,
device_name: str, device_name: str,
entity_name: str | None, entity_name: str | None,
supported_features: NotifyEntityFeature = NotifyEntityFeature(0),
) -> None: ) -> None:
"""Initialize the Demo button entity.""" """Initialize the Demo button entity."""
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
self._attr_supported_features = supported_features
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)}, identifiers={(DOMAIN, unique_id)},
name=device_name, name=device_name,
) )
self._attr_name = entity_name self._attr_name = entity_name
async def async_send_message(self, message: str) -> None: async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send out a persistent notification.""" """Send out a persistent notification."""
persistent_notification.async_create(self.hass, message, "Demo notification") persistent_notification.async_create(
self.hass, message, title or "Demo notification"
)

View File

@ -108,6 +108,6 @@ class KNXNotify(KnxEntity, NotifyEntity):
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address) self._attr_unique_id = str(self._device.remote_value.group_address)
async def async_send_message(self, message: str) -> None: async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a notification to knx bus.""" """Send a notification to knx bus."""
await self._device.set(message) await self._device.set(message)

View File

@ -10,19 +10,18 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import CONF_STORAGE_KEY, CONF_TODO_LIST_NAME, DOMAIN from .const import CONF_STORAGE_KEY, CONF_TODO_LIST_NAME
from .store import LocalTodoListStore from .store import LocalTodoListStore
PLATFORMS: list[Platform] = [Platform.TODO] PLATFORMS: list[Platform] = [Platform.TODO]
STORAGE_PATH = ".storage/local_todo.{key}.ics" STORAGE_PATH = ".storage/local_todo.{key}.ics"
LocalTodoConfigEntry = ConfigEntry[LocalTodoListStore]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: LocalTodoConfigEntry) -> bool:
"""Set up Local To-do from a config entry.""" """Set up Local To-do from a config entry."""
hass.data.setdefault(DOMAIN, {})
path = Path(hass.config.path(STORAGE_PATH.format(key=entry.data[CONF_STORAGE_KEY]))) path = Path(hass.config.path(STORAGE_PATH.format(key=entry.data[CONF_STORAGE_KEY])))
store = LocalTodoListStore(hass, path) store = LocalTodoListStore(hass, path)
try: try:
@ -30,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except OSError as err: except OSError as err:
raise ConfigEntryNotReady("Failed to load file {path}: {err}") from err raise ConfigEntryNotReady("Failed to load file {path}: {err}") from err
hass.data[DOMAIN][entry.entry_id] = store entry.runtime_data = store
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -39,10 +38,7 @@ 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 unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -14,14 +14,14 @@ from homeassistant.components.todo import (
TodoListEntity, TodoListEntity,
TodoListEntityFeature, TodoListEntityFeature,
) )
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.setup import SetupPhases, async_pause_setup from homeassistant.setup import SetupPhases, async_pause_setup
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import CONF_TODO_LIST_NAME, DOMAIN from . import LocalTodoConfigEntry
from .const import CONF_TODO_LIST_NAME
from .store import LocalTodoListStore from .store import LocalTodoListStore
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,12 +63,12 @@ def _migrate_calendar(calendar: Calendar) -> bool:
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LocalTodoConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the local_todo todo platform.""" """Set up the local_todo todo platform."""
store: LocalTodoListStore = hass.data[DOMAIN][config_entry.entry_id] store = config_entry.runtime_data
ics = await store.async_load() ics = await store.async_load()
with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES):

View File

@ -106,4 +106,4 @@ class LutronEventEntity(LutronKeypad, EventEntity):
} }
self.hass.bus.fire("lutron_event", data) self.hass.bus.fire("lutron_event", data)
self._trigger_event(action) self._trigger_event(action)
self.async_write_ha_state() self.schedule_update_ha_state()

View File

@ -398,6 +398,8 @@ class MatterLight(MatterEntity, LightEntity):
def _check_transition_blocklist(self) -> None: def _check_transition_blocklist(self) -> None:
"""Check if this device is reported to have non working transitions.""" """Check if this device is reported to have non working transitions."""
device_info = self._endpoint.device_info device_info = self._endpoint.device_info
if isinstance(device_info, clusters.BridgedDeviceBasicInformation):
return
if ( if (
device_info.vendorID, device_info.vendorID,
device_info.productID, device_info.productID,

View File

@ -21,8 +21,12 @@ PLATFORMS = [Platform.WEATHER]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MetWeatherConfigEntry = ConfigEntry[MetDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, config_entry: MetWeatherConfigEntry
) -> bool:
"""Set up Met as config entry.""" """Set up Met as config entry."""
# Don't setup if tracking home location and latitude or longitude isn't set. # Don't setup if tracking home location and latitude or longitude isn't set.
# Also, filters out our onboarding default location. # Also, filters out our onboarding default location.
@ -44,10 +48,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
if config_entry.data.get(CONF_TRACK_HOME, False): if config_entry.data.get(CONF_TRACK_HOME, False):
coordinator.track_home() coordinator.track_home()
hass.data.setdefault(DOMAIN, {}) config_entry.runtime_data = coordinator
hass.data[DOMAIN][config_entry.entry_id] = coordinator
config_entry.async_on_unload(config_entry.add_update_listener(async_update_entry)) config_entry.async_on_unload(config_entry.add_update_listener(async_update_entry))
config_entry.async_on_unload(coordinator.untrack_home)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
@ -56,19 +60,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, config_entry: MetWeatherConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
config_entry, PLATFORMS
)
hass.data[DOMAIN][config_entry.entry_id].untrack_home()
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_update_entry(hass: HomeAssistant, config_entry: MetWeatherConfigEntry):
"""Reload Met component when options changed.""" """Reload Met component when options changed."""
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)

View File

@ -21,7 +21,6 @@ from homeassistant.components.weather import (
SingleCoordinatorWeatherEntity, SingleCoordinatorWeatherEntity,
WeatherEntityFeature, WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_LATITUDE, CONF_LATITUDE,
CONF_LONGITUDE, CONF_LONGITUDE,
@ -37,6 +36,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.unit_system import METRIC_SYSTEM
from . import MetWeatherConfigEntry
from .const import ( from .const import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY, ATTR_CONDITION_SUNNY,
@ -53,11 +53,11 @@ DEFAULT_NAME = "Met.no"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: MetWeatherConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add a weather entity from a config_entry.""" """Add a weather entity from a config_entry."""
coordinator: MetDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
name: str | None name: str | None
@ -120,7 +120,7 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
def __init__( def __init__(
self, self,
coordinator: MetDataUpdateCoordinator, coordinator: MetDataUpdateCoordinator,
config_entry: ConfigEntry, config_entry: MetWeatherConfigEntry,
name: str, name: str,
is_metric: bool, is_metric: bool,
) -> None: ) -> None:

View File

@ -91,8 +91,6 @@ class MetOfficeWeather(
CoordinatorWeatherEntity[ CoordinatorWeatherEntity[
TimestampDataUpdateCoordinator[MetOfficeData], TimestampDataUpdateCoordinator[MetOfficeData],
TimestampDataUpdateCoordinator[MetOfficeData], TimestampDataUpdateCoordinator[MetOfficeData],
TimestampDataUpdateCoordinator[MetOfficeData],
TimestampDataUpdateCoordinator[MetOfficeData], # Can be removed in Python 3.12
] ]
): ):
"""Implementation of a Met Office weather condition.""" """Implementation of a Met Office weather condition."""

View File

@ -218,8 +218,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_reauth( async def async_step_reauth(
self, entry_data: Mapping[str, Any] self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle re-authentication with Aladdin Connect.""" """Handle re-authentication with MQTT broker."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()

View File

@ -83,7 +83,7 @@ class MqttNotify(MqttEntity, NotifyEntity):
async def _subscribe_topics(self) -> None: async def _subscribe_topics(self) -> None:
"""(Re)Subscribe to topics.""" """(Re)Subscribe to topics."""
async def async_send_message(self, message: str) -> None: async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message.""" """Send a message."""
payload = self._command_template(message) payload = self._command_template(message)
await self.async_publish( await self.async_publish(

View File

@ -2,17 +2,14 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from typing import cast from typing import TYPE_CHECKING
from aiohttp.client_exceptions import ClientConnectorError, ClientError from aiohttp.client_exceptions import ClientConnectorError, ClientError
from nettigo_air_monitor import ( from nettigo_air_monitor import (
ApiError, ApiError,
AuthFailedError, AuthFailedError,
ConnectionOptions, ConnectionOptions,
InvalidSensorDataError,
NAMSensors,
NettigoAirMonitor, NettigoAirMonitor,
) )
@ -21,25 +18,20 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import ATTR_SDS011, ATTR_SPS30, DOMAIN
ATTR_SDS011, from .coordinator import NAMDataUpdateCoordinator
ATTR_SPS30,
DEFAULT_UPDATE_INTERVAL,
DOMAIN,
MANUFACTURER,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BUTTON, Platform.SENSOR] PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
NAMConfigEntry = ConfigEntry[NAMDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool:
"""Set up Nettigo as config entry.""" """Set up Nettigo as config entry."""
host: str = entry.data[CONF_HOST] host: str = entry.data[CONF_HOST]
username: str | None = entry.data.get(CONF_USERNAME) username: str | None = entry.data.get(CONF_USERNAME)
@ -60,11 +52,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except AuthFailedError as err: except AuthFailedError as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
if TYPE_CHECKING:
assert entry.unique_id
coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id) coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -81,57 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]): # pylint: disable=hass-enforce-coordinator-module
"""Class to manage fetching Nettigo Air Monitor data."""
def __init__(
self,
hass: HomeAssistant,
nam: NettigoAirMonitor,
unique_id: str | None,
) -> None:
"""Initialize."""
self._unique_id = unique_id
self.nam = nam
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
)
async def _async_update_data(self) -> NAMSensors:
"""Update data via library."""
try:
async with asyncio.timeout(10):
data = await self.nam.async_update()
# We do not need to catch AuthFailed exception here because sensor data is
# always available without authorization.
except (ApiError, ClientConnectorError, InvalidSensorDataError) as error:
raise UpdateFailed(error) from error
return data
@property
def unique_id(self) -> str | None:
"""Return a unique_id."""
return self._unique_id
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, cast(str, self._unique_id))},
name="Nettigo Air Monitor",
sw_version=self.nam.software_version,
manufacturer=MANUFACTURER,
configuration_url=f"http://{self.nam.host}/",
)

View File

@ -9,14 +9,12 @@ from homeassistant.components.button import (
ButtonEntity, ButtonEntity,
ButtonEntityDescription, ButtonEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
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 . import NAMDataUpdateCoordinator from . import NAMConfigEntry, NAMDataUpdateCoordinator
from .const import DOMAIN
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -30,10 +28,10 @@ RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: NAMConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Add a Nettigo Air Monitor entities from a config_entry.""" """Add a Nettigo Air Monitor entities from a config_entry."""
coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
buttons: list[NAMButton] = [] buttons: list[NAMButton] = []
buttons.append(NAMButton(coordinator, RESTART_BUTTON)) buttons.append(NAMButton(coordinator, RESTART_BUTTON))

View File

@ -0,0 +1,57 @@
"""The Nettigo Air Monitor coordinator."""
import asyncio
import logging
from aiohttp.client_exceptions import ClientConnectorError
from nettigo_air_monitor import (
ApiError,
InvalidSensorDataError,
NAMSensors,
NettigoAirMonitor,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]):
"""Class to manage fetching Nettigo Air Monitor data."""
def __init__(
self,
hass: HomeAssistant,
nam: NettigoAirMonitor,
unique_id: str,
) -> None:
"""Initialize."""
self.unique_id = unique_id
self.device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, unique_id)},
name="Nettigo Air Monitor",
sw_version=nam.software_version,
manufacturer=MANUFACTURER,
configuration_url=f"http://{nam.host}/",
)
self.nam = nam
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
)
async def _async_update_data(self) -> NAMSensors:
"""Update data via library."""
try:
async with asyncio.timeout(10):
data = await self.nam.async_update()
# We do not need to catch AuthFailed exception here because sensor data is
# always available without authorization.
except (ApiError, ClientConnectorError, InvalidSensorDataError) as error:
raise UpdateFailed(error) from error
return data

View File

@ -6,21 +6,19 @@ from dataclasses import asdict
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import NAMDataUpdateCoordinator from . import NAMConfigEntry
from .const import DOMAIN
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: NAMConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
return { return {
"info": async_redact_data(config_entry.data, TO_REDACT), "info": async_redact_data(config_entry.data, TO_REDACT),

View File

@ -8,7 +8,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["nettigo_air_monitor"], "loggers": ["nettigo_air_monitor"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["nettigo-air-monitor==3.0.0"], "requirements": ["nettigo-air-monitor==3.0.1"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
@ -33,7 +32,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import NAMDataUpdateCoordinator from . import NAMConfigEntry, NAMDataUpdateCoordinator
from .const import ( from .const import (
ATTR_BME280_HUMIDITY, ATTR_BME280_HUMIDITY,
ATTR_BME280_PRESSURE, ATTR_BME280_PRESSURE,
@ -347,10 +346,10 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: NAMConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Add a Nettigo Air Monitor entities from a config_entry.""" """Add a Nettigo Air Monitor entities from a config_entry."""
coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
# Due to the change of the attribute name of two sensors, it is necessary to migrate # Due to the change of the attribute name of two sensors, it is necessary to migrate
# the unique_ids to the new names. # the unique_ids to the new names.

View File

@ -30,8 +30,10 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NextcloudConfigEntry = ConfigEntry[NextcloudDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: NextcloudConfigEntry) -> bool:
"""Set up the Nextcloud integration.""" """Set up the Nextcloud integration."""
# migrate old entity unique ids # migrate old entity unique ids
@ -71,17 +73,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: NextcloudConfigEntry) -> bool:
"""Unload Nextcloud integration.""" """Unload Nextcloud integration."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return unload_ok

View File

@ -8,13 +8,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
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 .const import DOMAIN from . import NextcloudConfigEntry
from .coordinator import NextcloudDataUpdateCoordinator
from .entity import NextcloudEntity from .entity import NextcloudEntity
BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [ BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [
@ -54,10 +52,12 @@ BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: NextcloudConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Nextcloud binary sensors.""" """Set up the Nextcloud binary sensors."""
coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
NextcloudBinarySensor(coordinator, entry, sensor) NextcloudBinarySensor(coordinator, entry, sensor)
for sensor in BINARY_SENSORS for sensor in BINARY_SENSORS

View File

@ -2,11 +2,11 @@
from urllib.parse import urlparse from urllib.parse import urlparse
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import NextcloudConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .coordinator import NextcloudDataUpdateCoordinator from .coordinator import NextcloudDataUpdateCoordinator
@ -19,7 +19,7 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]):
def __init__( def __init__(
self, self,
coordinator: NextcloudDataUpdateCoordinator, coordinator: NextcloudDataUpdateCoordinator,
entry: ConfigEntry, entry: NextcloudConfigEntry,
description: EntityDescription, description: EntityDescription,
) -> None: ) -> None:
"""Initialize the Nextcloud sensor.""" """Initialize the Nextcloud sensor."""

View File

@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
EntityCategory, EntityCategory,
@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from .const import DOMAIN from . import NextcloudConfigEntry
from .coordinator import NextcloudDataUpdateCoordinator
from .entity import NextcloudEntity from .entity import NextcloudEntity
UNIT_OF_LOAD: Final[str] = "load" UNIT_OF_LOAD: Final[str] = "load"
@ -602,10 +600,12 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: NextcloudConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Nextcloud sensors.""" """Set up the Nextcloud sensors."""
coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
NextcloudSensor(coordinator, entry, sensor) NextcloudSensor(coordinator, entry, sensor)
for sensor in SENSORS for sensor in SENSORS

View File

@ -3,20 +3,20 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components.update import UpdateEntity, UpdateEntityDescription from homeassistant.components.update import UpdateEntity, UpdateEntityDescription
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 .const import DOMAIN from . import NextcloudConfigEntry
from .coordinator import NextcloudDataUpdateCoordinator
from .entity import NextcloudEntity from .entity import NextcloudEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: NextcloudConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Nextcloud update entity.""" """Set up the Nextcloud update entity."""
coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
if coordinator.data.get("update_available") is None: if coordinator.data.get("update_available") is None:
return return
async_add_entities( async_add_entities(

View File

@ -3,10 +3,21 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from nextdns import ApiError, NextDns from nextdns import (
AnalyticsDnssec,
AnalyticsEncryption,
AnalyticsIpVersions,
AnalyticsProtocols,
AnalyticsStatus,
ApiError,
ConnectionStatus,
NextDns,
Settings,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform from homeassistant.const import CONF_API_KEY, Platform
@ -23,7 +34,6 @@ from .const import (
ATTR_SETTINGS, ATTR_SETTINGS,
ATTR_STATUS, ATTR_STATUS,
CONF_PROFILE_ID, CONF_PROFILE_ID,
DOMAIN,
UPDATE_INTERVAL_ANALYTICS, UPDATE_INTERVAL_ANALYTICS,
UPDATE_INTERVAL_CONNECTION, UPDATE_INTERVAL_CONNECTION,
UPDATE_INTERVAL_SETTINGS, UPDATE_INTERVAL_SETTINGS,
@ -39,6 +49,22 @@ from .coordinator import (
NextDnsUpdateCoordinator, NextDnsUpdateCoordinator,
) )
NextDnsConfigEntry = ConfigEntry["NextDnsData"]
@dataclass
class NextDnsData:
"""Data for the NextDNS integration."""
connection: NextDnsUpdateCoordinator[ConnectionStatus]
dnssec: NextDnsUpdateCoordinator[AnalyticsDnssec]
encryption: NextDnsUpdateCoordinator[AnalyticsEncryption]
ip_versions: NextDnsUpdateCoordinator[AnalyticsIpVersions]
protocols: NextDnsUpdateCoordinator[AnalyticsProtocols]
settings: NextDnsUpdateCoordinator[Settings]
status: NextDnsUpdateCoordinator[AnalyticsStatus]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [ COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [
(ATTR_CONNECTION, NextDnsConnectionUpdateCoordinator, UPDATE_INTERVAL_CONNECTION), (ATTR_CONNECTION, NextDnsConnectionUpdateCoordinator, UPDATE_INTERVAL_CONNECTION),
@ -51,7 +77,7 @@ COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [
] ]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> bool:
"""Set up NextDNS as config entry.""" """Set up NextDNS as config entry."""
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
profile_id = entry.data[CONF_PROFILE_ID] profile_id = entry.data[CONF_PROFILE_ID]
@ -75,18 +101,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators entry.runtime_data = NextDnsData(**coordinators)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok: bool = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Generic
from nextdns import ConnectionStatus from nextdns import ConnectionStatus
@ -13,36 +12,33 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
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 .const import ATTR_CONNECTION, DOMAIN from . import NextDnsConfigEntry
from .coordinator import CoordinatorDataT, NextDnsConnectionUpdateCoordinator from .coordinator import NextDnsUpdateCoordinator
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class NextDnsBinarySensorEntityDescription( class NextDnsBinarySensorEntityDescription(BinarySensorEntityDescription):
BinarySensorEntityDescription, Generic[CoordinatorDataT]
):
"""NextDNS binary sensor entity description.""" """NextDNS binary sensor entity description."""
state: Callable[[CoordinatorDataT, str], bool] state: Callable[[ConnectionStatus, str], bool]
SENSORS = ( SENSORS = (
NextDnsBinarySensorEntityDescription[ConnectionStatus]( NextDnsBinarySensorEntityDescription(
key="this_device_nextdns_connection_status", key="this_device_nextdns_connection_status",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
translation_key="device_connection_status", translation_key="device_connection_status",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
state=lambda data, _: data.connected, state=lambda data, _: data.connected,
), ),
NextDnsBinarySensorEntityDescription[ConnectionStatus]( NextDnsBinarySensorEntityDescription(
key="this_device_profile_connection_status", key="this_device_profile_connection_status",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
translation_key="device_profile_connection_status", translation_key="device_profile_connection_status",
@ -54,13 +50,11 @@ SENSORS = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add NextDNS entities from a config_entry.""" """Add NextDNS entities from a config_entry."""
coordinator: NextDnsConnectionUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ coordinator = entry.runtime_data.connection
ATTR_CONNECTION
]
async_add_entities( async_add_entities(
NextDnsBinarySensor(coordinator, description) for description in SENSORS NextDnsBinarySensor(coordinator, description) for description in SENSORS
@ -68,7 +62,7 @@ async def async_setup_entry(
class NextDnsBinarySensor( class NextDnsBinarySensor(
CoordinatorEntity[NextDnsConnectionUpdateCoordinator], BinarySensorEntity CoordinatorEntity[NextDnsUpdateCoordinator[ConnectionStatus]], BinarySensorEntity
): ):
"""Define an NextDNS binary sensor.""" """Define an NextDNS binary sensor."""
@ -77,7 +71,7 @@ class NextDnsBinarySensor(
def __init__( def __init__(
self, self,
coordinator: NextDnsConnectionUpdateCoordinator, coordinator: NextDnsUpdateCoordinator[ConnectionStatus],
description: NextDnsBinarySensorEntityDescription, description: NextDnsBinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""

View File

@ -2,15 +2,16 @@
from __future__ import annotations from __future__ import annotations
from nextdns import AnalyticsStatus
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
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 .const import ATTR_STATUS, DOMAIN from . import NextDnsConfigEntry
from .coordinator import NextDnsStatusUpdateCoordinator from .coordinator import NextDnsUpdateCoordinator
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -22,27 +23,26 @@ CLEAR_LOGS_BUTTON = ButtonEntityDescription(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add aNextDNS entities from a config_entry.""" """Add aNextDNS entities from a config_entry."""
coordinator: NextDnsStatusUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ coordinator = entry.runtime_data.status
ATTR_STATUS
]
buttons: list[NextDnsButton] = [] async_add_entities([NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)])
buttons.append(NextDnsButton(coordinator, CLEAR_LOGS_BUTTON))
async_add_entities(buttons)
class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity): class NextDnsButton(
CoordinatorEntity[NextDnsUpdateCoordinator[AnalyticsStatus]], ButtonEntity
):
"""Define an NextDNS button.""" """Define an NextDNS button."""
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: NextDnsStatusUpdateCoordinator, coordinator: NextDnsUpdateCoordinator[AnalyticsStatus],
description: ButtonEntityDescription, description: ButtonEntityDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""

View File

@ -6,36 +6,25 @@ from dataclasses import asdict
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import ( from . import NextDnsConfigEntry
ATTR_DNSSEC, from .const import CONF_PROFILE_ID
ATTR_ENCRYPTION,
ATTR_IP_VERSIONS,
ATTR_PROTOCOLS,
ATTR_SETTINGS,
ATTR_STATUS,
CONF_PROFILE_ID,
DOMAIN,
)
TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID} TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: NextDnsConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinators = hass.data[DOMAIN][config_entry.entry_id] dnssec_coordinator = config_entry.runtime_data.dnssec
encryption_coordinator = config_entry.runtime_data.encryption
dnssec_coordinator = coordinators[ATTR_DNSSEC] ip_versions_coordinator = config_entry.runtime_data.ip_versions
encryption_coordinator = coordinators[ATTR_ENCRYPTION] protocols_coordinator = config_entry.runtime_data.protocols
ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS] settings_coordinator = config_entry.runtime_data.settings
protocols_coordinator = coordinators[ATTR_PROTOCOLS] status_coordinator = config_entry.runtime_data.status
settings_coordinator = coordinators[ATTR_SETTINGS]
status_coordinator = coordinators[ATTR_STATUS]
return { return {
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),

View File

@ -19,20 +19,19 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import NextDnsConfigEntry
from .const import ( from .const import (
ATTR_DNSSEC, ATTR_DNSSEC,
ATTR_ENCRYPTION, ATTR_ENCRYPTION,
ATTR_IP_VERSIONS, ATTR_IP_VERSIONS,
ATTR_PROTOCOLS, ATTR_PROTOCOLS,
ATTR_STATUS, ATTR_STATUS,
DOMAIN,
) )
from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator
@ -301,14 +300,14 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add a NextDNS entities from a config_entry.""" """Add a NextDNS entities from a config_entry."""
coordinators = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
NextDnsSensor(coordinators[description.coordinator_type], description) NextDnsSensor(
getattr(entry.runtime_data, description.coordinator_type), description
)
for description in SENSORS for description in SENSORS
) )

View File

@ -4,518 +4,515 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Generic from typing import Any
from aiohttp import ClientError from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from nextdns import ApiError, Settings from nextdns import ApiError, Settings
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
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.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_SETTINGS, DOMAIN from . import NextDnsConfigEntry
from .coordinator import CoordinatorDataT, NextDnsSettingsUpdateCoordinator from .coordinator import NextDnsUpdateCoordinator
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class NextDnsSwitchEntityDescription( class NextDnsSwitchEntityDescription(SwitchEntityDescription):
SwitchEntityDescription, Generic[CoordinatorDataT]
):
"""NextDNS switch entity description.""" """NextDNS switch entity description."""
state: Callable[[CoordinatorDataT], bool] state: Callable[[Settings], bool]
SWITCHES = ( SWITCHES = (
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_page", key="block_page",
translation_key="block_page", translation_key="block_page",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_page, state=lambda data: data.block_page,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="cache_boost", key="cache_boost",
translation_key="cache_boost", translation_key="cache_boost",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.cache_boost, state=lambda data: data.cache_boost,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="cname_flattening", key="cname_flattening",
translation_key="cname_flattening", translation_key="cname_flattening",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.cname_flattening, state=lambda data: data.cname_flattening,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="anonymized_ecs", key="anonymized_ecs",
translation_key="anonymized_ecs", translation_key="anonymized_ecs",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.anonymized_ecs, state=lambda data: data.anonymized_ecs,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="logs", key="logs",
translation_key="logs", translation_key="logs",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.logs, state=lambda data: data.logs,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="web3", key="web3",
translation_key="web3", translation_key="web3",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.web3, state=lambda data: data.web3,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="allow_affiliate", key="allow_affiliate",
translation_key="allow_affiliate", translation_key="allow_affiliate",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.allow_affiliate, state=lambda data: data.allow_affiliate,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_disguised_trackers", key="block_disguised_trackers",
translation_key="block_disguised_trackers", translation_key="block_disguised_trackers",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_disguised_trackers, state=lambda data: data.block_disguised_trackers,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="ai_threat_detection", key="ai_threat_detection",
translation_key="ai_threat_detection", translation_key="ai_threat_detection",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.ai_threat_detection, state=lambda data: data.ai_threat_detection,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_csam", key="block_csam",
translation_key="block_csam", translation_key="block_csam",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_csam, state=lambda data: data.block_csam,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_ddns", key="block_ddns",
translation_key="block_ddns", translation_key="block_ddns",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_ddns, state=lambda data: data.block_ddns,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_nrd", key="block_nrd",
translation_key="block_nrd", translation_key="block_nrd",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_nrd, state=lambda data: data.block_nrd,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_parked_domains", key="block_parked_domains",
translation_key="block_parked_domains", translation_key="block_parked_domains",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_parked_domains, state=lambda data: data.block_parked_domains,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="cryptojacking_protection", key="cryptojacking_protection",
translation_key="cryptojacking_protection", translation_key="cryptojacking_protection",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.cryptojacking_protection, state=lambda data: data.cryptojacking_protection,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="dga_protection", key="dga_protection",
translation_key="dga_protection", translation_key="dga_protection",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.dga_protection, state=lambda data: data.dga_protection,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="dns_rebinding_protection", key="dns_rebinding_protection",
translation_key="dns_rebinding_protection", translation_key="dns_rebinding_protection",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.dns_rebinding_protection, state=lambda data: data.dns_rebinding_protection,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="google_safe_browsing", key="google_safe_browsing",
translation_key="google_safe_browsing", translation_key="google_safe_browsing",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.google_safe_browsing, state=lambda data: data.google_safe_browsing,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="idn_homograph_attacks_protection", key="idn_homograph_attacks_protection",
translation_key="idn_homograph_attacks_protection", translation_key="idn_homograph_attacks_protection",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.idn_homograph_attacks_protection, state=lambda data: data.idn_homograph_attacks_protection,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="threat_intelligence_feeds", key="threat_intelligence_feeds",
translation_key="threat_intelligence_feeds", translation_key="threat_intelligence_feeds",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.threat_intelligence_feeds, state=lambda data: data.threat_intelligence_feeds,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="typosquatting_protection", key="typosquatting_protection",
translation_key="typosquatting_protection", translation_key="typosquatting_protection",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.typosquatting_protection, state=lambda data: data.typosquatting_protection,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_bypass_methods", key="block_bypass_methods",
translation_key="block_bypass_methods", translation_key="block_bypass_methods",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_bypass_methods, state=lambda data: data.block_bypass_methods,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="safesearch", key="safesearch",
translation_key="safesearch", translation_key="safesearch",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.safesearch, state=lambda data: data.safesearch,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="youtube_restricted_mode", key="youtube_restricted_mode",
translation_key="youtube_restricted_mode", translation_key="youtube_restricted_mode",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
state=lambda data: data.youtube_restricted_mode, state=lambda data: data.youtube_restricted_mode,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_9gag", key="block_9gag",
translation_key="block_9gag", translation_key="block_9gag",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_9gag, state=lambda data: data.block_9gag,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_amazon", key="block_amazon",
translation_key="block_amazon", translation_key="block_amazon",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_amazon, state=lambda data: data.block_amazon,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_bereal", key="block_bereal",
translation_key="block_bereal", translation_key="block_bereal",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_bereal, state=lambda data: data.block_bereal,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_blizzard", key="block_blizzard",
translation_key="block_blizzard", translation_key="block_blizzard",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_blizzard, state=lambda data: data.block_blizzard,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_chatgpt", key="block_chatgpt",
translation_key="block_chatgpt", translation_key="block_chatgpt",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_chatgpt, state=lambda data: data.block_chatgpt,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_dailymotion", key="block_dailymotion",
translation_key="block_dailymotion", translation_key="block_dailymotion",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_dailymotion, state=lambda data: data.block_dailymotion,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_discord", key="block_discord",
translation_key="block_discord", translation_key="block_discord",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_discord, state=lambda data: data.block_discord,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_disneyplus", key="block_disneyplus",
translation_key="block_disneyplus", translation_key="block_disneyplus",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_disneyplus, state=lambda data: data.block_disneyplus,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_ebay", key="block_ebay",
translation_key="block_ebay", translation_key="block_ebay",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_ebay, state=lambda data: data.block_ebay,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_facebook", key="block_facebook",
translation_key="block_facebook", translation_key="block_facebook",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_facebook, state=lambda data: data.block_facebook,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_fortnite", key="block_fortnite",
translation_key="block_fortnite", translation_key="block_fortnite",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_fortnite, state=lambda data: data.block_fortnite,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_google_chat", key="block_google_chat",
translation_key="block_google_chat", translation_key="block_google_chat",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_google_chat, state=lambda data: data.block_google_chat,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_hbomax", key="block_hbomax",
translation_key="block_hbomax", translation_key="block_hbomax",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_hbomax, state=lambda data: data.block_hbomax,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_hulu", key="block_hulu",
name="Block Hulu", name="Block Hulu",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_hulu, state=lambda data: data.block_hulu,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_imgur", key="block_imgur",
translation_key="block_imgur", translation_key="block_imgur",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_imgur, state=lambda data: data.block_imgur,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_instagram", key="block_instagram",
translation_key="block_instagram", translation_key="block_instagram",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_instagram, state=lambda data: data.block_instagram,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_leagueoflegends", key="block_leagueoflegends",
translation_key="block_leagueoflegends", translation_key="block_leagueoflegends",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_leagueoflegends, state=lambda data: data.block_leagueoflegends,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_mastodon", key="block_mastodon",
translation_key="block_mastodon", translation_key="block_mastodon",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_mastodon, state=lambda data: data.block_mastodon,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_messenger", key="block_messenger",
translation_key="block_messenger", translation_key="block_messenger",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_messenger, state=lambda data: data.block_messenger,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_minecraft", key="block_minecraft",
translation_key="block_minecraft", translation_key="block_minecraft",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_minecraft, state=lambda data: data.block_minecraft,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_netflix", key="block_netflix",
translation_key="block_netflix", translation_key="block_netflix",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_netflix, state=lambda data: data.block_netflix,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_pinterest", key="block_pinterest",
translation_key="block_pinterest", translation_key="block_pinterest",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_pinterest, state=lambda data: data.block_pinterest,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_playstation_network", key="block_playstation_network",
translation_key="block_playstation_network", translation_key="block_playstation_network",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_playstation_network, state=lambda data: data.block_playstation_network,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_primevideo", key="block_primevideo",
translation_key="block_primevideo", translation_key="block_primevideo",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_primevideo, state=lambda data: data.block_primevideo,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_reddit", key="block_reddit",
translation_key="block_reddit", translation_key="block_reddit",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_reddit, state=lambda data: data.block_reddit,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_roblox", key="block_roblox",
translation_key="block_roblox", translation_key="block_roblox",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_roblox, state=lambda data: data.block_roblox,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_signal", key="block_signal",
translation_key="block_signal", translation_key="block_signal",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_signal, state=lambda data: data.block_signal,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_skype", key="block_skype",
translation_key="block_skype", translation_key="block_skype",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_skype, state=lambda data: data.block_skype,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_snapchat", key="block_snapchat",
translation_key="block_snapchat", translation_key="block_snapchat",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_snapchat, state=lambda data: data.block_snapchat,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_spotify", key="block_spotify",
translation_key="block_spotify", translation_key="block_spotify",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_spotify, state=lambda data: data.block_spotify,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_steam", key="block_steam",
translation_key="block_steam", translation_key="block_steam",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_steam, state=lambda data: data.block_steam,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_telegram", key="block_telegram",
translation_key="block_telegram", translation_key="block_telegram",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_telegram, state=lambda data: data.block_telegram,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_tiktok", key="block_tiktok",
translation_key="block_tiktok", translation_key="block_tiktok",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_tiktok, state=lambda data: data.block_tiktok,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_tinder", key="block_tinder",
translation_key="block_tinder", translation_key="block_tinder",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_tinder, state=lambda data: data.block_tinder,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_tumblr", key="block_tumblr",
translation_key="block_tumblr", translation_key="block_tumblr",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_tumblr, state=lambda data: data.block_tumblr,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_twitch", key="block_twitch",
translation_key="block_twitch", translation_key="block_twitch",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_twitch, state=lambda data: data.block_twitch,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_twitter", key="block_twitter",
translation_key="block_twitter", translation_key="block_twitter",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_twitter, state=lambda data: data.block_twitter,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_vimeo", key="block_vimeo",
translation_key="block_vimeo", translation_key="block_vimeo",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_vimeo, state=lambda data: data.block_vimeo,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_vk", key="block_vk",
translation_key="block_vk", translation_key="block_vk",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_vk, state=lambda data: data.block_vk,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_whatsapp", key="block_whatsapp",
translation_key="block_whatsapp", translation_key="block_whatsapp",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_whatsapp, state=lambda data: data.block_whatsapp,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_xboxlive", key="block_xboxlive",
translation_key="block_xboxlive", translation_key="block_xboxlive",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_xboxlive, state=lambda data: data.block_xboxlive,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_youtube", key="block_youtube",
translation_key="block_youtube", translation_key="block_youtube",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_youtube, state=lambda data: data.block_youtube,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_zoom", key="block_zoom",
translation_key="block_zoom", translation_key="block_zoom",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_zoom, state=lambda data: data.block_zoom,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_dating", key="block_dating",
translation_key="block_dating", translation_key="block_dating",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_dating, state=lambda data: data.block_dating,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_gambling", key="block_gambling",
translation_key="block_gambling", translation_key="block_gambling",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_gambling, state=lambda data: data.block_gambling,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_online_gaming", key="block_online_gaming",
translation_key="block_online_gaming", translation_key="block_online_gaming",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_online_gaming, state=lambda data: data.block_online_gaming,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_piracy", key="block_piracy",
translation_key="block_piracy", translation_key="block_piracy",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_piracy, state=lambda data: data.block_piracy,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_porn", key="block_porn",
translation_key="block_porn", translation_key="block_porn",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_porn, state=lambda data: data.block_porn,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_social_networks", key="block_social_networks",
translation_key="block_social_networks", translation_key="block_social_networks",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state=lambda data: data.block_social_networks, state=lambda data: data.block_social_networks,
), ),
NextDnsSwitchEntityDescription[Settings]( NextDnsSwitchEntityDescription(
key="block_video_streaming", key="block_video_streaming",
translation_key="block_video_streaming", translation_key="block_video_streaming",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
@ -526,19 +523,21 @@ SWITCHES = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add NextDNS entities from a config_entry.""" """Add NextDNS entities from a config_entry."""
coordinator: NextDnsSettingsUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ coordinator = entry.runtime_data.settings
ATTR_SETTINGS
]
async_add_entities( async_add_entities(
NextDnsSwitch(coordinator, description) for description in SWITCHES NextDnsSwitch(coordinator, description) for description in SWITCHES
) )
class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchEntity): class NextDnsSwitch(
CoordinatorEntity[NextDnsUpdateCoordinator[Settings]], SwitchEntity
):
"""Define an NextDNS switch.""" """Define an NextDNS switch."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -546,7 +545,7 @@ class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchE
def __init__( def __init__(
self, self,
coordinator: NextDnsSettingsUpdateCoordinator, coordinator: NextDnsUpdateCoordinator[Settings],
description: NextDnsSwitchEntityDescription, description: NextDnsSwitchEntityDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from enum import IntFlag
from functools import cached_property, partial from functools import cached_property, partial
import logging import logging
from typing import Any, final, override from typing import Any, final, override
@ -58,6 +59,12 @@ PLATFORM_SCHEMA = vol.Schema(
) )
class NotifyEntityFeature(IntFlag):
"""Supported features of a notify entity."""
TITLE = 1
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the notify services.""" """Set up the notify services."""
@ -73,7 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass) component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass)
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SEND_MESSAGE, SERVICE_SEND_MESSAGE,
{vol.Required(ATTR_MESSAGE): cv.string}, {
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_TITLE): cv.string,
},
"_async_send_message", "_async_send_message",
) )
@ -128,6 +138,7 @@ class NotifyEntity(RestoreEntity):
"""Representation of a notify entity.""" """Representation of a notify entity."""
entity_description: NotifyEntityDescription entity_description: NotifyEntityDescription
_attr_supported_features: NotifyEntityFeature = NotifyEntityFeature(0)
_attr_should_poll = False _attr_should_poll = False
_attr_device_class: None _attr_device_class: None
_attr_state: None = None _attr_state: None = None
@ -162,10 +173,19 @@ class NotifyEntity(RestoreEntity):
self.async_write_ha_state() self.async_write_ha_state()
await self.async_send_message(**kwargs) await self.async_send_message(**kwargs)
def send_message(self, message: str) -> None: def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message.""" """Send a message."""
raise NotImplementedError raise NotImplementedError
async def async_send_message(self, message: str) -> None: async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message.""" """Send a message."""
await self.hass.async_add_executor_job(partial(self.send_message, message)) kwargs: dict[str, Any] = {}
if (
title is not None
and self.supported_features
and self.supported_features & NotifyEntityFeature.TITLE
):
kwargs[ATTR_TITLE] = title
await self.hass.async_add_executor_job(
partial(self.send_message, message, **kwargs)
)

View File

@ -29,6 +29,13 @@ send_message:
required: true required: true
selector: selector:
text: text:
title:
required: false
selector:
text:
filter:
supported_features:
- notify.NotifyEntityFeature.TITLE
persistent_notification: persistent_notification:
fields: fields:

View File

@ -35,6 +35,10 @@
"message": { "message": {
"name": "Message", "name": "Message",
"description": "Your notification message." "description": "Your notification message."
},
"title": {
"name": "Title",
"description": "Title for your notification message."
} }
} }
}, },

View File

@ -26,22 +26,30 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
COORDINATOR,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
INTEGRATION_SUPPORTED_COMMANDS, INTEGRATION_SUPPORTED_COMMANDS,
PLATFORMS, PLATFORMS,
PYNUT_DATA,
PYNUT_UNIQUE_ID,
USER_AVAILABLE_COMMANDS,
) )
NUT_FAKE_SERIAL = ["unknown", "blank"] NUT_FAKE_SERIAL = ["unknown", "blank"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NutConfigEntry = ConfigEntry["NutRuntimeData"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@dataclass
class NutRuntimeData:
"""Runtime data definition."""
coordinator: DataUpdateCoordinator
data: PyNUTData
unique_id: str
user_available_commands: set[str]
async def async_setup_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool:
"""Set up Network UPS Tools (NUT) from a config entry.""" """Set up Network UPS Tools (NUT) from a config entry."""
# strip out the stale options CONF_RESOURCES, # strip out the stale options CONF_RESOURCES,
@ -110,13 +118,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
else: else:
user_available_commands = set() user_available_commands = set()
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = NutRuntimeData(
hass.data[DOMAIN][entry.entry_id] = { coordinator, data, unique_id, user_available_commands
COORDINATOR: coordinator, )
PYNUT_DATA: data,
PYNUT_UNIQUE_ID: unique_id,
USER_AVAILABLE_COMMANDS: user_available_commands,
}
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(
@ -135,9 +139,7 @@ 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 unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -15,15 +15,8 @@ DEFAULT_PORT = 3493
KEY_STATUS = "ups.status" KEY_STATUS = "ups.status"
KEY_STATUS_DISPLAY = "ups.status.display" KEY_STATUS_DISPLAY = "ups.status.display"
COORDINATOR = "coordinator"
DEFAULT_SCAN_INTERVAL = 60 DEFAULT_SCAN_INTERVAL = 60
PYNUT_DATA = "data"
PYNUT_UNIQUE_ID = "unique_id"
USER_AVAILABLE_COMMANDS = "user_available_commands"
STATE_TYPES = { STATE_TYPES = {
"OL": "Online", "OL": "Online",
"OB": "On Battery", "OB": "On Battery",

View File

@ -4,19 +4,15 @@ from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
from homeassistant.core import Context, HomeAssistant from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import PyNUTData from . import NutRuntimeData
from .const import ( from .const import DOMAIN, INTEGRATION_SUPPORTED_COMMANDS
DOMAIN,
INTEGRATION_SUPPORTED_COMMANDS,
PYNUT_DATA,
USER_AVAILABLE_COMMANDS,
)
ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS} ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS}
@ -31,18 +27,15 @@ async def async_get_actions(
hass: HomeAssistant, device_id: str hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]: ) -> list[dict[str, str]]:
"""List device actions for Network UPS Tools (NUT) devices.""" """List device actions for Network UPS Tools (NUT) devices."""
if (entry_id := _get_entry_id_from_device_id(hass, device_id)) is None: if (runtime_data := _get_runtime_data_from_device_id(hass, device_id)) is None:
return [] return []
base_action = { base_action = {
CONF_DEVICE_ID: device_id, CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN, CONF_DOMAIN: DOMAIN,
} }
user_available_commands: set[str] = hass.data[DOMAIN][entry_id][
USER_AVAILABLE_COMMANDS
]
return [ return [
{CONF_TYPE: _get_device_action_name(command_name)} | base_action {CONF_TYPE: _get_device_action_name(command_name)} | base_action
for command_name in user_available_commands for command_name in runtime_data.user_available_commands
] ]
@ -56,9 +49,12 @@ async def async_call_action_from_config(
device_action_name: str = config[CONF_TYPE] device_action_name: str = config[CONF_TYPE]
command_name = _get_command_name(device_action_name) command_name = _get_command_name(device_action_name)
device_id: str = config[CONF_DEVICE_ID] device_id: str = config[CONF_DEVICE_ID]
entry_id = _get_entry_id_from_device_id(hass, device_id) runtime_data = _get_runtime_data_from_device_id(hass, device_id)
data: PyNUTData = hass.data[DOMAIN][entry_id][PYNUT_DATA] if not runtime_data:
await data.async_run_command(command_name) raise InvalidDeviceAutomationConfig(
f"Unable to find a NUT device with id {device_id}"
)
await runtime_data.data.async_run_command(command_name)
def _get_device_action_name(command_name: str) -> str: def _get_device_action_name(command_name: str) -> str:
@ -69,8 +65,14 @@ def _get_command_name(device_action_name: str) -> str:
return device_action_name.replace("_", ".") return device_action_name.replace("_", ".")
def _get_entry_id_from_device_id(hass: HomeAssistant, device_id: str) -> str | None: def _get_runtime_data_from_device_id(
hass: HomeAssistant, device_id: str
) -> NutRuntimeData | None:
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
if (device := device_registry.async_get(device_id)) is None: if (device := device_registry.async_get(device_id)) is None:
return None return None
return next(entry for entry in device.config_entries) entry = hass.config_entries.async_get_entry(
next(entry_id for entry_id in device.config_entries)
)
assert entry and isinstance(entry.runtime_data, NutRuntimeData)
return entry.runtime_data

View File

@ -7,27 +7,26 @@ from typing import Any
import attr import attr
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import PyNUTData from . import NutConfigEntry
from .const import DOMAIN, PYNUT_DATA, PYNUT_UNIQUE_ID, USER_AVAILABLE_COMMANDS from .const import DOMAIN
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: NutConfigEntry
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)}
hass_data = hass.data[DOMAIN][entry.entry_id] hass_data = entry.runtime_data
# Get information from Nut library # Get information from Nut library
nut_data: PyNUTData = hass_data[PYNUT_DATA] nut_data = hass_data.data
nut_cmd: set[str] = hass_data[USER_AVAILABLE_COMMANDS] nut_cmd = hass_data.user_available_commands
data["nut_data"] = { data["nut_data"] = {
"ups_list": nut_data.ups_list, "ups_list": nut_data.ups_list,
"status": nut_data.status, "status": nut_data.status,
@ -38,7 +37,7 @@ async def async_get_config_entry_diagnostics(
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
hass_device = device_registry.async_get_device( hass_device = device_registry.async_get_device(
identifiers={(DOMAIN, hass_data[PYNUT_UNIQUE_ID])} identifiers={(DOMAIN, hass_data.unique_id)}
) )
if not hass_device: if not hass_device:
return data return data

View File

@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_MANUFACTURER, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_MODEL,
@ -36,16 +35,8 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from . import PyNUTData from . import NutConfigEntry, PyNUTData
from .const import ( from .const import DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, STATE_TYPES
COORDINATOR,
DOMAIN,
KEY_STATUS,
KEY_STATUS_DISPLAY,
PYNUT_DATA,
PYNUT_UNIQUE_ID,
STATE_TYPES,
)
NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = { NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = {
"manufacturer": ATTR_MANUFACTURER, "manufacturer": ATTR_MANUFACTURER,
@ -968,15 +959,15 @@ def _get_nut_device_info(data: PyNUTData) -> DeviceInfo:
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: NutConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the NUT sensors.""" """Set up the NUT sensors."""
pynut_data = hass.data[DOMAIN][config_entry.entry_id] pynut_data = config_entry.runtime_data
coordinator = pynut_data[COORDINATOR] coordinator = pynut_data.coordinator
data = pynut_data[PYNUT_DATA] data = pynut_data.data
unique_id = pynut_data[PYNUT_UNIQUE_ID] unique_id = pynut_data.unique_id
status = coordinator.data status = coordinator.data
resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status] resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status]

View File

@ -2,21 +2,18 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
import datetime import datetime
import logging import logging
from typing import TYPE_CHECKING
from pynws import SimpleNWS from pynws import SimpleNWS, call_with_retry
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.helpers import debounce from homeassistant.helpers import debounce
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -27,8 +24,10 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) RETRY_INTERVAL = datetime.timedelta(minutes=1)
DEBOUNCE_TIME = 60 # in seconds RETRY_STOP = datetime.timedelta(minutes=10)
DEBOUNCE_TIME = 10 * 60 # in seconds
def base_unique_id(latitude: float, longitude: float) -> str: def base_unique_id(latitude: float, longitude: float) -> str:
@ -41,62 +40,9 @@ class NWSData:
"""Data for the National Weather Service integration.""" """Data for the National Weather Service integration."""
api: SimpleNWS api: SimpleNWS
coordinator_observation: NwsDataUpdateCoordinator coordinator_observation: TimestampDataUpdateCoordinator[None]
coordinator_forecast: NwsDataUpdateCoordinator coordinator_forecast: TimestampDataUpdateCoordinator[None]
coordinator_forecast_hourly: NwsDataUpdateCoordinator coordinator_forecast_hourly: TimestampDataUpdateCoordinator[None]
class NwsDataUpdateCoordinator(TimestampDataUpdateCoordinator[None]): # pylint: disable=hass-enforce-coordinator-module
"""NWS data update coordinator.
Implements faster data update intervals for failed updates and exposes a last successful update time.
"""
def __init__(
self,
hass: HomeAssistant,
logger: logging.Logger,
*,
name: str,
update_interval: datetime.timedelta,
failed_update_interval: datetime.timedelta,
update_method: Callable[[], Awaitable[None]] | None = None,
request_refresh_debouncer: debounce.Debouncer | None = None,
) -> None:
"""Initialize NWS coordinator."""
super().__init__(
hass,
logger,
name=name,
update_interval=update_interval,
update_method=update_method,
request_refresh_debouncer=request_refresh_debouncer,
)
self.failed_update_interval = failed_update_interval
@callback
def _schedule_refresh(self) -> None:
"""Schedule a refresh."""
if self._unsub_refresh:
self._unsub_refresh()
self._unsub_refresh = None
# We _floor_ utcnow to create a schedule on a rounded second,
# minimizing the time between the point and the real activation.
# That way we obtain a constant update frequency,
# as long as the update process takes less than a second
if self.last_update_success:
if TYPE_CHECKING:
# the base class allows None, but this one doesn't
assert self.update_interval is not None
update_interval = self.update_interval
else:
update_interval = self.failed_update_interval
self._unsub_refresh = async_track_point_in_utc_time(
self.hass,
self._handle_refresh_interval,
utcnow().replace(microsecond=0) + update_interval,
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -114,39 +60,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_observation() -> None: async def update_observation() -> None:
"""Retrieve recent observations.""" """Retrieve recent observations."""
await nws_data.update_observation(start_time=utcnow() - UPDATE_TIME_PERIOD) await call_with_retry(
nws_data.update_observation,
RETRY_INTERVAL,
RETRY_STOP,
start_time=utcnow() - UPDATE_TIME_PERIOD,
)
coordinator_observation = NwsDataUpdateCoordinator( async def update_forecast() -> None:
"""Retrieve twice-daily forecsat."""
await call_with_retry(
nws_data.update_forecast,
RETRY_INTERVAL,
RETRY_STOP,
)
async def update_forecast_hourly() -> None:
"""Retrieve hourly forecast."""
await call_with_retry(
nws_data.update_forecast_hourly,
RETRY_INTERVAL,
RETRY_STOP,
)
coordinator_observation = TimestampDataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
name=f"NWS observation station {station}", name=f"NWS observation station {station}",
update_method=update_observation, update_method=update_observation,
update_interval=DEFAULT_SCAN_INTERVAL, update_interval=DEFAULT_SCAN_INTERVAL,
failed_update_interval=FAILED_SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer( request_refresh_debouncer=debounce.Debouncer(
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
), ),
) )
coordinator_forecast = NwsDataUpdateCoordinator( coordinator_forecast = TimestampDataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
name=f"NWS forecast station {station}", name=f"NWS forecast station {station}",
update_method=nws_data.update_forecast, update_method=update_forecast,
update_interval=DEFAULT_SCAN_INTERVAL, update_interval=DEFAULT_SCAN_INTERVAL,
failed_update_interval=FAILED_SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer( request_refresh_debouncer=debounce.Debouncer(
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
), ),
) )
coordinator_forecast_hourly = NwsDataUpdateCoordinator( coordinator_forecast_hourly = TimestampDataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
name=f"NWS forecast hourly station {station}", name=f"NWS forecast hourly station {station}",
update_method=nws_data.update_forecast_hourly, update_method=update_forecast_hourly,
update_interval=DEFAULT_SCAN_INTERVAL, update_interval=DEFAULT_SCAN_INTERVAL,
failed_update_interval=FAILED_SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer( request_refresh_debouncer=debounce.Debouncer(
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
), ),

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