"""AsusRouter router module.""" from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import UTC, datetime, timedelta import logging from typing import Any 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 from homeassistant.components.device_tracker import CONF_CONSIDER_HOME from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL, Platform, ) from homeassistant.core import ( CALLBACK_TYPE, HomeAssistant, ServiceCall, callback, ) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, UpdateFailed, ) from .aimesh import AiMeshNode from .bridge import ARBridge from .client import ARClient from .const import ( ACCESS_POINT, AIMESH, CONF_CLIENT_DEVICE, CONF_CLIENT_FILTER, CONF_CLIENT_FILTER_LIST, CONF_CLIENTS_IN_ATTR, CONF_CREATE_DEVICES, CONF_DEFAULT_CLIENT_DEVICE, CONF_DEFAULT_CLIENT_FILTER, CONF_DEFAULT_CLIENTS_IN_ATTR, CONF_DEFAULT_CONSIDER_HOME, CONF_DEFAULT_CREATE_DEVICES, CONF_DEFAULT_EVENT, CONF_DEFAULT_INTERVALS, CONF_DEFAULT_LATEST_CONNECTED, CONF_DEFAULT_MODE, CONF_DEFAULT_PORT, CONF_DEFAULT_PORTS, CONF_DEFAULT_SCAN_INTERVAL, CONF_DEFAULT_SPLIT_INTERVALS, CONF_DEFAULT_TRACK_DEVICES, CONF_EVENT_NODE_CONNECTED, CONF_INTERVAL, CONF_INTERVAL_DEVICES, CONF_LATEST_CONNECTED, CONF_MODE, CONF_REQ_RELOAD, CONF_SPLIT_INTERVALS, CONF_TRACK_DEVICES, CONNECTED, COORDINATOR, DEVICES, DOMAIN, FIRMWARE, LIST, MAC, MEDIA_BRIDGE, METHOD, NO_SSL, NUMBER, ROUTER, SENSORS, SENSORS_AIMESH, SENSORS_CONNECTED_DEVICES, SSL, ) from .helpers import as_dict _LOGGER = logging.getLogger(__name__) class ARSensorHandler: """Handler for AsusRouter sensors.""" def __init__( self, hass: HomeAssistant, bridge: ARBridge, options: dict[str, Any], ) -> None: """Initialise sensor handler.""" self.hass = hass self.bridge = bridge # Selected options self._options = options self._mode = options.get(CONF_MODE, CONF_DEFAULT_MODE) self._split_intervals = options.get( CONF_SPLIT_INTERVALS, CONF_DEFAULT_SPLIT_INTERVALS ) # Sensors self._clients_number: int = 0 self._clients_list: list[dict[str, Any]] | None = [] self._latest_connected: datetime | None = None 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.""" return { SENSORS_CONNECTED_DEVICES[0]: self._clients_number, 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]: """Return aimesh sensors.""" # In router / AP / Media Bridge mode if self._mode in (ACCESS_POINT, MEDIA_BRIDGE, ROUTER): return { NUMBER: self._aimesh_number, LIST: self._aimesh_list, } return {} def update_clients( self, clients_number: int, clients_list: list[Any] | None, latest_connected: datetime | None, latest_connected_list: list[Any], gn_clients_number: int, ) -> bool: """Update connected devices attribute.""" if ( self._clients_number == clients_number 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( self, nodes_number: int, nodes_list: list[dict[str, Any]], ) -> bool: """Update aimesh sensors.""" if ( self._aimesh_number == nodes_number and self._aimesh_list == nodes_list ): return False self._aimesh_number = nodes_number self._aimesh_list = nodes_list return True async def get_coordinator( self, sensor_type: str, update_method: Callable[[], Awaitable[dict[str, Any]]] | None = None, ) -> DataUpdateCoordinator: """Find coordinator for the sensor type.""" # Should sensor be polled? should_poll = True # Sensor-specific rules method: Callable[[], Awaitable[dict[str, Any]]] if sensor_type == DEVICES: should_poll = False method = self._get_clients elif sensor_type == AIMESH: should_poll = False method = self._get_aimesh elif update_method is not None: method = update_method else: raise RuntimeError(f"Unknown sensor type: {sensor_type}") # Update interval update_interval: timedelta # Static intervals if sensor_type == FIRMWARE: update_interval = timedelta( seconds=self._options.get( CONF_INTERVAL + sensor_type, CONF_DEFAULT_INTERVALS[CONF_INTERVAL + sensor_type], ) ) # Configurable intervals else: update_interval = timedelta( seconds=self._options.get( CONF_INTERVAL + sensor_type, self._options.get( CONF_SCAN_INTERVAL, CONF_DEFAULT_SCAN_INTERVAL ), ) if self._options.get( CONF_SPLIT_INTERVALS, CONF_DEFAULT_SPLIT_INTERVALS ) else self._options.get( CONF_SCAN_INTERVAL, CONF_DEFAULT_SCAN_INTERVAL ) ) # Coordinator coordinator = DataUpdateCoordinator( self.hass, _LOGGER, name=sensor_type, update_method=method, update_interval=update_interval if should_poll else None, ) _LOGGER.debug( "Coordinator initialized for `%s`. Update interval: `%s`", sensor_type, update_interval, ) # Update coordinator await coordinator.async_refresh() return coordinator class ARDevice: """Representatiion of AsusRouter.""" def __init__( self, hass: HomeAssistant, config_entry: ConfigEntry, ) -> None: """Initialize the object.""" self.hass = hass self._config_entry = config_entry self._options = config_entry.options.copy() # Device configs self._conf_host: str = config_entry.data[CONF_HOST] self._conf_port: int = self._options[CONF_PORT] self._conf_name: str = "AsusRouter" if self._conf_port == CONF_DEFAULT_PORT: self._conf_port = ( CONF_DEFAULT_PORTS[SSL] if self._options[CONF_SSL] else CONF_DEFAULT_PORTS[NO_SSL] ) self._mode = self._options.get(CONF_MODE, CONF_DEFAULT_MODE) # Bridge & device information self.bridge: ARBridge = ARBridge( hass, dict(self._config_entry.data), self._options ) self._identity: AsusDevice = AsusDevice() self._mac: str = "" # Device sensors self._sensor_handler: ARSensorHandler | None = None self._sensor_coordinator: dict[str, Any] = {} self._aimesh: dict[str, Any] = {} self._clients: dict[str, Any] = {} self._clients_number: int = 0 self._clients_list: list[dict[str, Any]] = [] self._aimesh_number: int = 0 self._aimesh_list: list[dict[str, Any]] = [] self._latest_connected: datetime | None = 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]] = {} # Client features self.client_device: bool = self._options.get( CONF_CLIENT_DEVICE, CONF_DEFAULT_CLIENT_DEVICE, ) self.clients_in_attr: bool = self._options.get( CONF_CLIENTS_IN_ATTR, CONF_DEFAULT_CLIENTS_IN_ATTR, ) if self.clients_in_attr is False: # Mask clients in attributes self.sensor_filters[(DEVICES, NUMBER)] = [DEVICES] self.create_devices: bool = self._options.get( CONF_CREATE_DEVICES, CONF_DEFAULT_CREATE_DEVICES, ) self._pc_rules: dict[str, Any] = {} # Client filter self._client_filter: str = self._options.get( CONF_CLIENT_FILTER, CONF_DEFAULT_CLIENT_FILTER ) self._client_filter_list: list[str] = self._options.get( CONF_CLIENT_FILTER_LIST, [] ) # On-close parameters self._on_close: list[Callable] = [] async def setup(self) -> None: # noqa: C901, PLR0912, PLR0915 """Set up an AsusRouter.""" _LOGGER.debug("Setting up router") # Connect & check connection try: await self.bridge.async_connect() except (OSError, AsusRouterError) as ex: raise ConfigEntryNotReady from ex if not self.bridge.connected: raise ConfigEntryNotReady _LOGGER.debug("Bridge connected") # Write the identity self._identity = self.bridge.identity self._mac = format_mac(self._identity.mac) # Use device model as the default device name if name not set if self._identity.model is not None: self._conf_name = self._identity.model # Tracked entities entity_reg = er.async_get(self.hass) tracked_entries = er.async_entries_for_config_entry( entity_reg, self._config_entry.entry_id ) # Clean up devices with no entities device_registry = dr.async_get(self.hass) devices = dr.async_entries_for_config_entry( dr.async_get(self.hass), self._config_entry.entry_id ) for device_entry in devices: identifiers = device_entry.identifiers if (DOMAIN, None) in identifiers: device_registry.async_remove_device(device_entry.id) _LOGGER.warning( "A device with merge bug was detected and removed. " "No worries, we fixed it and it should be recreated " "properly now. This warning is a single time notice. " "If not sure or something does not work as expected, " "reload the integration before creating an issue report." ) raise ConfigEntryNotReady("A bugged device was removed") entries = er.async_entries_for_device(entity_reg, device_entry.id) # No entities for the device if len(entries) == 0: _LOGGER.debug( "Removing device `%s` since it has no entities", device_entry.name, ) device_registry.async_remove_device(device_entry.id) for entry in tracked_entries: # Clients already tracked if entry.domain != "device_tracker": continue capabilities = entry.capabilities # Check that capabilities is a dictionary and that it has # the MAC address. I actually don't know how this can be possible, # but the issue https://github.com/Vaskivskyi/ha-asusrouter/issues/785 # shows that device_tracker entry can exist without a MAC address if isinstance(capabilities, dict) and "mac" in capabilities: mac = capabilities["mac"] self._clients[mac] = ARClient(mac) # Create devices when tracker was enabled disabled_by = entry.disabled_by if disabled_by is None: self._clients[mac].device = True # Mode-specific if self._mode in (ACCESS_POINT, MEDIA_BRIDGE, ROUTER): # Update AiMesh await self.update_nodes() # Update clients await self.update_clients() # Update parental control await self.update_pc_rules() else: _LOGGER.debug( "Device is in AiMesh node mode. Device tracking and " "AiMesh monitoring is disabled" ) # Clients filter match self._client_filter: case "include": _LOGGER.debug("Setting clients filter: `include`") self._clients = { mac: client for mac, client in self._clients.items() if mac in self._client_filter_list } case "exclude": _LOGGER.debug("Setting clients filter: `exclude`") self._clients = { mac: client for mac, client in self._clients.items() if mac not in self._client_filter_list } case _: _LOGGER.debug("Setting clients filter: `no_filter`") # Initialize sensor coordinators await self._init_sensor_coordinators() # Initialize services await self._init_services() # On-close parameters self.async_on_close( async_track_time_interval( self.hass, self.update_all, timedelta( seconds=self._options.get( CONF_INTERVAL_DEVICES, CONF_DEFAULT_SCAN_INTERVAL ) ), ) ) async def update_all( self, now: datetime | None = None, ) -> None: """Update all AsusRouter platforms.""" if self._mode in (ACCESS_POINT, MEDIA_BRIDGE, ROUTER): await self.update_clients() await self.update_nodes() await self.update_pc_rules() async def update_clients(self) -> None: # noqa: C901, PLR0912, PLR0915 """Update AsusRouter clients.""" # Check clients tracking settings if ( self._options.get(CONF_TRACK_DEVICES, CONF_DEFAULT_TRACK_DEVICES) is False ): _LOGGER.debug("Device tracking is disabled") else: _LOGGER.debug("Device tracking is enabled") # Get client list _LOGGER.debug( "Updating AsusRouter device list for '%s'", self._conf_host ) try: api_clients = await self.bridge.async_get_clients() # For Media bridge mode only leave wired devices if self._mode == MEDIA_BRIDGE: api_clients = { mac: client for mac, client in api_clients.items() if client.connection is not None and client.connection.type == ConnectionType.WIRED } except UpdateFailed as ex: if not self._connect_error: self._connect_error = True _LOGGER.error( "Cannot get clients from '%s': %s", self._conf_host, ex, ) return # Notify about reconnection if self._connect_error: self._connect_error = False _LOGGER.info("Reconnected to '%s'", self._conf_host) # Get the consider home settings consider_home = self._options.get( CONF_CONSIDER_HOME, CONF_DEFAULT_CONSIDER_HOME ) # Format clients MAC clients = { format_mac(mac): client for mac, client in api_clients.items() } # Update known clients for client_mac, client_state in self._clients.items(): client_info = clients.pop(client_mac, None) client_state.update( client_info, consider_home, event_call=self.fire_event, ) # Add new clients new_clients = [] new_client = False for client_mac, client_info in clients.items(): # We proceed only if device is online # This way we'll avoid adding long history of devices # which might have been connected long time ago # and are not important anymore state = client_info.state if state is not ConnectionState.CONNECTED: continue # Flag that new client is added new_client = True # Create new client and process it client_name = ( client_info.description.name if client_info.description else None ) client = ARClient(client_mac, client_name) client.update( client_info, consider_home, event_call=self.fire_event, ) # Add client to the storage self._clients[client_mac] = client # Add client to the list of new clients new_clients.append(client) # Notify about new clients for client in new_clients: _LOGGER.debug("New client: %s", client.identity) self.fire_event( "device_connected", client.identity, ) # Connected clients sensor 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": self._clients = { mac: client for mac, client in self._clients.items() if mac in self._client_filter_list } # Exclude the listed clients elif self._client_filter == "exclude": self._clients = { mac: client for mac, client in self._clients.items() if mac not in self._client_filter_list } async_dispatcher_send(self.hass, self.signal_device_update) if new_client: async_dispatcher_send(self.hass, self.signal_device_new) # Update latest connected sensors self.update_latest_connected() await self._update_unpolled_sensors() def update_latest_connected(self) -> None: """Update latest connected sensors.""" def latest_connected_time(self, element: dict[str, Any]) -> datetime: """Get connected time for the device.""" connected = element.get("connected") if isinstance(connected, datetime): return connected # If not connected, return the current time # This is just a fallback, since the device should be connected return datetime.now(UTC) # New list # We take all the clients currently connected from the # self._clients_list which have the connected time set new_list = [ client for client in self._clients_list if client.get("connected") ] # Append any client which was already in the list # self._latest_connected_list but is not in the new list. # This means that the client has disconnected, but the sensor # should be showing all the connections made. Since client # itself might have changed, we should compare MAC addresses for client in self._latest_connected_list: if client["mac"] not in [x["mac"] for x in new_list]: new_list.append(client) # Sort the list by time using the latest_connected_time function # Newer devices should be at the end of the list new_list.sort(key=lambda x: latest_connected_time(self, x)) # Reduce the list to the size specified in the configuration while len(new_list) > self._options.get( CONF_LATEST_CONNECTED, CONF_DEFAULT_LATEST_CONNECTED ): new_list.pop(0) # Update the self._latest_connected and self._latest_connected_list # Check that list has at least one element so that we # don't get an error if len(new_list) > 0: self._latest_connected = new_list[-1].get(CONNECTED) self._latest_connected_list = new_list async def update_nodes(self) -> None: """Update AsusRouter AiMesh nodes.""" _LOGGER.debug("Updating AiMesh status for '%s'", self._conf_host) try: aimesh = await self.bridge.async_get_aimesh_nodes() except UpdateFailed as ex: if not self._connect_error: self._connect_error = True _LOGGER.error( "Error connecting to '%s' for aimesh update: %s", self._conf_host, ex, ) return new_node = False # Update existing nodes nodes = { format_mac(mac): description for mac, description in aimesh.items() } for node_mac, node in self._aimesh.items(): node_info = nodes.pop(node_mac, None) node.update( node_info, event_call=self.fire_event, ) # Add new nodes new_nodes = [] for node_mac, node_info in nodes.items(): new_node = True node = AiMeshNode(node_mac) node.update( node_info, event_call=self.fire_event, ) self._aimesh[node_mac] = node new_nodes.append(node) # Notify new nodes for node in new_nodes: self.fire_event( CONF_EVENT_NODE_CONNECTED, node.identity, ) # AiMesh sensors self._aimesh_number = 0 self._aimesh_list = [] for node in self._aimesh.values(): if node.identity[CONNECTED]: self._aimesh_number += 1 self._aimesh_list.append(node.identity) async_dispatcher_send(self.hass, self.signal_aimesh_update) if new_node: async_dispatcher_send(self.hass, self.signal_aimesh_new) async def update_pc_rules(self) -> None: """Update parental control rules.""" _LOGGER.debug( "Updating parental control rules for '%s'", self._conf_host ) try: pc_data = ( await self.bridge._get_data_parental_control() # pylint: disable=protected-access ) except UpdateFailed as ex: if not self._connect_error: self._connect_error = True _LOGGER.error( "Error connecting to '%s' for pc rules update: %s", self._conf_host, ex, ) return new_flag = False rules: dict[str, ParentalControlRule] = pc_data.get("rules", {}) rules_to_save = {} # Update existing rules for mac in self._pc_rules: incoming_rule = rules.pop(mac, None) if incoming_rule is None: # If the rule was removed new_flag = True continue rules_to_save[mac] = incoming_rule # Add new rules for mac, rule in rules.items(): new_flag = True rules_to_save[mac] = rule # Check the rules for non-valid ones rules_to_save = { mac: rule for mac, rule in rules_to_save.items() if mac != "" and rule.mac is not None } # Save rules self._pc_rules = rules_to_save async_dispatcher_send(self.hass, self.signal_pc_rules_update) if new_flag: async_dispatcher_send(self.hass, self.signal_pc_rules_new) async def _init_services(self) -> None: """Initialize AsusRouter services.""" # Parental control service async def async_service_device_internet_access(service: ServiceCall): """Adjust device internet access.""" await self.bridge.async_pc_rule(raw=service.data) # Force PC rules update await self.update_pc_rules() # In case of removing rule(s) we need to reload the platform if service.data.get("state") == "remove": unload = await self.hass.config_entries.async_unload_platforms( self._config_entry, [Platform.SWITCH] ) if unload: await self.hass.config_entries.async_forward_entry_setups( self._config_entry, [Platform.SWITCH] ) if self._mode == ROUTER: self.hass.services.async_register( DOMAIN, "device_internet_access", async_service_device_internet_access, ) # Remove device trackers service async def async_service_remove_trackers(service: ServiceCall): """Remove device trackers.""" await self.remove_trackers(raw=service.data) self.hass.services.async_register( DOMAIN, "remove_trackers", async_service_remove_trackers ) async def _init_sensor_coordinators(self) -> None: """Initialize sensor coordinators.""" # If already initialized if self._sensor_handler: return # Initialize sensor handler self._sensor_handler = ARSensorHandler( self.hass, self.bridge, self._options ) # Update devices self._sensor_handler.update_clients( self._clients_number, self._clients_list, self._latest_connected, self._latest_connected_list, self._gn_clients_number, ) self._sensor_handler.update_aimesh( self._aimesh_number, self._aimesh_list, ) # Get available sensors available_sensors = await self.bridge.async_get_available_sensors() # Add devices sensors if self._mode in (ACCESS_POINT, MEDIA_BRIDGE, ROUTER): available_sensors[DEVICES] = {SENSORS: SENSORS_CONNECTED_DEVICES} available_sensors[AIMESH] = {SENSORS: SENSORS_AIMESH} # Process available sensors for sensor_type, sensor_definition in available_sensors.items(): sensor_names = sensor_definition.get(SENSORS) if not sensor_names: continue # Find and initialize coordinator coordinator = await self._sensor_handler.get_coordinator( sensor_type, sensor_definition.get(METHOD) ) # Save the coordinator self._sensor_coordinator[sensor_type] = { COORDINATOR: coordinator, sensor_type: sensor_names, } async def _update_unpolled_sensors(self) -> None: """Request refresh for AsusRouter unpolled sensors.""" # If sensor handler is not initialized if not self._sensor_handler: return # AiMesh if AIMESH in self._sensor_coordinator: coordinator = self._sensor_coordinator[AIMESH][COORDINATOR] if self._sensor_handler.update_aimesh( self._aimesh_number, self._aimesh_list, ): await coordinator.async_refresh() # Devices if DEVICES in self._sensor_coordinator: coordinator = self._sensor_coordinator[DEVICES][COORDINATOR] # Block clients list for attributes clients_list = ( None if self.clients_in_attr is False else self._clients_list ) if self._sensor_handler.update_clients( self._clients_number, clients_list, self._latest_connected, self._latest_connected_list, self._gn_clients_number, ): await coordinator.async_refresh() async def close(self) -> None: """Close the connection.""" # Disconnect the bridge if self.bridge.connected: await self.bridge.async_disconnect() # Run on-close methods for func in self._on_close: func() self._on_close.clear() @callback def async_on_close( self, func: CALLBACK_TYPE, ) -> None: """Functions on router close.""" self._on_close.append(func) def update_options( self, new_options: dict, ) -> bool: """Update router options.""" require_reload = False for name, new_option in new_options.items(): if name in CONF_REQ_RELOAD: old_opt = self._options.get(name) if not old_opt or old_opt != new_option: require_reload = True break self._options.update(new_options) return require_reload @callback def fire_event( self, event: str, args: dict[str, Any] | None = None, ): """Fire HA event.""" # Check for mute _event_mac = args.get("mac") if isinstance(args, dict) else None if _event_mac is not None: match self._client_filter: case "include": if _event_mac not in self._client_filter_list: return case "exclude": if _event_mac in self._client_filter_list: return _event_status = self._options.get(event) if _event_status is None: _event_status = CONF_DEFAULT_EVENT.get(event, False) if _event_status is True: event_name = f"{DOMAIN}_{event}" _LOGGER.debug( "Firing event `%s` with arguments: %s", event_name, args ) self.hass.bus.fire( event_name, args, ) async def remove_trackers(self, **kwargs: Any) -> None: """Remove device trackers.""" _LOGGER.debug("Removing trackers") # Check that data is provided raw = kwargs.get("raw") if raw is None: return # Get entities to remove if "entities" in raw: entities = raw["entities"] entity_reg = er.async_get(self.hass) for entity in entities: reg_value = entity_reg.async_get(entity) if not isinstance(reg_value, er.RegistryEntry): continue capabilities: dict[str, Any] = as_dict(reg_value.capabilities) mac = capabilities[MAC] _LOGGER.debug("Trying to remove tracker with mac: %s", mac) if mac in self._clients: self._clients.pop(mac) _LOGGER.debug("Found and removed") # Update clients await self.update_clients() # Reload device tracker platform unload = await self.hass.config_entries.async_unload_platforms( self._config_entry, [Platform.DEVICE_TRACKER] ) if unload: await self.hass.config_entries.async_forward_entry_setups( self._config_entry, [Platform.DEVICE_TRACKER] ) @property def device_info(self) -> DeviceInfo: """Device information.""" return DeviceInfo( configuration_url=self.bridge.configuration_url, identifiers=self.bridge.identifiers, manufacturer=self.bridge.manufacturer, model=self.bridge.model, model_id=self.bridge.model_id, name=self.bridge.name, serial_number=self.bridge.serial_number, sw_version=self.bridge.sw_version, ) @property def signal_aimesh_new(self) -> str: """Notify new AiMesh nodes.""" return f"{DOMAIN}-aimesh-new" @property def signal_aimesh_update(self) -> str: """Notify updated AiMesh nodes.""" return f"{DOMAIN}-aimesh-update" @property def signal_device_new(self) -> str: """Notify new device.""" return f"{DOMAIN}-device-new" @property def signal_device_update(self) -> str: """Notify updated device.""" return f"{DOMAIN}-device-update" @property def signal_pc_rules_new(self) -> str: """Notify new parental control rules.""" return f"{DOMAIN}-pc-rules-new" @property def signal_pc_rules_update(self) -> str: """Notify updated parental control rules.""" return f"{DOMAIN}-pc-rules-update" @property def aimesh(self) -> dict[str, Any]: """Return AiMesh nodes.""" return self._aimesh @property def devices(self) -> dict[str, Any]: """Return devices.""" return self._clients @property def mac(self) -> str: """Router MAC address.""" return self._mac @property def pc_rules(self) -> dict[str, ParentalControlRule]: """Return parental control rules.""" return self._pc_rules @property def sensor_coordinator(self) -> dict[str, Any]: """Return sensor coordinator.""" return self._sensor_coordinator