mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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:
parent
cd67ddbe26
commit
69a004a2f6
@ -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)
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user