mirror of
https://github.com/home-assistant/core.git
synced 2026-04-13 13:16:15 +02:00
Compare commits
35 Commits
python-3.1
...
hassio-spl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e55d7ad12 | ||
|
|
5ac545e7e1 | ||
|
|
0c2153dc1e | ||
|
|
0dd31eedc9 | ||
|
|
7e6cc18489 | ||
|
|
632c9d12ce | ||
|
|
109ec0705c | ||
|
|
6f7fa85d18 | ||
|
|
8d2564f00f | ||
|
|
f7096e3744 | ||
|
|
d7f28a09bb | ||
|
|
a54ea071f8 | ||
|
|
1597b740da | ||
|
|
3758d606c9 | ||
|
|
a79988aca7 | ||
|
|
837cd7d89d | ||
|
|
038bb6c15d | ||
|
|
6ccede7f30 | ||
|
|
fb541d8835 | ||
|
|
fbf9b47dc4 | ||
|
|
39a2c08d4e | ||
|
|
ea642980f2 | ||
|
|
4c8ea3669c | ||
|
|
14f24226ae | ||
|
|
3a9f805f10 | ||
|
|
6185663e76 | ||
|
|
f1946cf08b | ||
|
|
9f91d906d2 | ||
|
|
f0d79f0af4 | ||
|
|
b11f55a369 | ||
|
|
99f943fc3a | ||
|
|
99bdde6641 | ||
|
|
56bf2e8f2d | ||
|
|
7ea801eb02 | ||
|
|
f6a155c7b2 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -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.02.0"
|
||||
BASE_IMAGE_VERSION: "2026.01.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14.3
|
||||
3.14.2
|
||||
|
||||
@@ -79,6 +79,7 @@ from .config import HassioConfig
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_REPOSITORIES,
|
||||
COORDINATOR,
|
||||
DATA_ADDONS_LIST,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
@@ -92,9 +93,12 @@ 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,
|
||||
@@ -384,12 +388,6 @@ 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)
|
||||
|
||||
@@ -462,9 +460,20 @@ 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[ADDONS_COORDINATOR] = coordinator
|
||||
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
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
@@ -531,10 +540,12 @@ 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[ADDONS_COORDINATOR]
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[COORDINATOR]
|
||||
coordinator.unload()
|
||||
|
||||
# Pop coordinator
|
||||
# Pop coordinators
|
||||
hass.data.pop(COORDINATOR, None)
|
||||
hass.data.pop(ADDONS_COORDINATOR, None)
|
||||
hass.data.pop(STATS_COORDINATOR, None)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -20,6 +20,7 @@ from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_STARTED,
|
||||
ATTR_STATE,
|
||||
COORDINATOR,
|
||||
DATA_KEY_ADDONS,
|
||||
DATA_KEY_MOUNTS,
|
||||
)
|
||||
@@ -60,17 +61,18 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Binary sensor set up for Hass.io config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
itertools.chain(
|
||||
[
|
||||
HassioAddonBinarySensor(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=entity_description,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in ADDON_ENTITY_DESCRIPTIONS
|
||||
],
|
||||
[
|
||||
|
||||
@@ -77,7 +77,9 @@ 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)
|
||||
@@ -95,6 +97,8 @@ 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"
|
||||
|
||||
@@ -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, cast
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
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,7 +35,6 @@ from .const import (
|
||||
ATTR_SLUG,
|
||||
ATTR_URL,
|
||||
ATTR_VERSION,
|
||||
CONTAINER_INFO,
|
||||
CONTAINER_STATS,
|
||||
CORE_CONTAINER,
|
||||
DATA_ADDONS_INFO,
|
||||
@@ -59,6 +58,8 @@ 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,
|
||||
@@ -318,7 +319,315 @@ def async_remove_devices_from_dev_reg(
|
||||
dev_reg.async_remove_device(dev.id)
|
||||
|
||||
|
||||
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
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 to retrieve Hass.io status."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
@@ -334,80 +643,72 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
name=DOMAIN,
|
||||
update_interval=HASSIO_UPDATE_INTERVAL,
|
||||
# We don't want an immediate refresh since we want to avoid
|
||||
# fetching the container stats right away and avoid hammering
|
||||
# the Supervisor API on startup
|
||||
# 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:
|
||||
await self.force_data_refresh(is_first_update)
|
||||
(
|
||||
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)
|
||||
except SupervisorError as err:
|
||||
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
|
||||
|
||||
# Build clean coordinator data
|
||||
new_data: dict[str, Any] = {}
|
||||
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] = get_os_info(self.hass)
|
||||
|
||||
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_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}
|
||||
if self.is_hass_os:
|
||||
new_data[DATA_KEY_OS] = os_info.to_dict()
|
||||
|
||||
# If this is the initial refresh, register all addons and return the dict
|
||||
# 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]
|
||||
|
||||
# If this is the initial refresh, register all main components
|
||||
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()
|
||||
)
|
||||
@@ -423,17 +724,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
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
|
||||
@@ -453,12 +743,11 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
# 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 add-ons or mounts, we should reload the config entry so we can
|
||||
# If there are new 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_ADDONS]) - set(self.data[DATA_KEY_ADDONS])
|
||||
or set(new_data[DATA_KEY_MOUNTS]) - set(self.data[DATA_KEY_MOUNTS])
|
||||
set(new_data[DATA_KEY_MOUNTS]) - set(self.data.get(DATA_KEY_MOUNTS, {}))
|
||||
):
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.entry_id)
|
||||
@@ -467,146 +756,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
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,
|
||||
@@ -616,14 +765,16 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
if not scheduled and not raise_on_auth_failed:
|
||||
# Force refreshing updates for non-scheduled updates
|
||||
# Force reloading updates of main components 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.refresh_updates()
|
||||
await self.supervisor_client.reload_updates()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Error on Supervisor API: %s", err)
|
||||
|
||||
@@ -631,18 +782,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
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."""
|
||||
|
||||
@@ -11,8 +11,12 @@ 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
|
||||
from .coordinator import HassioDataUpdateCoordinator
|
||||
from .const import ADDONS_COORDINATOR, COORDINATOR, STATS_COORDINATOR
|
||||
from .coordinator import (
|
||||
HassioAddOnDataUpdateCoordinator,
|
||||
HassioDataUpdateCoordinator,
|
||||
HassioStatsDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -20,7 +24,9 @@ async def async_get_config_entry_diagnostics(
|
||||
config_entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[COORDINATOR]
|
||||
addons_coordinator: HassioAddOnDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
stats_coordinator: HassioStatsDataUpdateCoordinator = hass.data[STATS_COORDINATOR]
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -53,5 +59,7 @@ 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,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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,
|
||||
@@ -21,20 +20,79 @@ from .const import (
|
||||
DATA_KEY_OS,
|
||||
DATA_KEY_SUPERVISOR,
|
||||
DOMAIN,
|
||||
KEY_TO_UPDATE_TYPES,
|
||||
SUPERVISOR_CONTAINER,
|
||||
)
|
||||
from .coordinator import HassioDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
HassioAddOnDataUpdateCoordinator,
|
||||
HassioDataUpdateCoordinator,
|
||||
HassioStatsDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
|
||||
class HassioAddonEntity(CoordinatorEntity[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]):
|
||||
"""Base entity for a Hass.io add-on."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HassioDataUpdateCoordinator,
|
||||
coordinator: HassioAddOnDataUpdateCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
addon: dict[str, Any],
|
||||
) -> None:
|
||||
@@ -56,16 +114,13 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to updates."""
|
||||
"""Subscribe to addon info 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(
|
||||
self._addon_slug, self.entity_id, update_types
|
||||
self.coordinator.async_enable_addon_info_updates(
|
||||
self._addon_slug, self.entity_id
|
||||
)
|
||||
)
|
||||
if CONTAINER_STATS in update_types:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
|
||||
@@ -146,18 +201,6 @@ 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."""
|
||||
@@ -184,18 +227,6 @@ 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."""
|
||||
|
||||
@@ -28,7 +28,6 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_SLUG,
|
||||
@@ -38,6 +37,7 @@ 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(ADDONS_COORDINATOR):
|
||||
if coordinator := self._hass.data.get(COORDINATOR):
|
||||
coordinator.config_entry.async_create_task(
|
||||
self._hass, coordinator.async_refresh()
|
||||
)
|
||||
|
||||
@@ -17,20 +17,24 @@ 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,
|
||||
HassioSupervisorEntity,
|
||||
HassioStatsEntity,
|
||||
)
|
||||
|
||||
COMMON_ENTITY_DESCRIPTIONS = (
|
||||
@@ -63,10 +67,7 @@ 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(
|
||||
@@ -114,36 +115,64 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Sensor set up for Hass.io config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
stats_coordinator = hass.data[STATS_COORDINATOR]
|
||||
|
||||
entities: list[
|
||||
HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor | HostSensor
|
||||
] = [
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
# Add-on non-stats sensors (version, version_latest)
|
||||
entities.extend(
|
||||
HassioAddonSensor(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=entity_description,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in ADDON_ENTITY_DESCRIPTIONS
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
CoreSensor(
|
||||
coordinator=coordinator,
|
||||
entity_description=entity_description,
|
||||
)
|
||||
for entity_description in CORE_ENTITY_DESCRIPTIONS
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in COMMON_ENTITY_DESCRIPTIONS
|
||||
)
|
||||
|
||||
# Add-on stats sensors (cpu_percent, memory_percent)
|
||||
entities.extend(
|
||||
SupervisorSensor(
|
||||
coordinator=coordinator,
|
||||
HassioStatsSensor(
|
||||
coordinator=stats_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 entity_description in SUPERVISOR_ENTITY_DESCRIPTIONS
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in STATS_ENTITY_DESCRIPTIONS
|
||||
)
|
||||
|
||||
# Core stats sensors
|
||||
entities.extend(
|
||||
HassioStatsSensor(
|
||||
coordinator=stats_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
|
||||
)
|
||||
|
||||
# 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,
|
||||
@@ -152,6 +181,7 @@ async def async_setup_entry(
|
||||
for entity_description in HOST_ENTITY_DESCRIPTIONS
|
||||
)
|
||||
|
||||
# OS sensors
|
||||
if coordinator.is_hass_os:
|
||||
entities.extend(
|
||||
HassioOSSensor(
|
||||
@@ -175,8 +205,21 @@ 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 add-on attribute."""
|
||||
"""Sensor to track a Hass.io OS attribute."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
@@ -184,24 +227,6 @@ 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."""
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ from homeassistant.helpers import (
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_ADDON,
|
||||
ATTR_ADDONS,
|
||||
ATTR_APP,
|
||||
@@ -45,6 +44,7 @@ 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(ADDONS_COORDINATOR)) is None
|
||||
or (coordinator := hass.data.get(COORDINATOR)) is None
|
||||
or coordinator.entry_id not in device.config_entries
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
|
||||
@@ -25,6 +25,7 @@ from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
COORDINATOR,
|
||||
DATA_KEY_ADDONS,
|
||||
DATA_KEY_CORE,
|
||||
DATA_KEY_OS,
|
||||
@@ -51,9 +52,9 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Supervisor update based on a config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
|
||||
entities = [
|
||||
entities: list[UpdateEntity] = [
|
||||
SupervisorSupervisorUpdateEntity(
|
||||
coordinator=coordinator,
|
||||
entity_description=ENTITY_DESCRIPTION,
|
||||
@@ -64,15 +65,6 @@ 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(
|
||||
@@ -81,6 +73,16 @@ 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)
|
||||
|
||||
|
||||
|
||||
@@ -62,14 +62,32 @@ class ResourceStorageCollection(collection.DictStorageCollection):
|
||||
)
|
||||
self.ll_config = ll_config
|
||||
|
||||
async def async_get_info(self) -> dict[str, int]:
|
||||
"""Return the resources info for YAML mode."""
|
||||
async def _async_ensure_loaded(self) -> None:
|
||||
"""Ensure the collection has been loaded from storage."""
|
||||
if not self.loaded:
|
||||
await self.async_load()
|
||||
self.loaded = True
|
||||
|
||||
async def async_get_info(self) -> dict[str, int]:
|
||||
"""Return the resources info for YAML mode."""
|
||||
await self._async_ensure_loaded()
|
||||
return {"resources": len(self.async_items() or [])}
|
||||
|
||||
async def async_create_item(self, data: dict) -> dict:
|
||||
"""Create a new item."""
|
||||
await self._async_ensure_loaded()
|
||||
return await super().async_create_item(data)
|
||||
|
||||
async def async_update_item(self, item_id: str, updates: dict) -> dict:
|
||||
"""Update item."""
|
||||
await self._async_ensure_loaded()
|
||||
return await super().async_update_item(item_id, updates)
|
||||
|
||||
async def async_delete_item(self, item_id: str) -> None:
|
||||
"""Delete item."""
|
||||
await self._async_ensure_loaded()
|
||||
await super().async_delete_item(item_id)
|
||||
|
||||
async def _async_load_data(self) -> collection.SerializedStorageCollection | None:
|
||||
"""Load the data."""
|
||||
if (store_data := await self.store.async_load()) is not None:
|
||||
@@ -118,10 +136,6 @@ class ResourceStorageCollection(collection.DictStorageCollection):
|
||||
|
||||
async def _update_data(self, item: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
if not self.loaded:
|
||||
await self.async_load()
|
||||
self.loaded = True
|
||||
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
if CONF_RESOURCE_TYPE_WS in update_data:
|
||||
update_data[CONF_TYPE] = update_data.pop(CONF_RESOURCE_TYPE_WS)
|
||||
|
||||
@@ -71,6 +71,8 @@ class LockUserData(TypedDict):
|
||||
user_type: str
|
||||
credential_rule: str
|
||||
credentials: list[LockUserCredentialData]
|
||||
creator_fabric_index: int | None
|
||||
last_modified_fabric_index: int | None
|
||||
next_user_index: int | None
|
||||
|
||||
|
||||
@@ -115,6 +117,8 @@ class GetLockCredentialStatusResult(TypedDict):
|
||||
|
||||
credential_exists: bool
|
||||
user_index: int | None
|
||||
creator_fabric_index: int | None
|
||||
last_modified_fabric_index: int | None
|
||||
next_credential_index: int | None
|
||||
|
||||
|
||||
@@ -214,6 +218,8 @@ def _format_user_response(user_data: Any) -> LockUserData | None:
|
||||
_get_attr(user_data, "credentialRule"), "unknown"
|
||||
),
|
||||
credentials=credentials,
|
||||
creator_fabric_index=_get_attr(user_data, "creatorFabricIndex"),
|
||||
last_modified_fabric_index=_get_attr(user_data, "lastModifiedFabricIndex"),
|
||||
next_user_index=_get_attr(user_data, "nextUserIndex"),
|
||||
)
|
||||
|
||||
@@ -817,7 +823,8 @@ async def get_lock_credential_status(
|
||||
) -> GetLockCredentialStatusResult:
|
||||
"""Get the status of a credential slot on the lock.
|
||||
|
||||
Returns typed dict with credential_exists, user_index, next_credential_index.
|
||||
Returns typed dict with credential_exists, user_index, creator_fabric_index,
|
||||
last_modified_fabric_index, and next_credential_index.
|
||||
Raises HomeAssistantError on failure.
|
||||
"""
|
||||
lock_endpoint = _get_lock_endpoint_or_raise(node)
|
||||
@@ -839,5 +846,7 @@ async def get_lock_credential_status(
|
||||
return GetLockCredentialStatusResult(
|
||||
credential_exists=bool(_get_attr(response, "credentialExists")),
|
||||
user_index=_get_attr(response, "userIndex"),
|
||||
creator_fabric_index=_get_attr(response, "creatorFabricIndex"),
|
||||
last_modified_fabric_index=_get_attr(response, "lastModifiedFabricIndex"),
|
||||
next_credential_index=_get_attr(response, "nextCredentialIndex"),
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from ring_doorbell import RingCapability, RingEvent as RingAlert
|
||||
from ring_doorbell.const import KIND_DING, KIND_INTERCOM_UNLOCK, KIND_MOTION
|
||||
|
||||
from homeassistant.components.event import (
|
||||
DoorbellEventType,
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
@@ -34,7 +35,7 @@ EVENT_DESCRIPTIONS: tuple[RingEventEntityDescription, ...] = (
|
||||
key=KIND_DING,
|
||||
translation_key=KIND_DING,
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
event_types=[KIND_DING],
|
||||
event_types=[DoorbellEventType.RING],
|
||||
capability=RingCapability.DING,
|
||||
),
|
||||
RingEventEntityDescription(
|
||||
@@ -100,7 +101,10 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity)
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
if (alert := self._get_coordinator_alert()) and not alert.is_update:
|
||||
self._async_handle_event(alert.kind)
|
||||
if alert.kind == KIND_DING:
|
||||
self._async_handle_event(DoorbellEventType.RING)
|
||||
else:
|
||||
self._async_handle_event(alert.kind)
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
|
||||
@@ -73,7 +73,14 @@
|
||||
},
|
||||
"event": {
|
||||
"ding": {
|
||||
"name": "Ding"
|
||||
"name": "Ding",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"ring": "[%key:component::event::entity_component::doorbell::state_attributes::event_type::state::ring%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"intercom_unlock": {
|
||||
"name": "Intercom unlock"
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
from sanix import Sanix
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN
|
||||
from .coordinator import SanixCoordinator
|
||||
from .const import CONF_SERIAL_NUMBER
|
||||
from .coordinator import SanixConfigEntry, SanixCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SanixConfigEntry) -> bool:
|
||||
"""Set up Sanix from a config entry."""
|
||||
|
||||
serial_no = entry.data[CONF_SERIAL_NUMBER]
|
||||
@@ -22,16 +21,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = SanixCoordinator(hass, entry, sanix_api)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SanixConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -15,14 +15,16 @@ from .const import MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SanixConfigEntry = ConfigEntry[SanixCoordinator]
|
||||
|
||||
|
||||
class SanixCoordinator(DataUpdateCoordinator[Measurement]):
|
||||
"""Sanix coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SanixConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, sanix_api: Sanix
|
||||
self, hass: HomeAssistant, config_entry: SanixConfigEntry, sanix_api: Sanix
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -20,7 +20,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfLength
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -28,7 +27,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import SanixCoordinator
|
||||
from .coordinator import SanixConfigEntry, SanixCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -83,11 +82,11 @@ SENSOR_TYPES: tuple[SanixSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SanixConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sanix Sensor entities based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
SanixSensorEntity(coordinator, description) for description in SENSOR_TYPES
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
"""The sia integration."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .hub import SIAHub
|
||||
from .const import PLATFORMS
|
||||
from .hub import SIAConfigEntry, SIAHub
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SIAConfigEntry) -> bool:
|
||||
"""Set up sia from a config entry."""
|
||||
hub: SIAHub = SIAHub(hass, entry)
|
||||
hub = SIAHub(hass, entry)
|
||||
hub.async_setup_hub()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = hub
|
||||
try:
|
||||
if hub.sia_client:
|
||||
await hub.sia_client.async_start(reuse_port=True)
|
||||
@@ -23,14 +20,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryNotReady(
|
||||
f"SIA Server at port {entry.data[CONF_PORT]} could not start."
|
||||
) from exc
|
||||
|
||||
entry.runtime_data = hub
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SIAConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hub: SIAHub = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await hub.async_shutdown()
|
||||
await entry.runtime_data.async_shutdown()
|
||||
return unload_ok
|
||||
|
||||
@@ -16,12 +16,7 @@ from pysiaalarm import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import CONF_PORT, CONF_PROTOCOL
|
||||
from homeassistant.core import callback
|
||||
|
||||
@@ -36,7 +31,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
TITLE,
|
||||
)
|
||||
from .hub import SIAHub
|
||||
from .hub import SIAConfigEntry, SIAHub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -100,7 +95,7 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: SIAConfigEntry,
|
||||
) -> SIAOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return SIAOptionsFlowHandler(config_entry)
|
||||
@@ -179,7 +174,9 @@ class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class SIAOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle SIA options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
config_entry: SIAConfigEntry
|
||||
|
||||
def __init__(self, config_entry: SIAConfigEntry) -> None:
|
||||
"""Initialize SIA options flow."""
|
||||
self.options = deepcopy(dict(config_entry.options))
|
||||
self.hub: SIAHub | None = None
|
||||
@@ -189,7 +186,7 @@ class SIAOptionsFlowHandler(OptionsFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the SIA options."""
|
||||
self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id]
|
||||
self.hub = self.config_entry.runtime_data
|
||||
assert self.hub is not None
|
||||
assert self.hub.sia_accounts is not None
|
||||
self.accounts_todo = [a.account_id for a in self.hub.sia_accounts]
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any
|
||||
|
||||
from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEvent
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -28,6 +28,8 @@ from .utils import get_event_data_from_sia_event
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SIAConfigEntry = ConfigEntry[SIAHub]
|
||||
|
||||
DEFAULT_TIMEBAND = (80, 40)
|
||||
|
||||
|
||||
@@ -37,11 +39,11 @@ class SIAHub:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SIAConfigEntry,
|
||||
) -> None:
|
||||
"""Create the SIAHub."""
|
||||
self._hass: HomeAssistant = hass
|
||||
self._entry: ConfigEntry = entry
|
||||
self._hass = hass
|
||||
self._entry = entry
|
||||
self._port: int = entry.data[CONF_PORT]
|
||||
self._title: str = entry.title
|
||||
self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS])
|
||||
@@ -131,7 +133,7 @@ class SIAHub:
|
||||
|
||||
@staticmethod
|
||||
async def async_config_entry_updated(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: SIAConfigEntry
|
||||
) -> None:
|
||||
"""Handle signals of config entry being updated.
|
||||
|
||||
@@ -139,8 +141,8 @@ class SIAHub:
|
||||
Second, unload underlying platforms, and then setup platforms, this reflects any changes in number of zones.
|
||||
|
||||
"""
|
||||
if not (hub := hass.data[DOMAIN].get(config_entry.entry_id)):
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
return
|
||||
hub.update_accounts()
|
||||
config_entry.runtime_data.update_accounts()
|
||||
await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from simplipy import API
|
||||
from simplipy.errors import (
|
||||
@@ -39,7 +39,7 @@ from simplipy.websocket import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
ATTR_DEVICE_ID,
|
||||
@@ -88,6 +88,8 @@ from .const import (
|
||||
from .coordinator import SimpliSafeDataUpdateCoordinator
|
||||
from .typing import SystemType
|
||||
|
||||
type SimpliSafeConfigEntry = ConfigEntry[SimpliSafe]
|
||||
|
||||
ATTR_CATEGORY = "category"
|
||||
ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by"
|
||||
ATTR_LAST_EVENT_SENSOR_SERIAL = "last_event_sensor_serial"
|
||||
@@ -223,10 +225,15 @@ def _async_get_system_for_service_call(
|
||||
]
|
||||
system_id = int(system_id_str)
|
||||
|
||||
entry: SimpliSafeConfigEntry | None
|
||||
for entry_id in base_station_device_entry.config_entries:
|
||||
if (simplisafe := hass.data[DOMAIN].get(entry_id)) is None:
|
||||
if (
|
||||
(entry := hass.config_entries.async_get_entry(entry_id)) is None
|
||||
or entry.domain != DOMAIN
|
||||
or entry.state != ConfigEntryState.LOADED
|
||||
):
|
||||
continue
|
||||
return cast(SystemType, simplisafe.systems[system_id])
|
||||
return entry.runtime_data.systems[system_id]
|
||||
|
||||
raise ValueError(f"No system for device ID: {device_id}")
|
||||
|
||||
@@ -286,7 +293,7 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) ->
|
||||
hass.config_entries.async_update_entry(entry, **entry_updates)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SimpliSafeConfigEntry) -> bool:
|
||||
"""Set up SimpliSafe as config entry."""
|
||||
_async_standardize_config_entry(hass, entry)
|
||||
|
||||
@@ -310,8 +317,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except SimplipyError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = simplisafe
|
||||
entry.runtime_data = simplisafe
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -396,11 +402,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SimpliSafeConfigEntry) -> bool:
|
||||
"""Unload a SimpliSafe config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
# If this is the last loaded instance of SimpliSafe, deregister any services
|
||||
|
||||
@@ -28,12 +28,11 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SimpliSafe
|
||||
from . import SimpliSafe, SimpliSafeConfigEntry
|
||||
from .const import (
|
||||
ATTR_ALARM_DURATION,
|
||||
ATTR_ALARM_VOLUME,
|
||||
@@ -44,7 +43,6 @@ from .const import (
|
||||
ATTR_EXIT_DELAY_HOME,
|
||||
ATTR_LIGHT,
|
||||
ATTR_VOICE_PROMPT_VOLUME,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .entity import SimpliSafeEntity
|
||||
@@ -104,11 +102,11 @@ WEBSOCKET_EVENTS_TO_LISTEN_FOR = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SimpliSafeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a SimpliSafe alarm control panel based on a config entry."""
|
||||
simplisafe = hass.data[DOMAIN][entry.entry_id]
|
||||
simplisafe = entry.runtime_data
|
||||
async_add_entities(
|
||||
[SimpliSafeAlarm(simplisafe, system) for system in simplisafe.systems.values()],
|
||||
True,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from simplipy.device import DeviceTypes, DeviceV3
|
||||
from simplipy.device.sensor.v3 import SensorV3
|
||||
from simplipy.system.v3 import SystemV3
|
||||
@@ -11,13 +13,12 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SimpliSafe
|
||||
from .const import DOMAIN, LOGGER
|
||||
from . import SimpliSafe, SimpliSafeConfigEntry
|
||||
from .const import LOGGER
|
||||
from .entity import SimpliSafeEntity
|
||||
|
||||
SUPPORTED_BATTERY_SENSOR_TYPES = [
|
||||
@@ -59,11 +60,11 @@ TRIGGERED_SENSOR_TYPES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SimpliSafeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SimpliSafe binary sensors based on a config entry."""
|
||||
simplisafe = hass.data[DOMAIN][entry.entry_id]
|
||||
simplisafe = entry.runtime_data
|
||||
|
||||
sensors: list[BatteryBinarySensor | TriggeredBinarySensor] = []
|
||||
|
||||
@@ -72,18 +73,22 @@ async def async_setup_entry(
|
||||
LOGGER.warning("Skipping sensor setup for V2 system: %s", system.system_id)
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(system, SystemV3)
|
||||
for sensor in system.sensors.values():
|
||||
if sensor.type in TRIGGERED_SENSOR_TYPES:
|
||||
sensors.append(
|
||||
TriggeredBinarySensor(
|
||||
simplisafe,
|
||||
system,
|
||||
sensor,
|
||||
cast(SensorV3, sensor),
|
||||
TRIGGERED_SENSOR_TYPES[sensor.type],
|
||||
)
|
||||
)
|
||||
if sensor.type in SUPPORTED_BATTERY_SENSOR_TYPES:
|
||||
sensors.append(BatteryBinarySensor(simplisafe, system, sensor))
|
||||
sensors.append(
|
||||
BatteryBinarySensor(simplisafe, system, cast(DeviceV3, sensor))
|
||||
)
|
||||
|
||||
sensors.extend(
|
||||
BatteryBinarySensor(simplisafe, system, lock)
|
||||
|
||||
@@ -9,14 +9,12 @@ from simplipy.errors import SimplipyError
|
||||
from simplipy.system import System
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SimpliSafe
|
||||
from .const import DOMAIN
|
||||
from . import SimpliSafe, SimpliSafeConfigEntry
|
||||
from .entity import SimpliSafeEntity
|
||||
from .typing import SystemType
|
||||
|
||||
@@ -47,11 +45,11 @@ BUTTON_DESCRIPTIONS = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SimpliSafeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SimpliSafe buttons based on a config entry."""
|
||||
simplisafe = hass.data[DOMAIN][entry.entry_id]
|
||||
simplisafe = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
|
||||
@@ -14,16 +14,12 @@ from simplipy.util.auth import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_URL, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from . import SimpliSafeConfigEntry
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
CONF_AUTH_CODE = "auth_code"
|
||||
@@ -68,7 +64,7 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: SimpliSafeConfigEntry,
|
||||
) -> SimpliSafeOptionsFlowHandler:
|
||||
"""Define the config flow to handle options."""
|
||||
return SimpliSafeOptionsFlowHandler()
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CODE,
|
||||
@@ -16,8 +15,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import SimpliSafe
|
||||
from .const import DOMAIN
|
||||
from . import SimpliSafeConfigEntry
|
||||
|
||||
CONF_CREDIT_CARD = "creditCard"
|
||||
CONF_EXPIRES = "expires"
|
||||
@@ -53,10 +51,10 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: SimpliSafeConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
simplisafe: SimpliSafe = hass.data[DOMAIN][entry.entry_id]
|
||||
simplisafe = entry.runtime_data
|
||||
|
||||
return async_redact_data(
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from simplipy.device.lock import Lock, LockStates
|
||||
from simplipy.errors import SimplipyError
|
||||
@@ -10,13 +10,12 @@ from simplipy.system.v3 import SystemV3
|
||||
from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, WebsocketEvent
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SimpliSafe
|
||||
from .const import DOMAIN, LOGGER
|
||||
from . import SimpliSafe, SimpliSafeConfigEntry
|
||||
from .const import LOGGER
|
||||
from .entity import SimpliSafeEntity
|
||||
|
||||
ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
|
||||
@@ -32,11 +31,11 @@ WEBSOCKET_EVENTS_TO_LISTEN_FOR = (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SimpliSafeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SimpliSafe locks based on a config entry."""
|
||||
simplisafe = hass.data[DOMAIN][entry.entry_id]
|
||||
simplisafe = entry.runtime_data
|
||||
locks: list[SimpliSafeLock] = []
|
||||
|
||||
for system in simplisafe.systems.values():
|
||||
@@ -44,6 +43,8 @@ async def async_setup_entry(
|
||||
LOGGER.warning("Skipping lock setup for V2 system: %s", system.system_id)
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(system, SystemV3)
|
||||
locks.extend(
|
||||
SimpliSafeLock(simplisafe, system, lock) for lock in system.locks.values()
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from simplipy.device import DeviceTypes
|
||||
from simplipy.device.sensor.v3 import SensorV3
|
||||
from simplipy.system.v3 import SystemV3
|
||||
@@ -11,23 +13,22 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SimpliSafe
|
||||
from .const import DOMAIN, LOGGER
|
||||
from . import SimpliSafe, SimpliSafeConfigEntry
|
||||
from .const import LOGGER
|
||||
from .entity import SimpliSafeEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SimpliSafeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SimpliSafe freeze sensors based on a config entry."""
|
||||
simplisafe = hass.data[DOMAIN][entry.entry_id]
|
||||
simplisafe = entry.runtime_data
|
||||
sensors: list[SimplisafeFreezeSensor] = []
|
||||
|
||||
for system in simplisafe.systems.values():
|
||||
@@ -35,8 +36,10 @@ async def async_setup_entry(
|
||||
LOGGER.warning("Skipping sensor setup for V2 system: %s", system.system_id)
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(system, SystemV3)
|
||||
sensors.extend(
|
||||
SimplisafeFreezeSensor(simplisafe, system, sensor)
|
||||
SimplisafeFreezeSensor(simplisafe, system, cast(SensorV3, sensor))
|
||||
for sensor in system.sensors.values()
|
||||
if sensor.type == DeviceTypes.TEMPERATURE
|
||||
)
|
||||
|
||||
@@ -7,14 +7,12 @@ import asyncio
|
||||
from aioskybell import Skybell
|
||||
from aioskybell.exceptions import SkybellAuthenticationException, SkybellException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SkybellDataUpdateCoordinator
|
||||
from .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -25,7 +23,7 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SkybellConfigEntry) -> bool:
|
||||
"""Set up Skybell from a config entry."""
|
||||
email = entry.data[CONF_EMAIL]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
@@ -53,14 +51,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
for coordinator in device_coordinators
|
||||
]
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device_coordinators
|
||||
entry.runtime_data = device_coordinators
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SkybellConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -9,12 +9,10 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN
|
||||
from .coordinator import SkybellDataUpdateCoordinator
|
||||
from .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
|
||||
from .entity import SkybellEntity
|
||||
|
||||
BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
@@ -32,14 +30,14 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SkybellConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Skybell binary sensor."""
|
||||
async_add_entities(
|
||||
SkybellBinarySensor(coordinator, sensor)
|
||||
for sensor in BINARY_SENSOR_TYPES
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id]
|
||||
for coordinator in entry.runtime_data
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ from haffmpeg.camera import CameraMjpeg
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityDescription
|
||||
from homeassistant.components.ffmpeg import get_ffmpeg_manager
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SkybellDataUpdateCoordinator
|
||||
from .coordinator import SkybellConfigEntry, SkybellDataUpdateCoordinator
|
||||
from .entity import SkybellEntity
|
||||
|
||||
CAMERA_TYPES: tuple[CameraEntityDescription, ...] = (
|
||||
@@ -31,13 +29,13 @@ CAMERA_TYPES: tuple[CameraEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SkybellConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Skybell camera."""
|
||||
entities = []
|
||||
for description in CAMERA_TYPES:
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id]:
|
||||
for coordinator in entry.runtime_data:
|
||||
if description.key == "avatar":
|
||||
entities.append(SkybellCamera(coordinator, description))
|
||||
else:
|
||||
|
||||
@@ -10,14 +10,19 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
type SkybellConfigEntry = ConfigEntry[list[SkybellDataUpdateCoordinator]]
|
||||
|
||||
|
||||
class SkybellDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Data update coordinator for the Skybell integration."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SkybellConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, device: SkybellDevice
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: SkybellConfigEntry,
|
||||
device: SkybellDevice,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -13,23 +13,22 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
LightEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SkybellConfigEntry
|
||||
from .entity import SkybellEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SkybellConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Skybell switch."""
|
||||
async_add_entities(
|
||||
SkybellLight(coordinator, LightEntityDescription(key="light"))
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id]
|
||||
for coordinator in entry.runtime_data
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .entity import DOMAIN, SkybellEntity
|
||||
from .coordinator import SkybellConfigEntry
|
||||
from .entity import SkybellEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -89,13 +89,13 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SkybellConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Skybell sensor."""
|
||||
async_add_entities(
|
||||
SkybellSensor(coordinator, description)
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id]
|
||||
for coordinator in entry.runtime_data
|
||||
for description in SENSOR_TYPES
|
||||
if coordinator.device.owner or description.key not in CONST.ATTR_OWNER_STATS
|
||||
)
|
||||
|
||||
@@ -5,11 +5,10 @@ from __future__ import annotations
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SkybellConfigEntry
|
||||
from .entity import SkybellEntity
|
||||
|
||||
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
|
||||
@@ -30,13 +29,13 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SkybellConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SkyBell switch."""
|
||||
async_add_entities(
|
||||
SkybellSwitch(coordinator, description)
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id]
|
||||
for coordinator in entry.runtime_data
|
||||
for description in SWITCH_TYPES
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
@@ -30,6 +31,17 @@ PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type SlackConfigEntry = ConfigEntry[SlackData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SlackData:
|
||||
"""Runtime data for the Slack integration."""
|
||||
|
||||
client: AsyncWebClient
|
||||
url: str
|
||||
user_id: str
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Slack component."""
|
||||
@@ -37,7 +49,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SlackConfigEntry) -> bool:
|
||||
"""Set up Slack from a config entry."""
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
slack = AsyncWebClient(
|
||||
@@ -52,19 +64,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return False
|
||||
raise ConfigEntryNotReady("Error while setting up integration") from ex
|
||||
|
||||
data = {
|
||||
DATA_CLIENT: slack,
|
||||
ATTR_URL: res[ATTR_URL],
|
||||
ATTR_USER_ID: res[ATTR_USER_ID],
|
||||
}
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data | {SLACK_DATA: data}
|
||||
entry.runtime_data = SlackData(
|
||||
client=slack,
|
||||
url=res[ATTR_URL],
|
||||
user_id=res[ATTR_USER_ID],
|
||||
)
|
||||
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
hass.data[DOMAIN][entry.entry_id],
|
||||
entry.data
|
||||
| {
|
||||
SLACK_DATA: {
|
||||
DATA_CLIENT: slack,
|
||||
ATTR_URL: res[ATTR_URL],
|
||||
ATTR_USER_ID: res[ATTR_USER_ID],
|
||||
}
|
||||
},
|
||||
hass.data[DATA_HASS_CONFIG],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
"""The slack integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
from .const import ATTR_URL, ATTR_USER_ID, DATA_CLIENT, DEFAULT_NAME, DOMAIN
|
||||
from . import SlackConfigEntry, SlackData
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
|
||||
class SlackEntity(Entity):
|
||||
@@ -16,16 +12,16 @@ class SlackEntity(Entity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: dict[str, AsyncWebClient],
|
||||
data: SlackData,
|
||||
description: EntityDescription,
|
||||
entry: ConfigEntry,
|
||||
entry: SlackConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize a Slack entity."""
|
||||
self._client: AsyncWebClient = data[DATA_CLIENT]
|
||||
self._client = data.client
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{data[ATTR_USER_ID]}_{description.key}"
|
||||
self._attr_unique_id = f"{data.user_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=str(data[ATTR_URL]),
|
||||
configuration_url=data.url,
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
|
||||
@@ -9,25 +9,25 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_SNOOZE, DOMAIN, SLACK_DATA
|
||||
from . import SlackConfigEntry
|
||||
from .const import ATTR_SNOOZE
|
||||
from .entity import SlackEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SlackConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Slack select."""
|
||||
"""Set up the Slack sensor."""
|
||||
async_add_entities(
|
||||
[
|
||||
SlackSensorEntity(
|
||||
hass.data[DOMAIN][entry.entry_id][SLACK_DATA],
|
||||
entry.runtime_data,
|
||||
SensorEntityDescription(
|
||||
key="do_not_disturb_until",
|
||||
translation_key="do_not_disturb_until",
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, IS_IN_BED, SLEEP_NUMBER
|
||||
from .coordinator import (
|
||||
SleepIQConfigEntry,
|
||||
SleepIQData,
|
||||
SleepIQDataUpdateCoordinator,
|
||||
SleepIQPauseUpdateCoordinator,
|
||||
@@ -64,7 +65,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> bool:
|
||||
"""Set up the SleepIQ config entry."""
|
||||
conf = entry.data
|
||||
email = conf[CONF_USERNAME]
|
||||
@@ -104,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await pause_coordinator.async_config_entry_first_refresh()
|
||||
await sleep_data_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SleepIQData(
|
||||
entry.runtime_data = SleepIQData(
|
||||
data_coordinator=coordinator,
|
||||
pause_coordinator=pause_coordinator,
|
||||
sleep_data_coordinator=sleep_data_coordinator,
|
||||
@@ -116,11 +117,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> bool:
|
||||
"""Unload the config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def _async_migrate_unique_ids(
|
||||
|
||||
@@ -6,22 +6,21 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED
|
||||
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
|
||||
from .const import ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED
|
||||
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||
from .entity import SleepIQSleeperEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SleepIQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SleepIQ bed binary sensors."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
async_add_entities(
|
||||
IsInBedBinarySensor(data.data_coordinator, bed, sleeper)
|
||||
for bed in data.client.beds.values()
|
||||
|
||||
@@ -9,12 +9,10 @@ from typing import Any
|
||||
from asyncsleepiq import SleepIQBed
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SleepIQData
|
||||
from .coordinator import SleepIQConfigEntry
|
||||
from .entity import SleepIQEntity
|
||||
|
||||
|
||||
@@ -43,11 +41,11 @@ ENTITY_DESCRIPTIONS = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SleepIQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sleep number buttons."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
SleepNumberButton(bed, ed)
|
||||
|
||||
@@ -18,16 +18,18 @@ UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
LONGER_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
SLEEP_DATA_UPDATE_INTERVAL = timedelta(hours=1) # Sleep data doesn't change frequently
|
||||
|
||||
type SleepIQConfigEntry = ConfigEntry[SleepIQData]
|
||||
|
||||
|
||||
class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""SleepIQ data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SleepIQConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: SleepIQConfigEntry,
|
||||
client: AsyncSleepIQ,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
@@ -51,12 +53,12 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""SleepIQ data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SleepIQConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: SleepIQConfigEntry,
|
||||
client: AsyncSleepIQ,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
@@ -78,12 +80,12 @@ class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
class SleepIQSleepDataCoordinator(DataUpdateCoordinator[None]):
|
||||
"""SleepIQ sleep health data coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SleepIQConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: SleepIQConfigEntry,
|
||||
client: AsyncSleepIQ,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
|
||||
@@ -6,12 +6,10 @@ from typing import Any
|
||||
from asyncsleepiq import SleepIQBed, SleepIQLight
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
|
||||
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||
from .entity import SleepIQBedEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -19,11 +17,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SleepIQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SleepIQ bed lights."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
async_add_entities(
|
||||
SleepIQLightEntity(data.data_coordinator, bed, light)
|
||||
for bed in data.client.beds.values()
|
||||
|
||||
@@ -21,7 +21,6 @@ from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -29,13 +28,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .const import (
|
||||
ACTUATOR,
|
||||
CORE_CLIMATE_TIMER,
|
||||
DOMAIN,
|
||||
ENTITY_TYPES,
|
||||
FIRMNESS,
|
||||
FOOT_WARMING_TIMER,
|
||||
ICON_OCCUPIED,
|
||||
)
|
||||
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
|
||||
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||
from .entity import SleepIQBedEntity, sleeper_for_side
|
||||
|
||||
|
||||
@@ -180,11 +178,11 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SleepIQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SleepIQ bed sensors."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
|
||||
entities: list[SleepIQNumberEntity] = []
|
||||
for bed in data.client.beds.values():
|
||||
|
||||
@@ -13,22 +13,21 @@ from asyncsleepiq import (
|
||||
)
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CORE_CLIMATE, DOMAIN, FOOT_WARMER
|
||||
from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator
|
||||
from .const import CORE_CLIMATE, FOOT_WARMER
|
||||
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||
from .entity import SleepIQBedEntity, SleepIQSleeperEntity, sleeper_for_side
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SleepIQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SleepIQ foundation preset select entities."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
entities: list[SleepIQBedEntity] = []
|
||||
for bed in data.client.beds.values():
|
||||
entities.extend(
|
||||
|
||||
@@ -13,13 +13,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
HEART_RATE,
|
||||
HRV,
|
||||
PRESSURE,
|
||||
@@ -29,7 +27,7 @@ from .const import (
|
||||
SLEEP_SCORE,
|
||||
)
|
||||
from .coordinator import (
|
||||
SleepIQData,
|
||||
SleepIQConfigEntry,
|
||||
SleepIQDataUpdateCoordinator,
|
||||
SleepIQSleepDataCoordinator,
|
||||
)
|
||||
@@ -112,11 +110,11 @@ SLEEP_HEALTH_SENSORS: tuple[SleepIQSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SleepIQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SleepIQ bed sensors."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
|
||||
@@ -7,22 +7,20 @@ from typing import Any
|
||||
from asyncsleepiq import SleepIQBed
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SleepIQData, SleepIQPauseUpdateCoordinator
|
||||
from .coordinator import SleepIQConfigEntry, SleepIQPauseUpdateCoordinator
|
||||
from .entity import SleepIQBedEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SleepIQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sleep number switches."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
async_add_entities(
|
||||
SleepNumberPrivateSwitch(data.pause_coordinator, bed)
|
||||
for bed in data.client.beds.values()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""The soundtouch component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from libsoundtouch import soundtouch_device
|
||||
from libsoundtouch.device import SoundTouchDevice
|
||||
@@ -22,6 +25,11 @@ from .const import (
|
||||
SERVICE_REMOVE_ZONE_SLAVE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .media_player import SoundTouchMediaPlayer
|
||||
|
||||
type SoundTouchConfigEntry = ConfigEntry[SoundTouchData]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_PLAY_EVERYWHERE_SCHEMA = vol.Schema({vol.Required("master"): cv.entity_id})
|
||||
@@ -50,12 +58,12 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
class SoundTouchData:
|
||||
"""SoundTouch data stored in the Home Assistant data object."""
|
||||
"""SoundTouch data stored in the config entry runtime data."""
|
||||
|
||||
def __init__(self, device: SoundTouchDevice) -> None:
|
||||
"""Initialize the SoundTouch data object for a device."""
|
||||
self.device = device
|
||||
self.media_player = None
|
||||
self.media_player: SoundTouchMediaPlayer | None = None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -65,20 +73,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Handle the applying of a service."""
|
||||
master_id = service.data.get("master")
|
||||
slaves_ids = service.data.get("slaves")
|
||||
all_media_players = [
|
||||
entry.runtime_data.media_player
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if entry.runtime_data.media_player is not None
|
||||
]
|
||||
slaves = []
|
||||
if slaves_ids:
|
||||
slaves = [
|
||||
data.media_player
|
||||
for data in hass.data[DOMAIN].values()
|
||||
if data.media_player.entity_id in slaves_ids
|
||||
media_player
|
||||
for media_player in all_media_players
|
||||
if media_player.entity_id in slaves_ids
|
||||
]
|
||||
|
||||
master = next(
|
||||
iter(
|
||||
[
|
||||
data.media_player
|
||||
for data in hass.data[DOMAIN].values()
|
||||
if data.media_player.entity_id == master_id
|
||||
media_player
|
||||
for media_player in all_media_players
|
||||
if media_player.entity_id == master_id
|
||||
]
|
||||
),
|
||||
None,
|
||||
@@ -90,9 +103,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
if service.service == SERVICE_PLAY_EVERYWHERE:
|
||||
slaves = [
|
||||
data.media_player
|
||||
for data in hass.data[DOMAIN].values()
|
||||
if data.media_player.entity_id != master_id
|
||||
media_player
|
||||
for media_player in all_media_players
|
||||
if media_player.entity_id != master_id
|
||||
]
|
||||
await hass.async_add_executor_job(master.create_zone, slaves)
|
||||
elif service.service == SERVICE_CREATE_ZONE:
|
||||
@@ -130,7 +143,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SoundTouchConfigEntry) -> bool:
|
||||
"""Set up Bose SoundTouch from a config entry."""
|
||||
try:
|
||||
device = await hass.async_add_executor_job(
|
||||
@@ -141,14 +154,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"Unable to connect to SoundTouch device at {entry.data[CONF_HOST]}"
|
||||
) from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device)
|
||||
entry.runtime_data = SoundTouchData(device)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SoundTouchConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import (
|
||||
@@ -29,6 +28,7 @@ from homeassistant.helpers.device_registry import (
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SoundTouchConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -46,16 +46,16 @@ ATTR_SOUNDTOUCH_ZONE = "soundtouch_zone"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SoundTouchConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Bose SoundTouch media player based on a config entry."""
|
||||
device = hass.data[DOMAIN][entry.entry_id].device
|
||||
device = entry.runtime_data.device
|
||||
media_player = SoundTouchMediaPlayer(device)
|
||||
|
||||
async_add_entities([media_player], True)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id].media_player = media_player
|
||||
entry.runtime_data.media_player = media_player
|
||||
|
||||
|
||||
class SoundTouchMediaPlayer(MediaPlayerEntity):
|
||||
@@ -388,14 +388,16 @@ class SoundTouchMediaPlayer(MediaPlayerEntity):
|
||||
|
||||
def _get_instance_by_ip(self, ip_address):
|
||||
"""Search and return a SoundTouchDevice instance by it's IP address."""
|
||||
for data in self.hass.data[DOMAIN].values():
|
||||
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
data = entry.runtime_data
|
||||
if data.device.config.device_ip == ip_address:
|
||||
return data.media_player
|
||||
return None
|
||||
|
||||
def _get_instance_by_id(self, instance_id):
|
||||
"""Search and return a SoundTouchDevice instance by it's ID (aka MAC address)."""
|
||||
for data in self.hass.data[DOMAIN].values():
|
||||
for entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
data = entry.runtime_data
|
||||
if data.device.config.device_id == instance_id:
|
||||
return data.media_player
|
||||
return None
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
from srpenergy.client import SrpEnergyClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import SRPEnergyDataUpdateCoordinator
|
||||
from .const import LOGGER
|
||||
from .coordinator import SRPEnergyConfigEntry, SRPEnergyDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SRPEnergyConfigEntry) -> bool:
|
||||
"""Set up the SRP Energy component from a config entry."""
|
||||
api_account_id: str = entry.data[CONF_ID]
|
||||
api_username: str = entry.data[CONF_USERNAME]
|
||||
@@ -30,17 +29,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SRPEnergyConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -23,14 +23,19 @@ from .const import (
|
||||
TIMEOUT = 10
|
||||
PHOENIX_ZONE_INFO = dt_util.get_time_zone(PHOENIX_TIME_ZONE)
|
||||
|
||||
type SRPEnergyConfigEntry = ConfigEntry[SRPEnergyDataUpdateCoordinator]
|
||||
|
||||
|
||||
class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]):
|
||||
"""A srp_energy Data Update Coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SRPEnergyConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, client: SrpEnergyClient
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: SRPEnergyConfigEntry,
|
||||
client: SrpEnergyClient,
|
||||
) -> None:
|
||||
"""Initialize the srp_energy data coordinator."""
|
||||
self._client = client
|
||||
|
||||
@@ -7,7 +7,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -15,19 +14,17 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import SRPEnergyDataUpdateCoordinator
|
||||
from .const import DEVICE_CONFIG_URL, DEVICE_MANUFACTURER, DEVICE_MODEL, DOMAIN
|
||||
from .coordinator import SRPEnergyConfigEntry, SRPEnergyDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SRPEnergyConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SRP Energy Usage sensor."""
|
||||
coordinator: SRPEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities([SrpEntity(coordinator, entry)])
|
||||
async_add_entities([SrpEntity(entry.runtime_data, entry)])
|
||||
|
||||
|
||||
class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity):
|
||||
@@ -43,7 +40,7 @@ class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity)
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SRPEnergyDataUpdateCoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: SRPEnergyConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the SrpEntity class."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
from streamlabswater.streamlabswater import StreamlabsClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StreamlabsCoordinator
|
||||
from .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator
|
||||
|
||||
ATTR_AWAY_MODE = "away_mode"
|
||||
SERVICE_SET_AWAY_MODE = "set_away_mode"
|
||||
@@ -30,7 +29,7 @@ SET_AWAY_MODE_SCHEMA = vol.Schema(
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: StreamlabsConfigEntry) -> bool:
|
||||
"""Set up StreamLabs from a config entry."""
|
||||
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
@@ -39,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
def set_away_mode(service: ServiceCall) -> None:
|
||||
@@ -55,9 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: StreamlabsConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import StreamlabsCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator
|
||||
from .entity import StreamlabsWaterEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: StreamlabsConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Streamlabs water binary sensor from a config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
StreamlabsAwayMode(coordinator, location_id) for location_id in coordinator.data
|
||||
|
||||
@@ -23,15 +23,18 @@ class StreamlabsData:
|
||||
yearly_usage: float
|
||||
|
||||
|
||||
type StreamlabsConfigEntry = ConfigEntry[StreamlabsCoordinator]
|
||||
|
||||
|
||||
class StreamlabsCoordinator(DataUpdateCoordinator[dict[str, StreamlabsData]]):
|
||||
"""Coordinator for Streamlabs."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: StreamlabsConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: StreamlabsConfigEntry,
|
||||
client: StreamlabsClient,
|
||||
) -> None:
|
||||
"""Coordinator for Streamlabs."""
|
||||
|
||||
@@ -10,15 +10,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import StreamlabsCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StreamlabsData
|
||||
from .coordinator import StreamlabsConfigEntry, StreamlabsCoordinator, StreamlabsData
|
||||
from .entity import StreamlabsWaterEntity
|
||||
|
||||
|
||||
@@ -59,11 +56,11 @@ SENSORS: tuple[StreamlabsWaterSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: StreamlabsConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Streamlabs water sensor from a config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
StreamLabsSensor(coordinator, location_id, entity_description)
|
||||
|
||||
@@ -9,7 +9,6 @@ from surepy.enums import Location
|
||||
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
@@ -24,7 +23,7 @@ from .const import (
|
||||
SERVICE_SET_LOCK_STATE,
|
||||
SERVICE_SET_PET_LOCATION,
|
||||
)
|
||||
from .coordinator import SurePetcareDataCoordinator
|
||||
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,15 +31,10 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=3)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SurePetcareConfigEntry) -> bool:
|
||||
"""Set up Sure Petcare from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
try:
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator = SurePetcareDataCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
)
|
||||
coordinator = SurePetcareDataCoordinator(hass, entry)
|
||||
except SurePetcareAuthenticationError as error:
|
||||
_LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!")
|
||||
raise ConfigEntryAuthFailed from error
|
||||
@@ -49,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
lock_state_service_schema = vol.Schema(
|
||||
@@ -91,10 +86,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SurePetcareConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -12,26 +12,24 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SurePetcareDataCoordinator
|
||||
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
|
||||
from .entity import SurePetcareEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SurePetcareConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sure PetCare Flaps binary sensors based on a config entry."""
|
||||
|
||||
entities: list[SurePetcareBinarySensor] = []
|
||||
|
||||
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
for surepy_entity in coordinator.data.values():
|
||||
# connectivity
|
||||
|
||||
@@ -29,13 +29,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=3)
|
||||
|
||||
type SurePetcareConfigEntry = ConfigEntry[SurePetcareDataCoordinator]
|
||||
|
||||
|
||||
class SurePetcareDataCoordinator(DataUpdateCoordinator[dict[int, SurepyEntity]]):
|
||||
"""Handle Surepetcare data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SurePetcareConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, entry: SurePetcareConfigEntry) -> None:
|
||||
"""Initialize the data handler."""
|
||||
self.surepy = Surepy(
|
||||
entry.data[CONF_USERNAME],
|
||||
|
||||
@@ -8,23 +8,21 @@ from surepy.entities import SurepyEntity
|
||||
from surepy.enums import EntityType, LockState as SurepyLockState
|
||||
|
||||
from homeassistant.components.lock import LockEntity, LockState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SurePetcareDataCoordinator
|
||||
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
|
||||
from .entity import SurePetcareEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SurePetcareConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sure PetCare locks on a config entry."""
|
||||
|
||||
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
SurePetcareLock(surepy_entity.id, coordinator, lock_state)
|
||||
|
||||
@@ -10,26 +10,25 @@ from surepy.entities.pet import Pet as SurepyPet
|
||||
from surepy.enums import EntityType
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_VOLTAGE, PERCENTAGE, EntityCategory, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW
|
||||
from .coordinator import SurePetcareDataCoordinator
|
||||
from .const import SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW
|
||||
from .coordinator import SurePetcareConfigEntry, SurePetcareDataCoordinator
|
||||
from .entity import SurePetcareEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SurePetcareConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sure PetCare Flaps sensors."""
|
||||
|
||||
entities: list[SurePetcareEntity] = []
|
||||
|
||||
coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
for surepy_entity in coordinator.data.values():
|
||||
if surepy_entity.type in [
|
||||
|
||||
@@ -9,7 +9,6 @@ from aiohttp import ClientSession
|
||||
from switchbee.api import CentralUnitPolling, CentralUnitWsRPC, is_wsrpc_api
|
||||
from switchbee.api.central_unit import SwitchBeeError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -17,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SwitchBeeCoordinator
|
||||
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,10 +52,9 @@ async def get_api_object(
|
||||
return api
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SwitchBeeConfigEntry) -> bool:
|
||||
"""Set up SwitchBee Smart Home from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
central_unit = entry.data[CONF_HOST]
|
||||
user = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
@@ -67,27 +65,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SwitchBeeConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
async def update_listener(
|
||||
hass: HomeAssistant, config_entry: SwitchBeeConfigEntry
|
||||
) -> None:
|
||||
"""Update listener."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: SwitchBeeConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
|
||||
@@ -4,23 +4,21 @@ from switchbee.api.central_unit import SwitchBeeError
|
||||
from switchbee.device import ApiStateCommand, DeviceType
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SwitchBeeCoordinator
|
||||
from .coordinator import SwitchBeeConfigEntry
|
||||
from .entity import SwitchBeeEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchBeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Switchbee button."""
|
||||
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
SwitchBeeButton(switchbee_device, coordinator)
|
||||
for switchbee_device in coordinator.data.values()
|
||||
|
||||
@@ -23,14 +23,12 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SwitchBeeCoordinator
|
||||
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
|
||||
from .entity import SwitchBeeDeviceEntity
|
||||
|
||||
FAN_SB_TO_HASS = {
|
||||
@@ -75,11 +73,11 @@ SUPPORTED_FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW]
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchBeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBee climate."""
|
||||
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
SwitchBeeClimateEntity(switchbee_device, coordinator)
|
||||
for switchbee_device in coordinator.data.values()
|
||||
|
||||
@@ -19,16 +19,18 @@ from .const import DOMAIN, SCAN_INTERVAL_SEC
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SwitchBeeConfigEntry = ConfigEntry[SwitchBeeCoordinator]
|
||||
|
||||
|
||||
class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevice]]):
|
||||
"""Class to manage fetching SwitchBee data API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SwitchBeeConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: SwitchBeeConfigEntry,
|
||||
swb_api: CentralUnitPolling | CentralUnitWsRPC,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
|
||||
@@ -14,23 +14,21 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SwitchBeeCoordinator
|
||||
from .coordinator import SwitchBeeConfigEntry
|
||||
from .entity import SwitchBeeDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchBeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBee switch."""
|
||||
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
"""Set up SwitchBee covers."""
|
||||
coordinator = entry.runtime_data
|
||||
entities: list[CoverEntity] = []
|
||||
|
||||
for device in coordinator.data.values():
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from switchbee.api.central_unit import SwitchBeeDeviceOfflineError, SwitchBeeError
|
||||
from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeDimmer
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SwitchBeeCoordinator
|
||||
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
|
||||
from .entity import SwitchBeeDeviceEntity
|
||||
|
||||
MAX_BRIGHTNESS = 255
|
||||
@@ -36,13 +34,13 @@ def _switchbee_brightness_to_hass(value: int) -> int:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchBeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBee light."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
SwitchBeeLightEntity(switchbee_device, coordinator)
|
||||
SwitchBeeLightEntity(cast(SwitchBeeDimmer, switchbee_device), coordinator)
|
||||
for switchbee_device in coordinator.data.values()
|
||||
if switchbee_device.type == DeviceType.Dimmer
|
||||
)
|
||||
|
||||
@@ -14,23 +14,21 @@ from switchbee.device import (
|
||||
)
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SwitchBeeCoordinator
|
||||
from .coordinator import SwitchBeeConfigEntry, SwitchBeeCoordinator
|
||||
from .entity import SwitchBeeDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchBeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Switchbee switch."""
|
||||
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
SwitchBeeSwitchEntity(device, coordinator)
|
||||
|
||||
@@ -114,21 +114,25 @@ PLATFORMS_BY_TYPE = {
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.SWITCH,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_US.value: [
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.SWITCH,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_TABLE_JP.value: [
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.SWITCH,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_TABLE_US.value: [
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.SWITCH,
|
||||
],
|
||||
SupportedModels.EVAPORATIVE_HUMIDIFIER.value: [
|
||||
Platform.HUMIDIFIER,
|
||||
|
||||
@@ -145,6 +145,20 @@
|
||||
"medium": "mdi:water"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"child_lock": {
|
||||
"state": {
|
||||
"off": "mdi:lock-open",
|
||||
"on": "mdi:lock"
|
||||
}
|
||||
},
|
||||
"wireless_charging": {
|
||||
"state": {
|
||||
"off": "mdi:battery-charging-wireless-outline",
|
||||
"on": "mdi:battery-charging-wireless"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -326,6 +326,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"child_lock": {
|
||||
"name": "Child lock"
|
||||
},
|
||||
"wireless_charging": {
|
||||
"name": "Wireless charging"
|
||||
}
|
||||
},
|
||||
"vacuum": {
|
||||
|
||||
@@ -2,22 +2,61 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import switchbot
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import AIRPURIFIER_BASIC_MODELS, AIRPURIFIER_TABLE_MODELS, DOMAIN
|
||||
from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator
|
||||
from .entity import SwitchbotSwitchedEntity, exception_handler
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SwitchbotSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes a Switchbot switch entity."""
|
||||
|
||||
is_on_fn: Callable[[switchbot.SwitchbotDevice], bool | None]
|
||||
turn_on_fn: Callable[[switchbot.SwitchbotDevice], Awaitable[Any]]
|
||||
turn_off_fn: Callable[[switchbot.SwitchbotDevice], Awaitable[Any]]
|
||||
|
||||
|
||||
AIRPURIFIER_BASIC_SWITCHES: tuple[SwitchbotSwitchEntityDescription, ...] = (
|
||||
SwitchbotSwitchEntityDescription(
|
||||
key="child_lock",
|
||||
translation_key="child_lock",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
is_on_fn=lambda device: device.is_child_lock_on(),
|
||||
turn_on_fn=lambda device: device.open_child_lock(),
|
||||
turn_off_fn=lambda device: device.close_child_lock(),
|
||||
),
|
||||
)
|
||||
|
||||
AIRPURIFIER_TABLE_SWITCHES: tuple[SwitchbotSwitchEntityDescription, ...] = (
|
||||
*AIRPURIFIER_BASIC_SWITCHES,
|
||||
SwitchbotSwitchEntityDescription(
|
||||
key="wireless_charging",
|
||||
translation_key="wireless_charging",
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
is_on_fn=lambda device: device.is_wireless_charging_on(),
|
||||
turn_on_fn=lambda device: device.open_wireless_charging(),
|
||||
turn_off_fn=lambda device: device.close_wireless_charging(),
|
||||
),
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,10 +75,64 @@ async def async_setup_entry(
|
||||
for channel in range(1, coordinator.device.channel + 1)
|
||||
]
|
||||
async_add_entities(entries)
|
||||
elif coordinator.model in AIRPURIFIER_BASIC_MODELS:
|
||||
async_add_entities(
|
||||
[
|
||||
SwitchbotGenericSwitch(coordinator, desc)
|
||||
for desc in AIRPURIFIER_BASIC_SWITCHES
|
||||
]
|
||||
)
|
||||
elif coordinator.model in AIRPURIFIER_TABLE_MODELS:
|
||||
async_add_entities(
|
||||
[
|
||||
SwitchbotGenericSwitch(coordinator, desc)
|
||||
for desc in AIRPURIFIER_TABLE_SWITCHES
|
||||
]
|
||||
)
|
||||
else:
|
||||
async_add_entities([SwitchBotSwitch(coordinator)])
|
||||
|
||||
|
||||
class SwitchbotGenericSwitch(SwitchbotSwitchedEntity, SwitchEntity):
|
||||
"""Representation of a Switchbot switch controlled via entity description."""
|
||||
|
||||
entity_description: SwitchbotSwitchEntityDescription
|
||||
_device: switchbot.SwitchbotDevice
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SwitchbotDataUpdateCoordinator,
|
||||
description: SwitchbotSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Switchbot generic switch."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.base_unique_id}-{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if device is on."""
|
||||
return self.entity_description.is_on_fn(self._device)
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
_LOGGER.debug(
|
||||
"Turning on %s for %s", self.entity_description.key, self._address
|
||||
)
|
||||
await self.entity_description.turn_on_fn(self._device)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
_LOGGER.debug(
|
||||
"Turning off %s for %s", self.entity_description.key, self._address
|
||||
)
|
||||
await self.entity_description.turn_off_fn(self._device)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class SwitchBotSwitch(SwitchbotSwitchedEntity, SwitchEntity, RestoreEntity):
|
||||
"""Representation of a Switchbot switch."""
|
||||
|
||||
|
||||
@@ -75,9 +75,12 @@ class SwitchbotCloudData:
|
||||
devices: SwitchbotDevices
|
||||
|
||||
|
||||
type SwitchbotCloudConfigEntry = ConfigEntry[SwitchbotCloudData]
|
||||
|
||||
|
||||
async def coordinator_for_device(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchbotCloudConfigEntry,
|
||||
api: SwitchBotAPI,
|
||||
device: Device | Remote,
|
||||
coordinators_by_id: dict[str, SwitchBotCoordinator],
|
||||
@@ -97,7 +100,7 @@ async def coordinator_for_device(
|
||||
|
||||
async def make_switchbot_devices(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchbotCloudConfigEntry,
|
||||
api: SwitchBotAPI,
|
||||
devices: list[Device | Remote],
|
||||
coordinators_by_id: dict[str, SwitchBotCoordinator],
|
||||
@@ -115,7 +118,7 @@ async def make_switchbot_devices(
|
||||
|
||||
async def make_device_data(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchbotCloudConfigEntry,
|
||||
api: SwitchBotAPI,
|
||||
device: Device | Remote,
|
||||
devices_data: SwitchbotDevices,
|
||||
@@ -330,7 +333,9 @@ async def make_device_data(
|
||||
devices_data.sensors.append((device, coordinator))
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: SwitchbotCloudConfigEntry
|
||||
) -> bool:
|
||||
"""Set up SwitchBot via API from a config entry."""
|
||||
token = entry.data[CONF_API_TOKEN]
|
||||
secret = entry.data[CONF_API_KEY]
|
||||
@@ -353,10 +358,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
switchbot_devices = await make_switchbot_devices(
|
||||
hass, entry, api, devices, coordinators_by_id
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = SwitchbotCloudData(
|
||||
api=api, devices=switchbot_devices
|
||||
)
|
||||
entry.runtime_data = SwitchbotCloudData(api=api, devices=switchbot_devices)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -365,17 +367,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SwitchbotCloudConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def _initialize_webhook(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchbotCloudConfigEntry,
|
||||
api: SwitchBotAPI,
|
||||
coordinators_by_id: dict[str, SwitchBotCoordinator],
|
||||
) -> None:
|
||||
|
||||
@@ -11,13 +11,11 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from .const import DOMAIN
|
||||
from . import SwitchbotCloudConfigEntry
|
||||
from .coordinator import SwitchBotCoordinator
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
@@ -137,11 +135,11 @@ BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
SwitchBotCloudBinarySensor(data.api, device, coordinator, description)
|
||||
|
||||
@@ -12,12 +12,10 @@ from switchbot_api import (
|
||||
from switchbot_api.commands import ArtFrameCommands, BotCommands, CommonCommands
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import DOMAIN
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
|
||||
@@ -58,11 +56,11 @@ BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
entities: list[SwitchBotCloudBot] = []
|
||||
for device, coordinator in data.devices.buttons:
|
||||
description_set = BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type]
|
||||
|
||||
@@ -26,7 +26,6 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PRECISION_TENTHS,
|
||||
STATE_UNAVAILABLE,
|
||||
@@ -37,10 +36,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .const import (
|
||||
CLIMATE_PRESET_SCHEDULE,
|
||||
DOMAIN,
|
||||
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH,
|
||||
)
|
||||
from .entity import SwitchBotCloudEntity
|
||||
@@ -69,11 +67,11 @@ _DEFAULT_SWITCHBOT_FAN_MODE = _SWITCHBOT_FAN_MODES[FanState.FAN_AUTO]
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
async_add_entities(
|
||||
_async_make_entity(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.climates
|
||||
|
||||
@@ -18,22 +18,21 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import COVER_ENTITY_AFTER_COMMAND_REFRESH, DOMAIN
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .const import COVER_ENTITY_AFTER_COMMAND_REFRESH
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
async_add_entities(
|
||||
_async_make_entity(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.covers
|
||||
|
||||
@@ -13,13 +13,12 @@ from switchbot_api import (
|
||||
)
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from .const import AFTER_COMMAND_REFRESH, DOMAIN, AirPurifierMode
|
||||
from . import SwitchbotCloudConfigEntry
|
||||
from .const import AFTER_COMMAND_REFRESH, AirPurifierMode
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -28,11 +27,11 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
for device, coordinator in data.devices.fans:
|
||||
if device.device_type.startswith("Air Purifier"):
|
||||
async_add_entities(
|
||||
|
||||
@@ -12,13 +12,12 @@ from homeassistant.components.humidifier import (
|
||||
HumidifierEntity,
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from .const import AFTER_COMMAND_REFRESH, DOMAIN, HUMIDITY_LEVELS, Humidifier2Mode
|
||||
from . import SwitchbotCloudConfigEntry
|
||||
from .const import AFTER_COMMAND_REFRESH, HUMIDITY_LEVELS, Humidifier2Mode
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -26,11 +25,11 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Switchbot based on a config entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
async_add_entities(
|
||||
SwitchBotHumidifier(data.api, device, coordinator)
|
||||
if device.device_type == "Humidifier"
|
||||
|
||||
@@ -6,22 +6,20 @@ from switchbot_api import Device, Remote, SwitchBotAPI
|
||||
from switchbot_api.utils import get_file_stream_from_cloud
|
||||
|
||||
from homeassistant.components.image import ImageEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import DOMAIN
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
async_add_entities(
|
||||
_async_make_entity(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.images
|
||||
|
||||
@@ -14,12 +14,11 @@ from switchbot_api import (
|
||||
)
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import AFTER_COMMAND_REFRESH, DOMAIN
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .const import AFTER_COMMAND_REFRESH
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
|
||||
@@ -35,11 +34,11 @@ def brightness_map_value(value: int) -> int:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
async_add_entities(
|
||||
_async_make_entity(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.lights
|
||||
|
||||
@@ -5,22 +5,20 @@ from typing import Any
|
||||
from switchbot_api import Device, LockCommands, LockV2Commands, Remote, SwitchBotAPI
|
||||
|
||||
from homeassistant.components.lock import LockEntity, LockEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import DOMAIN
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
async_add_entities(
|
||||
SwitchBotCloudLock(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.locks
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
@@ -26,7 +25,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from . import SwitchbotCloudConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SwitchBotCoordinator
|
||||
from .entity import SwitchBotCloudEntity
|
||||
@@ -267,11 +266,11 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
entities: list[SwitchBotCloudSensor] = []
|
||||
for device, coordinator in data.devices.sensors:
|
||||
for description in SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type]:
|
||||
|
||||
@@ -6,12 +6,11 @@ from typing import Any
|
||||
from switchbot_api import CommonCommands, Device, PowerState, Remote, SwitchBotAPI
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from . import SwitchbotCloudConfigEntry
|
||||
from .const import AFTER_COMMAND_REFRESH, DOMAIN
|
||||
from .coordinator import SwitchBotCoordinator
|
||||
from .entity import SwitchBotCloudEntity
|
||||
@@ -19,11 +18,11 @@ from .entity import SwitchBotCloudEntity
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
entities: list[SwitchBotCloudSwitch] = []
|
||||
for device, coordinator in data.devices.switches:
|
||||
if device.device_type == "Relay Switch 2PM":
|
||||
|
||||
@@ -17,13 +17,11 @@ from homeassistant.components.vacuum import (
|
||||
VacuumActivity,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from . import SwitchbotCloudConfigEntry
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
VACUUM_FAN_SPEED_MAX,
|
||||
VACUUM_FAN_SPEED_QUIET,
|
||||
VACUUM_FAN_SPEED_STANDARD,
|
||||
@@ -35,11 +33,11 @@ from .entity import SwitchBotCloudEntity
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
data = config.runtime_data
|
||||
async_add_entities(
|
||||
_async_make_entity(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.vacuums
|
||||
|
||||
@@ -21,7 +21,7 @@ from systembridgeconnector.models.open_url import OpenUrl
|
||||
from systembridgeconnector.version import Version
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_COMMAND,
|
||||
@@ -57,7 +57,24 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
||||
|
||||
from .config_flow import SystemBridgeConfigFlow
|
||||
from .const import DATA_WAIT_TIMEOUT, DOMAIN, MODULES
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
|
||||
|
||||
def _get_coordinator(
|
||||
hass: HomeAssistant, entry_id: str
|
||||
) -> SystemBridgeDataUpdateCoordinator:
|
||||
"""Return the coordinator for a config entry id."""
|
||||
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
entry_id
|
||||
)
|
||||
if entry is None or entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"device": entry_id},
|
||||
)
|
||||
return entry.runtime_data
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -93,7 +110,7 @@ POWER_COMMAND_MAP = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
) -> bool:
|
||||
"""Set up System Bridge from a config entry."""
|
||||
|
||||
@@ -198,8 +215,7 @@ async def async_setup_entry(
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Set up all platforms except notify
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
@@ -216,7 +232,7 @@ async def async_setup_entry(
|
||||
CONF_NAME: f"{DOMAIN}_{coordinator.data.system.hostname}",
|
||||
CONF_ENTITY_ID: entry.entry_id,
|
||||
},
|
||||
hass.data[DOMAIN][entry.entry_id],
|
||||
{},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -249,9 +265,7 @@ async def async_setup_entry(
|
||||
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the get process by id service call."""
|
||||
_LOGGER.debug("Get process by id: %s", service_call.data)
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
service_call.data[CONF_BRIDGE]
|
||||
]
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
processes: list[Process] = coordinator.data.processes
|
||||
|
||||
# Find process.id from list, raise ServiceValidationError if not found
|
||||
@@ -275,9 +289,7 @@ async def async_setup_entry(
|
||||
) -> ServiceResponse:
|
||||
"""Handle the get process by name service call."""
|
||||
_LOGGER.debug("Get process by name: %s", service_call.data)
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
service_call.data[CONF_BRIDGE]
|
||||
]
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
|
||||
# Find processes from list
|
||||
items: list[dict[str, Any]] = [
|
||||
@@ -295,9 +307,7 @@ async def async_setup_entry(
|
||||
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the open path service call."""
|
||||
_LOGGER.debug("Open path: %s", service_call.data)
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
service_call.data[CONF_BRIDGE]
|
||||
]
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.open_path(
|
||||
OpenPath(path=service_call.data[CONF_PATH])
|
||||
)
|
||||
@@ -306,9 +316,7 @@ async def async_setup_entry(
|
||||
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the power command service call."""
|
||||
_LOGGER.debug("Power command: %s", service_call.data)
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
service_call.data[CONF_BRIDGE]
|
||||
]
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await getattr(
|
||||
coordinator.websocket_client,
|
||||
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
|
||||
@@ -318,9 +326,7 @@ async def async_setup_entry(
|
||||
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the open url service call."""
|
||||
_LOGGER.debug("Open URL: %s", service_call.data)
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
service_call.data[CONF_BRIDGE]
|
||||
]
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.open_url(
|
||||
OpenUrl(url=service_call.data[CONF_URL])
|
||||
)
|
||||
@@ -328,9 +334,7 @@ async def async_setup_entry(
|
||||
|
||||
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
service_call.data[CONF_BRIDGE]
|
||||
]
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.keyboard_keypress(
|
||||
KeyboardKey(key=service_call.data[CONF_KEY])
|
||||
)
|
||||
@@ -338,9 +342,7 @@ async def async_setup_entry(
|
||||
|
||||
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
service_call.data[CONF_BRIDGE]
|
||||
]
|
||||
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
|
||||
response = await coordinator.websocket_client.keyboard_text(
|
||||
KeyboardText(text=service_call.data[CONF_TEXT])
|
||||
)
|
||||
@@ -446,33 +448,27 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SystemBridgeConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
|
||||
)
|
||||
if unload_ok:
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Ensure disconnected and cleanup stop sub
|
||||
await coordinator.websocket_client.close()
|
||||
if coordinator.unsub:
|
||||
coordinator.unsub()
|
||||
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, SERVICE_OPEN_PATH)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_OPEN_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SEND_KEYPRESS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SEND_TEXT)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def async_reload_entry(
|
||||
hass: HomeAssistant, entry: SystemBridgeConfigEntry
|
||||
) -> None:
|
||||
"""Reload the config entry when it changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
from .data import SystemBridgeData
|
||||
from .entity import SystemBridgeEntity
|
||||
|
||||
@@ -64,11 +62,11 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ..
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up System Bridge binary sensor based on a config entry."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities = [
|
||||
SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT])
|
||||
|
||||
@@ -36,18 +36,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import DOMAIN, GET_DATA_WAIT_TIMEOUT, MODULES
|
||||
from .data import SystemBridgeData
|
||||
|
||||
type SystemBridgeConfigEntry = ConfigEntry[SystemBridgeDataUpdateCoordinator]
|
||||
|
||||
|
||||
class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[SystemBridgeData]):
|
||||
"""Class to manage fetching System Bridge data from single endpoint."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: SystemBridgeConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
LOGGER: logging.Logger,
|
||||
*,
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize global System Bridge data updater."""
|
||||
self.title = entry.title
|
||||
|
||||
@@ -15,13 +15,11 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
RepeatMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
from .data import SystemBridgeData
|
||||
from .entity import SystemBridgeEntity
|
||||
|
||||
@@ -64,11 +62,11 @@ MEDIA_PLAYER_DESCRIPTION: Final[MediaPlayerEntityDescription] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up System Bridge media players based on a config entry."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
data = coordinator.data
|
||||
|
||||
if data.media is not None:
|
||||
|
||||
@@ -15,12 +15,22 @@ from homeassistant.components.media_source import (
|
||||
MediaSourceItem,
|
||||
PlayMedia,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeConfigEntry
|
||||
|
||||
|
||||
def _get_loaded_entry(hass: HomeAssistant, entry_id: str) -> SystemBridgeConfigEntry:
|
||||
"""Return a loaded System Bridge config entry by id."""
|
||||
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
entry_id
|
||||
)
|
||||
if entry is None or entry.state is not ConfigEntryState.LOADED:
|
||||
raise ValueError("Invalid entry")
|
||||
return entry
|
||||
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
@@ -46,9 +56,7 @@ class SystemBridgeSource(MediaSource):
|
||||
) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
entry_id, path, mime_type = item.identifier.split("~~", 2)
|
||||
entry = self.hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None:
|
||||
raise ValueError("Invalid entry")
|
||||
entry = _get_loaded_entry(self.hass, entry_id)
|
||||
path_split = path.split("/", 1)
|
||||
return PlayMedia(
|
||||
f"{_build_base_url(entry)}&base={path_split[0]}&path={path_split[1]}",
|
||||
@@ -64,21 +72,14 @@ class SystemBridgeSource(MediaSource):
|
||||
return self._build_bridges()
|
||||
|
||||
if "~~" not in item.identifier:
|
||||
entry = self.hass.config_entries.async_get_entry(item.identifier)
|
||||
if entry is None:
|
||||
raise ValueError("Invalid entry")
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = self.hass.data[DOMAIN].get(
|
||||
entry.entry_id
|
||||
)
|
||||
entry = _get_loaded_entry(self.hass, item.identifier)
|
||||
coordinator = entry.runtime_data
|
||||
directories = await coordinator.websocket_client.get_directories()
|
||||
return _build_root_paths(entry, directories)
|
||||
|
||||
entry_id, path = item.identifier.split("~~", 1)
|
||||
entry = self.hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None:
|
||||
raise ValueError("Invalid entry")
|
||||
|
||||
coordinator = self.hass.data[DOMAIN].get(entry.entry_id)
|
||||
entry = _get_loaded_entry(self.hass, entry_id)
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
path_split = path.split("/", 1)
|
||||
|
||||
@@ -123,7 +124,7 @@ class SystemBridgeSource(MediaSource):
|
||||
|
||||
|
||||
def _build_base_url(
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
) -> str:
|
||||
"""Build base url for System Bridge media."""
|
||||
return (
|
||||
@@ -133,7 +134,7 @@ def _build_base_url(
|
||||
|
||||
|
||||
def _build_root_paths(
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
media_directories: list[MediaDirectory],
|
||||
) -> BrowseMediaSource:
|
||||
"""Build base categories for System Bridge media."""
|
||||
@@ -164,7 +165,7 @@ def _build_root_paths(
|
||||
|
||||
|
||||
def _build_media_items(
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
media_files: MediaFiles,
|
||||
path: str,
|
||||
identifier: str,
|
||||
|
||||
@@ -17,8 +17,7 @@ from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,11 +36,13 @@ async def async_get_service(
|
||||
if discovery_info is None:
|
||||
return None
|
||||
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
discovery_info[CONF_ENTITY_ID]
|
||||
]
|
||||
)
|
||||
if entry is None:
|
||||
return None
|
||||
|
||||
return SystemBridgeNotificationService(coordinator)
|
||||
return SystemBridgeNotificationService(entry.runtime_data)
|
||||
|
||||
|
||||
class SystemBridgeNotificationService(BaseNotificationService):
|
||||
|
||||
@@ -17,7 +17,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
PERCENTAGE,
|
||||
@@ -33,8 +32,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import UNDEFINED, StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
from .data import SystemBridgeData
|
||||
from .entity import SystemBridgeEntity
|
||||
|
||||
@@ -364,11 +362,11 @@ BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up System Bridge sensor based on a config entry."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities = [
|
||||
SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT])
|
||||
|
||||
@@ -3,23 +3,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.update import UpdateEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SystemBridgeDataUpdateCoordinator
|
||||
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
|
||||
from .entity import SystemBridgeEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: SystemBridgeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up System Bridge update based on a config entry."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
|
||||
@@ -12,7 +12,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import ATTR_BOOT_TIME, ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_HOST
|
||||
from .const import ATTR_BOOT_TIME, ATTR_LOAD, ROUTER_DEFAULT_HOST
|
||||
|
||||
type VilfoConfigEntry = ConfigEntry[VilfoRouterData]
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
@@ -21,7 +23,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: VilfoConfigEntry) -> bool:
|
||||
"""Set up Vilfo Router from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
access_token = entry.data[CONF_ACCESS_TOKEN]
|
||||
@@ -33,21 +35,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not vilfo_router.available:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = vilfo_router
|
||||
entry.runtime_data = vilfo_router
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: VilfoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
class VilfoRouterData:
|
||||
|
||||
@@ -7,12 +7,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import VilfoConfigEntry
|
||||
from .const import (
|
||||
ATTR_API_DATA_FIELD_BOOT_TIME,
|
||||
ATTR_API_DATA_FIELD_LOAD,
|
||||
@@ -50,11 +50,11 @@ SENSOR_TYPES: tuple[VilfoSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: VilfoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Vilfo Router entities from a config_entry."""
|
||||
vilfo = hass.data[DOMAIN][config_entry.entry_id]
|
||||
vilfo = config_entry.runtime_data
|
||||
|
||||
entities = [VilfoRouterSensor(vilfo, description) for description in SENSOR_TYPES]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -155,7 +155,7 @@ async def test_setup_api_ping(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
|
||||
|
||||
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) == 20
|
||||
assert len(supervisor_client.mock_calls) == 21
|
||||
|
||||
await hass.services.async_call("homeassistant", "check_config")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(supervisor_client.mock_calls) == 20
|
||||
assert len(supervisor_client.mock_calls) == 21
|
||||
|
||||
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) == 21
|
||||
assert len(supervisor_client.mock_calls) == 22
|
||||
|
||||
|
||||
@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.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_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.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_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.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_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.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_updates.assert_called_once()
|
||||
|
||||
supervisor_client.refresh_updates.reset_mock()
|
||||
supervisor_client.refresh_updates.side_effect = SupervisorError("Unknown")
|
||||
supervisor_client.reload_updates.reset_mock()
|
||||
supervisor_client.reload_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.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_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.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
|
||||
# Refresh with stats once we know which ones are needed
|
||||
# Stats entities trigger refresh on the stats coordinator,
|
||||
# which does not call reload_updates
|
||||
async_fire_time_changed(
|
||||
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
supervisor_client.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
|
||||
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.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_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.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_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.refresh_updates.reset_mock()
|
||||
supervisor_client.refresh_updates.side_effect = SupervisorError("Unknown")
|
||||
supervisor_client.reload_updates.reset_mock()
|
||||
supervisor_client.reload_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.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_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) == 23
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.hassio import DOMAIN, HASSIO_UPDATE_INTERVAL
|
||||
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
|
||||
from homeassistant.components.hassio import DOMAIN
|
||||
from homeassistant.components.hassio.const import (
|
||||
HASSIO_STATS_UPDATE_INTERVAL,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -176,14 +179,14 @@ async def test_stats_addon_sensor(
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
addon_stats.side_effect = SupervisorError
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_STATS_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_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_STATS_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
@@ -199,13 +202,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_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_STATS_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_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_STATS_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.
|
||||
@@ -213,10 +216,29 @@ async def test_stats_addon_sensor(
|
||||
assert state.state == expected
|
||||
|
||||
addon_stats.side_effect = SupervisorError
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_STATS_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
Reference in New Issue
Block a user