Skip to content

Commit

Permalink
0.28.0 (#767)
Browse files Browse the repository at this point in the history
* Add clients filter option (#761)

* Add option to create devices when enabling device_trackers (#762)

* Remove devices when no entities are available / enabled (#763)

* Remove blank option from the configure menu (#764)

* Add option to remove clients list from `connected_devices` sensor (#765)

* Improve clients filter vs events (#766)

* Bump version to `0.28.0`
  • Loading branch information
Vaskivskyi authored Jan 6, 2024
1 parent 60e978d commit a16e260
Show file tree
Hide file tree
Showing 21 changed files with 364 additions and 37 deletions.
2 changes: 2 additions & 0 deletions custom_components/asusrouter/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def __init__(
self._mac = mac
self._name = name

self.device: bool = False

# To be recieved from the device
# Client description
self.description: Optional[AsusClientDescription] = None
Expand Down
79 changes: 76 additions & 3 deletions custom_components/asusrouter/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,24 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac

from .bridge import ARBridge
from .const import (
ACCESS_POINT,
ALL_CLIENTS,
BASE,
CONF_CACHE_TIME,
CONF_CLIENT_DEVICE,
CONF_CLIENT_FILTER,
CONF_CLIENT_FILTER_LIST,
CONF_CLIENTS_IN_ATTR,
CONF_CONSIDER_HOME,
CONF_CREATE_DEVICES,
CONF_DEFAULT_CACHE_TIME,
CONF_DEFAULT_CLIENT_DEVICE,
CONF_DEFAULT_CLIENT_FILTER,
CONF_DEFAULT_CLIENTS_IN_ATTR,
CONF_DEFAULT_CONSIDER_HOME,
CONF_DEFAULT_CREATE_DEVICES,
CONF_DEFAULT_EVENT,
Expand All @@ -58,6 +67,7 @@
CONF_INTERVAL,
CONF_INTERVAL_DEVICES,
CONF_INTERVALS,
CONF_LABELS_CLIENT_FILTER,
CONF_LABELS_INTERFACES,
CONF_LABELS_MODE,
CONF_LATEST_CONNECTED,
Expand Down Expand Up @@ -123,6 +133,32 @@ def _check_errors(
return False


async def _async_get_clients(
hass: HomeAssistant,
configs: dict[str, Any],
options: dict[str, Any],
) -> dict[str, Any]:
"""Return list of all the known clients."""

bridge = ARBridge(hass, configs, options)

try:
if not bridge.connected:
await bridge.async_connect()
clients = await bridge.api.async_get_data(AsusData.CLIENTS)
await bridge.async_disconnect()

result = {
format_mac(mac): client.description.name
for mac, client in clients.items()
if client.description.name
}
return result
except Exception as ex: # pylint: disable=broad-except
_LOGGER.warning("Cannot get clients for %s: %s", configs[CONF_HOST], ex)
return {}


async def _async_get_network_interfaces(
hass: HomeAssistant,
configs: dict[str, Any],
Expand Down Expand Up @@ -363,17 +399,39 @@ def _create_form_operation(
def _create_form_connected_devices(
user_input: dict[str, Any] | None = None,
mode: str = CONF_DEFAULT_MODE,
default: list[str] | None = None,
) -> vol.Schema:
"""Create a form for the `connected_devices` step."""

if not user_input:
user_input = {}

if not default:
default = []

schema = {
vol.Required(
CONF_TRACK_DEVICES,
default=user_input.get(CONF_TRACK_DEVICES, CONF_DEFAULT_TRACK_DEVICES),
): cv.boolean,
vol.Required(
CONF_CLIENT_DEVICE,
default=user_input.get(CONF_CLIENT_DEVICE, CONF_DEFAULT_CLIENT_DEVICE),
): cv.boolean,
vol.Required(
CONF_CLIENTS_IN_ATTR,
default=user_input.get(CONF_CLIENTS_IN_ATTR, CONF_DEFAULT_CLIENTS_IN_ATTR),
): cv.boolean,
vol.Required(
CONF_CLIENT_FILTER,
default=user_input.get(CONF_CLIENT_FILTER, CONF_DEFAULT_CLIENT_FILTER),
): vol.In(CONF_LABELS_CLIENT_FILTER),
vol.Optional(
CONF_CLIENT_FILTER_LIST,
default=default,
): cv.multi_select(
dict(sorted(user_input[ALL_CLIENTS].items(), key=lambda item: item[1]))
),
vol.Required(
CONF_FORCE_CLIENTS,
default=user_input.get(CONF_FORCE_CLIENTS, CONF_DEFAULT_FORCE_CLIENTS),
Expand Down Expand Up @@ -733,7 +791,6 @@ async def async_step_options(
STEP_INTERFACES,
STEP_EVENTS,
STEP_SECURITY,
STEP_OPTIONS,
STEP_FINISH,
]

Expand All @@ -757,6 +814,9 @@ async def async_step_connected_devices(

if not user_input:
user_input = self._options.copy()
user_input[ALL_CLIENTS] = await _async_get_clients(
self.hass, self._configs, self._options
)
return self.async_show_form(
step_id=step_id,
data_schema=_create_form_connected_devices(user_input, self._mode),
Expand Down Expand Up @@ -909,7 +969,6 @@ async def async_step_options(
STEP_INTERFACES,
STEP_EVENTS,
STEP_SECURITY,
STEP_OPTIONS,
STEP_FINISH,
]

Expand Down Expand Up @@ -993,9 +1052,23 @@ async def async_step_connected_devices(

if not user_input:
user_input = self._options.copy()
# Get the selected clients
selected = user_input.get(CONF_CLIENT_FILTER_LIST, []).copy()
# Get all the clients
all_clients = await _async_get_clients(
self.hass, self._configs, self._options
)
# If client was in the list, but cannot be found now, still add it
for client in selected:
if client not in all_clients:
all_clients[client] = client.upper()
# Save the clients as options
user_input[ALL_CLIENTS] = all_clients
return self.async_show_form(
step_id=step_id,
data_schema=_create_form_connected_devices(user_input, self._mode),
data_schema=_create_form_connected_devices(
user_input, self._mode, default=selected
),
)
self._options.update(user_input)

Expand Down
21 changes: 19 additions & 2 deletions custom_components/asusrouter/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@
ACCESS_POINT = "access_point"
ACTION = "action"
ACTION_MODE = "action_mode"
ALIAS = "alias"
AIMESH = "aimesh"
ALIAS = "alias"
ALL_CLIENTS = "all_clients"
API_ID = "api_id"
API_TYPE = "api_type"
APPLY = "apply"
Expand Down Expand Up @@ -444,6 +445,10 @@
# Keys
CONF_CACHE_TIME = "cache_time"
CONF_CERT_PATH = "cert_path"
CONF_CLIENT_DEVICE = "client_device"
CONF_CLIENT_FILTER = "client_filter"
CONF_CLIENT_FILTER_LIST = "client_filter_list"
CONF_CLIENTS_IN_ATTR = "clients_in_attr"
CONF_CONFIRM = "confirm"
CONF_CONSIDER_HOME = "consider_home"
CONF_CREATE_DEVICES = "create_devices"
Expand Down Expand Up @@ -486,6 +491,9 @@

# Defaults
CONF_DEFAULT_CACHE_TIME = 5
CONF_DEFAULT_CLIENT_DEVICE = False
CONF_DEFAULT_CLIENT_FILTER = "no_filter"
CONF_DEFAULT_CLIENTS_IN_ATTR = True
CONF_DEFAULT_CONSIDER_HOME = 45
CONF_DEFAULT_CREATE_DEVICES = False
CONF_DEFAULT_ENABLE_CONTROL = False
Expand Down Expand Up @@ -515,6 +523,11 @@
CONF_DEFAULT_USERNAME = "admin"

# Labels
CONF_LABELS_CLIENT_FILTER = {
"no_filter": "No filter / All clients",
"include": "Include only",
"exclude": "Exclude devices",
}
CONF_LABELS_INTERFACES = {
BRIDGE: "Bridge",
f"{LACP}1": "LACP1",
Expand All @@ -538,6 +551,10 @@
CONF_REQ_RELOAD = [
CONF_CACHE_TIME,
CONF_CERT_PATH,
CONF_CLIENT_DEVICE,
CONF_CLIENT_FILTER,
CONF_CLIENT_FILTER_LIST,
CONF_CLIENTS_IN_ATTR,
CONF_CONFIRM,
CONF_CONSIDER_HOME,
CONF_CREATE_DEVICES,
Expand All @@ -556,8 +573,8 @@
CONF_LATEST_CONNECTED,
CONF_MODE,
CONF_SPLIT_INTERVALS,
CONF_TRACK_DEVICES,
CONF_SCAN_INTERVAL,
CONF_TRACK_DEVICES,
]
CONF_REQ_RELOAD.extend(CONF_INTERVALS)

Expand Down
2 changes: 1 addition & 1 deletion custom_components/asusrouter/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class AREntityDescription(EntityDescription):
"""Describe AsusRouter entity."""

capabilities: Optional[dict[str, Any]] = None
key_group: Optional[str] = None
key_group: str = ""
value: Callable[[Any], Any] = lambda val: val
extra_state_attributes: Optional[dict[str, Any]] = None

Expand Down
55 changes: 42 additions & 13 deletions custom_components/asusrouter/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from __future__ import annotations

import logging
from typing import Any, Optional

from homeassistant.components.device_tracker import SourceType
from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .client import ARClient
Expand All @@ -21,6 +24,8 @@
)
from .router import ARDevice

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
Expand Down Expand Up @@ -81,19 +86,43 @@ class ARDeviceEntity(ScannerEntity):
def __init__(
self,
router: ARDevice,
device: ARClient,
client: ARClient,
) -> None:
"""Initialize connected device."""

self._router = router
self._device = device
self._attr_unique_id = f"{router.mac}_{device.mac_address}"
self._attr_name = device.name or DEFAULT_DEVICE_NAME
self._client = client
self._attr_unique_id = f"{router.mac}_{client.mac_address}"
self._attr_name = client.name or DEFAULT_DEVICE_NAME
self._attr_capability_attributes = {
"mac": device.mac_address,
"mac": client.mac_address,
"name": self._attr_name,
}

# Assign device info if set up
if router.client_device is True and client.device is True:
self._attr_device_info = self._compile_device_info(
client.mac_address, client.name
)

def _compile_device_info(self, mac_address: str, name: Optional[str]) -> DeviceInfo:
"""Compile device info."""

return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
identifiers={
(DOMAIN, mac_address),
},
name=name,
via_device=(DOMAIN, self._router.mac),
)

@property
def device_info(self) -> DeviceInfo:
"""Return device info."""

return self._attr_device_info

@property
def source_type(self) -> SourceType:
"""Source type."""
Expand All @@ -104,31 +133,31 @@ def source_type(self) -> SourceType:
def is_connected(self) -> Optional[bool]:
"""Device status."""

return self._device.state
return self._client.state

@property
def ip_address(self) -> Optional[str]:
"""Device IP address."""

return self._device.ip_address
return self._client.ip_address

@property
def mac_address(self) -> str:
"""Device MAC address."""

return self._device.mac_address
return self._client.mac_address

@property
def hostname(self) -> Optional[str]:
"""Device hostname."""

return self._device.name
return self._client.name

@property
def icon(self) -> str:
"""Device icon."""

return "mdi:lan-connect" if self._device.state else "mdi:lan-disconnect"
return "mdi:lan-connect" if self._client.state else "mdi:lan-disconnect"

@property
def unique_id(self) -> Optional[str]:
Expand All @@ -140,14 +169,14 @@ def unique_id(self) -> Optional[str]:
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra state attributes."""

return dict(sorted(self._device.extra_state_attributes.items())) or {}
return dict(sorted(self._client.extra_state_attributes.items())) or {}

@callback
def async_on_demand_update(self) -> None:
"""Update the state."""

if self._device.mac_address in self._router.devices:
self._device = self._router.devices[self._device.mac_address]
if self._client.mac_address in self._router.devices:
self._client = self._router.devices[self._client.mac_address]
self.async_write_ha_state()

async def async_added_to_hass(self) -> None:
Expand Down
9 changes: 9 additions & 0 deletions custom_components/asusrouter/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ def extra_state_attributes(self) -> dict[str, Any]:
if not _attributes:
return {}

# Mask attributes
attrs_to_mask = self.router.sensor_filters.get(
(description.key_group, description.key),
[],
)
for attr in attrs_to_mask:
if attr in _attributes:
_attributes.pop(attr, None)

attributes = {}

for attr in _attributes:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/asusrouter/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"manufacturer": "ASUSTeK Computer Inc."
}
],
"version": "0.27.2"
"version": "0.28.0"
}
Loading

0 comments on commit a16e260

Please sign in to comment.