Skip to content

Commit

Permalink
Added support for router-specific zones (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
vmakeev authored Dec 24, 2022
1 parent 05a7e0e commit d45b8b7
Show file tree
Hide file tree
Showing 26 changed files with 471 additions and 110 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ Home Assistant custom component for control Huawei mesh routers over LAN.

## Supported models

| Name | Model | Confirmed | Notes |
|------------------------------------------------------------------------------------|--------|-----------|-----------------------------------------|
| [Huawei WiFi Mesh 3](https://consumer.huawei.com/en/routers/wifi-mesh3/) | WS8100 | Yes | All features are available |
| [Huawei WiFi AX3 Dual-core](https://consumer.huawei.com/en/routers/ax3-dual-core/) | WS7100 | Yes | No NFC switches (unsupported by router) |
| [Huawei WiFi AX3 Quad-core](https://consumer.huawei.com/en/routers/ax3-quad-core/) | WS7200 | Yes | --- |
| [Huawei WiFi AX3 Pro](https://consumer.huawei.com/en/routers/ax3-pro/) | WS7206 | No | --- |
| Other routers with HarmonyOS | ------ | No | Will most likely work
| Name | Model | Confirmed | Notes |
|------------------------------------------------------------------------------------|--------|-----------|---------------------------------------------|
| [Huawei WiFi AX3 Dual-core](https://consumer.huawei.com/en/routers/ax3-dual-core/) | WS7100 | Yes | No NFC switches (unsupported by router) |
| [Huawei WiFi AX3 Quad-core](https://consumer.huawei.com/en/routers/ax3-quad-core/) | WS7200 | Yes | |
| [Huawei WiFi AX3 Pro](https://consumer.huawei.com/en/routers/ax3-pro/) | WS7206 | No | |
| [Huawei WiFi Mesh 3](https://consumer.huawei.com/en/routers/wifi-mesh3/) | WS8100 | Yes | My router model. All features are available |
| [Huawei WiFi Mesh 7](https://consumer.huawei.com/en/routers/wifi-mesh7/) | WS8800 | Yes | |
| Other routers with HarmonyOS | ------ | No | Will most likely work |

## Installation

Expand Down Expand Up @@ -74,6 +75,7 @@ Advanced settings include:
| Enabling or disabling [Number of connected devices for each router](docs/sensors.md#number-of-connected-devices) | Enabled |
| Enabling or disabling [Device tags](docs/device-tags.md#device-tags) | Disabled |
| Enabling or disabling [Devices tracking](docs/device-tracking.md#devices-tracking) | Enabled |
| Enabling or disabling [Router-specific zones](docs/device-tracking.md#router-specific-zones) | Disabled |


![Options 1/2](docs/images/options_1.png)
Expand All @@ -83,7 +85,7 @@ Advanced settings include:

## Devices tracking

The component allows you to track all devices connected to your mesh network. [Read more](docs/device-tracking.md)
The component allows you to track all devices connected to your mesh network. Each router can provide information about the zone to which all devices connected to it will be assigned (if enabled in the [advanced options](#advanced-options)). [Read more](docs/device-tracking.md)

You can 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. [Read more](docs/device-tags.md#device-tags)

Expand All @@ -109,8 +111,8 @@ You can attach one or more tags to each client device in order to be able to use
* Device Wi-Fi Access ([read more](docs/controls.md#device-wi-fi-access))

### Selects

* Wi-Fi access control mode ([read more](docs/controls.md#wi-fi-access-control-mode))
* Router-specific zone ([read more](docs/controls.md#router-specific-zone))

## Services

Expand Down
28 changes: 20 additions & 8 deletions custom_components/huawei_mesh_router/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

from .client.huaweiapi import HuaweiApi
from .const import (
DEFAULT_DEVICE_TRACKER_ZONES,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
OPT_DEVICE_TRACKER,
OPT_DEVICE_TRACKER_ZONES,
OPT_DEVICES_TAGS,
OPT_ROUTER_CLIENTS_SENSORS,
OPT_WIFI_ACCESS_SWITCHES,
Expand All @@ -23,7 +25,7 @@
from .helpers import pop_coordinator, set_coordinator
from .options import HuaweiIntegrationOptions
from .services import async_setup_services, async_unload_services
from .update_coordinator import HuaweiControllerDataUpdateCoordinator
from .update_coordinator import HuaweiDataUpdateCoordinator

CONFIG_SCHEMA = config_validation.removed(DOMAIN, raise_if_present=False)

Expand Down Expand Up @@ -59,14 +61,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
integration_options = HuaweiIntegrationOptions(config_entry)

if integration_options.devices_tags:
store = Store(
tags_store = Store(
hass, STORAGE_VERSION, f"huawei_mesh_{config_entry.entry_id}_tags"
)
else:
store = None
tags_store = None

coordinator = HuaweiControllerDataUpdateCoordinator(
hass, config_entry, integration_options, store
if integration_options.device_tracker_zones:
zones_store = Store(
hass, STORAGE_VERSION, f"huawei_mesh_{config_entry.entry_id}_router_zones"
)
else:
zones_store = None

coordinator = HuaweiDataUpdateCoordinator(
hass, config_entry, integration_options, tags_store, zones_store
)
await coordinator.async_config_entry_first_refresh()

Expand Down Expand Up @@ -108,9 +117,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
)
if unload_ok:
coordinator = pop_coordinator(hass, config_entry)
if coordinator and isinstance(
coordinator, HuaweiControllerDataUpdateCoordinator
):
if coordinator and isinstance(coordinator, HuaweiDataUpdateCoordinator):
coordinator.unload()
await async_unload_services(hass, config_entry)
return unload_ok
Expand Down Expand Up @@ -141,6 +148,11 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
}
config_entry.version = 2

if config_entry.version == 2:
_LOGGER.debug("Migrating to version 3")
updated_options[OPT_DEVICE_TRACKER_ZONES] = DEFAULT_DEVICE_TRACKER_ZONES
config_entry.version = 3

hass.config_entries.async_update_entry(
config_entry, data=updated_data, options=updated_options
)
Expand Down
8 changes: 4 additions & 4 deletions custom_components/huawei_mesh_router/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
get_coordinator,
get_past_moment,
)
from .update_coordinator import HuaweiControllerDataUpdateCoordinator
from .update_coordinator import HuaweiDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -96,13 +96,13 @@ async def async_setup_entry(
# HuaweiBinarySensor
# ---------------------------
class HuaweiBinarySensor(
CoordinatorEntity[HuaweiControllerDataUpdateCoordinator], BinarySensorEntity
CoordinatorEntity[HuaweiDataUpdateCoordinator], BinarySensorEntity
):
entity_description: HuaweiBinarySensorEntityDescription

def __init__(
self,
coordinator: HuaweiControllerDataUpdateCoordinator,
coordinator: HuaweiDataUpdateCoordinator,
description: HuaweiBinarySensorEntityDescription,
) -> None:
"""Initialize."""
Expand Down Expand Up @@ -139,7 +139,7 @@ class HuaweiWanBinarySensor(HuaweiBinarySensor):

def __init__(
self,
coordinator: HuaweiControllerDataUpdateCoordinator,
coordinator: HuaweiDataUpdateCoordinator,
description: HuaweiBinarySensorEntityDescription,
) -> None:
"""Initialize."""
Expand Down
13 changes: 4 additions & 9 deletions custom_components/huawei_mesh_router/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@
generate_entity_unique_id,
get_coordinator,
)
from .update_coordinator import (
ActiveRoutersWatcher,
HuaweiControllerDataUpdateCoordinator,
)
from .update_coordinator import ActiveRoutersWatcher, HuaweiDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -77,14 +74,12 @@ def coordinator_updated() -> None:
# ---------------------------
# HuaweiButton
# ---------------------------
class HuaweiButton(
CoordinatorEntity[HuaweiControllerDataUpdateCoordinator], ButtonEntity, ABC
):
class HuaweiButton(CoordinatorEntity[HuaweiDataUpdateCoordinator], ButtonEntity, ABC):
_update_url: str

def __init__(
self,
coordinator: HuaweiControllerDataUpdateCoordinator,
coordinator: HuaweiDataUpdateCoordinator,
action_name: str,
device_mac: MAC_ADDR | None,
) -> None:
Expand Down Expand Up @@ -132,7 +127,7 @@ def press(self) -> None:
class HuaweiRebootButton(HuaweiButton):
def __init__(
self,
coordinator: HuaweiControllerDataUpdateCoordinator,
coordinator: HuaweiDataUpdateCoordinator,
device: ConnectedDevice | None,
) -> None:
"""Initialize."""
Expand Down
15 changes: 15 additions & 0 deletions custom_components/huawei_mesh_router/classes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Huawei Mesh Router component classes."""

from dataclasses import dataclass
from typing import Any, Dict, Iterable, Tuple

from homeassistant.backports.enum import StrEnum
Expand All @@ -9,6 +10,15 @@
DEVICE_TAG = str


# ---------------------------
# ZoneInfo
# ---------------------------
@dataclass()
class ZoneInfo:
name: str
entity_id: str


# ---------------------------
# HuaweiInterfaceType
# ---------------------------
Expand Down Expand Up @@ -95,6 +105,11 @@ def connected_via_id(self) -> str | None:
"""Return the id of parent device."""
return self._data.get("connected_via_id")

@property
def zone(self) -> ZoneInfo | None:
"""Return the zone of the device."""
return self._data.get("zone")

@property
def interface_type(self) -> HuaweiInterfaceType | None:
"""Return the connection interface type."""
Expand Down
2 changes: 1 addition & 1 deletion custom_components/huawei_mesh_router/client/huaweiapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ async def _process_access_lists(
filter_action: FilterAction,
device_mac: MAC_ADDR,
device_name: str | None,
) -> (bool | None, dict[str, Any] | None, dict[str, Any] | None):
) -> Tuple[bool | None, dict[str, Any] | None, dict[str, Any] | None]:
"""Return (need_action, whitelist, blacklist)"""
whitelist = state.get("WMACAddresses")
blacklist = state.get("BMACAddresses")
Expand Down
10 changes: 9 additions & 1 deletion custom_components/huawei_mesh_router/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .client.huaweiapi import HuaweiApi
from .const import (
DEFAULT_DEVICE_TRACKER,
DEFAULT_DEVICE_TRACKER_ZONES,
DEFAULT_DEVICES_TAGS,
DEFAULT_HOST,
DEFAULT_NAME,
Expand All @@ -34,6 +35,7 @@
DEFAULT_WIFI_ACCESS_SWITCHES,
DOMAIN,
OPT_DEVICE_TRACKER,
OPT_DEVICE_TRACKER_ZONES,
OPT_DEVICES_TAGS,
OPT_ROUTER_CLIENTS_SENSORS,
OPT_WIFI_ACCESS_SWITCHES,
Expand All @@ -59,7 +61,7 @@ def configured_instances(hass):
class HuaweiControllerConfigFlow(ConfigFlow, domain=DOMAIN):
"""HuaweiControllerConfigFlow class"""

VERSION = 2
VERSION = 3

def __init__(self):
"""Initialize HuaweiControllerConfigFlow."""
Expand Down Expand Up @@ -219,6 +221,12 @@ async def async_step_features_select(self, user_input=None) -> FlowResult:
OPT_DEVICE_TRACKER, DEFAULT_DEVICE_TRACKER
),
): bool,
vol.Required(
OPT_DEVICE_TRACKER_ZONES,
default=self.options.get(
OPT_DEVICE_TRACKER_ZONES, DEFAULT_DEVICE_TRACKER_ZONES
),
): bool,
},
),
)
2 changes: 2 additions & 0 deletions custom_components/huawei_mesh_router/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
OPT_ROUTER_CLIENTS_SENSORS = "router_clients_sensors"
OPT_DEVICES_TAGS = "devices_tags"
OPT_DEVICE_TRACKER = "device_tracker"
OPT_DEVICE_TRACKER_ZONES = "device_tracker_zones"

DEFAULT_HOST: Final = "192.168.3.1"
DEFAULT_USER: Final = "admin"
Expand All @@ -28,6 +29,7 @@
DEFAULT_ROUTER_CLIENTS_SENSORS: Final = True
DEFAULT_DEVICES_TAGS: Final = False
DEFAULT_DEVICE_TRACKER: Final = True
DEFAULT_DEVICE_TRACKER_ZONES: Final = False

ATTR_MANUFACTURER: Final = "Huawei"
PLATFORMS: Final = [
Expand Down
23 changes: 17 additions & 6 deletions custom_components/huawei_mesh_router/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any

from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER
from homeassistant.components.device_tracker.const import SourceType
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
Expand All @@ -15,9 +15,9 @@
from .client.classes import MAC_ADDR
from .helpers import get_coordinator
from .options import HuaweiIntegrationOptions
from .update_coordinator import HuaweiControllerDataUpdateCoordinator
from .update_coordinator import HuaweiDataUpdateCoordinator

FILTER_ATTRS = ["ip_address", "connected_via_id", "vendor_class_id"]
FILTER_ATTRS = ["ip_address", "connected_via_id", "vendor_class_id", "zone"]


# ---------------------------
Expand Down Expand Up @@ -47,7 +47,7 @@ def coordinator_updated():
# ---------------------------
@callback
def update_items(
coordinator: HuaweiControllerDataUpdateCoordinator,
coordinator: HuaweiDataUpdateCoordinator,
integration_options: HuaweiIntegrationOptions,
async_add_entities: AddEntitiesCallback,
tracked: dict[MAC_ADDR, HuaweiTracker],
Expand All @@ -73,10 +73,11 @@ def __init__(
self,
device: ConnectedDevice,
integration_options: HuaweiIntegrationOptions,
coordinator: HuaweiControllerDataUpdateCoordinator,
coordinator: HuaweiDataUpdateCoordinator,
) -> None:
"""Initialize the tracked device."""
self.device: ConnectedDevice = device
self._use_zones = integration_options.device_tracker_zones

if integration_options.devices_tags:
self._filter_attrs = FILTER_ATTRS
Expand All @@ -86,6 +87,16 @@ def __init__(

super().__init__(coordinator)

@property
def state(self) -> str:
if self._use_zones and self.is_connected:
return (
self.device.zone.name
if self.device and self.device.zone
else super().state
)
return super().state

@property
def is_connected(self) -> bool:
"""Return true if the client is connected to the network."""
Expand All @@ -94,7 +105,7 @@ def is_connected(self) -> bool:
@property
def source_type(self) -> str:
"""Return the source type of the client."""
return SOURCE_TYPE_ROUTER
return SourceType.GPS if self._use_zones else SourceType.ROUTER

@property
def name(self) -> str:
Expand Down
Loading

0 comments on commit d45b8b7

Please sign in to comment.