mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 02:07:54 +00:00

* Add a method to throttle calls to services This adds CallRateDelayThrottle. This is a class that provides an decorator to throttle calls to services. Instead of the Throttle in homeassistant.util it does this by delaying all subsequent calls instead of just dropping them. Dropping of calls would be bad if we call services to actual change the state of a connected hardware (like rf controlled power plugs). Ihe delay is done by rescheduling the call using track_point_in_utc_time from homeassistant.helpers.event so it should not block the mainloop at all. * Add unittests for CallRateDelayThrottle Signed-off-by: Jan Losinski <losinski@wh2.tu-dresden.de> * Introduce a send_delay for pilight component If pilight is used with a "pilight USB Nano" between the daemon and the hardware, we must use a delay between sending multiple signals. Otherwise the hardware will just skip random codes. We hit this condition for example, if we switch a group of pilight switches on or off. Without the delay, random switch signals will not be transmitted by the RF transmitter. As this seems not necessary, if the transmitter is directly connected via GPIO, we introduce a optional configuration to set the delay. * Add unittests for pilight send_delay handling This adds an unittest to test the delayed calls to the send_code service.
176 lines
5.7 KiB
Python
176 lines
5.7 KiB
Python
"""
|
|
Component to create an interface to a Pilight daemon.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/components/pilight/
|
|
"""
|
|
import logging
|
|
import functools
|
|
import socket
|
|
import threading
|
|
|
|
from datetime import timedelta
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.helpers.event import track_point_in_utc_time
|
|
from homeassistant.util import dt as dt_util
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.const import (
|
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT,
|
|
CONF_WHITELIST, CONF_PROTOCOL)
|
|
|
|
REQUIREMENTS = ['pilight==0.1.1']
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
CONF_SEND_DELAY = "send_delay"
|
|
|
|
DEFAULT_HOST = '127.0.0.1'
|
|
DEFAULT_PORT = 5000
|
|
DEFAULT_SEND_DELAY = 0.0
|
|
DOMAIN = 'pilight'
|
|
|
|
EVENT = 'pilight_received'
|
|
|
|
# The Pilight code schema depends on the protocol. Thus only require to have
|
|
# the protocol information. Ensure that protocol is in a list otherwise
|
|
# segfault in pilight-daemon, https://github.com/pilight/pilight/issues/296
|
|
RF_CODE_SCHEMA = vol.Schema({
|
|
vol.Required(CONF_PROTOCOL): vol.All(cv.ensure_list, [cv.string]),
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
SERVICE_NAME = 'send'
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
DOMAIN: vol.Schema({
|
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
vol.Optional(CONF_WHITELIST, default={}): {cv.string: [cv.string]},
|
|
vol.Optional(CONF_SEND_DELAY, default=DEFAULT_SEND_DELAY):
|
|
vol.Coerce(float),
|
|
}),
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
def setup(hass, config):
|
|
"""Setup the Pilight component."""
|
|
from pilight import pilight
|
|
|
|
host = config[DOMAIN][CONF_HOST]
|
|
port = config[DOMAIN][CONF_PORT]
|
|
send_throttler = CallRateDelayThrottle(hass,
|
|
config[DOMAIN][CONF_SEND_DELAY])
|
|
|
|
try:
|
|
pilight_client = pilight.Client(host=host, port=port)
|
|
except (socket.error, socket.timeout) as err:
|
|
_LOGGER.error("Unable to connect to %s on port %s: %s",
|
|
host, port, err)
|
|
return False
|
|
|
|
def start_pilight_client(_):
|
|
"""Called once when Home Assistant starts."""
|
|
pilight_client.start()
|
|
|
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_pilight_client)
|
|
|
|
def stop_pilight_client(_):
|
|
"""Called once when Home Assistant stops."""
|
|
pilight_client.stop()
|
|
|
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_pilight_client)
|
|
|
|
@send_throttler.limited
|
|
def send_code(call):
|
|
"""Send RF code to the pilight-daemon."""
|
|
# Change type to dict from mappingproxy since data has to be JSON
|
|
# serializable
|
|
message_data = dict(call.data)
|
|
|
|
try:
|
|
pilight_client.send_code(message_data)
|
|
except IOError:
|
|
_LOGGER.error('Pilight send failed for %s', str(message_data))
|
|
|
|
hass.services.register(
|
|
DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA)
|
|
|
|
# Publish received codes on the HA event bus
|
|
# A whitelist of codes to be published in the event bus
|
|
whitelist = config[DOMAIN].get(CONF_WHITELIST)
|
|
|
|
def handle_received_code(data):
|
|
"""Called when RF codes are received."""
|
|
# Unravel dict of dicts to make event_data cut in automation rule
|
|
# possible
|
|
data = dict({'protocol': data['protocol'], 'uuid': data['uuid']},
|
|
**data['message'])
|
|
|
|
# No whitelist defined, put data on event bus
|
|
if not whitelist:
|
|
hass.bus.fire(EVENT, data)
|
|
# Check if data matches the defined whitelist
|
|
elif all(str(data[key]) in whitelist[key] for key in whitelist):
|
|
hass.bus.fire(EVENT, data)
|
|
|
|
pilight_client.set_callback(handle_received_code)
|
|
|
|
return True
|
|
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
class CallRateDelayThrottle(object):
|
|
"""Helper class to provide service call rate throttling.
|
|
|
|
This class provides a decorator to decorate service methods that need
|
|
to be throttled to not exceed a certain call rate per second.
|
|
One instance can be used on multiple service methods to archive
|
|
an overall throttling.
|
|
|
|
As this uses track_point_in_utc_time to schedule delayed executions
|
|
it should not block the mainloop.
|
|
"""
|
|
|
|
def __init__(self, hass, delay_seconds: float):
|
|
"""Initialize the delay handler."""
|
|
self._delay = timedelta(seconds=max(0.0, delay_seconds))
|
|
self._queue = []
|
|
self._active = False
|
|
self._lock = threading.Lock()
|
|
self._next_ts = dt_util.utcnow()
|
|
self._schedule = functools.partial(track_point_in_utc_time, hass)
|
|
|
|
def limited(self, method):
|
|
"""Decorator to delay calls on a certain method."""
|
|
@functools.wraps(method)
|
|
def decorated(*args, **kwargs):
|
|
"""The decorated function."""
|
|
if self._delay.total_seconds() == 0.0:
|
|
method(*args, **kwargs)
|
|
return
|
|
|
|
def action(event):
|
|
"""The action wrapper that gets scheduled."""
|
|
method(*args, **kwargs)
|
|
|
|
with self._lock:
|
|
self._next_ts = dt_util.utcnow() + self._delay
|
|
|
|
if len(self._queue) == 0:
|
|
self._active = False
|
|
else:
|
|
next_action = self._queue.pop(0)
|
|
self._schedule(next_action, self._next_ts)
|
|
|
|
with self._lock:
|
|
if self._active:
|
|
self._queue.append(action)
|
|
else:
|
|
self._active = True
|
|
schedule_ts = max(dt_util.utcnow(), self._next_ts)
|
|
self._schedule(action, schedule_ts)
|
|
|
|
return decorated
|