Compare commits

...

19 Commits

Author SHA1 Message Date
Robert Resch
46f8374063 Use runtime_data in plaato integration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:44:15 +00:00
Robert Resch
109ec0705c Use runtime_data in vilfo integration (#167886)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:54:32 +02:00
epenet
6f7fa85d18 Use runtime_data in system_bridge integration (#167880)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:53:53 +02:00
epenet
8d2564f00f Use runtime_data in soundtouch integration (#167869)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:52:47 +02:00
epenet
f7096e3744 Use runtime_data in srp_energy integration (#167870)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:51:29 +02:00
epenet
d7f28a09bb Use runtime_data in sleepiq integration (#167865)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:50:53 +02:00
epenet
a54ea071f8 Use runtime_data in Slack (#167864)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 11:50:19 +02:00
epenet
1597b740da Use runtime_data in skybell integration (#167862)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:48:48 +02:00
epenet
3758d606c9 Use runtime_data in simplisafe integration (#167858)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:47:58 +02:00
epenet
a79988aca7 Use runtime_data in sia integration (#167857)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:46:43 +02:00
epenet
837cd7d89d Use runtime_data in sanix integration (#167856)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:46:12 +02:00
Retha Runolfsson
038bb6c15d Add child lock and wireless charging switches for air purifier (#167140)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:29:02 +02:00
Paul Bottein
6ccede7f30 Add fabric index fields to Matter lock user and credential responses (#167875) 2026-04-10 11:18:10 +02:00
Abílio Costa
fb541d8835 Replace ding with new ring event in Ring integration doorbell (#167728) 2026-04-10 11:04:52 +02:00
epenet
39a2c08d4e Use runtime_data in switchbot_cloud integration (#167879)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:59:20 +02:00
epenet
ea642980f2 Use runtime_data in switchbee (#167878)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 10:45:04 +02:00
Renaud Allard
4c8ea3669c Load lovelace resource collection eagerly during setup (#165773) 2026-04-10 04:38:17 -04:00
epenet
14f24226ae Use runtime_data in streamlabswater (#167874)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:31:55 +02:00
epenet
3a9f805f10 Use runtime_data in surepetcare integration (#167877)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 10:31:42 +02:00
98 changed files with 989 additions and 629 deletions

View File

@@ -62,14 +62,32 @@ class ResourceStorageCollection(collection.DictStorageCollection):
)
self.ll_config = ll_config
async def async_get_info(self) -> dict[str, int]:
"""Return the resources info for YAML mode."""
async def _async_ensure_loaded(self) -> None:
"""Ensure the collection has been loaded from storage."""
if not self.loaded:
await self.async_load()
self.loaded = True
async def async_get_info(self) -> dict[str, int]:
"""Return the resources info for YAML mode."""
await self._async_ensure_loaded()
return {"resources": len(self.async_items() or [])}
async def async_create_item(self, data: dict) -> dict:
"""Create a new item."""
await self._async_ensure_loaded()
return await super().async_create_item(data)
async def async_update_item(self, item_id: str, updates: dict) -> dict:
"""Update item."""
await self._async_ensure_loaded()
return await super().async_update_item(item_id, updates)
async def async_delete_item(self, item_id: str) -> None:
"""Delete item."""
await self._async_ensure_loaded()
await super().async_delete_item(item_id)
async def _async_load_data(self) -> collection.SerializedStorageCollection | None:
"""Load the data."""
if (store_data := await self.store.async_load()) is not None:
@@ -118,10 +136,6 @@ class ResourceStorageCollection(collection.DictStorageCollection):
async def _update_data(self, item: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
if not self.loaded:
await self.async_load()
self.loaded = True
update_data = self.UPDATE_SCHEMA(update_data)
if CONF_RESOURCE_TYPE_WS in update_data:
update_data[CONF_TYPE] = update_data.pop(CONF_RESOURCE_TYPE_WS)

View File

@@ -71,6 +71,8 @@ class LockUserData(TypedDict):
user_type: str
credential_rule: str
credentials: list[LockUserCredentialData]
creator_fabric_index: int | None
last_modified_fabric_index: int | None
next_user_index: int | None
@@ -115,6 +117,8 @@ class GetLockCredentialStatusResult(TypedDict):
credential_exists: bool
user_index: int | None
creator_fabric_index: int | None
last_modified_fabric_index: int | None
next_credential_index: int | None
@@ -214,6 +218,8 @@ def _format_user_response(user_data: Any) -> LockUserData | None:
_get_attr(user_data, "credentialRule"), "unknown"
),
credentials=credentials,
creator_fabric_index=_get_attr(user_data, "creatorFabricIndex"),
last_modified_fabric_index=_get_attr(user_data, "lastModifiedFabricIndex"),
next_user_index=_get_attr(user_data, "nextUserIndex"),
)
@@ -817,7 +823,8 @@ async def get_lock_credential_status(
) -> GetLockCredentialStatusResult:
"""Get the status of a credential slot on the lock.
Returns typed dict with credential_exists, user_index, next_credential_index.
Returns typed dict with credential_exists, user_index, creator_fabric_index,
last_modified_fabric_index, and next_credential_index.
Raises HomeAssistantError on failure.
"""
lock_endpoint = _get_lock_endpoint_or_raise(node)
@@ -839,5 +846,7 @@ async def get_lock_credential_status(
return GetLockCredentialStatusResult(
credential_exists=bool(_get_attr(response, "credentialExists")),
user_index=_get_attr(response, "userIndex"),
creator_fabric_index=_get_attr(response, "creatorFabricIndex"),
last_modified_fabric_index=_get_attr(response, "lastModifiedFabricIndex"),
next_credential_index=_get_attr(response, "nextCredentialIndex"),
)

View File

@@ -1,10 +1,14 @@
"""Support for Plaato devices."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import timedelta
import logging
from aiohttp import web
from pyplaato.models.airlock import PlaatoAirlock
from pyplaato.models.device import PlaatoDevice
from pyplaato.plaato import (
ATTR_ABV,
ATTR_BATCH_VOLUME,
@@ -39,21 +43,28 @@ from .const import (
CONF_DEVICE_NAME,
CONF_DEVICE_TYPE,
CONF_USE_WEBHOOK,
COORDINATOR,
DEFAULT_SCAN_INTERVAL,
DEVICE,
DEVICE_ID,
DEVICE_NAME,
DEVICE_TYPE,
DOMAIN,
PLATFORMS,
SENSOR_DATA,
UNDO_UPDATE_LISTENER,
)
from .coordinator import PlaatoCoordinator
_LOGGER = logging.getLogger(__name__)
@dataclass
class PlaatoData:
"""Runtime data for the Plaato integration."""
coordinator: PlaatoCoordinator | None
device_name: str
device_type: str
device_id: str | None
sensor_data: PlaatoDevice | None = field(default=None)
type PlaatoConfigEntry = ConfigEntry[PlaatoData]
DEPENDENCIES = ["webhook"]
SENSOR_UPDATE = f"{DOMAIN}_sensor_update"
@@ -82,15 +93,15 @@ WEBHOOK_SCHEMA = vol.Schema(
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: PlaatoConfigEntry) -> bool:
"""Configure based on config entry."""
hass.data.setdefault(DOMAIN, {})
if entry.data[CONF_USE_WEBHOOK]:
async_setup_webhook(hass, entry)
else:
await async_setup_coordinator(hass, entry)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
await hass.config_entries.async_forward_entry_setups(
entry, [platform for platform in PLATFORMS if entry.options.get(platform, True)]
)
@@ -99,19 +110,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@callback
def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry):
def async_setup_webhook(hass: HomeAssistant, entry: PlaatoConfigEntry) -> None:
"""Init webhook based on config entry."""
webhook_id = entry.data[CONF_WEBHOOK_ID]
device_name = entry.data[CONF_DEVICE_NAME]
_set_entry_data(entry, hass)
entry.runtime_data = PlaatoData(
coordinator=None,
device_name=entry.data[CONF_DEVICE_NAME],
device_type=entry.data[CONF_DEVICE_TYPE],
device_id=None,
)
webhook.async_register(
hass, DOMAIN, f"{DOMAIN}.{device_name}", webhook_id, handle_webhook
)
async def async_setup_coordinator(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_coordinator(
hass: HomeAssistant, entry: PlaatoConfigEntry
) -> None:
"""Init auth token based on config entry."""
auth_token = entry.data[CONF_TOKEN]
device_type = entry.data[CONF_DEVICE_TYPE]
@@ -126,62 +144,44 @@ async def async_setup_coordinator(hass: HomeAssistant, entry: ConfigEntry):
)
await coordinator.async_config_entry_first_refresh()
_set_entry_data(entry, hass, coordinator, auth_token)
entry.runtime_data = PlaatoData(
coordinator=coordinator,
device_name=entry.data[CONF_DEVICE_NAME],
device_type=entry.data[CONF_DEVICE_TYPE],
device_id=auth_token,
)
for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
def _set_entry_data(entry, hass, coordinator=None, device_id=None):
device = {
DEVICE_NAME: entry.data[CONF_DEVICE_NAME],
DEVICE_TYPE: entry.data[CONF_DEVICE_TYPE],
DEVICE_ID: device_id,
}
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator,
DEVICE: device,
SENSOR_DATA: None,
UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener),
}
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: PlaatoConfigEntry) -> bool:
"""Unload a config entry."""
use_webhook = entry.data[CONF_USE_WEBHOOK]
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
if use_webhook:
if entry.data[CONF_USE_WEBHOOK]:
return await async_unload_webhook(hass, entry)
return await async_unload_coordinator(hass, entry)
async def async_unload_webhook(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_webhook(hass: HomeAssistant, entry: PlaatoConfigEntry) -> bool:
"""Unload webhook based entry."""
if entry.data[CONF_WEBHOOK_ID] is not None:
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
return await async_unload_platforms(hass, entry, PLATFORMS)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_unload_coordinator(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_coordinator(
hass: HomeAssistant, entry: PlaatoConfigEntry
) -> bool:
"""Unload auth token based entry."""
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
return await async_unload_platforms(hass, entry, coordinator.platforms)
coordinator = entry.runtime_data.coordinator
return await hass.config_entries.async_unload_platforms(
entry, coordinator.platforms if coordinator else PLATFORMS
)
async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platforms):
"""Unload platforms."""
unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)
return unloaded
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def _async_update_listener(hass: HomeAssistant, entry: PlaatoConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -8,17 +8,17 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_USE_WEBHOOK, COORDINATOR, DOMAIN
from . import PlaatoConfigEntry
from .const import CONF_USE_WEBHOOK
from .entity import PlaatoEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: PlaatoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Plaato from a config entry."""
@@ -26,10 +26,12 @@ async def async_setup_entry(
if config_entry.data[CONF_USE_WEBHOOK]:
return
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
entry_data = config_entry.runtime_data
coordinator = entry_data.coordinator
assert coordinator is not None
async_add_entities(
PlaatoBinarySensor(
hass.data[DOMAIN][config_entry.entry_id],
entry_data,
sensor_type,
coordinator,
)

View File

@@ -19,13 +19,7 @@ PLACEHOLDER_DEVICE_TYPE = "device_type"
PLACEHOLDER_DEVICE_NAME = "device_name"
DOCS_URL = "https://www.home-assistant.io/integrations/plaato/"
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
SENSOR_DATA = "sensor_data"
COORDINATOR = "coordinator"
DEVICE = "device"
DEVICE_NAME = "device_name"
DEVICE_TYPE = "device_type"
DEVICE_ID = "device_id"
UNDO_UPDATE_LISTENER = "undo_update_listener"
DEFAULT_SCAN_INTERVAL = 5
MIN_UPDATE_INTERVAL = timedelta(minutes=1)

View File

@@ -1,30 +1,36 @@
"""Coordinator for Plaato devices."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
from pyplaato.models.device import PlaatoDevice
from pyplaato.plaato import Plaato, PlaatoDeviceType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
if TYPE_CHECKING:
from . import PlaatoConfigEntry
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class PlaatoCoordinator(DataUpdateCoordinator):
class PlaatoCoordinator(DataUpdateCoordinator[PlaatoDevice]):
"""Class to manage fetching data from the API."""
config_entry: ConfigEntry
config_entry: PlaatoConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: PlaatoConfigEntry,
auth_token: str,
device_type: PlaatoDeviceType,
update_interval: timedelta,

View File

@@ -1,6 +1,8 @@
"""PlaatoEntity class."""
from typing import Any
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from pyplaato.models.device import PlaatoDevice
@@ -8,16 +10,10 @@ from homeassistant.helpers import entity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
DEVICE,
DEVICE_ID,
DEVICE_NAME,
DEVICE_TYPE,
DOMAIN,
EXTRA_STATE_ATTRIBUTES,
SENSOR_DATA,
SENSOR_SIGNAL,
)
from .const import DOMAIN, EXTRA_STATE_ATTRIBUTES, SENSOR_SIGNAL
if TYPE_CHECKING:
from . import PlaatoData
class PlaatoEntity(entity.Entity):
@@ -25,21 +21,21 @@ class PlaatoEntity(entity.Entity):
_attr_should_poll = False
def __init__(self, data, sensor_type, coordinator=None):
def __init__(self, data: PlaatoData, sensor_type, coordinator=None) -> None:
"""Initialize the sensor."""
self._coordinator = coordinator
self._entry_data = data
self._sensor_type = sensor_type
self._device_id = data[DEVICE][DEVICE_ID]
self._device_type = data[DEVICE][DEVICE_TYPE]
self._device_name = data[DEVICE][DEVICE_NAME]
self._device_id = data.device_id
self._device_type = data.device_type
self._device_name = data.device_name
self._attr_unique_id = f"{self._device_id}_{self._sensor_type}"
self._attr_name = f"{DOMAIN} {self._device_type} {self._device_name} {self._sensor_name}".title()
sw_version = None
if firmware := self._sensor_data.firmware_version:
sw_version = firmware
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._device_id)},
identifiers={(DOMAIN, self._device_id)}, # type: ignore[arg-type]
manufacturer="Plaato",
model=self._device_type,
name=self._device_name,
@@ -58,7 +54,7 @@ class PlaatoEntity(entity.Entity):
def _sensor_data(self) -> PlaatoDevice:
if self._coordinator:
return self._coordinator.data
return self._entry_data[SENSOR_DATA]
return self._entry_data.sensor_data
@property
def extra_state_attributes(self) -> dict[str, Any] | None:

View File

@@ -6,7 +6,6 @@ from pyplaato.models.device import PlaatoDevice
from pyplaato.plaato import PlaatoKeg
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -18,16 +17,8 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import ATTR_TEMP, SENSOR_UPDATE
from .const import (
CONF_USE_WEBHOOK,
COORDINATOR,
DEVICE,
DEVICE_ID,
DOMAIN,
SENSOR_DATA,
SENSOR_SIGNAL,
)
from . import ATTR_TEMP, SENSOR_UPDATE, PlaatoConfigEntry
from .const import CONF_USE_WEBHOOK, SENSOR_SIGNAL
from .entity import PlaatoEntity
@@ -42,19 +33,19 @@ async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: PlaatoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Plaato from a config entry."""
entry_data = hass.data[DOMAIN][entry.entry_id]
entry_data = entry.runtime_data
@callback
def _async_update_from_webhook(device_id, sensor_data: PlaatoDevice):
"""Update/Create the sensors."""
entry_data[SENSOR_DATA] = sensor_data
entry_data.sensor_data = sensor_data
if device_id != entry_data[DEVICE][DEVICE_ID]:
entry_data[DEVICE][DEVICE_ID] = device_id
if device_id != entry_data.device_id:
entry_data.device_id = device_id
async_add_entities(
[
PlaatoSensor(entry_data, sensor_type)
@@ -68,7 +59,8 @@ async def async_setup_entry(
if entry.data[CONF_USE_WEBHOOK]:
async_dispatcher_connect(hass, SENSOR_UPDATE, _async_update_from_webhook)
else:
coordinator = entry_data[COORDINATOR]
coordinator = entry_data.coordinator
assert coordinator is not None
async_add_entities(
PlaatoSensor(entry_data, sensor_type, coordinator)
for sensor_type in coordinator.data.sensors

View File

@@ -7,6 +7,7 @@ from ring_doorbell import RingCapability, RingEvent as RingAlert
from ring_doorbell.const import KIND_DING, KIND_INTERCOM_UNLOCK, KIND_MOTION
from homeassistant.components.event import (
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
@@ -34,7 +35,7 @@ EVENT_DESCRIPTIONS: tuple[RingEventEntityDescription, ...] = (
key=KIND_DING,
translation_key=KIND_DING,
device_class=EventDeviceClass.DOORBELL,
event_types=[KIND_DING],
event_types=[DoorbellEventType.RING],
capability=RingCapability.DING,
),
RingEventEntityDescription(
@@ -100,7 +101,10 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity)
@callback
def _handle_coordinator_update(self) -> None:
if (alert := self._get_coordinator_alert()) and not alert.is_update:
self._async_handle_event(alert.kind)
if alert.kind == KIND_DING:
self._async_handle_event(DoorbellEventType.RING)
else:
self._async_handle_event(alert.kind)
super()._handle_coordinator_update()
@property

View File

@@ -73,7 +73,14 @@
},
"event": {
"ding": {
"name": "Ding"
"name": "Ding",
"state_attributes": {
"event_type": {
"state": {
"ring": "[%key:component::event::entity_component::doorbell::state_attributes::event_type::state::ring%]"
}
}
}
},
"intercom_unlock": {
"name": "Intercom unlock"

View File

@@ -2,17 +2,16 @@
from sanix import Sanix
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from .const import CONF_SERIAL_NUMBER, DOMAIN
from .coordinator import SanixCoordinator
from .const import CONF_SERIAL_NUMBER
from .coordinator import SanixConfigEntry, SanixCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SanixConfigEntry) -> bool:
"""Set up Sanix from a config entry."""
serial_no = entry.data[CONF_SERIAL_NUMBER]
@@ -22,16 +21,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = SanixCoordinator(hass, entry, sanix_api)
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)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SanixConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -15,14 +15,16 @@ from .const import MANUFACTURER
_LOGGER = logging.getLogger(__name__)
type SanixConfigEntry = ConfigEntry[SanixCoordinator]
class SanixCoordinator(DataUpdateCoordinator[Measurement]):
"""Sanix coordinator."""
config_entry: ConfigEntry
config_entry: SanixConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, sanix_api: Sanix
self, hass: HomeAssistant, config_entry: SanixConfigEntry, sanix_api: Sanix
) -> None:
"""Initialize coordinator."""
super().__init__(

View File

@@ -20,7 +20,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfLength
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
@@ -28,7 +27,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .coordinator import SanixCoordinator
from .coordinator import SanixConfigEntry, SanixCoordinator
@dataclass(frozen=True, kw_only=True)
@@ -83,11 +82,11 @@ SENSOR_TYPES: tuple[SanixSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SanixConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sanix Sensor entities based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
SanixSensorEntity(coordinator, description) for description in SENSOR_TYPES

View File

@@ -1,21 +1,18 @@
"""The sia integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, PLATFORMS
from .hub import SIAHub
from .const import PLATFORMS
from .hub import SIAConfigEntry, SIAHub
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SIAConfigEntry) -> bool:
"""Set up sia from a config entry."""
hub: SIAHub = SIAHub(hass, entry)
hub = SIAHub(hass, entry)
hub.async_setup_hub()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = hub
try:
if hub.sia_client:
await hub.sia_client.async_start(reuse_port=True)
@@ -23,14 +20,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady(
f"SIA Server at port {entry.data[CONF_PORT]} could not start."
) from exc
entry.runtime_data = hub
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SIAConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hub: SIAHub = hass.data[DOMAIN].pop(entry.entry_id)
await hub.async_shutdown()
await entry.runtime_data.async_shutdown()
return unload_ok

View File

@@ -16,12 +16,7 @@ from pysiaalarm import (
)
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.const import CONF_PORT, CONF_PROTOCOL
from homeassistant.core import callback
@@ -36,7 +31,7 @@ from .const import (
DOMAIN,
TITLE,
)
from .hub import SIAHub
from .hub import SIAConfigEntry, SIAHub
_LOGGER = logging.getLogger(__name__)
@@ -100,7 +95,7 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: SIAConfigEntry,
) -> SIAOptionsFlowHandler:
"""Get the options flow for this handler."""
return SIAOptionsFlowHandler(config_entry)
@@ -179,7 +174,9 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
class SIAOptionsFlowHandler(OptionsFlow):
"""Handle SIA options."""
def __init__(self, config_entry: ConfigEntry) -> None:
config_entry: SIAConfigEntry
def __init__(self, config_entry: SIAConfigEntry) -> None:
"""Initialize SIA options flow."""
self.options = deepcopy(dict(config_entry.options))
self.hub: SIAHub | None = None
@@ -189,7 +186,7 @@ class SIAOptionsFlowHandler(OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the SIA options."""
self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id]
self.hub = self.config_entry.runtime_data
assert self.hub is not None
assert self.hub.sia_accounts is not None
self.accounts_todo = [a.account_id for a in self.hub.sia_accounts]

View File

@@ -8,7 +8,7 @@ from typing import Any
from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
@@ -28,6 +28,8 @@ from .utils import get_event_data_from_sia_event
_LOGGER = logging.getLogger(__name__)
type SIAConfigEntry = ConfigEntry[SIAHub]
DEFAULT_TIMEBAND = (80, 40)
@@ -37,11 +39,11 @@ class SIAHub:
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
entry: SIAConfigEntry,
) -> None:
"""Create the SIAHub."""
self._hass: HomeAssistant = hass
self._entry: ConfigEntry = entry
self._hass = hass
self._entry = entry
self._port: int = entry.data[CONF_PORT]
self._title: str = entry.title
self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS])
@@ -131,7 +133,7 @@ class SIAHub:
@staticmethod
async def async_config_entry_updated(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: SIAConfigEntry
) -> None:
"""Handle signals of config entry being updated.
@@ -139,8 +141,8 @@ class SIAHub:
Second, unload underlying platforms, and then setup platforms, this reflects any changes in number of zones.
"""
if not (hub := hass.data[DOMAIN].get(config_entry.entry_id)):
if config_entry.state != ConfigEntryState.LOADED:
return
hub.update_accounts()
config_entry.runtime_data.update_accounts()
await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from typing import Any, cast
from typing import Any
from simplipy import API
from simplipy.errors import (
@@ -39,7 +39,7 @@ from simplipy.websocket import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
ATTR_CODE,
ATTR_DEVICE_ID,
@@ -88,6 +88,8 @@ from .const import (
from .coordinator import SimpliSafeDataUpdateCoordinator
from .typing import SystemType
type SimpliSafeConfigEntry = ConfigEntry[SimpliSafe]
ATTR_CATEGORY = "category"
ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by"
ATTR_LAST_EVENT_SENSOR_SERIAL = "last_event_sensor_serial"
@@ -223,10 +225,15 @@ def _async_get_system_for_service_call(
]
system_id = int(system_id_str)
entry: SimpliSafeConfigEntry | None
for entry_id in base_station_device_entry.config_entries:
if (simplisafe := hass.data[DOMAIN].get(entry_id)) is None:
if (
(entry := hass.config_entries.async_get_entry(entry_id)) is None
or entry.domain != DOMAIN
or entry.state != ConfigEntryState.LOADED
):
continue
return cast(SystemType, simplisafe.systems[system_id])
return entry.runtime_data.systems[system_id]
raise ValueError(f"No system for device ID: {device_id}")
@@ -286,7 +293,7 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) ->
hass.config_entries.async_update_entry(entry, **entry_updates)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SimpliSafeConfigEntry) -> bool:
"""Set up SimpliSafe as config entry."""
_async_standardize_config_entry(hass, entry)
@@ -310,8 +317,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SimplipyError as err:
raise ConfigEntryNotReady from err
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = simplisafe
entry.runtime_data = simplisafe
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -396,11 +402,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SimpliSafeConfigEntry) -> bool:
"""Unload a SimpliSafe config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.config_entries.async_loaded_entries(DOMAIN):
# If this is the last loaded instance of SimpliSafe, deregister any services

View File

@@ -28,12 +28,11 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SimpliSafe
from . import SimpliSafe, SimpliSafeConfigEntry
from .const import (
ATTR_ALARM_DURATION,
ATTR_ALARM_VOLUME,
@@ -44,7 +43,6 @@ from .const import (
ATTR_EXIT_DELAY_HOME,
ATTR_LIGHT,
ATTR_VOICE_PROMPT_VOLUME,
DOMAIN,
LOGGER,
)
from .entity import SimpliSafeEntity
@@ -104,11 +102,11 @@ WEBSOCKET_EVENTS_TO_LISTEN_FOR = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SimpliSafeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a SimpliSafe alarm control panel based on a config entry."""
simplisafe = hass.data[DOMAIN][entry.entry_id]
simplisafe = entry.runtime_data
async_add_entities(
[SimpliSafeAlarm(simplisafe, system) for system in simplisafe.systems.values()],
True,

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from simplipy.device import DeviceTypes, DeviceV3
from simplipy.device.sensor.v3 import SensorV3
from simplipy.system.v3 import SystemV3
@@ -11,13 +13,12 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SimpliSafe
from .const import DOMAIN, LOGGER
from . import SimpliSafe, SimpliSafeConfigEntry
from .const import LOGGER
from .entity import SimpliSafeEntity
SUPPORTED_BATTERY_SENSOR_TYPES = [
@@ -59,11 +60,11 @@ TRIGGERED_SENSOR_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SimpliSafeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe binary sensors based on a config entry."""
simplisafe = hass.data[DOMAIN][entry.entry_id]
simplisafe = entry.runtime_data
sensors: list[BatteryBinarySensor | TriggeredBinarySensor] = []
@@ -72,18 +73,22 @@ async def async_setup_entry(
LOGGER.warning("Skipping sensor setup for V2 system: %s", system.system_id)
continue
if TYPE_CHECKING:
assert isinstance(system, SystemV3)
for sensor in system.sensors.values():
if sensor.type in TRIGGERED_SENSOR_TYPES:
sensors.append(
TriggeredBinarySensor(
simplisafe,
system,
sensor,
cast(SensorV3, sensor),
TRIGGERED_SENSOR_TYPES[sensor.type],
)
)
if sensor.type in SUPPORTED_BATTERY_SENSOR_TYPES:
sensors.append(BatteryBinarySensor(simplisafe, system, sensor))
sensors.append(
BatteryBinarySensor(simplisafe, system, cast(DeviceV3, sensor))
)
sensors.extend(
BatteryBinarySensor(simplisafe, system, lock)

View File

@@ -9,14 +9,12 @@ from simplipy.errors import SimplipyError
from simplipy.system import System
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SimpliSafe
from .const import DOMAIN
from . import SimpliSafe, SimpliSafeConfigEntry
from .entity import SimpliSafeEntity
from .typing import SystemType
@@ -47,11 +45,11 @@ BUTTON_DESCRIPTIONS = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SimpliSafeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe buttons based on a config entry."""
simplisafe = hass.data[DOMAIN][entry.entry_id]
simplisafe = entry.runtime_data
async_add_entities(
[

View File

@@ -14,16 +14,12 @@ from simplipy.util.auth import (
)
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_URL, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
from . import SimpliSafeConfigEntry
from .const import DOMAIN, LOGGER
CONF_AUTH_CODE = "auth_code"
@@ -68,7 +64,7 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: SimpliSafeConfigEntry,
) -> SimpliSafeOptionsFlowHandler:
"""Define the config flow to handle options."""
return SimpliSafeOptionsFlowHandler()

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ADDRESS,
CONF_CODE,
@@ -16,8 +15,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from . import SimpliSafe
from .const import DOMAIN
from . import SimpliSafeConfigEntry
CONF_CREDIT_CARD = "creditCard"
CONF_EXPIRES = "expires"
@@ -53,10 +51,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: SimpliSafeConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
simplisafe: SimpliSafe = hass.data[DOMAIN][entry.entry_id]
simplisafe = entry.runtime_data
return async_redact_data(
{

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING, Any
from simplipy.device.lock import Lock, LockStates
from simplipy.errors import SimplipyError
@@ -10,13 +10,12 @@ from simplipy.system.v3 import SystemV3
from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, WebsocketEvent
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SimpliSafe
from .const import DOMAIN, LOGGER
from . import SimpliSafe, SimpliSafeConfigEntry
from .const import LOGGER
from .entity import SimpliSafeEntity
ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
@@ -32,11 +31,11 @@ WEBSOCKET_EVENTS_TO_LISTEN_FOR = (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SimpliSafeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe locks based on a config entry."""
simplisafe = hass.data[DOMAIN][entry.entry_id]
simplisafe = entry.runtime_data
locks: list[SimpliSafeLock] = []
for system in simplisafe.systems.values():
@@ -44,6 +43,8 @@ async def async_setup_entry(
LOGGER.warning("Skipping lock setup for V2 system: %s", system.system_id)
continue
if TYPE_CHECKING:
assert isinstance(system, SystemV3)
locks.extend(
SimpliSafeLock(simplisafe, system, lock) for lock in system.locks.values()
)

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from simplipy.device import DeviceTypes
from simplipy.device.sensor.v3 import SensorV3
from simplipy.system.v3 import SystemV3
@@ -11,23 +13,22 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SimpliSafe
from .const import DOMAIN, LOGGER
from . import SimpliSafe, SimpliSafeConfigEntry
from .const import LOGGER
from .entity import SimpliSafeEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SimpliSafeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe freeze sensors based on a config entry."""
simplisafe = hass.data[DOMAIN][entry.entry_id]
simplisafe = entry.runtime_data
sensors: list[SimplisafeFreezeSensor] = []
for system in simplisafe.systems.values():
@@ -35,8 +36,10 @@ async def async_setup_entry(
LOGGER.warning("Skipping sensor setup for V2 system: %s", system.system_id)
continue
if TYPE_CHECKING:
assert isinstance(system, SystemV3)
sensors.extend(
SimplisafeFreezeSensor(simplisafe, system, sensor)
SimplisafeFreezeSensor(simplisafe, system, cast(SensorV3, sensor))
for sensor in system.sensors.values()
if sensor.type == DeviceTypes.TEMPERATURE
)

View File

@@ -7,14 +7,12 @@ import asyncio
from aioskybell import Skybell
from aioskybell.exceptions import SkybellAuthenticationException, SkybellException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import SkybellDataUpdateCoordinator
from .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -25,7 +23,7 @@ PLATFORMS = [
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SkybellConfigEntry) -> bool:
"""Set up Skybell from a config entry."""
email = entry.data[CONF_EMAIL]
password = entry.data[CONF_PASSWORD]
@@ -53,14 +51,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
for coordinator in device_coordinators
]
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device_coordinators
entry.runtime_data = device_coordinators
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SkybellConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -9,12 +9,10 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN
from .coordinator import SkybellDataUpdateCoordinator
from .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
from .entity import SkybellEntity
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
@@ -32,14 +30,14 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SkybellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Skybell binary sensor."""
async_add_entities(
SkybellBinarySensor(coordinator, sensor)
for sensor in BINARY_SENSOR_TYPES
for coordinator in hass.data[DOMAIN][entry.entry_id]
for coordinator in entry.runtime_data
)

View File

@@ -7,14 +7,12 @@ from haffmpeg.camera import CameraMjpeg
from homeassistant.components.camera import Camera, CameraEntityDescription
from homeassistant.components.ffmpeg import get_ffmpeg_manager
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SkybellDataUpdateCoordinator
from .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
from .entity import SkybellEntity
CAMERA_TYPES: tuple[CameraEntityDescription, ...] = (
@@ -31,13 +29,13 @@ CAMERA_TYPES: tuple[CameraEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SkybellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Skybell camera."""
entities = []
for description in CAMERA_TYPES:
for coordinator in hass.data[DOMAIN][entry.entry_id]:
for coordinator in entry.runtime_data:
if description.key == "avatar":
entities.append(SkybellCamera(coordinator, description))
else:

View File

@@ -10,14 +10,19 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import LOGGER
type SkybellConfigEntry = ConfigEntry[list[SkybellDataUpdateCoordinator]]
class SkybellDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Data update coordinator for the Skybell integration."""
config_entry: ConfigEntry
config_entry: SkybellConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, device: SkybellDevice
self,
hass: HomeAssistant,
config_entry: SkybellConfigEntry,
device: SkybellDevice,
) -> None:
"""Initialize the coordinator."""
super().__init__(

View File

@@ -13,23 +13,22 @@ from homeassistant.components.light import (
LightEntity,
LightEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SkybellConfigEntry
from .entity import SkybellEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SkybellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Skybell switch."""
async_add_entities(
SkybellLight(coordinator, LightEntityDescription(key="light"))
for coordinator in hass.data[DOMAIN][entry.entry_id]
for coordinator in entry.runtime_data
)

View File

@@ -14,13 +14,13 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .entity import DOMAIN, SkybellEntity
from .coordinator import SkybellConfigEntry
from .entity import SkybellEntity
@dataclass(frozen=True, kw_only=True)
@@ -89,13 +89,13 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SkybellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Skybell sensor."""
async_add_entities(
SkybellSensor(coordinator, description)
for coordinator in hass.data[DOMAIN][entry.entry_id]
for coordinator in entry.runtime_data
for description in SENSOR_TYPES
if coordinator.device.owner or description.key not in CONST.ATTR_OWNER_STATS
)

View File

@@ -5,11 +5,10 @@ from __future__ import annotations
from typing import Any, cast
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SkybellConfigEntry
from .entity import SkybellEntity
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
@@ -30,13 +29,13 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SkybellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SkyBell switch."""
async_add_entities(
SkybellSwitch(coordinator, description)
for coordinator in hass.data[DOMAIN][entry.entry_id]
for coordinator in entry.runtime_data
for description in SWITCH_TYPES
)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from aiohttp.client_exceptions import ClientError
@@ -30,6 +31,17 @@ PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type SlackConfigEntry = ConfigEntry[SlackData]
@dataclass
class SlackData:
"""Runtime data for the Slack integration."""
client: AsyncWebClient
url: str
user_id: str
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Slack component."""
@@ -37,7 +49,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SlackConfigEntry) -> bool:
"""Set up Slack from a config entry."""
session = aiohttp_client.async_get_clientsession(hass)
slack = AsyncWebClient(
@@ -52,19 +64,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return False
raise ConfigEntryNotReady("Error while setting up integration") from ex
data = {
DATA_CLIENT: slack,
ATTR_URL: res[ATTR_URL],
ATTR_USER_ID: res[ATTR_USER_ID],
}
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data | {SLACK_DATA: data}
entry.runtime_data = SlackData(
client=slack,
url=res[ATTR_URL],
user_id=res[ATTR_USER_ID],
)
hass.async_create_task(
discovery.async_load_platform(
hass,
Platform.NOTIFY,
DOMAIN,
hass.data[DOMAIN][entry.entry_id],
entry.data
| {
SLACK_DATA: {
DATA_CLIENT: slack,
ATTR_URL: res[ATTR_URL],
ATTR_USER_ID: res[ATTR_USER_ID],
}
},
hass.data[DATA_HASS_CONFIG],
)
)

View File

@@ -1,14 +1,10 @@
"""The slack integration."""
from __future__ import annotations
from slack_sdk.web.async_client import AsyncWebClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from .const import ATTR_URL, ATTR_USER_ID, DATA_CLIENT, DEFAULT_NAME, DOMAIN
from . import SlackConfigEntry, SlackData
from .const import DEFAULT_NAME, DOMAIN
class SlackEntity(Entity):
@@ -16,16 +12,16 @@ class SlackEntity(Entity):
def __init__(
self,
data: dict[str, AsyncWebClient],
data: SlackData,
description: EntityDescription,
entry: ConfigEntry,
entry: SlackConfigEntry,
) -> None:
"""Initialize a Slack entity."""
self._client: AsyncWebClient = data[DATA_CLIENT]
self._client = data.client
self.entity_description = description
self._attr_unique_id = f"{data[ATTR_USER_ID]}_{description.key}"
self._attr_unique_id = f"{data.user_id}_{description.key}"
self._attr_device_info = DeviceInfo(
configuration_url=str(data[ATTR_URL]),
configuration_url=data.url,
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, entry.entry_id)},
manufacturer=DEFAULT_NAME,

View File

@@ -9,25 +9,25 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import ATTR_SNOOZE, DOMAIN, SLACK_DATA
from . import SlackConfigEntry
from .const import ATTR_SNOOZE
from .entity import SlackEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SlackConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Slack select."""
"""Set up the Slack sensor."""
async_add_entities(
[
SlackSensorEntity(
hass.data[DOMAIN][entry.entry_id][SLACK_DATA],
entry.runtime_data,
SensorEntityDescription(
key="do_not_disturb_until",
translation_key="do_not_disturb_until",

View File

@@ -23,6 +23,7 @@ from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, IS_IN_BED, SLEEP_NUMBER
from .coordinator import (
SleepIQConfigEntry,
SleepIQData,
SleepIQDataUpdateCoordinator,
SleepIQPauseUpdateCoordinator,
@@ -64,7 +65,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> bool:
"""Set up the SleepIQ config entry."""
conf = entry.data
email = conf[CONF_USERNAME]
@@ -104,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await pause_coordinator.async_config_entry_first_refresh()
await sleep_data_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SleepIQData(
entry.runtime_data = SleepIQData(
data_coordinator=coordinator,
pause_coordinator=pause_coordinator,
sleep_data_coordinator=sleep_data_coordinator,
@@ -116,11 +117,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> bool:
"""Unload the config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def _async_migrate_unique_ids(

View File

@@ -6,22 +6,21 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .const import ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .entity import SleepIQSleeperEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SleepIQConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed binary sensors."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
async_add_entities(
IsInBedBinarySensor(data.data_coordinator, bed, sleeper)
for bed in data.client.beds.values()

View File

@@ -9,12 +9,10 @@ from typing import Any
from asyncsleepiq import SleepIQBed
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SleepIQData
from .coordinator import SleepIQConfigEntry
from .entity import SleepIQEntity
@@ -43,11 +41,11 @@ ENTITY_DESCRIPTIONS = [
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SleepIQConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sleep number buttons."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
async_add_entities(
SleepNumberButton(bed, ed)

View File

@@ -18,16 +18,18 @@ UPDATE_INTERVAL = timedelta(seconds=60)
LONGER_UPDATE_INTERVAL = timedelta(minutes=5)
SLEEP_DATA_UPDATE_INTERVAL = timedelta(hours=1) # Sleep data doesn't change frequently
type SleepIQConfigEntry = ConfigEntry[SleepIQData]
class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""SleepIQ data update coordinator."""
config_entry: ConfigEntry
config_entry: SleepIQConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: SleepIQConfigEntry,
client: AsyncSleepIQ,
) -> None:
"""Initialize coordinator."""
@@ -51,12 +53,12 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]):
"""SleepIQ data update coordinator."""
config_entry: ConfigEntry
config_entry: SleepIQConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: SleepIQConfigEntry,
client: AsyncSleepIQ,
) -> None:
"""Initialize coordinator."""
@@ -78,12 +80,12 @@ class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]):
class SleepIQSleepDataCoordinator(DataUpdateCoordinator[None]):
"""SleepIQ sleep health data coordinator."""
config_entry: ConfigEntry
config_entry: SleepIQConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: SleepIQConfigEntry,
client: AsyncSleepIQ,
) -> None:
"""Initialize coordinator."""

View File

@@ -6,12 +6,10 @@ from typing import Any
from asyncsleepiq import SleepIQBed, SleepIQLight
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .entity import SleepIQBedEntity
_LOGGER = logging.getLogger(__name__)
@@ -19,11 +17,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SleepIQConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed lights."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
async_add_entities(
SleepIQLightEntity(data.data_coordinator, bed, light)
for bed in data.client.beds.values()

View File

@@ -21,7 +21,6 @@ from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -29,13 +28,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
ACTUATOR,
CORE_CLIMATE_TIMER,
DOMAIN,
ENTITY_TYPES,
FIRMNESS,
FOOT_WARMING_TIMER,
ICON_OCCUPIED,
)
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .entity import SleepIQBedEntity, sleeper_for_side
@@ -180,11 +178,11 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SleepIQConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed sensors."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
entities: list[SleepIQNumberEntity] = []
for bed in data.client.beds.values():

View File

@@ -13,22 +13,21 @@ from asyncsleepiq import (
)
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CORE_CLIMATE, DOMAIN, FOOT_WARMER
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .const import CORE_CLIMATE, FOOT_WARMER
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .entity import SleepIQBedEntity, SleepIQSleeperEntity, sleeper_for_side
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SleepIQConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ foundation preset select entities."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
entities: list[SleepIQBedEntity] = []
for bed in data.client.beds.values():
entities.extend(

View File

@@ -13,13 +13,11 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
DOMAIN,
HEART_RATE,
HRV,
PRESSURE,
@@ -29,7 +27,7 @@ from .const import (
SLEEP_SCORE,
)
from .coordinator import (
SleepIQData,
SleepIQConfigEntry,
SleepIQDataUpdateCoordinator,
SleepIQSleepDataCoordinator,
)
@@ -112,11 +110,11 @@ SLEEP_HEALTH_SENSORS: tuple[SleepIQSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SleepIQConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed sensors."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
entities: list[SensorEntity] = []

View File

@@ -7,22 +7,20 @@ from typing import Any
from asyncsleepiq import SleepIQBed
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SleepIQData, SleepIQPauseUpdateCoordinator
from .coordinator import SleepIQConfigEntry, SleepIQPauseUpdateCoordinator
from .entity import SleepIQBedEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SleepIQConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sleep number switches."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
async_add_entities(
SleepNumberPrivateSwitch(data.pause_coordinator, bed)
for bed in data.client.beds.values()

View File

@@ -1,6 +1,9 @@
"""The soundtouch component."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from libsoundtouch import soundtouch_device
from libsoundtouch.device import SoundTouchDevice
@@ -22,6 +25,11 @@ from .const import (
SERVICE_REMOVE_ZONE_SLAVE,
)
if TYPE_CHECKING:
from .media_player import SoundTouchMediaPlayer
type SoundTouchConfigEntry = ConfigEntry[SoundTouchData]
_LOGGER = logging.getLogger(__name__)
SERVICE_PLAY_EVERYWHERE_SCHEMA = vol.Schema({vol.Required("master"): cv.entity_id})
@@ -50,12 +58,12 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
class SoundTouchData:
"""SoundTouch data stored in the Home Assistant data object."""
"""SoundTouch data stored in the config entry runtime data."""
def __init__(self, device: SoundTouchDevice) -> None:
"""Initialize the SoundTouch data object for a device."""
self.device = device
self.media_player = None
self.media_player: SoundTouchMediaPlayer | None = None
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -65,20 +73,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Handle the applying of a service."""
master_id = service.data.get("master")
slaves_ids = service.data.get("slaves")
all_media_players = [
entry.runtime_data.media_player
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
if entry.runtime_data.media_player is not None
]
slaves = []
if slaves_ids:
slaves = [
data.media_player
for data in hass.data[DOMAIN].values()
if data.media_player.entity_id in slaves_ids
media_player
for media_player in all_media_players
if media_player.entity_id in slaves_ids
]
master = next(
iter(
[
data.media_player
for data in hass.data[DOMAIN].values()
if data.media_player.entity_id == master_id
media_player
for media_player in all_media_players
if media_player.entity_id == master_id
]
),
None,
@@ -90,9 +103,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if service.service == SERVICE_PLAY_EVERYWHERE:
slaves = [
data.media_player
for data in hass.data[DOMAIN].values()
if data.media_player.entity_id != master_id
media_player
for media_player in all_media_players
if media_player.entity_id != master_id
]
await hass.async_add_executor_job(master.create_zone, slaves)
elif service.service == SERVICE_CREATE_ZONE:
@@ -130,7 +143,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SoundTouchConfigEntry) -> bool:
"""Set up Bose SoundTouch from a config entry."""
try:
device = await hass.async_add_executor_job(
@@ -141,14 +154,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Unable to connect to SoundTouch device at {entry.data[CONF_HOST]}"
) from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device)
entry.runtime_data = SoundTouchData(device)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SoundTouchConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
del hass.data[DOMAIN][entry.entry_id]
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -19,7 +19,6 @@ from homeassistant.components.media_player import (
MediaType,
async_process_play_media_url,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import (
@@ -29,6 +28,7 @@ from homeassistant.helpers.device_registry import (
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SoundTouchConfigEntry
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -46,16 +46,16 @@ ATTR_SOUNDTOUCH_ZONE = "soundtouch_zone"
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SoundTouchConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Bose SoundTouch media player based on a config entry."""
device = hass.data[DOMAIN][entry.entry_id].device
device = entry.runtime_data.device
media_player = SoundTouchMediaPlayer(device)
async_add_entities([media_player], True)
hass.data[DOMAIN][entry.entry_id].media_player = media_player
entry.runtime_data.media_player = media_player
class SoundTouchMediaPlayer(MediaPlayerEntity):
@@ -388,14 +388,16 @@ class SoundTouchMediaPlayer(MediaPlayerEntity):
def _get_instance_by_ip(self, ip_address):
"""Search and return a SoundTouchDevice instance by it's IP address."""
for data in self.hass.data[DOMAIN].values():
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
data = entry.runtime_data
if data.device.config.device_ip == ip_address:
return data.media_player
return None
def _get_instance_by_id(self, instance_id):
"""Search and return a SoundTouchDevice instance by it's ID (aka MAC address)."""
for data in self.hass.data[DOMAIN].values():
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
data = entry.runtime_data
if data.device.config.device_id == instance_id:
return data.media_player
return None

View File

@@ -2,17 +2,16 @@
from srpenergy.client import SrpEnergyClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN, LOGGER
from .coordinator import SRPEnergyDataUpdateCoordinator
from .const import LOGGER
from .coordinator import SRPEnergyConfigEntry, SRPEnergyDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SRPEnergyConfigEntry) -> bool:
"""Set up the SRP Energy component from a config entry."""
api_account_id: str = entry.data[CONF_ID]
api_username: str = entry.data[CONF_USERNAME]
@@ -30,17 +29,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SRPEnergyConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -23,14 +23,19 @@ from .const import (
TIMEOUT = 10
PHOENIX_ZONE_INFO = dt_util.get_time_zone(PHOENIX_TIME_ZONE)
type SRPEnergyConfigEntry = ConfigEntry[SRPEnergyDataUpdateCoordinator]
class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]):
"""A srp_energy Data Update Coordinator."""
config_entry: ConfigEntry
config_entry: SRPEnergyConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, client: SrpEnergyClient
self,
hass: HomeAssistant,
config_entry: SRPEnergyConfigEntry,
client: SrpEnergyClient,
) -> None:
"""Initialize the srp_energy data coordinator."""
self._client = client

View File

@@ -7,7 +7,6 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
@@ -15,19 +14,17 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SRPEnergyDataUpdateCoordinator
from .const import DEVICE_CONFIG_URL, DEVICE_MANUFACTURER, DEVICE_MODEL, DOMAIN
from .coordinator import SRPEnergyConfigEntry, SRPEnergyDataUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SRPEnergyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SRP Energy Usage sensor."""
coordinator: SRPEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([SrpEntity(coordinator, entry)])
async_add_entities([SrpEntity(entry.runtime_data, entry)])
class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity):
@@ -43,7 +40,7 @@ class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity)
def __init__(
self,
coordinator: SRPEnergyDataUpdateCoordinator,
config_entry: ConfigEntry,
config_entry: SRPEnergyConfigEntry,
) -> None:
"""Initialize the SrpEntity class."""
super().__init__(coordinator)

View File

@@ -3,13 +3,12 @@
from streamlabswater.streamlabswater import StreamlabsClient
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .coordinator import StreamlabsCoordinator
from .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator
ATTR_AWAY_MODE = "away_mode"
SERVICE_SET_AWAY_MODE = "set_away_mode"
@@ -30,7 +29,7 @@ SET_AWAY_MODE_SCHEMA = vol.Schema(
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: StreamlabsConfigEntry) -> bool:
"""Set up StreamLabs from a config entry."""
api_key = entry.data[CONF_API_KEY]
@@ -39,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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)
def set_away_mode(service: ServiceCall) -> None:
@@ -55,9 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: StreamlabsConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -3,22 +3,20 @@
from __future__ import annotations
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import StreamlabsCoordinator
from .const import DOMAIN
from .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator
from .entity import StreamlabsWaterEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: StreamlabsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Streamlabs water binary sensor from a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
StreamlabsAwayMode(coordinator, location_id) for location_id in coordinator.data

View File

@@ -23,15 +23,18 @@ class StreamlabsData:
yearly_usage: float
type StreamlabsConfigEntry = ConfigEntry[StreamlabsCoordinator]
class StreamlabsCoordinator(DataUpdateCoordinator[dict[str, StreamlabsData]]):
"""Coordinator for Streamlabs."""
config_entry: ConfigEntry
config_entry: StreamlabsConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: StreamlabsConfigEntry,
client: StreamlabsClient,
) -> None:
"""Coordinator for Streamlabs."""

View File

@@ -10,15 +10,12 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import StreamlabsCoordinator
from .const import DOMAIN
from .coordinator import StreamlabsData
from .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator, StreamlabsData
from .entity import StreamlabsWaterEntity
@@ -59,11 +56,11 @@ SENSORS: tuple[StreamlabsWaterSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: StreamlabsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Streamlabs water sensor from a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
StreamLabsSensor(coordinator, location_id, entity_description)

View File

@@ -9,7 +9,6 @@ from surepy.enums import Location
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
@@ -24,7 +23,7 @@ from .const import (
SERVICE_SET_LOCK_STATE,
SERVICE_SET_PET_LOCATION,
)
from .coordinator import SurePetcareDataCoordinator
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -32,15 +31,10 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=3)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SurePetcareConfigEntry) -> bool:
"""Set up Sure Petcare from a config entry."""
hass.data.setdefault(DOMAIN, {})
try:
hass.data[DOMAIN][entry.entry_id] = coordinator = SurePetcareDataCoordinator(
hass,
entry,
)
coordinator = SurePetcareDataCoordinator(hass, entry)
except SurePetcareAuthenticationError as error:
_LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!")
raise ConfigEntryAuthFailed from error
@@ -49,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
lock_state_service_schema = vol.Schema(
@@ -91,10 +86,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: SurePetcareConfigEntry
) -> bool:
"""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)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -12,26 +12,24 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SurePetcareDataCoordinator
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
from .entity import SurePetcareEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SurePetcareConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sure PetCare Flaps binary sensors based on a config entry."""
entities: list[SurePetcareBinarySensor] = []
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
for surepy_entity in coordinator.data.values():
# connectivity

View File

@@ -29,13 +29,15 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=3)
type SurePetcareConfigEntry = ConfigEntry[SurePetcareDataCoordinator]
class SurePetcareDataCoordinator(DataUpdateCoordinator[dict[int, SurepyEntity]]):
"""Handle Surepetcare data."""
config_entry: ConfigEntry
config_entry: SurePetcareConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, entry: SurePetcareConfigEntry) -> None:
"""Initialize the data handler."""
self.surepy = Surepy(
entry.data[CONF_USERNAME],

View File

@@ -8,23 +8,21 @@ from surepy.entities import SurepyEntity
from surepy.enums import EntityType, LockState as SurepyLockState
from homeassistant.components.lock import LockEntity, LockState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SurePetcareDataCoordinator
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
from .entity import SurePetcareEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SurePetcareConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sure PetCare locks on a config entry."""
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
SurePetcareLock(surepy_entity.id, coordinator, lock_state)

View File

@@ -10,26 +10,25 @@ from surepy.entities.pet import Pet as SurepyPet
from surepy.enums import EntityType
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_VOLTAGE, PERCENTAGE, EntityCategory, UnitOfVolume
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW
from .coordinator import SurePetcareDataCoordinator
from .const import SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
from .entity import SurePetcareEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SurePetcareConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sure PetCare Flaps sensors."""
entities: list[SurePetcareEntity] = []
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
for surepy_entity in coordinator.data.values():
if surepy_entity.type in [

View File

@@ -9,7 +9,6 @@ from aiohttp import ClientSession
from switchbee.api import CentralUnitPolling, CentralUnitWsRPC, is_wsrpc_api
from switchbee.api.central_unit import SwitchBeeError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
@@ -17,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -53,10 +52,9 @@ async def get_api_object(
return api
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: SwitchBeeConfigEntry) -> bool:
"""Set up SwitchBee Smart Home from a config entry."""
hass.data.setdefault(DOMAIN, {})
central_unit = entry.data[CONF_HOST]
user = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
@@ -67,27 +65,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(update_listener))
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: SwitchBeeConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
async def update_listener(
hass: HomeAssistant, config_entry: SwitchBeeConfigEntry
) -> None:
"""Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_migrate_entry(
hass: HomeAssistant, config_entry: SwitchBeeConfigEntry
) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)

View File

@@ -4,23 +4,21 @@ from switchbee.api.central_unit import SwitchBeeError
from switchbee.device import ApiStateCommand, DeviceType
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .coordinator import SwitchBeeConfigEntry
from .entity import SwitchBeeEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchBeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbee button."""
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
SwitchBeeButton(switchbee_device, coordinator)
for switchbee_device in coordinator.data.values()

View File

@@ -23,14 +23,12 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
FAN_SB_TO_HASS = {
@@ -75,11 +73,11 @@ SUPPORTED_FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchBeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBee climate."""
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
SwitchBeeClimateEntity(switchbee_device, coordinator)
for switchbee_device in coordinator.data.values()

View File

@@ -19,16 +19,18 @@ from .const import DOMAIN, SCAN_INTERVAL_SEC
_LOGGER = logging.getLogger(__name__)
type SwitchBeeConfigEntry = ConfigEntry[SwitchBeeCoordinator]
class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevice]]):
"""Class to manage fetching SwitchBee data API."""
config_entry: ConfigEntry
config_entry: SwitchBeeConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: SwitchBeeConfigEntry,
swb_api: CentralUnitPolling | CentralUnitWsRPC,
) -> None:
"""Initialize."""

View File

@@ -14,23 +14,21 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .coordinator import SwitchBeeConfigEntry
from .entity import SwitchBeeDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchBeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBee switch."""
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
"""Set up SwitchBee covers."""
coordinator = entry.runtime_data
entities: list[CoverEntity] = []
for device in coordinator.data.values():

View File

@@ -2,19 +2,17 @@
from __future__ import annotations
from typing import Any
from typing import Any, cast
from switchbee.api.central_unit import SwitchBeeDeviceOfflineError, SwitchBeeError
from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeDimmer
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
MAX_BRIGHTNESS = 255
@@ -36,13 +34,13 @@ def _switchbee_brightness_to_hass(value: int) -> int:
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchBeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBee light."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
SwitchBeeLightEntity(switchbee_device, coordinator)
SwitchBeeLightEntity(cast(SwitchBeeDimmer, switchbee_device), coordinator)
for switchbee_device in coordinator.data.values()
if switchbee_device.type == DeviceType.Dimmer
)

View File

@@ -14,23 +14,21 @@ from switchbee.device import (
)
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchBeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbee switch."""
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
SwitchBeeSwitchEntity(device, coordinator)

View File

@@ -114,21 +114,25 @@ PLATFORMS_BY_TYPE = {
Platform.FAN,
Platform.SENSOR,
Platform.BUTTON,
Platform.SWITCH,
],
SupportedModels.AIR_PURIFIER_US.value: [
Platform.FAN,
Platform.SENSOR,
Platform.BUTTON,
Platform.SWITCH,
],
SupportedModels.AIR_PURIFIER_TABLE_JP.value: [
Platform.FAN,
Platform.SENSOR,
Platform.BUTTON,
Platform.SWITCH,
],
SupportedModels.AIR_PURIFIER_TABLE_US.value: [
Platform.FAN,
Platform.SENSOR,
Platform.BUTTON,
Platform.SWITCH,
],
SupportedModels.EVAPORATIVE_HUMIDIFIER.value: [
Platform.HUMIDIFIER,

View File

@@ -145,6 +145,20 @@
"medium": "mdi:water"
}
}
},
"switch": {
"child_lock": {
"state": {
"off": "mdi:lock-open",
"on": "mdi:lock"
}
},
"wireless_charging": {
"state": {
"off": "mdi:battery-charging-wireless-outline",
"on": "mdi:battery-charging-wireless"
}
}
}
},
"services": {

View File

@@ -326,6 +326,12 @@
}
}
}
},
"child_lock": {
"name": "Child lock"
},
"wireless_charging": {
"name": "Wireless charging"
}
},
"vacuum": {

View File

@@ -2,22 +2,61 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
from typing import Any
import switchbot
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import DOMAIN
from .const import AIRPURIFIER_BASIC_MODELS, AIRPURIFIER_TABLE_MODELS, DOMAIN
from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator
from .entity import SwitchbotSwitchedEntity, exception_handler
@dataclass(frozen=True, kw_only=True)
class SwitchbotSwitchEntityDescription(SwitchEntityDescription):
"""Describes a Switchbot switch entity."""
is_on_fn: Callable[[switchbot.SwitchbotDevice], bool | None]
turn_on_fn: Callable[[switchbot.SwitchbotDevice], Awaitable[Any]]
turn_off_fn: Callable[[switchbot.SwitchbotDevice], Awaitable[Any]]
AIRPURIFIER_BASIC_SWITCHES: tuple[SwitchbotSwitchEntityDescription, ...] = (
SwitchbotSwitchEntityDescription(
key="child_lock",
translation_key="child_lock",
device_class=SwitchDeviceClass.SWITCH,
is_on_fn=lambda device: device.is_child_lock_on(),
turn_on_fn=lambda device: device.open_child_lock(),
turn_off_fn=lambda device: device.close_child_lock(),
),
)
AIRPURIFIER_TABLE_SWITCHES: tuple[SwitchbotSwitchEntityDescription, ...] = (
*AIRPURIFIER_BASIC_SWITCHES,
SwitchbotSwitchEntityDescription(
key="wireless_charging",
translation_key="wireless_charging",
device_class=SwitchDeviceClass.SWITCH,
is_on_fn=lambda device: device.is_wireless_charging_on(),
turn_on_fn=lambda device: device.open_wireless_charging(),
turn_off_fn=lambda device: device.close_wireless_charging(),
),
)
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
@@ -36,10 +75,64 @@ async def async_setup_entry(
for channel in range(1, coordinator.device.channel + 1)
]
async_add_entities(entries)
elif coordinator.model in AIRPURIFIER_BASIC_MODELS:
async_add_entities(
[
SwitchbotGenericSwitch(coordinator, desc)
for desc in AIRPURIFIER_BASIC_SWITCHES
]
)
elif coordinator.model in AIRPURIFIER_TABLE_MODELS:
async_add_entities(
[
SwitchbotGenericSwitch(coordinator, desc)
for desc in AIRPURIFIER_TABLE_SWITCHES
]
)
else:
async_add_entities([SwitchBotSwitch(coordinator)])
class SwitchbotGenericSwitch(SwitchbotSwitchedEntity, SwitchEntity):
"""Representation of a Switchbot switch controlled via entity description."""
entity_description: SwitchbotSwitchEntityDescription
_device: switchbot.SwitchbotDevice
def __init__(
self,
coordinator: SwitchbotDataUpdateCoordinator,
description: SwitchbotSwitchEntityDescription,
) -> None:
"""Initialize the Switchbot generic switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.base_unique_id}-{description.key}"
@property
def is_on(self) -> bool | None:
"""Return true if device is on."""
return self.entity_description.is_on_fn(self._device)
@exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on."""
_LOGGER.debug(
"Turning on %s for %s", self.entity_description.key, self._address
)
await self.entity_description.turn_on_fn(self._device)
self.async_write_ha_state()
@exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off."""
_LOGGER.debug(
"Turning off %s for %s", self.entity_description.key, self._address
)
await self.entity_description.turn_off_fn(self._device)
self.async_write_ha_state()
class SwitchBotSwitch(SwitchbotSwitchedEntity, SwitchEntity, RestoreEntity):
"""Representation of a Switchbot switch."""

View File

@@ -75,9 +75,12 @@ class SwitchbotCloudData:
devices: SwitchbotDevices
type SwitchbotCloudConfigEntry = ConfigEntry[SwitchbotCloudData]
async def coordinator_for_device(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchbotCloudConfigEntry,
api: SwitchBotAPI,
device: Device | Remote,
coordinators_by_id: dict[str, SwitchBotCoordinator],
@@ -97,7 +100,7 @@ async def coordinator_for_device(
async def make_switchbot_devices(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchbotCloudConfigEntry,
api: SwitchBotAPI,
devices: list[Device | Remote],
coordinators_by_id: dict[str, SwitchBotCoordinator],
@@ -115,7 +118,7 @@ async def make_switchbot_devices(
async def make_device_data(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchbotCloudConfigEntry,
api: SwitchBotAPI,
device: Device | Remote,
devices_data: SwitchbotDevices,
@@ -330,7 +333,9 @@ async def make_device_data(
devices_data.sensors.append((device, coordinator))
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: SwitchbotCloudConfigEntry
) -> bool:
"""Set up SwitchBot via API from a config entry."""
token = entry.data[CONF_API_TOKEN]
secret = entry.data[CONF_API_KEY]
@@ -353,10 +358,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
switchbot_devices = await make_switchbot_devices(
hass, entry, api, devices, coordinators_by_id
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = SwitchbotCloudData(
api=api, devices=switchbot_devices
)
entry.runtime_data = SwitchbotCloudData(api=api, devices=switchbot_devices)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -365,17 +367,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: SwitchbotCloudConfigEntry
) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def _initialize_webhook(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchbotCloudConfigEntry,
api: SwitchBotAPI,
coordinators_by_id: dict[str, SwitchBotCoordinator],
) -> None:

View File

@@ -11,13 +11,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData
from .const import DOMAIN
from . import SwitchbotCloudConfigEntry
from .coordinator import SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
@@ -137,11 +135,11 @@ BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
async_add_entities(
SwitchBotCloudBinarySensor(data.api, device, coordinator, description)

View File

@@ -12,12 +12,10 @@ from switchbot_api import (
from switchbot_api.commands import ArtFrameCommands, BotCommands, CommonCommands
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import DOMAIN
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
@@ -58,11 +56,11 @@ BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
entities: list[SwitchBotCloudBot] = []
for device, coordinator in data.devices.buttons:
description_set = BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type]

View File

@@ -26,7 +26,6 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PRECISION_TENTHS,
STATE_UNAVAILABLE,
@@ -37,10 +36,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import SwitchbotCloudData, SwitchBotCoordinator
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .const import (
CLIMATE_PRESET_SCHEDULE,
DOMAIN,
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH,
)
from .entity import SwitchBotCloudEntity
@@ -69,11 +67,11 @@ _DEFAULT_SWITCHBOT_FAN_MODE = _SWITCHBOT_FAN_MODES[FanState.FAN_AUTO]
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.climates

View File

@@ -18,22 +18,21 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import COVER_ENTITY_AFTER_COMMAND_REFRESH, DOMAIN
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .const import COVER_ENTITY_AFTER_COMMAND_REFRESH
from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.covers

View File

@@ -13,13 +13,12 @@ from switchbot_api import (
)
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData
from .const import AFTER_COMMAND_REFRESH, DOMAIN, AirPurifierMode
from . import SwitchbotCloudConfigEntry
from .const import AFTER_COMMAND_REFRESH, AirPurifierMode
from .entity import SwitchBotCloudEntity
_LOGGER = logging.getLogger(__name__)
@@ -28,11 +27,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
for device, coordinator in data.devices.fans:
if device.device_type.startswith("Air Purifier"):
async_add_entities(

View File

@@ -12,13 +12,12 @@ from homeassistant.components.humidifier import (
HumidifierEntity,
HumidifierEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData
from .const import AFTER_COMMAND_REFRESH, DOMAIN, HUMIDITY_LEVELS, Humidifier2Mode
from . import SwitchbotCloudConfigEntry
from .const import AFTER_COMMAND_REFRESH, HUMIDITY_LEVELS, Humidifier2Mode
from .entity import SwitchBotCloudEntity
PARALLEL_UPDATES = 0
@@ -26,11 +25,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbot based on a config entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][entry.entry_id]
data = entry.runtime_data
async_add_entities(
SwitchBotHumidifier(data.api, device, coordinator)
if device.device_type == "Humidifier"

View File

@@ -6,22 +6,20 @@ from switchbot_api import Device, Remote, SwitchBotAPI
from switchbot_api.utils import get_file_stream_from_cloud
from homeassistant.components.image import ImageEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import DOMAIN
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.images

View File

@@ -14,12 +14,11 @@ from switchbot_api import (
)
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import AFTER_COMMAND_REFRESH, DOMAIN
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .const import AFTER_COMMAND_REFRESH
from .entity import SwitchBotCloudEntity
@@ -35,11 +34,11 @@ def brightness_map_value(value: int) -> int:
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.lights

View File

@@ -5,22 +5,20 @@ from typing import Any
from switchbot_api import Device, LockCommands, LockV2Commands, Remote, SwitchBotAPI
from homeassistant.components.lock import LockEntity, LockEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import DOMAIN
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
async_add_entities(
SwitchBotCloudLock(data.api, device, coordinator)
for device, coordinator in data.devices.locks

View File

@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
@@ -26,7 +25,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData
from . import SwitchbotCloudConfigEntry
from .const import DOMAIN
from .coordinator import SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
@@ -267,11 +266,11 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
entities: list[SwitchBotCloudSensor] = []
for device, coordinator in data.devices.sensors:
for description in SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type]:

View File

@@ -6,12 +6,11 @@ from typing import Any
from switchbot_api import CommonCommands, Device, PowerState, Remote, SwitchBotAPI
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData
from . import SwitchbotCloudConfigEntry
from .const import AFTER_COMMAND_REFRESH, DOMAIN
from .coordinator import SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
@@ -19,11 +18,11 @@ from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
entities: list[SwitchBotCloudSwitch] = []
for device, coordinator in data.devices.switches:
if device.device_type == "Relay Switch 2PM":

View File

@@ -17,13 +17,11 @@ from homeassistant.components.vacuum import (
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData
from . import SwitchbotCloudConfigEntry
from .const import (
DOMAIN,
VACUUM_FAN_SPEED_MAX,
VACUUM_FAN_SPEED_QUIET,
VACUUM_FAN_SPEED_STANDARD,
@@ -35,11 +33,11 @@ from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
config: SwitchbotCloudConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
data = config.runtime_data
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.vacuums

View File

@@ -21,7 +21,7 @@ from systembridgeconnector.models.open_url import OpenUrl
from systembridgeconnector.version import Version
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
CONF_API_KEY,
CONF_COMMAND,
@@ -57,7 +57,24 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from .config_flow import SystemBridgeConfigFlow
from .const import DATA_WAIT_TIMEOUT, DOMAIN, MODULES
from .coordinator import SystemBridgeDataUpdateCoordinator
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
def _get_coordinator(
hass: HomeAssistant, entry_id: str
) -> SystemBridgeDataUpdateCoordinator:
"""Return the coordinator for a config entry id."""
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
entry_id
)
if entry is None or entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": entry_id},
)
return entry.runtime_data
_LOGGER = logging.getLogger(__name__)
@@ -93,7 +110,7 @@ POWER_COMMAND_MAP = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
) -> bool:
"""Set up System Bridge from a config entry."""
@@ -198,8 +215,7 @@ async def async_setup_entry(
# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
# Set up all platforms except notify
await hass.config_entries.async_forward_entry_setups(
@@ -216,7 +232,7 @@ async def async_setup_entry(
CONF_NAME: f"{DOMAIN}_{coordinator.data.system.hostname}",
CONF_ENTITY_ID: entry.entry_id,
},
hass.data[DOMAIN][entry.entry_id],
{},
)
)
@@ -249,9 +265,7 @@ async def async_setup_entry(
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
"""Handle the get process by id service call."""
_LOGGER.debug("Get process by id: %s", service_call.data)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
processes: list[Process] = coordinator.data.processes
# Find process.id from list, raise ServiceValidationError if not found
@@ -275,9 +289,7 @@ async def async_setup_entry(
) -> ServiceResponse:
"""Handle the get process by name service call."""
_LOGGER.debug("Get process by name: %s", service_call.data)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
# Find processes from list
items: list[dict[str, Any]] = [
@@ -295,9 +307,7 @@ async def async_setup_entry(
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open path service call."""
_LOGGER.debug("Open path: %s", service_call.data)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_path(
OpenPath(path=service_call.data[CONF_PATH])
)
@@ -306,9 +316,7 @@ async def async_setup_entry(
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
"""Handle the power command service call."""
_LOGGER.debug("Power command: %s", service_call.data)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await getattr(
coordinator.websocket_client,
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
@@ -318,9 +326,7 @@ async def async_setup_entry(
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open url service call."""
_LOGGER.debug("Open URL: %s", service_call.data)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_url(
OpenUrl(url=service_call.data[CONF_URL])
)
@@ -328,9 +334,7 @@ async def async_setup_entry(
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_keypress(
KeyboardKey(key=service_call.data[CONF_KEY])
)
@@ -338,9 +342,7 @@ async def async_setup_entry(
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_text(
KeyboardText(text=service_call.data[CONF_TEXT])
)
@@ -446,33 +448,27 @@ async def async_setup_entry(
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: SystemBridgeConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
if unload_ok:
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]
coordinator = entry.runtime_data
# Ensure disconnected and cleanup stop sub
await coordinator.websocket_client.close()
if coordinator.unsub:
coordinator.unsub()
del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_OPEN_PATH)
hass.services.async_remove(DOMAIN, SERVICE_OPEN_URL)
hass.services.async_remove(DOMAIN, SERVICE_SEND_KEYPRESS)
hass.services.async_remove(DOMAIN, SERVICE_SEND_TEXT)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_reload_entry(
hass: HomeAssistant, entry: SystemBridgeConfigEntry
) -> None:
"""Reload the config entry when it changed."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -10,13 +10,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .data import SystemBridgeData
from .entity import SystemBridgeEntity
@@ -64,11 +62,11 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ..
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up System Bridge binary sensor based on a config entry."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
entities = [
SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT])

View File

@@ -36,18 +36,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN, GET_DATA_WAIT_TIMEOUT, MODULES
from .data import SystemBridgeData
type SystemBridgeConfigEntry = ConfigEntry[SystemBridgeDataUpdateCoordinator]
class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[SystemBridgeData]):
"""Class to manage fetching System Bridge data from single endpoint."""
config_entry: ConfigEntry
config_entry: SystemBridgeConfigEntry
def __init__(
self,
hass: HomeAssistant,
LOGGER: logging.Logger,
*,
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
) -> None:
"""Initialize global System Bridge data updater."""
self.title = entry.title

View File

@@ -15,13 +15,11 @@ from homeassistant.components.media_player import (
MediaPlayerState,
RepeatMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .data import SystemBridgeData
from .entity import SystemBridgeEntity
@@ -64,11 +62,11 @@ MEDIA_PLAYER_DESCRIPTION: Final[MediaPlayerEntityDescription] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up System Bridge media players based on a config entry."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
data = coordinator.data
if data.media is not None:

View File

@@ -15,12 +15,22 @@ from homeassistant.components.media_source import (
MediaSourceItem,
PlayMedia,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .coordinator import SystemBridgeConfigEntry
def _get_loaded_entry(hass: HomeAssistant, entry_id: str) -> SystemBridgeConfigEntry:
"""Return a loaded System Bridge config entry by id."""
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
entry_id
)
if entry is None or entry.state is not ConfigEntryState.LOADED:
raise ValueError("Invalid entry")
return entry
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
@@ -46,9 +56,7 @@ class SystemBridgeSource(MediaSource):
) -> PlayMedia:
"""Resolve media to a url."""
entry_id, path, mime_type = item.identifier.split("~~", 2)
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
raise ValueError("Invalid entry")
entry = _get_loaded_entry(self.hass, entry_id)
path_split = path.split("/", 1)
return PlayMedia(
f"{_build_base_url(entry)}&base={path_split[0]}&path={path_split[1]}",
@@ -64,21 +72,14 @@ class SystemBridgeSource(MediaSource):
return self._build_bridges()
if "~~" not in item.identifier:
entry = self.hass.config_entries.async_get_entry(item.identifier)
if entry is None:
raise ValueError("Invalid entry")
coordinator: SystemBridgeDataUpdateCoordinator = self.hass.data[DOMAIN].get(
entry.entry_id
)
entry = _get_loaded_entry(self.hass, item.identifier)
coordinator = entry.runtime_data
directories = await coordinator.websocket_client.get_directories()
return _build_root_paths(entry, directories)
entry_id, path = item.identifier.split("~~", 1)
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
raise ValueError("Invalid entry")
coordinator = self.hass.data[DOMAIN].get(entry.entry_id)
entry = _get_loaded_entry(self.hass, entry_id)
coordinator = entry.runtime_data
path_split = path.split("/", 1)
@@ -123,7 +124,7 @@ class SystemBridgeSource(MediaSource):
def _build_base_url(
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
) -> str:
"""Build base url for System Bridge media."""
return (
@@ -133,7 +134,7 @@ def _build_base_url(
def _build_root_paths(
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
media_directories: list[MediaDirectory],
) -> BrowseMediaSource:
"""Build base categories for System Bridge media."""
@@ -164,7 +165,7 @@ def _build_root_paths(
def _build_media_items(
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
media_files: MediaFiles,
path: str,
identifier: str,

View File

@@ -17,8 +17,7 @@ from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -37,11 +36,13 @@ async def async_get_service(
if discovery_info is None:
return None
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
discovery_info[CONF_ENTITY_ID]
]
)
if entry is None:
return None
return SystemBridgeNotificationService(coordinator)
return SystemBridgeNotificationService(entry.runtime_data)
class SystemBridgeNotificationService(BaseNotificationService):

View File

@@ -17,7 +17,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_PORT,
PERCENTAGE,
@@ -33,8 +32,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED, StateType
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .data import SystemBridgeData
from .entity import SystemBridgeEntity
@@ -364,11 +362,11 @@ BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up System Bridge sensor based on a config entry."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
entities = [
SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT])

View File

@@ -3,23 +3,21 @@
from __future__ import annotations
from homeassistant.components.update import UpdateEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .entity import SystemBridgeEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: SystemBridgeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up System Bridge update based on a config entry."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
[

View File

@@ -12,7 +12,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.util import Throttle
from .const import ATTR_BOOT_TIME, ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_HOST
from .const import ATTR_BOOT_TIME, ATTR_LOAD, ROUTER_DEFAULT_HOST
type VilfoConfigEntry = ConfigEntry[VilfoRouterData]
PLATFORMS = [Platform.SENSOR]
@@ -21,7 +23,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: VilfoConfigEntry) -> bool:
"""Set up Vilfo Router from a config entry."""
host = entry.data[CONF_HOST]
access_token = entry.data[CONF_ACCESS_TOKEN]
@@ -33,21 +35,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not vilfo_router.available:
raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = vilfo_router
entry.runtime_data = vilfo_router
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: VilfoConfigEntry) -> bool:
"""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)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
class VilfoRouterData:

View File

@@ -7,12 +7,12 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import VilfoConfigEntry
from .const import (
ATTR_API_DATA_FIELD_BOOT_TIME,
ATTR_API_DATA_FIELD_LOAD,
@@ -50,11 +50,11 @@ SENSOR_TYPES: tuple[VilfoSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: VilfoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add Vilfo Router entities from a config_entry."""
vilfo = hass.data[DOMAIN][config_entry.entry_id]
vilfo = config_entry.runtime_data
entities = [VilfoRouterSensor(vilfo, description) for description in SENSOR_TYPES]

View File

@@ -8,6 +8,7 @@ import uuid
import pytest
from homeassistant.components.lovelace import dashboard, resources
from homeassistant.components.lovelace.const import LOVELACE_DATA
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -278,6 +279,41 @@ async def test_storage_resources_import_invalid(
)
async def test_storage_resources_create_preserves_existing(
hass: HomeAssistant,
hass_storage: dict[str, Any],
) -> None:
"""Test async_create_item lazy-loads before writing.
Custom integrations may call async_create_item() during startup before the
frontend triggers a resource listing. Without a lazy-load guard, the
collection is empty and async_create_item() overwrites all existing
resources on disk.
"""
resource_config = [{**item, "id": uuid.uuid4().hex} for item in RESOURCE_EXAMPLES]
hass_storage[resources.RESOURCE_STORAGE_KEY] = {
"key": resources.RESOURCE_STORAGE_KEY,
"version": 1,
"data": {"items": resource_config},
}
assert await async_setup_component(hass, "lovelace", {})
resource_collection = hass.data[LOVELACE_DATA].resources
# Directly call async_create_item before any websocket listing
await resource_collection.async_create_item(
{"res_type": "module", "url": "/local/new.js"}
)
# Existing resources must still be present
items = resource_collection.async_items()
assert len(items) == len(resource_config) + 1
urls = [item["url"] for item in items]
for original in resource_config:
assert original["url"] in urls
assert "/local/new.js" in urls
@pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"])
async def test_storage_resources_safe_mode(
hass: HomeAssistant,

View File

@@ -587,6 +587,8 @@ async def test_get_lock_users_service(
"user_type": "unrestricted_user",
"credential_rule": "single",
"credentials": [],
"creator_fabric_index": None,
"last_modified_fabric_index": None,
"next_user_index": None,
}
],
@@ -745,6 +747,8 @@ async def test_get_lock_users_iterates_with_next_index(
"user_type": "unrestricted_user",
"credential_rule": "single",
"credentials": [],
"creator_fabric_index": None,
"last_modified_fabric_index": None,
"next_user_index": 5,
},
{
@@ -755,6 +759,8 @@ async def test_get_lock_users_iterates_with_next_index(
"user_type": "unrestricted_user",
"credential_rule": "single",
"credentials": [],
"creator_fabric_index": None,
"last_modified_fabric_index": None,
"next_user_index": None,
},
],
@@ -889,6 +895,8 @@ async def test_get_lock_users_with_credentials(
{"type": "pin", "index": 1},
{"type": "pin", "index": 2},
],
"creator_fabric_index": None,
"last_modified_fabric_index": None,
"next_user_index": None,
}
],
@@ -942,6 +950,59 @@ async def test_get_lock_users_with_nullvalue_credentials(
assert user["credentials"] == []
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
async def test_get_lock_users_with_fabric_indices(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test get_lock_users returns fabric indices and normalizes NullValue."""
matter_client.send_device_command = AsyncMock(
side_effect=[
{
"userIndex": 1,
"userName": "HA User",
"userUniqueID": None,
"userStatus": 1,
"userType": 0,
"credentialRule": 0,
"credentials": None,
"creatorFabricIndex": 3,
"lastModifiedFabricIndex": NullValue,
"nextUserIndex": 2,
},
{
"userIndex": 2,
"userName": "External User",
"userUniqueID": None,
"userStatus": 1,
"userType": 0,
"credentialRule": 0,
"credentials": None,
"creatorFabricIndex": NullValue,
"lastModifiedFabricIndex": 5,
"nextUserIndex": None,
},
]
)
result = await hass.services.async_call(
DOMAIN,
"get_lock_users",
{ATTR_ENTITY_ID: "lock.mock_door_lock"},
blocking=True,
return_response=True,
)
users = result["lock.mock_door_lock"]["users"]
assert len(users) == 2
assert users[0]["creator_fabric_index"] == 3
assert users[0]["last_modified_fabric_index"] is None
assert users[1]["creator_fabric_index"] is None
assert users[1]["last_modified_fabric_index"] == 5
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
@pytest.mark.parametrize(
@@ -1524,6 +1585,8 @@ async def test_get_lock_credential_status(
assert result["lock.mock_door_lock"] == {
"credential_exists": True,
"user_index": 2,
"creator_fabric_index": None,
"last_modified_fabric_index": None,
"next_credential_index": 3,
}
@@ -1571,10 +1634,62 @@ async def test_get_lock_credential_status_empty_slot(
assert result["lock.mock_door_lock"] == {
"credential_exists": False,
"user_index": None,
"creator_fabric_index": None,
"last_modified_fabric_index": None,
"next_credential_index": None,
}
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
@pytest.mark.parametrize("attributes", [{"1/257/65532": _FEATURE_USR_PIN}])
@pytest.mark.parametrize(
("creator", "last_modified", "expected_creator", "expected_last_modified"),
[
(3, NullValue, 3, None),
(NullValue, 2, None, 2),
],
)
async def test_get_lock_credential_status_with_fabric_indices(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
creator: int,
last_modified: int,
expected_creator: int | None,
expected_last_modified: int | None,
) -> None:
"""Test get_lock_credential_status returns fabric indices and normalizes NullValue."""
matter_client.send_device_command = AsyncMock(
return_value={
"credentialExists": True,
"userIndex": 2,
"creatorFabricIndex": creator,
"lastModifiedFabricIndex": last_modified,
"nextCredentialIndex": 5,
}
)
result = await hass.services.async_call(
DOMAIN,
"get_lock_credential_status",
{
ATTR_ENTITY_ID: "lock.mock_door_lock",
ATTR_CREDENTIAL_TYPE: "pin",
ATTR_CREDENTIAL_INDEX: 1,
},
blocking=True,
return_response=True,
)
assert result["lock.mock_door_lock"] == {
"credential_exists": True,
"user_index": 2,
"creator_fabric_index": expected_creator,
"last_modified_fabric_index": expected_last_modified,
"next_credential_index": 5,
}
@pytest.mark.parametrize("node_fixture", ["mock_door_lock"])
async def test_credential_services_without_usr_feature(
hass: HomeAssistant,

View File

@@ -7,7 +7,7 @@
'area_id': None,
'capabilities': dict({
'event_types': list([
'ding',
<DoorbellEventType.RING: 'ring'>,
]),
}),
'config_entry_id': <ANY>,
@@ -47,7 +47,7 @@
'device_class': 'doorbell',
'event_type': None,
'event_types': list([
'ding',
<DoorbellEventType.RING: 'ring'>,
]),
'friendly_name': 'Front Door Ding',
}),
@@ -187,7 +187,7 @@
'area_id': None,
'capabilities': dict({
'event_types': list([
'ding',
<DoorbellEventType.RING: 'ring'>,
]),
}),
'config_entry_id': <ANY>,
@@ -227,7 +227,7 @@
'device_class': 'doorbell',
'event_type': None,
'event_types': list([
'ding',
<DoorbellEventType.RING: 'ring'>,
]),
'friendly_name': 'Ingress Ding',
}),

View File

@@ -9,6 +9,7 @@ import pytest
from ring_doorbell import Ring
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.event import ATTR_EVENT_TYPE, DoorbellEventType
from homeassistant.components.ring.binary_sensor import RingEvent
from homeassistant.components.ring.coordinator import RingEventListener
from homeassistant.const import Platform
@@ -35,26 +36,38 @@ async def test_states(
@pytest.mark.parametrize(
("device_id", "device_name", "alert_kind", "device_class"),
("device_id", "device_name", "alert_kind", "device_class", "event_type"),
[
pytest.param(
FRONT_DOOR_DEVICE_ID,
"front_door",
"motion",
"motion",
"motion",
id="front_door_motion",
),
pytest.param(
FRONT_DOOR_DEVICE_ID, "front_door", "ding", "doorbell", id="front_door_ding"
FRONT_DOOR_DEVICE_ID,
"front_door",
"ding",
"doorbell",
DoorbellEventType.RING,
id="front_door_ding",
),
pytest.param(
INGRESS_DEVICE_ID, "ingress", "ding", "doorbell", id="ingress_ding"
INGRESS_DEVICE_ID,
"ingress",
"ding",
"doorbell",
DoorbellEventType.RING,
id="ingress_ding",
),
pytest.param(
INGRESS_DEVICE_ID,
"ingress",
"intercom_unlock",
"button",
"intercom_unlock",
id="ingress_unlock",
),
],
@@ -68,6 +81,7 @@ async def test_event(
device_name: str,
alert_kind: str,
device_class: str,
event_type: str,
) -> None:
"""Test the Ring event platforms."""
@@ -96,3 +110,4 @@ async def test_event(
state = hass.states.get(entity_id)
assert state is not None
assert state.state == start_time_str
assert state.attributes[ATTR_EVENT_TYPE] == event_type

View File

@@ -18,6 +18,10 @@ from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from . import (
AIR_PURIFIER_JP_SERVICE_INFO,
AIR_PURIFIER_TABLE_JP_SERVICE_INFO,
AIR_PURIFIER_TABLE_US_SERVICE_INFO,
AIR_PURIFIER_US_SERVICE_INFO,
PLUG_MINI_EU_SERVICE_INFO,
RELAY_SWITCH_1_SERVICE_INFO,
RELAY_SWITCH_2PM_SERVICE_INFO,
@@ -294,3 +298,101 @@ async def test_relay_switch_control_with_exception(
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
@pytest.mark.parametrize(
(
"service_info",
"sensor_type",
"entity_id",
"turn_on_method",
"turn_off_method",
),
[
(
AIR_PURIFIER_JP_SERVICE_INFO,
"air_purifier_jp",
"switch.test_name_child_lock",
"open_child_lock",
"close_child_lock",
),
(
AIR_PURIFIER_TABLE_JP_SERVICE_INFO,
"air_purifier_table_jp",
"switch.test_name_child_lock",
"open_child_lock",
"close_child_lock",
),
(
AIR_PURIFIER_US_SERVICE_INFO,
"air_purifier_us",
"switch.test_name_child_lock",
"open_child_lock",
"close_child_lock",
),
(
AIR_PURIFIER_TABLE_US_SERVICE_INFO,
"air_purifier_table_us",
"switch.test_name_child_lock",
"open_child_lock",
"close_child_lock",
),
(
AIR_PURIFIER_TABLE_JP_SERVICE_INFO,
"air_purifier_table_jp",
"switch.test_name_wireless_charging",
"open_wireless_charging",
"close_wireless_charging",
),
(
AIR_PURIFIER_TABLE_US_SERVICE_INFO,
"air_purifier_table_us",
"switch.test_name_wireless_charging",
"open_wireless_charging",
"close_wireless_charging",
),
],
)
async def test_air_purifier_switch_control(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
service_info: BluetoothServiceInfoBleak,
sensor_type: str,
entity_id: str,
turn_on_method: str,
turn_off_method: str,
) -> None:
"""Test air purifier switch control."""
inject_bluetooth_service_info(hass, service_info)
entry = mock_entry_encrypted_factory(sensor_type=sensor_type)
entry.add_to_hass(hass)
mocked_turn_on = AsyncMock(return_value=True)
mocked_turn_off = AsyncMock(return_value=True)
with patch.multiple(
"homeassistant.components.switchbot.switch.switchbot.SwitchbotAirPurifier",
update=AsyncMock(return_value=None),
**{turn_on_method: mocked_turn_on, turn_off_method: mocked_turn_off},
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mocked_turn_on.assert_awaited_once()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mocked_turn_off.assert_awaited_once()

View File

@@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, patch
from switchbot_api import Device
from homeassistant.components.switchbot_cloud import DOMAIN
from homeassistant.components.switchbot_cloud.image import SwitchBotCloudImage
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNKNOWN
@@ -63,7 +62,7 @@ async def test_async_image(
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
cloud_data = hass.data[DOMAIN][entry.entry_id]
cloud_data = entry.runtime_data
device, coordinator = cloud_data.devices.images[0]
image_entity = SwitchBotCloudImage(cloud_data.api, device, coordinator)

View File

@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.vilfo import DOMAIN
from homeassistant.components.vilfo.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
from tests.common import MockConfigEntry