Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Rflink devices."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import defaultdict
7 import logging
8 
9 from rflink.protocol import create_rflink_connection
10 from serial import SerialException
11 import voluptuous as vol
12 
13 from homeassistant.const import (
14  CONF_COMMAND,
15  CONF_DEVICE_ID,
16  CONF_HOST,
17  CONF_PORT,
18  EVENT_HOMEASSISTANT_STOP,
19 )
20 from homeassistant.core import CoreState, HassJob, HomeAssistant, ServiceCall, callback
23  async_dispatcher_connect,
24  async_dispatcher_send,
25 )
26 from homeassistant.helpers.event import async_call_later
27 from homeassistant.helpers.typing import ConfigType
28 
29 from .const import (
30  DATA_DEVICE_REGISTER,
31  DATA_ENTITY_GROUP_LOOKUP,
32  DATA_ENTITY_LOOKUP,
33  EVENT_KEY_COMMAND,
34  EVENT_KEY_ID,
35  EVENT_KEY_SENSOR,
36  SIGNAL_AVAILABILITY,
37  SIGNAL_HANDLE_EVENT,
38  TMP_ENTITY,
39 )
40 from .entity import RflinkCommand
41 from .utils import identify_event_type
42 
43 _LOGGER = logging.getLogger(__name__)
44 
45 CONF_IGNORE_DEVICES = "ignore_devices"
46 CONF_RECONNECT_INTERVAL = "reconnect_interval"
47 CONF_WAIT_FOR_ACK = "wait_for_ack"
48 CONF_KEEPALIVE_IDLE = "tcp_keepalive_idle_timer"
49 
50 DEFAULT_RECONNECT_INTERVAL = 10
51 DEFAULT_TCP_KEEPALIVE_IDLE_TIMER = 3600
52 CONNECTION_TIMEOUT = 10
53 
54 RFLINK_GROUP_COMMANDS = ["allon", "alloff"]
55 
56 DOMAIN = "rflink"
57 
58 SERVICE_SEND_COMMAND = "send_command"
59 
60 SIGNAL_EVENT = "rflink_event"
61 
62 
63 CONFIG_SCHEMA = vol.Schema(
64  {
65  DOMAIN: vol.Schema(
66  {
67  vol.Required(CONF_PORT): vol.Any(cv.port, cv.string),
68  vol.Optional(CONF_HOST): cv.string,
69  vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean,
70  vol.Optional(
71  CONF_KEEPALIVE_IDLE, default=DEFAULT_TCP_KEEPALIVE_IDLE_TIMER
72  ): int,
73  vol.Optional(
74  CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL
75  ): int,
76  vol.Optional(CONF_IGNORE_DEVICES, default=[]): vol.All(
77  cv.ensure_list, [cv.string]
78  ),
79  }
80  )
81  },
82  extra=vol.ALLOW_EXTRA,
83 )
84 
85 SEND_COMMAND_SCHEMA = vol.Schema(
86  {vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_COMMAND): cv.string}
87 )
88 
89 
90 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
91  """Set up the Rflink component."""
92  # Allow entities to register themselves by device_id to be looked up when
93  # new rflink events arrive to be handled
94  hass.data[DATA_ENTITY_LOOKUP] = {
95  EVENT_KEY_COMMAND: defaultdict(list),
96  EVENT_KEY_SENSOR: defaultdict(list),
97  }
98  hass.data[DATA_ENTITY_GROUP_LOOKUP] = {EVENT_KEY_COMMAND: defaultdict(list)}
99 
100  # Allow platform to specify function to register new unknown devices
101  hass.data[DATA_DEVICE_REGISTER] = {}
102 
103  async def async_send_command(call: ServiceCall) -> None:
104  """Send Rflink command."""
105  _LOGGER.debug("Rflink command for %s", str(call.data))
106  if not (
107  await RflinkCommand.send_command(
108  call.data.get(CONF_DEVICE_ID), call.data.get(CONF_COMMAND)
109  )
110  ):
111  _LOGGER.error("Failed Rflink command for %s", str(call.data))
112  else:
114  hass,
115  SIGNAL_EVENT,
116  {
117  EVENT_KEY_ID: call.data.get(CONF_DEVICE_ID),
118  EVENT_KEY_COMMAND: call.data.get(CONF_COMMAND),
119  },
120  )
121 
122  hass.services.async_register(
123  DOMAIN, SERVICE_SEND_COMMAND, async_send_command, schema=SEND_COMMAND_SCHEMA
124  )
125 
126  @callback
127  def event_callback(event):
128  """Handle incoming Rflink events.
129 
130  Rflink events arrive as dictionaries of varying content
131  depending on their type. Identify the events and distribute
132  accordingly.
133  """
134  event_type = identify_event_type(event)
135  _LOGGER.debug("event of type %s: %s", event_type, event)
136 
137  # Don't propagate non entity events (eg: version string, ack response)
138  if event_type not in hass.data[DATA_ENTITY_LOOKUP]:
139  _LOGGER.debug("unhandled event of type: %s", event_type)
140  return
141 
142  # Lookup entities who registered this device id as device id or alias
143  event_id = event.get(EVENT_KEY_ID)
144 
145  is_group_event = (
146  event_type == EVENT_KEY_COMMAND
147  and event[EVENT_KEY_COMMAND] in RFLINK_GROUP_COMMANDS
148  )
149  if is_group_event:
150  entity_ids = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get(
151  event_id, []
152  )
153  else:
154  entity_ids = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id]
155 
156  _LOGGER.debug("entity_ids: %s", entity_ids)
157  if entity_ids:
158  # Propagate event to every entity matching the device id
159  for entity in entity_ids:
160  _LOGGER.debug("passing event to %s", entity)
161  async_dispatcher_send(hass, SIGNAL_HANDLE_EVENT.format(entity), event)
162  elif not is_group_event:
163  # If device is not yet known, register with platform (if loaded)
164  if event_type in hass.data[DATA_DEVICE_REGISTER]:
165  _LOGGER.debug("device_id not known, adding new device")
166  # Add bogus event_id first to avoid race if we get another
167  # event before the device is created
168  # Any additional events received before the device has been
169  # created will thus be ignored.
170  hass.data[DATA_ENTITY_LOOKUP][event_type][event_id].append(
171  TMP_ENTITY.format(event_id)
172  )
173  hass.async_create_task(
174  hass.data[DATA_DEVICE_REGISTER][event_type](event),
175  eager_start=False,
176  )
177  else:
178  _LOGGER.debug("device_id not known and automatic add disabled")
179 
180  # When connecting to tcp host instead of serial port (optional)
181  host = config[DOMAIN].get(CONF_HOST)
182  # TCP port when host configured, otherwise serial port
183  port = config[DOMAIN][CONF_PORT]
184 
185  keepalive_idle_timer = None
186  # TCP KeepAlive only if this is TCP based connection (not serial)
187  if host is not None:
188  # TCP KEEPALIVE will be enabled if value > 0
189  keepalive_idle_timer = config[DOMAIN][CONF_KEEPALIVE_IDLE]
190  if keepalive_idle_timer < 0:
191  _LOGGER.error(
192  (
193  "A bogus TCP Keepalive IDLE timer was provided (%d secs), "
194  "it will be disabled. "
195  "Recommended values: 60-3600 (seconds)"
196  ),
197  keepalive_idle_timer,
198  )
199  keepalive_idle_timer = None
200  elif keepalive_idle_timer == 0:
201  keepalive_idle_timer = None
202  elif keepalive_idle_timer <= 30:
203  _LOGGER.warning(
204  (
205  "A very short TCP Keepalive IDLE timer was provided (%d secs) "
206  "and may produce unexpected disconnections from RFlink device."
207  " Recommended values: 60-3600 (seconds)"
208  ),
209  keepalive_idle_timer,
210  )
211 
212  @callback
213  def reconnect(_: Exception | None = None) -> None:
214  """Schedule reconnect after connection has been unexpectedly lost."""
215  # Reset protocol binding before starting reconnect
216  RflinkCommand.set_rflink_protocol(None)
217 
218  async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
219 
220  # If HA is not stopping, initiate new connection
221  if hass.state is not CoreState.stopping:
222  _LOGGER.warning("Disconnected from Rflink, reconnecting")
223  hass.async_create_task(connect(), eager_start=False)
224 
225  _reconnect_job = HassJob(reconnect, "Rflink reconnect", cancel_on_shutdown=True)
226 
227  async def connect():
228  """Set up connection and hook it into HA for reconnect/shutdown."""
229  _LOGGER.debug("Initiating Rflink connection")
230 
231  # Rflink create_rflink_connection decides based on the value of host
232  # (string or None) if serial or tcp mode should be used
233 
234  # Initiate serial/tcp connection to Rflink gateway
235  connection = create_rflink_connection(
236  port=port,
237  host=host,
238  keepalive=keepalive_idle_timer,
239  event_callback=event_callback,
240  disconnect_callback=reconnect,
241  loop=hass.loop,
242  ignore=config[DOMAIN][CONF_IGNORE_DEVICES],
243  )
244 
245  try:
246  async with asyncio.timeout(CONNECTION_TIMEOUT):
247  transport, protocol = await connection
248 
249  except (
250  SerialException,
251  OSError,
252  TimeoutError,
253  ):
254  reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL]
255  _LOGGER.exception(
256  "Error connecting to Rflink, reconnecting in %s", reconnect_interval
257  )
258  # Connection to Rflink device is lost, make entities unavailable
259  async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
260 
261  async_call_later(hass, reconnect_interval, _reconnect_job)
262  return
263 
264  # There is a valid connection to a Rflink device now so
265  # mark entities as available
266  async_dispatcher_send(hass, SIGNAL_AVAILABILITY, True)
267 
268  # Bind protocol to command class to allow entities to send commands
269  RflinkCommand.set_rflink_protocol(protocol, config[DOMAIN][CONF_WAIT_FOR_ACK])
270 
271  # handle shutdown of Rflink asyncio transport
272  hass.bus.async_listen_once(
273  EVENT_HOMEASSISTANT_STOP, lambda x: transport.close()
274  )
275 
276  _LOGGER.debug("Connected to Rflink")
277 
278  hass.async_create_task(connect(), eager_start=False)
279  async_dispatcher_connect(hass, SIGNAL_EVENT, event_callback)
280  return True
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_send_command(HomeAssistant hass, Mapping[str, Any] data)
Definition: __init__.py:87
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597