2019-02-13 20:21:14 +00:00
|
|
|
"""Support for RFXtrx devices."""
|
2021-12-09 21:35:53 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-07-21 21:43:05 +00:00
|
|
|
import asyncio
|
2019-10-12 19:37:59 +00:00
|
|
|
import binascii
|
2022-09-26 22:32:32 +00:00
|
|
|
from collections.abc import Callable, Mapping
|
2020-10-13 11:51:42 +00:00
|
|
|
import copy
|
2018-05-14 11:05:52 +00:00
|
|
|
import logging
|
2022-09-23 14:47:58 +00:00
|
|
|
from typing import Any, NamedTuple, cast
|
2019-10-14 15:38:34 +00:00
|
|
|
|
2019-10-12 19:37:59 +00:00
|
|
|
import RFXtrx as rfxtrxmod
|
2020-10-01 06:55:57 +00:00
|
|
|
import async_timeout
|
2016-04-09 03:55:31 +00:00
|
|
|
import voluptuous as vol
|
2016-02-19 05:27:50 +00:00
|
|
|
|
2022-01-19 17:00:34 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2017-06-22 05:48:45 +00:00
|
|
|
from homeassistant.const import (
|
2021-01-04 14:31:10 +00:00
|
|
|
ATTR_DEVICE_ID,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_DEVICE,
|
2020-07-24 09:49:48 +00:00
|
|
|
CONF_DEVICE_ID,
|
2020-07-12 20:03:22 +00:00
|
|
|
CONF_DEVICES,
|
2019-11-20 02:11:17 +00:00
|
|
|
CONF_HOST,
|
|
|
|
CONF_PORT,
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
2021-12-04 12:19:49 +00:00
|
|
|
Platform,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2022-02-23 19:17:48 +00:00
|
|
|
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
2022-05-17 19:22:15 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
2022-05-17 16:41:36 +00:00
|
|
|
from homeassistant.helpers.dispatcher import (
|
|
|
|
async_dispatcher_connect,
|
|
|
|
async_dispatcher_send,
|
|
|
|
)
|
2021-12-22 21:38:55 +00:00
|
|
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
2020-07-17 08:27:07 +00:00
|
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
2016-04-09 03:55:31 +00:00
|
|
|
|
2020-07-13 14:54:52 +00:00
|
|
|
from .const import (
|
|
|
|
ATTR_EVENT,
|
2021-07-23 16:45:31 +00:00
|
|
|
COMMAND_GROUP_LIST,
|
2020-10-01 06:55:57 +00:00
|
|
|
CONF_AUTOMATIC_ADD,
|
|
|
|
CONF_DATA_BITS,
|
2022-02-25 19:43:29 +00:00
|
|
|
CONF_PROTOCOLS,
|
2020-11-04 10:30:46 +00:00
|
|
|
DATA_RFXOBJECT,
|
2020-07-13 14:54:52 +00:00
|
|
|
DEVICE_PACKET_TYPE_LIGHTING4,
|
2022-03-12 04:27:08 +00:00
|
|
|
DOMAIN,
|
2020-07-13 14:54:52 +00:00
|
|
|
EVENT_RFXTRX_EVENT,
|
|
|
|
SERVICE_SEND,
|
|
|
|
)
|
2020-07-10 12:52:07 +00:00
|
|
|
|
2022-02-25 06:53:22 +00:00
|
|
|
DEFAULT_OFF_DELAY = 2.0
|
2016-04-24 09:48:01 +00:00
|
|
|
|
2020-07-05 22:10:26 +00:00
|
|
|
SIGNAL_EVENT = f"{DOMAIN}_event"
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2015-09-29 06:20:25 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2020-07-12 20:03:22 +00:00
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
class DeviceTuple(NamedTuple):
|
|
|
|
"""Representation of a device in rfxtrx."""
|
|
|
|
|
|
|
|
packettype: str
|
|
|
|
subtype: str
|
|
|
|
id_string: str
|
|
|
|
|
|
|
|
|
2022-09-23 14:47:58 +00:00
|
|
|
def _bytearray_string(data: Any) -> bytearray:
|
2020-07-13 14:54:52 +00:00
|
|
|
val = cv.string(data)
|
|
|
|
try:
|
|
|
|
return bytearray.fromhex(val)
|
2020-08-28 11:50:32 +00:00
|
|
|
except ValueError as err:
|
|
|
|
raise vol.Invalid(
|
|
|
|
"Data must be a hex string with multiple of two characters"
|
|
|
|
) from err
|
2020-07-13 14:54:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
SERVICE_SEND_SCHEMA = vol.Schema({ATTR_EVENT: _bytearray_string})
|
|
|
|
|
2021-12-04 12:19:49 +00:00
|
|
|
PLATFORMS = [
|
|
|
|
Platform.SWITCH,
|
|
|
|
Platform.SENSOR,
|
|
|
|
Platform.LIGHT,
|
|
|
|
Platform.BINARY_SENSOR,
|
|
|
|
Platform.COVER,
|
2022-02-25 06:53:22 +00:00
|
|
|
Platform.SIREN,
|
2021-12-04 12:19:49 +00:00
|
|
|
]
|
2020-07-21 21:43:05 +00:00
|
|
|
|
2016-04-09 03:55:31 +00:00
|
|
|
|
2022-02-26 23:13:48 +00:00
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
2020-07-13 21:24:28 +00:00
|
|
|
"""Set up the RFXtrx component."""
|
2020-07-21 21:43:05 +00:00
|
|
|
hass.data.setdefault(DOMAIN, {})
|
|
|
|
|
2020-11-09 23:30:15 +00:00
|
|
|
try:
|
|
|
|
await async_setup_internal(hass, entry)
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
# Library currently doesn't support reload
|
|
|
|
_LOGGER.error(
|
|
|
|
"Connection timeout: failed to receive response from RFXtrx device"
|
|
|
|
)
|
|
|
|
return False
|
2020-07-13 21:24:28 +00:00
|
|
|
|
2022-07-09 15:27:42 +00:00
|
|
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
2020-07-13 21:24:28 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2022-01-19 17:00:34 +00:00
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
2020-07-21 21:43:05 +00:00
|
|
|
"""Unload RFXtrx component."""
|
2021-04-27 20:10:04 +00:00
|
|
|
if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
2020-07-24 12:40:10 +00:00
|
|
|
return False
|
2020-07-21 21:43:05 +00:00
|
|
|
|
2020-07-24 12:40:10 +00:00
|
|
|
hass.services.async_remove(DOMAIN, SERVICE_SEND)
|
2020-07-21 21:43:05 +00:00
|
|
|
|
|
|
|
rfx_object = hass.data[DOMAIN][DATA_RFXOBJECT]
|
2020-07-24 12:40:10 +00:00
|
|
|
await hass.async_add_executor_job(rfx_object.close_connection)
|
2020-07-21 21:43:05 +00:00
|
|
|
|
2020-11-04 10:30:46 +00:00
|
|
|
hass.data.pop(DOMAIN)
|
|
|
|
|
2020-07-24 12:40:10 +00:00
|
|
|
return True
|
2020-07-21 21:43:05 +00:00
|
|
|
|
2020-07-24 09:49:48 +00:00
|
|
|
|
2022-09-26 22:32:32 +00:00
|
|
|
def _create_rfx(config: Mapping[str, Any]) -> rfxtrxmod.Connect:
|
2020-07-24 12:40:10 +00:00
|
|
|
"""Construct a rfx object based on config."""
|
2022-02-25 19:43:29 +00:00
|
|
|
|
|
|
|
modes = config.get(CONF_PROTOCOLS)
|
|
|
|
|
|
|
|
if modes:
|
|
|
|
_LOGGER.debug("Using modes: %s", ",".join(modes))
|
|
|
|
else:
|
|
|
|
_LOGGER.debug("No modes defined, using device configuration")
|
|
|
|
|
2020-07-24 12:40:10 +00:00
|
|
|
if config[CONF_PORT] is not None:
|
|
|
|
# If port is set then we create a TCP connection
|
|
|
|
rfx = rfxtrxmod.Connect(
|
|
|
|
(config[CONF_HOST], config[CONF_PORT]),
|
|
|
|
None,
|
|
|
|
transport_protocol=rfxtrxmod.PyNetworkTransport,
|
2022-02-25 19:43:29 +00:00
|
|
|
modes=modes,
|
2020-07-24 12:40:10 +00:00
|
|
|
)
|
|
|
|
else:
|
2022-02-25 19:43:29 +00:00
|
|
|
rfx = rfxtrxmod.Connect(
|
|
|
|
config[CONF_DEVICE],
|
|
|
|
None,
|
|
|
|
modes=modes,
|
|
|
|
)
|
2020-07-24 12:40:10 +00:00
|
|
|
|
|
|
|
return rfx
|
|
|
|
|
|
|
|
|
2022-09-23 14:47:58 +00:00
|
|
|
def _get_device_lookup(
|
|
|
|
devices: dict[str, dict[str, Any]]
|
|
|
|
) -> dict[DeviceTuple, dict[str, Any]]:
|
2020-07-24 12:40:10 +00:00
|
|
|
"""Get a lookup structure for devices."""
|
2020-10-06 13:02:23 +00:00
|
|
|
lookup = {}
|
2020-07-24 12:40:10 +00:00
|
|
|
for event_code, event_config in devices.items():
|
2021-10-31 17:45:27 +00:00
|
|
|
if (event := get_rfx_object(event_code)) is None:
|
2020-08-28 20:05:11 +00:00
|
|
|
continue
|
2020-07-13 00:57:19 +00:00
|
|
|
device_id = get_device_id(
|
|
|
|
event.device, data_bits=event_config.get(CONF_DATA_BITS)
|
|
|
|
)
|
2020-07-24 12:40:10 +00:00
|
|
|
lookup[device_id] = event_config
|
|
|
|
return lookup
|
|
|
|
|
|
|
|
|
2022-09-23 14:47:58 +00:00
|
|
|
async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
2020-07-24 12:40:10 +00:00
|
|
|
"""Set up the RFXtrx component."""
|
|
|
|
config = entry.data
|
|
|
|
|
|
|
|
# Initialize library
|
2020-11-09 23:30:15 +00:00
|
|
|
async with async_timeout.timeout(30):
|
|
|
|
rfx_object = await hass.async_add_executor_job(_create_rfx, config)
|
2020-07-24 12:40:10 +00:00
|
|
|
|
|
|
|
# Setup some per device config
|
|
|
|
devices = _get_device_lookup(config[CONF_DEVICES])
|
2023-01-07 09:52:05 +00:00
|
|
|
pt2262_devices: set[str] = set()
|
2020-07-13 00:57:19 +00:00
|
|
|
|
2022-05-17 19:22:15 +00:00
|
|
|
device_registry = dr.async_get(hass)
|
2021-01-04 14:31:10 +00:00
|
|
|
|
2015-09-27 09:13:49 +00:00
|
|
|
# Declare the Handle event
|
2020-07-24 12:40:10 +00:00
|
|
|
@callback
|
2022-09-23 14:47:58 +00:00
|
|
|
def async_handle_receive(event: rfxtrxmod.RFXtrxEvent) -> None:
|
2018-01-29 22:37:19 +00:00
|
|
|
"""Handle received messages from RFXtrx gateway."""
|
2015-10-03 09:26:18 +00:00
|
|
|
# Log RFXCOM event
|
2016-02-05 12:46:54 +00:00
|
|
|
if not event.device.id_string:
|
|
|
|
return
|
2020-07-13 00:57:19 +00:00
|
|
|
|
|
|
|
event_data = {
|
|
|
|
"packet_type": event.device.packettype,
|
|
|
|
"sub_type": event.device.subtype,
|
|
|
|
"type_string": event.device.type_string,
|
|
|
|
"id_string": event.device.id_string,
|
2020-07-22 22:09:37 +00:00
|
|
|
"data": binascii.hexlify(event.data).decode("ASCII"),
|
2020-07-13 00:57:19 +00:00
|
|
|
"values": getattr(event, "values", None),
|
|
|
|
}
|
|
|
|
|
|
|
|
_LOGGER.debug("Receive RFXCOM event: %s", event_data)
|
|
|
|
|
2020-07-24 09:49:48 +00:00
|
|
|
data_bits = get_device_data_bits(event.device, devices)
|
2020-07-13 00:57:19 +00:00
|
|
|
device_id = get_device_id(event.device, data_bits=data_bits)
|
2015-10-03 09:26:18 +00:00
|
|
|
|
2021-01-04 14:31:10 +00:00
|
|
|
if device_id not in devices:
|
|
|
|
if config[CONF_AUTOMATIC_ADD]:
|
|
|
|
_add_device(event, device_id)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
2021-12-22 21:38:55 +00:00
|
|
|
if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4:
|
|
|
|
find_possible_pt2262_device(pt2262_devices, event.device.id_string)
|
2023-01-07 09:52:05 +00:00
|
|
|
pt2262_devices.add(event.device.id_string)
|
2021-12-22 21:38:55 +00:00
|
|
|
|
2021-01-04 14:31:10 +00:00
|
|
|
device_entry = device_registry.async_get_device(
|
2022-09-23 14:47:58 +00:00
|
|
|
identifiers={(DOMAIN, *device_id)}, # type: ignore[arg-type]
|
2021-01-04 14:31:10 +00:00
|
|
|
)
|
|
|
|
if device_entry:
|
|
|
|
event_data[ATTR_DEVICE_ID] = device_entry.id
|
2020-07-24 12:40:10 +00:00
|
|
|
|
2016-03-07 17:49:31 +00:00
|
|
|
# Callback to HA registered components.
|
2022-05-17 16:41:36 +00:00
|
|
|
async_dispatcher_send(hass, SIGNAL_EVENT, event, device_id)
|
2020-07-13 00:57:19 +00:00
|
|
|
|
|
|
|
# Signal event to any other listeners
|
2021-11-16 18:03:18 +00:00
|
|
|
hass.bus.async_fire(EVENT_RFXTRX_EVENT, event_data)
|
2015-09-27 09:13:49 +00:00
|
|
|
|
2020-07-24 09:49:48 +00:00
|
|
|
@callback
|
2022-09-23 14:47:58 +00:00
|
|
|
def _add_device(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None:
|
2020-07-24 12:40:10 +00:00
|
|
|
"""Add a device to config entry."""
|
2021-09-09 05:17:02 +00:00
|
|
|
config = {}
|
2020-07-25 17:13:10 +00:00
|
|
|
config[CONF_DEVICE_ID] = device_id
|
|
|
|
|
2021-12-22 21:38:55 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"Added device (Device ID: %s Class: %s Sub: %s, Event: %s)",
|
|
|
|
event.device.id_string.lower(),
|
|
|
|
event.device.__class__.__name__,
|
|
|
|
event.device.subtype,
|
|
|
|
"".join(f"{x:02x}" for x in event.data),
|
|
|
|
)
|
|
|
|
|
2020-07-24 12:40:10 +00:00
|
|
|
data = entry.data.copy()
|
2020-10-13 11:51:42 +00:00
|
|
|
data[CONF_DEVICES] = copy.deepcopy(entry.data[CONF_DEVICES])
|
2020-07-24 12:40:10 +00:00
|
|
|
event_code = binascii.hexlify(event.data).decode("ASCII")
|
2020-07-25 17:13:10 +00:00
|
|
|
data[CONF_DEVICES][event_code] = config
|
2020-07-24 12:40:10 +00:00
|
|
|
hass.config_entries.async_update_entry(entry=entry, data=data)
|
2020-07-25 17:13:10 +00:00
|
|
|
devices[device_id] = config
|
2017-08-29 14:22:28 +00:00
|
|
|
|
2022-02-23 19:17:48 +00:00
|
|
|
@callback
|
2022-09-23 14:47:58 +00:00
|
|
|
def _remove_device(device_id: DeviceTuple) -> None:
|
2022-02-23 19:17:48 +00:00
|
|
|
data = {
|
|
|
|
**entry.data,
|
|
|
|
CONF_DEVICES: {
|
|
|
|
packet_id: entity_info
|
|
|
|
for packet_id, entity_info in entry.data[CONF_DEVICES].items()
|
|
|
|
if tuple(entity_info.get(CONF_DEVICE_ID)) != device_id
|
|
|
|
},
|
|
|
|
}
|
|
|
|
hass.config_entries.async_update_entry(entry=entry, data=data)
|
|
|
|
devices.pop(device_id)
|
|
|
|
|
2022-02-26 23:14:12 +00:00
|
|
|
@callback
|
2022-09-23 14:47:58 +00:00
|
|
|
def _updated_device(event: Event) -> None:
|
2022-02-26 23:14:12 +00:00
|
|
|
if event.data["action"] != "remove":
|
|
|
|
return
|
|
|
|
device_entry = device_registry.deleted_devices[event.data["device_id"]]
|
|
|
|
if entry.entry_id not in device_entry.config_entries:
|
|
|
|
return
|
|
|
|
device_id = get_device_tuple_from_identifiers(device_entry.identifiers)
|
|
|
|
if device_id:
|
|
|
|
_remove_device(device_id)
|
|
|
|
|
2022-02-23 19:17:48 +00:00
|
|
|
entry.async_on_unload(
|
2022-05-17 19:22:15 +00:00
|
|
|
hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device)
|
2022-02-23 19:17:48 +00:00
|
|
|
)
|
|
|
|
|
2022-09-23 14:47:58 +00:00
|
|
|
def _shutdown_rfxtrx(event: Event) -> None:
|
2016-09-19 04:40:37 +00:00
|
|
|
"""Close connection with RFXtrx."""
|
2018-01-12 19:52:53 +00:00
|
|
|
rfx_object.close_connection()
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2021-12-22 21:38:55 +00:00
|
|
|
entry.async_on_unload(
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx)
|
|
|
|
)
|
2020-07-21 21:43:05 +00:00
|
|
|
hass.data[DOMAIN][DATA_RFXOBJECT] = rfx_object
|
2020-07-12 20:03:22 +00:00
|
|
|
|
2020-07-27 22:29:35 +00:00
|
|
|
rfx_object.event_callback = lambda event: hass.add_job(async_handle_receive, event)
|
|
|
|
|
2021-12-28 12:10:17 +00:00
|
|
|
def send(call: ServiceCall) -> None:
|
2020-07-13 14:54:52 +00:00
|
|
|
event = call.data[ATTR_EVENT]
|
|
|
|
rfx_object.transport.send(event)
|
|
|
|
|
2020-07-24 12:40:10 +00:00
|
|
|
hass.services.async_register(DOMAIN, SERVICE_SEND, send, schema=SERVICE_SEND_SCHEMA)
|
2015-10-02 20:39:30 +00:00
|
|
|
|
2015-10-06 06:44:15 +00:00
|
|
|
|
2021-12-22 21:38:55 +00:00
|
|
|
async def async_setup_platform_entry(
|
|
|
|
hass: HomeAssistant,
|
2022-02-26 23:13:48 +00:00
|
|
|
config_entry: ConfigEntry,
|
2021-12-22 21:38:55 +00:00
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
supported: Callable[[rfxtrxmod.RFXtrxEvent], bool],
|
|
|
|
constructor: Callable[
|
2022-09-23 14:47:58 +00:00
|
|
|
[
|
|
|
|
rfxtrxmod.RFXtrxEvent,
|
|
|
|
rfxtrxmod.RFXtrxEvent | None,
|
|
|
|
DeviceTuple,
|
|
|
|
dict[str, Any],
|
|
|
|
],
|
2021-12-22 21:38:55 +00:00
|
|
|
list[Entity],
|
|
|
|
],
|
2022-09-23 14:47:58 +00:00
|
|
|
) -> None:
|
2021-12-22 21:38:55 +00:00
|
|
|
"""Set up config entry."""
|
|
|
|
entry_data = config_entry.data
|
|
|
|
device_ids: set[DeviceTuple] = set()
|
|
|
|
|
|
|
|
# Add entities from config
|
|
|
|
entities = []
|
|
|
|
for packet_id, entity_info in entry_data[CONF_DEVICES].items():
|
|
|
|
if (event := get_rfx_object(packet_id)) is None:
|
|
|
|
_LOGGER.error("Invalid device: %s", packet_id)
|
|
|
|
continue
|
|
|
|
if not supported(event):
|
|
|
|
continue
|
|
|
|
|
|
|
|
device_id = get_device_id(
|
|
|
|
event.device, data_bits=entity_info.get(CONF_DATA_BITS)
|
|
|
|
)
|
|
|
|
if device_id in device_ids:
|
|
|
|
continue
|
|
|
|
device_ids.add(device_id)
|
|
|
|
|
|
|
|
entities.extend(constructor(event, None, device_id, entity_info))
|
|
|
|
|
|
|
|
async_add_entities(entities)
|
|
|
|
|
|
|
|
# If automatic add is on, hookup listener
|
|
|
|
if entry_data[CONF_AUTOMATIC_ADD]:
|
|
|
|
|
|
|
|
@callback
|
2022-09-23 14:47:58 +00:00
|
|
|
def _update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None:
|
2021-12-22 21:38:55 +00:00
|
|
|
"""Handle light updates from the RFXtrx gateway."""
|
|
|
|
if not supported(event):
|
|
|
|
return
|
|
|
|
|
|
|
|
if device_id in device_ids:
|
|
|
|
return
|
|
|
|
device_ids.add(device_id)
|
|
|
|
async_add_entities(constructor(event, event, device_id, {}))
|
|
|
|
|
|
|
|
config_entry.async_on_unload(
|
2022-03-24 12:19:11 +00:00
|
|
|
async_dispatcher_connect(hass, SIGNAL_EVENT, _update)
|
2021-12-22 21:38:55 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
def get_rfx_object(packetid: str) -> rfxtrxmod.RFXtrxEvent | None:
|
2016-03-07 17:49:31 +00:00
|
|
|
"""Return the RFXObject with the packetid."""
|
2016-07-12 08:45:22 +00:00
|
|
|
try:
|
|
|
|
binarypacket = bytearray.fromhex(packetid)
|
|
|
|
except ValueError:
|
|
|
|
return None
|
2021-12-22 21:38:55 +00:00
|
|
|
return rfxtrxmod.RFXtrxTransport.parse(binarypacket)
|
2016-04-06 17:34:51 +00:00
|
|
|
|
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
def get_pt2262_deviceid(device_id: str, nb_data_bits: int | None) -> bytes | None:
|
2017-06-22 05:48:45 +00:00
|
|
|
"""Extract and return the address bits from a Lighting4/PT2262 packet."""
|
2018-01-15 22:26:27 +00:00
|
|
|
if nb_data_bits is None:
|
2021-12-09 21:35:53 +00:00
|
|
|
return None
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2017-06-22 05:48:45 +00:00
|
|
|
try:
|
|
|
|
data = bytearray.fromhex(device_id)
|
|
|
|
except ValueError:
|
|
|
|
return None
|
|
|
|
mask = 0xFF & ~((1 << nb_data_bits) - 1)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
data[len(data) - 1] &= mask
|
2017-06-22 05:48:45 +00:00
|
|
|
|
|
|
|
return binascii.hexlify(data)
|
|
|
|
|
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
def get_pt2262_cmd(device_id: str, data_bits: int) -> str | None:
|
2017-06-22 05:48:45 +00:00
|
|
|
"""Extract and return the data bits from a Lighting4/PT2262 packet."""
|
|
|
|
try:
|
|
|
|
data = bytearray.fromhex(device_id)
|
|
|
|
except ValueError:
|
|
|
|
return None
|
|
|
|
|
|
|
|
mask = 0xFF & ((1 << data_bits) - 1)
|
|
|
|
|
|
|
|
return hex(data[-1] & mask)
|
|
|
|
|
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
def get_device_data_bits(
|
2022-09-23 14:47:58 +00:00
|
|
|
device: rfxtrxmod.RFXtrxDevice, devices: dict[DeviceTuple, dict[str, Any]]
|
2021-12-09 21:35:53 +00:00
|
|
|
) -> int | None:
|
2020-07-13 00:57:19 +00:00
|
|
|
"""Deduce data bits for device based on a cache of device bits."""
|
|
|
|
data_bits = None
|
|
|
|
if device.packettype == DEVICE_PACKET_TYPE_LIGHTING4:
|
2020-07-24 09:49:48 +00:00
|
|
|
for device_id, entity_config in devices.items():
|
|
|
|
bits = entity_config.get(CONF_DATA_BITS)
|
2020-07-13 00:57:19 +00:00
|
|
|
if get_device_id(device, bits) == device_id:
|
|
|
|
data_bits = bits
|
|
|
|
break
|
|
|
|
return data_bits
|
|
|
|
|
|
|
|
|
2023-01-07 09:52:05 +00:00
|
|
|
def find_possible_pt2262_device(device_ids: set[str], device_id: str) -> str | None:
|
2017-06-22 05:48:45 +00:00
|
|
|
"""Look for the device which id matches the given device_id parameter."""
|
2020-07-10 12:52:07 +00:00
|
|
|
for dev_id in device_ids:
|
|
|
|
if len(dev_id) == len(device_id):
|
2017-06-22 05:48:45 +00:00
|
|
|
size = None
|
2018-02-11 17:20:28 +00:00
|
|
|
for i, (char1, char2) in enumerate(zip(dev_id, device_id)):
|
|
|
|
if char1 != char2:
|
2017-06-22 05:48:45 +00:00
|
|
|
break
|
|
|
|
size = i
|
|
|
|
if size is not None:
|
|
|
|
size = len(dev_id) - size - 1
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
2022-12-23 12:27:27 +00:00
|
|
|
(
|
|
|
|
"Found possible device %s for %s "
|
|
|
|
"with the following configuration:\n"
|
|
|
|
"data_bits=%d\n"
|
|
|
|
"command_on=0x%s\n"
|
|
|
|
"command_off=0x%s\n"
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
device_id,
|
|
|
|
dev_id,
|
|
|
|
size * 4,
|
|
|
|
dev_id[-size:],
|
|
|
|
device_id[-size:],
|
|
|
|
)
|
2020-07-10 12:52:07 +00:00
|
|
|
return dev_id
|
2017-06-22 05:48:45 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
def get_device_id(
|
|
|
|
device: rfxtrxmod.RFXtrxDevice, data_bits: int | None = None
|
|
|
|
) -> DeviceTuple:
|
2020-07-10 12:52:07 +00:00
|
|
|
"""Calculate a device id for device."""
|
2021-12-09 21:35:53 +00:00
|
|
|
id_string: str = device.id_string
|
2021-10-31 17:45:27 +00:00
|
|
|
if (
|
|
|
|
data_bits
|
|
|
|
and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4
|
|
|
|
and (masked_id := get_pt2262_deviceid(id_string, data_bits))
|
|
|
|
):
|
|
|
|
id_string = masked_id.decode("ASCII")
|
2016-04-06 17:34:51 +00:00
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
return DeviceTuple(f"{device.packettype:x}", f"{device.subtype:x}", id_string)
|
2016-04-06 17:34:51 +00:00
|
|
|
|
|
|
|
|
2022-02-26 23:14:12 +00:00
|
|
|
def get_device_tuple_from_identifiers(
|
|
|
|
identifiers: set[tuple[str, str]]
|
|
|
|
) -> DeviceTuple | None:
|
|
|
|
"""Calculate the device tuple from a device entry."""
|
2023-01-07 09:52:46 +00:00
|
|
|
identifier = next((x for x in identifiers if x[0] == DOMAIN and len(x) == 4), None)
|
2022-02-26 23:14:12 +00:00
|
|
|
if not identifier:
|
|
|
|
return None
|
|
|
|
# work around legacy identifier, being a multi tuple value
|
|
|
|
identifier2 = cast(tuple[str, str, str, str], identifier)
|
|
|
|
return DeviceTuple(identifier2[1], identifier2[2], identifier2[3])
|
|
|
|
|
|
|
|
|
2022-07-11 14:21:26 +00:00
|
|
|
def get_identifiers_from_device_tuple(
|
|
|
|
device_tuple: DeviceTuple,
|
|
|
|
) -> set[tuple[str, str]]:
|
|
|
|
"""Calculate the device identifier from a device tuple."""
|
|
|
|
# work around legacy identifier, being a multi tuple value
|
|
|
|
return {(DOMAIN, *device_tuple)} # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
2022-02-23 19:17:48 +00:00
|
|
|
async def async_remove_config_entry_device(
|
2022-05-17 19:22:15 +00:00
|
|
|
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
2022-02-23 19:17:48 +00:00
|
|
|
) -> bool:
|
|
|
|
"""Remove config entry from a device.
|
|
|
|
|
|
|
|
The actual cleanup is done in the device registry event
|
|
|
|
"""
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2020-07-18 11:43:38 +00:00
|
|
|
class RfxtrxEntity(RestoreEntity):
|
2016-04-06 17:34:51 +00:00
|
|
|
"""Represents a Rfxtrx device.
|
|
|
|
|
2016-04-24 09:48:01 +00:00
|
|
|
Contains the common logic for Rfxtrx lights and switches.
|
2016-04-06 17:34:51 +00:00
|
|
|
"""
|
|
|
|
|
2022-07-09 15:15:08 +00:00
|
|
|
_attr_assumed_state = True
|
|
|
|
_attr_has_entity_name = True
|
|
|
|
_attr_should_poll = False
|
2021-12-09 21:35:53 +00:00
|
|
|
_device: rfxtrxmod.RFXtrxDevice
|
|
|
|
_event: rfxtrxmod.RFXtrxEvent | None
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
device: rfxtrxmod.RFXtrxDevice,
|
|
|
|
device_id: DeviceTuple,
|
|
|
|
event: rfxtrxmod.RFXtrxEvent | None = None,
|
|
|
|
) -> None:
|
2016-04-06 17:34:51 +00:00
|
|
|
"""Initialize the device."""
|
2022-07-11 14:21:26 +00:00
|
|
|
self._attr_device_info = DeviceInfo(
|
|
|
|
identifiers=get_identifiers_from_device_tuple(device_id),
|
|
|
|
model=device.type_string,
|
|
|
|
name=f"{device.type_string} {device.id_string}",
|
|
|
|
)
|
|
|
|
self._attr_unique_id = "_".join(x for x in device_id)
|
2020-07-09 09:40:37 +00:00
|
|
|
self._device = device
|
2020-07-18 11:43:38 +00:00
|
|
|
self._event = event
|
2020-07-13 00:57:19 +00:00
|
|
|
self._device_id = device_id
|
2021-07-23 16:45:31 +00:00
|
|
|
# If id_string is 213c7f2:1, the group_id is 213c7f2, and the device will respond to
|
|
|
|
# group events regardless of their group indices.
|
2022-09-23 14:47:58 +00:00
|
|
|
(self._group_id, _, _) = cast(str, device.id_string).partition(":")
|
2020-07-09 09:40:37 +00:00
|
|
|
|
2022-09-23 14:47:58 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
2020-07-17 08:27:07 +00:00
|
|
|
"""Restore RFXtrx device state (ON/OFF)."""
|
|
|
|
if self._event:
|
2020-07-18 11:43:38 +00:00
|
|
|
self._apply_event(self._event)
|
|
|
|
|
|
|
|
self.async_on_remove(
|
2022-03-24 12:19:11 +00:00
|
|
|
async_dispatcher_connect(self.hass, SIGNAL_EVENT, self._handle_event)
|
2020-07-18 11:43:38 +00:00
|
|
|
)
|
2020-07-17 08:27:07 +00:00
|
|
|
|
|
|
|
@property
|
2022-09-23 14:47:58 +00:00
|
|
|
def extra_state_attributes(self) -> dict[str, str] | None:
|
2020-07-17 08:27:07 +00:00
|
|
|
"""Return the device state attributes."""
|
|
|
|
if not self._event:
|
|
|
|
return None
|
|
|
|
return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)}
|
|
|
|
|
2022-09-23 14:47:58 +00:00
|
|
|
def _event_applies(
|
|
|
|
self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple
|
|
|
|
) -> bool:
|
2021-07-23 16:45:31 +00:00
|
|
|
"""Check if event applies to me."""
|
2021-12-09 21:35:53 +00:00
|
|
|
if isinstance(event, rfxtrxmod.ControlEvent):
|
|
|
|
if (
|
|
|
|
"Command" in event.values
|
|
|
|
and event.values["Command"] in COMMAND_GROUP_LIST
|
|
|
|
):
|
|
|
|
device: rfxtrxmod.RFXtrxDevice = event.device
|
2022-09-23 14:47:58 +00:00
|
|
|
(group_id, _, _) = cast(str, device.id_string).partition(":")
|
2021-12-09 21:35:53 +00:00
|
|
|
return group_id == self._group_id
|
2021-07-23 16:45:31 +00:00
|
|
|
|
|
|
|
# Otherwise, the event only applies to the matching device.
|
|
|
|
return device_id == self._device_id
|
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None:
|
2020-07-05 22:10:26 +00:00
|
|
|
"""Apply a received event."""
|
2020-07-17 08:27:07 +00:00
|
|
|
self._event = event
|
2016-04-23 17:55:05 +00:00
|
|
|
|
2020-07-18 11:43:38 +00:00
|
|
|
@callback
|
2021-12-09 21:35:53 +00:00
|
|
|
def _handle_event(
|
|
|
|
self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple
|
|
|
|
) -> None:
|
2020-07-18 11:43:38 +00:00
|
|
|
"""Handle a reception of data, overridden by other classes."""
|
|
|
|
|
|
|
|
|
|
|
|
class RfxtrxCommandEntity(RfxtrxEntity):
|
|
|
|
"""Represents a Rfxtrx device.
|
|
|
|
|
|
|
|
Contains the common logic for Rfxtrx lights and switches.
|
|
|
|
"""
|
|
|
|
|
2021-12-09 21:35:53 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
device: rfxtrxmod.RFXtrxDevice,
|
|
|
|
device_id: DeviceTuple,
|
|
|
|
event: rfxtrxmod.RFXtrxEvent | None = None,
|
|
|
|
) -> None:
|
2020-07-18 11:43:38 +00:00
|
|
|
"""Initialzie a switch or light device."""
|
|
|
|
super().__init__(device, device_id, event=event)
|
|
|
|
|
2022-09-23 14:47:58 +00:00
|
|
|
async def _async_send(self, fun: Callable[..., None], *args: Any) -> None:
|
2020-07-21 21:43:05 +00:00
|
|
|
rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT]
|
2022-03-06 09:29:20 +00:00
|
|
|
await self.hass.async_add_executor_job(fun, rfx_object.transport, *args)
|