Initial Commit

This commit is contained in:
2026-06-11 11:50:50 -04:00
commit d4a69c41be
2748 changed files with 80489 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
"""Support for AsusRouter devices."""
from __future__ import annotations
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .const import ASUSROUTER, DOMAIN, PLATFORMS, STOP_LISTENER
from .router import ARDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
"""Set up AsusRouter platform."""
_LOGGER.debug("Setting up entry")
router = ARDevice(hass, config_entry)
await router.setup()
router.async_on_close(config_entry.add_update_listener(update_listener))
async def async_close_connection(event):
"""Close router connection on HA stop."""
await router.close()
stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, async_close_connection
)
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
ASUSROUTER: router,
STOP_LISTENER: stop_listener,
}
await hass.config_entries.async_forward_entry_setups(
config_entry, PLATFORMS
)
return True
async def async_unload_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
"""Unload AsusRouter config entry."""
_LOGGER.debug("Unloading entry")
unload = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload:
# Close connection
hass.data[DOMAIN][config_entry.entry_id][STOP_LISTENER]()
await hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER].close()
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload
async def update_listener(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> None:
"""Reload on config entry update."""
_LOGGER.debug("Update listener activated")
router = hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER]
if router.update_options(config_entry.options):
await hass.config_entries.async_reload(config_entry.entry_id)
# Example migration function
async def async_migrate_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 4: # noqa: PLR2004
new_options = {**config_entry.options}
new_options["interval_network"] = new_options.pop(
"interval_network_stat", 30
)
config_entry.version = 5
hass.config_entries.async_update_entry(
config_entry, options=new_options
)
_LOGGER.debug("Migration to version %s successful", config_entry.version)
return True
async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
) -> bool:
"""Remove a device."""
# This would actually work and should not provide any issues
_LOGGER.debug("Removing device")
return True
+118
View File
@@ -0,0 +1,118 @@
"""AsusRouter AiMesh module."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from asusrouter.modules.aimesh import AiMeshDevice
from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
class AiMeshNode:
"""Representation of an AiMesh node."""
def __init__(
self,
mac: str,
) -> None:
"""Initialize an AiMesh node."""
self._mac: str = mac
self.native = AiMeshDevice()
self.identity: dict[str, Any] = {
"mac": None,
"ip": None,
"alias": None,
"model": None,
"type": None,
"connected": None,
}
self._extra_state_attributes: dict[str, Any] = {}
@callback
def update(
self,
node_info: AiMeshDevice | None = None,
event_call: Callable[[str, dict[str, Any] | None], None] | None = None,
) -> None:
"""Update AiMesh device."""
if node_info:
self.native = node_info
self._mac = self._extra_state_attributes["mac"] = self.identity[
"mac"
] = format_mac(node_info.mac)
# Online
if node_info.status:
# State: router / node
self._extra_state_attributes["type"] = self.identity[
"type"
] = node_info.type
# IP
self._extra_state_attributes["ip"] = self.identity["ip"] = (
node_info.ip
)
# Alias
self._extra_state_attributes["alias"] = self.identity[
"alias"
] = node_info.alias
# Model
self._extra_state_attributes["model"] = self.identity[
"model"
] = node_info.model
# Product ID
self._extra_state_attributes["product_id"] = (
node_info.product_id
)
# Node level
self._extra_state_attributes["level"] = node_info.level
# Node parent
if node_info.parent == {}:
self._extra_state_attributes["parent"] = {
"connection": "wired",
}
else:
self._extra_state_attributes["parent"] = node_info.parent
# Node config
# self._extra_state_attributes[CONFIG] = node_info.config
# Access point
# self._extra_state_attributes[ACCESS_POINT] = node_info.ap
# Notify reconnect
if self.identity["connected"] is False and callable(
event_call
):
event_call(
"node_reconnected",
self.identity,
)
# Connection status
self.identity["connected"] = True
else:
# Notify disconnect
if self.identity["connected"] is True and callable(event_call):
event_call(
"node_disconnected",
self.identity,
)
# Connection status
self.identity["connected"] = False
elif callable(event_call):
# Notify disconnect
event_call(
"node_disconnected",
self.identity,
)
@property
def mac(self):
"""Return node mac address."""
return self._mac
@property
def extra_state_attributes(self):
"""Return extra state attributes."""
return self._extra_state_attributes
@@ -0,0 +1,189 @@
"""AsusRouter binary sensor module."""
from __future__ import annotations
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
AIMESH,
ASUSROUTER,
CONF_DEFAULT_HIDE_PASSWORDS,
CONF_HIDE_PASSWORDS,
DOMAIN,
MANUFACTURER,
PASSWORD,
STATIC_BINARY_SENSORS,
)
from .dataclass import ARBinarySensorDescription
from .entity import ARBinaryEntity, async_setup_ar_entry
from .helpers import to_unique_id
from .router import AiMeshNode, ARDevice
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AsusRouter binary sensors."""
binary_sensors = STATIC_BINARY_SENSORS.copy()
hide = []
if config_entry.options.get(
CONF_HIDE_PASSWORDS, CONF_DEFAULT_HIDE_PASSWORDS
):
hide.append(PASSWORD)
await async_setup_ar_entry(
hass,
config_entry,
async_add_entities,
binary_sensors,
ARBinarySensor,
hide,
)
router = hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER]
tracked: set = set()
@callback
def update_router():
"""Update the values of the router."""
add_entities(router, async_add_entities, tracked)
router.async_on_close(
async_dispatcher_connect(hass, router.signal_aimesh_new, update_router)
)
update_router()
class ARBinarySensor(ARBinaryEntity, BinarySensorEntity):
"""AsusRouter binary sensor."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: ARDevice,
description: ARBinarySensorDescription,
) -> None:
"""Initialize AsusRouter binary sensor."""
super().__init__(coordinator, router, description)
self.entity_description: ARBinarySensorDescription = description
@callback
def add_entities(
router: ARDevice,
async_add_entities: AddEntitiesCallback,
tracked: set[str],
) -> None:
"""Add new tracker entities from the router."""
new_tracked = []
for mac, node in router.aimesh.items():
if mac in tracked:
continue
new_tracked.append(AMBinarySensor(router, node))
tracked.add(mac)
if new_tracked:
async_add_entities(new_tracked)
class AMBinarySensor(BinarySensorEntity):
"""AsusRouter AiMesh sensor."""
def __init__(
self,
router: ARDevice,
node: AiMeshNode,
) -> None:
"""Initialize AsusRouter AiMesh sensor."""
self._router = router
self._node = node
self._attr_unique_id = to_unique_id(
f"{router.mac}_{AIMESH}_{node.mac}"
)
self._attr_name = f"AiMesh {node.native.model} ({node.native.mac})"
@property
def is_on(self) -> bool:
"""Get the state."""
return self._node.native.status
@property
def device_class(self) -> BinarySensorDeviceClass:
"""Device class."""
return BinarySensorDeviceClass.CONNECTIVITY
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra state attributes."""
return dict(sorted(self._node.extra_state_attributes.items())) or {}
@property
def device_info(self) -> DeviceInfo:
"""Return device info."""
device_info: DeviceInfo = DeviceInfo(
identifiers={
(DOMAIN, self._node.mac),
},
name=self._node.native.model,
model=self._node.native.model,
manufacturer=MANUFACTURER,
sw_version=self._node.native.fw,
)
if self._router.mac != self._node.mac:
device_info = DeviceInfo(
identifiers={
(DOMAIN, self._node.mac),
},
name=self._node.native.model,
model=self._node.native.model,
manufacturer=MANUFACTURER,
sw_version=self._node.native.fw,
via_device=(DOMAIN, self._router.mac),
)
return device_info
@callback
def async_on_demand_update(self) -> None:
"""Update the state."""
if self._node.mac in self._router.aimesh:
self._node = self._router.aimesh[self._node.mac]
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register state update callback."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self._router.signal_aimesh_update,
self.async_on_demand_update,
)
)
+824
View File
@@ -0,0 +1,824 @@
"""AsusRouter bridge module."""
from __future__ import annotations
from collections.abc import Callable
import dataclasses
import logging
from typing import Any
import aiohttp
from asusrouter import AsusRouter
from asusrouter.config import ARConfig, ARConfigKey as ARConfKey
from asusrouter.const import DEFAULT_PORT_HTTP, DEFAULT_PORT_HTTPS
from asusrouter.error import AsusRouterError
from asusrouter.modules.aimesh import AiMeshDevice
from asusrouter.modules.client import AsusClient
from asusrouter.modules.data import AsusData
from asusrouter.modules.homeassistant import (
convert_to_ha_data,
convert_to_ha_sensors,
convert_to_ha_state_bool,
)
from asusrouter.modules.identity import AsusDevice
from asusrouter.modules.parental_control import ParentalControlRule, PCRuleType
from asusrouter.tools.connection import get_cookie_jar
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.device_registry import format_mac
from homeassistant.helpers.update_coordinator import UpdateFailed
from . import helpers
from .const import (
AURA,
BOOTTIME,
CONF_CACHE_TIME,
CONF_DEFAULT_CACHE_TIME,
CONF_DEFAULT_MODE,
CONF_DEFAULT_PORT,
CONF_MODE,
CPU,
DDNS,
DEFAULT_SENSORS,
DOMAIN,
DSL,
FIRMWARE,
GWLAN,
LED,
LIST,
LIST_PORTS,
METHOD,
MODE_SENSORS,
NETWORK,
PARENTAL_CONTROL,
PORT_FORWARDING,
PORTS,
RAM,
SENSORS,
SENSORS_BOOTTIME,
SENSORS_FIRMWARE,
SENSORS_LED,
SENSORS_PARENTAL_CONTROL,
SENSORS_PORT_FORWARDING,
SENSORS_RAM,
STATE,
SYSINFO,
TEMPERATURE,
WLAN,
)
from .const_v1 import DEFAULT_IDENTITY_BRAND, DEFAULT_IDENTITY_NAME
from .modules.aura import aura_to_ha
from .modules.firmware import to_ha as firmware_to_ha
_LOGGER = logging.getLogger(__name__)
class ARBridge:
"""Bridge to the AsusRouter library."""
def __init__(
self,
hass: HomeAssistant,
configs: dict[str, Any],
options: dict[str, Any] | None = None,
) -> None:
"""Initialize bridge to the library."""
self.hass = hass
# Save all the HA configs and options
self._configs = configs.copy()
if options:
self._configs.update(options)
# Get session from HA
# By default, don't verify SSL <- this is a temp solution
# which should be done properly in the future
session = async_create_clientsession(
hass,
verify_ssl=False,
cookie_jar=get_cookie_jar(),
)
# Prepare configs
config = self._get_api_config()
# Initialize API
self._api = self._get_api(self._configs, session, config)
# Switch API to robust mode
# Robust boottime will avoid 1 second jitter due to the raw data
# uncertainty. This can provide up to 1 second overestimation
# of the boottime, but will avoid saving extra data when the
# integration restarts and loses the previous boottime data.
ARConfig.set(ARConfKey.ROBUST_BOOTTIME, True)
self._host = self._configs[CONF_HOST]
self._identity: AsusDevice | None = None
# Define properties
port = self._configs.get(CONF_PORT, None)
if not port:
port = (
DEFAULT_PORT_HTTPS
if self._configs.get(CONF_SSL, False)
else DEFAULT_PORT_HTTP
)
self._configuration_url = (
f"http{'s' if self._configs.get(CONF_SSL, False) else ''}"
f"://{self._host}:{port}"
)
self._identifiers: set[tuple[str, str]] = set()
self._manufacturer = DEFAULT_IDENTITY_BRAND
self._model: str | None = None
self._model_id: str | None = None
self._name: str = DEFAULT_IDENTITY_NAME
self._serial_number: str | None = None
self._sw_version: str | None = None
@staticmethod
def _get_api(
configs: dict[str, Any],
session: aiohttp.ClientSession,
config: dict[ARConfKey, Any],
) -> AsusRouter:
"""Get AsusRouter API."""
return AsusRouter(
hostname=configs[CONF_HOST],
username=configs[CONF_USERNAME],
password=configs[CONF_PASSWORD],
port=configs.get(CONF_PORT, CONF_DEFAULT_PORT),
use_ssl=configs[CONF_SSL],
cache_time=configs.get(CONF_CACHE_TIME, CONF_DEFAULT_CACHE_TIME),
session=session,
config=config,
)
def _get_api_config(self) -> dict[ARConfKey, Any]:
"""Get configuration for AsusRouter instance."""
return {
# Enable automatic temperature fix
ARConfKey.OPTIMISTIC_TEMPERATURE: True,
# Disable log warning message
ARConfKey.NOTIFIED_OPTIMISTIC_TEMPERATURE: True,
}
@property
def api(self) -> AsusRouter:
"""Return API."""
return self._api
@property
def configuration_url(self) -> str:
"""Return device configuration URL."""
return self._configuration_url
@property
def connected(self) -> bool:
"""Return connection state."""
return self._api.connected
@property
def identifiers(self) -> set[tuple[str, str]]:
"""Return device identifiers."""
return self._identifiers
@property
def identity(self) -> AsusDevice | None:
"""Return device identity."""
return self._identity
@property
def manufacturer(self) -> str:
"""Return device manufacturer."""
return self._manufacturer
@property
def model(self) -> str | None:
"""Return device model."""
return self._model
@property
def model_id(self) -> str | None:
"""Return device model ID."""
return self._model_id
@property
def name(self) -> str:
"""Return device name."""
return self._name
@property
def serial_number(self) -> str | None:
"""Return device serial number."""
return self._serial_number
@property
def sw_version(self) -> str | None:
"""Return device software version."""
return self._sw_version
# --------------------
# Connection -->
# --------------------
async def async_connect(self) -> None:
"""Connect to the device."""
_LOGGER.debug("Connecting to the API")
await self.api.async_connect()
identity = await self.api.async_get_identity()
self._identity = identity
# Set properties
self._identifiers = set()
if identity.mac is not None:
self._identifiers.add((DOMAIN, format_mac(identity.mac)))
if identity.serial is not None:
self._identifiers.add((DOMAIN, identity.serial))
self._manufacturer = identity.brand
self._model = identity.model
self._model_id = identity.product_id
self._name = identity.model or DEFAULT_IDENTITY_NAME
self._serial_number = identity.serial
self._sw_version = (
str(identity.firmware) if identity.firmware else None
)
async def async_disconnect(self) -> None:
"""Disconnect from the device."""
_LOGGER.debug("Disconnecting from the API")
await self.api.async_disconnect()
async def async_clean(self) -> None:
"""Cleanup."""
_LOGGER.debug("Cleaning up")
await self.api.async_cleanup()
# --------------------
# <-- Connection
# --------------------
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)
return {
group: details
for group, details in sensors.items()
if group in available
}
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
"""Get available sensors."""
sensors = {
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,
},
DDNS: {
SENSORS: await self._get_sensors_modern(AsusData.DDNS),
METHOD: self._get_data_ddns,
},
DSL: {
SENSORS: await self._get_sensors_modern(AsusData.DSL),
METHOD: self._get_data_dsl,
},
FIRMWARE: {
SENSORS: SENSORS_FIRMWARE,
METHOD: self._get_data_firmware,
},
GWLAN: {
SENSORS: await self._get_sensors_modern(AsusData.GWLAN),
METHOD: self._get_data_gwlan,
},
LED: {
SENSORS: SENSORS_LED,
METHOD: self._get_data_led,
},
NETWORK: {
SENSORS: await self._get_sensors_modern(AsusData.NETWORK),
METHOD: self._get_data_network,
},
"ovpn_client": {
SENSORS: await self._get_sensors_modern(
AsusData.OPENVPN_CLIENT
),
METHOD: self._get_data_ovpn_client,
},
"ovpn_server": {
SENSORS: await self._get_sensors_ovpn_server(),
METHOD: self._get_data_ovpn_server,
},
PARENTAL_CONTROL: {
SENSORS: SENSORS_PARENTAL_CONTROL,
METHOD: self._get_data_parental_control,
},
PORT_FORWARDING: {
SENSORS: SENSORS_PORT_FORWARDING,
METHOD: self._get_data_port_forwarding,
},
PORTS: {
SENSORS: await self._get_sensors_ports(),
METHOD: self._get_data_ports,
},
RAM: {SENSORS: SENSORS_RAM, METHOD: self._get_data_ram},
SYSINFO: {
SENSORS: await self._get_sensors_modern(AsusData.SYSINFO),
METHOD: self._get_data_sysinfo,
},
TEMPERATURE: {
SENSORS: await self._get_sensors_modern(AsusData.TEMPERATURE),
METHOD: self._get_data_temperature,
},
"wan": {
SENSORS: await self._get_sensors_modern(AsusData.WAN),
METHOD: self._get_data_wan,
},
"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
),
METHOD: self._get_data_wireguard_server,
},
WLAN: {
SENSORS: await self._get_sensors_modern(AsusData.WLAN),
METHOD: self._get_data_wlan,
},
}
# Cleanup sensors if needed
return await self.async_cleanup_sensors(sensors)
# GET DATA FROM DEVICE ->
# General method
async def _get_data(
self,
datatype: AsusData,
process: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
force: bool = False,
) -> dict[str, Any]:
"""Get data from the device. This is a generic method."""
try:
raw = await self.api.async_get_data(datatype, force=force)
if raw is None:
raw = {}
if process is not None:
return process(raw)
return self._process_data(raw)
except AsusRouterError as ex:
raise UpdateFailed(ex) from ex
async def _get_data_modern(
self,
datatype: AsusData,
force: bool = False,
) -> dict[str, Any]:
"""Get data from the device. This is a generic method."""
try:
raw = await self.api.async_get_data(datatype, force=force)
return self._process_data_modern(raw)
except AsusRouterError as ex:
raise UpdateFailed(ex) from ex
# AiMesh nodes
async def async_get_aimesh_nodes(self) -> dict[str, AiMeshDevice]:
"""Get dict of AiMesh nodes."""
return await self._get_data(AsusData.AIMESH)
# Connected devices
async def async_get_clients(self) -> dict[str, AsusClient]:
"""Get clients."""
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."""
return await self._get_data(AsusData.BOOTTIME)
async def _get_data_cpu(self) -> dict[str, Any]:
"""Get CPU data from the device."""
return await self._get_data(AsusData.CPU)
async def _get_data_ddns(self) -> dict[str, Any]:
"""Get DDNS data from the device."""
return await self._get_data_modern(AsusData.DDNS)
async def _get_data_dsl(self) -> dict[str, Any]:
"""Get DSL data from the device."""
return await self._get_data_modern(AsusData.DSL)
async def _get_data_firmware(self) -> dict[str, Any]:
"""Get firmware data from the device."""
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."""
return await self._get_data(AsusData.GWLAN)
async def _get_data_led(self) -> dict[str, Any]:
"""Get light data from the device."""
return await self._get_data(AsusData.LED)
async def _get_data_network(self) -> dict[str, Any]:
"""Get network data from device."""
return await self._get_data(AsusData.NETWORK)
async def _get_data_ovpn_client(self) -> dict[str, Any]:
"""Get OpenVPN client data from the device."""
return await self._get_data_modern(AsusData.OPENVPN_CLIENT)
async def _get_data_ovpn_server(self) -> dict[str, Any]:
"""Get OpenVPN server data from the device."""
return await self._get_data(AsusData.OPENVPN_SERVER)
async def _get_data_parental_control(self) -> dict[str, Any]:
"""Get parental control data from the device."""
return await self._get_data(
AsusData.PARENTAL_CONTROL,
self._process_data_parental_control,
)
async def _get_data_port_forwarding(self) -> dict[str, Any]:
"""Get port forwarding data from the device."""
return await self._get_data(
AsusData.PORT_FORWARDING,
self._process_data_port_forwarding,
)
async def _get_data_ports(self) -> dict[str, dict[str, int]]:
"""Get ports data from the device."""
return await self._get_data(AsusData.PORTS, self._process_data_ports)
async def _get_data_ram(self) -> dict[str, Any]:
"""Get RAM data from the device."""
return await self._get_data(AsusData.RAM)
async def _get_data_sysinfo(self) -> dict[str, Any]:
"""Get sysinfo data from the device."""
return await self._get_data(AsusData.SYSINFO)
async def _get_data_temperature(self) -> dict[str, Any]:
"""Get temperarture data from the device."""
return await self._get_data_modern(AsusData.TEMPERATURE)
async def _get_data_wan(self) -> dict[str, Any]:
"""Get WAN data from the device."""
return await self._get_data_modern(AsusData.WAN)
async def _get_data_wireguard_client(self) -> dict[str, Any]:
"""Get WireGuard client data from the device."""
return await self._get_data_modern(AsusData.WIREGUARD_CLIENT)
async def _get_data_wireguard_server(self) -> dict[str, Any]:
"""Get WireGuard server data from the device."""
return await self._get_data_modern(AsusData.WIREGUARD_SERVER)
async def _get_data_wlan(self) -> dict[str, Any]:
"""Get WLAN data from the device."""
return await self._get_data(AsusData.WLAN)
# <- GET DATA FROM DEVICE
# PROCESS DATA ->
@staticmethod
def _process_data(raw: dict[str, Any]) -> dict[str, Any]:
"""Process data received from the device. This is a generic method."""
return helpers.as_dict(helpers.flatten_dict(raw))
@staticmethod
def _process_data_modern(raw: dict[str, Any]) -> dict[str, Any]:
"""Process `ovpn_client` data."""
return helpers.clean_dict(convert_to_ha_data(raw))
@staticmethod
def _process_data_parental_control(raw: dict[str, Any]) -> dict[str, Any]:
"""Process `parental control` data."""
rules_list = []
rules = raw.get("rules")
if rules is not None:
for rule in raw["rules"]:
device = dataclasses.asdict(raw["rules"][rule])
device.pop("timemap")
device["type"] = device["type"].name.lower()
rules_list.append(device)
data = convert_to_ha_data(raw)
data["list"] = rules_list.copy()
data["block_all"] = convert_to_ha_state_bool(raw.get("block_all"))
return data
@staticmethod
def _process_data_port_forwarding(raw: dict[str, Any]) -> dict[str, Any]:
"""Process `port forwarding` data."""
data: dict[str, Any] = {}
data[STATE] = convert_to_ha_state_bool(raw.get(STATE))
devices = []
rules = raw.get("rules")
if rules is not None:
for rule in rules:
device = dataclasses.asdict(rule)
devices.append(device)
data[LIST] = devices.copy()
return data
@staticmethod
def _process_data_ports(raw: dict[str, Any]) -> dict[str, Any]:
"""Process `ports` data."""
data: dict[str, Any] = {}
for port_type in LIST_PORTS:
# Mark port type as disconnected
data[port_type] = False
# Skip if no data is provided from API
if port_type not in raw:
continue
# Create ports list
data[f"{port_type}_{LIST}"] = {}
ports_by_type: dict[int, dict[str, Any]] = raw[port_type]
for port_number, port_description in ports_by_type.items():
# Mark port type connected
if port_description.get(STATE):
data[port_type] = True
# Copy port data to the list
data[f"{port_type}_{LIST}"][port_number] = port_description
return data
# <- PROCESS DATA
# GET SENSORS LIST ->
async def _get_sensors(
self,
datatype: AsusData,
process: Callable[[dict[str, Any]], list[str]] | None = None,
sensor_type: str | None = None,
defaults: bool = False,
) -> list[str]:
"""Get the available sensors. This is a generic method."""
sensors = []
try:
data = await self.api.async_get_data(datatype)
_LOGGER.debug(
"Raw `%s` sensors of type (%s): %s", datatype, type(data), data
)
sensors = (
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:
if sensor_type in DEFAULT_SENSORS and defaults:
sensors = DEFAULT_SENSORS[sensor_type]
_LOGGER.debug(
"Cannot get available `%s` sensors with exception: %s. \
Will use the following list: {sensors}",
sensor_type,
ex,
)
return sensors
async def _get_sensors_modern(self, datatype: AsusData) -> list[str]:
"""Get the available sensors. This is a generic method."""
sensors = []
try:
data = await self.api.async_get_data(datatype)
_LOGGER.debug(
"Raw `%s` sensors of type (%s): %s", datatype, type(data), data
)
sensors = convert_to_ha_sensors(data, datatype)
_LOGGER.debug(
"Available `%s` sensors: %s", datatype.value, sensors
)
except AsusRouterError as ex:
if datatype.value in DEFAULT_SENSORS:
sensors = DEFAULT_SENSORS[datatype.value]
_LOGGER.debug(
"Cannot get available `%s` sensors with exception: %s. \
Will use the following list: {sensors}",
datatype.value,
ex,
)
return sensors
async def _get_sensors_ovpn_server(self) -> list[str]:
"""Get the available OpenVPN server sensors."""
return await self._get_sensors(
AsusData.OPENVPN_SERVER,
self._process_sensors_ovpn_server,
sensor_type="ovpn_server",
)
async def _get_sensors_ports(self) -> list[str]:
"""Get the available ports sensors."""
return await self._get_sensors(
AsusData.PORTS,
self._process_sensors_ports,
sensor_type=PORTS,
)
# <- GET SENSORS LIST
# PROCESS SENSORS LIST->
@staticmethod
def _process_sensors(raw: dict[str, Any]) -> list[str]:
"""Process sensors from the backend library. This is a generic method.
For the most of sensors which are returned as nested dicts
and only the top level keys are the one we are looking for.
"""
flat = helpers.as_dict(helpers.flatten_dict(raw))
return helpers.list_from_dict(flat)
@staticmethod
def _process_sensors_ports(raw: dict[str, Any]) -> list[str]:
"""Process ports sensors."""
sensors = []
for port_type in LIST_PORTS:
sensors.append(port_type)
sensors.append(f"{port_type}_{LIST}")
return sensors
@staticmethod
def _process_sensors_ovpn_server(raw: dict[str, Any]) -> list[str]:
"""Process OpenVPN server sensors."""
return convert_to_ha_sensors(raw, AsusData.OPENVPN_SERVER)
# <- PROCESS SENSORS LIST
# --------------------
# Services -->
# --------------------
def _pc_device2rule(
self, device: dict[str, Any], rule_type: PCRuleType
) -> ParentalControlRule | None:
"""Convert device to parental control rule."""
mac = device.get("mac")
if mac is None:
return None
return ParentalControlRule(
mac=mac.upper(),
name=device.get("name", ""),
type=rule_type,
)
async def async_pc_rule(self, **kwargs: Any) -> bool: # noqa: C901, PLR0912
"""Change parental control rule(s)."""
# Get the passed data
raw = kwargs.get("raw")
# Abort if no data is passed
if raw is None:
return False
# Get the state to set
state = raw.get("state", None)
match state:
case a if a in ("disable", "allow"):
rule_type = PCRuleType.DISABLE
case "block":
rule_type = PCRuleType.BLOCK
case "remove":
rule_type = PCRuleType.REMOVE
case _:
_LOGGER.warning("Unknown parental control state: %s", state)
return False
# Get the targets to set
devices = raw.get("devices", [])
entities = raw.get("entities", [])
# Prepare the rules list
rules_to_set = []
# Process entities if any
if len(entities) > 0:
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] = helpers.as_dict(
reg_value.capabilities
)
devices.append(capabilities)
# Convert devices to rules
for device in devices:
rule = self._pc_device2rule(device, rule_type)
if rule is not None:
rules_to_set.append(rule)
# Set the rules
for rule in rules_to_set:
result = await self.api.async_set_state(rule)
if result is True:
_LOGGER.debug("Parental control rule set: %s", rule)
else:
_LOGGER.warning("Cannot set parental control rule: %s", rule)
return True
# --------------------
# <-- Services
# --------------------
# --------------------
+108
View File
@@ -0,0 +1,108 @@
"""AsusRouter button module."""
from __future__ import annotations
import logging
from typing import Any
from asusrouter.modules.state import AsusState
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
ASUSROUTER,
CONF_MODE,
DOMAIN,
ROUTER,
STATIC_BUTTONS,
STATIC_BUTTONS_OPTIONAL,
)
from .dataclass import ARButtonDescription
from .helpers import to_unique_id
from .router import ARDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AsusRouter buttons."""
buttons = STATIC_BUTTONS.copy()
router: ARDevice = hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER]
entities = []
if config_entry.options.get(CONF_MODE) == ROUTER:
buttons.extend(STATIC_BUTTONS_OPTIONAL)
for button in buttons:
try:
entities.append(ARButton(router, button))
except Exception as ex: # noqa: BLE001
_LOGGER.warning(ex)
async_add_entities(entities)
class ARButton(ButtonEntity):
"""AsusRouter button."""
def __init__(
self,
router: ARDevice,
description: ARButtonDescription,
) -> None:
"""Initialize AsusRouter button."""
self.router = router
self.api = router.bridge.api
self._attr_device_info = router.device_info
self._attr_name = f"{router._conf_name} {description.name}"
self._attr_unique_id = to_unique_id(f"{router.mac}_{description.name}")
self._attr_capability_attributes = description.capabilities
self._state = description.state
self._state_args = description.state_args
self._state_expect_modify = description.state_expect_modify
if description.icon:
self._attr_icon = description.icon
async def async_press(
self,
**kwargs: Any,
) -> None:
"""Press button."""
kwargs = self._state_args if self._state_args is not None else {}
await self._set_state(
state=self._state,
expect_modify=self._state_expect_modify,
**kwargs,
)
async def _set_state(
self,
state: AsusState,
expect_modify: bool = False,
**kwargs: Any,
) -> None:
"""Set switch state."""
try:
_LOGGER.debug("Pressing %s", state)
result = await self.api.async_set_state(
state=state, expect_modify=expect_modify, **kwargs
)
if not result:
_LOGGER.debug("Didn't manage to press %s", state)
except Exception as ex: # noqa: BLE001
_LOGGER.error("Pressing %s caused an exception: %s", state, ex)
+214
View File
@@ -0,0 +1,214 @@
"""AsusRouter Client module."""
from __future__ import annotations
from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any
from asusrouter.modules.client import (
AsusClient,
AsusClientConnection,
AsusClientConnectionWlan,
AsusClientDescription,
)
from asusrouter.modules.connection import ConnectionState, ConnectionType
from asusrouter.modules.homeassistant import convert_to_ha_state_bool
from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
from .helpers import clean_dict
class ARClient:
"""AsussRouter Client class."""
def __init__(
self,
mac: str,
name: str | None = None,
):
"""Initialize the client."""
self._mac = mac
self._name = name
self.device: bool = False
# To be recieved from the device
# Client description
self.description: AsusClientDescription | None = None
# Connection description -
# AsusClientConnection / AsusClientConnectionWlan
self.connection: AsusClientConnection | None = None
# To be generated for other parts of the integration
self._identity: dict[str, Any] | None = None
self._extra_state_attributes: dict[str, Any] = {}
# 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: datetime | None = None
@callback
def update(
self,
client_info: AsusClient | None = None,
consider_home: int = 0,
event_call: Callable[[str, dict[str, Any] | None], None] | None = None,
) -> None:
"""Update client information."""
utc_now: datetime = datetime.now(UTC)
state: ConnectionState | None = None
# If client information is provided
if client_info is not None:
# Description
self.description = client_info.description
# Connection
self.connection = client_info.connection
# Connected state
state = client_info.state
self._identity = self.generate_identity(state)
self._extra_state_attributes = self.generate_extra_state_attributes()
# If is connected
if state is ConnectionState.CONNECTED:
# Update last activity
self._last_activity = utc_now
# Device was disconnected, now it has reconnected
# Fire event and connected callback
if (
self._state is ConnectionState.DISCONNECTED
and event_call is not None
):
event_call("device_reconnected", self.identity)
# Update connection status
self._state = ConnectionState.CONNECTED
# If was connected, check if it's time to mark it as disconnected
# when we are above the consider home time
elif (
self._state is ConnectionState.CONNECTED
and self._last_activity is not None
and (utc_now - self._last_activity).total_seconds() > consider_home
):
# Update connection status
self._state = ConnectionState.DISCONNECTED
# Fire event
if event_call is not None:
event_call(
"device_disconnected",
self.identity,
)
def generate_identity(
self, state: ConnectionState | None
) -> dict[str, Any]:
"""Generate client identity."""
identity: dict[str, Any] = {
"mac": self.mac_address,
"ip": self.ip_address,
"name": self.name,
}
if isinstance(self.connection, AsusClientConnection):
# 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
)
if isinstance(self.connection, AsusClientConnectionWlan):
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)
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["last_activity"] = self._last_activity
if isinstance(self.description, AsusClientDescription):
attributes["vendor"] = self.description.vendor
if isinstance(self.connection, AsusClientConnection):
attributes["ip_type"] = self.connection.ip_method
attributes["internet_mode"] = self.connection.internet_mode
attributes["internet"] = self.connection.internet_state
if isinstance(self.connection, AsusClientConnectionWlan):
attributes["rssi"] = self.connection.rssi
attributes["rx_speed"] = self.connection.rx_speed
attributes["tx_speed"] = self.connection.tx_speed
return clean_dict(attributes)
@property
def state(self) -> bool | None:
"""Return if the device is connected."""
return convert_to_ha_state_bool(self._state)
@property
def ip_address(self) -> str | None:
"""Return IP address."""
return (
self.connection.ip_address if self.connection is not None else None
)
@property
def mac_address(self) -> str:
"""Return MAC address."""
return self._mac
@property
def name(self) -> str | None:
"""Return name."""
return (
self.description.name
if self.description is not None
else self._name
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra state attributes."""
return self._extra_state_attributes
@property
def identity(self) -> dict[str, Any] | None:
"""Return identity."""
return self._identity
+58
View File
@@ -0,0 +1,58 @@
"""AsusRouter compilers module."""
from __future__ import annotations
from .const import (
CONF_LABELS_INTERFACES,
MAP_NETWORK_TEMP,
NAME,
NETWORK,
SENSORS_PARAM_NETWORK,
)
from .dataclass import ARSensorDescription
def list_sensors_network(
interfaces: list[str] | None = None,
) -> list[ARSensorDescription]:
"""Compile a list of network sensors."""
sensors: list[ARSensorDescription] = []
if not interfaces or len(interfaces) < 1:
return sensors
for intf in interfaces:
interface = MAP_NETWORK_TEMP.get(intf, intf)
for sensor_type, data in SENSORS_PARAM_NETWORK.items():
key = f"{interface}_{sensor_type}"
sensors.append(
ARSensorDescription(
key=key,
key_group=NETWORK,
name=f"{CONF_LABELS_INTERFACES.get(interface, interface)} "
f"{data[NAME]}"
or None,
icon=data["icon"] or None,
state_class=data["state_class"] or None,
device_class=data["device_class"] or None,
native_unit_of_measurement=data[
"native_unit_of_measurement"
]
or None,
suggested_unit_of_measurement=data[
"suggested_unit_of_measurement"
]
or None,
suggested_display_precision=data[
"suggested_display_precision"
]
or None,
entity_registry_enabled_default=data[
"entity_registry_enabled_default"
]
or True,
)
)
return sensors
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
"""AsusRouter constants module / v1."""
from __future__ import annotations
from typing import Final
DEFAULT_IDENTITY_BRAND: Final = "ASUSTek"
DEFAULT_IDENTITY_NAME: Final = "AsusRouter"
+85
View File
@@ -0,0 +1,85 @@
"""AsusRouter dataclass module."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from asusrouter.modules.state import AsusState, AsusStateNone
from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription,
)
from homeassistant.components.button import ButtonEntityDescription
from homeassistant.components.light import LightEntityDescription
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.components.switch import SwitchEntityDescription
from homeassistant.components.update import UpdateEntityDescription
from homeassistant.helpers.entity import EntityDescription
@dataclass
class AREntityDescription(EntityDescription):
"""Describe AsusRouter entity."""
capabilities: dict[str, Any] | None = None
key_group: str = ""
value: Callable[[Any], Any] = lambda val: val
extra_state_attributes: dict[str, Any] | None = None
@dataclass
class ARBinaryDescription(AREntityDescription, BinarySensorEntityDescription):
"""Describe AsusRouter binary entity."""
icon_on: str | None = None
icon_off: str | None = None
@dataclass
class ARSensorDescription(AREntityDescription, SensorEntityDescription):
"""Describe AsusRouter sensor."""
factor: int | None = None
precision: int = 3
@dataclass
class ARBinarySensorDescription(
ARBinaryDescription, BinarySensorEntityDescription
):
"""Describe AsusRouter sensor."""
@dataclass
class ARLightDescription(ARBinaryDescription, LightEntityDescription):
"""Describe AsusRouter light."""
@dataclass
class ARSwitchDescription(AREntityDescription, SwitchEntityDescription):
"""Describe AsusRouter switch."""
icon_on: str | None = None
icon_off: str | None = None
state_on: AsusState = AsusStateNone.NONE
state_on_args: dict[str, Any] | None = None
state_off: AsusState = AsusStateNone.NONE
state_off_args: dict[str, Any] | None = None
state_expect_modify: bool = False
@dataclass
class ARButtonDescription(AREntityDescription, ButtonEntityDescription):
"""Describe AsusRouter button."""
state: AsusState = AsusStateNone.NONE
state_args: dict[str, Any] | None = None
state_expect_modify: bool = False
@dataclass
class ARUpdateDescription(AREntityDescription, UpdateEntityDescription):
"""Describe AsusRouter update."""
@@ -0,0 +1,194 @@
"""AsusRouter device tracker module."""
from __future__ import annotations
import logging
from typing import Any
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
from .const import (
ASUSROUTER,
CONF_DEFAULT_TRACK_DEVICES,
CONF_TRACK_DEVICES,
DEFAULT_DEVICE_NAME,
DOMAIN,
)
from .router import ARDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up device tracker for AsusRouter component."""
# If device tracking is disabled
if (
config_entry.options.get(
CONF_TRACK_DEVICES, CONF_DEFAULT_TRACK_DEVICES
)
is False
):
return
router = hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER]
tracked: set = set()
@callback
def update_router():
"""Update the values of the router."""
add_entities(router, async_add_entities, tracked)
router.async_on_close(
async_dispatcher_connect(hass, router.signal_device_new, update_router)
)
update_router()
@callback
def add_entities(
router: ARDevice,
async_add_entities: AddEntitiesCallback,
tracked: set[str],
) -> None:
"""Add new tracker entities from the router."""
new_tracked = []
for mac, device in router.devices.items():
if mac in tracked:
continue
new_tracked.append(ARDeviceEntity(router, device))
tracked.add(mac)
if new_tracked:
async_add_entities(new_tracked)
class ARDeviceEntity(ScannerEntity):
"""Connected device class."""
_attr_should_poll = False
def __init__(
self,
router: ARDevice,
client: ARClient,
) -> None:
"""Initialize connected device."""
self._router = router
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": 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: str | None
) -> DeviceInfo:
"""Compile device info."""
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
default_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."""
return SourceType.ROUTER
@property
def is_connected(self) -> bool | None:
"""Device status."""
return self._client.state
@property
def ip_address(self) -> str | None:
"""Device IP address."""
return self._client.ip_address
@property
def mac_address(self) -> str:
"""Device MAC address."""
return self._client.mac_address
@property
def hostname(self) -> str | None:
"""Device hostname."""
return self._client.name
@property
def icon(self) -> str:
"""Device icon."""
return (
"mdi:lan-connect" if self._client.state else "mdi:lan-disconnect"
)
@property
def unique_id(self) -> str | None:
"""Return unique ID of the entity."""
return self._attr_unique_id
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra state attributes."""
return dict(sorted(self._client.extra_state_attributes.items())) or {}
@callback
def async_on_demand_update(self) -> None:
"""Update the state."""
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:
"""Register state update callback."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self._router.signal_device_update,
self.async_on_demand_update,
)
)
@@ -0,0 +1,92 @@
"""AsusRouter diagnostics module."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import (
ASUSROUTER,
DEVICE_ATTRIBUTE_LAST_ACTIVITY,
DOMAIN,
TO_REDACT,
TO_REDACT_ATTRS,
TO_REDACT_DEV,
TO_REDACT_STATE,
)
from .router import ARDevice
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: ConfigEntry,
) -> dict[str, dict[str, Any]]:
"""Return diagnostics for a config entry."""
data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)}
router: ARDevice = hass.data[DOMAIN][entry.entry_id][ASUSROUTER]
# Gather information how this device is represented in Home Assistant
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
hass_device = device_registry.async_get_device(
identifiers=router.device_info["identifiers"]
)
if not hass_device:
return data
data["device"] = {
**async_redact_data(hass_device.dict_repr, TO_REDACT_DEV),
"entities": {},
"tracked_devices": [],
}
hass_entities = er.async_entries_for_device(
entity_registry,
device_id=hass_device.id,
include_disabled_entities=True,
)
for entity_entry in hass_entities:
state = hass.states.get(entity_entry.entity_id)
state_dict = None
if state:
state_dict = dict(state.as_dict())
# The entity_id is already provided at root level.
state_dict.pop("entity_id", None)
# The context doesn't provide useful information in this case.
state_dict.pop("context", None)
# Remove sensitive info from attributes.
if "attributes" in state_dict:
state_dict["attributes"] = async_redact_data(
dict(state_dict["attributes"]), TO_REDACT_ATTRS
)
# Remove sensitive info from sensors states.
if entity_entry.original_name in TO_REDACT_STATE:
state_dict = async_redact_data(state_dict, "state")
data["device"]["entities"][entity_entry.entity_id] = {
**async_redact_data(
entity_entry.as_partial_dict,
TO_REDACT,
),
"state": state_dict,
}
for device in router.devices.values():
data["device"]["tracked_devices"].append(
{
"name": device.name,
"ip_address": device.ip_address,
"last_activity": device.extra_state_attributes.get(
DEVICE_ATTRIBUTE_LAST_ACTIVITY, None
),
}
)
return data
+192
View File
@@ -0,0 +1,192 @@
"""AsusRouter entity module."""
from __future__ import annotations
import logging
from typing import Any
from asusrouter.modules.homeassistant import convert_to_ha_state_bool
from asusrouter.modules.state import AsusState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import ASUSROUTER, COORDINATOR, DOMAIN
from .dataclass import (
ARBinaryDescription,
AREntityDescription,
ARSwitchDescription,
)
from .helpers import to_unique_id
from .router import ARDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_ar_entry( # noqa: PLR0913
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
sensors: list[AREntityDescription],
sensor_class: type[AREntity],
hide: list[str] | None = None,
) -> None:
"""Set up AsusRouter entities."""
router: ARDevice = hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER]
entities = []
if not hide:
hide = []
for sensor_data in router.sensor_coordinator.values():
coordinator = sensor_data[COORDINATOR]
for sensor_description in sensors:
try:
sensor_type = sensor_description.key_group
# Make sure extra state attributes are dict
if not sensor_description.extra_state_attributes:
sensor_description.extra_state_attributes = {}
if (
sensor_type in sensor_data
and sensor_description.key in sensor_data[sensor_type]
):
# Hide protected values
sensor_description.extra_state_attributes = {
key: value
for key, value in sensor_description.extra_state_attributes.items() # noqa: E501
if value not in hide
}
entities.append(
sensor_class(coordinator, router, sensor_description)
)
except Exception as ex: # noqa: BLE001
_LOGGER.warning(
"Got an exception when creating entities: %s. "
"Please, report this. Sensor description: %s",
ex,
sensor_description,
)
async_add_entities(entities)
class AREntity(CoordinatorEntity):
"""AsusRouter entity."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: ARDevice,
description: AREntityDescription,
) -> None:
"""Initialize AsusRouter entity."""
super().__init__(coordinator)
self.router = router
self.api = router.bridge.api
self.coordinator = coordinator
self._attr_name = f"{router._conf_name} {description.name}"
self._attr_unique_id = to_unique_id(f"{router.mac}_{description.name}")
self._attr_device_info = router.device_info
self._attr_capability_attributes = description.capabilities
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra state attributes."""
# Check if description is of the needed class
if not isinstance(self.entity_description, AREntityDescription):
return {}
description = self.entity_description
_attributes = description.extra_state_attributes
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:
if attr in self.coordinator.data:
attributes[_attributes[attr]] = self.coordinator.data[attr]
return dict(sorted(attributes.items())) or {}
class ARBinaryEntity(AREntity):
"""AsusRouter binary entity."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: ARDevice,
description: AREntityDescription,
) -> None:
"""Initialize AsusRouter binary entity."""
super().__init__(coordinator, router, description)
if isinstance(description, ARBinaryDescription | ARSwitchDescription):
self._icon_onoff = bool(
description.icon_on and description.icon_off
)
@property
def is_on(self) -> bool | None:
"""Get the state."""
return convert_to_ha_state_bool(
self.coordinator.data.get(self.entity_description.key)
)
@property
def icon(self) -> str | None:
"""Get the icon."""
if (
isinstance(
self.entity_description,
ARBinaryDescription | ARSwitchDescription,
)
and self._icon_onoff
):
if self.is_on:
return self.entity_description.icon_on
return self.entity_description.icon_off
if self.entity_description.icon:
return self.entity_description.icon
return None
async def _set_state(
self,
state: AsusState,
expect_modify: bool = False,
**kwargs: Any,
) -> None:
"""Set switch state."""
try:
_LOGGER.debug("Setting state to %s", state)
result = await self.api.async_set_state(
state=state, expect_modify=expect_modify, **kwargs
)
await self.coordinator.async_request_refresh()
if not result:
_LOGGER.debug("State was not set!")
except Exception as ex: # noqa: BLE001
_LOGGER.error("Unable to set state with an exception: %s", ex)
+52
View File
@@ -0,0 +1,52 @@
"""AsusRouter helpers module."""
from __future__ import annotations
import re
from typing import Any
def clean_dict(raw: dict[str, Any]) -> dict[str, Any]:
"""Clean dictionary from None values."""
return {
k: v for k, v in raw.items() if v is not None or k.endswith("state")
}
def flatten_dict(obj: Any, keystring: str = "", delimiter: str = "_"):
"""Flatten dictionary."""
if isinstance(obj, dict):
keystring = keystring + delimiter if keystring else keystring
for key in obj:
yield from flatten_dict(obj[key], keystring + str(key))
else:
yield keystring, obj
def as_dict(pyobj):
"""Return generator object as dictionary."""
return dict(pyobj)
def list_from_dict(raw: dict[str, Any]) -> list[str]:
"""Return dictionary keys as list."""
return list(raw.keys())
def to_unique_id(raw: str):
"""Convert string to unique_id."""
string = (
re.sub(r"(?<=[a-z0-9:_])(?=[A-Z])|[^a-zA-Z0-9:_]", " ", raw)
.strip()
.replace(" ", "_")
)
result = "".join(string.lower())
while "__" in result:
result = result.replace("__", "_")
return result
+190
View File
@@ -0,0 +1,190 @@
"""AsusRouter light module."""
from __future__ import annotations
import logging
from typing import Any
from asusrouter.modules.aura import AsusAura
from asusrouter.modules.color import ColorRGB, scale_value_int
from asusrouter.modules.led import AsusLED
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_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,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AsusRouter lights."""
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):
"""AsusRouter LED light."""
_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: ARDevice,
description: ARLightDescription,
) -> None:
"""Initialize AsusRouter LED light."""
super().__init__(coordinator, router, description)
self.entity_description: ARLightDescription = description
async def async_turn_on(
self,
**kwargs: Any,
) -> None:
"""Turn on LED."""
await self._set_state(AsusLED.ON)
async def async_turn_off(
self,
**kwargs: Any,
) -> None:
"""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)
@@ -0,0 +1,13 @@
{
"domain": "asusrouter",
"name": "AsusRouter",
"codeowners": ["@vaskivskyi"],
"config_flow": true,
"documentation": "https://asusrouter.vaskivskyi.com",
"integration_type": "hub",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/Vaskivskyi/ha-asusrouter/issues",
"loggers": ["asusrouter"],
"requirements": ["asusrouter>=1.21.0"],
"version": "0.40.1"
}
@@ -0,0 +1 @@
"""Complex modules for AsusRouter integration."""
@@ -0,0 +1,131 @@
"""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 asusrouter.modules.color import ColorRGBB
from asusrouter.tools.converters import scale_value_int
from homeassistant.components.light import ColorMode
from homeassistant.const import EntityCategory
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)
]
@@ -0,0 +1,31 @@
"""Firmware module for AsusRouter integration."""
from __future__ import annotations
from typing import Any
def to_ha(data: dict[str, Any] | None) -> 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"),
}
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
"""AsusRouter sensor module."""
from __future__ import annotations
import logging
from homeassistant.components.sensor import SensorEntity
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 .compilers import list_sensors_network
from .const import CONF_INTERFACES, STATIC_SENSORS
from .dataclass import ARSensorDescription
from .entity import AREntity, async_setup_ar_entry
from .router import ARDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AsusRouter sensors."""
sensors = STATIC_SENSORS.copy()
interfaces = config_entry.options.get(CONF_INTERFACES, [])
if len(interfaces) > 0:
_LOGGER.debug(
"Interfaces selected: %s. Initializing sensors", interfaces
)
sensors.extend(
list_sensors_network(
config_entry.options[CONF_INTERFACES],
)
)
await async_setup_ar_entry(
hass, config_entry, async_add_entities, sensors, ARSensor
)
class ARSensor(AREntity, SensorEntity):
"""AsusRouter sensor."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: ARDevice,
description: ARSensorDescription,
) -> None:
"""Initialize AsusRouter sensor."""
super().__init__(coordinator, router, description)
self.entity_description: ARSensorDescription = description
@property
def native_value(
self,
) -> float | str | None:
"""Return state."""
description = self.entity_description
return self.coordinator.data.get(description.key)
@@ -0,0 +1,32 @@
device_internet_access:
fields:
entities:
required: false
selector:
entity:
integration: asusrouter
domain: device_tracker
multiple: true
state:
required: true
selector:
select:
translation_key: set_pc_rule
mode: dropdown
options:
- "block"
- "allow"
- "remove"
remove_trackers:
name: Remove device trackers
description: This service allows removing device_tracker entities. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.
fields:
entities:
name: Entities
description: Entities to remove
required: true
selector:
entity:
integration: asusrouter
domain: device_tracker
multiple: true
+269
View File
@@ -0,0 +1,269 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Device location",
"description": "How to find your device",
"data": {
"host": "Hostname / IP address"
}
},
"credentials": {
"title": "Credentials",
"description": "The same as for the device web panel",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "Port (0 to use default value)",
"ssl": "Use SSL connection"
}
},
"operation": {
"title": "Operation mode",
"data": {
"mode": "Device mode",
"enable_control": "Enable device control",
"split_intervals": "Enable per-sensor update intervals"
}
},
"options": {
"title": "Additional options",
"menu_options": {
"connected_devices": "Connected devices",
"intervals": "Update intervals",
"interfaces": "Interfaces to monitor",
"events": "Home Assistant events",
"security": "Security options",
"finish": "Save and finish"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Enable device trackers",
"client_device": "Create a device when I enable device_tracker entity",
"clients_in_attr": "Store clients list in attributes of the connected devices sensor",
"client_filter": "Filter clients",
"client_filter_list": "List of clients to filter (only active if filter is enabled)",
"force_clients": "Force clients update",
"force_clients_waittime": "Wait time (force update -> check) (seconds)",
"latest_connected": "Number of latest connected devices to store",
"interval_devices": "Devices / AiMesh update",
"consider_home": "Consider device at home for (after last 'online' state)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Update intervals",
"description": "Values are in seconds",
"data": {
"cache_time": "Caching time",
"scan_interval": "Entities update",
"interval_cpu": "CPU data",
"interval_firmware": "Firmware data",
"interval_gwlan": "GWLAN interval",
"interval_light": "Light data",
"interval_misc": "Misc data",
"interval_network": "Network stat data",
"interval_parental_control": "Parental control data",
"interval_ports": "Ports data",
"interval_ram": "RAM data",
"interval_sysinfo": "Sysinfo data",
"interval_temperature": "Temperature data",
"interval_vpn": "VPN data",
"interval_wan": "WAN data",
"interval_wlan": "WLAN data"
}
},
"interfaces": {
"title": "Interfaces to monitor",
"data": {
"interfaces": "Select network interfaces to monitor",
"units_speed": "Units for speed",
"units_traffic": "Units for traffic"
}
},
"events": {
"title": "Events",
"description": "Which events should be raised",
"data": {
"device_connected": "Device connected (the device was not tracked before)",
"device_disconnected": "Device disconnected",
"device_reconnected": "Device reconnected (this device was already tracked before)",
"node_connected": "AiMesh node connected (not seen before)",
"node_disconnected": "AiMesh node disconnected",
"node_reconnected": "AiMesh node reconnected"
}
},
"security": {
"title": "Security",
"data": {
"hide_passwords": "Hide passwords from sensors and attributes"
}
}
},
"error": {
"access_error": "Access error. Refer to the log for details",
"cannot_resolve_host": "Cannot resolve hostname. Try with the IP address",
"connection_error": "Connection error. Refer to the log for details",
"connection_refused": "Connection refused",
"error": "[%key:common::config_flow::error::cannot_connect%]",
"login_blocked": "Login is blocked by the device. Please, wait",
"password_missing": "Password is missing",
"timeout": "Timeout error",
"unknown": "Unknown error",
"wrong_credentials": "Wrong credentials"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_serial": "This device does not provide serial number. Discovery aborted",
"not_router": "Not an Asus router. Cannot be configured"
}
},
"options": {
"step": {
"options": {
"title": "Configurations to change",
"menu_options": {
"credentials": "Credentials and connection",
"operation": "Operation mode",
"connected_devices": "Connected devices",
"intervals": "Update intervals",
"interfaces": "Interfaces to monitor",
"events": "Home Assistant events",
"security": "Security options",
"finish": "Save and finish"
}
},
"credentials": {
"title": "Credentials",
"description": "The same as for the device web panel",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "Port (0 to use default value)",
"ssl": "Use SSL connection"
}
},
"operation": {
"title": "Operation mode",
"data": {
"mode": "Device mode",
"enable_control": "Enable device control",
"split_intervals": "Enable per-sensor update intervals"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Enable device trackers",
"client_device": "Create a device when I enable device_tracker entity",
"clients_in_attr": "Store clients list in attributes of the connected devices sensor",
"client_filter": "Filter clients",
"client_filter_list": "List of clients to filter (only active if filter is enabled)",
"force_clients": "Force clients update",
"force_clients_waittime": "Wait time (force update -> check) (seconds)",
"latest_connected": "Number of latest connected devices to store",
"interval_devices": "Devices / AiMesh update",
"consider_home": "Consider device at home for (after last 'online' state)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Update intervals",
"description": "Values are in seconds",
"data": {
"cache_time": "Caching time",
"scan_interval": "Entities update",
"interval_cpu": "CPU data",
"interval_firmware": "Firmware data",
"interval_gwlan": "GWLAN interval",
"interval_light": "Light data",
"interval_misc": "Misc data",
"interval_network": "Network stat data",
"interval_parental_control": "Parental control data",
"interval_ports": "Ports data",
"interval_ram": "RAM data",
"interval_sysinfo": "Sysinfo data",
"interval_temperature": "Temperature data",
"interval_vpn": "VPN data",
"interval_wan": "WAN data",
"interval_wlan": "WLAN data"
}
},
"interfaces": {
"title": "Interfaces to monitor",
"data": {
"interfaces": "Select network interfaces to monitor",
"units_speed": "Units for speed",
"units_traffic": "Units for traffic"
}
},
"events": {
"title": "Events",
"description": "Which events should be raised",
"data": {
"device_connected": "Device connected (the device was not tracked before)",
"device_disconnected": "Device disconnected",
"device_reconnected": "Device reconnected (this device was already tracked before)",
"node_connected": "AiMesh node connected (not seen before)",
"node_disconnected": "AiMesh node disconnected",
"node_reconnected": "AiMesh node reconnected"
}
},
"security": {
"title": "Security",
"data": {
"hide_passwords": "Hide passwords from sensors and attributes"
}
}
},
"error": {
"access_error": "Access error. Refer to the log for details",
"connection_error": "Connection error. Refer to the log for details",
"connection_refused": "Connection refused",
"error": "[%key:common::config_flow::error::cannot_connect%]",
"login_blocked": "Login is blocked by the device. Please, wait",
"not_confirmed": "You have to confirm",
"timeout": "Timeout error",
"unknown": "Unknown error",
"wrong_credentials": "Wrong credentials"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
+271
View File
@@ -0,0 +1,271 @@
"""AsusRouter switch module."""
from __future__ import annotations
import logging
from typing import Any
from asusrouter.modules.parental_control import ParentalControlRule, PCRuleType
from homeassistant.components.switch import SwitchEntity
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
ASUSROUTER,
CONF_DEFAULT_HIDE_PASSWORDS,
CONF_HIDE_PASSWORDS,
DOMAIN,
ICON_INTERNET_ACCESS_OFF,
ICON_INTERNET_ACCESS_ON,
STATIC_SWITCHES,
)
from .dataclass import ARSwitchDescription
from .entity import ARBinaryEntity, async_setup_ar_entry
from .helpers import to_unique_id
from .router import ARDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AsusRouter switches."""
switches = STATIC_SWITCHES.copy()
hide = []
if config_entry.options.get(
CONF_HIDE_PASSWORDS, CONF_DEFAULT_HIDE_PASSWORDS
):
hide.extend(["password", "private_key", "psk"])
await async_setup_ar_entry(
hass, config_entry, async_add_entities, switches, ARSwitch, hide
)
router = hass.data[DOMAIN][config_entry.entry_id][ASUSROUTER]
tracked: set = set()
@callback
def update_router():
"""Update the values of the router."""
add_entities(router, async_add_entities, tracked)
router.async_on_close(
async_dispatcher_connect(
hass, router.signal_pc_rules_new, update_router
)
)
update_router()
@callback
def add_entities(
router: ARDevice,
async_add_entities: AddEntitiesCallback,
tracked: set[str],
) -> None:
"""Add new tracker entities from the router."""
new_tracked = []
for mac, rule in router.pc_rules.items():
if mac in tracked:
continue
new_tracked.append(ClientInternetSwitch(router, rule))
tracked.add(mac)
if new_tracked:
async_add_entities(new_tracked)
class ARSwitch(ARBinaryEntity, SwitchEntity):
"""AsusRouter switch."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: ARDevice,
description: ARSwitchDescription,
) -> None:
"""Initialize AsusRouter switch."""
super().__init__(coordinator, router, description)
self.entity_description: ARSwitchDescription = description
# State on
self._state_on = description.state_on
self._state_on_args = description.state_on_args
# State off
self._state_off = description.state_off
self._state_off_args = description.state_off_args
# Expect modify
self._state_expect_modify = description.state_expect_modify
async def async_turn_on(
self,
**kwargs: Any,
) -> None:
"""Turn on switch."""
kwargs = self._state_on_args if self._state_on_args is not None else {}
await self._set_state(
state=self._state_on,
expect_modify=self._state_expect_modify,
**kwargs,
)
async def async_turn_off(
self,
**kwargs: Any,
) -> None:
"""Turn off switch."""
kwargs = (
self._state_off_args if self._state_off_args is not None else {}
)
await self._set_state(
state=self._state_off,
expect_modify=self._state_expect_modify,
**kwargs,
)
class ClientInternetSwitch(SwitchEntity):
"""Client internet switch."""
def __init__(
self,
router: ARDevice,
rule: ParentalControlRule,
):
"""Initialize client switch."""
self._router = router
self._rule = rule
self._mac = dr.format_mac(rule.mac)
self._attr_unique_id = to_unique_id(
f"{router.mac}_{self._mac}_block_internet"
)
self._attr_name = f"{rule.name} Block Internet"
# Assign device info if set up
if router.create_devices is True:
self._attr_device_info = self._compile_device_info()
def _compile_device_info(self) -> DeviceInfo:
"""Compile device info."""
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self._mac)},
default_name=self._rule.name,
via_device=(DOMAIN, self._router.mac),
)
@property
def is_on(self) -> bool | None:
"""Get the state."""
match self._rule.type:
case PCRuleType.BLOCK:
return True
case PCRuleType.DISABLE:
return False
case _:
return None
@property
def icon(self) -> str | None:
"""Get the icon."""
if self.is_on:
return ICON_INTERNET_ACCESS_OFF
return ICON_INTERNET_ACCESS_ON
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra state attributes."""
return {
"mac": self._mac,
}
async def _set_state(
self,
state: ParentalControlRule,
**kwargs: Any,
) -> None:
"""Set state."""
try:
_LOGGER.debug("Changing PC rule to %s", state)
result = await self._router.bridge.api.async_set_state(
state=state, **kwargs
)
self._rule = state
if not result:
_LOGGER.debug("State was not set!")
except Exception as ex: # noqa: BLE001
_LOGGER.error("Unable to set state with an exception: %s", ex)
async def async_turn_on(
self,
**kwargs: Any,
) -> None:
"""Turn on block."""
await self._set_state(
state=ParentalControlRule(
mac=self._rule.mac,
name=self._rule.name,
type=PCRuleType.BLOCK,
),
**kwargs,
)
async def async_turn_off(
self,
**kwargs: Any,
) -> None:
"""Turn off block."""
await self._set_state(
state=ParentalControlRule(
mac=self._rule.mac,
name=self._rule.name,
type=PCRuleType.DISABLE,
),
**kwargs,
)
@callback
def async_on_demand_update(self) -> None:
"""Update the state."""
if self._rule.mac in self._router.pc_rules:
self._rule = self._router.pc_rules[self._rule.mac]
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register state update callback."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self._router.signal_pc_rules_update,
self.async_on_demand_update,
)
)
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Přidat zařízení",
"description": "Jak přidat své zařízení",
"data": {
"host": "Hostitel / IP addresa"
}
},
"credentials": {
"title": "Přihlašovací údaje",
"description": "Stejné jako pro webový panel zařízení",
"data": {
"username": "Uživatelské jméno",
"password": "Heslo",
"port": "Port (0 pro použití výchozí hodnoty)",
"ssl": "Použít připojení SSL"
}
},
"operation": {
"title": "Provozní režim",
"data": {
"mode": "[NT] Device mode",
"enable_control": "Povolit ovládání zařízení",
"split_intervals": "Povolit intervaly aktualizace pro každý senzor"
}
},
"options": {
"title": "[NT] Additional options",
"menu_options": {
"connected_devices": "[NT] Connected devices",
"intervals": "Intervaly aktualizace",
"interfaces": "Rozhraní ke sledování",
"events": "Události Home Assistant",
"security": "Možnosti zabezpečení",
"finish": "[NT] Save and finish"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Povolit sledování zařízení",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Počet nejnovějších připojených zařízení k uložení",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Považovat zařízení že je doma (po posledním 'online' stavu)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervaly aktualizace",
"description": "Hodnoty jsou v sekundách",
"data": {
"cache_time": "Čas mezipaměti",
"scan_interval": "Aktualizace entit",
"interval_cpu": "Data CPU",
"interval_firmware": "Data firmwaru",
"interval_gwlan": "Interval GWLAN",
"interval_light": "Lehká aktualizace dat",
"interval_misc": "Různá data",
"interval_network": "Údaje o statistikách sítě",
"interval_parental_control": "Data rodičovské kontroly",
"interval_ports": "Data portů",
"interval_ram": "Data RAM",
"interval_sysinfo": "Data systém info",
"interval_temperature": "Údaje o teplotě",
"interval_vpn": "Data VPN",
"interval_wan": "Data WAN",
"interval_wlan": "Data WLAN"
}
},
"interfaces": {
"title": "Rozhraní ke sledování",
"data": {
"interfaces": "Vyberte síťová rozhraní ke sledování",
"units_speed": "Jednotky pro rychlost",
"units_traffic": "Jednotky pro provoz"
}
},
"events": {
"title": "Události",
"description": "Které události by měly být oznámeny",
"data": {
"device_connected": "Zařízení připojeno (zařízení nebylo dříve sledováno)",
"device_disconnected": "Zařízení odpojeno",
"device_reconnected": "Zařízení bylo znovu připojeno (toto zařízení již bylo sledováno dříve)",
"node_connected": "[NT] AiMesh node connected (not seen before)",
"node_disconnected": "[NT] AiMesh node disconnected",
"node_reconnected": "[NT] AiMesh node reconnected"
}
},
"security": {
"title": "Zabezpečení",
"data": {
"hide_passwords": "Skrýt hesla ze senzorů a atributů"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Nelze přeložit název hostitele. Zkuste použít IP adresu",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Připojení odmítnuto",
"error": "Nepodařilo se připojit",
"login_blocked": "Přihlášení je blokováno zařízením. Čekejte prosím",
"password_missing": "Chybí heslo",
"timeout": "[NT] Timeout error",
"unknown": "Neznámá chyba",
"wrong_credentials": "Chybné přihlašovací údaje"
},
"abort": {
"already_configured": "[NT] Device is already configured",
"no_serial": "[NT] This device does not provide serial number. Discovery aborted",
"not_router": "[NT] Not an Asus router. Cannot be configured"
}
},
"options": {
"step": {
"options": {
"title": "Změnit konfiguraci",
"menu_options": {
"credentials": "Přihlašovací údaje a nastavení připojení",
"operation": "Provozní režim",
"connected_devices": "[NT] Connected devices",
"intervals": "Intervaly aktualizace",
"interfaces": "Rozhraní ke sledování",
"events": "Události Home Assistant",
"security": "Možnosti zabezpečení",
"finish": "[NT] Save and finish",
"device": "Identifikační a spojovací parametry"
}
},
"credentials": {
"title": "Přihlašovací údaje",
"description": "Stejné jako pro webový panel zařízení",
"data": {
"username": "Uživatelské jméno",
"password": "Heslo",
"port": "Port (0 pro použití výchozí hodnoty)",
"ssl": "Použít připojení SSL"
}
},
"operation": {
"title": "Provozní režim",
"data": {
"mode": "[NT] Device mode",
"enable_control": "Povolit ovládání zařízení",
"split_intervals": "Povolit intervaly aktualizace pro každý senzor"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Povolit sledování zařízení",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Počet nejnovějších připojených zařízení k uložení",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Považovat zařízení že je doma (po posledním 'online' stavu)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervaly aktualizace",
"description": "Hodnoty jsou v sekundách",
"data": {
"cache_time": "Čas mezipaměti",
"scan_interval": "Aktualizace entit",
"interval_cpu": "Data CPU",
"interval_firmware": "Data firmwaru",
"interval_gwlan": "Interval GWLAN",
"interval_light": "Lehká aktualizace dat",
"interval_misc": "Různá data",
"interval_network": "Údaje o statistikách sítě",
"interval_parental_control": "Data rodičovské kontroly",
"interval_ports": "Data portů",
"interval_ram": "Data RAM",
"interval_sysinfo": "Data systém info",
"interval_temperature": "Údaje o teplotě",
"interval_vpn": "Data VPN",
"interval_wan": "Data WAN",
"interval_wlan": "Data WLAN"
}
},
"interfaces": {
"title": "Rozhraní ke sledování",
"data": {
"interfaces": "Vyberte síťová rozhraní ke sledování",
"units_speed": "Jednotky pro rychlost",
"units_traffic": "Jednotky pro provoz"
}
},
"events": {
"title": "Události",
"description": "Které události by měly být oznámeny",
"data": {
"device_connected": "Zařízení připojeno (zařízení nebylo dříve sledováno)",
"device_disconnected": "Zařízení odpojeno",
"device_reconnected": "Zařízení bylo znovu připojeno (toto zařízení již bylo sledováno dříve)",
"node_connected": "[NT] AiMesh node connected (not seen before)",
"node_disconnected": "[NT] AiMesh node disconnected",
"node_reconnected": "[NT] AiMesh node reconnected"
}
},
"security": {
"title": "Zabezpečení",
"data": {
"hide_passwords": "Skrýt hesla ze senzorů a atributů"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Připojení odmítnuto",
"error": "Nepodařilo se připojit",
"login_blocked": "Přihlášení je blokováno zařízením. Čekejte prosím",
"not_confirmed": "Nepotvrzeno",
"timeout": "[NT] Timeout error",
"unknown": "Neznámá chyba",
"wrong_credentials": "Chybné přihlašovací údaje"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,268 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Gerätestandort",
"description": "So finden Sie Ihr Gerät",
"data": {
"host": "Hostname / IP-Adresse"
}
},
"credentials": {
"title": "Referenzen",
"description": "Dasselbe wie für das Geräte-Webpanel",
"data": {
"username": "Nutzername",
"password": "Passwort",
"port": "Port (0, um den Standardwert zu verwenden)",
"ssl": "Verwenden Sie eine SSL-Verbindung"
}
},
"operation": {
"title": "Betriebsmodus",
"data": {
"mode": "Gerätemodus",
"enable_control": "Aktivieren Sie die Gerätesteuerung",
"split_intervals": "Aktivieren Sie Aktualisierungsintervalle pro Sensor"
}
},
"options": {
"title": "Zusatzoptionen",
"menu_options": {
"connected_devices": "Verbundene Geräte",
"intervals": "Aktualisierungsintervalle",
"interfaces": "Zu überwachende Schnittstellen",
"events": "Home Assistant-Ereignisse",
"security": "Sicherheitsoptionen",
"finish": "Speichern und fertig"
}
},
"connected_devices": {
"title": "Verbundene Geräte",
"data": {
"track_devices": "Geräte-Tracker aktivieren",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Anzahl der zuletzt verbundenen Geräte, die gespeichert werden sollen",
"interval_devices": "Geräte / AiMesh-Update",
"consider_home": "Betrachten Sie das Gerät zu Hause für (nach dem letzten „Online“-Zustand)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Aktualisierungsintervalle",
"description": "Die Werte sind in Sekunden",
"data": {
"cache_time": "Caching-Zeit",
"scan_interval": "Aktualisierung der Entitäten",
"interval_cpu": "CPU-Daten",
"interval_firmware": "Firmware-Daten",
"interval_gwlan": "GWLAN-Intervall",
"interval_light": "Lichtdaten",
"interval_misc": "Verschiedene Daten",
"interval_network": "Netzwerkstatistikdaten",
"interval_parental_control": "Daten der Kindersicherung",
"interval_ports": "Portdaten",
"interval_ram": "RAM-Daten",
"interval_sysinfo": "Sysinfo-Daten",
"interval_temperature": "Temperaturdaten",
"interval_vpn": "VPN-Daten",
"interval_wan": "WAN-Daten",
"interval_wlan": "WLAN-Daten"
}
},
"interfaces": {
"title": "Zu überwachende Schnittstellen",
"data": {
"interfaces": "Wählen Sie die zu überwachenden Netzwerkschnittstellen aus",
"units_speed": "Einheiten für Geschwindigkeit",
"units_traffic": "Einheiten für den Verkehr"
}
},
"events": {
"title": "Events",
"description": "Welche Ereignisse sollten ausgelöst werden",
"data": {
"device_connected": "Gerät verbunden (das Gerät wurde zuvor nicht getrackt)",
"device_disconnected": "Gerät getrennt",
"device_reconnected": "Gerät neu verbunden (dieses Gerät wurde bereits zuvor getrackt)",
"node_connected": "AiMesh-Knoten verbunden (noch nie zuvor gesehen)",
"node_disconnected": "AiMesh-Knoten getrennt",
"node_reconnected": "AiMesh-Knoten wieder verbunden"
}
},
"security": {
"title": "Sicherheit",
"data": {
"hide_passwords": "Passwörter vor Sensoren und Attributen verbergen"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Kann den Hostnamen nicht auflösen. Versuchen Sie es mit der IP-Adresse",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Verbindung abgelehnt",
"error": "Verbindung nicht möglich",
"login_blocked": "Die Anmeldung wird vom Gerät blockiert. Warten Sie",
"password_missing": "Passwort fehlt",
"timeout": "[NT] Timeout error",
"unknown": "Unbekannter Fehler",
"wrong_credentials": "Falsche Anmeldeinformationen"
},
"abort": {
"already_configured": "Gerät ist bereits konfiguriert",
"no_serial": "Dieses Gerät gibt keine Seriennummer an. Erkennung abgebrochen",
"not_router": "Kein Asus-Router. Kann nicht konfiguriert werden"
}
},
"options": {
"step": {
"options": {
"title": "Konfigurationen zu ändern",
"menu_options": {
"credentials": "Anmeldeinformationen und Verbindung",
"operation": "Betriebsmodus",
"connected_devices": "Verbundene Geräte",
"intervals": "Aktualisierungsintervalle",
"interfaces": "Zu überwachende Schnittstellen",
"events": "Home Assistant-Ereignisse",
"security": "Sicherheitsoptionen",
"finish": "Speichern und fertig",
"device": "Identifikations- und Verbindungsparameter"
}
},
"credentials": {
"title": "Referenzen",
"description": "Dasselbe wie für das Geräte-Webpanel",
"data": {
"username": "Nutzername",
"password": "Passwort",
"port": "Port (0, um den Standardwert zu verwenden)",
"ssl": "Verwenden Sie eine SSL-Verbindung"
}
},
"operation": {
"title": "Betriebsmodus",
"data": {
"mode": "Gerätemodus",
"enable_control": "Aktivieren Sie die Gerätesteuerung",
"split_intervals": "Aktivieren Sie Aktualisierungsintervalle pro Sensor"
}
},
"connected_devices": {
"title": "Verbundene Geräte",
"data": {
"track_devices": "Geräte-Tracker aktivieren",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Anzahl der zuletzt verbundenen Geräte, die gespeichert werden sollen",
"interval_devices": "Geräte / AiMesh-Update",
"consider_home": "Betrachten Sie das Gerät zu Hause für (nach dem letzten „Online“-Zustand)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Aktualisierungsintervalle",
"description": "Die Werte sind in Sekunden",
"data": {
"cache_time": "Caching-Zeit",
"scan_interval": "Aktualisierung der Entitäten",
"interval_cpu": "CPU-Daten",
"interval_firmware": "Firmware-Daten",
"interval_gwlan": "GWLAN-Intervall",
"interval_light": "Lichtdaten",
"interval_misc": "Verschiedene Daten",
"interval_network": "Netzwerkstatistikdaten",
"interval_wlan": "WLAN-Daten",
"interval_wan": "WAN-Daten",
"interval_vpn": "VPN-Daten",
"interval_temperature": "Temperaturdaten",
"interval_sysinfo": "Sysinfo-Daten",
"interval_ram": "RAM-Daten",
"interval_ports": "Portdaten",
"interval_parental_control": "Daten der Kindersicherung"
}
},
"interfaces": {
"data": {
"interfaces": "Wählen Sie die zu überwachenden Netzwerkschnittstellen aus",
"units_speed": "Einheiten für Geschwindigkeit",
"units_traffic": "Einheiten für den Verkehr"
},
"title": "Zu überwachende Schnittstellen"
},
"events": {
"title": "Veranstaltungen",
"description": "Welche Ereignisse sollten ausgelöst werden",
"data": {
"device_connected": "Gerät verbunden (das Gerät wurde zuvor nicht getrackt)",
"device_disconnected": "Gerät getrennt",
"device_reconnected": "Gerät neu verbunden (dieses Gerät wurde bereits zuvor getrackt)",
"node_connected": "AiMesh-Knoten verbunden (noch nie zuvor gesehen)",
"node_disconnected": "AiMesh-Knoten getrennt",
"node_reconnected": "AiMesh-Knoten wieder verbunden"
}
},
"security": {
"title": "Sicherheit",
"data": {
"hide_passwords": "Passwörter vor Sensoren und Attributen verbergen"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Verbindung abgelehnt",
"error": "Verbindung nicht möglich",
"login_blocked": "Die Anmeldung wird vom Gerät blockiert. Warten Sie mal",
"not_confirmed": "Sie müssen bestätigen",
"timeout": "[NT] Timeout error"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Device location",
"description": "How to find your device",
"data": {
"host": "Hostname / IP address"
}
},
"credentials": {
"title": "Credentials",
"description": "The same as for the device web panel",
"data": {
"username": "Username",
"password": "Password",
"port": "Port (0 to use default value)",
"ssl": "Use SSL connection"
}
},
"operation": {
"title": "Operation mode",
"data": {
"mode": "Device mode",
"enable_control": "Enable device control",
"split_intervals": "Enable per-sensor update intervals"
}
},
"options": {
"title": "Additional options",
"menu_options": {
"connected_devices": "Connected devices",
"intervals": "Update intervals",
"interfaces": "Interfaces to monitor",
"events": "Home Assistant events",
"security": "Security options",
"finish": "Save and finish"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Enable device trackers",
"client_device": "Create a device when I enable device_tracker entity",
"clients_in_attr": "Store clients list in attributes of the connected devices sensor",
"client_filter": "Filter clients",
"client_filter_list": "List of clients to filter (only active if filter is enabled)",
"force_clients": "Force clients update",
"force_clients_waittime": "Wait time (force update -> check) (seconds)",
"latest_connected": "Number of latest connected devices to store",
"interval_devices": "Devices / AiMesh update",
"consider_home": "Consider device at home for (after last 'online' state)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Update intervals",
"description": "Values are in seconds",
"data": {
"cache_time": "Caching time",
"scan_interval": "Entities update",
"interval_cpu": "CPU data",
"interval_firmware": "Firmware data",
"interval_gwlan": "GWLAN interval",
"interval_light": "Light data",
"interval_misc": "Misc data",
"interval_network": "Network stat data",
"interval_parental_control": "Parental control data",
"interval_ports": "Ports data",
"interval_ram": "RAM data",
"interval_sysinfo": "Sysinfo data",
"interval_temperature": "Temperature data",
"interval_vpn": "VPN data",
"interval_wan": "WAN data",
"interval_wlan": "WLAN data"
}
},
"interfaces": {
"title": "Interfaces to monitor",
"data": {
"interfaces": "Select network interfaces to monitor",
"units_speed": "Units for speed",
"units_traffic": "Units for traffic"
}
},
"events": {
"title": "Events",
"description": "Which events should be raised",
"data": {
"device_connected": "Device connected (the device was not tracked before)",
"device_disconnected": "Device disconnected",
"device_reconnected": "Device reconnected (this device was already tracked before)",
"node_connected": "AiMesh node connected (not seen before)",
"node_disconnected": "AiMesh node disconnected",
"node_reconnected": "AiMesh node reconnected"
}
},
"security": {
"title": "Security",
"data": {
"hide_passwords": "Hide passwords from sensors and attributes"
}
}
},
"error": {
"access_error": "Access error. Refer to the log for details",
"cannot_resolve_host": "Cannot resolve hostname. Try with the IP address",
"connection_error": "Connection error. Refer to the log for details",
"connection_refused": "Connection refused",
"error": "Failed to connect",
"login_blocked": "Login is blocked by the device. Please, wait",
"password_missing": "Password is missing",
"timeout": "Timeout error",
"unknown": "Unknown error",
"wrong_credentials": "Wrong credentials"
},
"abort": {
"already_configured": "Device is already configured",
"no_serial": "This device does not provide serial number. Discovery aborted",
"not_router": "Not an Asus router. Cannot be configured"
}
},
"options": {
"step": {
"options": {
"title": "Configurations to change",
"menu_options": {
"credentials": "Credentials and connection",
"operation": "Operation mode",
"connected_devices": "Connected devices",
"intervals": "Update intervals",
"interfaces": "Interfaces to monitor",
"events": "Home Assistant events",
"security": "Security options",
"finish": "Save and finish",
"device": "Identification and connection parameters"
}
},
"credentials": {
"title": "Credentials",
"description": "The same as for the device web panel",
"data": {
"username": "Username",
"password": "Password",
"port": "Port (0 to use default value)",
"ssl": "Use SSL connection"
}
},
"operation": {
"title": "Operation mode",
"data": {
"mode": "Device mode",
"enable_control": "Enable device control",
"split_intervals": "Enable per-sensor update intervals"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Enable device trackers",
"client_device": "Create a device when I enable device_tracker entity",
"clients_in_attr": "Store clients list in attributes of the connected devices sensor",
"client_filter": "Filter clients",
"client_filter_list": "List of clients to filter",
"force_clients": "Force clients update",
"force_clients_waittime": "Wait time (force update -> check) (seconds)",
"latest_connected": "Number of latest connected devices to store",
"interval_devices": "Devices / AiMesh update",
"consider_home": "Consider device at home for (after last 'online' state)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Update intervals",
"description": "Values are in seconds",
"data": {
"cache_time": "Caching time",
"scan_interval": "Entities update",
"interval_cpu": "CPU data",
"interval_firmware": "Firmware data",
"interval_gwlan": "GWLAN interval",
"interval_light": "Light data",
"interval_misc": "Misc data",
"interval_network": "Network stat data",
"interval_parental_control": "Parental control data",
"interval_ports": "Ports data",
"interval_ram": "RAM data",
"interval_sysinfo": "Sysinfo data",
"interval_temperature": "Temperature data",
"interval_vpn": "VPN data",
"interval_wan": "WAN data",
"interval_wlan": "WLAN data"
}
},
"interfaces": {
"title": "Interfaces to monitor",
"data": {
"interfaces": "Select network interfaces to monitor",
"units_speed": "Units for speed",
"units_traffic": "Units for traffic"
}
},
"events": {
"title": "Events",
"description": "Which events should be raised",
"data": {
"device_connected": "Device connected (the device was not tracked before)",
"device_disconnected": "Device disconnected",
"device_reconnected": "Device reconnected (this device was already tracked before)",
"node_connected": "AiMesh node connected (not seen before)",
"node_disconnected": "AiMesh node disconnected",
"node_reconnected": "AiMesh node reconnected"
}
},
"security": {
"title": "Security",
"data": {
"hide_passwords": "Hide passwords from sensors and attributes"
}
}
},
"error": {
"access_error": "Access error. Refer to the log for details",
"connection_error": "Connection error. Refer to the log for details",
"connection_refused": "Connection refused",
"error": "Failed to connect",
"login_blocked": "Login is blocked by the device. Please, wait",
"not_confirmed": "You have to confirm",
"timeout": "Timeout error",
"unknown": "Unknown error",
"wrong_credentials": "Wrong credentials"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Localizar dispositivo",
"description": "Cómo encontrar tu dispositivo",
"data": {
"host": "Nombre de equipo / Dirección IP"
}
},
"credentials": {
"title": "Credenciales",
"description": "Los mismos que en la interfaz Web",
"data": {
"username": "Usuario",
"password": "Contraseña",
"port": "Puerto (0 para usar el valor por defecto)",
"ssl": "Usar conexión SSL"
}
},
"operation": {
"title": "Modo de funcionamiento",
"data": {
"mode": "Modo de dispositivo",
"enable_control": "Habilitar control del dispositivo",
"split_intervals": "Habilitar intervalos de actualización por sensor"
}
},
"options": {
"title": "Opciones adicionales",
"menu_options": {
"connected_devices": "Dispositivos conectados",
"intervals": "Intervalos de actualización",
"interfaces": "Interfaces a monitorizar",
"events": "Eventos Home Assistant",
"security": "Opciones de seguridad",
"finish": "Guardar y finalizar"
}
},
"connected_devices": {
"title": "Dispositivos conectados",
"data": {
"track_devices": "Habilitar rastreadores de dispositivo",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Número de dispositivos más recientes a almacenar",
"interval_devices": "Actualización del dispositivo / AiMesh",
"consider_home": "Considerar dispositivo en casa durante (tras el último estado 'online')",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervalos de actualización",
"description": "Los valores se indican en segundos",
"data": {
"cache_time": "Tiempo de caché para los datos del dispositivo",
"scan_interval": "Intervalo para actualizar entidades",
"interval_cpu": "Datos CPU",
"interval_firmware": "Datos firmware",
"interval_gwlan": "Datos GWLAN",
"interval_light": "Datos luces",
"interval_misc": "Datos miscelánea",
"interval_network": "Datos estadísticas de red",
"interval_parental_control": "Datos de control parental",
"interval_ports": "Datos de puertos",
"interval_ram": "Datos RAM",
"interval_sysinfo": "Datos de sistema",
"interval_temperature": "Datos de temperatura",
"interval_vpn": "Datos VPN",
"interval_wan": "Datos WAN",
"interval_wlan": "Datos WLAN"
}
},
"interfaces": {
"title": "Interfaces a monitorizar",
"data": {
"interfaces": "Seleccionar interfaces de red para monitorizar",
"units_speed": "Unidad de velocidad",
"units_traffic": "Unidad de tráfico"
}
},
"events": {
"title": "Eventos",
"description": "Qué eventos deben ser disparados",
"data": {
"device_connected": "Dispositivo conectado (el dispositivo no fue rastreado previamente)",
"device_disconnected": "Dispositivo desconectado",
"device_reconnected": "Dispositivo reconectado (el dispositivo ya fue rastreado previamente)",
"node_connected": "Nodo AiMesh conectado (el dispositivo no fue rastreado previamente)",
"node_disconnected": "Nodo AiMesh desconectado",
"node_reconnected": "Nodo AiMesh reconectado"
}
},
"security": {
"title": "Seguridad",
"data": {
"hide_passwords": "Ocultar contraseñas de sensores y atributos"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "No se puede encontrar el nombre del equipo. Intenta con la dirección IP",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Conexión rechazada",
"error": "Fallo al conectar",
"login_blocked": "Login bloqueado por el dispositivo. Por favor, espera",
"password_missing": "Falta la contraseña",
"timeout": "[NT] Timeout error",
"unknown": "Error desconocido",
"wrong_credentials": "Credenciales inválidos"
},
"abort": {
"already_configured": "El dispositivo ya está configurado",
"no_serial": "Este dispositivo no provee un número de serie. Cancelado descubrimiento",
"not_router": "No es un router Asus. No puede ser configurado"
}
},
"options": {
"step": {
"options": {
"title": "Opciones a cambiar",
"menu_options": {
"credentials": "Credenciales y configuración de conexión",
"operation": "Modo de funcionamiento",
"connected_devices": "Dispositivos conectados",
"intervals": "Intervalos de actualización",
"interfaces": "Interfaces a monitorizar",
"events": "Eventos Home Assistant",
"security": "Opciones de seguridad",
"finish": "Guardar y finalizar",
"device": "Parámetros de identificación y conexión"
}
},
"credentials": {
"title": "Credenciales",
"description": "Los mismos que en la interfaz Web",
"data": {
"username": "Usuario",
"password": "Contraseña",
"port": "Puerto (0 para usar el valor por defecto)",
"ssl": "Usar conexión SSL"
}
},
"operation": {
"title": "Modo de funcionamiento",
"data": {
"mode": "Modo de dispositivo",
"enable_control": "Habilitar control del dispositivo",
"split_intervals": "Habilitar intervalos de actualización por sensor"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Habilitar rastreadores de dispositivo",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Número de dispositivos más recientes a almacenar",
"interval_devices": "Actualización del dispositivo / AiMesh",
"consider_home": "Considerar dispositivo en casa durante (tras el último estado 'online')",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervalos de actualización",
"description": "Los valores se indican en segundos",
"data": {
"cache_time": "Tiempo de caché para los datos del dispositivo",
"scan_interval": "Intervalo para actualizar entidades",
"interval_cpu": "Datos CPU",
"interval_firmware": "Datos firmware",
"interval_gwlan": "Datos GWLAN",
"interval_light": "Datos luces",
"interval_misc": "Datos miscelánea",
"interval_network": "Datos estadísticas de red",
"interval_parental_control": "Datos de control parental",
"interval_ports": "Datos de puertos",
"interval_ram": "Datos RAM",
"interval_sysinfo": "Datos de sistema",
"interval_temperature": "Datos de temperatura",
"interval_vpn": "Datos VPN",
"interval_wan": "Datos WAN",
"interval_wlan": "Datos WLAN"
}
},
"interfaces": {
"title": "Interfaces a monitorizar",
"data": {
"interfaces": "Seleccionar interfaces de red para monitorizar",
"units_speed": "Unidad de velocidad",
"units_traffic": "Unidad de tráfico"
}
},
"events": {
"title": "Eventos",
"description": "Qué eventos deben ser disparados",
"data": {
"device_connected": "Dispositivo conectado (el dispositivo no fue rastreado previamente)",
"device_disconnected": "Dispositivo desconectado",
"device_reconnected": "Dispositivo reconectado (el dispositivo ya fue rastreado previamente)",
"node_connected": "Nodo AiMesh conectado (el dispositivo no fue rastreado previamente)",
"node_disconnected": "Nodo AiMesh desconectado",
"node_reconnected": "Nodo AiMesh reconectado"
}
},
"security": {
"title": "Seguridad",
"data": {
"hide_passwords": "Ocultar contraseñas de sensores y atributos"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Conexión rechazada",
"error": "Fallo al conectar",
"login_blocked": "Login bloqueado por el dispositivo. Por favor, espera",
"not_confirmed": "Tienes que confirmar",
"timeout": "[NT] Timeout error",
"unknown": "Error desconocido",
"wrong_credentials": "Credenciales inválidos"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Emplacement de l'appareil",
"description": "Comment retrouver votre appareil",
"data": {
"host": "Nom d'hôte / Addresse IP "
}
},
"credentials": {
"title": "Identifiants",
"description": "Les même que sur l'interface de gestion du routeur",
"data": {
"username": "Nom d'utilisateur",
"password": "Mot de pass",
"port": "Port (0 pour utiliser la valeur par défaut)",
"ssl": "Utiliser une connexion SSL"
}
},
"operation": {
"title": "Mode de fonctionnement",
"data": {
"mode": "Mode de l'appareil",
"enable_control": "Activer le contrôle des périphériques",
"split_intervals": "Activer les intervalles de mise à jour par capteur"
}
},
"options": {
"title": "[NT] Additional options",
"menu_options": {
"connected_devices": "[NT] Connected devices",
"intervals": "Intervalles d'actualisation",
"interfaces": "Interfaces à monitorer",
"events": "Événements de Home Assistant",
"security": "Options de sécurité",
"finish": "[NT] Save and finish"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Activer le suivis des périphériques",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Nombre d'appareils connectés les plus récents à stocker",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Considérez l'appareil à la maison pour (après le dernier état « en ligne »)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervalles d'actualisation",
"description": "Les valeurs sont en secondes",
"data": {
"cache_time": "Temps de mise en cache",
"scan_interval": "Intervalles d'actualisation des entités",
"interval_cpu": "Données CPU",
"interval_firmware": "Données du micrologiciel",
"interval_gwlan": "Intervalle GWLAN",
"interval_light": "État des LED",
"interval_misc": "Réglages divers",
"interval_network": "Données statistiques du réseau",
"interval_parental_control": "Données de contrôle parental",
"interval_ports": "Données des Ports",
"interval_ram": "Données RAM",
"interval_sysinfo": "Données Sysinfo",
"interval_temperature": "Température",
"interval_vpn": "Données VPN",
"interval_wan": "Données WAN",
"interval_wlan": "Données WLAN"
}
},
"interfaces": {
"title": "Interfaces à monitorer",
"data": {
"interfaces": "Sélection des interfaces réseau à monitorer",
"units_speed": "Unités de mesure de vitesse",
"units_traffic": "Unités de mesure du trafic"
}
},
"events": {
"title": "Événements",
"description": "Quels événements doivent être déclenchés ?",
"data": {
"device_connected": "Périphérique connecté (l'appareil n'a pas été suivi auparavant)",
"device_disconnected": "Périphérique déconnecté",
"device_reconnected": "Périphérique reconnecté (cet appareil était déjà suivi auparavant)",
"node_connected": "Node AiMesh connecté (l'appareil n'a pas été suivi auparavant)",
"node_disconnected": "Node AiMesh déconnecté",
"node_reconnected": "Node AiMesh reconnecté"
}
},
"security": {
"title": "Securitée",
"data": {
"hide_passwords": "Masquer les mots de passe des capteurs et attributs"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Impossible de résoudre le nom d'hôte. Essayez avec l'adresse IP",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Connexion rejetée",
"error": "Échec de connexion",
"login_blocked": "La connexion est bloquée par le routeur. Veuillez patienter",
"password_missing": "Mot de passe manquant",
"timeout": "[NT] Timeout error",
"unknown": "Erreur inconnue",
"wrong_credentials": "Identifiants erronés"
},
"abort": {
"already_configured": "[NT] Device is already configured",
"no_serial": "[NT] This device does not provide serial number. Discovery aborted",
"not_router": "[NT] Not an Asus router. Cannot be configured"
}
},
"options": {
"step": {
"options": {
"title": "Paramétrages à changer",
"menu_options": {
"device": "Paramètres d'identification et de connexion",
"operation": "Mode de fonctionnement",
"connected_devices": "[NT] Connected devices",
"intervals": "Intervalles d'actualisation",
"interfaces": "Interfaces à monitorer",
"events": "Événements de Home Assistant",
"security": "Options de sécurité",
"finish": "[NT] Save and finish",
"credentials": "Identifiants et connexion"
}
},
"credentials": {
"title": "Identifiants",
"description": "Les même que sur l'interface de gestion du routeur",
"data": {
"username": "Nom d'utilisateur",
"password": "Mot de pass",
"port": "Port (0 pour utiliser la valeur par défaut)",
"ssl": "Utiliser une connexion SSL"
}
},
"operation": {
"title": "Mode de fonctionnement",
"data": {
"mode": "Mode de l'appareil",
"enable_control": "Activer le contrôle des périphériques",
"split_intervals": "Activer les intervalles de mise à jour par capteur"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Activer le suivis des périphériques",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Nombre d'appareils connectés les plus récents à stocker",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Considérez l'appareil à la maison pour (après le dernier état « en ligne »)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervalles d'actualisation",
"description": "Les valeurs sont en secondes",
"data": {
"cache_time": "Temps de mise en cache",
"scan_interval": "Intervalles d'actualisation des entités",
"interval_cpu": "Données CPU",
"interval_firmware": "Données du micrologiciel",
"interval_gwlan": "Intervalle GWLAN",
"interval_light": "État des LED",
"interval_misc": "Réglages divers",
"interval_network": "Données statistiques du réseau",
"interval_parental_control": "Données de contrôle parental",
"interval_ports": "Données des Ports",
"interval_ram": "Données RAM",
"interval_sysinfo": "Données Sysinfo",
"interval_temperature": "Température",
"interval_vpn": "Données VPN",
"interval_wan": "Données WAN",
"interval_wlan": "Données WLAN"
}
},
"interfaces": {
"title": "Interfaces à monitorer",
"data": {
"interfaces": "Sélection des interfaces réseau à monitorer",
"units_speed": "Unités de mesure de vitesse",
"units_traffic": "Unités de mesure du trafic"
}
},
"events": {
"title": "Événements",
"description": "Quels événements doivent être déclenchés ?",
"data": {
"device_connected": "Périphérique connecté (l'appareil n'a pas été suivi auparavant)",
"device_disconnected": "Périphérique déconnecté",
"device_reconnected": "Périphérique reconnecté (cet appareil était déjà suivi auparavant)",
"node_connected": "Node AiMesh connecté (l'appareil n'a pas été suivi auparavant)",
"node_disconnected": "Node AiMesh déconnecté",
"node_reconnected": "Node AiMesh reconnecté"
}
},
"security": {
"title": "Securitée",
"data": {
"hide_passwords": "Masquer les mots de passe des capteurs et des attributs"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Connexion rejetée",
"error": "Échec de connexion",
"login_blocked": "La connexion est bloquée par le routeur, merci de patienter",
"not_confirmed": "Vous devez confirmer",
"timeout": "[NT] Timeout error",
"unknown": "Erreur inconnue",
"wrong_credentials": "Identifiants erronés"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,269 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Az eszköz helye",
"description": "Hogyan található meg készüléke",
"data": {
"host": "Gazdanév / IP-cím"
}
},
"credentials": {
"title": "Hitelesítő adatok",
"description": "Ugyanaz, mint az eszköz webes felületén",
"data": {
"username": "Felhasználónév",
"password": "Jelszó",
"port": "Port (0 az alapértelmezett érték használatához)",
"ssl": "SSL kapcsolat használata"
}
},
"operation": {
"title": "Üzemmód",
"data": {
"mode": "Eszköz mód",
"enable_control": "Eszközvezérlés engedélyezése",
"split_intervals": "Érzékelőnkénti frissítési időközök engedélyezése"
}
},
"options": {
"title": "További beállítások",
"menu_options": {
"connected_devices": "Csatlakoztatott eszközök",
"intervals": "Frissítési időközök",
"interfaces": "Interfészek a monitorozáshoz",
"events": "Home Assistant események",
"security": "Biztonsági beállítások",
"finish": "Mentés és bezárás"
}
},
"connected_devices": {
"title": "Csatlakoztatott eszközök",
"data": {
"track_devices": "Eszközkövetők engedélyezése",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "A legutóbb csatlakoztatott eszközök tárolásának maximális száma",
"interval_devices": "Eszközök / AiMesh frissítés",
"consider_home": "Otthon van az eszköz (az utolsó „online” állapot utáni megállapítás)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Frissítési időközök",
"description": "Az értékek másodpercben vannak megadva",
"data": {
"cache_time": "Gyorsítótárazási idő",
"scan_interval": "Az entitások frissítése",
"interval_cpu": "CPU adatok",
"interval_firmware": "Firmware adatok",
"interval_gwlan": "GWLAN intervallum",
"interval_light": "LED adatok",
"interval_misc": "Egyéb adatok",
"interval_network": "Hálózati statisztikai adatok",
"interval_parental_control": "Szülői felügyeleti adatok",
"interval_ports": "Portok adatai",
"interval_ram": "RAM adatok",
"interval_sysinfo": "Rendszerinfo adatok",
"interval_temperature": "Hőmérséklet adatok",
"interval_vpn": "VPN adat",
"interval_wan": "WAN adat",
"interval_wlan": "WLAN adat"
}
},
"interfaces": {
"title": "Interfészek a monitorozáshoz",
"data": {
"interfaces": "Válassza ki a figyelni kívánt hálózati interfészeket",
"units_speed": "Sebesség mértékegységei",
"units_traffic": "Egységek a forgalom számára"
}
},
"events": {
"title": "Események",
"description": "Milyen eseményeket kell felvetni",
"data": {
"device_connected": "Eszköz csatlakoztatva (új eszköz)",
"device_disconnected": "Az eszköz leválasztva",
"device_reconnected": "Az eszköz újracsatlakozva",
"node_connected": "AiMesh csomópont csatlakoztatva (új eszköz)",
"node_disconnected": "Az AiMesh csomópont nem csatlakozik",
"node_reconnected": "Az AiMesh csomópont újracsatlakozva"
}
},
"security": {
"title": "Biztonság",
"data": {
"hide_passwords": "Jelszavak elrejtése az érzékelők és attribútumok elől"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Nem lehet sikerült hálózati nevévvel. Próbálja meg IP címmel",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Kapcsolódás elutasítva",
"error": "Nem sikerült csatlakozni",
"login_blocked": "A bejelentkezést az eszköz blokkolja. Kérlek várjon",
"password_missing": "Hiányzó jelszó",
"timeout": "[NT] Timeout error",
"unknown": "Ismeretlen hiba",
"wrong_credentials": "Nem megfelelő hitelesítő adatok"
},
"abort": {
"already_configured": "Az eszköz már konfigurálva van",
"no_serial": "Ez a készülék nem közölt sorozatszámot. A felfedezés megszakadt",
"not_router": "Nem Asus router, az eszköz nem konfigurálható."
}
},
"options": {
"step": {
"options": {
"title": "A módosítandó konfigurációk",
"menu_options": {
"credentials": "Hitelesítési adatok és kapcsolat",
"operation": "Üzemmód",
"connected_devices": "Csatlakoztatott eszközök",
"intervals": "Frissítési időközök",
"interfaces": "Interfészek a monitorozáshoz",
"events": "Home Assistant események",
"security": "Biztonsági beállítások",
"finish": "Mentés és bezárás"
}
},
"credentials": {
"title": "Hitelesítő adatok",
"description": "Ugyanaz, mint az eszköz webpaneljénél",
"data": {
"username": "Felhasználónév",
"password": "Jelszó",
"port": "Port (0 az alapértelmezett érték használatához)",
"ssl": "SSL kapcsolat használata"
}
},
"operation": {
"title": "Üzemmód",
"data": {
"mode": "Eszköz mód",
"enable_control": "Eszközvezérlés engedélyezése",
"split_intervals": "Érzékelőnkénti frissítési időközök engedélyezése"
}
},
"connected_devices": {
"title": "Csatlakoztatott eszközök",
"data": {
"track_devices": "Eszközkövetők engedélyezése",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "A legutóbb tárolandó csatlakoztatott eszközök száma",
"interval_devices": "Eszközök / AiMesh frissítés",
"consider_home": "Otthon van az eszköz (az utolsó „online” állapot utáni megállapítás)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Frissítési időközök",
"description": "Az értékeket másodpercben adja meg",
"data": {
"cache_time": "Gyorsítótárazási idő",
"scan_interval": "Az entitások frissítése",
"interval_cpu": "CPU adatok",
"interval_firmware": "Firmware adatok",
"interval_gwlan": "GWLAN intervallum",
"interval_light": "LED adatok",
"interval_misc": "Egyéb adatok",
"interval_network": "Hálózati statisztikai adatok",
"interval_parental_control": "Szülői felügyeleti adatok",
"interval_ports": "Portok adatai",
"interval_ram": "RAM adatok",
"interval_sysinfo": "Rendszerinfó adatok",
"interval_temperature": "Hőmérséklet adatok",
"interval_vpn": "VPN adatok",
"interval_wan": "WAN adatok",
"interval_wlan": "WLAN adatok"
}
},
"interfaces": {
"title": "Interfészek a monitorozáshoz",
"data": {
"interfaces": "Válassza ki a figyelni kívánt hálózati interfészeket",
"units_speed": "Sebesség mértékegységei",
"units_traffic": "Egységek a forgalom számára"
}
},
"events": {
"title": "Események",
"description": "Milyen eseményeket kell felvetni",
"data": {
"device_connected": "Eszköz csatlakoztatva (új eszköz)",
"device_disconnected": "Az eszköz leválasztva",
"device_reconnected": "Az eszköz újracsatlakozva",
"node_connected": "AiMesh csomópont csatlakoztatva (új eszköz)",
"node_disconnected": "Az AiMesh csomópont nem csatlakozik",
"node_reconnected": "Az AiMesh csomópont újracsatlakozva"
}
},
"security": {
"title": "Biztonság",
"data": {
"hide_passwords": "Jelszavak elrejtése az érzékelők és attribútumok elől"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Kapcsolódás elutasítva",
"error": "Nem sikerült csatlakozni",
"login_blocked": "A bejelentkezést az eszköz blokkolja. Kérlek várjon",
"not_confirmed": "Meg kell erősíteni",
"timeout": "[NT] Timeout error",
"unknown": "Ismeretlen hiba",
"wrong_credentials": "Nem megfelelő hitelesítő adatok"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Device locatie",
"description": "Hoe vind je jou device",
"data": {
"host": "Hostnaam / IP adres"
}
},
"credentials": {
"title": "Login gegevens",
"description": "Dezelfde als die van je router web paneel",
"data": {
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"port": "Poort (0 voor standaard waarde)",
"ssl": "Gebruik SSL verbinding"
}
},
"operation": {
"title": "Operatie mode",
"data": {
"mode": "[NT] Device mode",
"enable_control": "Activeer apparaat controle",
"split_intervals": "Activeer per-sensor update interval"
}
},
"options": {
"title": "[NT] Additional options",
"menu_options": {
"connected_devices": "[NT] Connected devices",
"intervals": "Update interval",
"interfaces": "Interfaces voor monitoring",
"events": "Home Assistant events",
"security": "Beveiliging opties",
"finish": "[NT] Save and finish"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Activeer apparaat trackers",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Nummer van laatst verbonden apparaat opslaan?",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Overweeg apparaat als thuis voor (na laatste 'online' staat)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Update interval",
"description": "Waarde in secondes",
"data": {
"cache_time": "Caching tijd",
"scan_interval": "Entities update",
"interval_cpu": "CPU data",
"interval_firmware": "Firmware data",
"interval_gwlan": "GWLAN interval",
"interval_light": "Kicht data",
"interval_misc": "Overige data",
"interval_network": "Network statestieken data",
"interval_parental_control": "Ouders controlle data",
"interval_ports": "Poorten data",
"interval_ram": "RAM data",
"interval_sysinfo": "Sysinfo data",
"interval_temperature": "Temperstuur data",
"interval_vpn": "VPN data",
"interval_wan": "WAN data",
"interval_wlan": "WLAN data"
}
},
"interfaces": {
"title": "Interface voor monitoring",
"data": {
"interfaces": "Selecteer netwerk interfaces voor monitoring",
"units_speed": "Eenheden per snelheid",
"units_traffic": "Eenhenden for verkeer"
}
},
"events": {
"title": "Gebeurtenis",
"description": "Welke gebeurtenis moet meer prioriteit krijgen?",
"data": {
"device_connected": "Apparaat verbonden (het apparaat was niet eerder gevolgt)",
"device_disconnected": "Apparaat verbroken",
"device_reconnected": "Apparaat opnieuw verbonden (Het apparaart is al eens gevolgt)",
"node_connected": "[NT] AiMesh node connected (not seen before)",
"node_disconnected": "[NT] AiMesh node disconnected",
"node_reconnected": "[NT] AiMesh node reconnected"
}
},
"security": {
"title": "Beveiliging",
"data": {
"hide_passwords": "Verberg wachtwoorden voor sensoren en accessoires"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Kan hostnaam niet herleiden. Probeer met IP adres",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Connectie afgewezen",
"error": "Problemen met connectie",
"login_blocked": "Inloggen is geblokkeerd door device. Wacht aub",
"password_missing": "Wachtwoord mist",
"timeout": "[NT] Timeout error",
"unknown": "Onbekende fout",
"wrong_credentials": "Verkeerde login gegevens"
},
"abort": {
"already_configured": "[NT] Device is already configured",
"no_serial": "[NT] This device does not provide serial number. Discovery aborted",
"not_router": "[NT] Not an Asus router. Cannot be configured"
}
},
"options": {
"step": {
"options": {
"title": "Verander configuratie",
"menu_options": {
"credentials": "Login gegevens en connectie instellingen",
"operation": "Operatie modus",
"connected_devices": "[NT] Connected devices",
"intervals": "Update interval",
"interfaces": "Interfaces voor monitoring",
"events": "Home Assistant events",
"security": "Beveiliging opties",
"finish": "[NT] Save and finish",
"device": "Identificatie- en verbindingsparameters"
}
},
"credentials": {
"title": "Login gegevens",
"description": "Dezelfde als die van je router web paneel",
"data": {
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"port": "Poort (0 voor standaard waarde)",
"ssl": "Gebruik SSL verbinding"
}
},
"operation": {
"title": "Operation mode",
"data": {
"mode": "[NT] Device mode",
"enable_control": "Activeer apparaat controle",
"split_intervals": "Activeer per-sensor update interval"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Activeer apparaat trackers",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Nummer van laatst verbonden apparaat opslaan?",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Overweeg apparaat als thuis voor (na laatste 'online' staat)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Update intervals",
"description": "Waardes zijn in secondes",
"data": {
"cache_time": "Caching tijd",
"scan_interval": "Entities update",
"interval_cpu": "CPU data",
"interval_firmware": "Firmware data",
"interval_gwlan": "GWLAN interval",
"interval_light": "Kicht data",
"interval_misc": "Overige data",
"interval_network": "Network stat data",
"interval_parental_control": "Ouders controlle data",
"interval_ports": "Poorten data",
"interval_ram": "RAM data",
"interval_sysinfo": "Sysinfo data",
"interval_temperature": "Temperstuur data",
"interval_vpn": "VPN data",
"interval_wan": "WAN data",
"interval_wlan": "WLAN data"
}
},
"interfaces": {
"title": "Interfaces to monitor",
"data": {
"interfaces": "Selecteer netwerk interfaces voor monitoring",
"units_speed": "Eenheden per snelheid",
"units_traffic": "Eenhenden for verkeer"
}
},
"events": {
"title": "Gebeurtenis",
"description": "Welke gebeurtenis moet meer prioriteit krijgen?",
"data": {
"device_connected": "Apparaat verbonden (het apparaat was niet eerder gevolgt)",
"device_disconnected": "Apparaat verbroken",
"device_reconnected": "Apparaat opnieuw verbonden (Het apparaart is al eens gevolgt)",
"node_connected": "[NT] AiMesh node connected (not seen before)",
"node_disconnected": "[NT] AiMesh node disconnected",
"node_reconnected": "[NT] AiMesh node reconnected"
}
},
"security": {
"title": "Security",
"data": {
"hide_passwords": "Verberg wachtwoorden voor sensoren en accessoires"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Connectie afgewezen",
"error": "Problemen met connectie",
"login_blocked": "Inloggen is geblokkeerd door device. Wacht aub",
"not_confirmed": "U dient te bevestigen",
"timeout": "[NT] Timeout error",
"unknown": "Onbekende fout",
"wrong_credentials": "Verkeerde login gegevens"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Localizar dispositivo",
"description": "Como encontrar seu dispositivo",
"data": {
"host": "Nome de rede / Endereço IP"
}
},
"credentials": {
"title": "Credenciais",
"description": "As mesmas do painel da web do dispositivo",
"data": {
"username": "Nome de usuário",
"password": "Senha",
"port": "Porta (0 para usar o valor padrão)",
"ssl": "Usar conexão SSL"
}
},
"operation": {
"title": "Modo de operação",
"data": {
"mode": "Modo do dispositivo",
"enable_control": "Ativar o controle do dispositivo",
"split_intervals": "Habilitar intervalos de atualização por sensor"
}
},
"options": {
"title": "Opções adicionais",
"menu_options": {
"connected_devices": "Dispositivos conectados",
"intervals": "Intervalos de atualização",
"interfaces": "Interfaces para monitorar",
"events": "Eventos do Home Assistant",
"security": "Opções de segurança",
"finish": "Guardar e finalizar"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Habilitar rastreadores de dispositivos",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Número de dispositivos conectatos recentemente a armazenar",
"interval_devices": "Atualização de dispositivos / AiMesh",
"consider_home": "Considere um dispositivo em casa após (desde o último estado 'online')",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervalos de atualização",
"description": "Valores indicados em segundos",
"data": {
"cache_time": "Tempo de cache para os dados do dispositivo",
"scan_interval": "Intervalo para atualizar entidades",
"interval_cpu": "Dados de CPU",
"interval_firmware": "Dados do firmware",
"interval_gwlan": "Dados GWLAN",
"interval_light": "Dados dos LEDs",
"interval_misc": "Dados diversos",
"interval_network": "Dados de estatística de rede",
"interval_parental_control": "Dados do controle dos pais",
"interval_ports": "Dados de portas",
"interval_ram": "Dados da RAM",
"interval_sysinfo": "Dados do sistema",
"interval_temperature": "Dados de temperatura",
"interval_vpn": "Dados de VPN",
"interval_wan": "Dados da WAN",
"interval_wlan": "Dados da WLAN"
}
},
"interfaces": {
"title": "Interfaces para monitorar",
"data": {
"interfaces": "Selecione interfaces de rede para monitorar",
"units_speed": "Unidades de velocidade",
"units_traffic": "Unidades de tráfego"
}
},
"events": {
"title": "Eventos",
"description": "Quais eventos devem ser disparados",
"data": {
"device_connected": "Dispositivo conectato (o dispositivo não foi rastreado anteriormente)",
"device_disconnected": "Dispositivo desconectato",
"device_reconnected": "Dispositivo reconectado (o dispositivo já foi rastreado anteriormente)",
"node_connected": "Nó AiMesh conectado (o dispositivo não foi rastreado anteriormente)",
"node_disconnected": "Nó AiMesh desconectado",
"node_reconnected": "Nó AiMesh reconectado"
}
},
"security": {
"title": "Segurança",
"data": {
"hide_passwords": "Ocultar senhas de sensores e atributos"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Não foi possível resolver o nome do host. Tente usar o endereço IP",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Conexão recusada",
"error": "Falhou ao conectar",
"login_blocked": "O login foi bloqueado pelo dispositivo. Por favor, espere",
"password_missing": "Falta a senha",
"timeout": "[NT] Timeout error",
"unknown": "Erro desconhecido",
"wrong_credentials": "Credenciais inválidas"
},
"abort": {
"already_configured": "Dispositivo já configurado",
"no_serial": "Este dispositivo não fornece um número de série. Descobrimento abortado.",
"not_router": "Não é um roteador ASUS. Não é possível configurar."
}
},
"options": {
"step": {
"options": {
"title": "Configurações a alterar",
"menu_options": {
"credentials": "Credenciais e configurações de conexão",
"operation": "Modo de operação",
"connected_devices": "Dispositivos conectados",
"intervals": "Intervalos de atualização",
"interfaces": "Interfaces para monitorar",
"events": "Eventos do Home Assistant",
"security": "Opções de segurança",
"finish": "Guardar e finalizar",
"device": "Parâmetros de identificação e conexão"
}
},
"credentials": {
"title": "Credenciais",
"description": "As mesmas do painel da web do dispositivo",
"data": {
"username": "Nome de usuário",
"password": "Senha",
"port": "Porta (0 para usar o valor padrão)",
"ssl": "Usar conexão SSL"
}
},
"operation": {
"title": "Modo de operação",
"data": {
"mode": "Modo do dispositivo",
"enable_control": "Ativar o controle do dispositivo",
"split_intervals": "Habilitar intervalos por sensor"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Habilitar rastreadores de dispositivos",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Número de dispositivos conectatos recentemente a armazenar",
"interval_devices": "Atualização de dispositivos / AiMesh",
"consider_home": "Considere um dispositivo em casa após (desde o último estado 'online')",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervalos de atualização",
"description": "Valores indicados em segundos",
"data": {
"cache_time": "Tempo de cache para os dados do dispositivo",
"scan_interval": "Intervalo para atualizar entidades",
"interval_cpu": "Dados de CPU",
"interval_firmware": "Dados do firmware",
"interval_gwlan": "Dados GWLAN",
"interval_light": "Dados dos LEDs",
"interval_misc": "Dados diversos",
"interval_network": "Dados de estatística da rede",
"interval_parental_control": "Dados do controle dos pais",
"interval_ports": "Dados de portas",
"interval_ram": "Dados de RAM",
"interval_sysinfo": "Dados do sistema",
"interval_temperature": "Dados de temperatura",
"interval_vpn": "Dados de VPN",
"interval_wan": "Dados da WAN",
"interval_wlan": "Dados da WLAN"
}
},
"interfaces": {
"title": "Interfaces para monitorar",
"data": {
"interfaces": "Selecione interfaces de rede para monitorar",
"units_speed": "Unidades para velocidade",
"units_traffic": "Unidades para tráfego"
}
},
"events": {
"title": "Eventos",
"description": "Quais eventos devem ser disparados",
"data": {
"device_connected": "Dispositivo conectato (o dispositivo não foi rastreado anteriormente)",
"device_disconnected": "Dispositivo desconectato",
"device_reconnected": "Dispositivo reconectado (o dispositivo já foi rastreado anteriormente)",
"node_connected": "Nó AiMesh conectado (o dispositivo não foi rastreado anteriormente)",
"node_disconnected": "Nó AiMesh desconectado",
"node_reconnected": "Nó AiMesh reconectado"
}
},
"security": {
"title": "Segurança",
"data": {
"hide_passwords": "Ocultar senhas de sensores e atributos"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Conexão recusada",
"error": "Falha ao conectar",
"login_blocked": "O login foi bloqueado pelo dispositivo. Por favor, espere",
"not_confirmed": "Você tem que confirmar",
"timeout": "[NT] Timeout error",
"unknown": "Erro desconhecido",
"wrong_credentials": "Credenciais erradas"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,195 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Localização do dispositivo",
"description": "Como encontrar o seu dispositivo",
"data": {
"host": "Nome do host / Endereço IP"
}
},
"credentials": {
"title": "Credenciais",
"description": "As mesmas utilizadas no painel web do dispositivo",
"data": {
"username": "Nome de utilizador",
"password": "Palavra-passe",
"port": "Porta (0 para usar o valor padrão)",
"ssl": "Usar ligação SSL"
}
},
"operation": {
"title": "Modo de operação",
"data": {
"mode": "Modo do dispositivo",
"enable_control": "Ativar controlo do dispositivo",
"split_intervals": "Ativar intervalos de atualização por sensor"
}
},
"options": {
"title": "Opções adicionais",
"menu_options": {
"connected_devices": "Dispositivos ligados",
"intervals": "Intervalos de atualização",
"interfaces": "Interfaces a monitorizar",
"events": "Eventos do Home Assistant",
"security": "Opções de segurança",
"finish": "Guardar e finalizar"
}
},
"connected_devices": {
"title": "Dispositivos ligados",
"data": {
"track_devices": "Ativar rastreadores de dispositivos",
"client_device": "Criar um dispositivo quando ativar a entidade device_tracker",
"clients_in_attr": "Guardar a lista de clientes nos atributos do sensor de dispositivos ligados",
"client_filter": "Filtrar clientes",
"client_filter_list": "Lista de clientes a filtrar (ativo apenas se o filtro estiver ativado)",
"force_clients": "Forçar atualização de clientes",
"force_clients_waittime": "Tempo de espera (forçar atualização -> verificar) (segundos)",
"latest_connected": "Número de últimos dispositivos ligados a guardar",
"interval_devices": "Atualização de dispositivos / AiMesh",
"consider_home": "Considerar dispositivo em casa durante (após o último estado 'online')",
"create_devices": "Criar dispositivos no HA ao criar entidades de clientes"
}
},
"intervals": {
"title": "Intervalos de atualização",
"description": "Valores em segundos",
"data": {
"cache_time": "Tempo de cache",
"scan_interval": "Atualização de entidades",
"interval_cpu": "Dados da CPU",
"interval_firmware": "Dados de firmware",
"interval_gwlan": "Intervalo GWLAN",
"interval_light": "Dados de luz",
"interval_misc": "Dados diversos",
"interval_network": "Dados de estatísticas de rede",
"interval_parental_control": "Dados de controlo parental",
"interval_ports": "Dados de portas",
"interval_ram": "Dados de RAM",
"interval_sysinfo": "Dados de informações do sistema",
"interval_temperature": "Dados de temperatura",
"interval_vpn": "Dados de VPN",
"interval_wan": "Dados de WAN",
"interval_wlan": "Dados de WLAN"
}
},
"interfaces": {
"title": "Interfaces a monitorizar",
"data": {
"interfaces": "Selecione as interfaces de rede a monitorizar",
"units_speed": "Unidades para velocidade",
"units_traffic": "Unidades para tráfego"
}
},
"events": {
"title": "Eventos",
"description": "Que eventos devem ser gerados",
"data": {
"device_connected": "Dispositivo ligado (o dispositivo não era rastreado anteriormente)",
"device_disconnected": "Dispositivo desligado",
"device_reconnected": "Dispositivo reconectado (este dispositivo já estava a ser rastreado anteriormente)",
"node_connected": "Nó AiMesh ligado (não visto antes)",
"node_disconnected": "Nó AiMesh desligado",
"node_reconnected": "Nó AiMesh reconectado"
}
},
"security": {
"title": "Segurança",
"data": {
"hide_passwords": "Ocultar palavras-passe de sensores e atributos"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Não foi possível resolver o nome do host. Experimente com o endereço IP",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Ligação recusada",
"error": "Falha ao ligar",
"login_blocked": "A autenticação foi bloqueada pelo dispositivo. Aguarde, por favor",
"password_missing": "Palavra-passe em falta",
"timeout": "[NT] Timeout error",
"unknown": "Erro desconhecido",
"wrong_credentials": "Credenciais incorretas"
},
"abort": {
"already_configured": "O dispositivo já está configurado",
"no_serial": "Este dispositivo não fornece número de série. A descoberta foi abortada",
"not_router": "Não é um router Asus. Não pode ser configurado"
}
},
"options": {
"step": {
"options": {
"title": "Configurações a alterar",
"menu_options": {
"credentials": "Credenciais e ligação",
"operation": "Modo de operação",
"connected_devices": "Dispositivos ligados",
"intervals": "Intervalos de atualização",
"interfaces": "Interfaces a monitorizar",
"events": "Eventos do Home Assistant",
"security": "Opções de segurança",
"finish": "Guardar e finalizar",
"device": "Identificação e parâmetros de ligação"
}
},
"credentials": {
"title": "Credenciais",
"description": "As mesmas utilizadas no painel web do dispositivo",
"data": {
"username": "Nome de utilizador",
"password": "Palavra-passe",
"port": "Porta (0 para usar o valor padrão)",
"ssl": "Usar ligação SSL"
}
},
"operation": {
"title": "Modo de operação",
"data": {
"mode": "Modo do dispositivo",
"enable_control": "Ativar controlo do dispositivo",
"split_intervals": "Ativar intervalos de atualização por sensor"
}
}
}
},
"services": {
"device_internet_access": {
"name": "Controlo de acesso à Internet do dispositivo",
"description": "Alterar o acesso à Internet do dispositivo utilizando a funcionalidade de controlo parental do router. Consulte a documentação para obter mais parâmetros possíveis",
"fields": {
"entities": {
"name": "Entidades",
"description": "Selecione os dispositivos para alterar as definições de acesso à Internet"
},
"state": {
"name": "Estado",
"description": "Defina o estado para os dispositivos"
}
}
},
"remove_trackers": {
"name": "Remover rastreadores de dispositivos",
"description": "Remover rastreadores de dispositivos do registo do dispositivo. Note que, ao executar, o serviço removerá os rastreadores selecionados e permitirá remover manualmente as entidades.",
"fields": {
"entities": {
"name": "Entidades",
"description": "Selecione os dispositivos para remover do registo do dispositivo"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Permitir acesso",
"block": "Bloquear acesso",
"remove": "Remover regra da lista"
}
}
}
}
@@ -0,0 +1,269 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Местоположение устройства",
"description": "Как найти свое устройство",
"data": {
"host": "Имя хоста / IP-адрес"
}
},
"credentials": {
"title": "Учетные данные",
"description": "Те же, что и для веб-интерфейса устройства",
"data": {
"username": "Имя пользователя",
"password": "Пароль",
"port": "Порт (0 - использовать значение по умолчанию)",
"ssl": "Использовать SSL-соединение"
}
},
"operation": {
"title": "Режим работы",
"data": {
"mode": "Режим работы устройства",
"enable_control": "Включить управление устройством",
"split_intervals": "Включить настройки частоты обновлений для каждого сенсора"
}
},
"options": {
"title": "Дополнительные параметры",
"menu_options": {
"connected_devices": "Подключенные устройства",
"intervals": "Частота обновлений",
"interfaces": "Мониторинг интерфейсов",
"events": "События Home Assistant",
"security": "Параметры безопасности",
"finish": "Сохранить и завершить"
}
},
"connected_devices": {
"title": "Подключенные устройства",
"data": {
"track_devices": "Включить отслеживание устройств",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Количество последних подключенных устройств для сохранения",
"interval_devices": "Обновление устройств / AiMesh",
"consider_home": "Считать 'дома' в течение (после последнего состояния 'онлайн')",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Частота обновлений",
"description": "Значения указываются в секундах",
"data": {
"cache_time": "Время кэширования",
"scan_interval": "Обновление объектов",
"interval_cpu": "Данные процессора",
"interval_firmware": "Данные прошивки",
"interval_gwlan": "Интервал GWLAN",
"interval_light": "Данные LED-подсветки",
"interval_misc": "Прочие данные",
"interval_network": "Данные о состоянии сети",
"interval_parental_control": "Данные родительского контроля",
"interval_ports": "Данные портов",
"interval_ram": "Данные RAM",
"interval_sysinfo": "Информация о системе",
"interval_temperature": "Данные о температуре",
"interval_vpn": "Данные VPN",
"interval_wan": "Данные WAN",
"interval_wlan": "Данные WLAN"
}
},
"interfaces": {
"title": "Мониторинг интерфейсов",
"data": {
"interfaces": "Выберите сетевые интерфейсы для мониторинга",
"units_speed": "Единицы измерения скорости",
"units_traffic": "Единицы измерения трафика"
}
},
"events": {
"title": "События",
"description": "Какие события необходимо вызывать",
"data": {
"device_connected": "Устройство подключено (устройство ранее не отслеживалось)",
"device_disconnected": "Устройство отключено",
"device_reconnected": "Устройство повторно подключено (это устройство уже отслеживалось ранее)",
"node_connected": "Узел AiMesh подключен (не отслеживался ранее)",
"node_disconnected": "Узел AiMesh отключен",
"node_reconnected": "Узел AiMesh повторно подключен"
}
},
"security": {
"title": "Безопасность",
"data": {
"hide_passwords": "Скрыть пароли от сенсоров и атрибутов"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Не удается разрешить имя хоста. Попробуйте использовать IP-адрес",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Отказ в подключении",
"error": "Не удалось подключиться",
"login_blocked": "Вход заблокирован устройством. Пожалуйста, подождите",
"password_missing": "Отсутствует пароль",
"timeout": "[NT] Timeout error",
"unknown": "Неизвестная ошибка",
"wrong_credentials": "Неверные учетные данные"
},
"abort": {
"already_configured": "Устройство уже настроено",
"no_serial": "Это устройство не предоставляет серийный номер. Обнаружение прервано",
"not_router": "Не является маршрутизатором Asus. Настройка невозможна"
}
},
"options": {
"step": {
"options": {
"title": "Изменение конфигурации",
"menu_options": {
"credentials": "Учетные данные и подключение",
"operation": "Режим работы",
"connected_devices": "Подключенные устройства",
"intervals": "Частота обновлений",
"interfaces": "Мониторинг интерфейсов",
"events": "События Home Assistant",
"security": "Параметры безопасности",
"finish": "Сохранить и завершить"
}
},
"credentials": {
"title": "Учетные данные",
"description": "Те же, что и для веб-интерфейса устройства",
"data": {
"username": "Имя пользователя",
"password": "Пароль",
"port": "Порт (0 - использовать значение по умолчанию)",
"ssl": "Использовать SSL-соединение"
}
},
"operation": {
"title": "Режим работы",
"data": {
"mode": "Режим работы устройства",
"enable_control": "Включить управление устройством",
"split_intervals": "Включить настройки частоты обновлений для каждого сенсора"
}
},
"connected_devices": {
"title": "Подключенные устройства",
"data": {
"track_devices": "Включить отслеживание устройств",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Количество последних подключенных устройств для сохранения",
"interval_devices": "Обновление устройств / AiMesh",
"consider_home": "Считать 'дома' в течение (после последнего состояния 'онлайн')",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Частота обновлений",
"description": "Значения указываются в секундах",
"data": {
"cache_time": "Время кэширования",
"scan_interval": "Обновление объектов",
"interval_cpu": "Данные процессора",
"interval_firmware": "Данные прошивки",
"interval_gwlan": "Интервал GWLAN",
"interval_light": "Данные LED-подсветки",
"interval_misc": "Прочие данные",
"interval_network": "Данные о состоянии сети",
"interval_parental_control": "Данные родительского контроля",
"interval_ports": "Данные портов",
"interval_ram": "Данные RAM",
"interval_sysinfo": "Информация о системе",
"interval_temperature": "Данные о температуре",
"interval_vpn": "Данные VPN",
"interval_wan": "Данные WAN",
"interval_wlan": "Данные WLAN"
}
},
"interfaces": {
"title": "Мониторинг интерфейсов",
"data": {
"interfaces": "Выберите сетевые интерфейсы для мониторинга",
"units_speed": "Единицы измерения скорости",
"units_traffic": "Единицы измерения трафика"
}
},
"events": {
"title": "События",
"description": "Какие события необходимо вызывать",
"data": {
"device_connected": "Устройство подключено (устройство ранее не отслеживалось)",
"device_disconnected": "Устройство отключено",
"device_reconnected": "Устройство повторно подключено (это устройство уже отслеживалось ранее)",
"node_connected": "Узел AiMesh подключен (не отслеживался ранее)",
"node_disconnected": "Узел AiMesh отключен",
"node_reconnected": "Узел AiMesh повторно подключен"
}
},
"security": {
"title": "Безопасность",
"data": {
"hide_passwords": "Скрыть пароли от сенсоров и атрибутов"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Отказ в подключении",
"error": "Не удалось подключиться",
"login_blocked": "Вход заблокирован устройством. Пожалуйста, подождите",
"not_confirmed": "Подтвердите",
"timeout": "[NT] Timeout error",
"unknown": "Неизвестная ошибка",
"wrong_credentials": "Неверные учетные данные"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Pridať zariadenie",
"description": "Ako pridať svoje zariadenie",
"data": {
"host": "Hostiteľ / IP adresa"
}
},
"credentials": {
"title": "Prihlasovacie údaje",
"description": "Rovnaké ako pre webový panel zariadenia",
"data": {
"username": "Uživateľské meno",
"password": "Heslo",
"port": "Port (0 pre použitie východzej hodnoty)",
"ssl": "Použiť pripojenie SSL"
}
},
"operation": {
"title": "Prevádzkový režim",
"data": {
"mode": "[NT] Device mode",
"enable_control": "Povoliť ovládanie zariadenia",
"split_intervals": "Povoliť intervaly aktualizáce pre každý senzor"
}
},
"options": {
"title": "[NT] Additional options",
"menu_options": {
"connected_devices": "[NT] Connected devices",
"intervals": "Intervaly aktualizácie",
"interfaces": "Rozhrania sledovania",
"events": "Udalosti Home Assistant",
"security": "Možnosti zabezpečenia",
"finish": "[NT] Save and finish"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Povoliť sledovanie zariadení",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Počet najnovších pripojených zariadení pre uloženie",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Považovať zariadenie že je doma (po poslednom 'online' stave)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervaly aktualizácie",
"description": "Hodnoty sú v sekundách",
"data": {
"cache_time": "Čas medzipamäti",
"scan_interval": "Aktualizácia entít",
"interval_cpu": "Údaje CPU",
"interval_firmware": "Údaje firmwaru",
"interval_gwlan": "Interval GWLAN",
"interval_light": "Stav LED",
"interval_misc": "Rôzne údaje",
"interval_network": "Údaje o štatistikách siete",
"interval_parental_control": "Údaje rodičovskej kontroly",
"interval_ports": "Údaje portov",
"interval_ram": "Údaje RAM",
"interval_sysinfo": "Údaje systémové info",
"interval_temperature": "Údaje o teplote",
"interval_vpn": "Údaje VPN",
"interval_wan": "Údaje WAN",
"interval_wlan": "Údaje WLAN"
}
},
"interfaces": {
"title": "Rozhranie sledovania",
"data": {
"interfaces": "Vyberte sieťové rozhranie sledovania",
"units_speed": "Jednotky pre rýchlost",
"units_traffic": "Jednotky pre sieťovú prevádzku"
}
},
"events": {
"title": "Udalosti",
"description": "Ktoré udalosti sa majú oznamovať",
"data": {
"device_connected": "Zariadenie pripojené (zariadenie nebolo predtým sledované)",
"device_disconnected": "Zariadenie odpojené",
"device_reconnected": "Zariadenie bolo znovu pripojené (toto zariadenie už bolo sledované)",
"node_connected": "[NT] AiMesh node connected (not seen before)",
"node_disconnected": "[NT] AiMesh node disconnected",
"node_reconnected": "[NT] AiMesh node reconnected"
}
},
"security": {
"title": "Zabezpečenie",
"data": {
"hide_passwords": "Skryť heslá zo senzorov a atribútov"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"cannot_resolve_host": "Nedá sa rozpoznať názov hostiteľa. Skúste použiť IP adresu",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Pripojenie odmietnuté",
"error": "Nepodarilo se pripojiť",
"login_blocked": "Prihlásenie je blokované zariadením. Počkajte prosím",
"password_missing": "Chýba heslo",
"timeout": "[NT] Timeout error",
"unknown": "Neznáma chyba",
"wrong_credentials": "Chybné prihlasovacie údaje"
},
"abort": {
"already_configured": "[NT] Device is already configured",
"no_serial": "[NT] This device does not provide serial number. Discovery aborted",
"not_router": "[NT] Not an Asus router. Cannot be configured"
}
},
"options": {
"step": {
"options": {
"title": "Zmena konfigurácií",
"menu_options": {
"credentials": "Prihlasovacie údaje a nastavenie pripojenia",
"operation": "Prevádzkový režim",
"connected_devices": "[NT] Connected devices",
"intervals": "Intervaly aktualizácie",
"interfaces": "Rozhrania sledovania",
"events": "Udalosti Home Assistant",
"security": "Možnosti zabezpečenia",
"finish": "[NT] Save and finish",
"device": "Parametre identifikácie a pripojenia"
}
},
"credentials": {
"title": "Prihlasovacie údaje",
"description": "Rovnaké ako pre webový panel zariadenia",
"data": {
"username": "Uživateľské meno",
"password": "Heslo",
"port": "Port (0 pre použitie východzej hodnoty)",
"ssl": "Použiť pripojenie SSL"
}
},
"operation": {
"title": "Prevádzkový režim",
"data": {
"mode": "[NT] Device mode",
"enable_control": "Povoliť ovladanie zariadenia",
"split_intervals": "Povoliť intervaly aktualizácie pre každý senzor"
}
},
"connected_devices": {
"title": "Connected devices",
"data": {
"track_devices": "Povoliť sledovanie zariadení",
"client_device": "[NT] Create a device when I enable device_tracker entity",
"clients_in_attr": "[NT] Store clients list in attributes of the connected devices sensor",
"client_filter": "[NT] Filter clients",
"client_filter_list": "[NT] List of clients to filter (only active if filter is enabled)",
"force_clients": "[NT] Force clients update",
"force_clients_waittime": "[NT] Wait time (force update -> check) (seconds)",
"latest_connected": "Počet najnovších pripojených zariadení pre uloženie",
"interval_devices": "[NT] Devices / AiMesh update",
"consider_home": "Považovať zariadenie že je doma (po poslednom 'online' stave)",
"create_devices": "Create HA devices when creating clients entities"
}
},
"intervals": {
"title": "Intervaly aktualizáce",
"description": "Hodnoty sú v sekundách",
"data": {
"cache_time": "Čas mezipamäte",
"scan_interval": "Aktualizácia entít",
"interval_cpu": "Údaje CPU",
"interval_firmware": "Údaje firmwaru",
"interval_gwlan": "Interval GWLAN",
"interval_light": "Stav LED",
"interval_misc": "Rôzne údaje",
"interval_network": "Údaje o statistikách siete",
"interval_parental_control": "Údaje rodičovskej kontroly",
"interval_ports": "Údaje portov",
"interval_ram": "Údaje RAM",
"interval_sysinfo": "Údaje systémové info",
"interval_temperature": "Údaje o teplote",
"interval_vpn": "Údaje VPN",
"interval_wan": "Údaje WAN",
"interval_wlan": "Údaje WLAN"
}
},
"interfaces": {
"title": "Rozhranie sledovania",
"data": {
"interfaces": "Vyberte sieťové rozhranie sledovania",
"units_speed": "Jednotky pre rýchlosť",
"units_traffic": "Jednotky pre sieťovú prevádzku"
}
},
"events": {
"title": "Udalosti",
"description": "Ktoré udalosti sa majú oznamovať",
"data": {
"device_connected": "Zariadenie pripojené (zariadenie nebolo predtým sledované)",
"device_disconnected": "Zariadenie odpojené",
"device_reconnected": "Zariadenie bolo znovu pripojené (toto zariadenie už bolo sledované)",
"node_connected": "[NT] AiMesh node connected (not seen before)",
"node_disconnected": "[NT] AiMesh node disconnected",
"node_reconnected": "[NT] AiMesh node reconnected"
}
},
"security": {
"title": "Zabezpečenie",
"data": {
"hide_passwords": "Skryť heslá zo senzorov a atribútov"
}
}
},
"error": {
"access_error": "[NT] Access error. Refer to the log for details",
"connection_error": "[NT] Connection error. Refer to the log for details",
"connection_refused": "Pripojenie odmietnuté",
"error": "Nepodarilo se pripojiť",
"login_blocked": "Prihlásenie je blokované zariadením. Čakaje prosím",
"not_confirmed": "Nepotvrdené",
"timeout": "[NT] Timeout error",
"unknown": "Neznámá chyba",
"wrong_credentials": "Chybné prihlasovacie údaje"
}
},
"services": {
"device_internet_access": {
"name": "Device internet access control",
"description": "Change device internet access using parental control feature of the router. Please, refer to the documentation for the additional possible parameters",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to change internet access settings"
},
"state": {
"name": "State",
"description": "Set the state for the device(s)"
}
}
},
"remove_trackers": {
"name": "Remove device trackers",
"description": "Remove device trackers from the device registry. Please, keep in mind, when run, the service will remove selected trackers from watching and will allow you to remove the entities manually.",
"fields": {
"entities": {
"name": "Entities",
"description": "Select device(s) to remove from the device registry"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Allow access",
"block": "Block access",
"remove": "Remove rule from the list"
}
}
}
}
@@ -0,0 +1,270 @@
{
"config": {
"flow_title": "{name} ({host})",
"step": {
"find": {
"title": "Розташування пристрою",
"description": "Де шукати ваш пристрій",
"data": {
"host": "Ім'я хоста / IP адреса"
}
},
"credentials": {
"title": "Параметри для входу",
"description": "Такі самі, як для веб-панелі пристрою",
"data": {
"username": "Ім'я користувача",
"password": "Пароль",
"port": "Порт (0 щоб використати стандартне значення)",
"ssl": "Використовувати SSL"
}
},
"operation": {
"title": "Режим роботи",
"data": {
"mode": "Режим роботи",
"enable_control": "Увімкнути контроль пристрою",
"split_intervals": "Незалежне налаштування інтервалу оновлення сенсорів"
}
},
"options": {
"title": "Додаткові налаштування",
"menu_options": {
"connected_devices": "Підключені пристрої",
"intervals": "Інтервали оновлення",
"interfaces": "Інтерфейси для відслідковування",
"events": "Події Home Assistant",
"security": "Налаштування безпеки",
"finish": "Зберегти і завершити"
}
},
"connected_devices": {
"title": "Підключені пристрої",
"data": {
"track_devices": "Увімкнути трекінг клієнтів",
"client_device": "Створювати пристрій, коли я активую device_tracker запис",
"clients_in_attr": "Зберігати повний список клієнтів у атрибутах до connected devices сенсору",
"client_filter": "Фільтрувати клієнтів",
"client_filter_list": "Список клієнтів для фільтру (лише якщо фільтр увімкнено)",
"force_clients": "Примусово оновлювати список клієнтів",
"force_clients_waittime": "Час очікування (примусове оновлення -> перевірка) (секунд)",
"latest_connected": "Кількість останніх підключених пристроїв для зберігання",
"interval_devices": "Оновлення клієнтів / AiMesh",
"consider_home": "Вважати клієнта 'вдома' протягом (після останнього значення 'online')",
"create_devices": "Створювати пристрої при створення записів для окремих клієнтів"
}
},
"intervals": {
"title": "Інтервали оновлення",
"description": "Значення в секундах",
"data": {
"cache_time": "Час кешування",
"scan_interval": "Інтервал оновлення",
"interval_cpu": "Оновлення даних CPU",
"interval_firmware": "Оновлення даних прошивки",
"interval_gwlan": "Оновлення даних гостьових WLAN",
"interval_light": "Оновлення даних light",
"interval_misc": "Оновлення даних misc",
"interval_network": "Оновлення даних network stat",
"interval_parental_control": "Оновлення даних parental control",
"interval_ports": "Оновлення даних ports",
"interval_ram": "Оновлення даних RAM",
"interval_sysinfo": "Оновлення даних sysinfo",
"interval_temperature": "Оновлення даних temperature",
"interval_vpn": "Оновлення даних VPN",
"interval_wan": "Оновлення даних WAN",
"interval_wlan": "Оновлення даних WLAN"
}
},
"interfaces": {
"title": "Інтерфейси для відслідковування",
"data": {
"interfaces": "Виберіть інтерфейси для відслідковування",
"units_speed": "Одиниці вимірювання швидкості",
"units_traffic": "Одиниці вимірювання трафіку"
}
},
"events": {
"title": "Події",
"description": "Які події повинні викликатися",
"data": {
"device_connected": "Пристрій підключено (цей пристрій раніше не відслідковувався)",
"device_disconnected": "Пристрій відключено",
"device_reconnected": "Пристрій знов підключено (цей пристрій вже відслідковувався)",
"node_connected": "AiMesh нода підключена (підключено вперше)",
"node_disconnected": "AiMesh нода відключена",
"node_reconnected": "AiMesh нода знов підключена"
}
},
"security": {
"title": "Безпека",
"data": {
"hide_passwords": "Приховати паролі з сенсорів та атрибутів"
}
}
},
"error": {
"access_error": "Помилка доступу. Перевірте деталі в журналі",
"cannot_resolve_host": "Неможливо знайти ім'я хоста. Спробуйте використати IP адресу",
"connection_error": "Помилка підключення. Перевірти деталі в журналі подій",
"connection_refused": "З'єднання відхилено",
"error": "Не вдалося підключитися",
"login_blocked": "Вхід заблоковано пристроєм. Будь ласка, зачекайте",
"password_missing": "Пароль відсутній",
"timeout": "Таймаут підключення",
"unknown": "Невідома помилка",
"wrong_credentials": "Неправильні облікові дані"
},
"abort": {
"already_configured": "Пристрій вже налаштовано",
"no_serial": "Пристрій не надає серійний номер. Налаштування скасоване",
"not_router": "Пристрій не є роутером Asus. Налаштування неможливе"
}
},
"options": {
"step": {
"options": {
"title": "Зміна налаштувань",
"menu_options": {
"credentials": "Облікові дані та підключення",
"operation": "Режим роботи",
"connected_devices": "Підключені пристрої",
"intervals": "Інтервали оновлення",
"interfaces": "Інтерфейси для відслідковування",
"events": "Події Home Assistant",
"security": "Налаштування безпеки",
"finish": "Зберегти і завершити",
"device": "Параметри ідентифікації та підключення"
}
},
"credentials": {
"title": "Параметри для входу",
"description": "Такі самі, як для веб-панелі пристрою",
"data": {
"username": "Ім'я користувача",
"password": "Пароль",
"port": "Порт (0 щоб використати стандартне значення)",
"ssl": "Використовувати SSL"
}
},
"operation": {
"title": "Режим роботи",
"data": {
"mode": "Режим роботи",
"enable_control": "Увімкнути контроль пристрою",
"split_intervals": "Незалежне налаштування інтервалу оновлення сенсорів"
}
},
"connected_devices": {
"title": "Підключені пристрої",
"data": {
"track_devices": "Увімкнути трекінг клієнтів",
"client_device": "Створювати пристрій, коли я активую device_tracker запис",
"clients_in_attr": "Зберігати повний список клієнтів у атрибутах до connected devices сенсору",
"client_filter": "Фільтрувати клієнтів",
"client_filter_list": "Список клієнтів для фільтру (лише якщо фільтр увімкнено)",
"force_clients": "Примусово оновлювати список клієнтів",
"force_clients_waittime": "Час очікування (примусове оновлення -> перевірка) (секунд)",
"latest_connected": "Кількість останніх підключених пристроїв для зберігання",
"interval_devices": "Оновлення клієнтів / AiMesh",
"consider_home": "Вважати клієнта 'вдома' протягом (після останнього значення 'online')",
"create_devices": "Створювати пристрої при створення записів для окремих клієнтів"
}
},
"intervals": {
"title": "Інтервали оновлення",
"description": "Значення в секундах",
"data": {
"cache_time": "Час кешування",
"scan_interval": "Інтервал оновлення",
"interval_cpu": "Оновлення даних CPU",
"interval_firmware": "Оновлення даних прошивки",
"interval_gwlan": "Оновлення даних гостьових WLAN",
"interval_light": "Оновлення даних light",
"interval_misc": "Оновлення даних misc",
"interval_network": "Оновлення даних network stat",
"interval_parental_control": "Оновлення даних parental control",
"interval_ports": "Оновлення даних ports",
"interval_ram": "Оновлення даних RAM",
"interval_sysinfo": "Оновлення даних sysinfo",
"interval_temperature": "Оновлення даних temperature",
"interval_vpn": "Оновлення даних VPN",
"interval_wan": "Оновлення даних WAN",
"interval_wlan": "Оновлення даних WLAN"
}
},
"interfaces": {
"title": "Інтерфейси для відслідковування",
"data": {
"interfaces": "Виберіть інтерфейси для відслідковування",
"units_speed": "Одиниці вимірювання швидкості",
"units_traffic": "Одиниці вимірювання трафіку"
}
},
"events": {
"title": "Події",
"description": "Які події повинні викликатися",
"data": {
"device_connected": "Пристрій підключено (цей пристрій раніше не відслідковувався)",
"device_disconnected": "Пристрій відключено",
"device_reconnected": "Пристрій знов підключено (цей пристрій вже відслідковувався)",
"node_connected": "AiMesh нода підключена (підключено вперше)",
"node_disconnected": "AiMesh нода відключена",
"node_reconnected": "AiMesh нода знов підключена"
}
},
"security": {
"title": "Безпека",
"data": {
"hide_passwords": "Приховати паролі з сенсорів та атрибутів"
}
}
},
"error": {
"access_error": "Помилка доступу. Перевірте деталі в журналі",
"connection_error": "Помилка підключення. Перевірти деталі в журналі подій",
"connection_refused": "З'єднання відхилено",
"error": "Не вдалося підключитися",
"login_blocked": "Вхід заблоковано пристроєм. Будь ласка, зачекайте",
"not_confirmed": "Необхідне підтвердження",
"timeout": "Таймаут підключення",
"unknown": "Невідома помилка",
"wrong_credentials": "Неправильні облікові дані"
}
},
"services": {
"device_internet_access": {
"name": "Інтернет доступ для пристрою",
"description": "Змініть доступ до Інтернету за допомогою функції батьківського контролю маршрутизатора. Будь ласка, зверніться до документації для отримання додаткових можливих параметрів",
"fields": {
"entities": {
"name": "Пристрої",
"description": "Оберіть пристрої для зміни доступу до Інтернету"
},
"state": {
"name": "Доступ",
"description": "Оберіть вид доступу до Інтернету"
}
}
},
"remove_trackers": {
"name": "Видалити трекери пристроїв",
"description": "Видалити трекери з реєстру. Врахуйте наступне: при виконанні цього сервісу, обрані трекери будуть видалені з реєстру. Після цього ви зможете видалити їх сутності вручну.",
"fields": {
"entities": {
"name": "Пристрої",
"description": "Оберіть пристрої для видалення з реєстру"
}
}
}
},
"selector": {
"set_pc_rule": {
"options": {
"allow": "Дозволити доступ",
"block": "Заблокувати доступ",
"remove": "Видалити правило зі списку"
}
}
}
}
+97
View File
@@ -0,0 +1,97 @@
"""AsusRouter update module."""
from __future__ import annotations
import asyncio
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 .const import STATIC_UPDATES
from .dataclass import ARUpdateDescription
from .entity import ARBinaryEntity, async_setup_ar_entry
from .router import ARDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AsusRouter updates."""
updates = STATIC_UPDATES.copy()
await async_setup_ar_entry(
hass, config_entry, async_add_entities, updates, ARUpdate
)
class ARUpdate(ARBinaryEntity, UpdateEntity):
"""AsusRouter update."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
router: ARDevice,
description: ARUpdateDescription,
) -> None:
"""Initialize AsusRouter update."""
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("latest")
self._attr_release_summary = self.extra_state_attributes.get(
"release_note"
)
self._attr_in_progress = False
self._attr_supported_features = (
UpdateEntityFeature.RELEASE_NOTES
| UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS
)
def release_notes(self) -> str | None:
"""Return the release notes."""
return self.extra_state_attributes.get("release_note")
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install the update."""
try:
_LOGGER.debug(
"Trying to install Firmware update. "
"This might take several minutes."
)
result = await self.api.async_set_state(
state=AsusSystem.FIRMWARE_UPGRADE,
)
if not result:
_LOGGER.debug(
"Something went wrong while trying to install the update."
)
else:
self._attr_in_progress = True
await asyncio.sleep(120)
self._attr_in_progress = False
except Exception as ex: # noqa: BLE001
_LOGGER.error(
"An exception occurred while trying to install the update: %s",
ex,
)
+37
View File
@@ -0,0 +1,37 @@
"""C.A.F.E. - Visual automation editor for Home Assistant."""
from __future__ import annotations
import logging
from pathlib import Path
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .panel import async_register_panel, async_unregister_panel
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the C.A.F.E. component."""
# This will be called when the integration is loaded
# But actual setup happens in async_setup_entry
return True
async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
"""Set up C.A.F.E. from a config entry."""
# Register the panel (frontend)
await async_register_panel(hass)
_LOGGER.info("C.A.F.E. integration set up successfully")
return True
async def async_unload_entry(hass: HomeAssistant, entry) -> bool:
"""Unload a config entry."""
async_unregister_panel(hass)
return True
+53
View File
@@ -0,0 +1,53 @@
"""Config flow for C.A.F.E. integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({})
class CafeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for C.A.F.E."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is not None:
# Check if already configured
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="C.A.F.E.",
data={}
)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"name": "C.A.F.E.",
"description": "Visual automation editor for Home Assistant"
}
)
async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
"""Handle import from configuration.yaml."""
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="C.A.F.E.", data={})
+6
View File
@@ -0,0 +1,6 @@
"""Constants for C.A.F.E.."""
DOMAIN = "cafe"
PANEL_URL = "/cafe_panel"
PANEL_TITLE = "C.A.F.E."
PANEL_ICON = "mdi:coffee-to-go"
+14
View File
@@ -0,0 +1,14 @@
{
"domain": "cafe",
"name": "C.A.F.E.",
"version": "0.6.0",
"codeowners": [],
"config_flow": true,
"dependencies": ["frontend", "http"],
"documentation": "https://github.com/FezVrasta/cafe-hass",
"documentation_url": "https://github.com/FezVrasta/cafe-hass",
"integration_type": "service",
"iot_class": "local_push",
"issue_tracker": "https://github.com/FezVrasta/cafe-hass/issues",
"requirements": []
}
+66
View File
@@ -0,0 +1,66 @@
"""Panel for C.A.F.E."""
import os
import logging
from pathlib import Path
from homeassistant.core import HomeAssistant
from homeassistant.components import frontend, panel_custom
from homeassistant.components.http import StaticPathConfig
from .const import DOMAIN, PANEL_TITLE, PANEL_ICON
_LOGGER = logging.getLogger(__name__)
PANEL_URL = f"/api/{DOMAIN}_panel"
PANEL_NAME = f"{DOMAIN}-panel"
async def async_register_panel(hass: HomeAssistant) -> None:
"""Register the C.A.F.E. panel."""
# Get the path to the www folder within the component
www_path = Path(__file__).parent / "www"
# Register static path for the frontend assets
await hass.http.async_register_static_paths([
StaticPathConfig("/cafe-hass", str(www_path), False)
])
# The panel wrapper has a fixed name (not hashed)
wrapper_path = www_path / "assets" / "panel-wrapper.js"
if not wrapper_path.exists():
_LOGGER.error("panel-wrapper.js not found in assets directory")
_LOGGER.error(f"www path: {www_path}, assets exist: {(www_path / 'assets').exists()}")
return
# Add cache-busting parameter to force fresh load
import time
cache_bust = int(time.time())
module_url = f"/cafe-hass/assets/panel-wrapper.js?v={cache_bust}"
# First try to unregister any existing panel
try:
hass.data.get("frontend_panels", {}).pop(DOMAIN, None)
_LOGGER.info("Removed any existing panel registration")
except:
pass
# Register the panel as a custom panel with module_url
await panel_custom.async_register_panel(
hass,
webcomponent_name=PANEL_NAME,
frontend_url_path=DOMAIN,
module_url=module_url,
sidebar_title=PANEL_TITLE,
sidebar_icon=PANEL_ICON,
require_admin=True,
config={},
config_panel_domain=DOMAIN,
)
_LOGGER.info("C.A.F.E. panel registered successfully with iframe wrapper")
def async_unregister_panel(hass: HomeAssistant) -> None:
"""Unregister the C.A.F.E. panel."""
frontend.async_remove_panel(hass, DOMAIN)
_LOGGER.info("C.A.F.E. panel unregistered")
+14
View File
@@ -0,0 +1,14 @@
{
"config": {
"title": "C.A.F.E.",
"step": {
"user": {
"title": "Set up C.A.F.E.",
"description": "C.A.F.E. is a visual automation editor for Home Assistant. It allows you to create and manage your automations using a visual flow editor.\n\nClick Submit to add C.A.F.E. to your Home Assistant."
}
},
"abort": {
"already_configured": "C.A.F.E. is already configured"
}
}
}
@@ -0,0 +1,14 @@
{
"config": {
"title": "C.A.F.E.",
"step": {
"user": {
"title": "Set up C.A.F.E.",
"description": "C.A.F.E. is a visual automation editor for Home Assistant. It allows you to create and manage your automations using a visual flow editor.\n\nClick Submit to add C.A.F.E. to your Home Assistant."
}
},
"abort": {
"already_configured": "C.A.F.E. is already configured"
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
var d=Object.defineProperty;var o=(t,s,e)=>s in t?d(t,s,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[s]=e;var a=(t,s,e)=>o(t,typeof s!="symbol"?s+"":s,e);class m extends HTMLElement{constructor(){super(...arguments);a(this,"_messageHandler");a(this,"iframe",null);a(this,"_hass")}set hass(e){var i;this._hass=e,window.hass=e,(i=this.iframe)!=null&&i.contentWindow&&this.iframe.contentWindow.setHass&&this.iframe.contentWindow.setHass(e)}get hass(){return this._hass}connectedCallback(){var n,r;this.style.display="block",this.style.width="100%",this.style.height="100%",this.style.position="relative";const i=((r=(n=this._hass)==null?void 0:n.themes)==null?void 0:r.darkMode)??!1?"hsl(222.2, 84%, 4.9%)":"hsl(0, 0%, 100%)";this.iframe=document.createElement("iframe"),this.iframe.src="/cafe-hass/index.html",this.iframe.style.width="100%",this.iframe.style.height="100%",this.iframe.style.border="none",this.iframe.style.display="block",this.iframe.style.background=i,this.iframe.setAttribute("allow","clipboard-read *; clipboard-write *"),this.appendChild(this.iframe),this._messageHandler=h=>{var l;h.source===((l=this.iframe)==null?void 0:l.contentWindow)&&h.data&&h.data.type==="CAFE_TOGGLE_SIDEBAR"&&this.dispatchEvent(new Event("hass-toggle-menu",{bubbles:!0,composed:!0}))},window.addEventListener("message",this._messageHandler)}disconnectedCallback(){this.iframe&&(this.removeChild(this.iframe),this.iframe=null),window.hass=void 0,this._messageHandler&&(window.removeEventListener("message",this._messageHandler),this._messageHandler=void 0)}}customElements.get("cafe-panel")||customElements.define("cafe-panel",m);
//# sourceMappingURL=panel-wrapper.js.map
@@ -0,0 +1 @@
{"version":3,"file":"panel-wrapper.js","sources":["../../src/panel-wrapper.ts"],"sourcesContent":["/**\n * Minimal wrapper for Home Assistant panel integration.\n * This web component receives the hass object from HA, exposes it on window,\n * and loads the actual app in an iframe for proper document isolation.\n */\nimport type { HomeAssistant } from './types/hass';\n\n// Type for window with hass\ndeclare const window: Window & {\n hass?: HomeAssistant;\n};\n\nclass CafePanelWrapper extends HTMLElement {\n private _messageHandler?: (event: MessageEvent) => void;\n private iframe: HTMLIFrameElement | null = null;\n private _hass: HomeAssistant | undefined = undefined;\n\n // Properties that HA will set\n set hass(value: HomeAssistant | undefined) {\n this._hass = value;\n // Expose hass on window so iframe can access via window.parent.hass\n window.hass = value;\n\n // Notify the iframe of the update if it has registered a listener\n if (this.iframe?.contentWindow && (this.iframe.contentWindow as any).setHass) {\n (this.iframe.contentWindow as any).setHass(value);\n }\n }\n\n get hass() {\n return this._hass;\n }\n\n connectedCallback() {\n // Style the wrapper to fill the container\n this.style.display = 'block';\n this.style.width = '100%';\n this.style.height = '100%';\n this.style.position = 'relative';\n\n // Detect dark mode from hass to set initial background and avoid white flash\n const isDarkMode = this._hass?.themes?.darkMode ?? false;\n // These match the CSS variables in index.css:\n // Light: --background: 0 0% 100% (white)\n // Dark: --background: 222.2 84% 4.9% (dark blue)\n const bgColor = isDarkMode ? 'hsl(222.2, 84%, 4.9%)' : 'hsl(0, 0%, 100%)';\n\n // Create iframe pointing to the app\n this.iframe = document.createElement('iframe');\n this.iframe.src = '/cafe-hass/index.html';\n this.iframe.style.width = '100%';\n this.iframe.style.height = '100%';\n this.iframe.style.border = 'none';\n this.iframe.style.display = 'block';\n this.iframe.style.background = bgColor;\n // Allow same-origin access\n this.iframe.setAttribute('allow', 'clipboard-read *; clipboard-write *');\n\n this.appendChild(this.iframe);\n\n // Listen for messages from the iframe to trigger sidebar toggle\n this._messageHandler = (event: MessageEvent) => {\n // Only accept messages from our iframe\n if (event.source !== this.iframe?.contentWindow) return;\n if (event.data && event.data.type === 'CAFE_TOGGLE_SIDEBAR') {\n this.dispatchEvent(new Event('hass-toggle-menu', { bubbles: true, composed: true }));\n }\n };\n window.addEventListener('message', this._messageHandler);\n }\n\n disconnectedCallback() {\n if (this.iframe) {\n this.removeChild(this.iframe);\n this.iframe = null;\n }\n // Clean up window properties\n window.hass = undefined;\n if (this._messageHandler) {\n window.removeEventListener('message', this._messageHandler);\n this._messageHandler = undefined;\n }\n }\n}\n\n// Register the custom element\nif (!customElements.get('cafe-panel')) {\n customElements.define('cafe-panel', CafePanelWrapper);\n}\n"],"names":["CafePanelWrapper","__publicField","value","_a","_b","bgColor","event"],"mappings":"oKAYA,MAAMA,UAAyB,WAAY,CAA3C,kCACUC,EAAA,wBACAA,EAAA,cAAmC,MACnCA,EAAA,cAGR,IAAI,KAAKC,EAAkC,CAN7C,IAAAC,EAOI,KAAK,MAAQD,EAEb,OAAO,KAAOA,GAGVC,EAAA,KAAK,SAAL,MAAAA,EAAa,eAAkB,KAAK,OAAO,cAAsB,SAClE,KAAK,OAAO,cAAsB,QAAQD,CAAK,CAEpD,CAEA,IAAI,MAAO,CACT,OAAO,KAAK,KACd,CAEA,mBAAoB,CArBtB,IAAAC,EAAAC,EAuBI,KAAK,MAAM,QAAU,QACrB,KAAK,MAAM,MAAQ,OACnB,KAAK,MAAM,OAAS,OACpB,KAAK,MAAM,SAAW,WAOtB,MAAMC,IAJaD,GAAAD,EAAA,KAAK,QAAL,YAAAA,EAAY,SAAZ,YAAAC,EAAoB,WAAY,GAItB,wBAA0B,mBAGvD,KAAK,OAAS,SAAS,cAAc,QAAQ,EAC7C,KAAK,OAAO,IAAM,wBAClB,KAAK,OAAO,MAAM,MAAQ,OAC1B,KAAK,OAAO,MAAM,OAAS,OAC3B,KAAK,OAAO,MAAM,OAAS,OAC3B,KAAK,OAAO,MAAM,QAAU,QAC5B,KAAK,OAAO,MAAM,WAAaC,EAE/B,KAAK,OAAO,aAAa,QAAS,qCAAqC,EAEvE,KAAK,YAAY,KAAK,MAAM,EAG5B,KAAK,gBAAmBC,GAAwB,CAjDpD,IAAAH,EAmDUG,EAAM,WAAWH,EAAA,KAAK,SAAL,YAAAA,EAAa,gBAC9BG,EAAM,MAAQA,EAAM,KAAK,OAAS,uBACpC,KAAK,cAAc,IAAI,MAAM,mBAAoB,CAAE,QAAS,GAAM,SAAU,EAAA,CAAM,CAAC,CAEvF,EACA,OAAO,iBAAiB,UAAW,KAAK,eAAe,CACzD,CAEA,sBAAuB,CACjB,KAAK,SACP,KAAK,YAAY,KAAK,MAAM,EAC5B,KAAK,OAAS,MAGhB,OAAO,KAAO,OACV,KAAK,kBACP,OAAO,oBAAoB,UAAW,KAAK,eAAe,EAC1D,KAAK,gBAAkB,OAE3B,CACF,CAGK,eAAe,IAAI,YAAY,GAClC,eAAe,OAAO,aAAcN,CAAgB"}
+30
View File
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>C.A.F.E. - Home Assistant</title>
<script>
// Set background color immediately to avoid white flash in dark mode
// This runs before CSS loads, checking parent window's hass dark mode setting
(function() {
try {
var isDark = window.parent && window.parent.hass && window.parent.hass.themes && window.parent.hass.themes.darkMode;
// Match CSS variables: light = hsl(0, 0%, 100%), dark = hsl(222.2, 84%, 4.9%)
var bg = isDark ? 'hsl(222.2, 84%, 4.9%)' : 'hsl(0, 0%, 100%)';
document.documentElement.style.background = bg;
// Inject style for body since it doesn't exist yet
var style = document.createElement('style');
style.textContent = 'body { background: ' + bg + ' !important; }';
document.head.appendChild(style);
} catch (e) {}
})();
</script>
<script type="module" crossorigin src="/cafe-hass/assets/main-BIzscans.js"></script>
<link rel="stylesheet" crossorigin href="/cafe-hass/assets/main-DfXhj31a.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
"""The Cync Room Lights integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .cync_hub import CyncHub
PLATFORMS: list[str] = ["light","binary_sensor","switch","fan"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cync Room Lights from a config entry."""
hass.data.setdefault(DOMAIN, {})
remove_options_update_listener = entry.add_update_listener(options_update_listener)
hub = CyncHub(entry.data, entry.options, remove_options_update_listener)
hass.data[DOMAIN][entry.entry_id] = hub
hub.start_tcp_client()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def options_update_listener(
hass: HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hub = hass.data[DOMAIN][entry.entry_id]
hub.remove_options_update_listener()
hub.disconnect()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
@@ -0,0 +1,123 @@
"""Platform for binary sensor integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.binary_sensor import (BinarySensorDeviceClass, BinarySensorEntity)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import DeviceInfo
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback
) -> None:
hub = hass.data[DOMAIN][config_entry.entry_id]
new_devices = []
for sensor in hub.cync_motion_sensors:
if not hub.cync_motion_sensors[sensor]._update_callback and sensor in config_entry.options["motion_sensors"]:
new_devices.append(CyncMotionSensorEntity(hub.cync_motion_sensors[sensor]))
for sensor in hub.cync_ambient_light_sensors:
if not hub.cync_ambient_light_sensors[sensor]._update_callback and sensor in config_entry.options["ambient_light_sensors"]:
new_devices.append(CyncAmbientLightSensorEntity(hub.cync_ambient_light_sensors[sensor]))
if new_devices:
async_add_entities(new_devices)
class CyncMotionSensorEntity(BinarySensorEntity):
"""Representation of a Cync Motion Sensor."""
should_poll = False
def __init__(self, motion_sensor) -> None:
"""Initialize the sensor."""
self.motion_sensor = motion_sensor
async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA."""
self.motion_sensor.register(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass."""
self.motion_sensor.reset()
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers = {(DOMAIN, f"{self.motion_sensor.room.name} ({self.motion_sensor.home_name})")},
manufacturer = "Cync by Savant",
name = f"{self.motion_sensor.room.name} ({self.motion_sensor.home_name})",
suggested_area = f"{self.motion_sensor.room.name}",
)
@property
def unique_id(self) -> str:
"""Return Unique ID string."""
return 'cync_motion_sensor_' + self.motion_sensor.device_id
@property
def name(self) -> str:
"""Return the name of the motion_sensor."""
return self.motion_sensor.name + " Motion"
@property
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.motion_sensor.motion
@property
def device_class(self) -> str | None:
"""Return the device class"""
return BinarySensorDeviceClass.MOTION
class CyncAmbientLightSensorEntity(BinarySensorEntity):
"""Representation of a Cync Ambient Light Sensor."""
should_poll = False
def __init__(self, ambient_light_sensor) -> None:
"""Initialize the sensor."""
self.ambient_light_sensor = ambient_light_sensor
async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA."""
self.ambient_light_sensor.register(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass."""
self.ambient_light_sensor.reset()
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers = {(DOMAIN, f"{self.ambient_light_sensor.room.name} ({self.ambient_light_sensor.home_name})")},
manufacturer = "Cync by Savant",
name = f"{self.ambient_light_sensor.room.name} ({self.ambient_light_sensor.home_name})",
suggested_area = f"{self.ambient_light_sensor.room.name}",
)
@property
def unique_id(self) -> str:
"""Return Unique ID string."""
return 'cync_ambient_light_sensor_' + self.ambient_light_sensor.device_id
@property
def name(self) -> str:
"""Return the name of the ambient_light_sensor."""
return self.ambient_light_sensor.name + " Ambient Light"
@property
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.ambient_light_sensor.ambient_light
@property
def device_class(self) -> str | None:
"""Return the device class"""
return BinarySensorDeviceClass.LIGHT
@@ -0,0 +1,289 @@
"""Config flow for Cync Room Lights integration."""
from __future__ import annotations
import logging
import voluptuous as vol
from typing import Any
from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.core import callback
from .const import DOMAIN
from .cync_hub import CyncUserData
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required("username"): str,
vol.Required("password"): str,
}
)
STEP_TWO_FACTOR_CODE = vol.Schema(
{
vol.Required("two_factor_code"): str,
}
)
async def cync_login(hub, user_input: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input"""
response = await hub.authenticate(user_input["username"], user_input["password"])
if response['authorized']:
return {'title':'cync_lights_'+ user_input['username'],'data':{'cync_credentials': hub.auth_code, 'user_input':user_input}}
else:
if response['two_factor_code_required']:
raise TwoFactorCodeRequired
else:
raise InvalidAuth
async def submit_two_factor_code(hub, user_input: dict[str, Any]) -> dict[str, Any]:
"""Validate the two factor code"""
response = await hub.auth_two_factor(user_input["two_factor_code"])
if response['authorized']:
return {'title':'cync_lights_'+ hub.username,'data':{'cync_credentials': hub.auth_code, 'user_input': {'username':hub.username,'password':hub.password}}}
else:
raise InvalidAuth
class CyncConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Cync Room Lights."""
def __init__(self):
self.cync_hub = CyncUserData()
self.data ={}
self.options = {}
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle user and password for Cync account."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
try:
info = await cync_login(self.cync_hub, user_input)
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
except TwoFactorCodeRequired:
return await self.async_step_two_factor_code()
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as e: # pylint: disable=broad-except
_LOGGER.error(e)
errors["base"] = "unknown"
else:
self.data = info
return await self.async_step_finish_setup()
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_two_factor_code(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle two factor authentication for Cync account."""
if user_input is None:
return self.async_show_form(
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE
)
errors = {}
try:
info = await submit_two_factor_code(self.cync_hub, user_input)
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as e: # pylint: disable=broad-except
_LOGGER.error(e)
errors["base"] = "unknown"
else:
self.data = info
return await self.async_step_select_switches()
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_select_switches(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Select rooms and individual switches for entity creation"""
if user_input is not None:
self.options = user_input
return await self._async_finish_setup()
switches_data_schema = vol.Schema(
{
vol.Optional(
"rooms",
description = {"suggested_value" : [room for room in self.data["data"]["cync_config"]["rooms"].keys() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']]},
): cv.multi_select({room : f'{room_info["name"]} ({room_info["home_name"]})' for room,room_info in self.data["data"]["cync_config"]["rooms"].items() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
vol.Optional(
"subgroups",
description = {"suggested_value" : [room for room in self.data["data"]["cync_config"]["rooms"].keys() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']]},
): cv.multi_select({room : f'{room_info["name"]} ({room_info.get("parent_room","")}:{room_info["home_name"]})' for room,room_info in self.data["data"]["cync_config"]["rooms"].items() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
vol.Optional(
"switches",
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['FAN']]},
): cv.multi_select({switch_id : f'{sw_info["name"]} ({sw_info["room_name"]}:{sw_info["home_name"]})' for switch_id,sw_info in self.data["data"]["cync_config"]["devices"].items() if sw_info.get('ONOFF',False) and sw_info.get('MULTIELEMENT',1) == 1}),
vol.Optional(
"motion_sensors",
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['MOTION']]},
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info.get('MOTION',False)}),
vol.Optional(
"ambient_light_sensors",
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['AMBIENT_LIGHT']]},
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info.get('AMBIENT_LIGHT',False)}),
}
)
return self.async_show_form(step_id="select_switches", data_schema=switches_data_schema)
async def _async_finish_setup(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Finish setup and create entry"""
existing_entry = await self.async_set_unique_id(self.data['title'])
if not existing_entry:
return self.async_create_entry(title=self.data["title"], data=self.data["data"], options=self.options)
else:
self.hass.config_entries.async_update_entry(existing_entry, data=self.data['data'], options=self.options)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.hass.config_entries.async_abort(reason="reauth_successful")
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return CyncOptionsFlowHandler(config_entry)
class CyncOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.entry = config_entry
self.cync_hub = CyncUserData()
self.data = {}
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
if user_input['re-authenticate'] == "No":
return await self.async_step_select_switches()
else:
return await self.async_step_auth()
data_schema = vol.Schema(
{
vol.Required(
"re-authenticate",default="No"): vol.In(["Yes","No"]),
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)
async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Attempt to re-authenticate"""
errors = {}
try:
info = await cync_login(self.cync_hub, self.entry.data['user_input'])
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
except TwoFactorCodeRequired:
return await self.async_step_two_factor_code()
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as e: # pylint: disable=broad-except
_LOGGER.error(e)
errors["base"] = "unknown"
else:
self.data = info
return await self.async_step_select_switches()
async def async_step_two_factor_code(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle two factor authentication for Cync account."""
if user_input is None:
return self.async_show_form(
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE
)
errors = {}
try:
info = await submit_two_factor_code(self.cync_hub, user_input)
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as e: # pylint: disable=broad-except
_LOGGER.error(e)
errors["base"] = "unknown"
else:
self.data = info
return await self.async_step_select_switches()
return self.async_show_form(
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE, errors=errors
)
async def async_step_select_switches(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if "data" in self.data and self.data["data"] != self.entry.data:
self.hass.config_entries.async_update_entry(self.entry, data = self.data["data"])
if user_input is not None:
return self.async_create_entry(title="",data=user_input)
switches_data_schema = vol.Schema(
{
vol.Optional(
"rooms",
description = {"suggested_value" : [room for room in self.entry.options["rooms"] if room in self.entry.data["cync_config"]["rooms"].keys()]},
): cv.multi_select({room : f'{room_info["name"]} ({room_info["home_name"]})' for room,room_info in self.entry.data["cync_config"]["rooms"].items() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
vol.Optional(
"subgroups",
description = {"suggested_value" : [room for room in self.entry.options["subgroups"] if room in self.entry.data["cync_config"]["rooms"].keys()]},
): cv.multi_select({room : f'{room_info["name"]} ({room_info.get("parent_room","")}:{room_info["home_name"]})' for room,room_info in self.entry.data["cync_config"]["rooms"].items() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
vol.Optional(
"switches",
description = {"suggested_value" : [sw for sw in self.entry.options["switches"] if sw in self.entry.data["cync_config"]["devices"].keys()]},
): cv.multi_select({switch_id : f'{sw_info["name"]} ({sw_info["room_name"]}:{sw_info["home_name"]})' for switch_id,sw_info in self.entry.data["cync_config"]["devices"].items() if sw_info.get('ONOFF',False) and sw_info.get('MULTIELEMENT',1) == 1}),
vol.Optional(
"motion_sensors",
description = {"suggested_value" : [sensor for sensor in self.entry.options["motion_sensors"] if sensor in self.entry.data["cync_config"]["devices"].keys()]},
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.entry.data["cync_config"]["devices"].items() if device_info.get('MOTION',False)}),
vol.Optional(
"ambient_light_sensors",
description = {"suggested_value" : [sensor for sensor in self.entry.options["ambient_light_sensors"] if sensor in self.entry.data["cync_config"]["devices"].keys()]},
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.entry.data["cync_config"]["devices"].items() if device_info.get('AMBIENT_LIGHT',False)}),
}
)
return self.async_show_form(step_id="select_switches", data_schema=switches_data_schema)
class TwoFactorCodeRequired(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
+3
View File
@@ -0,0 +1,3 @@
"""Constants for the Cync Room Lights integration."""
DOMAIN = "cync_lights"
+847
View File
@@ -0,0 +1,847 @@
import logging
import threading
import asyncio
import struct
import aiohttp
import math
import ssl
from typing import Any
_LOGGER = logging.getLogger(__name__)
API_AUTH = "https://api.gelighting.com/v2/user_auth"
API_REQUEST_CODE = "https://api.gelighting.com/v2/two_factor/email/verifycode"
API_2FACTOR_AUTH = "https://api.gelighting.com/v2/user_auth/two_factor"
API_DEVICES = "https://api.gelighting.com/v2/user/{user}/subscribe/devices"
API_DEVICE_INFO = "https://api.gelighting.com/v2/product/{product_id}/device/{device_id}/property"
Capabilities = {
"ONOFF":[1,5,6,7,8,9,10,11,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,47,48,49,51,52,53,54,55,56,57,58,59,61,62,63,64,65,66,67,68,80,81,82,83,85,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171,172],
"BRIGHTNESS":[1,5,6,7,8,9,10,11,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,47,48,49,55,56,80,81,82,83,85,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171],
"COLORTEMP":[5,6,7,8,10,11,14,15,19,20,21,22,23,25,26,28,29,30,31,32,33,34,35,47,80,82,83,85,129,130,131,132,133,135,136,137,138,139,140,141,142,143,144,145,146,147,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171],
"RGB":[6,7,8,21,22,23,30,31,32,33,34,35,47,131,132,133,137,138,139,140,141,142,143,146,147,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171],
"MOTION":[37,49,54],
"AMBIENT_LIGHT":[37,49,54],
"WIFICONTROL":[36,37,38,39,40,47,48,49,51,52,53,54,55,56,57,58,59,61,62,63,64,65,66,67,68,80,81,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171,172],
"PLUG":[64,65,66,67,68,172],
"FAN":[81],
"MULTIELEMENT":{'67':2}
}
class CyncHub:
def __init__(self, user_data, options, remove_options_update_listener):
self.thread = None
self.loop = None
self.reader = None
self.writer = None
self.login_code = bytearray(user_data['cync_credentials'])
self.logged_in = False
self.home_devices = user_data['cync_config']['home_devices']
self.home_controllers = user_data['cync_config']['home_controllers']
self.switchID_to_homeID = user_data['cync_config']['switchID_to_homeID']
self.connected_devices = {home_id:[] for home_id in self.home_controllers.keys()}
self.shutting_down = False
self.remove_options_update_listener = remove_options_update_listener
self.cync_rooms = {room_id:CyncRoom(room_id,room_info,self) for room_id,room_info in user_data['cync_config']['rooms'].items()}
self.cync_switches = {device_id:CyncSwitch(device_id,switch_info,self.cync_rooms.get(switch_info['room'], None),self) for device_id,switch_info in user_data['cync_config']['devices'].items() if switch_info.get("ONOFF",False)}
self.cync_motion_sensors = {device_id:CyncMotionSensor(device_id,device_info,self.cync_rooms.get(device_info['room'], None)) for device_id,device_info in user_data['cync_config']['devices'].items() if device_info.get("MOTION",False)}
self.cync_ambient_light_sensors = {device_id:CyncAmbientLightSensor(device_id,device_info,self.cync_rooms.get(device_info['room'], None)) for device_id,device_info in user_data['cync_config']['devices'].items() if device_info.get("AMBIENT_LIGHT",False)}
self.switchID_to_deviceIDs = {device_info.switch_id:[dev_id for dev_id, dev_info in self.cync_switches.items() if dev_info.switch_id == device_info.switch_id] for device_id, device_info in self.cync_switches.items() if int(device_info.switch_id) > 0}
self.connected_devices_updated = False
self.options = options
self._seq_num = 0
self.pending_commands = {}
[room.initialize() for room in self.cync_rooms.values() if room.is_subgroup]
[room.initialize() for room in self.cync_rooms.values() if not room.is_subgroup]
def start_tcp_client(self):
self.thread = threading.Thread(target=self._start_tcp_client,daemon=True)
self.thread.start()
def _start_tcp_client(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self._connect())
def disconnect(self):
self.shutting_down = True
for home_controllers in self.home_controllers.values(): #send packets to server to generate data to be read which will initiate shutdown
for controller in home_controllers:
seq = self.get_seq_num()
state_request = bytes.fromhex('7300000018') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('007e00000000f85206000000ffff0000567e')
self.loop.call_soon_threadsafe(self.send_request,state_request)
async def _connect(self):
while not self.shutting_down:
try:
context = ssl.create_default_context()
try:
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23779, ssl = context)
except Exception as e:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
try:
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23779, ssl = context)
except Exception as e:
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23778)
except Exception as e:
_LOGGER.error(e)
await asyncio.sleep(5)
else:
read_tcp_messages = asyncio.create_task(self._read_tcp_messages(), name = "Read TCP Messages")
maintain_connection = asyncio.create_task(self._maintain_connection(), name = "Maintain Connection")
update_state = asyncio.create_task(self._update_state(), name = "Update State")
update_connected_devices = asyncio.create_task(self._update_connected_devices(), name = "Update Connected Devices")
read_write_tasks = [read_tcp_messages, maintain_connection, update_state, update_connected_devices]
try:
done, pending = await asyncio.wait(read_write_tasks,return_when=asyncio.FIRST_EXCEPTION)
for task in done:
name = task.get_name()
exception = task.exception()
try:
result = task.result()
except Exception as e:
_LOGGER.error(e)
for task in pending:
task.cancel()
if not self.shutting_down:
_LOGGER.info("Connection to Cync server reset, restarting in 15 seconds")
await asyncio.sleep(15)
else:
_LOGGER.info("Cync client shutting down")
except Exception as e:
_LOGGER.error(e)
async def _read_tcp_messages(self):
self.writer.write(self.login_code)
await self.writer.drain()
await self.reader.read(1000)
self.logged_in = True
while not self.shutting_down:
data = await self.reader.read(1000)
if len(data) == 0:
self.logged_in = False
raise LostConnection
while len(data) >= 12:
packet_type = int(data[0])
packet_length = struct.unpack(">I", data[1:5])[0]
packet = data[5:packet_length+5]
try:
if packet_length == len(packet):
if packet_type == 115:
switch_id = str(struct.unpack(">I", packet[0:4])[0])
home_id = self.switchID_to_homeID[switch_id]
#send response packet
response_id = struct.unpack(">H", packet[4:6])[0]
response_packet = bytes.fromhex('7300000007') + int(switch_id).to_bytes(4,'big') + response_id.to_bytes(2,'big') + bytes.fromhex('00')
self.loop.call_soon_threadsafe(self.send_request, response_packet)
if packet_length >= 33 and int(packet[13]) == 219:
#parse state and brightness change packet
deviceID = self.home_devices[home_id][int(packet[21])]
state = int(packet[27]) > 0
brightness = int(packet[28]) if state else 0
if deviceID in self.cync_switches:
self.cync_switches[deviceID].update_switch(state,brightness,self.cync_switches[deviceID].color_temp,self.cync_switches[deviceID].rgb)
elif packet_length >= 25 and int(packet[13]) == 84:
#parse motion and ambient light sensor packet
deviceID = self.home_devices[home_id][int(packet[16])]
motion = int(packet[22]) > 0
ambient_light = int(packet[24]) > 0
if deviceID in self.cync_motion_sensors:
self.cync_motion_sensors[deviceID].update_motion_sensor(motion)
if deviceID in self.cync_ambient_light_sensors:
self.cync_ambient_light_sensors[deviceID].update_ambient_light_sensor(ambient_light)
elif packet_length > 51 and int(packet[13]) == 82:
#parse initial state packet
switch_id = str(struct.unpack(">I", packet[0:4])[0])
home_id = self.switchID_to_homeID[switch_id]
self._add_connected_devices(switch_id, home_id)
packet = packet[22:]
while len(packet) > 24:
deviceID = self.home_devices[home_id][int(packet[0])]
if deviceID in self.cync_switches:
if self.cync_switches[deviceID].elements > 1:
for i in range(self.cync_switches[deviceID].elements):
device_id = self.home_devices[home_id][(i+1)*256 + int(packet[0])]
state = int((int(packet[12]) >> i) & int(packet[8])) > 0
brightness = 100 if state else 0
self.cync_switches[device_id].update_switch(state,brightness,self.cync_switches[device_id].color_temp,self.cync_switches[device_id].rgb)
else:
state = int(packet[8]) > 0
brightness = int(packet[12]) if state else 0
color_temp = int(packet[16])
rgb = {'r':int(packet[20]),'g':int(packet[21]),'b':int(packet[22]),'active':int(packet[16])==254}
self.cync_switches[deviceID].update_switch(state,brightness,color_temp,rgb)
packet = packet[24:]
elif packet_type == 131:
switch_id = str(struct.unpack(">I", packet[0:4])[0])
home_id = self.switchID_to_homeID[switch_id]
if packet_length >= 33 and int(packet[13]) == 219:
#parse state and brightness change packet
deviceID = self.home_devices[home_id][int(packet[21])]
state = int(packet[27]) > 0
brightness = int(packet[28]) if state else 0
if deviceID in self.cync_switches:
self.cync_switches[deviceID].update_switch(state,brightness,self.cync_switches[deviceID].color_temp,self.cync_switches[deviceID].rgb)
elif packet_length >= 25 and int(packet[13]) == 84:
#parse motion and ambient light sensor packet
deviceID = self.home_devices[home_id][int(packet[16])]
motion = int(packet[22]) > 0
ambient_light = int(packet[24]) > 0
if deviceID in self.cync_motion_sensors:
self.cync_motion_sensors[deviceID].update_motion_sensor(motion)
if deviceID in self.cync_ambient_light_sensors:
self.cync_ambient_light_sensors[deviceID].update_ambient_light_sensor(ambient_light)
elif packet_type == 67 and packet_length >= 26 and int(packet[4]) == 1 and int(packet[5]) == 1 and int(packet[6]) == 6:
#parse state packet
switch_id = str(struct.unpack(">I", packet[0:4])[0])
home_id = self.switchID_to_homeID[switch_id]
packet = packet[7:]
while len(packet) >= 19:
if int(packet[3]) < len(self.home_devices[home_id]):
deviceID = self.home_devices[home_id][int(packet[3])]
if deviceID in self.cync_switches:
if self.cync_switches[deviceID].elements > 1:
for i in range(self.cync_switches[deviceID].elements):
device_id = self.home_devices[home_id][(i+1)*256 + int(packet[3])]
state = int((int(packet[5]) >> i) & int(packet[4])) > 0
brightness = 100 if state else 0
self.cync_switches[device_id].update_switch(state,brightness,self.cync_switches[device_id].color_temp,self.cync_switches[device_id].rgb)
else:
state = int(packet[4]) > 0
brightness = int(packet[5]) if state else 0
color_temp = int(packet[6])
rgb = {'r':int(packet[7]),'g':int(packet[8]),'b':int(packet[9]),'active':int(packet[6])==254}
self.cync_switches[deviceID].update_switch(state,brightness,color_temp,rgb)
packet = packet[19:]
elif packet_type == 171:
switch_id = str(struct.unpack(">I", packet[0:4])[0])
home_id = self.switchID_to_homeID[switch_id]
self._add_connected_devices(switch_id, home_id)
elif packet_type == 123:
seq = str(struct.unpack(">H", packet[4:6])[0])
command_received = self.pending_commands.get(seq,None)
if command_received is not None:
command_received(seq)
except Exception as e:
_LOGGER.error(e)
data = data[packet_length+5:]
raise ShuttingDown
async def _maintain_connection(self):
while not self.shutting_down:
await asyncio.sleep(180)
self.writer.write(bytes.fromhex('d300000000'))
await self.writer.drain()
raise ShuttingDown
def _add_connected_devices(self,switch_id, home_id):
for dev in self.switchID_to_deviceIDs[switch_id]:
#update list of WiFi connected devices
if dev not in self.connected_devices[home_id]:
self.connected_devices[home_id].append(dev)
if self.connected_devices_updated:
for dev in self.cync_switches.values():
dev.update_controllers()
for room in self.cync_rooms.values():
room.update_controllers()
async def _update_connected_devices(self):
while not self.shutting_down:
self.connected_devices_updated = False
for devices in self.connected_devices.values():
devices.clear()
while not self.logged_in:
await asyncio.sleep(2)
attempts = 0
while True in [len(devices) < len(self.home_controllers[home_id]) * 0.5 for home_id,devices in self.connected_devices.items()] and attempts < 10:
for home_id, home_controllers in self.home_controllers.items():
for controller in home_controllers:
seq = self.get_seq_num()
ping = bytes.fromhex('a300000007') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('00')
self.loop.call_soon_threadsafe(self.send_request, ping)
await asyncio.sleep(0.15)
await asyncio.sleep(2)
attempts += 1
for dev in self.cync_switches.values():
dev.update_controllers()
for room in self.cync_rooms.values():
room.update_controllers()
self.connected_devices_updated = True
await asyncio.sleep(3600)
raise ShuttingDown
async def _update_state(self):
while not self.connected_devices_updated:
await asyncio.sleep(2)
for connected_devices in self.connected_devices.values():
if len(connected_devices) > 0:
controller = self.cync_switches[connected_devices[0]].switch_id
seq = self.get_seq_num()
state_request = bytes.fromhex('7300000018') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('007e00000000f85206000000ffff0000567e')
self.loop.call_soon_threadsafe(self.send_request,state_request)
while False in [self.cync_switches[dev_id]._update_callback is not None for dev_id in self.options["switches"]] and False in [self.cync_rooms[dev_id]._update_callback is not None for dev_id in self.options["rooms"]]:
await asyncio.sleep(2)
for dev in self.cync_switches.values():
dev.publish_update()
for room in self.cync_rooms.values():
dev.publish_update()
def send_request(self,request):
async def send():
self.writer.write(request)
await self.writer.drain()
self.loop.create_task(send())
def combo_control(self,state,brightness,color_tone,rgb,switch_id,mesh_id,seq):
combo_request = bytes.fromhex('7300000022') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8f010000000000000') + mesh_id + bytes.fromhex('f00000') + (1 if state else 0).to_bytes(1,'big') + brightness.to_bytes(1,'big') + color_tone.to_bytes(1,'big') + rgb[0].to_bytes(1,'big') + rgb[1].to_bytes(1,'big') + rgb[2].to_bytes(1,'big') + ((496 + int(mesh_id[0]) + int(mesh_id[1]) + (1 if state else 0) + brightness + color_tone + sum(rgb))%256).to_bytes(1,'big') + bytes.fromhex('7e')
self.loop.call_soon_threadsafe(self.send_request,combo_request)
def turn_on(self,switch_id,mesh_id,seq):
power_request = bytes.fromhex('730000001f') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8d00d000000000000') + mesh_id + bytes.fromhex('d00000010000') + ((430 + int(mesh_id[0]) + int(mesh_id[1]))%256).to_bytes(1,'big') + bytes.fromhex('7e')
self.loop.call_soon_threadsafe(self.send_request,power_request)
def turn_off(self,switch_id,mesh_id,seq):
power_request = bytes.fromhex('730000001f') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8d00d000000000000') + mesh_id + bytes.fromhex('d00000000000') + ((429 + int(mesh_id[0]) + int(mesh_id[1]))%256).to_bytes(1,'big') + bytes.fromhex('7e')
self.loop.call_soon_threadsafe(self.send_request,power_request)
def set_color_temp(self,color_temp,switch_id,mesh_id,seq):
color_temp_request = bytes.fromhex('730000001e') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8e20c000000000000') + mesh_id + bytes.fromhex('e2000005') + color_temp.to_bytes(1,'big') + ((469 + int(mesh_id[0]) + int(mesh_id[1]) + color_temp)%256).to_bytes(1,'big') + bytes.fromhex('7e')
self.loop.call_soon_threadsafe(self.send_request,color_temp_request)
def get_seq_num(self):
if self._seq_num == 65535:
self._seq_num = 1
else:
self._seq_num += 1
return self._seq_num
class CyncRoom:
def __init__(self, room_id, room_info, hub):
self.hub = hub
self.room_id = room_id
self.home_id = room_id.split('-')[0]
self.name = room_info.get('name','unknown')
self.home_name = room_info.get('home_name','unknown')
self.parent_room = room_info.get('parent_room', 'unknown')
self.mesh_id = int(room_info.get('mesh_id',0)).to_bytes(2,'little')
self.power_state = False
self.brightness = 0
self.color_temp = 0
self.rgb = {'r':0, 'g':0, 'b':0, 'active': False}
self.switches = room_info.get('switches',[])
self.subgroups = room_info.get('subgroups',[])
self.is_subgroup = room_info.get('isSubgroup', False)
self.all_room_switches = self.switches
self.controllers = []
self.default_controller = room_info.get('room_controller',self.hub.home_controllers[self.home_id][0])
self._update_callback = None
self._update_parent_room = None
self.support_brightness = False
self.support_color_temp = False
self.support_rgb = False
self.switches_support_brightness = False
self.switches_support_color_temp = False
self.switches_support_rgb = False
self.groups_support_brightness = False
self.groups_support_color_temp = False
self.groups_support_rgb = False
self._command_timout = 0.5
self._command_retry_time = 5
def initialize(self):
"""Initialization of supported features and registration of update function for all switches and subgroups in the room"""
self.switches_support_brightness = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_brightness]
self.switches_support_color_temp = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_color_temp]
self.switches_support_rgb = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_rgb]
self.groups_support_brightness = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_brightness]
self.groups_support_color_temp = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_color_temp]
self.groups_support_rgb = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_rgb]
self.support_brightness = (len(self.switches_support_brightness) + len(self.groups_support_brightness)) > 0
self.support_color_temp = (len(self.switches_support_color_temp) + len(self.groups_support_color_temp)) > 0
self.support_rgb = (len(self.switches_support_rgb) + len(self.groups_support_rgb)) > 0
for switch_id in self.switches:
self.hub.cync_switches[switch_id].register_room_updater(self.update_room)
for subgroup in self.subgroups:
self.hub.cync_rooms[subgroup].register_room_updater(self.update_room)
self.all_room_switches = self.all_room_switches + self.hub.cync_rooms[subgroup].switches
for subgroup in self.subgroups:
self.hub.cync_rooms[subgroup].all_room_switches = self.all_room_switches
def register(self, update_callback) -> None:
"""Register callback, called when switch changes state."""
self._update_callback = update_callback
def reset(self) -> None:
"""Remove previously registered callback."""
self._update_callback = None
def register_room_updater(self, parent_updater):
self._update_parent_room = parent_updater
@property
def max_color_temp_kelvin(self) -> int:
"""Return maximum supported color temperature."""
return 7000
@property
def min_color_temp_kelvin(self) -> int:
"""Return minimum supported color temperature."""
return 2000
async def turn_on(self, attr_rgb, attr_br, attr_ct) -> None:
"""Turn on the light."""
attempts = 0
update_received = False
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
seq = str(self.hub.get_seq_num())
if len(self.controllers) > 0:
controller = self.controllers[attempts%len(self.controllers)]
else:
controller = self.default_controller
if attr_rgb is not None and attr_br is not None:
if math.isclose(attr_br, max([self.rgb['r'],self.rgb['g'],self.rgb['b']])*self.brightness/100, abs_tol = 2):
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
else:
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
elif attr_rgb is None and attr_ct is None and attr_br is not None:
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
elif attr_rgb is not None and attr_br is None:
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
elif attr_ct is not None:
self.hub.turn_on(controller, self.mesh_id, seq)
# Cync uses the range 0% to 100% to set the color temp, so we need to
# calculate the percentage of the color temp range that is being requested
color_temp = round(100*((attr_ct - self.min_color_temp_kelvin) / (self.max_color_temp_kelvin - self.min_color_temp_kelvin)))
self.hub.set_color_temp(color_temp, controller, self.mesh_id, seq)
else:
self.hub.turn_on(controller, self.mesh_id, seq)
self.hub.pending_commands[seq] = self.command_received
await asyncio.sleep(self._command_timout)
if self.hub.pending_commands.get(seq, None) is not None:
self.hub.pending_commands.pop(seq)
attempts += 1
else:
update_received = True
async def turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
attempts = 0
update_received = False
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
seq = str(self.hub.get_seq_num())
if len(self.controllers) > 0:
controller = self.controllers[attempts%len(self.controllers)]
else:
controller = self.default_controller
self.hub.turn_off(controller, self.mesh_id, seq)
self.hub.pending_commands[seq] = self.command_received
await asyncio.sleep(self._command_timout)
if self.hub.pending_commands.get(seq, None) is not None:
self.hub.pending_commands.pop(seq)
attempts += 1
else:
update_received = True
def command_received(self, seq):
"""Remove command from hub.pending_commands when a reply is received from Cync server"""
if self.hub.pending_commands.get(seq) is not None:
self.hub.pending_commands.pop(seq)
def update_room(self):
"""Update the current state of the room"""
_brightness = self.brightness
_color_temp = self.color_temp
_rgb = self.rgb
_power_state = True in ([self.hub.cync_switches[device_id].power_state for device_id in self.switches] + [self.hub.cync_rooms[room_id].power_state for room_id in self.subgroups])
if self.support_brightness:
_brightness = round(sum([self.hub.cync_switches[device_id].brightness for device_id in self.switches] + [self.hub.cync_rooms[room_id].brightness for room_id in self.subgroups])/(len(self.switches) + len(self.subgroups)))
else:
_brightness = 100 if _power_state else 0
if self.support_color_temp:
_color_temp = round(sum([self.hub.cync_switches[device_id].color_temp for device_id in self.switches_support_color_temp] + [self.hub.cync_rooms[room_id].color_temp for room_id in self.groups_support_color_temp])/(len(self.switches_support_color_temp) + len(self.groups_support_color_temp)))
if self.support_rgb:
_rgb['r'] = round(sum([self.hub.cync_switches[device_id].rgb['r'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['r'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
_rgb['g'] = round(sum([self.hub.cync_switches[device_id].rgb['g'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['g'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
_rgb['b'] = round(sum([self.hub.cync_switches[device_id].rgb['b'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['b'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
_rgb['active'] = True in ([self.hub.cync_switches[device_id].rgb['active'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['active'] for room_id in self.groups_support_rgb])
if _power_state != self.power_state or _brightness != self.brightness or _color_temp != self.color_temp or _rgb != self.rgb:
self.power_state = _power_state
self.brightness = _brightness
self.color_temp = _color_temp
self.rgb = _rgb
self.publish_update()
if self._update_parent_room:
self._update_parent_room()
def update_controllers(self):
"""Update the list of responsive, Wi-Fi connected controller devices"""
connected_devices = self.hub.connected_devices[self.home_id]
controllers = []
if len(connected_devices) > 0:
controllers = [self.hub.cync_switches[dev_id].switch_id for dev_id in self.all_room_switches if dev_id in connected_devices]
others_available = [self.hub.cync_switches[dev_id].switch_id for dev_id in connected_devices]
for controller in controllers:
if controller in others_available:
others_available.remove(controller)
self.controllers = controllers + others_available
else:
self.controllers = [self.default_controller]
def publish_update(self):
if self._update_callback:
self._update_callback()
class CyncSwitch:
def __init__(self, device_id, switch_info, room, hub):
self.hub = hub
self.device_id = device_id
self.switch_id = switch_info.get('switch_id','0')
self.home_id = [home_id for home_id, home_devices in self.hub.home_devices.items() if self.device_id in home_devices][0]
self.name = switch_info.get('name','unknown')
self.home_name = switch_info.get('home_name','unknown')
self.mesh_id = switch_info.get('mesh_id',0).to_bytes(2,'little')
self.room = room
self.power_state = False
self.brightness = 0
self.color_temp = 0
self.rgb = {'r':0, 'g':0, 'b':0, 'active':False}
self.default_controller = switch_info.get('switch_controller',self.hub.home_controllers[self.home_id][0])
self.controllers = []
self._update_callback = None
self._update_parent_room = None
self.support_brightness = switch_info.get('BRIGHTNESS',False)
self.support_color_temp = switch_info.get('COLORTEMP',False)
self.support_rgb = switch_info.get('RGB',False)
self.plug = switch_info.get('PLUG',False)
self.fan = switch_info.get('FAN',False)
self.elements = switch_info.get('MULTIELEMENT',1)
self._command_timout = 0.5
self._command_retry_time = 5
def register(self, update_callback) -> None:
"""Register callback, called when switch changes state."""
self._update_callback = update_callback
def reset(self) -> None:
"""Remove previously registered callback."""
self._update_callback = None
def register_room_updater(self, parent_updater):
self._update_parent_room = parent_updater
@property
def max_color_temp_kelvin(self) -> int:
"""Return maximum supported color temperature."""
return 7000
@property
def min_color_temp_kelvin(self) -> int:
"""Return minimum supported color temperature."""
return 2000
async def turn_on(self, attr_rgb, attr_br, attr_ct) -> None:
"""Turn on the light."""
attempts = 0
update_received = False
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
seq = str(self.hub.get_seq_num())
if len(self.controllers) > 0:
controller = self.controllers[attempts%len(self.controllers)]
else:
controller = self.default_controller
if attr_rgb is not None and attr_br is not None:
if math.isclose(attr_br, max([self.rgb['r'],self.rgb['g'],self.rgb['b']])*self.brightness/100, abs_tol = 2):
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
else:
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
elif attr_rgb is None and attr_ct is None and attr_br is not None:
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
elif attr_rgb is not None and attr_br is None:
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
elif attr_ct is not None:
# Cync uses the range 0% to 100% to set the color temp, so we need to
# calculate the percentage of the color temp range that is being requested
color_temp = round(100*((attr_ct - self.min_color_temp_kelvin) /(self.max_color_temp_kelvin - self.min_color_temp_kelvin)))
self.hub.set_color_temp(color_temp, controller, self.mesh_id, seq)
else:
self.hub.turn_on(controller, self.mesh_id, seq)
self.hub.pending_commands[seq] = self.command_received
await asyncio.sleep(self._command_timout)
if self.hub.pending_commands.get(seq, None) is not None:
self.hub.pending_commands.pop(seq)
attempts += 1
else:
update_received = True
async def turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
attempts = 0
update_received = False
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
seq = str(self.hub.get_seq_num())
if len(self.controllers) > 0:
controller = self.controllers[attempts%len(self.controllers)]
else:
controller = self.default_controller
self.hub.turn_off(controller, self.mesh_id, seq)
self.hub.pending_commands[seq] = self.command_received
await asyncio.sleep(self._command_timout)
if self.hub.pending_commands.get(seq, None) is not None:
self.hub.pending_commands.pop(seq)
attempts += 1
else:
update_received = True
def command_received(self, seq):
"""Remove command from hub.pending_commands when a reply is received from Cync server"""
if self.hub.pending_commands.get(seq) is not None:
self.hub.pending_commands.pop(seq)
def update_switch(self,state,brightness,color_temp,rgb):
"""Update the state of the switch as updates are received from the Cync server"""
self.update_received = True
if self.power_state != state or self.brightness != brightness or self.color_temp != color_temp or self.rgb != rgb:
self.power_state = state
self.brightness = brightness if self.support_brightness and state else 100 if state else 0
self.color_temp = color_temp
self.rgb = rgb
self.publish_update()
if self._update_parent_room:
self._update_parent_room()
def update_controllers(self):
"""Update the list of responsive, Wi-Fi connected controller devices"""
connected_devices = self.hub.connected_devices[self.home_id]
controllers = []
if len(connected_devices) > 0:
if int(self.switch_id) > 0:
if self.device_id in connected_devices:
#if this device is connected, make this the first available controller
controllers.append(self.switch_id)
if self.room:
controllers = controllers + [self.hub.cync_switches[device_id].switch_id for device_id in self.room.all_room_switches if device_id in connected_devices and device_id != self.device_id]
others_available = [self.hub.cync_switches[device_id].switch_id for device_id in connected_devices]
for controller in controllers:
if controller in others_available:
others_available.remove(controller)
self.controllers = controllers + others_available
else:
self.controllers = [self.default_controller]
def publish_update(self):
if self._update_callback:
self._update_callback()
class CyncMotionSensor:
def __init__(self, device_id, device_info, room):
self.device_id = device_id
self.name = device_info['name']
self.home_name = device_info['home_name']
self.room = room
self.motion = False
self._update_callback = None
def register(self, update_callback) -> None:
"""Register callback, called when switch changes state."""
self._update_callback = update_callback
def reset(self) -> None:
"""Remove previously registered callback."""
self._update_callback = None
def update_motion_sensor(self,motion):
self.motion = motion
self.publish_update()
def publish_update(self):
if self._update_callback:
self._update_callback()
class CyncAmbientLightSensor:
def __init__(self, device_id, device_info, room):
self.device_id = device_id
self.name = device_info['name']
self.home_name = device_info['home_name']
self.room = room
self.ambient_light = False
self._update_callback = None
def register(self, update_callback) -> None:
"""Register callback, called when switch changes state."""
self._update_callback = update_callback
def reset(self) -> None:
"""Remove previously registered callback."""
self._update_callback = None
def update_ambient_light_sensor(self,ambient_light):
self.ambient_light = ambient_light
self.publish_update()
def publish_update(self):
if self._update_callback:
self._update_callback()
class CyncUserData:
def __init__(self):
self.username = ''
self.password = ''
self.auth_code = None
self.user_credentials = {}
async def authenticate(self,username,password):
"""Authenticate with the API and get a token."""
self.username = username
self.password = password
auth_data = {'corp_id': "1007d2ad150c4000", 'email': self.username, 'password': self.password}
async with aiohttp.ClientSession() as session:
async with session.post(API_AUTH, json=auth_data) as resp:
if resp.status == 200:
self.user_credentials = await resp.json()
login_code = bytearray.fromhex('13000000') + (10 + len(self.user_credentials['authorize'])).to_bytes(1,'big') + bytearray.fromhex('03') + self.user_credentials['user_id'].to_bytes(4,'big') + len(self.user_credentials['authorize']).to_bytes(2,'big') + bytearray(self.user_credentials['authorize'],'ascii') + bytearray.fromhex('0000b4')
self.auth_code = [int.from_bytes([byt],'big') for byt in login_code]
return {'authorized':True}
elif resp.status == 400:
request_code_data = {'corp_id': "1007d2ad150c4000", 'email': self.username, 'local_lang': "en-us"}
async with aiohttp.ClientSession() as session:
async with session.post(API_REQUEST_CODE,json=request_code_data) as resp:
if resp.status == 200:
return {'authorized':False,'two_factor_code_required':True}
else:
return {'authorized':False,'two_factor_code_required':False}
else:
return {'authorized':False,'two_factor_code_required':False}
async def auth_two_factor(self, code):
"""Authenticate with 2 Factor Code."""
two_factor_data = {'corp_id': "1007d2ad150c4000", 'email': self.username,'password': self.password, 'two_factor': code, 'resource':"abcdefghijklmnop"}
async with aiohttp.ClientSession() as session:
async with session.post(API_2FACTOR_AUTH,json=two_factor_data) as resp:
if resp.status == 200:
self.user_credentials = await resp.json()
login_code = bytearray.fromhex('13000000') + (10 + len(self.user_credentials['authorize'])).to_bytes(1,'big') + bytearray.fromhex('03') + self.user_credentials['user_id'].to_bytes(4,'big') + len(self.user_credentials['authorize']).to_bytes(2,'big') + bytearray(self.user_credentials['authorize'],'ascii') + bytearray.fromhex('0000b4')
self.auth_code = [int.from_bytes([byt],'big') for byt in login_code]
return {'authorized':True}
else:
return {'authorized':False}
async def get_cync_config(self):
home_devices = {}
home_controllers = {}
switchID_to_homeID = {}
devices = {}
rooms = {}
homes = await self._get_homes()
for home in homes:
home_info = await self._get_home_properties(home['product_id'], home['id'])
if home_info.get('groupsArray',False) and home_info.get('bulbsArray',False) and len(home_info['groupsArray']) > 0 and len(home_info['bulbsArray']) > 0:
home_id = str(home['id'])
bulbs_array_length = max([((device['deviceID'] % home['id']) % 1000) + (int((device['deviceID'] % home['id']) / 1000)*256) for device in home_info['bulbsArray']]) + 1
home_devices[home_id] = [""]*(bulbs_array_length)
home_controllers[home_id] = []
for device in home_info['bulbsArray']:
device_type = device['deviceType']
device_id = str(device['deviceID'])
current_index = ((device['deviceID'] % home['id']) % 1000) + (int((device['deviceID'] % home['id']) / 1000)*256)
home_devices[home_id][current_index] = device_id
devices[device_id] = {'name':device['displayName'],
'mesh_id':current_index,
'switch_id':str(device.get('switchID',0)),
'ONOFF': device_type in Capabilities['ONOFF'],
'BRIGHTNESS': device_type in Capabilities["BRIGHTNESS"],
"COLORTEMP":device_type in Capabilities["COLORTEMP"],
"RGB": device_type in Capabilities["RGB"],
"MOTION": device_type in Capabilities["MOTION"],
"AMBIENT_LIGHT": device_type in Capabilities["AMBIENT_LIGHT"],
"WIFICONTROL": device_type in Capabilities["WIFICONTROL"],
"PLUG" : device_type in Capabilities["PLUG"],
"FAN" : device_type in Capabilities["FAN"],
'home_name':home['name'],
'room':'',
'room_name':''
}
if str(device_type) in Capabilities['MULTIELEMENT'] and current_index < 256:
devices[device_id]['MULTIELEMENT'] = Capabilities['MULTIELEMENT'][str(device_type)]
if devices[device_id].get('WIFICONTROL',False) and 'switchID' in device and device['switchID'] > 0:
switchID_to_homeID[str(device['switchID'])] = home_id
devices[device_id]['switch_controller'] = device['switchID']
home_controllers[home_id].append(device['switchID'])
if len(home_controllers[home_id]) == 0:
for device in home_info['bulbsArray']:
device_id = str(device['deviceID'])
devices.pop(device_id,'')
home_devices.pop(home_id,'')
home_controllers.pop(home_id,'')
else:
for room in home_info['groupsArray']:
if (len(room.get('deviceIDArray',[])) + len(room.get('subgroupIDArray',[]))) > 0:
room_id = home_id + '-' + str(room['groupID'])
room_controller = home_controllers[home_id][0]
available_room_controllers = [(id%1000) + (int(id/1000)*256) for id in room.get('deviceIDArray',[]) if 'switch_controller' in devices[home_devices[home_id][(id%1000)+(int(id/1000)*256)]]]
if len(available_room_controllers) > 0:
room_controller = devices[home_devices[home_id][available_room_controllers[0]]]['switch_controller']
for id in room.get('deviceIDArray',[]):
id = (id % 1000) + (int(id / 1000)*256)
devices[home_devices[home_id][id]]['room'] = room_id
devices[home_devices[home_id][id]]['room_name'] = room['displayName']
if 'switch_controller' not in devices[home_devices[home_id][id]] and devices[home_devices[home_id][id]].get('ONOFF',False):
devices[home_devices[home_id][id]]['switch_controller'] = room_controller
rooms[room_id] = {'name':room['displayName'],
'mesh_id' : room['groupID'],
'room_controller' : room_controller,
'home_name' : home['name'],
'switches' : [home_devices[home_id][(i%1000)+(int(i/1000)*256)] for i in room.get('deviceIDArray',[]) if devices[home_devices[home_id][(i%1000)+(int(i/1000)*256)]].get('ONOFF',False)],
'isSubgroup' : room.get('isSubgroup',False),
'subgroups' : [home_id + '-' + str(subgroup) for subgroup in room.get('subgroupIDArray',[])]
}
for room,room_info in rooms.items():
if not room_info.get("isSubgroup",False) and len(subgroups := room_info.get("subgroups",[]).copy()) > 0:
for subgroup in subgroups:
if rooms.get(subgroup,None):
rooms[subgroup]["parent_room"] = room_info["name"]
else:
room_info['subgroups'].pop(room_info['subgroups'].index(subgroup))
if len(rooms) == 0 or len(devices) == 0 or len(home_controllers) == 0 or len(home_devices) == 0 or len(switchID_to_homeID) == 0:
raise InvalidCyncConfiguration
else:
return {'rooms':rooms, 'devices':devices, 'home_devices':home_devices, 'home_controllers':home_controllers, 'switchID_to_homeID':switchID_to_homeID}
async def _get_homes(self):
"""Get a list of devices for a particular user."""
headers = {'Access-Token': self.user_credentials['access_token']}
async with aiohttp.ClientSession() as session:
async with session.get(API_DEVICES.format(user=self.user_credentials['user_id']), headers=headers) as resp:
response = await resp.json()
return response
async def _get_home_properties(self, product_id, device_id):
"""Get properties for a single device."""
headers = {'Access-Token': self.user_credentials['access_token']}
async with aiohttp.ClientSession() as session:
async with session.get(API_DEVICE_INFO.format(product_id=product_id, device_id=device_id), headers=headers) as resp:
response = await resp.json()
return response
class LostConnection(Exception):
"""Lost connection to Cync Server"""
class ShuttingDown(Exception):
"""Cync client shutting down"""
class InvalidCyncConfiguration(Exception):
"""Cync configuration is not supported"""
+101
View File
@@ -0,0 +1,101 @@
"""Platform for light integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback
) -> None:
hub = hass.data[DOMAIN][config_entry.entry_id]
new_devices = []
for switch_id in hub.cync_switches:
if not hub.cync_switches[switch_id]._update_callback and hub.cync_switches[switch_id].fan and switch_id in config_entry.options["switches"]:
new_devices.append(CyncFanEntity(hub.cync_switches[switch_id]))
if new_devices:
async_add_entities(new_devices)
class CyncFanEntity(FanEntity):
"""Representation of a Cync Fan Switch Entity."""
should_poll = False
def __init__(self, cync_switch) -> None:
"""Initialize the light."""
self.cync_switch = cync_switch
async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA."""
self.cync_switch.register(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass."""
self.cync_switch.reset()
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
manufacturer = "Cync by Savant",
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
suggested_area = f"{self.cync_switch.room.name}",
)
@property
def unique_id(self) -> str:
"""Return Unique ID string."""
return 'cync_switch_' + self.cync_switch.device_id
@property
def name(self) -> str:
"""Return the name of the switch."""
return self.cync_switch.name
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
@property
def is_on(self) -> bool | None:
"""Return true if fan is on."""
return self.cync_switch.power_state
@property
def percentage(self) -> int | None:
"""Return the fan speed percentage of this switch"""
return self.cync_switch.brightness
@property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return 4
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn on the light."""
await self.cync_switch.turn_on(None,percentage*255/100 if percentage is not None else None,None)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self.cync_switch.turn_off()
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
if percentage == 0:
await self.async_turn_off()
else:
await self.cync_switch.turn_on(None,percentage*255/100,None)
+250
View File
@@ -0,0 +1,250 @@
"""Platform for light integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ColorMode, LightEntity)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import DeviceInfo
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback
) -> None:
hub = hass.data[DOMAIN][config_entry.entry_id]
new_devices = []
for room in hub.cync_rooms:
if not hub.cync_rooms[room]._update_callback and (room in config_entry.options["rooms"] or room in config_entry.options["subgroups"]):
new_devices.append(CyncRoomEntity(hub.cync_rooms[room]))
for switch_id in hub.cync_switches:
if not hub.cync_switches[switch_id]._update_callback and not hub.cync_switches[switch_id].plug and not hub.cync_switches[switch_id].fan and switch_id in config_entry.options["switches"]:
new_devices.append(CyncSwitchEntity(hub.cync_switches[switch_id]))
if new_devices:
async_add_entities(new_devices)
class CyncRoomEntity(LightEntity):
"""Representation of a Cync Room Light Entity."""
should_poll = False
def __init__(self, room) -> None:
"""Initialize the light."""
self.room = room
async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA."""
self.room.register(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass."""
self.room.reset()
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers = {(DOMAIN, f"{self.room.parent_room if self.room.is_subgroup else self.room.name} ({self.room.home_name})")},
manufacturer = "Cync by Savant",
name = f"{self.room.parent_room if self.room.is_subgroup else self.room.name} ({self.room.home_name})",
suggested_area = f"{self.room.parent_room if self.room.is_subgroup else self.room.name}",
)
@property
def icon(self) -> str | None:
"""Icon of the entity."""
if self.room.is_subgroup:
return "mdi:lightbulb-group-outline"
else:
return "mdi:lightbulb-group"
@property
def unique_id(self) -> str:
"""Return Unique ID string."""
uid = 'cync_room_' + '-'.join(self.room.switches) + '_' + '-'.join(self.room.subgroups)
return uid
@property
def name(self) -> str:
"""Return the name of the room."""
return self.room.name
@property
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.room.power_state
@property
def brightness(self) -> int | None:
"""Return the brightness of this room between 0..255."""
return round(self.room.brightness*255/100)
@property
def max_color_temp_kelvin(self) -> int:
"""Return maximum supported color temperature."""
return self.room.max_color_temp_kelvin
@property
def min_color_temp_kelvin(self) -> int:
"""Return minimum supported color temperature."""
return self.room.min_color_temp_kelvin
@property
def color_temp_kelvin(self) -> int:
"""Return color temperature in kelvin."""
return self.min_color_temp_kelvin + round((self.max_color_temp_kelvin-self.min_color_temp_kelvin)*self.room.color_temp/100)
@property
def rgb_color(self) -> tuple[int, int, int] | None:
"""Return the RGB color tuple of this light switch"""
return (self.room.rgb['r'],self.room.rgb['g'],self.room.rgb['b'])
@property
def supported_color_modes(self) -> set[str] | None:
"""Return list of available color modes."""
modes: set[ColorMode | str] = set()
if self.room.support_color_temp:
modes.add(ColorMode.COLOR_TEMP)
if self.room.support_rgb:
modes.add(ColorMode.RGB)
if self.room.support_brightness and not modes:
modes.add(ColorMode.BRIGHTNESS)
if not modes:
modes.add(ColorMode.ONOFF)
return modes
@property
def color_mode(self) -> str | None:
"""Return the active color mode."""
if self.room.support_color_temp:
if self.room.support_rgb and self.room.rgb['active']:
return ColorMode.RGB
else:
return ColorMode.COLOR_TEMP
if self.room.support_brightness:
return ColorMode.BRIGHTNESS
else:
return ColorMode.ONOFF
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
await self.room.turn_on(kwargs.get(ATTR_RGB_COLOR),kwargs.get(ATTR_BRIGHTNESS),kwargs.get(ATTR_COLOR_TEMP_KELVIN))
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self.room.turn_off()
class CyncSwitchEntity(LightEntity):
"""Representation of a Cync Switch Light Entity."""
should_poll = False
def __init__(self, cync_switch) -> None:
"""Initialize the light."""
self.cync_switch = cync_switch
async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA."""
self.cync_switch.register(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass."""
self.cync_switch.reset()
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
manufacturer = "Cync by Savant",
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
suggested_area = f"{self.cync_switch.room.name}",
)
@property
def unique_id(self) -> str:
"""Return Unique ID string."""
return 'cync_switch_' + self.cync_switch.device_id
@property
def name(self) -> str:
"""Return the name of the switch."""
return self.cync_switch.name
@property
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.cync_switch.power_state
@property
def brightness(self) -> int | None:
"""Return the brightness of this switch between 0..255."""
return round(self.cync_switch.brightness*255/100)
@property
def max_color_temp_kelvin(self) -> int:
"""Return maximum supported color temperature."""
return self.cync_switch.max_color_temp_kelvin
@property
def min_color_temp_kelvin(self) -> int:
"""Return minimum supported color temperature."""
return self.cync_switch.min_color_temp_kelvin
@property
def color_temp_kelvin(self) -> int | None:
"""Return the color temperature of this light for HA."""
return self.min_color_temp_kelvin + round((self.max_color_temp_kelvin-self.min_color_temp_kelvin)*self.cync_switch.color_temp/100)
@property
def rgb_color(self) -> tuple[int, int, int] | None:
"""Return the RGB color tuple of this light switch"""
return (self.cync_switch.rgb['r'],self.cync_switch.rgb['g'],self.cync_switch.rgb['b'])
@property
def supported_color_modes(self) -> set[str] | None:
"""Return list of available color modes."""
modes: set[ColorMode | str] = set()
if self.cync_switch.support_color_temp:
modes.add(ColorMode.COLOR_TEMP)
if self.cync_switch.support_rgb:
modes.add(ColorMode.RGB)
if self.cync_switch.support_brightness and not modes:
modes.add(ColorMode.BRIGHTNESS)
if not modes:
modes.add(ColorMode.ONOFF)
return modes
@property
def color_mode(self) -> str | None:
"""Return the active color mode."""
if self.cync_switch.support_color_temp:
if self.cync_switch.support_rgb and self.cync_switch.rgb['active']:
return ColorMode.RGB
else:
return ColorMode.COLOR_TEMP
if self.cync_switch.support_brightness:
return ColorMode.BRIGHTNESS
else:
return ColorMode.ONOFF
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
await self.cync_switch.turn_on(kwargs.get(ATTR_RGB_COLOR),kwargs.get(ATTR_BRIGHTNESS),kwargs.get(ATTR_COLOR_TEMP_KELVIN))
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self.cync_switch.turn_off()
@@ -0,0 +1,15 @@
{
"domain": "cync_lights",
"name": "Cync Lights",
"version": "1.0.1",
"config_flow": true,
"documentation": "https://github.com/nikshriv/cync_lights",
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@nikshriv"
],
"iot_class": "local_push"
}
@@ -0,0 +1,72 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "Username",
"password": "Password"
},
"title": "Cync User Credentials",
"description": "Please enter your Cync account credentials"
},
"two_factor_code": {
"data": {
"two_factor_code":"Two Factor Code"
},
"title": "Cync Two Factor Authentication",
"description": "Please check your email and enter the two factor code"
},
"select_switches": {
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
"data":{
"rooms":"Rooms [room (home)]",
"subgroups":"Groups [group (room:home)]",
"switches":"Switches [switch/bulb (room:home)]",
"motion_sensors":"Motion Sensors [sensor (room:home)]",
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
}
}
},
"error": {
"invalid_auth": "Invalid Cync user credentials or two factor code",
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
},
"abort": {
"reauth_successful": "Reauthorization successful"
}
},
"options": {
"step": {
"init": {
"title": "Reload Cync Configuration",
"description": "In case you have added new devices to your Cync account, you can opt to reauthorize and download your latest configuration.",
"data":{
"re-authenticate":"Reauthorize?"
}
},
"two_factor_code": {
"data": {
"two_factor_code":"Two Factor Code"
},
"title": "Cync Two Factor Authentication",
"description": "Please check your email and enter the two factor code"
},
"select_switches": {
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
"data":{
"rooms":"Rooms [room (home)]",
"subgroups":"Groups [group (room:home)]",
"switches":"Switches [switch/bulb (room:home)]",
"motion_sensors":"Motion Sensors [sensor (room:home)]",
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
}
}
},
"error": {
"invalid_auth": "Invalid Cync user credentials or two factor code",
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
}
}
}
+79
View File
@@ -0,0 +1,79 @@
"""Platform for light integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback
) -> None:
hub = hass.data[DOMAIN][config_entry.entry_id]
new_devices = []
for switch_id in hub.cync_switches:
if not hub.cync_switches[switch_id]._update_callback and hub.cync_switches[switch_id].plug and switch_id in config_entry.options["switches"]:
new_devices.append(CyncPlugEntity(hub.cync_switches[switch_id]))
if new_devices:
async_add_entities(new_devices)
class CyncPlugEntity(SwitchEntity):
"""Representation of a Cync Switch Light Entity."""
should_poll = False
def __init__(self, cync_switch) -> None:
"""Initialize the light."""
self.cync_switch = cync_switch
async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA."""
self.cync_switch.register(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass."""
self.cync_switch.reset()
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
manufacturer = "Cync by Savant",
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
suggested_area = f"{self.cync_switch.room.name}",
)
@property
def unique_id(self) -> str:
"""Return Unique ID string."""
return 'cync_switch_' + self.cync_switch.device_id
@property
def name(self) -> str:
"""Return the name of the switch."""
return self.cync_switch.name
@property
def device_class(self) -> str | None:
"""Return the device class"""
return SwitchDeviceClass.OUTLET
@property
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.cync_switch.power_state
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the outlet."""
await self.cync_switch.turn_on(None, None, None)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the outlet."""
await self.cync_switch.turn_off()
@@ -0,0 +1,72 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "Username",
"password": "Password"
},
"title": "Cync User Credentials",
"description": "Please enter your Cync account credentials"
},
"two_factor_code": {
"data": {
"two_factor_code":"Two Factor Code"
},
"title": "Cync Two Factor Authentication",
"description": "Please check your email and enter the two factor code"
},
"select_switches": {
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
"data":{
"rooms":"Rooms [room (home)]",
"subgroups":"Groups [group (room:home)]",
"switches":"Switches [switch/bulb (room:home)]",
"motion_sensors":"Motion Sensors [sensor (room:home)]",
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
}
}
},
"error": {
"invalid_auth": "Invalid Cync user credentials or two factor code",
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
},
"abort": {
"reauth_successful": "Reauthorization successful"
}
},
"options": {
"step": {
"init": {
"title": "Reload Cync Configuration",
"description": "In case you have added new devices to your Cync account, you can opt to reauthorize and download your latest configuration.",
"data":{
"re-authenticate":"Reauthorize?"
}
},
"two_factor_code": {
"data": {
"two_factor_code":"Two Factor Code"
},
"title": "Cync Two Factor Authentication",
"description": "Please check your email and enter the two factor code"
},
"select_switches": {
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
"data":{
"rooms":"Rooms [room (home)]",
"subgroups":"Groups [group (room:home)]",
"switches":"Switches [switch/bulb (room:home)]",
"motion_sensors":"Motion Sensors [sensor (room:home)]",
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
}
}
},
"error": {
"invalid_auth": "Invalid Cync user credentials or two factor code",
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
}
}
}
+229
View File
@@ -0,0 +1,229 @@
"""HACS gives you a powerful UI to handle downloads of all your custom needs.
For more details about this integration, please refer to the documentation at
https://hacs.xyz/
"""
from __future__ import annotations
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
from aiogithubapi.const import ACCEPT_HEADERS
from awesomeversion import AwesomeVersion
from homeassistant.components.frontend import async_remove_panel
from homeassistant.components.lovelace.system_health import system_health_info
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform, __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
from homeassistant.loader import async_get_integration
from .base import HacsBase
from .const import DOMAIN, HACS_SYSTEM_ID, MINIMUM_HA_VERSION, STARTUP
from .data_client import HacsDataClient
from .enums import HacsDisabledReason, HacsStage, LovelaceMode
from .frontend import async_register_frontend
from .utils.data import HacsData
from .utils.queue_manager import QueueManager
from .utils.version import version_left_higher_or_equal_then_right
from .websocket import async_register_websocket_commands
PLATFORMS = [Platform.SWITCH, Platform.UPDATE]
async def _async_initialize_integration(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
"""Initialize the integration"""
hass.data[DOMAIN] = hacs = HacsBase()
hacs.enable_hacs()
if config_entry.source == SOURCE_IMPORT:
# Import is not supported
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
hacs.configuration.update_from_dict(
{
"config_entry": config_entry,
**config_entry.data,
**config_entry.options,
},
)
integration = await async_get_integration(hass, DOMAIN)
hacs.set_stage(None)
hacs.log.info(STARTUP, integration.version)
clientsession = async_get_clientsession(hass)
hacs.integration = integration
hacs.version = integration.version
hacs.configuration.dev = integration.version == "0.0.0"
hacs.hass = hass
hacs.queue = QueueManager(hass=hass)
hacs.data = HacsData(hacs=hacs)
hacs.data_client = HacsDataClient(
session=clientsession,
client_name=f"HACS/{integration.version}",
)
hacs.system.running = True
hacs.session = clientsession
hacs.core.lovelace_mode = LovelaceMode.YAML
try:
lovelace_info = await system_health_info(hacs.hass)
hacs.core.lovelace_mode = LovelaceMode(lovelace_info.get("mode", "yaml"))
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
# If this happens, the users YAML is not valid, we assume YAML mode
pass
hacs.core.config_path = hacs.hass.config.path()
if hacs.core.ha_version is None:
hacs.core.ha_version = AwesomeVersion(HAVERSION)
## Legacy GitHub client
hacs.github = GitHub(
hacs.configuration.token,
clientsession,
headers={
"User-Agent": f"HACS/{hacs.version}",
"Accept": ACCEPT_HEADERS["preview"],
},
)
## New GitHub client
hacs.githubapi = GitHubAPI(
token=hacs.configuration.token,
session=clientsession,
**{"client_name": f"HACS/{hacs.version}"},
)
async def async_startup():
"""HACS startup tasks."""
hacs.enable_hacs()
try:
import custom_components.custom_updater
except ImportError:
pass
else:
hacs.log.critical(
"HACS cannot be used with custom_updater. "
"To use HACS you need to remove custom_updater from `custom_components`",
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not version_left_higher_or_equal_then_right(
hacs.core.ha_version.string,
MINIMUM_HA_VERSION,
):
hacs.log.critical(
"You need HA version %s or newer to use this integration.",
MINIMUM_HA_VERSION,
)
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
return False
if not await hacs.data.restore():
hacs.disable_hacs(HacsDisabledReason.RESTORE)
return False
hacs.set_active_categories()
async_register_websocket_commands(hass)
await async_register_frontend(hass, hacs)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hacs.set_stage(HacsStage.SETUP)
if hacs.system.disabled:
return False
hacs.set_stage(HacsStage.WAITING)
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
# Schedule startup tasks
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
return not hacs.system.disabled
async def async_try_startup(_=None):
"""Startup wrapper for yaml config."""
try:
startup_result = await async_startup()
except AIOGitHubAPIException:
startup_result = False
if not startup_result:
if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN:
hacs.log.info("Could not setup HACS, trying again in 15 min")
async_call_later(hass, 900, async_try_startup)
return
hacs.enable_hacs()
await async_try_startup()
# Remove old (v0-v1) sensor if it exists, can be removed in v3
er = async_get_entity_registry(hass)
if old_sensor := er.async_get_entity_id("sensor", DOMAIN, HACS_SYSTEM_ID):
er.async_remove(old_sensor)
# Mischief managed!
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
setup_result = await _async_initialize_integration(hass=hass, config_entry=config_entry)
hacs: HacsBase = hass.data[DOMAIN]
return setup_result and not hacs.system.disabled
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
hacs: HacsBase = hass.data[DOMAIN]
if hacs.queue.has_pending_tasks:
hacs.log.warning("Pending tasks, can not unload, try again later.")
return False
# Clear out pending queue
hacs.queue.clear()
for task in hacs.recurring_tasks:
# Cancel all pending tasks
task()
# Store data
await hacs.data.async_write(force=True)
try:
if hass.data.get("frontend_panels", {}).get("hacs"):
hacs.log.info("Removing sidepanel")
async_remove_panel(hass, "hacs")
except AttributeError:
pass
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
hacs.set_stage(None)
hacs.disable_hacs(HacsDisabledReason.REMOVED)
hass.data.pop(DOMAIN, None)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Reload the HACS config entry."""
if not await async_unload_entry(hass, config_entry):
return
await async_setup_entry(hass, config_entry)

Some files were not shown because too many files have changed in this diff Show More