Skip to content

Commit 7151549

Browse files
authored
Device tags added (#9)
1 parent bcc7778 commit 7151549

File tree

8 files changed

+185
-23
lines changed

8 files changed

+185
-23
lines changed

README.md

+84-9
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
Home Assistant custom component for control [Huawei WiFi Mesh 3](https://consumer.huawei.com/en/routers/wifi-mesh3/) routers over LAN.
44

5-
**0.7.3**
5+
**0.7.4**
66

7+
- tagging connected devices
78
- sensors for the number of connected devices (total and for each individual router)
89
- enable/disable NFC on each router separately
910
- enable/disable TWT (reduce power consumption of Wi-Fi 6 devices in sleep mode)
@@ -38,6 +39,7 @@ Each tracked device exposes the following attributes:
3839
| `rssi` | Signal strength for wireless connections | Yes |
3940
| `is_guest` | Is the device connected to the guest network | Yes |
4041
| `is_hilink` | Is the device connected via HiLink (usually other routers) | Yes |
42+
| `tags` | List of [tags](#device-tags) that marked the device | No |
4143
| `friendly_name` | Device name provided by the router | No |
4244

4345
Tracked device names, including routers, can be changed in [your mesh control interface](http://192.168.3.1/html/index.html#/devicecontrol), after which the component will update them in Home Assistant
@@ -71,11 +73,84 @@ _Note: when additional routers are disconnected from the network, their personal
7173

7274
Each sensor exposes the following attributes:
7375

74-
| Attribute | Description |
75-
|--------------------|--------------------------------------------------|
76-
| `guest_clients` | Number of devices connected to the guest network |
77-
| `hilink_clients` | Number of devices connected via HiLink |
78-
| `wireless_clients` | Number of devices connected wirelessly |
79-
| `lan_clients` | Number of devices connected by cable |
80-
| `wifi_2_4_clients` | Number of devices connected to Wi-Fi 2.4 GHz |
81-
| `wifi_5_clients` | Number of devices connected to Wi-Fi 5 GHz |
76+
| Attribute | Description |
77+
|------------------------------|--------------------------------------------------------------|
78+
| `guest_clients` | Number of devices connected to the guest network |
79+
| `hilink_clients` | Number of devices connected via HiLink |
80+
| `wireless_clients` | Number of devices connected wirelessly |
81+
| `lan_clients` | Number of devices connected by cable |
82+
| `wifi_2_4_clients` | Number of devices connected to Wi-Fi 2.4 GHz |
83+
| `wifi_5_clients` | Number of devices connected to Wi-Fi 5 GHz |
84+
| `tagged_<tag_name>_clients` | Number of connected devices with a specific [tag](#device-tags) `<tag_name>` |
85+
86+
## Customization
87+
88+
### Device tags
89+
90+
The component allows you to attach one or more tags to each client device in order to be able to use in automation the number of devices marked with a tag, connected to a specific router, or to the entire mesh network.
91+
92+
The component will attempt to load the device tag-to-MAC mapping from the file located at `<home assistant config folder>/.storage/huawei_mesh_<long_config_id>_tags`. If the file does not exist, then the component will create it with a usage example:
93+
94+
```
95+
{
96+
"version": 1,
97+
"minor_version": 1,
98+
"key": "huawei_mesh_<long_config_id>_tags",
99+
"data": {
100+
"homeowners": [
101+
"place_mac_addresses_here"
102+
],
103+
"visitors": [
104+
"place_mac_addresses_here"
105+
]
106+
}
107+
}
108+
```
109+
110+
_Note: unfortunately, editing the list of tags and devices associated with them is currently available only through editing this file._
111+
112+
Each tag can have multiple devices associated with it. Each device can be associated with multiple tags.
113+
114+
Example:
115+
```
116+
{
117+
"version": 1,
118+
"minor_version": 1,
119+
"key": "huawei_mesh_<long_config_id>_tags",
120+
"data": {
121+
"my_awesome_tag": [
122+
"00:11:22:33:44:55",
123+
"A0:B1:C2:D3:E4:F5",
124+
"F5:E4:D3:C2:B1:A0"
125+
],
126+
"another_tag": [
127+
"00:11:22:33:44:55",
128+
"A9:B8:C7:D6:E5:F4"
129+
],
130+
"third_tag": [
131+
"99:88:77:66:55:44"
132+
]
133+
}
134+
}
135+
```
136+
137+
**Usage example:**
138+
139+
| Tag name | Tagged Devices |
140+
|--------------|-----------------------------------|
141+
| homeowners | Michael's phone, Michael's laptop |
142+
| visitors | Victoria's phone, Eugene's phone |
143+
144+
145+
- Michael's phone is connected to the "Garage" router
146+
- Michael's laptop is connected to the "Living room" router
147+
- Victoria's phone is connected to the "Living room" router
148+
- Eugene's phone is connected to the "primary" router
149+
150+
In this scenario, the sensors for the number of connected devices will provide the following attributes:
151+
152+
| Sensor | Attributes and values |
153+
|-----------------------------------------------|-----------------------|
154+
| `sensor.huawei_mesh_3_clients_garage` | `guest_clients`: 0 <br/> `hilink_clients`: 0<br/>`wireless_clients`: 1<br />`lan_clients`: 0<br />`wifi_2_4_clients`: 0<br />`wifi_5_clients`: 1<br />`tagged_homeowners_clients`: 1 _// Michael's phone_<br />`tagged_visitors_clients`: 0 |
155+
| `sensor.huawei_mesh_3_clients_living_room` | `guest_clients`: 0 <br/> `hilink_clients`: 0<br/>`wireless_clients`: 2<br />`lan_clients`: 0<br />`wifi_2_4_clients`: 0<br />`wifi_5_clients`: 2<br />`tagged_homeowners_clients`: 1 _// Michael's laptop_<br />`tagged_visitors_clients`: 1 _// Victoria's phone_ |
156+
| `sensor.huawei_mesh_3_clients_primary_router` | `guest_clients`: 0 <br/> `hilink_clients`: 2<br/>`wireless_clients`: 3<br />`lan_clients`: 0<br />`wifi_2_4_clients`: 0<br />`wifi_5_clients`: 3<br />`tagged_homeowners_clients`: 0<br />`tagged_visitors_clients`: 1 _// Eugene's phone_ |

custom_components/huawei_mesh_router/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from homeassistant.config_entries import ConfigEntry
55
from homeassistant.core import HomeAssistant
66
from homeassistant.helpers import config_validation
7+
from homeassistant.helpers.storage import Store
78

89
from .const import (
10+
STORAGE_VERSION,
911
DOMAIN,
1012
PLATFORMS,
1113
ATTR_MANUFACTURER
@@ -31,7 +33,8 @@ async def async_setup(hass, _config):
3133
# ---------------------------
3234
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
3335
"""Set up Huawei Router as config entry."""
34-
coordinator = HuaweiControllerDataUpdateCoordinator(hass, config_entry)
36+
store: Store = Store(hass, STORAGE_VERSION, f"huawei_mesh_{config_entry.entry_id}_tags")
37+
coordinator = HuaweiControllerDataUpdateCoordinator(hass, config_entry, store)
3538
await coordinator.async_config_entry_first_refresh()
3639

3740
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))

custom_components/huawei_mesh_router/connected_device.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict
1+
from typing import Any, Dict, Iterable, Tuple
22
from homeassistant.backports.enum import StrEnum
33

44
from .const import VENDOR_CLASS_ID_ROUTER
@@ -23,21 +23,25 @@ def __init__(self,
2323
host_name: str,
2424
mac: str,
2525
is_active: bool,
26+
tags: list[str],
2627
**kwargs: Dict) -> None:
2728
self._name: str = name
2829
self._host_name: str = host_name
2930
self._mac: str = mac
3031
self._is_active: bool = is_active
32+
self._tags: list[str] = tags
3133
self._data: Dict = kwargs or {}
3234

3335
def update_device_data(self,
3436
name: str,
3537
host_name: str,
3638
is_active: bool,
39+
tags: list[str],
3740
**kwargs: Dict):
3841
self._name: str = name
3942
self._host_name: str = host_name
4043
self._is_active: bool = is_active
44+
self._tags: list[str] = tags
4145
self._data: Dict = kwargs or {}
4246

4347
def __str__(self) -> str:
@@ -97,6 +101,13 @@ def is_router(self) -> bool:
97101
return self.is_hilink and self._data.get("vendor_class_id") == VENDOR_CLASS_ID_ROUTER
98102

99103
@property
100-
def all_attrs(self) -> Dict:
104+
def tags(self) -> list[str]:
105+
"""Return device tags list."""
106+
return self._tags
107+
108+
@property
109+
def all_attrs(self) -> Iterable[Tuple[str, Any]]:
101110
"""Return dictionary with additional attributes."""
102-
return self._data
111+
for key, value in self._data.items():
112+
yield key, value
113+
yield "tags", self._tags

custom_components/huawei_mesh_router/const.py

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
DOMAIN = "huawei_mesh_router"
44

5+
STORAGE_VERSION = 1
6+
57
DEFAULT_HOST = "192.168.3.1"
68
DEFAULT_USER = "admin"
79
DEFAULT_PORT = 80

custom_components/huawei_mesh_router/device_tracker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def unique_id(self) -> str:
107107
@property
108108
def extra_state_attributes(self) -> dict[str, Any]:
109109
"""Return the device state attributes."""
110-
return {k: v for k, v in self.device.all_attrs.items() if k not in FILTER_ATTRS}
110+
return {k: v for k, v in self.device.all_attrs if k not in FILTER_ATTRS}
111111

112112
@property
113113
def entity_registry_enabled_default(self) -> bool:
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"domain": "huawei_mesh_router",
33
"name": "Huawei Mesh Router",
4+
"integration_type": "device",
45
"documentation": "https://github.com/vmakeev/huawei_mesh_router",
6+
"issue_tracker": "https://github.com/vmakeev/huawei_mesh_router/issues",
57
"codeowners": ["@vmakeev"],
68
"iot_class": "local_polling",
7-
"version": "0.7.3",
9+
"version": "0.7.4",
810
"config_flow": true
911
}

custom_components/huawei_mesh_router/sensor.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from homeassistant.config_entries import ConfigEntry
77
from homeassistant.const import Platform
88
from homeassistant.core import callback, HomeAssistant
9-
from homeassistant.helpers import entity_registry
109
from homeassistant.helpers.entity import EntityCategory
1110
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1211
from homeassistant.helpers.entity_registry import EntityRegistry
@@ -160,6 +159,10 @@ def _handle_coordinator_update(self) -> None:
160159
wifi_2_4_clients: int = 0
161160
wifi_5_clients: int = 0
162161

162+
tagged_devices: dict[str, int] = {}
163+
for tag in self.coordinator.tags_map.get_all_tags():
164+
tagged_devices[tag] = 0
165+
163166
for device in self.coordinator.connected_devices.values():
164167

165168
if not self._devices_predicate(device):
@@ -181,6 +184,12 @@ def _handle_coordinator_update(self) -> None:
181184
wireless_clients += 1
182185
wifi_5_clients += 1
183186

187+
for tag in device.tags:
188+
if tag in tagged_devices:
189+
tagged_devices[tag] += 1
190+
else:
191+
tagged_devices[tag] = 1
192+
184193
self._actual_value = total_clients
185194

186195
self._attrs["guest_clients"] = guest_clients
@@ -190,6 +199,9 @@ def _handle_coordinator_update(self) -> None:
190199
self._attrs["wifi_2_4_clients"] = wifi_2_4_clients
191200
self._attrs["wifi_5_clients"] = wifi_5_clients
192201

202+
for tag, count in tagged_devices.items():
203+
self._attrs[f"tagged_{tag}_clients"] = count
204+
193205
super()._handle_coordinator_update()
194206

195207
@property

custom_components/huawei_mesh_router/update_coordinator.py

+64-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
"""Huawei Controller for Huawei Router."""
22
from __future__ import annotations
33

4-
import asyncio
54
import logging
65
from datetime import timedelta
7-
from typing import Any, cast, Callable, Iterator, List
6+
from typing import Any, cast, Callable, Iterable, List
87

98
from aiohttp import ClientResponse
109

@@ -13,6 +12,7 @@
1312
from homeassistant.config_entries import ConfigEntry
1413
from homeassistant.helpers import entity_registry
1514
from homeassistant.helpers.entity_registry import EntityRegistry
15+
from homeassistant.helpers.storage import Store
1616
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1717
from .connected_device import ConnectedDevice
1818
from .router_info import RouterInfo
@@ -107,13 +107,62 @@ def watch_for_changes(
107107
on_router_removed(er, device_id, unavailable_router)
108108

109109

110+
# ---------------------------
111+
# TagsMap
112+
# ---------------------------
113+
class TagsMap:
114+
115+
def __init__(self, tags_map_storage: Store):
116+
self._storage: Store = tags_map_storage
117+
self._mac_to_tags: dict[str, list[str]] = {}
118+
self._tag_to_macs: dict[str, list[str]] = {}
119+
self._is_loaded: bool = False
120+
121+
@property
122+
def is_loaded(self) -> bool:
123+
return self._is_loaded
124+
125+
async def load(self):
126+
_LOGGER.debug("Stored tags loading started")
127+
128+
self._mac_to_tags.clear()
129+
self._tag_to_macs.clear()
130+
131+
self._tag_to_macs = await self._storage.async_load()
132+
if not self._tag_to_macs:
133+
_LOGGER.debug("No stored tags found, creating sample")
134+
default_tags = {"homeowners": ["place_mac_addresses_here"], "visitors": ["place_mac_addresses_here"]}
135+
await self._storage.async_save(default_tags)
136+
self._tag_to_macs = default_tags
137+
138+
for tag, devices_macs in self._tag_to_macs.items():
139+
for device_mac in devices_macs:
140+
if device_mac not in self._mac_to_tags:
141+
self._mac_to_tags[device_mac] = []
142+
self._mac_to_tags[device_mac].append(tag)
143+
144+
self._is_loaded = True
145+
146+
_LOGGER.debug("Stored tags loading finished")
147+
148+
def get_tags(self, mac_address: str) -> list[str]:
149+
return self._mac_to_tags.get(mac_address, [])
150+
151+
def get_all_tags(self) -> Iterable[str]:
152+
return self._tag_to_macs.keys()
153+
154+
def get_devices(self, tag: str) -> list[str]:
155+
return self._tag_to_macs.get(tag, [])
156+
157+
110158
# ---------------------------
111159
# HuaweiControllerDataUpdateCoordinator
112160
# ---------------------------
113161
class HuaweiControllerDataUpdateCoordinator(DataUpdateCoordinator):
114162

115-
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
163+
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, tags_map_storage: Store) -> None:
116164
"""Initialize HuaweiController."""
165+
self._tags_map: TagsMap = TagsMap(tags_map_storage)
117166
self._router_info: RouterInfo | None = None
118167
self._switch_states: dict[str, bool] = {}
119168
self._connected_devices: dict[str, ConnectedDevice] = {}
@@ -188,6 +237,10 @@ def device_info(self) -> DeviceInfo:
188237
sw_version=self.router_info.software_version,
189238
)
190239

240+
@property
241+
def tags_map(self) -> TagsMap:
242+
return self._tags_map
243+
191244
def _safe_disconnect(self, api: HuaweiApi) -> None:
192245
"""Disconnect from API."""
193246
try:
@@ -289,7 +342,7 @@ async def _update_connected_devices(self) -> None:
289342

290343
"""recursively search all HiLink routers with connected devices"""
291344

292-
def get_mesh_routers(devices: List[dict[str, Any]]) -> Iterator[dict[str, Any]]:
345+
def get_mesh_routers(devices: List[dict[str, Any]]) -> Iterable[dict[str, Any]]:
293346
for candidate in devices:
294347
if candidate.get('HiLinkType') == "Device":
295348
yield candidate
@@ -316,16 +369,20 @@ def get_mesh_routers(devices: List[dict[str, Any]]) -> Iterator[dict[str, Any]]:
316369
"id": router.get('MACAddress')
317370
}
318371

372+
if not self._tags_map.is_loaded:
373+
await self._tags_map.load()
374+
319375
for device_data in devices_data:
320376
mac: str = device_data['MACAddress']
321377
host_name: str = device_data.get('HostName', f'device_{mac}')
322378
name: str = device_data.get('ActualName', host_name)
323379
is_active: bool = device_data.get('Active', False)
380+
tags = self._tags_map.get_tags(mac)
324381

325382
if mac in self._connected_devices:
326383
device = self._connected_devices[mac]
327384
else:
328-
device = ConnectedDevice(name, host_name, mac, is_active)
385+
device = ConnectedDevice(name, host_name, mac, is_active, tags)
329386
self._connected_devices[device.mac] = device
330387

331388
if is_active:
@@ -335,7 +392,7 @@ def get_mesh_routers(devices: List[dict[str, Any]]) -> Iterator[dict[str, Any]]:
335392
"name": self.name or 'Primary router',
336393
"id": CONNECTED_VIA_ID_PRIMARY
337394
})
338-
device.update_device_data(name, host_name, True,
395+
device.update_device_data(name, host_name, True, tags,
339396
connected_via=connected_via.get("name"),
340397
ip_address=device_data.get('IPAddress'),
341398
interface_type=device_data.get('InterfaceType'),
@@ -346,7 +403,7 @@ def get_mesh_routers(devices: List[dict[str, Any]]) -> Iterator[dict[str, Any]]:
346403
connected_via_id=connected_via.get("id")
347404
)
348405
else:
349-
device.update_device_data(name, host_name, False)
406+
device.update_device_data(name, host_name, False, tags)
350407

351408
_LOGGER.debug('Connected devices updated')
352409

0 commit comments

Comments
 (0)