mirror of https://github.com/home-assistant/core
281 lines
9.8 KiB
Python
281 lines
9.8 KiB
Python
"""Support for Rflink devices."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from collections import defaultdict
|
|
import logging
|
|
|
|
from rflink.protocol import create_rflink_connection
|
|
from serial import SerialException
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import (
|
|
CONF_COMMAND,
|
|
CONF_DEVICE_ID,
|
|
CONF_HOST,
|
|
CONF_PORT,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
)
|
|
from homeassistant.core import CoreState, HassJob, HomeAssistant, ServiceCall, callback
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_connect,
|
|
async_dispatcher_send,
|
|
)
|
|
from homeassistant.helpers.event import async_call_later
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
from .const import (
|
|
DATA_DEVICE_REGISTER,
|
|
DATA_ENTITY_GROUP_LOOKUP,
|
|
DATA_ENTITY_LOOKUP,
|
|
EVENT_KEY_COMMAND,
|
|
EVENT_KEY_ID,
|
|
EVENT_KEY_SENSOR,
|
|
SIGNAL_AVAILABILITY,
|
|
SIGNAL_HANDLE_EVENT,
|
|
TMP_ENTITY,
|
|
)
|
|
from .entity import RflinkCommand
|
|
from .utils import identify_event_type
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_IGNORE_DEVICES = "ignore_devices"
|
|
CONF_RECONNECT_INTERVAL = "reconnect_interval"
|
|
CONF_WAIT_FOR_ACK = "wait_for_ack"
|
|
CONF_KEEPALIVE_IDLE = "tcp_keepalive_idle_timer"
|
|
|
|
DEFAULT_RECONNECT_INTERVAL = 10
|
|
DEFAULT_TCP_KEEPALIVE_IDLE_TIMER = 3600
|
|
CONNECTION_TIMEOUT = 10
|
|
|
|
RFLINK_GROUP_COMMANDS = ["allon", "alloff"]
|
|
|
|
DOMAIN = "rflink"
|
|
|
|
SERVICE_SEND_COMMAND = "send_command"
|
|
|
|
SIGNAL_EVENT = "rflink_event"
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_PORT): vol.Any(cv.port, cv.string),
|
|
vol.Optional(CONF_HOST): cv.string,
|
|
vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean,
|
|
vol.Optional(
|
|
CONF_KEEPALIVE_IDLE, default=DEFAULT_TCP_KEEPALIVE_IDLE_TIMER
|
|
): int,
|
|
vol.Optional(
|
|
CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL
|
|
): int,
|
|
vol.Optional(CONF_IGNORE_DEVICES, default=[]): vol.All(
|
|
cv.ensure_list, [cv.string]
|
|
),
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
SEND_COMMAND_SCHEMA = vol.Schema(
|
|
{vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_COMMAND): cv.string}
|
|
)
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up the Rflink component."""
|
|
# Allow entities to register themselves by device_id to be looked up when
|
|
# new rflink events arrive to be handled
|
|
hass.data[DATA_ENTITY_LOOKUP] = {
|
|
EVENT_KEY_COMMAND: defaultdict(list),
|
|
EVENT_KEY_SENSOR: defaultdict(list),
|
|
}
|
|
hass.data[DATA_ENTITY_GROUP_LOOKUP] = {EVENT_KEY_COMMAND: defaultdict(list)}
|
|
|
|
# Allow platform to specify function to register new unknown devices
|
|
hass.data[DATA_DEVICE_REGISTER] = {}
|
|
|
|
async def async_send_command(call: ServiceCall) -> None:
|
|
"""Send Rflink command."""
|
|
_LOGGER.debug("Rflink command for %s", str(call.data))
|
|
if not (
|
|
await RflinkCommand.send_command(
|
|
call.data.get(CONF_DEVICE_ID), call.data.get(CONF_COMMAND)
|
|
)
|
|
):
|
|
_LOGGER.error("Failed Rflink command for %s", str(call.data))
|
|
else:
|
|
async_dispatcher_send(
|
|
hass,
|
|
SIGNAL_EVENT,
|
|
{
|
|
EVENT_KEY_ID: call.data.get(CONF_DEVICE_ID),
|
|
EVENT_KEY_COMMAND: call.data.get(CONF_COMMAND),
|
|
},
|
|
)
|
|
|
|
hass.services.async_register(
|
|
DOMAIN, SERVICE_SEND_COMMAND, async_send_command, schema=SEND_COMMAND_SCHEMA
|
|
)
|
|
|
|
@callback
|
|
def event_callback(event):
|
|
"""Handle incoming Rflink events.
|
|
|
|
Rflink events arrive as dictionaries of varying content
|
|
depending on their type. Identify the events and distribute
|
|
accordingly.
|
|
"""
|
|
event_type = identify_event_type(event)
|
|
_LOGGER.debug("event of type %s: %s", event_type, event)
|
|
|
|
# Don't propagate non entity events (eg: version string, ack response)
|
|
if event_type not in hass.data[DATA_ENTITY_LOOKUP]:
|
|
_LOGGER.debug("unhandled event of type: %s", event_type)
|
|
return
|
|
|
|
# Lookup entities who registered this device id as device id or alias
|
|
event_id = event.get(EVENT_KEY_ID)
|
|
|
|
is_group_event = (
|
|
event_type == EVENT_KEY_COMMAND
|
|
and event[EVENT_KEY_COMMAND] in RFLINK_GROUP_COMMANDS
|
|
)
|
|
if is_group_event:
|
|
entity_ids = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get(
|
|
event_id, []
|
|
)
|
|
else:
|
|
entity_ids = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id]
|
|
|
|
_LOGGER.debug("entity_ids: %s", entity_ids)
|
|
if entity_ids:
|
|
# Propagate event to every entity matching the device id
|
|
for entity in entity_ids:
|
|
_LOGGER.debug("passing event to %s", entity)
|
|
async_dispatcher_send(hass, SIGNAL_HANDLE_EVENT.format(entity), event)
|
|
elif not is_group_event:
|
|
# If device is not yet known, register with platform (if loaded)
|
|
if event_type in hass.data[DATA_DEVICE_REGISTER]:
|
|
_LOGGER.debug("device_id not known, adding new device")
|
|
# Add bogus event_id first to avoid race if we get another
|
|
# event before the device is created
|
|
# Any additional events received before the device has been
|
|
# created will thus be ignored.
|
|
hass.data[DATA_ENTITY_LOOKUP][event_type][event_id].append(
|
|
TMP_ENTITY.format(event_id)
|
|
)
|
|
hass.async_create_task(
|
|
hass.data[DATA_DEVICE_REGISTER][event_type](event),
|
|
eager_start=False,
|
|
)
|
|
else:
|
|
_LOGGER.debug("device_id not known and automatic add disabled")
|
|
|
|
# When connecting to tcp host instead of serial port (optional)
|
|
host = config[DOMAIN].get(CONF_HOST)
|
|
# TCP port when host configured, otherwise serial port
|
|
port = config[DOMAIN][CONF_PORT]
|
|
|
|
keepalive_idle_timer = None
|
|
# TCP KeepAlive only if this is TCP based connection (not serial)
|
|
if host is not None:
|
|
# TCP KEEPALIVE will be enabled if value > 0
|
|
keepalive_idle_timer = config[DOMAIN][CONF_KEEPALIVE_IDLE]
|
|
if keepalive_idle_timer < 0:
|
|
_LOGGER.error(
|
|
(
|
|
"A bogus TCP Keepalive IDLE timer was provided (%d secs), "
|
|
"it will be disabled. "
|
|
"Recommended values: 60-3600 (seconds)"
|
|
),
|
|
keepalive_idle_timer,
|
|
)
|
|
keepalive_idle_timer = None
|
|
elif keepalive_idle_timer == 0:
|
|
keepalive_idle_timer = None
|
|
elif keepalive_idle_timer <= 30:
|
|
_LOGGER.warning(
|
|
(
|
|
"A very short TCP Keepalive IDLE timer was provided (%d secs) "
|
|
"and may produce unexpected disconnections from RFlink device."
|
|
" Recommended values: 60-3600 (seconds)"
|
|
),
|
|
keepalive_idle_timer,
|
|
)
|
|
|
|
@callback
|
|
def reconnect(_: Exception | None = None) -> None:
|
|
"""Schedule reconnect after connection has been unexpectedly lost."""
|
|
# Reset protocol binding before starting reconnect
|
|
RflinkCommand.set_rflink_protocol(None)
|
|
|
|
async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
|
|
|
|
# If HA is not stopping, initiate new connection
|
|
if hass.state is not CoreState.stopping:
|
|
_LOGGER.warning("Disconnected from Rflink, reconnecting")
|
|
hass.async_create_task(connect(), eager_start=False)
|
|
|
|
_reconnect_job = HassJob(reconnect, "Rflink reconnect", cancel_on_shutdown=True)
|
|
|
|
async def connect():
|
|
"""Set up connection and hook it into HA for reconnect/shutdown."""
|
|
_LOGGER.debug("Initiating Rflink connection")
|
|
|
|
# Rflink create_rflink_connection decides based on the value of host
|
|
# (string or None) if serial or tcp mode should be used
|
|
|
|
# Initiate serial/tcp connection to Rflink gateway
|
|
connection = create_rflink_connection(
|
|
port=port,
|
|
host=host,
|
|
keepalive=keepalive_idle_timer,
|
|
event_callback=event_callback,
|
|
disconnect_callback=reconnect,
|
|
loop=hass.loop,
|
|
ignore=config[DOMAIN][CONF_IGNORE_DEVICES],
|
|
)
|
|
|
|
try:
|
|
async with asyncio.timeout(CONNECTION_TIMEOUT):
|
|
transport, protocol = await connection
|
|
|
|
except (
|
|
SerialException,
|
|
OSError,
|
|
TimeoutError,
|
|
):
|
|
reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL]
|
|
_LOGGER.exception(
|
|
"Error connecting to Rflink, reconnecting in %s", reconnect_interval
|
|
)
|
|
# Connection to Rflink device is lost, make entities unavailable
|
|
async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
|
|
|
|
async_call_later(hass, reconnect_interval, _reconnect_job)
|
|
return
|
|
|
|
# There is a valid connection to a Rflink device now so
|
|
# mark entities as available
|
|
async_dispatcher_send(hass, SIGNAL_AVAILABILITY, True)
|
|
|
|
# Bind protocol to command class to allow entities to send commands
|
|
RflinkCommand.set_rflink_protocol(protocol, config[DOMAIN][CONF_WAIT_FOR_ACK])
|
|
|
|
# handle shutdown of Rflink asyncio transport
|
|
hass.bus.async_listen_once(
|
|
EVENT_HOMEASSISTANT_STOP, lambda x: transport.close()
|
|
)
|
|
|
|
_LOGGER.debug("Connected to Rflink")
|
|
|
|
hass.async_create_task(connect(), eager_start=False)
|
|
async_dispatcher_connect(hass, SIGNAL_EVENT, event_callback)
|
|
return True
|