Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to create an interface to a Pilight daemon."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from datetime import timedelta
7 import functools
8 import logging
9 import threading
10 from typing import Any
11 
12 from pilight import pilight
13 import voluptuous as vol
14 
15 from homeassistant.const import (
16  CONF_HOST,
17  CONF_PORT,
18  CONF_PROTOCOL,
19  CONF_WHITELIST,
20  EVENT_HOMEASSISTANT_START,
21  EVENT_HOMEASSISTANT_STOP,
22 )
23 from homeassistant.core import HomeAssistant, ServiceCall
25 from homeassistant.helpers.event import track_point_in_utc_time
26 from homeassistant.helpers.typing import ConfigType
27 from homeassistant.util import dt as dt_util
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 CONF_SEND_DELAY = "send_delay"
32 
33 DEFAULT_HOST = "127.0.0.1"
34 DEFAULT_PORT = 5001
35 DEFAULT_SEND_DELAY = 0.0
36 DOMAIN = "pilight"
37 
38 EVENT = "pilight_received"
39 
40 # The Pilight code schema depends on the protocol. Thus only require to have
41 # the protocol information. Ensure that protocol is in a list otherwise
42 # segfault in pilight-daemon, https://github.com/pilight/pilight/issues/296
43 RF_CODE_SCHEMA = vol.Schema(
44  {vol.Required(CONF_PROTOCOL): vol.All(cv.ensure_list, [cv.string])},
45  extra=vol.ALLOW_EXTRA,
46 )
47 
48 SERVICE_NAME = "send"
49 
50 CONFIG_SCHEMA = vol.Schema(
51  {
52  DOMAIN: vol.Schema(
53  {
54  vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
55  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
56  vol.Optional(CONF_WHITELIST, default={}): {cv.string: [cv.string]},
57  vol.Optional(CONF_SEND_DELAY, default=DEFAULT_SEND_DELAY): vol.Coerce(
58  float
59  ),
60  }
61  )
62  },
63  extra=vol.ALLOW_EXTRA,
64 )
65 
66 
67 def setup(hass: HomeAssistant, config: ConfigType) -> bool:
68  """Set up the Pilight component."""
69 
70  host = config[DOMAIN][CONF_HOST]
71  port = config[DOMAIN][CONF_PORT]
72  send_throttler = CallRateDelayThrottle(hass, config[DOMAIN][CONF_SEND_DELAY])
73 
74  try:
75  pilight_client = pilight.Client(host=host, port=port)
76  except (OSError, TimeoutError) as err:
77  _LOGGER.error("Unable to connect to %s on port %s: %s", host, port, err)
78  return False
79 
80  def start_pilight_client(_):
81  """Run when Home Assistant starts."""
82  pilight_client.start()
83 
84  hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_pilight_client)
85 
86  def stop_pilight_client(_):
87  """Run once when Home Assistant stops."""
88  pilight_client.stop()
89 
90  hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_pilight_client)
91 
92  @send_throttler.limited
93  def send_code(call: ServiceCall) -> None:
94  """Send RF code to the pilight-daemon."""
95  # Change type to dict from mappingproxy since data has to be JSON
96  # serializable
97  message_data = dict(call.data)
98 
99  try:
100  pilight_client.send_code(message_data)
101  except OSError:
102  _LOGGER.error("Pilight send failed for %s", str(message_data))
103 
104  hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA)
105 
106  # Publish received codes on the HA event bus
107  # A whitelist of codes to be published in the event bus
108  whitelist = config[DOMAIN].get(CONF_WHITELIST)
109 
110  def handle_received_code(data):
111  """Run when RF codes are received."""
112  # Unravel dict of dicts to make event_data cut in automation rule
113  # possible
114  data = dict(
115  {"protocol": data["protocol"], "uuid": data["uuid"]}, **data["message"]
116  )
117 
118  # No whitelist defined or data matches whitelist, put data on event bus
119  if not whitelist or all(str(data[key]) in whitelist[key] for key in whitelist):
120  hass.bus.fire(EVENT, data)
121 
122  pilight_client.set_callback(handle_received_code)
123 
124  return True
125 
126 
128  """Helper class to provide service call rate throttling.
129 
130  This class provides a decorator to decorate service methods that need
131  to be throttled to not exceed a certain call rate per second.
132  One instance can be used on multiple service methods to archive
133  an overall throttling.
134 
135  As this uses track_point_in_utc_time to schedule delayed executions
136  it should not block the mainloop.
137  """
138 
139  def __init__(self, hass: HomeAssistant, delay_seconds: float) -> None:
140  """Initialize the delay handler."""
141  self._delay_delay = timedelta(seconds=max(0.0, delay_seconds))
142  self._queue: list[Callable[[Any], None]] = []
143  self._active_active = False
144  self._lock_lock = threading.Lock()
145  self._next_ts_next_ts = dt_util.utcnow()
146  self._schedule_schedule = functools.partial(track_point_in_utc_time, hass)
147 
148  def limited[**_P](self, method: Callable[_P, Any]) -> Callable[_P, None]:
149  """Decorate to delay calls on a certain method."""
150 
151  @functools.wraps(method)
152  def decorated(*args: _P.args, **kwargs: _P.kwargs) -> None:
153  """Delay a call."""
154  if self._delay_delay.total_seconds() == 0.0:
155  method(*args, **kwargs)
156  return
157 
158  def action(event: Any) -> None:
159  """Wrap an action that gets scheduled."""
160  method(*args, **kwargs)
161 
162  with self._lock_lock:
163  self._next_ts_next_ts = dt_util.utcnow() + self._delay_delay
164 
165  if not self._queue:
166  self._active_active = False
167  else:
168  next_action = self._queue.pop(0)
169  self._schedule_schedule(next_action, self._next_ts_next_ts)
170 
171  with self._lock_lock:
172  if self._active_active:
173  self._queue.append(action)
174  else:
175  self._active_active = True
176  schedule_ts = max(dt_util.utcnow(), self._next_ts_next_ts)
177  self._schedule_schedule(action, schedule_ts)
178 
179  return decorated
None __init__(self, HomeAssistant hass, float delay_seconds)
Definition: __init__.py:139
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:67