diff --git a/custom_components/sagemcom_fast/__init__.py b/custom_components/sagemcom_fast/__init__.py index 1aa60c7..a51d0d4 100644 --- a/custom_components/sagemcom_fast/__init__.py +++ b/custom_components/sagemcom_fast/__init__.py @@ -1,10 +1,17 @@ """The Sagemcom integration.""" import asyncio +from datetime import timedelta import logging from aiohttp.client_exceptions import ClientError from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_SOURCE, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, service @@ -18,7 +25,8 @@ UnauthorizedException, ) -from .const import CONF_ENCRYPTION_METHOD, DOMAIN +from .const import CONF_ENCRYPTION_METHOD, DEFAULT_SCAN_INTERVAL, DOMAIN +from .device_tracker import SagemcomDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -80,9 +88,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.exception(exception) return False + update_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + + coordinator = SagemcomDataUpdateCoordinator( + hass, + _LOGGER, + name="sagemcom_hosts", + client=client, + update_interval=timedelta(seconds=update_interval), + ) + + await coordinator.async_refresh() + hass.data[DOMAIN][entry.entry_id] = { - "client": client, - "devices": await client.get_hosts(only_active=True), + "coordinator": coordinator, + "update_listener": entry.add_update_listener(update_listener), } # Create gateway device in Home Assistant @@ -129,6 +149,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: + hass.data[DOMAIN][entry.entry_id]["update_listener"]() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Update when entry options update.""" + if entry.options[CONF_SCAN_INTERVAL]: + coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"] + coordinator.update_interval = timedelta( + seconds=entry.options[CONF_SCAN_INTERVAL] + ) + + await coordinator.async_refresh() diff --git a/custom_components/sagemcom_fast/config_flow.py b/custom_components/sagemcom_fast/config_flow.py index a79ecb0..dabddaa 100644 --- a/custom_components/sagemcom_fast/config_flow.py +++ b/custom_components/sagemcom_fast/config_flow.py @@ -4,6 +4,7 @@ from aiohttp import ClientError from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from sagemcom_api.client import SagemcomClient from sagemcom_api.enums import EncryptionMethod from sagemcom_api.exceptions import ( @@ -13,8 +14,8 @@ ) import voluptuous as vol -from .const import CONF_ENCRYPTION_METHOD -from .const import DOMAIN # pylint: disable=unused-import +from .const import CONF_ENCRYPTION_METHOD, DOMAIN +from .options_flow import OptionsFlow _LOGGER = logging.getLogger(__name__) @@ -77,3 +78,9 @@ async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow for this handler.""" + return OptionsFlow(config_entry) diff --git a/custom_components/sagemcom_fast/const.py b/custom_components/sagemcom_fast/const.py index 5524b56..dc5acd2 100644 --- a/custom_components/sagemcom_fast/const.py +++ b/custom_components/sagemcom_fast/const.py @@ -9,3 +9,6 @@ DEFAULT_TRACK_WIRED_CLIENTS = True ATTR_MANUFACTURER = "Sagemcom" + +MIN_SCAN_INTERVAL = 10 +DEFAULT_SCAN_INTERVAL = 10 diff --git a/custom_components/sagemcom_fast/device_tracker.py b/custom_components/sagemcom_fast/device_tracker.py index fd077c2..33a12f8 100644 --- a/custom_components/sagemcom_fast/device_tracker.py +++ b/custom_components/sagemcom_fast/device_tracker.py @@ -1,11 +1,21 @@ """Support for device tracking of client router.""" +from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any, Dict, Optional +import async_timeout from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) +from sagemcom_api.client import SagemcomClient +from sagemcom_api.models import Device from .const import DOMAIN @@ -15,42 +25,80 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up from config entry.""" - # TODO Handle status of disconnected devices - entities = [] - client = hass.data[DOMAIN][config_entry.entry_id]["client"] - - new_devices = await client.get_hosts(only_active=True) - - for device in new_devices: - entity = SagemcomScannerEntity(device, config_entry.entry_id) - entities.append(entity) - - async_add_entities(entities, update_before_add=True) - - -class SagemcomScannerEntity(ScannerEntity, RestoreEntity): + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + async_add_entities( + SagemcomScannerEntity(coordinator, idx, config_entry.entry_id) + for idx, device in coordinator.data.items() + ) + + +class SagemcomDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Sagemcom data.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + client: SagemcomClient, + update_interval: Optional[timedelta] = None, + ): + """Initialize update coordinator.""" + super().__init__( + hass, + logger, + name=name, + update_interval=update_interval, + ) + self.data = {} + self.hosts: Dict[str, Device] = {} + self._client = client + + async def _async_update_data(self) -> Dict[str, Device]: + """Update hosts data.""" + try: + async with async_timeout.timeout(10): + hosts = await self._client.get_hosts(only_active=True) + """Mark all device as non-active.""" + for idx, host in self.hosts.items(): + host.active = False + self.hosts[idx] = host + for host in hosts: + self.hosts[host.id] = host + return self.hosts + except Exception as exception: + raise UpdateFailed(f"Error communicating with API: {exception}") + + +class SagemcomScannerEntity(ScannerEntity, RestoreEntity, CoordinatorEntity): """Sagemcom router scanner entity.""" - def __init__(self, device, parent): + def __init__(self, coordinator, idx, parent): """Initialize the device.""" - self._device = device + super().__init__(coordinator) + self._idx = idx self._via_device = parent - super().__init__() + @property + def device(self): + """Return the device entity.""" + return self.coordinator.data[self._idx] @property def name(self) -> str: """Return the name of the device.""" return ( - self._device.name - or self._device.user_friendly_name - or self._device.mac_address + self.device.name + or self.device.user_friendly_name + or self.device.mac_address ) @property def unique_id(self) -> str: """Return a unique ID.""" - return self._device.id + return self.device.id @property def source_type(self) -> str: @@ -60,7 +108,7 @@ def source_type(self) -> str: @property def is_connected(self) -> bool: """Get whether the entity is connected.""" - return self._device.active or False + return self.device.active or False @property def device_info(self): @@ -74,21 +122,21 @@ def device_info(self): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the device.""" - attr = {"interface_type": self._device.interface_type} + attr = {"interface_type": self.device.interface_type} return attr @property def ip_address(self) -> str: """Return the primary ip address of the device.""" - return self._device.ip_address or None + return self.device.ip_address or None @property def mac_address(self) -> str: """Return the mac address of the device.""" - return self._device.phys_address + return self.device.phys_address @property def hostname(self) -> str: """Return hostname of the device.""" - return self._device.user_host_name or self._device.host_name + return self.device.user_host_name or self.device.host_name diff --git a/custom_components/sagemcom_fast/manifest.json b/custom_components/sagemcom_fast/manifest.json index ab8a25d..963e16c 100644 --- a/custom_components/sagemcom_fast/manifest.json +++ b/custom_components/sagemcom_fast/manifest.json @@ -1,6 +1,7 @@ { "domain": "sagemcom_fast", "name": "Sagemcom F@st", + "version": "0.2.0", "config_flow": true, "documentation": "https://github.com/imicknl/ha-sagemcom-fast", "requirements": [ diff --git a/custom_components/sagemcom_fast/options_flow.py b/custom_components/sagemcom_fast/options_flow.py new file mode 100644 index 0000000..94e24e1 --- /dev/null +++ b/custom_components/sagemcom_fast/options_flow.py @@ -0,0 +1,35 @@ +"""Options flow for Sagemcom integration.""" + +from homeassistant import config_entries +from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +from .const import DEFAULT_SCAN_INTERVAL, MIN_SCAN_INTERVAL + + +class OptionsFlow(config_entries.OptionsFlow): + """Handle a options flow for Sagemcom.""" + + def __init__(self, config_entry): + """Initialize Sagemcom options flow.""" + self._options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self._options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)) + } + ), + )