Compare commits

..

1 Commits

Author SHA1 Message Date
Jan Čermák
427faf4854 Bump base image to 2026.02.0 with Python 3.14.3, use 3.14.3 in CI
This also bumps libcec used in the base image to 7.1.1, full changelog:
* https://github.com/home-assistant/docker/releases/tag/2026.02.0

Python changelog:
* https://docs.python.org/release/3.14.3/whatsnew/changelog.html
2026-04-10 10:23:28 +02:00
107 changed files with 895 additions and 1509 deletions

View File

@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.01.0"
BASE_IMAGE_VERSION: "2026.02.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}

View File

@@ -1 +1 @@
3.14.2
3.14.3

View File

@@ -79,7 +79,6 @@ from .config import HassioConfig
from .const import (
ADDONS_COORDINATOR,
ATTR_REPOSITORIES,
COORDINATOR,
DATA_ADDONS_LIST,
DATA_COMPONENT,
DATA_CONFIG_STORE,
@@ -93,12 +92,9 @@ from .const import (
DATA_SUPERVISOR_INFO,
DOMAIN,
HASSIO_UPDATE_INTERVAL,
STATS_COORDINATOR,
)
from .coordinator import (
HassioAddOnDataUpdateCoordinator,
HassioDataUpdateCoordinator,
HassioStatsDataUpdateCoordinator,
get_addons_info,
get_addons_list,
get_addons_stats,
@@ -388,6 +384,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
]
hass.data[DATA_SUPERVISOR_INFO]["addons"] = hass.data[DATA_ADDONS_LIST]
async_call_later(
hass,
HASSIO_UPDATE_INTERVAL,
HassJob(update_info_data, cancel_on_shutdown=True),
)
# Fetch data
update_info_task = hass.async_create_task(update_info_data(), eager_start=True)
@@ -460,20 +462,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
dev_reg = dr.async_get(hass)
coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg)
await coordinator.async_config_entry_first_refresh()
hass.data[COORDINATOR] = coordinator
addon_coordinator = HassioAddOnDataUpdateCoordinator(
hass, entry, dev_reg, coordinator.jobs
)
await addon_coordinator.async_config_entry_first_refresh()
hass.data[ADDONS_COORDINATOR] = addon_coordinator
stats_coordinator = HassioStatsDataUpdateCoordinator(hass, entry)
await stats_coordinator.async_config_entry_first_refresh()
hass.data[STATS_COORDINATOR] = stats_coordinator
hass.data[ADDONS_COORDINATOR] = coordinator
def deprecated_setup_issue() -> None:
os_info = get_os_info(hass)
@@ -540,12 +531,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
# Unload coordinator
coordinator: HassioDataUpdateCoordinator = hass.data[COORDINATOR]
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
coordinator.unload()
# Pop coordinators
hass.data.pop(COORDINATOR, None)
# Pop coordinator
hass.data.pop(ADDONS_COORDINATOR, None)
hass.data.pop(STATS_COORDINATOR, None)
return unload_ok

View File

@@ -20,7 +20,6 @@ from .const import (
ADDONS_COORDINATOR,
ATTR_STARTED,
ATTR_STATE,
COORDINATOR,
DATA_KEY_ADDONS,
DATA_KEY_MOUNTS,
)
@@ -61,18 +60,17 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Binary sensor set up for Hass.io config entry."""
addons_coordinator = hass.data[ADDONS_COORDINATOR]
coordinator = hass.data[COORDINATOR]
coordinator = hass.data[ADDONS_COORDINATOR]
async_add_entities(
itertools.chain(
[
HassioAddonBinarySensor(
addon=addon,
coordinator=addons_coordinator,
coordinator=coordinator,
entity_description=entity_description,
)
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
for addon in coordinator.data[DATA_KEY_ADDONS].values()
for entity_description in ADDON_ENTITY_DESCRIPTIONS
],
[

View File

@@ -77,9 +77,7 @@ EVENT_JOB = "job"
UPDATE_KEY_SUPERVISOR = "supervisor"
STARTUP_COMPLETE = "complete"
COORDINATOR = "hassio_coordinator"
ADDONS_COORDINATOR = "hassio_addons_coordinator"
STATS_COORDINATOR = "hassio_stats_coordinator"
DATA_COMPONENT: HassKey[HassIO] = HassKey(DOMAIN)
@@ -97,8 +95,6 @@ DATA_ADDONS_INFO = "hassio_addons_info"
DATA_ADDONS_STATS = "hassio_addons_stats"
DATA_ADDONS_LIST = "hassio_addons_list"
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
HASSIO_ADDON_UPDATE_INTERVAL = timedelta(minutes=15)
HASSIO_STATS_UPDATE_INTERVAL = timedelta(seconds=60)
ATTR_AUTO_UPDATE = "auto_update"
ATTR_VERSION = "version"

View File

@@ -7,7 +7,7 @@ from collections import defaultdict
from collections.abc import Awaitable
from copy import deepcopy
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
from aiohasupervisor import SupervisorError, SupervisorNotFoundError
from aiohasupervisor.models import (
@@ -15,9 +15,9 @@ from aiohasupervisor.models import (
CIFSMountResponse,
InstalledAddon,
NFSMountResponse,
ResponseData,
StoreInfo,
)
from aiohasupervisor.models.base import ResponseData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME
@@ -35,6 +35,7 @@ from .const import (
ATTR_SLUG,
ATTR_URL,
ATTR_VERSION,
CONTAINER_INFO,
CONTAINER_STATS,
CORE_CONTAINER,
DATA_ADDONS_INFO,
@@ -58,8 +59,6 @@ from .const import (
DATA_SUPERVISOR_INFO,
DATA_SUPERVISOR_STATS,
DOMAIN,
HASSIO_ADDON_UPDATE_INTERVAL,
HASSIO_STATS_UPDATE_INTERVAL,
HASSIO_UPDATE_INTERVAL,
REQUEST_REFRESH_DELAY,
SUPERVISOR_CONTAINER,
@@ -319,315 +318,7 @@ def async_remove_devices_from_dev_reg(
dev_reg.async_remove_device(dev.id)
class HassioStatsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to retrieve Hass.io container stats."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=HASSIO_STATS_UPDATE_INTERVAL,
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
self.supervisor_client = get_supervisor_client(hass)
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
lambda: defaultdict(set)
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update stats data via library."""
try:
await self._fetch_stats()
except SupervisorError as err:
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
new_data: dict[str, Any] = {}
new_data[DATA_KEY_CORE] = get_core_stats(self.hass)
new_data[DATA_KEY_SUPERVISOR] = get_supervisor_stats(self.hass)
new_data[DATA_KEY_ADDONS] = get_addons_stats(self.hass)
return new_data
async def _fetch_stats(self) -> None:
"""Fetch container stats for subscribed entities."""
container_updates = self._container_updates
data = self.hass.data
client = self.supervisor_client
# Fetch core and supervisor stats
updates: dict[str, Awaitable] = {}
if container_updates.get(CORE_CONTAINER, {}).get(CONTAINER_STATS):
updates[DATA_CORE_STATS] = client.homeassistant.stats()
if container_updates.get(SUPERVISOR_CONTAINER, {}).get(CONTAINER_STATS):
updates[DATA_SUPERVISOR_STATS] = client.supervisor.stats()
if updates:
api_results: list[ResponseData] = await asyncio.gather(*updates.values())
for key, result in zip(updates, api_results, strict=True):
data[key] = result.to_dict()
# Fetch addon stats
addons_list = get_addons_list(self.hass) or []
started_addons = {
addon[ATTR_SLUG]
for addon in addons_list
if addon.get("state") in {AddonState.STARTED, AddonState.STARTUP}
}
addons_stats: dict[str, Any] = data.setdefault(DATA_ADDONS_STATS, {})
# Clean up cache for stopped/removed addons
for slug in addons_stats.keys() - started_addons:
del addons_stats[slug]
# Fetch stats for addons with subscribed entities
addon_stats_results = dict(
await asyncio.gather(
*[
self._update_addon_stats(slug)
for slug in started_addons
if container_updates.get(slug, {}).get(CONTAINER_STATS)
]
)
)
addons_stats.update(addon_stats_results)
async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Update single addon stats."""
try:
stats = await self.supervisor_client.addons.addon_stats(slug)
except SupervisorError as err:
_LOGGER.warning("Could not fetch stats for %s: %s", slug, err)
return (slug, None)
return (slug, stats.to_dict())
@callback
def async_enable_container_updates(
self, slug: str, entity_id: str, types: set[str]
) -> CALLBACK_TYPE:
"""Enable stats updates for a container."""
enabled_updates = self._container_updates[slug]
for key in types:
enabled_updates[key].add(entity_id)
@callback
def _remove() -> None:
for key in types:
enabled_updates[key].discard(entity_id)
if not enabled_updates[key]:
del enabled_updates[key]
if not enabled_updates:
self._container_updates.pop(slug, None)
return _remove
class HassioAddOnDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to retrieve Hass.io Add-on status."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
dev_reg: dr.DeviceRegistry,
jobs: SupervisorJobs,
) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=HASSIO_ADDON_UPDATE_INTERVAL,
# We don't want an immediate refresh since we want to avoid
# hammering the Supervisor API on startup
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
self.hassio = hass.data[DATA_COMPONENT]
self.entry_id = config_entry.entry_id
self.dev_reg = dev_reg
self._addon_info_subscriptions: defaultdict[str, set[str]] = defaultdict(set)
self.supervisor_client = get_supervisor_client(hass)
self.jobs = jobs
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
is_first_update = not self.data
client = self.supervisor_client
try:
installed_addons: list[InstalledAddon] = await client.addons.list()
all_addons = {addon.slug for addon in installed_addons}
# Fetch addon info for all addons on first update, or only
# for addons with subscribed entities on subsequent updates.
addon_info_results = dict(
await asyncio.gather(
*[
self._update_addon_info(slug)
for slug in all_addons
if is_first_update or self._addon_info_subscriptions.get(slug)
]
)
)
except SupervisorError as err:
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
# Update hass.data for legacy accessor functions
data = self.hass.data
addons_list_dicts = [addon.to_dict() for addon in installed_addons]
data[DATA_ADDONS_LIST] = addons_list_dicts
# Update addon info cache in hass.data
addon_info_cache: dict[str, Any] = data.setdefault(DATA_ADDONS_INFO, {})
for slug in addon_info_cache.keys() - all_addons:
del addon_info_cache[slug]
addon_info_cache.update(addon_info_results)
# Deprecated 2026.4.0: Folding addons.list results into supervisor_info
# for compatibility. Written to hass.data only, not coordinator data.
if DATA_SUPERVISOR_INFO in data:
data[DATA_SUPERVISOR_INFO]["addons"] = addons_list_dicts
# Build clean coordinator data
store_data = get_store(self.hass)
if store_data:
repositories = {
repo.slug: repo.name
for repo in StoreInfo.from_dict(store_data).repositories
}
else:
repositories = {}
new_data: dict[str, Any] = {}
new_data[DATA_KEY_ADDONS] = {
(slug := addon[ATTR_SLUG]): {
**addon,
ATTR_AUTO_UPDATE: (addon_info_cache.get(slug) or {}).get(
ATTR_AUTO_UPDATE, False
),
ATTR_REPOSITORY: repositories.get(
repo_slug := addon.get(ATTR_REPOSITORY, ""), repo_slug
),
}
for addon in addons_list_dicts
}
# If this is the initial refresh, register all addons
if is_first_update:
async_register_addons_in_dev_reg(
self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values()
)
# Remove add-ons that are no longer installed from device registry
supervisor_addon_devices = {
list(device.identifiers)[0][1]
for device in self.dev_reg.devices.get_devices_for_config_entry_id(
self.entry_id
)
if device.model == SupervisorEntityModel.ADDON
}
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
async_remove_devices_from_dev_reg(self.dev_reg, stale_addons)
# If there are new add-ons, we should reload the config entry so we can
# create new devices and entities. We can return an empty dict because
# coordinator will be recreated.
if self.data and (
set(new_data[DATA_KEY_ADDONS]) - set(self.data[DATA_KEY_ADDONS])
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(self.entry_id)
)
return {}
return new_data
async def get_changelog(self, addon_slug: str) -> str | None:
"""Get the changelog for an add-on."""
try:
return await self.supervisor_client.store.addon_changelog(addon_slug)
except SupervisorNotFoundError:
return None
async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Return the info for an addon."""
try:
info = await self.supervisor_client.addons.addon_info(slug)
except SupervisorError as err:
_LOGGER.warning("Could not fetch info for %s: %s", slug, err)
return (slug, None)
# Translate to legacy hassio names for compatibility
info_dict = info.to_dict()
info_dict["hassio_api"] = info_dict.pop("supervisor_api")
info_dict["hassio_role"] = info_dict.pop("supervisor_role")
return (slug, info_dict)
@callback
def async_enable_addon_info_updates(
self, slug: str, entity_id: str
) -> CALLBACK_TYPE:
"""Enable info updates for an add-on."""
self._addon_info_subscriptions[slug].add(entity_id)
@callback
def _remove() -> None:
self._addon_info_subscriptions[slug].discard(entity_id)
if not self._addon_info_subscriptions[slug]:
del self._addon_info_subscriptions[slug]
return _remove
async def _async_refresh(
self,
log_failures: bool = True,
raise_on_auth_failed: bool = False,
scheduled: bool = False,
raise_on_entry_error: bool = False,
) -> None:
"""Refresh data."""
if not scheduled and not raise_on_auth_failed:
# Force reloading add-on updates for non-scheduled
# updates.
#
# If `raise_on_auth_failed` is set, it means this is
# the first refresh and we do not want to delay
# startup or cause a timeout so we only refresh the
# updates if this is not a scheduled refresh and
# we are not doing the first refresh.
try:
await self.supervisor_client.store.reload()
except SupervisorError as err:
_LOGGER.warning("Error on Supervisor API: %s", err)
await super()._async_refresh(
log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error
)
async def force_addon_info_data_refresh(self, addon_slug: str) -> None:
"""Force refresh of addon info data for a specific addon."""
try:
slug, info = await self._update_addon_info(addon_slug)
if info is not None and DATA_KEY_ADDONS in self.data:
if slug in self.data[DATA_KEY_ADDONS]:
data = deepcopy(self.data)
data[DATA_KEY_ADDONS][slug].update(info)
self.async_set_updated_data(data)
except SupervisorError as err:
_LOGGER.warning("Could not refresh info for %s: %s", addon_slug, err)
class HassioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to retrieve Hass.io status."""
config_entry: ConfigEntry
@@ -643,72 +334,80 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
name=DOMAIN,
update_interval=HASSIO_UPDATE_INTERVAL,
# We don't want an immediate refresh since we want to avoid
# hammering the Supervisor API on startup
# fetching the container stats right away and avoid hammering
# the Supervisor API on startup
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
self.hassio = hass.data[DATA_COMPONENT]
self.data = {}
self.entry_id = config_entry.entry_id
self.dev_reg = dev_reg
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
lambda: defaultdict(set)
)
self.supervisor_client = get_supervisor_client(hass)
self.jobs = SupervisorJobs(hass)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
is_first_update = not self.data
client = self.supervisor_client
try:
(
info,
core_info,
supervisor_info,
os_info,
host_info,
store_info,
network_info,
) = await asyncio.gather(
client.info(),
client.homeassistant.info(),
client.supervisor.info(),
client.os.info(),
client.host.info(),
client.store.info(),
client.network.info(),
)
mounts_info = await client.mounts.info()
await self.jobs.refresh_data(is_first_update)
await self.force_data_refresh(is_first_update)
except SupervisorError as err:
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
# Build clean coordinator data
new_data: dict[str, Any] = {}
new_data[DATA_KEY_CORE] = core_info.to_dict()
new_data[DATA_KEY_SUPERVISOR] = supervisor_info.to_dict()
new_data[DATA_KEY_HOST] = host_info.to_dict()
new_data[DATA_KEY_MOUNTS] = {mount.name: mount for mount in mounts_info.mounts}
supervisor_info = get_supervisor_info(self.hass) or {}
addons_info = get_addons_info(self.hass) or {}
addons_stats = get_addons_stats(self.hass)
store_data = get_store(self.hass)
mounts_info = await self.supervisor_client.mounts.info()
addons_list = get_addons_list(self.hass) or []
if store_data:
repositories = {
repo.slug: repo.name
for repo in StoreInfo.from_dict(store_data).repositories
}
else:
repositories = {}
new_data[DATA_KEY_ADDONS] = {
(slug := addon[ATTR_SLUG]): {
**addon,
**(addons_stats.get(slug) or {}),
ATTR_AUTO_UPDATE: (addons_info.get(slug) or {}).get(
ATTR_AUTO_UPDATE, False
),
ATTR_REPOSITORY: repositories.get(
repo_slug := addon.get(ATTR_REPOSITORY, ""), repo_slug
),
}
for addon in addons_list
}
if self.is_hass_os:
new_data[DATA_KEY_OS] = os_info.to_dict()
new_data[DATA_KEY_OS] = get_os_info(self.hass)
# Update hass.data for legacy accessor functions
data = self.hass.data
data[DATA_INFO] = info.to_dict()
data[DATA_CORE_INFO] = new_data[DATA_KEY_CORE]
data[DATA_OS_INFO] = new_data.get(DATA_KEY_OS, os_info.to_dict())
data[DATA_HOST_INFO] = new_data[DATA_KEY_HOST]
data[DATA_STORE] = store_info.to_dict()
data[DATA_NETWORK_INFO] = network_info.to_dict()
# Separate dict for hass.data supervisor info since we add deprecated
# compat keys that should not be in coordinator data
data[DATA_SUPERVISOR_INFO] = supervisor_info.to_dict()
# Deprecated 2026.4.0: Folding repositories into supervisor_info for
# compatibility. Written to hass.data only, not coordinator data.
data[DATA_SUPERVISOR_INFO]["repositories"] = data[DATA_STORE][ATTR_REPOSITORIES]
new_data[DATA_KEY_CORE] = {
**(get_core_info(self.hass) or {}),
**get_core_stats(self.hass),
}
new_data[DATA_KEY_SUPERVISOR] = {
**supervisor_info,
**get_supervisor_stats(self.hass),
}
new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {}
new_data[DATA_KEY_MOUNTS] = {mount.name: mount for mount in mounts_info.mounts}
# If this is the initial refresh, register all main components
# If this is the initial refresh, register all addons and return the dict
if is_first_update:
async_register_addons_in_dev_reg(
self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values()
)
async_register_mounts_in_dev_reg(
self.entry_id, self.dev_reg, new_data[DATA_KEY_MOUNTS].values()
)
@@ -724,6 +423,17 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.entry_id, self.dev_reg, new_data[DATA_KEY_OS]
)
# Remove add-ons that are no longer installed from device registry
supervisor_addon_devices = {
list(device.identifiers)[0][1]
for device in self.dev_reg.devices.get_devices_for_config_entry_id(
self.entry_id
)
if device.model == SupervisorEntityModel.ADDON
}
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
async_remove_devices_from_dev_reg(self.dev_reg, stale_addons)
# Remove mounts that no longer exists from device registry
supervisor_mount_devices = {
device.name
@@ -743,11 +453,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Remove the OS device if it exists and the installation is not hassos
self.dev_reg.async_remove_device(dev.id)
# If there are new mounts, we should reload the config entry so we can
# If there are new add-ons or mounts, we should reload the config entry so we can
# create new devices and entities. We can return an empty dict because
# coordinator will be recreated.
if self.data and (
set(new_data[DATA_KEY_MOUNTS]) - set(self.data.get(DATA_KEY_MOUNTS, {}))
set(new_data[DATA_KEY_ADDONS]) - set(self.data[DATA_KEY_ADDONS])
or set(new_data[DATA_KEY_MOUNTS]) - set(self.data[DATA_KEY_MOUNTS])
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(self.entry_id)
@@ -756,6 +467,146 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
return new_data
async def get_changelog(self, addon_slug: str) -> str | None:
"""Get the changelog for an add-on."""
try:
return await self.supervisor_client.store.addon_changelog(addon_slug)
except SupervisorNotFoundError:
return None
async def force_data_refresh(self, first_update: bool) -> None:
"""Force update of the addon info."""
container_updates = self._container_updates
data = self.hass.data
client = self.supervisor_client
updates: dict[str, Awaitable[ResponseData]] = {
DATA_INFO: client.info(),
DATA_CORE_INFO: client.homeassistant.info(),
DATA_SUPERVISOR_INFO: client.supervisor.info(),
DATA_OS_INFO: client.os.info(),
DATA_STORE: client.store.info(),
}
if CONTAINER_STATS in container_updates[CORE_CONTAINER]:
updates[DATA_CORE_STATS] = client.homeassistant.stats()
if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]:
updates[DATA_SUPERVISOR_STATS] = client.supervisor.stats()
# Pull off addons.list results for further processing before caching
addons_list, *results = await asyncio.gather(
client.addons.list(), *updates.values()
)
for key, result in zip(updates, cast(list[ResponseData], results), strict=True):
data[key] = result.to_dict()
installed_addons = cast(list[InstalledAddon], addons_list)
data[DATA_ADDONS_LIST] = [addon.to_dict() for addon in installed_addons]
# Deprecated 2026.4.0: Folding repositories and addons.list results into supervisor_info for compatibility
# Can drop this after removal period
data[DATA_SUPERVISOR_INFO].update(
{
"repositories": data[DATA_STORE][ATTR_REPOSITORIES],
"addons": [addon.to_dict() for addon in installed_addons],
}
)
all_addons = {addon.slug for addon in installed_addons}
started_addons = {
addon.slug
for addon in installed_addons
if addon.state in {AddonState.STARTED, AddonState.STARTUP}
}
#
# Update addon info if its the first update or
# there is at least one entity that needs the data.
#
# When entities are added they call async_enable_container_updates
# to enable updates for the endpoints they need via
# async_added_to_hass. This ensures that we only update
# the data for the endpoints that are needed to avoid unnecessary
# API calls since otherwise we would fetch stats for all containers
# and throw them away.
#
for data_key, update_func, enabled_key, wanted_addons, needs_first_update in (
(
DATA_ADDONS_STATS,
self._update_addon_stats,
CONTAINER_STATS,
started_addons,
False,
),
(
DATA_ADDONS_INFO,
self._update_addon_info,
CONTAINER_INFO,
all_addons,
True,
),
):
container_data: dict[str, Any] = data.setdefault(data_key, {})
# Clean up cache
for slug in container_data.keys() - wanted_addons:
del container_data[slug]
# Update cache from API
container_data.update(
dict(
await asyncio.gather(
*[
update_func(slug)
for slug in wanted_addons
if (first_update and needs_first_update)
or enabled_key in container_updates[slug]
]
)
)
)
# Refresh jobs data
await self.jobs.refresh_data(first_update)
async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Update single addon stats."""
try:
stats = await self.supervisor_client.addons.addon_stats(slug)
except SupervisorError as err:
_LOGGER.warning("Could not fetch stats for %s: %s", slug, err)
return (slug, None)
return (slug, stats.to_dict())
async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Return the info for an addon."""
try:
info = await self.supervisor_client.addons.addon_info(slug)
except SupervisorError as err:
_LOGGER.warning("Could not fetch info for %s: %s", slug, err)
return (slug, None)
# Translate to legacy hassio names for compatibility
info_dict = info.to_dict()
info_dict["hassio_api"] = info_dict.pop("supervisor_api")
info_dict["hassio_role"] = info_dict.pop("supervisor_role")
return (slug, info_dict)
@callback
def async_enable_container_updates(
self, slug: str, entity_id: str, types: set[str]
) -> CALLBACK_TYPE:
"""Enable updates for an add-on."""
enabled_updates = self._container_updates[slug]
for key in types:
enabled_updates[key].add(entity_id)
@callback
def _remove() -> None:
for key in types:
enabled_updates[key].remove(entity_id)
return _remove
async def _async_refresh(
self,
log_failures: bool = True,
@@ -765,16 +616,14 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) -> None:
"""Refresh data."""
if not scheduled and not raise_on_auth_failed:
# Force reloading updates of main components for
# non-scheduled updates.
#
# Force refreshing updates for non-scheduled updates
# If `raise_on_auth_failed` is set, it means this is
# the first refresh and we do not want to delay
# startup or cause a timeout so we only refresh the
# updates if this is not a scheduled refresh and
# we are not doing the first refresh.
try:
await self.supervisor_client.reload_updates()
await self.supervisor_client.refresh_updates()
except SupervisorError as err:
_LOGGER.warning("Error on Supervisor API: %s", err)
@@ -782,6 +631,18 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error
)
async def force_addon_info_data_refresh(self, addon_slug: str) -> None:
"""Force refresh of addon info data for a specific addon."""
try:
slug, info = await self._update_addon_info(addon_slug)
if info is not None and DATA_KEY_ADDONS in self.data:
if slug in self.data[DATA_KEY_ADDONS]:
data = deepcopy(self.data)
data[DATA_KEY_ADDONS][slug].update(info)
self.async_set_updated_data(data)
except SupervisorError as err:
_LOGGER.warning("Could not refresh info for %s: %s", addon_slug, err)
@callback
def unload(self) -> None:
"""Clean up when config entry unloaded."""

View File

@@ -11,12 +11,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import ADDONS_COORDINATOR, COORDINATOR, STATS_COORDINATOR
from .coordinator import (
HassioAddOnDataUpdateCoordinator,
HassioDataUpdateCoordinator,
HassioStatsDataUpdateCoordinator,
)
from .const import ADDONS_COORDINATOR
from .coordinator import HassioDataUpdateCoordinator
async def async_get_config_entry_diagnostics(
@@ -24,9 +20,7 @@ async def async_get_config_entry_diagnostics(
config_entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: HassioDataUpdateCoordinator = hass.data[COORDINATOR]
addons_coordinator: HassioAddOnDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
stats_coordinator: HassioStatsDataUpdateCoordinator = hass.data[STATS_COORDINATOR]
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
@@ -59,7 +53,5 @@ async def async_get_config_entry_diagnostics(
return {
"coordinator_data": coordinator.data,
"addons_coordinator_data": addons_coordinator.data,
"stats_coordinator_data": stats_coordinator.data,
"devices": devices,
}

View File

@@ -13,6 +13,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTR_SLUG,
CONTAINER_STATS,
CORE_CONTAINER,
DATA_KEY_ADDONS,
DATA_KEY_CORE,
DATA_KEY_HOST,
@@ -20,79 +21,20 @@ from .const import (
DATA_KEY_OS,
DATA_KEY_SUPERVISOR,
DOMAIN,
KEY_TO_UPDATE_TYPES,
SUPERVISOR_CONTAINER,
)
from .coordinator import (
HassioAddOnDataUpdateCoordinator,
HassioDataUpdateCoordinator,
HassioStatsDataUpdateCoordinator,
)
from .coordinator import HassioDataUpdateCoordinator
class HassioStatsEntity(CoordinatorEntity[HassioStatsDataUpdateCoordinator]):
"""Base entity for container stats (CPU, memory)."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: HassioStatsDataUpdateCoordinator,
entity_description: EntityDescription,
*,
container_id: str,
data_key: str,
device_id: str,
unique_id_prefix: str,
) -> None:
"""Initialize base entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._container_id = container_id
self._data_key = data_key
self._attr_unique_id = f"{unique_id_prefix}_{entity_description.key}"
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_id)})
@property
def available(self) -> bool:
"""Return True if entity is available."""
if self._data_key == DATA_KEY_ADDONS:
return (
super().available
and DATA_KEY_ADDONS in self.coordinator.data
and self.entity_description.key
in (
self.coordinator.data[DATA_KEY_ADDONS].get(self._container_id) or {}
)
)
return (
super().available
and self._data_key in self.coordinator.data
and self.entity_description.key in self.coordinator.data[self._data_key]
)
async def async_added_to_hass(self) -> None:
"""Subscribe to stats updates."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_enable_container_updates(
self._container_id, self.entity_id, {CONTAINER_STATS}
)
)
# Stats are only fetched for containers with subscribed entities.
# The first coordinator refresh (before entities exist) has no
# subscribers, so no stats are fetched. Schedule a debounced
# refresh so that all stats entities registering during platform
# setup are batched into a single API call.
await self.coordinator.async_request_refresh()
class HassioAddonEntity(CoordinatorEntity[HassioAddOnDataUpdateCoordinator]):
class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Base entity for a Hass.io add-on."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: HassioAddOnDataUpdateCoordinator,
coordinator: HassioDataUpdateCoordinator,
entity_description: EntityDescription,
addon: dict[str, Any],
) -> None:
@@ -114,13 +56,16 @@ class HassioAddonEntity(CoordinatorEntity[HassioAddOnDataUpdateCoordinator]):
)
async def async_added_to_hass(self) -> None:
"""Subscribe to addon info updates."""
"""Subscribe to updates."""
await super().async_added_to_hass()
update_types = KEY_TO_UPDATE_TYPES[self.entity_description.key]
self.async_on_remove(
self.coordinator.async_enable_addon_info_updates(
self._addon_slug, self.entity_id
self.coordinator.async_enable_container_updates(
self._addon_slug, self.entity_id, update_types
)
)
if CONTAINER_STATS in update_types:
await self.coordinator.async_request_refresh()
class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
@@ -201,6 +146,18 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
in self.coordinator.data[DATA_KEY_SUPERVISOR]
)
async def async_added_to_hass(self) -> None:
"""Subscribe to updates."""
await super().async_added_to_hass()
update_types = KEY_TO_UPDATE_TYPES[self.entity_description.key]
self.async_on_remove(
self.coordinator.async_enable_container_updates(
SUPERVISOR_CONTAINER, self.entity_id, update_types
)
)
if CONTAINER_STATS in update_types:
await self.coordinator.async_request_refresh()
class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Base Entity for Core."""
@@ -227,6 +184,18 @@ class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
and self.entity_description.key in self.coordinator.data[DATA_KEY_CORE]
)
async def async_added_to_hass(self) -> None:
"""Subscribe to updates."""
await super().async_added_to_hass()
update_types = KEY_TO_UPDATE_TYPES[self.entity_description.key]
self.async_on_remove(
self.coordinator.async_enable_container_updates(
CORE_CONTAINER, self.entity_id, update_types
)
)
if CONTAINER_STATS in update_types:
await self.coordinator.async_request_refresh()
class HassioMountEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Base Entity for Mount."""

View File

@@ -28,6 +28,7 @@ from homeassistant.helpers.issue_registry import (
)
from .const import (
ADDONS_COORDINATOR,
ATTR_DATA,
ATTR_HEALTHY,
ATTR_SLUG,
@@ -37,7 +38,6 @@ from .const import (
ATTR_UNSUPPORTED_REASONS,
ATTR_UPDATE_KEY,
ATTR_WS_EVENT,
COORDINATOR,
DOMAIN,
EVENT_HEALTH_CHANGED,
EVENT_ISSUE_CHANGED,
@@ -418,7 +418,7 @@ class SupervisorIssues:
def _async_coordinator_refresh(self) -> None:
"""Refresh coordinator to update latest data in entities."""
coordinator: HassioDataUpdateCoordinator | None
if coordinator := self._hass.data.get(COORDINATOR):
if coordinator := self._hass.data.get(ADDONS_COORDINATOR):
coordinator.config_entry.async_create_task(
self._hass, coordinator.async_refresh()
)

View File

@@ -17,24 +17,20 @@ from .const import (
ADDONS_COORDINATOR,
ATTR_CPU_PERCENT,
ATTR_MEMORY_PERCENT,
ATTR_SLUG,
ATTR_VERSION,
ATTR_VERSION_LATEST,
COORDINATOR,
CORE_CONTAINER,
DATA_KEY_ADDONS,
DATA_KEY_CORE,
DATA_KEY_HOST,
DATA_KEY_OS,
DATA_KEY_SUPERVISOR,
STATS_COORDINATOR,
SUPERVISOR_CONTAINER,
)
from .entity import (
HassioAddonEntity,
HassioCoreEntity,
HassioHostEntity,
HassioOSEntity,
HassioStatsEntity,
HassioSupervisorEntity,
)
COMMON_ENTITY_DESCRIPTIONS = (
@@ -67,7 +63,10 @@ STATS_ENTITY_DESCRIPTIONS = (
),
)
ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + STATS_ENTITY_DESCRIPTIONS
CORE_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS
OS_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS
SUPERVISOR_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS
HOST_ENTITY_DESCRIPTIONS = (
SensorEntityDescription(
@@ -115,64 +114,36 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Sensor set up for Hass.io config entry."""
addons_coordinator = hass.data[ADDONS_COORDINATOR]
coordinator = hass.data[COORDINATOR]
stats_coordinator = hass.data[STATS_COORDINATOR]
coordinator = hass.data[ADDONS_COORDINATOR]
entities: list[SensorEntity] = []
# Add-on non-stats sensors (version, version_latest)
entities.extend(
entities: list[
HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor | HostSensor
] = [
HassioAddonSensor(
addon=addon,
coordinator=addons_coordinator,
coordinator=coordinator,
entity_description=entity_description,
)
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
for entity_description in COMMON_ENTITY_DESCRIPTIONS
)
for addon in coordinator.data[DATA_KEY_ADDONS].values()
for entity_description in ADDON_ENTITY_DESCRIPTIONS
]
# Add-on stats sensors (cpu_percent, memory_percent)
entities.extend(
HassioStatsSensor(
coordinator=stats_coordinator,
CoreSensor(
coordinator=coordinator,
entity_description=entity_description,
container_id=addon[ATTR_SLUG],
data_key=DATA_KEY_ADDONS,
device_id=addon[ATTR_SLUG],
unique_id_prefix=addon[ATTR_SLUG],
)
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
for entity_description in STATS_ENTITY_DESCRIPTIONS
for entity_description in CORE_ENTITY_DESCRIPTIONS
)
# Core stats sensors
entities.extend(
HassioStatsSensor(
coordinator=stats_coordinator,
SupervisorSensor(
coordinator=coordinator,
entity_description=entity_description,
container_id=CORE_CONTAINER,
data_key=DATA_KEY_CORE,
device_id="core",
unique_id_prefix="home_assistant_core",
)
for entity_description in STATS_ENTITY_DESCRIPTIONS
for entity_description in SUPERVISOR_ENTITY_DESCRIPTIONS
)
# Supervisor stats sensors
entities.extend(
HassioStatsSensor(
coordinator=stats_coordinator,
entity_description=entity_description,
container_id=SUPERVISOR_CONTAINER,
data_key=DATA_KEY_SUPERVISOR,
device_id="supervisor",
unique_id_prefix="home_assistant_supervisor",
)
for entity_description in STATS_ENTITY_DESCRIPTIONS
)
# Host sensors
entities.extend(
HostSensor(
coordinator=coordinator,
@@ -181,7 +152,6 @@ async def async_setup_entry(
for entity_description in HOST_ENTITY_DESCRIPTIONS
)
# OS sensors
if coordinator.is_hass_os:
entities.extend(
HassioOSSensor(
@@ -205,21 +175,8 @@ class HassioAddonSensor(HassioAddonEntity, SensorEntity):
]
class HassioStatsSensor(HassioStatsEntity, SensorEntity):
"""Sensor to track container stats."""
@property
def native_value(self) -> str:
"""Return native value of entity."""
if self._data_key == DATA_KEY_ADDONS:
return self.coordinator.data[DATA_KEY_ADDONS][self._container_id][
self.entity_description.key
]
return self.coordinator.data[self._data_key][self.entity_description.key]
class HassioOSSensor(HassioOSEntity, SensorEntity):
"""Sensor to track a Hass.io OS attribute."""
"""Sensor to track a Hass.io add-on attribute."""
@property
def native_value(self) -> str:
@@ -227,6 +184,24 @@ class HassioOSSensor(HassioOSEntity, SensorEntity):
return self.coordinator.data[DATA_KEY_OS][self.entity_description.key]
class CoreSensor(HassioCoreEntity, SensorEntity):
"""Sensor to track a core attribute."""
@property
def native_value(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_CORE][self.entity_description.key]
class SupervisorSensor(HassioSupervisorEntity, SensorEntity):
"""Sensor to track a supervisor attribute."""
@property
def native_value(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_SUPERVISOR][self.entity_description.key]
class HostSensor(HassioHostEntity, SensorEntity):
"""Sensor to track a host attribute."""

View File

@@ -32,6 +32,7 @@ from homeassistant.helpers import (
from homeassistant.util.dt import now
from .const import (
ADDONS_COORDINATOR,
ATTR_ADDON,
ATTR_ADDONS,
ATTR_APP,
@@ -44,7 +45,6 @@ from .const import (
ATTR_LOCATION,
ATTR_PASSWORD,
ATTR_SLUG,
COORDINATOR,
DOMAIN,
SupervisorEntityModel,
)
@@ -417,7 +417,7 @@ def async_register_network_storage_services(
if (
device.name is None
or device.model != SupervisorEntityModel.MOUNT
or (coordinator := hass.data.get(COORDINATOR)) is None
or (coordinator := hass.data.get(ADDONS_COORDINATOR)) is None
or coordinator.entry_id not in device.config_entries
):
raise ServiceValidationError(

View File

@@ -25,7 +25,6 @@ from .const import (
ATTR_AUTO_UPDATE,
ATTR_VERSION,
ATTR_VERSION_LATEST,
COORDINATOR,
DATA_KEY_ADDONS,
DATA_KEY_CORE,
DATA_KEY_OS,
@@ -52,9 +51,9 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Supervisor update based on a config entry."""
coordinator = hass.data[COORDINATOR]
coordinator = hass.data[ADDONS_COORDINATOR]
entities: list[UpdateEntity] = [
entities = [
SupervisorSupervisorUpdateEntity(
coordinator=coordinator,
entity_description=ENTITY_DESCRIPTION,
@@ -65,6 +64,15 @@ async def async_setup_entry(
),
]
entities.extend(
SupervisorAddonUpdateEntity(
addon=addon,
coordinator=coordinator,
entity_description=ENTITY_DESCRIPTION,
)
for addon in coordinator.data[DATA_KEY_ADDONS].values()
)
if coordinator.is_hass_os:
entities.append(
SupervisorOSUpdateEntity(
@@ -73,16 +81,6 @@ async def async_setup_entry(
)
)
addons_coordinator = hass.data[ADDONS_COORDINATOR]
entities.extend(
SupervisorAddonUpdateEntity(
addon=addon,
coordinator=addons_coordinator,
entity_description=ENTITY_DESCRIPTION,
)
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
)
async_add_entities(entities)

View File

@@ -62,32 +62,14 @@ class ResourceStorageCollection(collection.DictStorageCollection):
)
self.ll_config = ll_config
async def _async_ensure_loaded(self) -> None:
"""Ensure the collection has been loaded from storage."""
async def async_get_info(self) -> dict[str, int]:
"""Return the resources info for YAML mode."""
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:
@@ -136,6 +118,10 @@ 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,8 +71,6 @@ 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
@@ -117,8 +115,6 @@ 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
@@ -218,8 +214,6 @@ 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"),
)
@@ -823,8 +817,7 @@ 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, creator_fabric_index,
last_modified_fabric_index, and next_credential_index.
Returns typed dict with credential_exists, user_index, next_credential_index.
Raises HomeAssistantError on failure.
"""
lock_endpoint = _get_lock_endpoint_or_raise(node)
@@ -846,7 +839,5 @@ 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

@@ -7,7 +7,6 @@ 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,
@@ -35,7 +34,7 @@ EVENT_DESCRIPTIONS: tuple[RingEventEntityDescription, ...] = (
key=KIND_DING,
translation_key=KIND_DING,
device_class=EventDeviceClass.DOORBELL,
event_types=[DoorbellEventType.RING],
event_types=[KIND_DING],
capability=RingCapability.DING,
),
RingEventEntityDescription(
@@ -101,10 +100,7 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity)
@callback
def _handle_coordinator_update(self) -> None:
if (alert := self._get_coordinator_alert()) and not alert.is_update:
if alert.kind == KIND_DING:
self._async_handle_event(DoorbellEventType.RING)
else:
self._async_handle_event(alert.kind)
self._async_handle_event(alert.kind)
super()._handle_coordinator_update()
@property

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,21 @@
"""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 PLATFORMS
from .hub import SIAConfigEntry, SIAHub
from .const import DOMAIN, PLATFORMS
from .hub import SIAHub
async def async_setup_entry(hass: HomeAssistant, entry: SIAConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up sia from a config entry."""
hub = SIAHub(hass, entry)
hub: SIAHub = 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)
@@ -20,15 +23,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: SIAConfigEntry) -> 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: SIAConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
await entry.runtime_data.async_shutdown()
hub: SIAHub = hass.data[DOMAIN].pop(entry.entry_id)
await hub.async_shutdown()
return unload_ok

View File

@@ -16,7 +16,12 @@ from pysiaalarm import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_PORT, CONF_PROTOCOL
from homeassistant.core import callback
@@ -31,7 +36,7 @@ from .const import (
DOMAIN,
TITLE,
)
from .hub import SIAConfigEntry, SIAHub
from .hub import SIAHub
_LOGGER = logging.getLogger(__name__)
@@ -95,7 +100,7 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: SIAConfigEntry,
config_entry: ConfigEntry,
) -> SIAOptionsFlowHandler:
"""Get the options flow for this handler."""
return SIAOptionsFlowHandler(config_entry)
@@ -174,9 +179,7 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
class SIAOptionsFlowHandler(OptionsFlow):
"""Handle SIA options."""
config_entry: SIAConfigEntry
def __init__(self, config_entry: SIAConfigEntry) -> None:
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize SIA options flow."""
self.options = deepcopy(dict(config_entry.options))
self.hub: SIAHub | None = None
@@ -186,7 +189,7 @@ class SIAOptionsFlowHandler(OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the SIA options."""
self.hub = self.config_entry.runtime_data
self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id]
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, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
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,8 +28,6 @@ from .utils import get_event_data_from_sia_event
_LOGGER = logging.getLogger(__name__)
type SIAConfigEntry = ConfigEntry[SIAHub]
DEFAULT_TIMEBAND = (80, 40)
@@ -39,11 +37,11 @@ class SIAHub:
def __init__(
self,
hass: HomeAssistant,
entry: SIAConfigEntry,
entry: ConfigEntry,
) -> None:
"""Create the SIAHub."""
self._hass = hass
self._entry = entry
self._hass: HomeAssistant = hass
self._entry: ConfigEntry = entry
self._port: int = entry.data[CONF_PORT]
self._title: str = entry.title
self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS])
@@ -133,7 +131,7 @@ class SIAHub:
@staticmethod
async def async_config_entry_updated(
hass: HomeAssistant, config_entry: SIAConfigEntry
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Handle signals of config entry being updated.
@@ -141,8 +139,8 @@ class SIAHub:
Second, unload underlying platforms, and then setup platforms, this reflects any changes in number of zones.
"""
if config_entry.state != ConfigEntryState.LOADED:
if not (hub := hass.data[DOMAIN].get(config_entry.entry_id)):
return
config_entry.runtime_data.update_accounts()
hub.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
from typing import Any, cast
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, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CODE,
ATTR_DEVICE_ID,
@@ -88,8 +88,6 @@ 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"
@@ -225,15 +223,10 @@ 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 (
(entry := hass.config_entries.async_get_entry(entry_id)) is None
or entry.domain != DOMAIN
or entry.state != ConfigEntryState.LOADED
):
if (simplisafe := hass.data[DOMAIN].get(entry_id)) is None:
continue
return entry.runtime_data.systems[system_id]
return cast(SystemType, simplisafe.systems[system_id])
raise ValueError(f"No system for device ID: {device_id}")
@@ -293,7 +286,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: SimpliSafeConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SimpliSafe as config entry."""
_async_standardize_config_entry(hass, entry)
@@ -317,7 +310,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SimpliSafeConfigEntry) -
except SimplipyError as err:
raise ConfigEntryNotReady from err
entry.runtime_data = simplisafe
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = simplisafe
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -402,9 +396,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: SimpliSafeConfigEntry) -
return True
async def async_unload_entry(hass: HomeAssistant, entry: SimpliSafeConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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,11 +28,12 @@ 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, SimpliSafeConfigEntry
from . import SimpliSafe
from .const import (
ATTR_ALARM_DURATION,
ATTR_ALARM_VOLUME,
@@ -43,6 +44,7 @@ from .const import (
ATTR_EXIT_DELAY_HOME,
ATTR_LIGHT,
ATTR_VOICE_PROMPT_VOLUME,
DOMAIN,
LOGGER,
)
from .entity import SimpliSafeEntity
@@ -102,11 +104,11 @@ WEBSOCKET_EVENTS_TO_LISTEN_FOR = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SimpliSafeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a SimpliSafe alarm control panel based on a config entry."""
simplisafe = entry.runtime_data
simplisafe = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[SimpliSafeAlarm(simplisafe, system) for system in simplisafe.systems.values()],
True,

View File

@@ -2,8 +2,6 @@
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
@@ -13,12 +11,13 @@ 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, SimpliSafeConfigEntry
from .const import LOGGER
from . import SimpliSafe
from .const import DOMAIN, LOGGER
from .entity import SimpliSafeEntity
SUPPORTED_BATTERY_SENSOR_TYPES = [
@@ -60,11 +59,11 @@ TRIGGERED_SENSOR_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
entry: SimpliSafeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe binary sensors based on a config entry."""
simplisafe = entry.runtime_data
simplisafe = hass.data[DOMAIN][entry.entry_id]
sensors: list[BatteryBinarySensor | TriggeredBinarySensor] = []
@@ -73,22 +72,18 @@ 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,
cast(SensorV3, sensor),
sensor,
TRIGGERED_SENSOR_TYPES[sensor.type],
)
)
if sensor.type in SUPPORTED_BATTERY_SENSOR_TYPES:
sensors.append(
BatteryBinarySensor(simplisafe, system, cast(DeviceV3, sensor))
)
sensors.append(BatteryBinarySensor(simplisafe, system, sensor))
sensors.extend(
BatteryBinarySensor(simplisafe, system, lock)

View File

@@ -9,12 +9,14 @@ 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, SimpliSafeConfigEntry
from . import SimpliSafe
from .const import DOMAIN
from .entity import SimpliSafeEntity
from .typing import SystemType
@@ -45,11 +47,11 @@ BUTTON_DESCRIPTIONS = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SimpliSafeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe buttons based on a config entry."""
simplisafe = entry.runtime_data
simplisafe = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[

View File

@@ -14,12 +14,16 @@ from simplipy.util.auth import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
ConfigEntry,
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"
@@ -64,7 +68,7 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: SimpliSafeConfigEntry,
config_entry: ConfigEntry,
) -> SimpliSafeOptionsFlowHandler:
"""Define the config flow to handle options."""
return SimpliSafeOptionsFlowHandler()

View File

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

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from typing import Any
from simplipy.device.lock import Lock, LockStates
from simplipy.errors import SimplipyError
@@ -10,12 +10,13 @@ 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, SimpliSafeConfigEntry
from .const import LOGGER
from . import SimpliSafe
from .const import DOMAIN, LOGGER
from .entity import SimpliSafeEntity
ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
@@ -31,11 +32,11 @@ WEBSOCKET_EVENTS_TO_LISTEN_FOR = (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED)
async def async_setup_entry(
hass: HomeAssistant,
entry: SimpliSafeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe locks based on a config entry."""
simplisafe = entry.runtime_data
simplisafe = hass.data[DOMAIN][entry.entry_id]
locks: list[SimpliSafeLock] = []
for system in simplisafe.systems.values():
@@ -43,8 +44,6 @@ 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,8 +2,6 @@
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
@@ -13,22 +11,23 @@ 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, SimpliSafeConfigEntry
from .const import LOGGER
from . import SimpliSafe
from .const import DOMAIN, LOGGER
from .entity import SimpliSafeEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SimpliSafeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SimpliSafe freeze sensors based on a config entry."""
simplisafe = entry.runtime_data
simplisafe = hass.data[DOMAIN][entry.entry_id]
sensors: list[SimplisafeFreezeSensor] = []
for system in simplisafe.systems.values():
@@ -36,10 +35,8 @@ 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, cast(SensorV3, sensor))
SimplisafeFreezeSensor(simplisafe, system, sensor)
for sensor in system.sensors.values()
if sensor.type == DeviceTypes.TEMPERATURE
)

View File

@@ -7,12 +7,14 @@ 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 .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import SkybellDataUpdateCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -23,7 +25,7 @@ PLATFORMS = [
]
async def async_setup_entry(hass: HomeAssistant, entry: SkybellConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Skybell from a config entry."""
email = entry.data[CONF_EMAIL]
password = entry.data[CONF_PASSWORD]
@@ -51,12 +53,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: SkybellConfigEntry) -> b
for coordinator in device_coordinators
]
)
entry.runtime_data = device_coordinators
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device_coordinators
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: SkybellConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -9,10 +9,12 @@ 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 .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
from . import DOMAIN
from .coordinator import SkybellDataUpdateCoordinator
from .entity import SkybellEntity
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
@@ -30,14 +32,14 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SkybellConfigEntry,
entry: ConfigEntry,
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 entry.runtime_data
for coordinator in hass.data[DOMAIN][entry.entry_id]
)

View File

@@ -7,12 +7,14 @@ 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 .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import SkybellDataUpdateCoordinator
from .entity import SkybellEntity
CAMERA_TYPES: tuple[CameraEntityDescription, ...] = (
@@ -29,13 +31,13 @@ CAMERA_TYPES: tuple[CameraEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SkybellConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Skybell camera."""
entities = []
for description in CAMERA_TYPES:
for coordinator in entry.runtime_data:
for coordinator in hass.data[DOMAIN][entry.entry_id]:
if description.key == "avatar":
entities.append(SkybellCamera(coordinator, description))
else:

View File

@@ -10,19 +10,14 @@ 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: SkybellConfigEntry
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: SkybellConfigEntry,
device: SkybellDevice,
self, hass: HomeAssistant, config_entry: ConfigEntry, device: SkybellDevice
) -> None:
"""Initialize the coordinator."""
super().__init__(

View File

@@ -13,22 +13,23 @@ 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 .coordinator import SkybellConfigEntry
from .const import DOMAIN
from .entity import SkybellEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SkybellConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Skybell switch."""
async_add_entities(
SkybellLight(coordinator, LightEntityDescription(key="light"))
for coordinator in entry.runtime_data
for coordinator in hass.data[DOMAIN][entry.entry_id]
)

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 .coordinator import SkybellConfigEntry
from .entity import SkybellEntity
from .entity import DOMAIN, SkybellEntity
@dataclass(frozen=True, kw_only=True)
@@ -89,13 +89,13 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SkybellConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Skybell sensor."""
async_add_entities(
SkybellSensor(coordinator, description)
for coordinator in entry.runtime_data
for coordinator in hass.data[DOMAIN][entry.entry_id]
for description in SENSOR_TYPES
if coordinator.device.owner or description.key not in CONST.ATTR_OWNER_STATS
)

View File

@@ -5,10 +5,11 @@ 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 .coordinator import SkybellConfigEntry
from .const import DOMAIN
from .entity import SkybellEntity
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
@@ -29,13 +30,13 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SkybellConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SkyBell switch."""
async_add_entities(
SkybellSwitch(coordinator, description)
for coordinator in entry.runtime_data
for coordinator in hass.data[DOMAIN][entry.entry_id]
for description in SWITCH_TYPES
)

View File

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

View File

@@ -1,10 +1,14 @@
"""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 . import SlackConfigEntry, SlackData
from .const import DEFAULT_NAME, DOMAIN
from .const import ATTR_URL, ATTR_USER_ID, DATA_CLIENT, DEFAULT_NAME, DOMAIN
class SlackEntity(Entity):
@@ -12,16 +16,16 @@ class SlackEntity(Entity):
def __init__(
self,
data: SlackData,
data: dict[str, AsyncWebClient],
description: EntityDescription,
entry: SlackConfigEntry,
entry: ConfigEntry,
) -> None:
"""Initialize a Slack entity."""
self._client = data.client
self._client: AsyncWebClient = data[DATA_CLIENT]
self.entity_description = description
self._attr_unique_id = f"{data.user_id}_{description.key}"
self._attr_unique_id = f"{data[ATTR_USER_ID]}_{description.key}"
self._attr_device_info = DeviceInfo(
configuration_url=data.url,
configuration_url=str(data[ATTR_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 . import SlackConfigEntry
from .const import ATTR_SNOOZE
from .const import ATTR_SNOOZE, DOMAIN, SLACK_DATA
from .entity import SlackEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SlackConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Slack sensor."""
"""Set up the Slack select."""
async_add_entities(
[
SlackSensorEntity(
entry.runtime_data,
hass.data[DOMAIN][entry.entry_id][SLACK_DATA],
SensorEntityDescription(
key="do_not_disturb_until",
translation_key="do_not_disturb_until",

View File

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

View File

@@ -6,21 +6,22 @@ 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 ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .entity import SleepIQSleeperEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SleepIQConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed binary sensors."""
data = entry.runtime_data
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
IsInBedBinarySensor(data.data_coordinator, bed, sleeper)
for bed in data.client.beds.values()

View File

@@ -9,10 +9,12 @@ 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 .coordinator import SleepIQConfigEntry
from .const import DOMAIN
from .coordinator import SleepIQData
from .entity import SleepIQEntity
@@ -41,11 +43,11 @@ ENTITY_DESCRIPTIONS = [
async def async_setup_entry(
hass: HomeAssistant,
entry: SleepIQConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sleep number buttons."""
data = entry.runtime_data
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SleepNumberButton(bed, ed)

View File

@@ -18,18 +18,16 @@ 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: SleepIQConfigEntry
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: SleepIQConfigEntry,
config_entry: ConfigEntry,
client: AsyncSleepIQ,
) -> None:
"""Initialize coordinator."""
@@ -53,12 +51,12 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]):
"""SleepIQ data update coordinator."""
config_entry: SleepIQConfigEntry
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: SleepIQConfigEntry,
config_entry: ConfigEntry,
client: AsyncSleepIQ,
) -> None:
"""Initialize coordinator."""
@@ -80,12 +78,12 @@ class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]):
class SleepIQSleepDataCoordinator(DataUpdateCoordinator[None]):
"""SleepIQ sleep health data coordinator."""
config_entry: SleepIQConfigEntry
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: SleepIQConfigEntry,
config_entry: ConfigEntry,
client: AsyncSleepIQ,
) -> None:
"""Initialize coordinator."""

View File

@@ -6,10 +6,12 @@ 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 .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .entity import SleepIQBedEntity
_LOGGER = logging.getLogger(__name__)
@@ -17,11 +19,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: SleepIQConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed lights."""
data = entry.runtime_data
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SleepIQLightEntity(data.data_coordinator, bed, light)
for bed in data.client.beds.values()

View File

@@ -21,6 +21,7 @@ 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
@@ -28,12 +29,13 @@ 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 SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .entity import SleepIQBedEntity, sleeper_for_side
@@ -178,11 +180,11 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
async def async_setup_entry(
hass: HomeAssistant,
entry: SleepIQConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed sensors."""
data = entry.runtime_data
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
entities: list[SleepIQNumberEntity] = []
for bed in data.client.beds.values():

View File

@@ -13,21 +13,22 @@ 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, FOOT_WARMER
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
from .const import CORE_CLIMATE, DOMAIN, FOOT_WARMER
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
from .entity import SleepIQBedEntity, SleepIQSleeperEntity, sleeper_for_side
async def async_setup_entry(
hass: HomeAssistant,
entry: SleepIQConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ foundation preset select entities."""
data = entry.runtime_data
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
entities: list[SleepIQBedEntity] = []
for bed in data.client.beds.values():
entities.extend(

View File

@@ -13,11 +13,13 @@ 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,
@@ -27,7 +29,7 @@ from .const import (
SLEEP_SCORE,
)
from .coordinator import (
SleepIQConfigEntry,
SleepIQData,
SleepIQDataUpdateCoordinator,
SleepIQSleepDataCoordinator,
)
@@ -110,11 +112,11 @@ SLEEP_HEALTH_SENSORS: tuple[SleepIQSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SleepIQConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SleepIQ bed sensors."""
data = entry.runtime_data
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
entities: list[SensorEntity] = []

View File

@@ -7,20 +7,22 @@ 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 .coordinator import SleepIQConfigEntry, SleepIQPauseUpdateCoordinator
from .const import DOMAIN
from .coordinator import SleepIQData, SleepIQPauseUpdateCoordinator
from .entity import SleepIQBedEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SleepIQConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sleep number switches."""
data = entry.runtime_data
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SleepNumberPrivateSwitch(data.pause_coordinator, bed)
for bed in data.client.beds.values()

View File

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

View File

@@ -19,6 +19,7 @@ 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 (
@@ -28,7 +29,6 @@ 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: SoundTouchConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Bose SoundTouch media player based on a config entry."""
device = entry.runtime_data.device
device = hass.data[DOMAIN][entry.entry_id].device
media_player = SoundTouchMediaPlayer(device)
async_add_entities([media_player], True)
entry.runtime_data.media_player = media_player
hass.data[DOMAIN][entry.entry_id].media_player = media_player
class SoundTouchMediaPlayer(MediaPlayerEntity):
@@ -388,16 +388,14 @@ class SoundTouchMediaPlayer(MediaPlayerEntity):
def _get_instance_by_ip(self, ip_address):
"""Search and return a SoundTouchDevice instance by it's IP address."""
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
data = entry.runtime_data
for data in self.hass.data[DOMAIN].values():
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 entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
data = entry.runtime_data
for data in self.hass.data[DOMAIN].values():
if data.device.config.device_id == instance_id:
return data.media_player
return None

View File

@@ -2,16 +2,17 @@
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 LOGGER
from .coordinator import SRPEnergyConfigEntry, SRPEnergyDataUpdateCoordinator
from .const import DOMAIN, LOGGER
from .coordinator import SRPEnergyDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: SRPEnergyConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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]
@@ -29,13 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: SRPEnergyConfigEntry) ->
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: SRPEnergyConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -23,19 +23,14 @@ 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: SRPEnergyConfigEntry
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: SRPEnergyConfigEntry,
client: SrpEnergyClient,
self, hass: HomeAssistant, config_entry: ConfigEntry, client: SrpEnergyClient
) -> None:
"""Initialize the srp_energy data coordinator."""
self._client = client

View File

@@ -7,6 +7,7 @@ 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
@@ -14,17 +15,19 @@ 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: SRPEnergyConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SRP Energy Usage sensor."""
async_add_entities([SrpEntity(entry.runtime_data, entry)])
coordinator: SRPEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([SrpEntity(coordinator, entry)])
class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity):
@@ -40,7 +43,7 @@ class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity)
def __init__(
self,
coordinator: SRPEnergyDataUpdateCoordinator,
config_entry: SRPEnergyConfigEntry,
config_entry: ConfigEntry,
) -> None:
"""Initialize the SrpEntity class."""
super().__init__(coordinator)

View File

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

View File

@@ -3,20 +3,22 @@
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 .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator
from . import StreamlabsCoordinator
from .const import DOMAIN
from .entity import StreamlabsWaterEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: StreamlabsConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Streamlabs water binary sensor from a config entry."""
coordinator = entry.runtime_data
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
StreamlabsAwayMode(coordinator, location_id) for location_id in coordinator.data

View File

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

View File

@@ -10,12 +10,15 @@ 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 .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator, StreamlabsData
from . import StreamlabsCoordinator
from .const import DOMAIN
from .coordinator import StreamlabsData
from .entity import StreamlabsWaterEntity
@@ -56,11 +59,11 @@ SENSORS: tuple[StreamlabsWaterSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: StreamlabsConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Streamlabs water sensor from a config entry."""
coordinator = entry.runtime_data
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
StreamLabsSensor(coordinator, location_id, entity_description)

View File

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

View File

@@ -12,24 +12,26 @@ 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 .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
from .const import DOMAIN
from .coordinator import SurePetcareDataCoordinator
from .entity import SurePetcareEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SurePetcareConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sure PetCare Flaps binary sensors based on a config entry."""
entities: list[SurePetcareBinarySensor] = []
coordinator = entry.runtime_data
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
for surepy_entity in coordinator.data.values():
# connectivity

View File

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

View File

@@ -8,21 +8,23 @@ 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 .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
from .const import DOMAIN
from .coordinator import SurePetcareDataCoordinator
from .entity import SurePetcareEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SurePetcareConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sure PetCare locks on a config entry."""
coordinator = entry.runtime_data
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SurePetcareLock(surepy_entity.id, coordinator, lock_state)

View File

@@ -10,25 +10,26 @@ 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 SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
from .const import DOMAIN, SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW
from .coordinator import SurePetcareDataCoordinator
from .entity import SurePetcareEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SurePetcareConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sure PetCare Flaps sensors."""
entities: list[SurePetcareEntity] = []
coordinator = entry.runtime_data
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
for surepy_entity in coordinator.data.values():
if surepy_entity.type in [

View File

@@ -9,6 +9,7 @@ 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
@@ -16,7 +17,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 SwitchBeeConfigEntry, SwitchBeeCoordinator
from .coordinator import SwitchBeeCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -52,9 +53,10 @@ async def get_api_object(
return api
async def async_setup_entry(hass: HomeAssistant, entry: SwitchBeeConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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]
@@ -65,28 +67,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchBeeConfigEntry) ->
await coordinator.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(update_listener))
entry.runtime_data = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: SwitchBeeConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def update_listener(
hass: HomeAssistant, config_entry: SwitchBeeConfigEntry
) -> None:
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: SwitchBeeConfigEntry
) -> bool:
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)

View File

@@ -4,21 +4,23 @@ 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 .coordinator import SwitchBeeConfigEntry
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .entity import SwitchBeeEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchBeeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbee button."""
coordinator = entry.runtime_data
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SwitchBeeButton(switchbee_device, coordinator)
for switchbee_device in coordinator.data.values()

View File

@@ -23,12 +23,14 @@ 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 .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
FAN_SB_TO_HASS = {
@@ -73,11 +75,11 @@ SUPPORTED_FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW]
async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchBeeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBee climate."""
coordinator = entry.runtime_data
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SwitchBeeClimateEntity(switchbee_device, coordinator)
for switchbee_device in coordinator.data.values()

View File

@@ -19,18 +19,16 @@ 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: SwitchBeeConfigEntry
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: SwitchBeeConfigEntry,
config_entry: ConfigEntry,
swb_api: CentralUnitPolling | CentralUnitWsRPC,
) -> None:
"""Initialize."""

View File

@@ -14,21 +14,23 @@ 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 .coordinator import SwitchBeeConfigEntry
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchBeeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBee covers."""
coordinator = entry.runtime_data
"""Set up SwitchBee switch."""
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[CoverEntity] = []
for device in coordinator.data.values():

View File

@@ -2,17 +2,19 @@
from __future__ import annotations
from typing import Any, cast
from typing import Any
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 .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
MAX_BRIGHTNESS = 255
@@ -34,13 +36,13 @@ def _switchbee_brightness_to_hass(value: int) -> int:
async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchBeeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBee light."""
coordinator = entry.runtime_data
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SwitchBeeLightEntity(cast(SwitchBeeDimmer, switchbee_device), coordinator)
SwitchBeeLightEntity(switchbee_device, coordinator)
for switchbee_device in coordinator.data.values()
if switchbee_device.type == DeviceType.Dimmer
)

View File

@@ -14,21 +14,23 @@ 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 .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
from .const import DOMAIN
from .coordinator import SwitchBeeCoordinator
from .entity import SwitchBeeDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchBeeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbee switch."""
coordinator = entry.runtime_data
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SwitchBeeSwitchEntity(device, coordinator)

View File

@@ -114,25 +114,21 @@ 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,20 +145,6 @@
"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,12 +326,6 @@
}
}
}
},
"child_lock": {
"name": "Child lock"
},
"wireless_charging": {
"name": "Wireless charging"
}
},
"vacuum": {

View File

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

View File

@@ -11,11 +11,13 @@ 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 SwitchbotCloudConfigEntry
from . import SwitchbotCloudData
from .const import DOMAIN
from .coordinator import SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
@@ -135,11 +137,11 @@ BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
SwitchBotCloudBinarySensor(data.api, device, coordinator, description)

View File

@@ -12,10 +12,12 @@ 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 SwitchbotCloudConfigEntry, SwitchBotCoordinator
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import DOMAIN
from .entity import SwitchBotCloudEntity
@@ -56,11 +58,11 @@ BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
entities: list[SwitchBotCloudBot] = []
for device, coordinator in data.devices.buttons:
description_set = BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type]

View File

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

View File

@@ -18,21 +18,22 @@ 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 SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .const import COVER_ENTITY_AFTER_COMMAND_REFRESH
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import COVER_ENTITY_AFTER_COMMAND_REFRESH, DOMAIN
from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.covers

View File

@@ -13,12 +13,13 @@ 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 SwitchbotCloudConfigEntry
from .const import AFTER_COMMAND_REFRESH, AirPurifierMode
from . import SwitchbotCloudData
from .const import AFTER_COMMAND_REFRESH, DOMAIN, AirPurifierMode
from .entity import SwitchBotCloudEntity
_LOGGER = logging.getLogger(__name__)
@@ -27,11 +28,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
for device, coordinator in data.devices.fans:
if device.device_type.startswith("Air Purifier"):
async_add_entities(

View File

@@ -12,12 +12,13 @@ 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 SwitchbotCloudConfigEntry
from .const import AFTER_COMMAND_REFRESH, HUMIDITY_LEVELS, Humidifier2Mode
from . import SwitchbotCloudData
from .const import AFTER_COMMAND_REFRESH, DOMAIN, HUMIDITY_LEVELS, Humidifier2Mode
from .entity import SwitchBotCloudEntity
PARALLEL_UPDATES = 0
@@ -25,11 +26,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchbotCloudConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbot based on a config entry."""
data = entry.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SwitchBotHumidifier(data.api, device, coordinator)
if device.device_type == "Humidifier"

View File

@@ -6,20 +6,22 @@ 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 SwitchbotCloudConfigEntry, SwitchBotCoordinator
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import DOMAIN
from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.images

View File

@@ -14,11 +14,12 @@ 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 SwitchbotCloudConfigEntry, SwitchBotCoordinator
from .const import AFTER_COMMAND_REFRESH
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import AFTER_COMMAND_REFRESH, DOMAIN
from .entity import SwitchBotCloudEntity
@@ -34,11 +35,11 @@ def brightness_map_value(value: int) -> int:
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.lights

View File

@@ -5,20 +5,22 @@ 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 SwitchbotCloudConfigEntry, SwitchBotCoordinator
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import DOMAIN
from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
SwitchBotCloudLock(data.api, device, coordinator)
for device, coordinator in data.devices.locks

View File

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

View File

@@ -6,11 +6,12 @@ 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 SwitchbotCloudConfigEntry
from . import SwitchbotCloudData
from .const import AFTER_COMMAND_REFRESH, DOMAIN
from .coordinator import SwitchBotCoordinator
from .entity import SwitchBotCloudEntity
@@ -18,11 +19,11 @@ from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
entities: list[SwitchBotCloudSwitch] = []
for device, coordinator in data.devices.switches:
if device.device_type == "Relay Switch 2PM":

View File

@@ -17,11 +17,13 @@ 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 SwitchbotCloudConfigEntry
from . import SwitchbotCloudData
from .const import (
DOMAIN,
VACUUM_FAN_SPEED_MAX,
VACUUM_FAN_SPEED_QUIET,
VACUUM_FAN_SPEED_STANDARD,
@@ -33,11 +35,11 @@ from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: SwitchbotCloudConfigEntry,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data = config.runtime_data
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
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, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_COMMAND,
@@ -57,24 +57,7 @@ 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 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
from .coordinator import SystemBridgeDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -110,7 +93,7 @@ POWER_COMMAND_MAP = {
async def async_setup_entry(
hass: HomeAssistant,
entry: SystemBridgeConfigEntry,
entry: ConfigEntry,
) -> bool:
"""Set up System Bridge from a config entry."""
@@ -215,7 +198,8 @@ async def async_setup_entry(
# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
# Set up all platforms except notify
await hass.config_entries.async_forward_entry_setups(
@@ -232,7 +216,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],
)
)
@@ -265,7 +249,9 @@ 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 = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
processes: list[Process] = coordinator.data.processes
# Find process.id from list, raise ServiceValidationError if not found
@@ -289,7 +275,9 @@ 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 = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
# Find processes from list
items: list[dict[str, Any]] = [
@@ -307,7 +295,9 @@ 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 = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
response = await coordinator.websocket_client.open_path(
OpenPath(path=service_call.data[CONF_PATH])
)
@@ -316,7 +306,9 @@ 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 = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
response = await getattr(
coordinator.websocket_client,
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
@@ -326,7 +318,9 @@ 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 = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
response = await coordinator.websocket_client.open_url(
OpenUrl(url=service_call.data[CONF_URL])
)
@@ -334,7 +328,9 @@ async def async_setup_entry(
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
response = await coordinator.websocket_client.keyboard_keypress(
KeyboardKey(key=service_call.data[CONF_KEY])
)
@@ -342,7 +338,9 @@ async def async_setup_entry(
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
service_call.data[CONF_BRIDGE]
]
response = await coordinator.websocket_client.keyboard_text(
KeyboardText(text=service_call.data[CONF_TEXT])
)
@@ -448,27 +446,33 @@ async def async_setup_entry(
return True
async def async_unload_entry(
hass: HomeAssistant, entry: SystemBridgeConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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 = entry.runtime_data
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]
# 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: SystemBridgeConfigEntry
) -> None:
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload the config entry when it changed."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -10,11 +10,13 @@ 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 .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .data import SystemBridgeData
from .entity import SystemBridgeEntity
@@ -62,11 +64,11 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ..
async def async_setup_entry(
hass: HomeAssistant,
entry: SystemBridgeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up System Bridge binary sensor based on a config entry."""
coordinator = entry.runtime_data
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = [
SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT])

View File

@@ -36,20 +36,18 @@ 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: SystemBridgeConfigEntry
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
LOGGER: logging.Logger,
*,
entry: SystemBridgeConfigEntry,
entry: ConfigEntry,
) -> None:
"""Initialize global System Bridge data updater."""
self.title = entry.title

View File

@@ -15,11 +15,13 @@ 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 .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .data import SystemBridgeData
from .entity import SystemBridgeEntity
@@ -62,11 +64,11 @@ MEDIA_PLAYER_DESCRIPTION: Final[MediaPlayerEntityDescription] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: SystemBridgeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up System Bridge media players based on a config entry."""
coordinator = entry.runtime_data
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
data = coordinator.data
if data.media is not None:

View File

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

View File

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

View File

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

View File

@@ -3,21 +3,23 @@
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 .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import SystemBridgeDataUpdateCoordinator
from .entity import SystemBridgeEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: SystemBridgeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up System Bridge update based on a config entry."""
coordinator = entry.runtime_data
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[

View File

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

View File

@@ -107,10 +107,10 @@ async def test_diagnostics(
hass, hass_client, config_entry
)
assert "addons" in diagnostics["coordinator_data"]
assert "core" in diagnostics["coordinator_data"]
assert "supervisor" in diagnostics["coordinator_data"]
assert "os" in diagnostics["coordinator_data"]
assert "host" in diagnostics["coordinator_data"]
assert "addons" in diagnostics["addons_coordinator_data"]
assert len(diagnostics["devices"]) == 6

View File

@@ -155,7 +155,7 @@ async def test_setup_api_ping(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
assert get_core_info(hass)["version_latest"] == "1.0.0"
assert is_hassio(hass)
@@ -222,7 +222,7 @@ async def test_setup_api_push_api_data(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY)
)
@@ -238,7 +238,7 @@ async def test_setup_api_push_api_data_error(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
assert "Failed to update Home Assistant options in Supervisor: boom" in caplog.text
@@ -255,7 +255,7 @@ async def test_setup_api_push_api_data_server_host(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY, watchdog=False)
)
@@ -273,7 +273,7 @@ async def test_setup_api_push_api_data_default(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=8123, refresh_token=ANY)
)
@@ -350,7 +350,7 @@ async def test_setup_api_existing_hassio_user(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=8123, refresh_token=token.token)
)
@@ -367,7 +367,7 @@ async def test_setup_core_push_config(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
supervisor_client.supervisor.set_options.assert_called_once_with(
SupervisorOptions(timezone="testzone")
)
@@ -392,7 +392,7 @@ async def test_setup_core_push_config_error(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
assert "Failed to update Supervisor options: boom" in caplog.text
@@ -408,7 +408,7 @@ async def test_setup_hassio_no_additional_data(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
async def test_fail_setup_without_environ_var(hass: HomeAssistant) -> None:
@@ -732,12 +732,12 @@ async def test_service_calls_core(
await hass.async_block_till_done()
supervisor_client.homeassistant.stop.assert_called_once_with()
assert len(supervisor_client.mock_calls) == 21
assert len(supervisor_client.mock_calls) == 20
await hass.services.async_call("homeassistant", "check_config")
await hass.async_block_till_done()
assert len(supervisor_client.mock_calls) == 21
assert len(supervisor_client.mock_calls) == 20
with patch(
"homeassistant.config.async_check_ha_config_file", return_value=None
@@ -747,7 +747,7 @@ async def test_service_calls_core(
assert mock_check_config.called
supervisor_client.homeassistant.restart.assert_called_once_with()
assert len(supervisor_client.mock_calls) == 22
assert len(supervisor_client.mock_calls) == 21
@pytest.mark.parametrize(
@@ -903,13 +903,13 @@ async def test_coordinator_updates(
await hass.async_block_till_done()
# Initial refresh, no update refresh call
supervisor_client.reload_updates.assert_not_called()
supervisor_client.refresh_updates.assert_not_called()
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
await hass.async_block_till_done(wait_background_tasks=True)
# Scheduled refresh, no update refresh call
supervisor_client.reload_updates.assert_not_called()
supervisor_client.refresh_updates.assert_not_called()
await hass.services.async_call(
HOMEASSISTANT_DOMAIN,
@@ -924,15 +924,15 @@ async def test_coordinator_updates(
)
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
supervisor_client.reload_updates.assert_not_called()
supervisor_client.refresh_updates.assert_not_called()
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done(wait_background_tasks=True)
supervisor_client.reload_updates.assert_called_once()
supervisor_client.refresh_updates.assert_called_once()
supervisor_client.reload_updates.reset_mock()
supervisor_client.reload_updates.side_effect = SupervisorError("Unknown")
supervisor_client.refresh_updates.reset_mock()
supervisor_client.refresh_updates.side_effect = SupervisorError("Unknown")
await hass.services.async_call(
HOMEASSISTANT_DOMAIN,
SERVICE_UPDATE_ENTITY,
@@ -949,7 +949,7 @@ async def test_coordinator_updates(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
supervisor_client.reload_updates.assert_called_once()
supervisor_client.refresh_updates.assert_called_once()
assert "Error on Supervisor API: Unknown" in caplog.text
@@ -967,20 +967,20 @@ async def test_coordinator_updates_stats_entities_enabled(
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Initial refresh without stats
supervisor_client.reload_updates.assert_not_called()
supervisor_client.refresh_updates.assert_not_called()
# Stats entities trigger refresh on the stats coordinator,
# which does not call reload_updates
# Refresh with stats once we know which ones are needed
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
supervisor_client.reload_updates.assert_not_called()
supervisor_client.refresh_updates.assert_called_once()
supervisor_client.refresh_updates.reset_mock()
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
await hass.async_block_till_done()
supervisor_client.reload_updates.assert_not_called()
supervisor_client.refresh_updates.assert_not_called()
await hass.services.async_call(
HOMEASSISTANT_DOMAIN,
@@ -993,7 +993,7 @@ async def test_coordinator_updates_stats_entities_enabled(
},
blocking=True,
)
supervisor_client.reload_updates.assert_not_called()
supervisor_client.refresh_updates.assert_not_called()
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
@@ -1001,8 +1001,8 @@ async def test_coordinator_updates_stats_entities_enabled(
)
await hass.async_block_till_done()
supervisor_client.reload_updates.reset_mock()
supervisor_client.reload_updates.side_effect = SupervisorError("Unknown")
supervisor_client.refresh_updates.reset_mock()
supervisor_client.refresh_updates.side_effect = SupervisorError("Unknown")
await hass.services.async_call(
HOMEASSISTANT_DOMAIN,
SERVICE_UPDATE_ENTITY,
@@ -1019,7 +1019,7 @@ async def test_coordinator_updates_stats_entities_enabled(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
supervisor_client.reload_updates.assert_called_once()
supervisor_client.refresh_updates.assert_called_once()
assert "Error on Supervisor API: Unknown" in caplog.text
@@ -1064,7 +1064,7 @@ async def test_setup_hardware_integration(
await hass.async_block_till_done(wait_background_tasks=True)
assert result
assert len(supervisor_client.mock_calls) == 25
assert len(supervisor_client.mock_calls) == 23
assert len(mock_setup_entry.mock_calls) == 1

View File

@@ -11,11 +11,8 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant import config_entries
from homeassistant.components.hassio import DOMAIN
from homeassistant.components.hassio.const import (
HASSIO_STATS_UPDATE_INTERVAL,
REQUEST_REFRESH_DELAY,
)
from homeassistant.components.hassio import DOMAIN, HASSIO_UPDATE_INTERVAL
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
@@ -179,14 +176,14 @@ async def test_stats_addon_sensor(
assert hass.states.get(entity_id) is None
addon_stats.side_effect = SupervisorError
freezer.tick(HASSIO_STATS_UPDATE_INTERVAL + timedelta(seconds=1))
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert "Could not fetch stats" not in caplog.text
addon_stats.side_effect = None
freezer.tick(HASSIO_STATS_UPDATE_INTERVAL + timedelta(seconds=1))
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
@@ -202,13 +199,13 @@ async def test_stats_addon_sensor(
assert entity_registry.async_get(entity_id).disabled_by is None
# The config entry just reloaded, so we need to wait for the next update
freezer.tick(HASSIO_STATS_UPDATE_INTERVAL + timedelta(seconds=1))
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert hass.states.get(entity_id) is not None
freezer.tick(HASSIO_STATS_UPDATE_INTERVAL + timedelta(seconds=1))
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Verify that the entity have the expected state.
@@ -216,29 +213,10 @@ async def test_stats_addon_sensor(
assert state.state == expected
addon_stats.side_effect = SupervisorError
freezer.tick(HASSIO_STATS_UPDATE_INTERVAL + timedelta(seconds=1))
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
assert "Could not fetch stats" in caplog.text
# Disable the entity again and verify stats API calls stop
addon_stats.side_effect = None
addon_stats.reset_mock()
entity_registry.async_update_entity(
entity_id, disabled_by=er.RegistryEntryDisabler.USER
)
freezer.tick(config_entries.RELOAD_AFTER_UPDATE_DELAY)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert config_entry.state is ConfigEntryState.LOADED
# After reload with entity disabled, stats should not be fetched
addon_stats.reset_mock()
freezer.tick(HASSIO_STATS_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
addon_stats.assert_not_called()

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