Netgear coordinator (#65255)

* implement coordinator

* fix styling

* fix async_uload_entry

* use async_config_entry_first_refresh

* use const

* fix black

* use KEY_ROUTER

* review comments

* fix black

* ensure the coordinator keeps updating

* fix flake8

* rework setup of entities using coordinator

* styling

* check for failed get_info call

* fix

* fix setup of entities

* simplify

* do not set unique_id and device_info on scanner entity

* Update homeassistant/components/netgear/sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/netgear/device_tracker.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/netgear/router.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* use entry_id instead of unique_id

* unused import

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
starkillerOG 2022-02-03 12:28:04 +01:00 committed by GitHub
parent cd67ddbe26
commit 69a004a2f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 149 additions and 121 deletions

View File

@ -1,4 +1,5 @@
"""Support for Netgear routers."""
from datetime import timedelta
import logging
from homeassistant.config_entries import ConfigEntry
@ -6,19 +7,23 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, PLATFORMS
from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER, PLATFORMS
from .errors import CannotLoginException
from .router import NetgearRouter
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Netgear component."""
router = NetgearRouter(hass, entry)
try:
await router.async_setup()
if not await router.async_setup():
raise ConfigEntryNotReady
except CannotLoginException as ex:
raise ConfigEntryNotReady from ex
@ -37,7 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = router
entry.async_on_unload(entry.add_update_listener(update_listener))
@ -52,6 +56,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
configuration_url=f"http://{entry.data[CONF_HOST]}/",
)
async def async_update_data() -> bool:
"""Fetch data from the router."""
data = await router.async_update_device_trackers()
return data
# Create update coordinator
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=router.device_name,
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
KEY_ROUTER: router,
KEY_COORDINATOR: coordinator,
}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@ -62,7 +87,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.unique_id)
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)

View File

@ -5,10 +5,13 @@ from homeassistant.const import Platform
DOMAIN = "netgear"
PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR]
CONF_CONSIDER_HOME = "consider_home"
KEY_ROUTER = "router"
KEY_COORDINATOR = "coordinator"
PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR]
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
DEFAULT_NAME = "Netgear router"

View File

@ -8,9 +8,10 @@ from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DEVICE_ICONS
from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry
from .const import DEVICE_ICONS, DOMAIN, KEY_COORDINATOR, KEY_ROUTER
from .router import NetgearBaseEntity, NetgearRouter
_LOGGER = logging.getLogger(__name__)
@ -19,19 +20,42 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up device tracker for Netgear component."""
router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER]
coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR]
tracked = set()
def generate_classes(router: NetgearRouter, device: dict):
return [NetgearScannerEntity(router, device)]
@callback
def new_device_callback() -> None:
"""Add new devices if needed."""
if not coordinator.data:
return
async_setup_netgear_entry(hass, entry, async_add_entities, generate_classes)
new_entities = []
for mac, device in router.devices.items():
if mac in tracked:
continue
new_entities.append(NetgearScannerEntity(coordinator, router, device))
tracked.add(mac)
if new_entities:
async_add_entities(new_entities)
entry.async_on_unload(coordinator.async_add_listener(new_device_callback))
coordinator.data = True
new_device_callback()
class NetgearScannerEntity(NetgearDeviceEntity, ScannerEntity):
class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity):
"""Representation of a device connected to a Netgear router."""
def __init__(self, router: NetgearRouter, device: dict) -> None:
def __init__(
self, coordinator: DataUpdateCoordinator, router: NetgearRouter, device: dict
) -> None:
"""Initialize a Netgear device."""
super().__init__(router, device)
super().__init__(coordinator, router, device)
self._hostname = self.get_hostname()
self._icon = DEVICE_ICONS.get(device["device_type"], "mdi:help-network")
@ -49,8 +73,6 @@ class NetgearScannerEntity(NetgearDeviceEntity, ScannerEntity):
self._active = self._device["active"]
self._icon = DEVICE_ICONS.get(self._device["device_type"], "mdi:help-network")
self.async_write_ha_state()
@property
def is_connected(self):
"""Return true if the device is connected to the router."""

View File

@ -2,7 +2,6 @@
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Callable
from datetime import timedelta
import logging
@ -19,13 +18,11 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import dt as dt_util
from .const import (
@ -37,8 +34,6 @@ from .const import (
)
from .errors import CannotLoginException
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
@ -58,47 +53,6 @@ def get_api(
return api
@callback
def async_setup_netgear_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
entity_class_generator: Callable[[NetgearRouter, dict], list],
) -> None:
"""Set up device tracker for Netgear component."""
router = hass.data[DOMAIN][entry.unique_id]
tracked = set()
@callback
def _async_router_updated():
"""Update the values of the router."""
async_add_new_entities(
router, async_add_entities, tracked, entity_class_generator
)
entry.async_on_unload(
async_dispatcher_connect(hass, router.signal_device_new, _async_router_updated)
)
_async_router_updated()
@callback
def async_add_new_entities(router, async_add_entities, tracked, entity_class_generator):
"""Add new tracker entities from the router."""
new_tracked = []
for mac, device in router.devices.items():
if mac in tracked:
continue
new_tracked.extend(entity_class_generator(router, device))
tracked.add(mac)
if new_tracked:
async_add_entities(new_tracked, True)
class NetgearRouter:
"""Representation of a Netgear router."""
@ -141,6 +95,9 @@ class NetgearRouter:
)
self._info = self._api.get_info()
if self._info is None:
return False
self.device_name = self._info.get("DeviceName", DEFAULT_NAME)
self.model = self._info.get("ModelName")
self.firmware_version = self._info.get("Firmwareversion")
@ -157,9 +114,12 @@ class NetgearRouter:
)
self.method_version = 1
async def async_setup(self) -> None:
return True
async def async_setup(self) -> bool:
"""Set up a Netgear router."""
await self.hass.async_add_executor_job(self._setup)
if not await self.hass.async_add_executor_job(self._setup):
return False
# set already known devices to away instead of unavailable
device_registry = dr.async_get(self.hass)
@ -184,14 +144,7 @@ class NetgearRouter:
"conn_ap_mac": None,
}
await self.async_update_device_trackers()
self.entry.async_on_unload(
async_track_time_interval(
self.hass, self.async_update_device_trackers, SCAN_INTERVAL
)
)
async_dispatcher_send(self.hass, self.signal_device_new)
return True
async def async_get_attached_devices(self) -> list:
"""Get the devices connected to the router."""
@ -228,21 +181,10 @@ class NetgearRouter:
for device in self.devices.values():
device["active"] = now - device["last_seen"] <= self._consider_home
async_dispatcher_send(self.hass, self.signal_device_update)
if new_device:
_LOGGER.debug("Netgear tracker: new device found")
async_dispatcher_send(self.hass, self.signal_device_new)
@property
def signal_device_new(self) -> str:
"""Event specific per Netgear entry to signal new device."""
return f"{DOMAIN}-{self._host}-device-new"
@property
def signal_device_update(self) -> str:
"""Event specific per Netgear entry to signal updates in devices."""
return f"{DOMAIN}-{self._host}-device-update"
return new_device
@property
def port(self) -> int:
@ -255,17 +197,19 @@ class NetgearRouter:
return self._api.ssl
class NetgearDeviceEntity(Entity):
class NetgearBaseEntity(CoordinatorEntity):
"""Base class for a device connected to a Netgear router."""
def __init__(self, router: NetgearRouter, device: dict) -> None:
def __init__(
self, coordinator: DataUpdateCoordinator, router: NetgearRouter, device: dict
) -> None:
"""Initialize a Netgear device."""
super().__init__(coordinator)
self._router = router
self._device = device
self._mac = device["mac"]
self._name = self.get_device_name()
self._device_name = self._name
self._unique_id = self._mac
self._active = device["active"]
def get_device_name(self):
@ -281,16 +225,33 @@ class NetgearDeviceEntity(Entity):
def async_update_device(self) -> None:
"""Update the Netgear device."""
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_update_device()
super()._handle_coordinator_update()
@property
def name(self) -> str:
"""Return the name."""
return self._name
class NetgearDeviceEntity(NetgearBaseEntity):
"""Base class for a device connected to a Netgear router."""
def __init__(
self, coordinator: DataUpdateCoordinator, router: NetgearRouter, device: dict
) -> None:
"""Initialize a Netgear device."""
super().__init__(coordinator, router, device)
self._unique_id = self._mac
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
@ -300,18 +261,3 @@ class NetgearDeviceEntity(Entity):
default_model=self._device["device_model"],
via_device=(DOMAIN, self._router.unique_id),
)
@property
def should_poll(self) -> bool:
"""No polling needed."""
return False
async def async_added_to_hass(self):
"""Register state update callback."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self._router.signal_device_update,
self.async_update_device,
)
)

View File

@ -9,8 +9,10 @@ from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry
from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER
from .router import NetgearDeviceEntity, NetgearRouter
SENSOR_TYPES = {
"type": SensorEntityDescription(
@ -48,15 +50,41 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up device tracker for Netgear component."""
router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER]
coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR]
tracked = set()
def generate_sensor_classes(router: NetgearRouter, device: dict):
sensors = ["type", "link_rate", "signal"]
if router.method_version == 2:
sensors.extend(["ssid", "conn_ap_mac"])
return [NetgearSensorEntity(router, device, attribute) for attribute in sensors]
@callback
def new_device_callback() -> None:
"""Add new devices if needed."""
if not coordinator.data:
return
async_setup_netgear_entry(hass, entry, async_add_entities, generate_sensor_classes)
new_entities = []
for mac, device in router.devices.items():
if mac in tracked:
continue
new_entities.extend(
[
NetgearSensorEntity(coordinator, router, device, attribute)
for attribute in sensors
]
)
tracked.add(mac)
if new_entities:
async_add_entities(new_entities)
entry.async_on_unload(coordinator.async_add_listener(new_device_callback))
coordinator.data = True
new_device_callback()
class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity):
@ -64,9 +92,15 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity):
_attr_entity_registry_enabled_default = False
def __init__(self, router: NetgearRouter, device: dict, attribute: str) -> None:
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: NetgearRouter,
device: dict,
attribute: str,
) -> None:
"""Initialize a Netgear device."""
super().__init__(router, device)
super().__init__(coordinator, router, device)
self._attribute = attribute
self.entity_description = SENSOR_TYPES[self._attribute]
self._name = f"{self.get_device_name()} {self.entity_description.name}"
@ -85,5 +119,3 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity):
self._active = self._device["active"]
if self._device.get(self._attribute) is not None:
self._state = self._device[self._attribute]
self.async_write_ha_state()