Skip to content

Commit

Permalink
Added port mapping switches
Browse files Browse the repository at this point in the history
  • Loading branch information
vmakeev committed Feb 27, 2024
1 parent 445f3d4 commit 3a90328
Show file tree
Hide file tree
Showing 22 changed files with 533 additions and 37 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Home Assistant custom component for control Huawei mesh routers over LAN.
- control of the NFC (OneHop Connect) on each router separately
- control of the Fast Roaming function (802.11r)
- control of the Target Wake Time (reduce power consumption of Wi-Fi 6 devices in sleep mode)
- port mapping switches
- reboot buttons
- events for connecting, disconnecting, or moving devices over a mesh network
- automatic detection of available functions
Expand All @@ -51,6 +52,7 @@ Home Assistant custom component for control Huawei mesh routers over LAN.
| [Huawei WiFi Mesh](https://consumer.huawei.com/en/routers/wifi-mesh/) | AC2200 | |
| [Huawei WiFi Mesh 3](https://consumer.huawei.com/en/routers/wifi-mesh3/) | WS8100 | My router model. All features are available |
| [Huawei WiFi Mesh 7](https://consumer.huawei.com/en/routers/wifi-mesh7/) | WS8800 | |
| [Honor Router 3](https://www.hihonor.com/global/routers/honor-router-3/) | XD20 | |


Other Huawei routers based on HarmonyOS (except Chinese domestic market) most likely will work.
Expand All @@ -74,7 +76,7 @@ Copy `huawei_mesh_router` folder from [latest release](https://github.com/vmakee

### HACS

`HACS` **->** press `EXPLORE & DOWNLOAD REPOSITORIES` button **->** type `huawei mesh router` **->** press `DOWNLOAD` button and follow the instructions.
`HACS` **->** type `huawei mesh router` **->** click on it **->** press `DOWNLOAD` button and follow the instructions.

Or simply click the button below:

Expand Down Expand Up @@ -111,6 +113,7 @@ Advanced settings include:
| Enabling or disabling [Router-specific zones](docs/device-tracking.md#router-specific-zones) | Disabled |
| Enabling or disabling [Website filtering switches](docs/controls.md#website-filtering) | Disabled |
| Enabling or disabling [Event entities](docs/events.md#event-entities) | Disabled |
| Enabling or disabling [Port mapping switches](docs/controls.md#port-mapping) | Disabled |


![Options 1/2](docs/images/options_1.png)
Expand Down Expand Up @@ -149,6 +152,7 @@ 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))
* Website filtering ([read more](docs/controls.md#website-filtering))
* Guest network ([read more](docs/controls.md#guest-network))
* Port mapping ([read more](docs/controls.md#port-mapping))

### Selects
* Wi-Fi access control mode ([read more](docs/controls.md#wi-fi-access-control-mode))
Expand Down
7 changes: 7 additions & 0 deletions custom_components/huawei_mesh_router/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .client.huaweiapi import HuaweiApi
from .const import (
DEFAULT_DEVICE_TRACKER_ZONES,
DEFAULT_PORT_MAPPING_SWITCHES,
DEFAULT_SCAN_INTERVAL,
DEFAULT_URL_FILTER_SWITCHES,
DEFAULT_EVENT_ENTITIES,
Expand All @@ -20,6 +21,7 @@
OPT_DEVICE_TRACKER_ZONES,
OPT_DEVICES_TAGS,
OPT_EVENT_ENTITIES,
OPT_PORT_MAPPING_SWITCHES,
OPT_ROUTER_CLIENTS_SENSORS,
OPT_URL_FILTER_SWITCHES,
OPT_WIFI_ACCESS_SWITCHES,
Expand Down Expand Up @@ -175,6 +177,11 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
updated_options[OPT_EVENT_ENTITIES] = DEFAULT_EVENT_ENTITIES
config_entry.version = 5

if config_entry.version == 5:
_LOGGER.debug("Migrating to version 6")
updated_options[OPT_PORT_MAPPING_SWITCHES] = DEFAULT_PORT_MAPPING_SWITCHES
config_entry.version = 6

hass.config_entries.async_update_entry(
config_entry, data=updated_data, options=updated_options
)
Expand Down
78 changes: 68 additions & 10 deletions custom_components/huawei_mesh_router/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Select(StrEnum):
class EmulatedSwitch(StrEnum):
DEVICE_ACCESS = "wlan_device_access_switch"
URL_FILTER = "url_filter_switch"
PORT_MAPPING = "port_mapping"


# ---------------------------
Expand Down Expand Up @@ -107,6 +108,63 @@ def devices(self) -> Iterable[HuaweiFilterItem]:
return self._devices


# ---------------------------
# PortMapping
# ---------------------------
class PortMapping:
def __init__(
self,
id: str,
name: str,
enabled: bool,
host_name: str,
host_ip: str,
host_mac: str,
) -> None:
self._id = id
self._name = name
self._enabled = enabled
self._host_name = host_name
self._host_ip = host_ip
self._host_mac = host_mac

def update_info(
self, name: str, enabled: bool, host_name: str, host_ip: str, host_mac: str
) -> None:
self._name = name
self._enabled = enabled
self._host_name = host_name
self._host_ip = host_ip
self._host_mac = host_mac

def set_enabled(self, enabled: bool) -> None:
self._enabled = enabled

@property
def id(self) -> str:
return self._id

@property
def name(self) -> str:
return self._name

@property
def enabled(self) -> bool:
return self._enabled

@property
def host_name(self) -> str:
return self._host_name

@property
def host_ip(self) -> str:
return self._host_ip

@property
def host_mac(self) -> str:
return self._host_mac


# ---------------------------
# ConnectedDevice
# ---------------------------
Expand Down Expand Up @@ -253,9 +311,9 @@ class EventTypes(StrEnum):
class HuaweiEvents:
def __init__(self, hass: HomeAssistant):
self._hass: HomeAssistant = hass
self._subscriptions: dict[
CALLBACK_TYPE, tuple[HANDLER_TYPE, object | None]
] = {}
self._subscriptions: dict[CALLBACK_TYPE, tuple[HANDLER_TYPE, object | None]] = (
{}
)

# ---------------------------
# async_subscribe_event
Expand Down Expand Up @@ -293,7 +351,7 @@ def fire_router_added(
primary_router_serial: str | None,
router_mac: MAC_ADDR,
router_ip: str,
router_name: str | None
router_name: str | None,
) -> None:
"""Fire an event when a new router is discovered."""
event_data = {
Expand All @@ -311,7 +369,7 @@ def fire_router_removed(
primary_router_serial: str | None,
router_mac: MAC_ADDR,
router_ip: str,
router_name: str | None
router_name: str | None,
) -> None:
"""Fire an event when a router becomes unavailable."""
event_data = {
Expand All @@ -331,14 +389,14 @@ def fire_device_connected(
device_ip: str,
device_name: str | None,
router_id: str,
router_name: str
router_name: str,
) -> None:
"""Fire an event when a new device is connected."""
event_data = {
"type": EventTypes.DEVICE_CONNECTED,
"primary_router": primary_router_serial,
"device": {"ip": device_ip, "mac": device_mac, "name": device_name},
"router": {"id": router_id, "name": router_name}
"router": {"id": router_id, "name": router_name},
}
self._fire(EVENT_TYPE_DEVICE, event_data)

Expand All @@ -352,14 +410,14 @@ def fire_device_disconnected(
device_ip: str,
device_name: str | None,
router_id: str,
router_name: str
router_name: str,
) -> None:
"""Fire an event when a device becomes disconnected."""
event_data = {
"type": EventTypes.DEVICE_DISCONNECTED,
"primary_router": primary_router_serial,
"device": {"ip": device_ip, "mac": device_mac, "name": device_name},
"router": {"id": router_id, "name": router_name}
"router": {"id": router_id, "name": router_name},
}
self._fire(EVENT_TYPE_DEVICE, event_data)

Expand All @@ -382,6 +440,6 @@ def fire_device_changed_router(
"primary_router": primary_router_serial,
"device": {"ip": device_ip, "mac": device_mac, "name": device_name},
"router_from": {"id": old_router_id, "name": old_router_name},
"router_to": {"id": actual_router_id, "name": actual_router_name}
"router_to": {"id": actual_router_id, "name": actual_router_name},
}
self._fire(EVENT_TYPE_DEVICE, event_data)
67 changes: 67 additions & 0 deletions custom_components/huawei_mesh_router/client/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Feature(StrEnum):
WLAN_FILTER = "feature_wlan_filter"
DEVICE_TOPOLOGY = "feature_device_topology"
GUEST_NETWORK = "feature_guest_network"
PORT_MAPPING = "feature_port_mapping"


# ---------------------------
Expand Down Expand Up @@ -128,6 +129,72 @@ class HuaweiUrlFilterInfo:
devices: list[HuaweiFilterItem]


# ---------------------------
# HuaweiPortMappingItem
# ---------------------------
class HuaweiPortMappingItem:
def __init__(
self,
id: str,
name: str,
enabled: bool,
host_ip: str,
host_mac: MAC_ADDR,
host_name: str,
application_id: str,
) -> None:
self._id = id
self._name = name
self._enabled = enabled
self._host_ip = host_ip
self._host_name = host_name
self._host_mac = host_mac
self._application_id = application_id

@classmethod
def parse(cls, raw_data: dict[str, Any]) -> HuaweiPortMappingItem:
id = raw_data.get("ID")
if not id:
raise ValueError("Id can not be empty")

raw_enabled = raw_data.get("Enable")
enabled = isinstance(raw_enabled, bool) and raw_enabled

ip_address = raw_data.get("HostIPAddress", "")
mac_address = raw_data.get("InternalHost", "")
name = raw_data.get("Name", "")
host_name = raw_data.get("HostName", "")
application_id = raw_data.get("ApplicationID", "")

return HuaweiPortMappingItem(
id, name, enabled, ip_address, mac_address, host_name, application_id
)

@property
def id(self) -> str:
return self._id

@property
def name(self) -> str:
return self._name

@property
def enabled(self) -> bool:
return self._enabled

@property
def host_name(self) -> str:
return self._host_name

@property
def host_ip(self) -> str:
return self._host_ip

@property
def host_mac(self) -> str:
return self._host_mac


# ---------------------------
# HuaweiFilterItem
# ---------------------------
Expand Down
3 changes: 2 additions & 1 deletion custom_components/huawei_mesh_router/client/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
URL_WLAN_FILTER: Final = "api/ntwk/wlanfilterenhance"
URL_URL_FILTER: Final = "api/ntwk/urlfilter"
URL_GUEST_NETWORK: Final = "api/ntwk/guest_network?type=notshowpassall"
URL_WAN_INFO : Final = "api/ntwk/wan?type=active"
URL_WAN_INFO: Final = "api/ntwk/wan?type=active"
URL_PORT_MAPPING: Final = "api/ntwk/portmapping"
27 changes: 26 additions & 1 deletion custom_components/huawei_mesh_router/client/huaweiapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
HuaweiFilterItem,
HuaweiGuestNetworkDuration,
HuaweiGuestNetworkItem,
HuaweiPortMappingItem,
HuaweiRouterInfo,
HuaweiRsaPublicKey,
HuaweiUrlFilterInfo,
Expand All @@ -29,6 +30,7 @@
URL_DEVICE_TOPOLOGY,
URL_GUEST_NETWORK,
URL_HOST_INFO,
URL_PORT_MAPPING,
URL_REBOOT,
URL_REPEATER_INFO,
URL_SWITCH_NFC,
Expand Down Expand Up @@ -165,7 +167,7 @@ async def get_wan_connection_info(self) -> HuaweiConnectionInfo:
connected=data.get("Status") == _STATUS_CONNECTED,
address=data.get("ExternalIPAddress"),
upload_rate=rate_data.get("UpBandwidth", 0),
download_rate=rate_data.get("DownBandwidth", 0)
download_rate=rate_data.get("DownBandwidth", 0),
)

async def get_switch_state(self, switch: Switch) -> bool:
Expand Down Expand Up @@ -702,6 +704,29 @@ async def set_guest_network_state(
headers={"Content-Type": "application/json;charset=UTF-8;enp"},
)

async def get_port_mappings(
self,
) -> Iterable[HuaweiPortMappingItem]:
return [
HuaweiPortMappingItem.parse(item)
for item in await self._core_api.get(URL_PORT_MAPPING)
]

async def set_port_mapping_state(self, port_mapping_id: str, enabled: bool) -> None:
port_mappings = await self._core_api.get(URL_PORT_MAPPING)
target = next(
(item for item in port_mappings if item.get("ID") == port_mapping_id)
)

if not target:
raise InvalidActionError(f"Unknown filter: {port_mapping_id}")

target["Enable"] = enabled

await self._core_api.post(
URL_PORT_MAPPING, target, extra_data={"action": "update"}
)

async def _set_guest_network_enabled(self, enabled: bool) -> None:
actual_2g, actual_5g = await self.get_guest_network_info()

Expand Down
10 changes: 10 additions & 0 deletions custom_components/huawei_mesh_router/client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .const import (
URL_DEVICE_TOPOLOGY,
URL_GUEST_NETWORK,
URL_PORT_MAPPING,
URL_SWITCH_NFC,
URL_SWITCH_WIFI_80211R,
URL_SWITCH_WIFI_TWT,
Expand Down Expand Up @@ -103,6 +104,12 @@ async def _is_guest_network_available(self) -> bool:
data = await self._core_api.get(URL_GUEST_NETWORK)
return data is not None

@log_feature(Feature.PORT_MAPPING)
@unauthorized_as_false
async def _is_port_mapping_available(self) -> bool:
data = await self._core_api.get(URL_PORT_MAPPING)
return data is not None

async def update(self) -> None:
"""Update the available features list."""
if await self._is_nfc_available():
Expand All @@ -126,6 +133,9 @@ async def update(self) -> None:
if await self._is_guest_network_available():
self._available_features.add(Feature.GUEST_NETWORK)

if await self._is_port_mapping_available():
self._available_features.add(Feature.PORT_MAPPING)

def is_available(self, feature: Feature) -> bool:
"""Return true if feature is available."""
return feature in self._available_features
Loading

0 comments on commit 3a90328

Please sign in to comment.