Connected device |
- Per-device traffic monitoring (#220)
- Possibility to use DHCP `hostname` value for device tracking (#119)
@@ -194,7 +205,6 @@ Here is the list of features being in process of development or considered for t
-
## Support the integration
### Issues and Pull requests
@@ -213,6 +223,6 @@ Moreover, you can support the integration by using the Amazon links provided in
## Thanks to
-The initial codebase for this integration is highly based on Home Assistant core integration [AsusWRT](https://www.home-assistant.io/integrations/asuswrt/) and [ollo69/ha_asuswrt_custom](https://github.com/ollo69/ha_asuswrt_custom).
+The initial codebase (from April 2022) for this integration is highly based on Home Assistant core integration [AsusWRT](https://www.home-assistant.io/integrations/asuswrt/) and [ollo69/ha_asuswrt_custom](https://github.com/ollo69/ha_asuswrt_custom).
[^amazon]: As an Amazon Associate I earn from qualifying purchases. Not like I ever got anything yet (:
diff --git a/custom_components/asusrouter/bridge.py b/custom_components/asusrouter/bridge.py
index 0b95522..4eda00c 100644
--- a/custom_components/asusrouter/bridge.py
+++ b/custom_components/asusrouter/bridge.py
@@ -7,6 +7,18 @@
from typing import Any, Callable, Optional
import aiohttp
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SSL,
+ CONF_USERNAME,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+from homeassistant.helpers.aiohttp_client import async_create_clientsession
+from homeassistant.helpers.update_coordinator import UpdateFailed
+
from asusrouter import AsusRouter
from asusrouter.error import AsusRouterError
from asusrouter.modules.aimesh import AiMeshDevice
@@ -20,20 +32,10 @@
)
from asusrouter.modules.identity import AsusDevice
from asusrouter.modules.parental_control import ParentalControlRule, PCRuleType
-from homeassistant.const import (
- CONF_HOST,
- CONF_PASSWORD,
- CONF_PORT,
- CONF_SSL,
- CONF_USERNAME,
-)
-from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry as er
-from homeassistant.helpers.aiohttp_client import async_create_clientsession
-from homeassistant.helpers.update_coordinator import UpdateFailed
from . import helpers
from .const import (
+ AURA,
BOOTTIME,
CONF_CACHE_TIME,
CONF_DEFAULT_CACHE_TIME,
@@ -66,6 +68,8 @@
TEMPERATURE,
WLAN,
)
+from .modules.aura import aura_to_ha
+from .modules.firmware import to_ha as firmware_to_ha
_LOGGER = logging.getLogger(__name__)
@@ -100,7 +104,9 @@ def __init__(
self._active: bool = False
@staticmethod
- def _get_api(configs: dict[str, Any], session: aiohttp.ClientSession) -> AsusRouter:
+ def _get_api(
+ configs: dict[str, Any], session: aiohttp.ClientSession
+ ) -> AsusRouter:
"""Get AsusRouter API."""
return AsusRouter(
@@ -169,14 +175,18 @@ async def async_clean(self) -> None:
# <-- Connection
# --------------------
- async def async_cleanup_sensors(self, sensors: dict[str, Any]) -> dict[str, Any]:
+ async def async_cleanup_sensors(
+ self, sensors: dict[str, Any]
+ ) -> dict[str, Any]:
"""Cleanup sensors depending on the device mode."""
mode = self._configs.get(CONF_MODE, CONF_DEFAULT_MODE)
available = MODE_SENSORS[mode]
_LOGGER.debug("Available sensors for mode=`%s`: %s", mode, available)
sensors = {
- group: details for group, details in sensors.items() if group in available
+ group: details
+ for group, details in sensors.items()
+ if group in available
}
return sensors
@@ -185,7 +195,14 @@ async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
"""Get available sensors."""
sensors = {
- BOOTTIME: {SENSORS: SENSORS_BOOTTIME, METHOD: self._get_data_boottime},
+ AURA: {
+ SENSORS: await self._get_sensors_modern(AsusData.AURA),
+ METHOD: self._get_data_aura,
+ },
+ BOOTTIME: {
+ SENSORS: SENSORS_BOOTTIME,
+ METHOD: self._get_data_boottime,
+ },
CPU: {
SENSORS: await self._get_sensors_modern(AsusData.CPU),
METHOD: self._get_data_cpu,
@@ -207,7 +224,9 @@ async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
METHOD: self._get_data_network,
},
"ovpn_client": {
- SENSORS: await self._get_sensors_modern(AsusData.OPENVPN_CLIENT),
+ SENSORS: await self._get_sensors_modern(
+ AsusData.OPENVPN_CLIENT
+ ),
METHOD: self._get_data_ovpn_client,
},
"ovpn_server": {
@@ -240,11 +259,15 @@ async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
METHOD: self._get_data_wan,
},
"wireguard_client": {
- SENSORS: await self._get_sensors_modern(AsusData.WIREGUARD_CLIENT),
+ SENSORS: await self._get_sensors_modern(
+ AsusData.WIREGUARD_CLIENT
+ ),
METHOD: self._get_data_wireguard_client,
},
"wireguard_server": {
- SENSORS: await self._get_sensors_modern(AsusData.WIREGUARD_SERVER),
+ SENSORS: await self._get_sensors_modern(
+ AsusData.WIREGUARD_SERVER
+ ),
METHOD: self._get_data_wireguard_server,
},
WLAN: {
@@ -304,6 +327,13 @@ async def async_get_clients(self) -> dict[str, AsusClient]:
return await self._get_data(AsusData.CLIENTS, force=True)
# Sensor-specific methods
+ async def _get_data_aura(self) -> dict[str, Any]:
+ """Get Aura data from the device."""
+
+ data = await self._get_data_modern(AsusData.AURA)
+
+ return aura_to_ha(data)
+
async def _get_data_boottime(self) -> dict[str, Any]:
"""Get `boottime` data from the device."""
@@ -317,7 +347,9 @@ async def _get_data_cpu(self) -> dict[str, Any]:
async def _get_data_firmware(self) -> dict[str, Any]:
"""Get firmware data from the device."""
- return await self._get_data(AsusData.FIRMWARE)
+ data = await self._get_data_modern(AsusData.FIRMWARE)
+
+ return firmware_to_ha(data)
async def _get_data_gwlan(self) -> dict[str, Any]:
"""Get GWLAN data from the device."""
@@ -492,7 +524,9 @@ async def _get_sensors(
"Raw `%s` sensors of type (%s): %s", datatype, type(data), data
)
sensors = (
- process(data) if process is not None else self._process_sensors(data)
+ process(data)
+ if process is not None
+ else self._process_sensors(data)
)
_LOGGER.debug("Available `%s` sensors: %s", sensor_type, sensors)
except AsusRouterError as ex:
@@ -516,7 +550,9 @@ async def _get_sensors_modern(self, datatype: AsusData) -> list[str]:
"Raw `%s` sensors of type (%s): %s", datatype, type(data), data
)
sensors = convert_to_ha_sensors_list(data)
- _LOGGER.debug("Available `%s` sensors: %s", datatype.value, sensors)
+ _LOGGER.debug(
+ "Available `%s` sensors: %s", datatype.value, sensors
+ )
except AsusRouterError as ex:
if datatype.value in DEFAULT_SENSORS:
sensors = DEFAULT_SENSORS[datatype.value]
@@ -637,7 +673,9 @@ async def async_pc_rule(self, **kwargs: Any) -> bool:
reg_value = entity_reg.async_get(entity)
if not isinstance(reg_value, er.RegistryEntry):
continue
- capabilities: dict[str, Any] = helpers.as_dict(reg_value.capabilities)
+ capabilities: dict[str, Any] = helpers.as_dict(
+ reg_value.capabilities
+ )
devices.append(capabilities)
# Convert devices to rules
diff --git a/custom_components/asusrouter/client.py b/custom_components/asusrouter/client.py
index 3342b53..a00d146 100644
--- a/custom_components/asusrouter/client.py
+++ b/custom_components/asusrouter/client.py
@@ -5,19 +5,17 @@
from datetime import datetime, timezone
from typing import Any, Callable, Optional
+from homeassistant.core import callback
+from homeassistant.helpers.device_registry import format_mac
+
from asusrouter.modules.client import (
AsusClient,
AsusClientConnection,
AsusClientConnectionWlan,
AsusClientDescription,
)
-from asusrouter.modules.connection import ConnectionState
-from asusrouter.modules.homeassistant import (
- convert_to_ha_state_bool,
- convert_to_ha_string,
-)
-from homeassistant.core import callback
-from homeassistant.helpers.device_registry import format_mac
+from asusrouter.modules.connection import ConnectionState, ConnectionType
+from asusrouter.modules.homeassistant import convert_to_ha_state_bool
from .helpers import clean_dict
@@ -49,6 +47,9 @@ def __init__(
# Connection state
self._state: ConnectionState = ConnectionState.UNKNOWN
+ self._connection_type: ConnectionType = ConnectionType.DISCONNECTED
+ self._guest: bool = False
+ self._guest_id: int = 0
# Device last active
self._last_activity: Optional[datetime] = None
@@ -58,7 +59,9 @@ def update(
self,
client_info: Optional[AsusClient] = None,
consider_home: int = 0,
- event_call: Optional[Callable[[str, Optional[dict[str, Any]]], None]] = None,
+ event_call: Optional[
+ Callable[[str, Optional[dict[str, Any]]], None]
+ ] = None,
):
"""Update client information."""
@@ -77,7 +80,7 @@ def update(
# Connected state
state = client_info.state
- self._identity = self.generate_identity()
+ self._identity = self.generate_identity(state)
self._extra_state_attributes = self.generate_extra_state_attributes()
# If is connected
@@ -111,7 +114,9 @@ def update(
self.identity,
)
- def generate_identity(self) -> dict[str, Any]:
+ def generate_identity(
+ self, state: Optional[ConnectionState]
+ ) -> dict[str, Any]:
"""Generate client identity."""
identity: dict[str, Any] = {
@@ -121,14 +126,22 @@ def generate_identity(self) -> dict[str, Any]:
}
if isinstance(self.connection, AsusClientConnection):
- identity["connection_type"] = convert_to_ha_string(self.connection.type)
+ # Rewrite guest from last known state if needed
+ if state == ConnectionState.DISCONNECTED:
+ identity["guest"] = self._guest
+ identity["guest_id"] = self._guest_id
+ if self.connection.type != ConnectionType.DISCONNECTED:
+ self._connection_type = self.connection.type
+ identity["connection_type"] = self._connection_type
identity["node"] = (
- format_mac(self.connection.node) if self.connection.node else None
+ format_mac(self.connection.node)
+ if self.connection.node
+ else None
)
if isinstance(self.connection, AsusClientConnectionWlan):
- identity["guest"] = self.connection.guest
- identity["guest_id"] = self.connection.guest_id
+ identity["guest"] = self._guest = self.connection.guest
+ identity["guest_id"] = self._guest_id = self.connection.guest_id
identity["connected"] = self.connection.since
return clean_dict(identity)
@@ -136,7 +149,9 @@ def generate_identity(self) -> dict[str, Any]:
def generate_extra_state_attributes(self) -> dict[str, Any]:
"""Generate extra state attributes."""
- attributes: dict[str, Any] = self._identity.copy() if self._identity else {}
+ attributes: dict[str, Any] = (
+ self._identity.copy() if self._identity else {}
+ )
attributes["last_activity"] = self._last_activity
@@ -165,7 +180,9 @@ def state(self) -> Optional[bool]:
def ip_address(self) -> Optional[str]:
"""Return IP address."""
- return self.connection.ip_address if self.connection is not None else None
+ return (
+ self.connection.ip_address if self.connection is not None else None
+ )
@property
def mac_address(self) -> str:
@@ -177,7 +194,11 @@ def mac_address(self) -> str:
def name(self) -> Optional[str]:
"""Return name."""
- return self.description.name if self.description is not None else self._name
+ return (
+ self.description.name
+ if self.description is not None
+ else self._name
+ )
@property
def extra_state_attributes(self) -> dict[str, Any]:
diff --git a/custom_components/asusrouter/const.py b/custom_components/asusrouter/const.py
index a4f4da9..402cf19 100644
--- a/custom_components/asusrouter/const.py
+++ b/custom_components/asusrouter/const.py
@@ -4,13 +4,6 @@
from typing import Any, Callable
-from asusrouter.modules.openvpn import AsusOVPNClient, AsusOVPNServer
-from asusrouter.modules.parental_control import AsusBlockAll, AsusParentalControl
-from asusrouter.modules.port_forwarding import AsusPortForwarding
-from asusrouter.modules.system import AsusSystem
-from asusrouter.modules.wireguard import AsusWireGuardClient, AsusWireGuardServer
-from asusrouter.modules.wlan import AsusWLAN, Wlan
-from asusrouter.tools import converters
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.button import ButtonDeviceClass
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
@@ -32,6 +25,20 @@
UnitOfTemperature,
)
+from asusrouter.modules.openvpn import AsusOVPNClient, AsusOVPNServer
+from asusrouter.modules.parental_control import (
+ AsusBlockAll,
+ AsusParentalControl,
+)
+from asusrouter.modules.port_forwarding import AsusPortForwarding
+from asusrouter.modules.system import AsusSystem
+from asusrouter.modules.wireguard import (
+ AsusWireGuardClient,
+ AsusWireGuardServer,
+)
+from asusrouter.modules.wlan import AsusWLAN, Wlan
+from asusrouter.tools import converters
+
from .dataclass import (
ARBinarySensorDescription,
ARButtonDescription,
@@ -81,6 +88,7 @@
API_ID = "api_id"
API_TYPE = "api_type"
APPLY = "apply"
+AURA = "aura"
BITS_PER_SECOND = "bits/s"
BOOTTIME = "boottime"
BRIDGE = "bridge"
@@ -255,7 +263,7 @@
# Access Point
MODE_ACCESS_POINT = MODE_NODE.copy()
-MODE_ACCESS_POINT.extend([WLAN])
+MODE_ACCESS_POINT.extend([WLAN, AURA])
# Media Bridge
MODE_MEDIA_BRIDGE = MODE_ACCESS_POINT.copy()
@@ -290,9 +298,15 @@
SENSORS_AIMESH = [NUMBER, LIST]
SENSORS_BOOTTIME = ["datetime"]
SENSORS_CHANGE = ["change"]
-SENSORS_CONNECTED_DEVICES = [NUMBER, DEVICES, "latest", "latest_time"]
+SENSORS_CONNECTED_DEVICES = [
+ NUMBER,
+ DEVICES,
+ "latest",
+ "latest_time",
+ "gn_number",
+]
SENSORS_CPU = [TOTAL, USED, USAGE]
-SENSORS_FIRMWARE = [STATE]
+SENSORS_FIRMWARE = [STATE, "state_beta"]
SENSORS_GWLAN = {
"sync_node": "aimesh_sync",
"auth_mode_x": "auth_method",
@@ -506,7 +520,7 @@
}
CONF_DEFAULT_HIDE_PASSWORDS = False
CONF_DEFAULT_INTERFACES = [WAN.upper()]
-CONF_DEFAULT_INTERVALS = {CONF_INTERVAL + FIRMWARE: 21600}
+CONF_DEFAULT_INTERVALS = {CONF_INTERVAL + FIRMWARE: 600}
CONF_DEFAULT_LATEST_CONNECTED = 5
CONF_DEFAULT_MODE = ROUTER
CONF_DEFAULT_PORT = 0
@@ -1029,6 +1043,22 @@
entity_registry_enabled_default=True,
),
]
+STATIC_AURA: list[AREntityDescription] = [
+ ARLightDescription(
+ key="state",
+ key_group="aura",
+ name="AURA",
+ icon_on=ICON_LIGHT_ON,
+ icon_off=ICON_LIGHT_OFF,
+ entity_category=EntityCategory.CONFIG,
+ entity_registry_enabled_default=True,
+ extra_state_attributes={
+ "brightness": "brightness",
+ "rgb_color": "rgb_color",
+ "zones": "zones",
+ },
+ ),
+]
STATIC_SENSORS: list[AREntityDescription] = [
# AiMesh
ARSensorDescription(
@@ -1066,6 +1096,17 @@
"devices": "devices",
},
),
+ # Connected GuestNetwork devices
+ ARSensorDescription(
+ key="gn_number",
+ key_group="devices",
+ name="Connected GuestNetwork Devices",
+ icon=ICON_ROUTER,
+ state_class=SensorStateClass.MEASUREMENT,
+ entity_category=EntityCategory.DIAGNOSTIC,
+ entity_registry_enabled_default=True,
+ extra_state_attributes={},
+ ),
# CPU
ARSensorDescription(
key="total_usage",
@@ -1076,7 +1117,9 @@
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
- extra_state_attributes={f"{num}_usage": f"core_{num}" for num in NUMERIC_CORES},
+ extra_state_attributes={
+ f"{num}_usage": f"core_{num}" for num in NUMERIC_CORES
+ },
),
# Latest connected
ARSensorDescription(
@@ -1225,7 +1268,8 @@
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
extra_state_attributes={
- f"{num}_{key}": value for key, value in SENSORS_OVPN_CLIENT.items()
+ f"{num}_{key}": value
+ for key, value in SENSORS_OVPN_CLIENT.items()
},
)
for num in range(1, 6)
@@ -1247,7 +1291,8 @@
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
extra_state_attributes={
- f"{num}_{key}": value for key, value in SENSORS_OVPN_SERVER.items()
+ f"{num}_{key}": value
+ for key, value in SENSORS_OVPN_SERVER.items()
},
)
for num in NUMERIC_OVPN_SERVER
@@ -1269,7 +1314,8 @@
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
extra_state_attributes={
- f"{num}_{key}": value for key, value in SENSORS_WIREGUARD_CLIENT.items()
+ f"{num}_{key}": value
+ for key, value in SENSORS_WIREGUARD_CLIENT.items()
},
)
for num in range(1, 6)
@@ -1291,7 +1337,8 @@
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
extra_state_attributes={
- f"{num}_{key}": value for key, value in SENSORS_WIREGUARD_SERVER.items()
+ f"{num}_{key}": value
+ for key, value in SENSORS_WIREGUARD_SERVER.items()
},
)
for num in range(1, 2)
@@ -1359,7 +1406,8 @@
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
extra_state_attributes={
- f"{wlan}_{gwlan}_{key}": value for key, value in SENSORS_GWLAN.items()
+ f"{wlan}_{gwlan}_{key}": value
+ for key, value in SENSORS_GWLAN.items()
},
)
for gwlan in NUMERIC_GWLAN
@@ -1392,12 +1440,26 @@
name="Firmware update",
icon=ICON_UPDATE,
device_class=UpdateDeviceClass.FIRMWARE,
+ entity_registry_enabled_default=True,
+ extra_state_attributes={
+ "current": "current",
+ "latest": "latest",
+ "release_note": "release_note",
+ },
+ ),
+ ARUpdateDescription(
+ key="state_beta",
+ key_group="firmware",
+ name="Firmware update (Beta)",
+ icon=ICON_UPDATE,
+ device_class=UpdateDeviceClass.FIRMWARE,
+ entity_registry_enabled_default=False,
extra_state_attributes={
"current": "current",
- "available": "available",
+ "latest_beta": "latest",
"release_note": "release_note",
},
- )
+ ),
]
# <-- SENSORS
diff --git a/custom_components/asusrouter/device_tracker.py b/custom_components/asusrouter/device_tracker.py
index b9c257f..45e375f 100644
--- a/custom_components/asusrouter/device_tracker.py
+++ b/custom_components/asusrouter/device_tracker.py
@@ -113,7 +113,7 @@ def _compile_device_info(self, mac_address: str, name: Optional[str]) -> DeviceI
identifiers={
(DOMAIN, mac_address),
},
- name=name,
+ default_name=name,
via_device=(DOMAIN, self._router.mac),
)
diff --git a/custom_components/asusrouter/light.py b/custom_components/asusrouter/light.py
index 99a0e62..9372a51 100644
--- a/custom_components/asusrouter/light.py
+++ b/custom_components/asusrouter/light.py
@@ -2,20 +2,37 @@
from __future__ import annotations
+import logging
from typing import Any
-from asusrouter.modules.led import AsusLED
-from homeassistant.components.light import ColorMode, LightEntity
+from homeassistant.components.light import (
+ ColorMode,
+ LightEntity,
+ LightEntityFeature,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
-from .const import ASUSROUTER, DOMAIN, STATIC_LIGHTS
+from asusrouter.modules.aura import AsusAura
+from asusrouter.modules.color import ColorRGB, scale_value_int
+from asusrouter.modules.led import AsusLED
+
+from .const import ASUSROUTER, DOMAIN, STATIC_AURA, STATIC_LIGHTS
from .dataclass import ARLightDescription
from .entity import ARBinaryEntity, async_setup_ar_entry
+from .modules.aura import AURA_EFFECTS, AURA_NO_EFFECT, per_zone_light
from .router import ARDevice
+_LOGGER = logging.getLogger(__name__)
+
+EFFECT = "effect"
+RGB_COLOR = "rgb_color"
+BRIGHTNESS = "brightness"
+COLOR_MODE = "color_mode"
+ZONE_ID = "zone_id"
+
async def async_setup_entry(
hass: HomeAssistant,
@@ -24,14 +41,36 @@ async def async_setup_entry(
) -> None:
"""Set up AsusRouter lights."""
- lights = STATIC_LIGHTS.copy()
-
- if not hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER].bridge.identity.led:
- return
-
- await async_setup_ar_entry(
- hass, config_entry, async_add_entities, lights, ARLightLED
- )
+ leds = STATIC_LIGHTS.copy()
+ if (
+ hass.data[DOMAIN][config_entry.entry_id][
+ ASUSROUTER
+ ].bridge.identity.led
+ is True
+ ):
+ await async_setup_ar_entry(
+ hass, config_entry, async_add_entities, leds, ARLightLED
+ )
+
+ auras = STATIC_AURA.copy()
+ if (
+ hass.data[DOMAIN][config_entry.entry_id][
+ ASUSROUTER
+ ].bridge.identity.aura
+ is True
+ ):
+ # Create per-zone lights
+ auras.extend(
+ per_zone_light(
+ hass.data[DOMAIN][config_entry.entry_id][
+ ASUSROUTER
+ ].bridge.identity.aura_zone
+ )
+ )
+
+ await async_setup_ar_entry(
+ hass, config_entry, async_add_entities, auras, ARLightAura
+ )
class ARLightLED(ARBinaryEntity, LightEntity):
@@ -66,3 +105,87 @@ async def async_turn_off(
"""Turn off LED."""
await self._set_state(AsusLED.OFF)
+
+
+class ARLightAura(ARBinaryEntity, LightEntity):
+ """AsusRouter Aura light."""
+
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ router: ARDevice,
+ description: ARLightDescription,
+ ) -> None:
+ """Initialize AsusRouter Aura light."""
+
+ self._attr_supported_features = LightEntityFeature.EFFECT
+ self._attr_effect_list = AURA_EFFECTS
+
+ super().__init__(coordinator, router, description)
+ self.entity_description: ARLightDescription = description
+
+ async def async_turn_on(
+ self,
+ **kwargs: Any,
+ ) -> None:
+ """Turn on Aura."""
+
+ effect = AsusAura.ON
+ if "effect" in kwargs:
+ effect = AsusAura.__members__.get(
+ kwargs["effect"].upper(), AsusAura.ON
+ )
+
+ color = None
+ if "rgb_color" in kwargs:
+ rgb = kwargs["rgb_color"]
+ color = ColorRGB(rgb[0], rgb[1], rgb[2], scale=255)
+
+ brightness = None
+ if "brightness" in kwargs:
+ brightness = scale_value_int(kwargs["brightness"], 128, 255)
+
+ zone_id = (
+ self.entity_description.capabilities.get("zone_id", None)
+ if self.entity_description.capabilities
+ else None
+ )
+
+ await self._set_state(
+ effect,
+ color=color,
+ brightness=brightness,
+ zone=zone_id,
+ )
+
+ async def async_turn_off(
+ self,
+ **kwargs: Any,
+ ) -> None:
+ """Turn off Aura."""
+
+ await self._set_state(AsusAura.OFF)
+
+ @property
+ def effect(self) -> str | None:
+ """Return the current effect."""
+
+ return self.coordinator.data.get("effect", AURA_NO_EFFECT)
+
+ @property
+ def supported_color_modes(self) -> set[str] | None:
+ """Return the supported color modes."""
+
+ # This is a workaround to avoid a bug in HA
+ # which does not hide the color picker and brightness slider
+ # even when the current color mode is set to ONOFF only
+ _supported_color_modes = self.coordinator.data.get(
+ "color_mode", ColorMode.RGB
+ )
+ return {_supported_color_modes}
+
+ @property
+ def color_mode(self) -> ColorMode | str | None:
+ """Return the color mode."""
+
+ return self.coordinator.data.get("color_mode", ColorMode.RGB)
diff --git a/custom_components/asusrouter/manifest.json b/custom_components/asusrouter/manifest.json
index e5e98df..783f9d4 100644
--- a/custom_components/asusrouter/manifest.json
+++ b/custom_components/asusrouter/manifest.json
@@ -8,6 +8,6 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/Vaskivskyi/ha-asusrouter/issues",
"loggers": ["asusrouter"],
- "requirements": ["asusrouter==1.11.0"],
- "version": "0.32.1"
+ "requirements": ["asusrouter==1.12.0"],
+ "version": "0.33.0"
}
diff --git a/custom_components/asusrouter/modules/__init__.py b/custom_components/asusrouter/modules/__init__.py
new file mode 100644
index 0000000..9dc1640
--- /dev/null
+++ b/custom_components/asusrouter/modules/__init__.py
@@ -0,0 +1 @@
+"""Complex modules for AsusRouter integration."""
diff --git a/custom_components/asusrouter/modules/aura.py b/custom_components/asusrouter/modules/aura.py
new file mode 100644
index 0000000..398686f
--- /dev/null
+++ b/custom_components/asusrouter/modules/aura.py
@@ -0,0 +1,132 @@
+"""Aura RGB module for AsusRouter integration.
+
+This module allows converting AsusRouter Aura data to the
+Home Assistant format. This is required due to the complex
+data structure and effect handling by Home Assistant.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.components.light import ColorMode
+from homeassistant.const import EntityCategory
+
+from asusrouter.modules.color import ColorRGBB
+from asusrouter.tools.converters import scale_value_int
+
+from ..const import ICON_LIGHT_OFF, ICON_LIGHT_ON
+from ..dataclass import AREntityDescription, ARLightDescription
+
+AURA_NO_EFFECT = "EFFECT_OFF"
+AURA_FALLBACK_BRIGHTNESS = 128
+AURA_FALLBACK_COLOR = ColorRGBB(
+ (128, 128, 128),
+ AURA_FALLBACK_BRIGHTNESS,
+ scale=128,
+)
+
+AURA_EFFECTS = [
+ "Gradient",
+ "Static",
+ "Breathing",
+ "Evolution",
+ "Rainbow",
+ "Wave",
+ "Marquee",
+]
+
+AURA_EFFECTS_MAP = {
+ 0: AURA_NO_EFFECT,
+ 1: "Gradient",
+ 2: "Static",
+ 3: "Breathing",
+ 4: "Evolution",
+ 5: "Rainbow",
+ 6: "Wave",
+ 7: "Marquee",
+}
+
+SUPPORTED_COLOR_MODES = {
+ 0: ColorMode.ONOFF,
+ 1: ColorMode.RGB,
+ 2: ColorMode.RGB,
+ 3: ColorMode.RGB,
+ 4: ColorMode.ONOFF,
+ 5: ColorMode.ONOFF,
+ 6: ColorMode.ONOFF,
+ 7: ColorMode.RGB,
+}
+
+ACTIVE_BRIGHTNESS = "active_brightness"
+ACTIVE_COLOR = "active_color"
+BRIGHTNESS = "brightness"
+RGB_COLOR = "rgb_color"
+SCHEME = "scheme"
+STATE = "state"
+ZONES = "zones"
+
+
+def aura_to_ha(data: dict[str, Any]) -> dict[str, Any]:
+ """Convert AsusRouter Aura data to Home Assistant format."""
+
+ result = {
+ STATE: data.get(STATE, False),
+ ZONES: data.get(ZONES, 0),
+ }
+
+ # Current active effect
+ _effect = data.get(SCHEME, 0)
+ result["effect"] = AURA_EFFECTS_MAP.get(_effect, AURA_NO_EFFECT)
+
+ # Colors
+ if ACTIVE_COLOR in data:
+ result[RGB_COLOR] = ColorRGBB(data[ACTIVE_COLOR], scale=255).as_tuple()
+ if ACTIVE_BRIGHTNESS in data:
+ result[BRIGHTNESS] = scale_value_int(
+ data.get(ACTIVE_BRIGHTNESS, AURA_FALLBACK_BRIGHTNESS),
+ 255,
+ 128,
+ )
+
+ for i in range(result["zones"]):
+ color_key = f"active_{i}_color"
+ brightness_key = f"active_{i}_brightness"
+ if color_key in data:
+ result[f"{RGB_COLOR}_{i}"] = ColorRGBB(
+ data.get(color_key, AURA_FALLBACK_COLOR),
+ scale=255,
+ ).as_tuple()
+ if brightness_key in data:
+ result[f"{BRIGHTNESS}_{i}"] = scale_value_int(
+ data.get(brightness_key, AURA_FALLBACK_BRIGHTNESS),
+ 255,
+ 128,
+ )
+
+ # Color mode support
+ result["color_mode"] = SUPPORTED_COLOR_MODES.get(_effect, ColorMode.ONOFF)
+
+ return result
+
+
+def per_zone_light(zones: int = 3) -> list[AREntityDescription]:
+ """Create a per-zone light entity descriptions."""
+
+ return [
+ ARLightDescription(
+ key=STATE,
+ key_group="aura",
+ name=f"AURA Zone {i+1}",
+ icon_on=ICON_LIGHT_ON,
+ icon_off=ICON_LIGHT_OFF,
+ capabilities={"zone_id": i},
+ entity_category=EntityCategory.CONFIG,
+ entity_registry_enabled_default=False,
+ extra_state_attributes={
+ f"{BRIGHTNESS}_{i}": BRIGHTNESS,
+ f"{RGB_COLOR}_{i}": RGB_COLOR,
+ },
+ )
+ for i in range(zones)
+ ]
diff --git a/custom_components/asusrouter/modules/firmware.py b/custom_components/asusrouter/modules/firmware.py
new file mode 100644
index 0000000..bf54484
--- /dev/null
+++ b/custom_components/asusrouter/modules/firmware.py
@@ -0,0 +1,31 @@
+"""Firmware module for AsusRouter integration."""
+
+from __future__ import annotations
+
+from typing import Any, Optional
+
+
+def to_ha(data: Optional[dict[str, Any]]) -> dict[str, Any]:
+ """Convert AsusRouter firmware data to Home Assistant format."""
+
+ if not data:
+ return {}
+
+ # Current firmware
+ _current = data.get("current")
+ _current = str(_current) if _current else None
+
+ # Available stable firmware
+ _latest = data.get("available")
+ _latest = str(_latest) if _latest else _current
+
+ # Available beta firmware
+ _latest_beta = data.get("available_beta")
+ _latest_beta = str(_latest_beta) if _latest_beta else _current
+
+ return {
+ "current": _current,
+ "latest": _latest,
+ "latest_beta": _latest_beta,
+ "release_note": data.get("release_note"),
+ }
diff --git a/custom_components/asusrouter/router.py b/custom_components/asusrouter/router.py
index 7bb6af5..9947549 100644
--- a/custom_components/asusrouter/router.py
+++ b/custom_components/asusrouter/router.py
@@ -8,6 +8,7 @@
from typing import Any, Optional
from asusrouter.error import AsusRouterError
+from asusrouter.modules.client import AsusClientConnectionWlan
from asusrouter.modules.connection import ConnectionState, ConnectionType
from asusrouter.modules.identity import AsusDevice
from asusrouter.modules.parental_control import ParentalControlRule
@@ -115,6 +116,7 @@ def __init__(
self._latest_connected_list: list[dict[str, Any]] = []
self._aimesh_number: int = 0
self._aimesh_list: list[dict[str, Any]] = []
+ self._gn_clients_number: int = 0
async def _get_clients(self) -> dict[str, Any]:
"""Return clients sensors."""
@@ -124,6 +126,7 @@ async def _get_clients(self) -> dict[str, Any]:
SENSORS_CONNECTED_DEVICES[1]: self._clients_list,
SENSORS_CONNECTED_DEVICES[2]: self._latest_connected_list,
SENSORS_CONNECTED_DEVICES[3]: self._latest_connected,
+ SENSORS_CONNECTED_DEVICES[4]: self._gn_clients_number,
}
async def _get_aimesh(self) -> dict[str, Any]:
@@ -143,6 +146,7 @@ def update_clients(
clients_list: Optional[list[Any]],
latest_connected: Optional[datetime],
latest_connected_list: list[Any],
+ gn_clients_number: int,
) -> bool:
"""Update connected devices attribute."""
@@ -151,12 +155,14 @@ def update_clients(
and self._clients_list == clients_list
and self._latest_connected == latest_connected
and self._latest_connected_list == latest_connected_list
+ and self._gn_clients_number == gn_clients_number
):
return False
self._clients_number = clients_number
self._clients_list = clients_list
self._latest_connected = latest_connected
self._latest_connected_list = latest_connected_list
+ self._gn_clients_number = gn_clients_number
return True
def update_aimesh(
@@ -286,6 +292,7 @@ def __init__(
self._latest_connected: Optional[datetime] = None
self._latest_connected_list: list[dict[str, Any]] = []
self._connect_error: bool = False
+ self._gn_clients_number: int = 0
# Sensor filters
self.sensor_filters: dict[tuple[str, str], list[str]] = {}
@@ -563,11 +570,18 @@ async def update_clients(self) -> None:
self._clients_number = 0
self._clients_list = []
+ # Connected GuestNetwork clients sensor
+ self._gn_clients_number = 0
+
for client_mac, client in self._clients.items():
if client.state:
self._clients_number += 1
self._clients_list.append(client.identity)
+ if isinstance(client.connection, AsusClientConnectionWlan) and client.connection.guest:
+ self._gn_clients_number += 1
+
+
# Filter clients
# Only include the listed clients
if self._client_filter == "include":
@@ -802,6 +816,7 @@ async def _init_sensor_coordinators(self) -> None:
self._clients_list,
self._latest_connected,
self._latest_connected_list,
+ self._gn_clients_number,
)
self._sensor_handler.update_aimesh(
self._aimesh_number,
@@ -861,6 +876,7 @@ async def _update_unpolled_sensors(self) -> None:
clients_list,
self._latest_connected,
self._latest_connected_list,
+ self._gn_clients_number,
):
await coordinator.async_refresh()
diff --git a/custom_components/asusrouter/update.py b/custom_components/asusrouter/update.py
index 70c750c..2f94705 100644
--- a/custom_components/asusrouter/update.py
+++ b/custom_components/asusrouter/update.py
@@ -6,13 +6,14 @@
import logging
from typing import Any
-from asusrouter.modules.system import AsusSystem
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+from asusrouter.modules.system import AsusSystem
+
from .const import STATIC_UPDATES
from .dataclass import ARUpdateDescription
from .entity import ARBinaryEntity, async_setup_ar_entry
@@ -49,9 +50,13 @@ def __init__(
super().__init__(coordinator, router, description)
self.entity_description: ARUpdateDescription = description
- self._attr_installed_version = self.extra_state_attributes.get("current")
- self._attr_latest_version = self.extra_state_attributes.get("available")
- self._attr_release_summary = self.extra_state_attributes.get("release_note")
+ self._attr_installed_version = self.extra_state_attributes.get(
+ "current"
+ )
+ self._attr_latest_version = self.extra_state_attributes.get("latest")
+ self._attr_release_summary = self.extra_state_attributes.get(
+ "release_note"
+ )
self._attr_in_progress = False
self._attr_supported_features = (
@@ -87,5 +92,6 @@ async def async_install(
except Exception as ex: # pylint: disable=broad-except
_LOGGER.error(
- "An exception occurred while trying to install the update: %s", ex
+ "An exception occurred while trying to install the update: %s",
+ ex,
)
|