diff --git a/.coveragerc b/.coveragerc index cd338ddccf0..0efed20b330 100644 --- a/.coveragerc +++ b/.coveragerc @@ -88,6 +88,9 @@ omit = homeassistant/components/homematic.py homeassistant/components/*/homematic.py + homeassistant/components/pilight.py + homeassistant/components/*/pilight.py + homeassistant/components/knx.py homeassistant/components/switch/knx.py homeassistant/components/binary_sensor/knx.py diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py new file mode 100644 index 00000000000..c1b8c1e26e0 --- /dev/null +++ b/homeassistant/components/pilight.py @@ -0,0 +1,112 @@ +""" +Component to create an interface to a Pilight daemon (https://pilight.org/). + +Pilight can be used to send and receive signals from a radio frequency +module (RF receiver). + +RF commands received by the daemon are put on the HA event bus. +RF commands can also be send with a pilight.send service call. +""" +# pylint: disable=import-error +import logging +import socket + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ensure_list +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_HOST, CONF_PORT + +REQUIREMENTS = ['pilight==0.0.2'] + +DOMAIN = "pilight" +EVENT = 'pilight_received' +SERVICE_NAME = 'send' + +CONF_WHITELIST = 'whitelist' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST, default='127.0.0.1'): cv.string, + vol.Required(CONF_PORT, default=5000): vol.Coerce(int), + vol.Optional(CONF_WHITELIST): {cv.string: [cv.string]} + }), +}, extra=vol.ALLOW_EXTRA) + +# The pilight code schema depends on the protocol +# Thus only require to have the protocol information +ATTR_PROTOCOL = 'protocol' +RF_CODE_SCHEMA = vol.Schema({vol.Required(ATTR_PROTOCOL): cv.string}, + extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """Setup pilight component.""" + from pilight import pilight + + try: + pilight_client = pilight.Client(host=config[DOMAIN][CONF_HOST], + port=config[DOMAIN][CONF_PORT]) + except (socket.error, socket.timeout) as err: + _LOGGER.error( + "Unable to connect to %s on port %s: %s", + config[CONF_HOST], config[CONF_PORT], err) + return False + + # Start / stop pilight-daemon connection with HA start/stop + 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) + + def send_code(call): + """Send RF code to the pilight-daemon.""" + message_data = call.data + + # Patch data because of bug: + # https://github.com/pilight/pilight/issues/296 + # Protocol has to be in a list otherwise segfault in pilight-daemon + message_data["protocol"] = ensure_list(message_data["protocol"]) + + 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('whitelist', False) + + 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(data[key] in whitelist[key] for key in whitelist): + hass.bus.fire(EVENT, data) + + pilight_client.set_callback(handle_received_code) + + return True diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py new file mode 100644 index 00000000000..47e040ddb67 --- /dev/null +++ b/homeassistant/components/switch/pilight.py @@ -0,0 +1,110 @@ +""" +Support for switching devices via pilight to on and off. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.pilight/ +""" +import logging + +from homeassistant.helpers.config_validation import ensure_list +import homeassistant.components.pilight as pilight +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['pilight'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the pilight platform.""" + # Find and return switches controlled by pilight + switches = config.get('switches', {}) + devices = [] + + for dev_name, properties in switches.items(): + devices.append( + PilightSwitch( + hass, + properties.get('name', dev_name), + properties.get('on_code'), + properties.get('off_code'), + ensure_list(properties.get('on_code_receive', False)), + ensure_list(properties.get('off_code_receive', False)))) + + add_devices_callback(devices) + + +class PilightSwitch(SwitchDevice): + """Representation of a pilight switch.""" + + # pylint: disable=too-many-arguments, too-many-instance-attributes + def __init__(self, hass, name, code_on, code_off, + code_on_receive, code_off_receive): + """Initialize the switch.""" + self._hass = hass + self._name = name + self._state = False + self._code_on = code_on + self._code_off = code_off + self._code_on_receive = code_on_receive + self._code_off_receive = code_off_receive + + if any(self._code_on_receive) or any(self._code_off_receive): + hass.bus.listen(pilight.EVENT, self._handle_code) + + @property + def name(self): + """Get the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed, state set when correct code is received.""" + return False + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state + + def _handle_code(self, call): + """Check if received code by the pilight-daemon. + + If the code matches the receive on / off codes of this switch + the switch state is changed accordingly. + """ + # Check if a on code is defined to turn this switch on + if any(self._code_on_receive): + for on_code in self._code_on_receive: # Loop through codes + # True if on_code is contained in received code dict, not + # all items have to match + if on_code.items() <= call.data.items(): + self.turn_on() + # Call turn on only once, even when more than one on + # code is received + break + + # Check if a off code is defined to turn this switch off + if any(self._code_off_receive): + for off_code in self._code_off_receive: # Loop through codes + # True if off_code is contained in received code dict, not + # all items have to match + if off_code.items() <= call.data.items(): + self.turn_off() + # Call turn off only once, even when more than one off + # code is received + break + + def turn_on(self): + """Turn the switch on by calling pilight.send service with on code.""" + self._hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, + self._code_on, blocking=True) + self._state = True + self.update_ha_state() + + def turn_off(self): + """Turn the switch on by calling pilight.send service with off code.""" + self._hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, + self._code_off, blocking=True) + self._state = False + self.update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index ed09ae53c6a..58decd00ae7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,6 +229,9 @@ pexpect==4.0.1 # homeassistant.components.light.hue phue==0.8 +# homeassistant.components.pilight +pilight==0.0.2 + # homeassistant.components.media_player.plex # homeassistant.components.sensor.plex plexapi==2.0.2