Support wired clients in Huawei LTE device tracker (#48987)
parent
cf16e651cf
commit
c825f88888
|
@ -64,6 +64,7 @@ from .const import (
|
|||
KEY_DEVICE_INFORMATION,
|
||||
KEY_DEVICE_SIGNAL,
|
||||
KEY_DIALUP_MOBILE_DATASWITCH,
|
||||
KEY_LAN_HOST_INFO,
|
||||
KEY_MONITORING_CHECK_NOTIFICATIONS,
|
||||
KEY_MONITORING_MONTH_STATISTICS,
|
||||
KEY_MONITORING_STATUS,
|
||||
|
@ -130,6 +131,7 @@ CONFIG_ENTRY_PLATFORMS = (
|
|||
class Router:
|
||||
"""Class for router state."""
|
||||
|
||||
config_entry: ConfigEntry = attr.ib()
|
||||
connection: Connection = attr.ib()
|
||||
url: str = attr.ib()
|
||||
mac: str = attr.ib()
|
||||
|
@ -261,6 +263,10 @@ class Router:
|
|||
self._get_data(KEY_NET_CURRENT_PLMN, self.client.net.current_plmn)
|
||||
self._get_data(KEY_NET_NET_MODE, self.client.net.net_mode)
|
||||
self._get_data(KEY_SMS_SMS_COUNT, self.client.sms.sms_count)
|
||||
self._get_data(KEY_LAN_HOST_INFO, self.client.lan.host_info)
|
||||
if self.data.get(KEY_LAN_HOST_INFO):
|
||||
# LAN host info includes everything in WLAN host list
|
||||
self.subscriptions.pop(KEY_WLAN_HOST_LIST, None)
|
||||
self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list)
|
||||
self._get_data(
|
||||
KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch
|
||||
|
@ -382,7 +388,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
|
|||
raise ConfigEntryNotReady from ex
|
||||
|
||||
# Set up router and store reference to it
|
||||
router = Router(connection, url, mac, signal_update)
|
||||
router = Router(config_entry, connection, url, mac, signal_update)
|
||||
hass.data[DOMAIN].routers[url] = router
|
||||
|
||||
# Do initial data update
|
||||
|
|
|
@ -33,9 +33,11 @@ from homeassistant.data_entry_flow import FlowResultDict
|
|||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CONF_TRACK_WIRED_CLIENTS,
|
||||
CONNECTION_TIMEOUT,
|
||||
DEFAULT_DEVICE_NAME,
|
||||
DEFAULT_NOTIFY_SERVICE_NAME,
|
||||
DEFAULT_TRACK_WIRED_CLIENTS,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
@ -284,6 +286,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
self.config_entry.options.get(CONF_RECIPIENT, [])
|
||||
),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_TRACK_WIRED_CLIENTS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
|
||||
),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
DOMAIN = "huawei_lte"
|
||||
|
||||
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
|
||||
|
||||
DEFAULT_DEVICE_NAME = "LTE"
|
||||
DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN
|
||||
DEFAULT_TRACK_WIRED_CLIENTS = True
|
||||
|
||||
UPDATE_SIGNAL = f"{DOMAIN}_update"
|
||||
|
||||
|
@ -26,6 +29,7 @@ KEY_DEVICE_BASIC_INFORMATION = "device_basic_information"
|
|||
KEY_DEVICE_INFORMATION = "device_information"
|
||||
KEY_DEVICE_SIGNAL = "device_signal"
|
||||
KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch"
|
||||
KEY_LAN_HOST_INFO = "lan_host_info"
|
||||
KEY_MONITORING_CHECK_NOTIFICATIONS = "monitoring_check_notifications"
|
||||
KEY_MONITORING_MONTH_STATISTICS = "monitoring_month_statistics"
|
||||
KEY_MONITORING_STATUS = "monitoring_status"
|
||||
|
@ -42,7 +46,10 @@ BINARY_SENSOR_KEYS = {
|
|||
KEY_WLAN_WIFI_FEATURE_SWITCH,
|
||||
}
|
||||
|
||||
DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST}
|
||||
DEVICE_TRACKER_KEYS = {
|
||||
KEY_LAN_HOST_INFO,
|
||||
KEY_WLAN_HOST_LIST,
|
||||
}
|
||||
|
||||
SENSOR_KEYS = {
|
||||
KEY_DEVICE_INFORMATION,
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Callable, cast
|
||||
from typing import Any, Callable, Dict, List, cast
|
||||
|
||||
import attr
|
||||
from stringcase import snakecase
|
||||
|
@ -21,13 +21,35 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from . import HuaweiLteBaseEntity
|
||||
from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL
|
||||
from . import HuaweiLteBaseEntity, Router
|
||||
from .const import (
|
||||
CONF_TRACK_WIRED_CLIENTS,
|
||||
DEFAULT_TRACK_WIRED_CLIENTS,
|
||||
DOMAIN,
|
||||
KEY_LAN_HOST_INFO,
|
||||
KEY_WLAN_HOST_LIST,
|
||||
UPDATE_SIGNAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan"
|
||||
|
||||
_HostType = Dict[str, Any]
|
||||
|
||||
|
||||
def _get_hosts(
|
||||
router: Router, ignore_subscriptions: bool = False
|
||||
) -> list[_HostType] | None:
|
||||
for key in KEY_LAN_HOST_INFO, KEY_WLAN_HOST_LIST:
|
||||
if not ignore_subscriptions and key not in router.subscriptions:
|
||||
continue
|
||||
try:
|
||||
return cast(List[_HostType], router.data[key]["Hosts"]["Host"])
|
||||
except KeyError:
|
||||
_LOGGER.debug("%s[%s][%s] not in data", key, "Hosts", "Host")
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
|
@ -40,28 +62,36 @@ async def async_setup_entry(
|
|||
# us, i.e. if wlan host list is supported. Only set up a subscription and proceed
|
||||
# with adding and tracking entities if it is.
|
||||
router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]]
|
||||
try:
|
||||
_ = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"]
|
||||
except KeyError:
|
||||
_LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host")
|
||||
if (hosts := _get_hosts(router, True)) is None:
|
||||
return
|
||||
|
||||
# Initialize already tracked entities
|
||||
tracked: set[str] = set()
|
||||
registry = await entity_registry.async_get_registry(hass)
|
||||
known_entities: list[Entity] = []
|
||||
track_wired_clients = router.config_entry.options.get(
|
||||
CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
|
||||
)
|
||||
for entity in registry.entities.values():
|
||||
if (
|
||||
entity.domain == DEVICE_TRACKER_DOMAIN
|
||||
and entity.config_entry_id == config_entry.entry_id
|
||||
):
|
||||
tracked.add(entity.unique_id)
|
||||
known_entities.append(
|
||||
HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2])
|
||||
)
|
||||
mac = entity.unique_id.partition("-")[2]
|
||||
# Do not add known wired clients if not tracking them (any more)
|
||||
skip = False
|
||||
if not track_wired_clients:
|
||||
for host in hosts:
|
||||
if host.get("MacAddress") == mac:
|
||||
skip = not _is_wireless(host)
|
||||
break
|
||||
if not skip:
|
||||
tracked.add(entity.unique_id)
|
||||
known_entities.append(HuaweiLteScannerEntity(router, mac))
|
||||
async_add_entities(known_entities, True)
|
||||
|
||||
# Tell parent router to poll hosts list to gather new devices
|
||||
router.subscriptions[KEY_LAN_HOST_INFO].add(_DEVICE_SCAN)
|
||||
router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN)
|
||||
|
||||
async def _async_maybe_add_new_entities(url: str) -> None:
|
||||
|
@ -79,6 +109,24 @@ async def async_setup_entry(
|
|||
async_add_new_entities(hass, router.url, async_add_entities, tracked)
|
||||
|
||||
|
||||
def _is_wireless(host: _HostType) -> bool:
|
||||
# LAN host info entries have an "InterfaceType" property, "Ethernet" / "Wireless".
|
||||
# WLAN host list ones don't, but they're expected to be all wireless.
|
||||
return cast(str, host.get("InterfaceType", "Wireless")) != "Ethernet"
|
||||
|
||||
|
||||
def _is_connected(host: _HostType | None) -> bool:
|
||||
# LAN host info entries have an "Active" property, "1" or "0".
|
||||
# WLAN host list ones don't, but that call appears to return active hosts only.
|
||||
return False if host is None else cast(str, host.get("Active", "1")) != "0"
|
||||
|
||||
|
||||
def _is_us(host: _HostType) -> bool:
|
||||
"""Try to determine if the host entry is us, the HA instance."""
|
||||
# LAN host info entries have an "isLocalDevice" property, "1" / "0"; WLAN host list ones don't.
|
||||
return cast(str, host.get("isLocalDevice", "0")) == "1"
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_new_entities(
|
||||
hass: HomeAssistantType,
|
||||
|
@ -88,14 +136,23 @@ def async_add_new_entities(
|
|||
) -> None:
|
||||
"""Add new entities that are not already being tracked."""
|
||||
router = hass.data[DOMAIN].routers[router_url]
|
||||
try:
|
||||
hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"]
|
||||
except KeyError:
|
||||
_LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host")
|
||||
hosts = _get_hosts(router)
|
||||
if not hosts:
|
||||
return
|
||||
|
||||
track_wired_clients = router.config_entry.options.get(
|
||||
CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
|
||||
)
|
||||
|
||||
new_entities: list[Entity] = []
|
||||
for host in (x for x in hosts if x.get("MacAddress")):
|
||||
for host in (
|
||||
x
|
||||
for x in hosts
|
||||
if not _is_us(x)
|
||||
and _is_connected(x)
|
||||
and x.get("MacAddress")
|
||||
and (track_wired_clients or _is_wireless(x))
|
||||
):
|
||||
entity = HuaweiLteScannerEntity(router, host["MacAddress"])
|
||||
if entity.unique_id in tracked:
|
||||
continue
|
||||
|
@ -124,29 +181,41 @@ def _better_snakecase(text: str) -> str:
|
|||
class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity):
|
||||
"""Huawei LTE router scanner entity."""
|
||||
|
||||
mac: str = attr.ib()
|
||||
_mac_address: str = attr.ib()
|
||||
|
||||
_ip_address: str | None = attr.ib(init=False, default=None)
|
||||
_is_connected: bool = attr.ib(init=False, default=False)
|
||||
_hostname: str | None = attr.ib(init=False, default=None)
|
||||
_extra_state_attributes: dict[str, Any] = attr.ib(init=False, factory=dict)
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
"""Initialize internal state."""
|
||||
self._extra_state_attributes["mac_address"] = self.mac
|
||||
|
||||
@property
|
||||
def _entity_name(self) -> str:
|
||||
return self._hostname or self.mac
|
||||
return self.hostname or self.mac_address
|
||||
|
||||
@property
|
||||
def _device_unique_id(self) -> str:
|
||||
return self.mac
|
||||
return self.mac_address
|
||||
|
||||
@property
|
||||
def source_type(self) -> str:
|
||||
"""Return SOURCE_TYPE_ROUTER."""
|
||||
return SOURCE_TYPE_ROUTER
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str | None:
|
||||
"""Return the primary ip address of the device."""
|
||||
return self._ip_address
|
||||
|
||||
@property
|
||||
def mac_address(self) -> str:
|
||||
"""Return the mac address of the device."""
|
||||
return self._mac_address
|
||||
|
||||
@property
|
||||
def hostname(self) -> str | None:
|
||||
"""Return hostname of the device."""
|
||||
return self._hostname
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Get whether the entity is connected."""
|
||||
|
@ -159,11 +228,27 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity):
|
|||
|
||||
async def async_update(self) -> None:
|
||||
"""Update state."""
|
||||
hosts = self.router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"]
|
||||
host = next((x for x in hosts if x.get("MacAddress") == self.mac), None)
|
||||
self._is_connected = host is not None
|
||||
hosts = _get_hosts(self.router)
|
||||
if hosts is None:
|
||||
self._available = False
|
||||
return
|
||||
self._available = True
|
||||
host = next(
|
||||
(x for x in hosts if x.get("MacAddress") == self._mac_address), None
|
||||
)
|
||||
self._is_connected = _is_connected(host)
|
||||
if host is not None:
|
||||
# IpAddress can contain multiple semicolon separated addresses.
|
||||
# Pick one for model sanity; e.g. the dhcp component to which it is fed, parses and expects to see just one.
|
||||
self._ip_address = (host.get("IpAddress") or "").split(";", 2)[0] or None
|
||||
self._hostname = host.get("HostName")
|
||||
self._extra_state_attributes = {
|
||||
_better_snakecase(k): v for k, v in host.items() if k != "HostName"
|
||||
_better_snakecase(k): v
|
||||
for k, v in host.items()
|
||||
if k
|
||||
in {
|
||||
"AddressSource",
|
||||
"AssociatedSsid",
|
||||
"InterfaceType",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@
|
|||
"init": {
|
||||
"data": {
|
||||
"name": "Notification service name (change requires restart)",
|
||||
"recipient": "SMS notification recipients"
|
||||
"recipient": "SMS notification recipients",
|
||||
"track_wired_clients": "Track wired network clients"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue