forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Keep track of addons and integrations when determining HA radio firmw…
…are type (home-assistant#134598) * Replace `FirmwareGuess` with `FirmwareInfo` with owner tracking * Fix up config flow * Account for OTBR addon existing independent of integration * Fix remaining unit tests * Add some tests for ownership * Unit test `get_zha_firmware_info` * ZHA `homeassistant_hardware` platform * OTBR `homeassistant_hardware` platform * Rework imports * Fix unit tests * Add OTBR unit tests * Add hassfest exemption for `homeassistant_hardware` and `otbr` * Invert registration to decouple the hardware integration * Revert "Add hassfest exemption for `homeassistant_hardware` and `otbr`" This reverts commit c8c6e70. * Fix circular imports * Fix unit tests * Address review comments * Simplify API a little * Fix `| None` mypy issues * Remove the `unregister_firmware_info_provider` API * 100% coverage * Add `HardwareInfoDispatcher.register_firmware_info_callback` * Unit test `register_firmware_info_callback` (zha) * Unit test `register_firmware_info_callback` (otbr) * Update existing hardware helper tests to use the new function * Add `async_` prefix to helper function names * Move OTBR implementation to a separate PR * Update ZHA diagnostics snapshot * Switch from `dict.setdefault` to `defaultdict` * Add some error handling to `iter_firmware_info` and increase test coverage * Oops
- Loading branch information
Showing
18 changed files
with
913 additions
and
231 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143
homeassistant/components/homeassistant_hardware/helpers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
"""Home Assistant Hardware integration helpers.""" | ||
|
||
from collections import defaultdict | ||
from collections.abc import AsyncIterator, Awaitable, Callable | ||
import logging | ||
from typing import Protocol | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback | ||
|
||
from . import DATA_COMPONENT | ||
from .util import FirmwareInfo | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class SyncHardwareFirmwareInfoModule(Protocol): | ||
"""Protocol type for Home Assistant Hardware firmware info platform modules.""" | ||
|
||
def get_firmware_info( | ||
self, | ||
hass: HomeAssistant, | ||
entry: ConfigEntry, | ||
) -> FirmwareInfo | None: | ||
"""Return radio firmware information for the config entry, synchronously.""" | ||
|
||
|
||
class AsyncHardwareFirmwareInfoModule(Protocol): | ||
"""Protocol type for Home Assistant Hardware firmware info platform modules.""" | ||
|
||
async def async_get_firmware_info( | ||
self, | ||
hass: HomeAssistant, | ||
entry: ConfigEntry, | ||
) -> FirmwareInfo | None: | ||
"""Return radio firmware information for the config entry, asynchronously.""" | ||
|
||
|
||
type HardwareFirmwareInfoModule = ( | ||
SyncHardwareFirmwareInfoModule | AsyncHardwareFirmwareInfoModule | ||
) | ||
|
||
|
||
class HardwareInfoDispatcher: | ||
"""Central dispatcher for hardware/firmware information.""" | ||
|
||
def __init__(self, hass: HomeAssistant) -> None: | ||
"""Initialize the dispatcher.""" | ||
self.hass = hass | ||
self._providers: dict[str, HardwareFirmwareInfoModule] = {} | ||
self._notification_callbacks: defaultdict[ | ||
str, set[Callable[[FirmwareInfo], None]] | ||
] = defaultdict(set) | ||
|
||
def register_firmware_info_provider( | ||
self, domain: str, platform: HardwareFirmwareInfoModule | ||
) -> None: | ||
"""Register a firmware info provider.""" | ||
if domain in self._providers: | ||
raise ValueError( | ||
f"Domain {domain} is already registered as a firmware info provider" | ||
) | ||
|
||
# There is no need to handle "unregistration" because integrations cannot be | ||
# wholly removed at runtime | ||
self._providers[domain] = platform | ||
_LOGGER.debug( | ||
"Registered firmware info provider from domain %r: %s", domain, platform | ||
) | ||
|
||
def register_firmware_info_callback( | ||
self, device: str, callback: Callable[[FirmwareInfo], None] | ||
) -> CALLBACK_TYPE: | ||
"""Register a firmware info notification callback.""" | ||
self._notification_callbacks[device].add(callback) | ||
|
||
@hass_callback | ||
def async_remove_callback() -> None: | ||
self._notification_callbacks[device].discard(callback) | ||
|
||
return async_remove_callback | ||
|
||
async def notify_firmware_info( | ||
self, domain: str, firmware_info: FirmwareInfo | ||
) -> None: | ||
"""Notify the dispatcher of new firmware information.""" | ||
_LOGGER.debug( | ||
"Received firmware info notification from %r: %s", domain, firmware_info | ||
) | ||
|
||
for callback in self._notification_callbacks.get(firmware_info.device, []): | ||
try: | ||
callback(firmware_info) | ||
except Exception: | ||
_LOGGER.exception( | ||
"Error while notifying firmware info listener %s", callback | ||
) | ||
|
||
async def iter_firmware_info(self) -> AsyncIterator[FirmwareInfo]: | ||
"""Iterate over all firmware information for all hardware.""" | ||
for domain, fw_info_module in self._providers.items(): | ||
for config_entry in self.hass.config_entries.async_entries(domain): | ||
try: | ||
if hasattr(fw_info_module, "get_firmware_info"): | ||
fw_info = fw_info_module.get_firmware_info( | ||
self.hass, config_entry | ||
) | ||
else: | ||
fw_info = await fw_info_module.async_get_firmware_info( | ||
self.hass, config_entry | ||
) | ||
except Exception: | ||
_LOGGER.exception( | ||
"Error while getting firmware info from %r", fw_info_module | ||
) | ||
continue | ||
|
||
if fw_info is not None: | ||
yield fw_info | ||
|
||
|
||
@hass_callback | ||
def async_register_firmware_info_provider( | ||
hass: HomeAssistant, domain: str, platform: HardwareFirmwareInfoModule | ||
) -> None: | ||
"""Register a firmware info provider.""" | ||
return hass.data[DATA_COMPONENT].register_firmware_info_provider(domain, platform) | ||
|
||
|
||
@hass_callback | ||
def async_register_firmware_info_callback( | ||
hass: HomeAssistant, device: str, callback: Callable[[FirmwareInfo], None] | ||
) -> CALLBACK_TYPE: | ||
"""Register a firmware info provider.""" | ||
return hass.data[DATA_COMPONENT].register_firmware_info_callback(device, callback) | ||
|
||
|
||
@hass_callback | ||
def async_notify_firmware_info( | ||
hass: HomeAssistant, domain: str, firmware_info: FirmwareInfo | ||
) -> Awaitable[None]: | ||
"""Notify the dispatcher of new firmware information.""" | ||
return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info) |
Oops, something went wrong.