From 8eef978241229b74980ccfbec162c01a8a70456c Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Sat, 9 Jan 2016 14:51:49 +0100 Subject: [PATCH] Add support for the SCSGate device Support the SCSGate device. This will allow home-assistant to interact with BTicino/Legrand MyHome system. Signed-off-by: Flavio Castelli --- .coveragerc | 3 + homeassistant/components/light/scsgate.py | 118 ++++++++++ .../components/rollershutter/scsgate.py | 98 +++++++++ homeassistant/components/scsgate.py | 162 ++++++++++++++ homeassistant/components/switch/scsgate.py | 202 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 586 insertions(+) create mode 100644 homeassistant/components/light/scsgate.py create mode 100644 homeassistant/components/rollershutter/scsgate.py create mode 100644 homeassistant/components/scsgate.py create mode 100644 homeassistant/components/switch/scsgate.py diff --git a/.coveragerc b/.coveragerc index 75f4cbd20fd..14bd1c68cb3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -53,6 +53,9 @@ omit = homeassistant/components/rpi_gpio.py homeassistant/components/*/rpi_gpio.py + homeassistant/components/scsgate.py + homeassistant/components/*/scsgate.py + homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/light/scsgate.py new file mode 100644 index 00000000000..275e9812ace --- /dev/null +++ b/homeassistant/components/light/scsgate.py @@ -0,0 +1,118 @@ +""" +homeassistant.components.light.scsgate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for SCSGate lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.scsgate/ +""" +import logging +import homeassistant.components.scsgate as scsgate + +from homeassistant.components.light import Light + +from homeassistant.const import ATTR_ENTITY_ID + +DEPENDENCIES = ['scsgate'] + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add the SCSGate swiches defined inside of the configuration file. """ + + devices = config.get('devices') + lights = [] + logger = logging.getLogger(__name__) + + if devices: + for _, entity_info in devices.items(): + if entity_info['scs_id'] in scsgate.SCSGATE.devices: + continue + + logger.info("Adding %s scsgate.light", entity_info['name']) + + name = entity_info['name'] + scs_id = entity_info['scs_id'] + light = SCSGateLight( + name=name, + scs_id=scs_id, + logger=logger) + lights.append(light) + + add_devices_callback(lights) + scsgate.SCSGATE.add_devices_to_register(lights) + + +class SCSGateLight(Light): + """ Provides a SCSGate light. """ + def __init__(self, scs_id, name, logger): + self._name = name + self._scs_id = scs_id + self._toggled = False + self._logger = logger + + @property + def scs_id(self): + """ SCS ID """ + return self._scs_id + + @property + def should_poll(self): + """ No polling needed for a SCSGate light. """ + return False + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def is_on(self): + """ True if light is on. """ + return self._toggled + + def turn_on(self, **kwargs): + """ Turn the device on. """ + from scsgate.tasks import ToggleStatusTask + + scsgate.SCSGATE.append_task( + ToggleStatusTask( + target=self._scs_id, + toggled=True)) + + self._toggled = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + from scsgate.tasks import ToggleStatusTask + + scsgate.SCSGATE.append_task( + ToggleStatusTask( + target=self._scs_id, + toggled=False)) + + self._toggled = False + self.update_ha_state() + + def process_event(self, message): + """ Handle a SCSGate message related with this light """ + if self._toggled == message.toggled: + self._logger.info( + "Light %s, ignoring message %s because state already active", + self._scs_id, message) + # Nothing changed, ignoring + return + + self._toggled = message.toggled + self.update_ha_state() + + command = "off" + if self._toggled: + command = "on" + + self.hass.bus.fire( + 'button_pressed', { + ATTR_ENTITY_ID: self._scs_id, + 'state': command + } + ) diff --git a/homeassistant/components/rollershutter/scsgate.py b/homeassistant/components/rollershutter/scsgate.py new file mode 100644 index 00000000000..e47d1574697 --- /dev/null +++ b/homeassistant/components/rollershutter/scsgate.py @@ -0,0 +1,98 @@ +""" +homeassistant.components.rollershutter.scsgate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a SCSGate rollershutter. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rollershutter.scsgate/ +""" +import logging +import homeassistant.components.scsgate as scsgate +from homeassistant.components.rollershutter import RollershutterDevice + + +DEPENDENCIES = ['scsgate'] + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add the SCSGate swiches defined inside of the configuration file. """ + + devices = config.get('devices') + rollershutters = [] + logger = logging.getLogger(__name__) + + if devices: + for _, entity_info in devices.items(): + if entity_info['scs_id'] in scsgate.SCSGATE.devices: + continue + + logger.info("Adding %s scsgate.rollershutter", entity_info['name']) + + name = entity_info['name'] + scs_id = entity_info['scs_id'] + rollershutter = SCSGateRollerShutter( + name=name, + scs_id=scs_id, + logger=logger) + scsgate.SCSGATE.add_device(rollershutter) + rollershutters.append(rollershutter) + + add_devices_callback(rollershutters) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class SCSGateRollerShutter(RollershutterDevice): + """ Represents a rollershutter that can be controlled using SCSGate. """ + def __init__(self, scs_id, name, logger): + self._scs_id = scs_id + self._name = name + self._logger = logger + + @property + def scs_id(self): + """ SCSGate ID """ + return self._scs_id + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the rollershutter. """ + return self._name + + @property + def current_position(self): + """ + Return current position of rollershutter. + None is unknown, 0 is closed, 100 is fully open. + """ + return None + + def move_up(self, **kwargs): + """ Move the rollershutter up. """ + from scsgate.tasks import RaiseRollerShutterTask + + scsgate.SCSGATE.append_task( + RaiseRollerShutterTask(target=self._scs_id)) + + def move_down(self, **kwargs): + """ Move the rollershutter down. """ + from scsgate.tasks import LowerRollerShutterTask + + scsgate.SCSGATE.append_task( + LowerRollerShutterTask(target=self._scs_id)) + + def stop(self, **kwargs): + """ Stop the device. """ + from scsgate.tasks import HaltRollerShutterTask + + scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id)) + + def process_event(self, message): + """ Handle a SCSGate message related with this rollershutter """ + self._logger.debug( + "Rollershutter %s, got message %s", + self._scs_id, message.toggled) diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate.py new file mode 100644 index 00000000000..e9b762f140c --- /dev/null +++ b/homeassistant/components/scsgate.py @@ -0,0 +1,162 @@ +""" +homeassistant.components.scsgate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides support for SCSGate components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/scsgate/ +""" +import logging +from threading import Lock +from homeassistant.core import EVENT_HOMEASSISTANT_STOP + +REQUIREMENTS = ['scsgate==0.1.0'] + + +DOMAIN = "scsgate" + +SCSGATE = None +_LOGGER = logging.getLogger(__name__) + + +class SCSGate: + """ Class dealing with the SCSGate device via scsgate.Reactor """ + + def __init__(self, device, logger): + self._logger = logger + self._devices = {} + self._devices_to_register = {} + self._devices_to_register_lock = Lock() + self._device_being_registered = None + self._device_being_registered_lock = Lock() + + from scsgate.connection import Connection + connection = Connection(device=device, logger=self._logger) + + from scsgate.reactor import Reactor + self._reactor = Reactor( + connection=connection, + logger=self._logger, + handle_message=self.handle_message) + + def handle_message(self, message): + """ Method called whenever a message is seen on the bus """ + from scsgate.messages import StateMessage, ScenarioTriggeredMessage + + self._logger.debug("Received message {}".format(message)) + if not isinstance(message, StateMessage) and \ + not isinstance(message, ScenarioTriggeredMessage): + msg = "Ignored message {} - not releavant type".format( + message) + self._logger.debug(msg) + return + + if message.entity in self._devices: + new_device_activated = False + with self._devices_to_register_lock: + if message.entity == self._device_being_registered: + self._device_being_registered = None + new_device_activated = True + if new_device_activated: + self._activate_next_device() + + # pylint: disable=broad-except + try: + self._devices[message.entity].process_event(message) + except Exception as exception: + msg = "Exception while processing event: {}".format( + exception) + self._logger.error(msg) + else: + self._logger.info( + "Ignoring state message for device {} because unknonw".format( + message.entity)) + + @property + def devices(self): + """ Dictionary with known devices. Key is device ID, + value is the device itself """ + return self._devices + + def add_device(self, device): + """ Adds the specified device to the list of the already registered ones. + + Beware: this is not what you usually want to do, take + a look at `add_devices_to_register` + """ + self._devices[device.scs_id] = device + + def add_devices_to_register(self, devices): + """ List of devices to be registered. + + Arguments: + * devices: list of devices to register + """ + with self._devices_to_register_lock: + for device in devices: + self._devices_to_register[device.scs_id] = device + self._activate_next_device() + + def _activate_next_device(self): + """ Starts the activation of the 1st device inside of self._devices """ + from scsgate.tasks import GetStatusTask + + with self._devices_to_register_lock: + if len(self._devices_to_register) == 0: + return + _, device = self._devices_to_register.popitem() + self._devices[device.scs_id] = device + self._device_being_registered = device.scs_id + self._reactor.append_task(GetStatusTask(target=device.scs_id)) + + def is_device_registered(self, device_id): + """ Checks whether a device is already registered or not + + Arguments: + device_id: the ID of the device to look for + """ + with self._devices_to_register_lock: + if device_id in self._devices_to_register.keys(): + return False + + with self._device_being_registered_lock: + if device_id == self._device_being_registered: + return False + + return True + + def start(self): + """ Start the scsgate.Reactor """ + self._reactor.start() + + def stop(self): + """ Stop the scsgate.Reactor """ + self._reactor.stop() + + def append_task(self, task): + """ Registers a new task to be executed """ + self._reactor.append_task(task) + + +def setup(hass, config): + """ Setup the SCSGate component. """ + device = config['scsgate']['device'] + global SCSGATE + + # pylint: disable=broad-except + try: + SCSGATE = SCSGate(device=device, logger=_LOGGER) + SCSGATE.start() + except Exception as exception: + _LOGGER.error("Cannot setup SCSGate component: %s", exception) + return False + + def stop_monitor(event): + """ Invoked when home-assistant is exiting. Performs the necessary + cleanups """ + _LOGGER.info("Stopping SCSGate monitor thread") + SCSGATE.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_monitor) + + return True diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py new file mode 100644 index 00000000000..7755168585e --- /dev/null +++ b/homeassistant/components/switch/scsgate.py @@ -0,0 +1,202 @@ +""" +homeassistant.components.switch.scsgate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for SCSGate switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.scsgate/ +""" +import logging +import homeassistant.components.scsgate as scsgate + +from homeassistant.components.switch import SwitchDevice + +from homeassistant.const import ATTR_ENTITY_ID + + +DEPENDENCIES = ['scsgate'] + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add the SCSGate swiches defined inside of the configuration file. """ + + logger = logging.getLogger(__name__) + + _setup_traditional_switches( + logger=logger, + config=config, + add_devices_callback=add_devices_callback) + + _setup_scenario_switches( + logger=logger, + config=config, + hass=hass) + + +def _setup_traditional_switches(logger, config, add_devices_callback): + """ Add traditional SCSGate switches """ + traditional = config.get('traditional') + switches = [] + + if traditional: + for _, entity_info in traditional.items(): + if entity_info['scs_id'] in scsgate.SCSGATE.devices: + continue + + logger.info( + "Adding %s scsgate.traditional_switch", entity_info['name']) + + name = entity_info['name'] + scs_id = entity_info['scs_id'] + + switch = SCSGateSwitch( + name=name, + scs_id=scs_id, + logger=logger) + switches.append(switch) + + add_devices_callback(switches) + scsgate.SCSGATE.add_devices_to_register(switches) + + +def _setup_scenario_switches(logger, config, hass): + """ Add only SCSGate scenario switches """ + scenario = config.get("scenario") + + if scenario: + for _, entity_info in scenario.items(): + if entity_info['scs_id'] in scsgate.SCSGATE.devices: + continue + + logger.info( + "Adding %s scsgate.scenario_switch", entity_info['name']) + + name = entity_info['name'] + scs_id = entity_info['scs_id'] + + switch = SCSGateScenarioSwitch( + name=name, + scs_id=scs_id, + logger=logger, + hass=hass) + scsgate.SCSGATE.add_device(switch) + + +class SCSGateSwitch(SwitchDevice): + """ Provides a SCSGate switch. """ + def __init__(self, scs_id, name, logger): + self._name = name + self._scs_id = scs_id + self._toggled = False + self._logger = logger + + @property + def scs_id(self): + """ SCS ID """ + return self._scs_id + + @property + def should_poll(self): + """ No polling needed for a SCSGate switch. """ + return False + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def is_on(self): + """ True if switch is on. """ + return self._toggled + + def turn_on(self, **kwargs): + """ Turn the device on. """ + from scsgate.tasks import ToggleStatusTask + + scsgate.SCSGATE.append_task( + ToggleStatusTask( + target=self._scs_id, + toggled=True)) + + self._toggled = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + from scsgate.tasks import ToggleStatusTask + + scsgate.SCSGATE.append_task( + ToggleStatusTask( + target=self._scs_id, + toggled=False)) + + self._toggled = False + self.update_ha_state() + + def process_event(self, message): + """ Handle a SCSGate message related with this switch""" + if self._toggled == message.toggled: + self._logger.info( + "Switch %s, ignoring message %s because state already active", + self._scs_id, message) + # Nothing changed, ignoring + return + + self._toggled = message.toggled + self.update_ha_state() + + command = "off" + if self._toggled: + command = "on" + + self.hass.bus.fire( + 'button_pressed', { + ATTR_ENTITY_ID: self._scs_id, + 'state': command + } + ) + + +class SCSGateScenarioSwitch: + """ Provides a SCSGate scenario switch. + + This switch is always in a 'off" state, when toggled + it's used to trigger events + """ + def __init__(self, scs_id, name, logger, hass): + self._name = name + self._scs_id = scs_id + self._logger = logger + self._hass = hass + + @property + def scs_id(self): + """ SCS ID """ + return self._scs_id + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + def process_event(self, message): + """ Handle a SCSGate message related with this switch""" + from scsgate.messages import StateMessage, ScenarioTriggeredMessage + + if isinstance(message, StateMessage): + scenario_id = message.bytes[4] + elif isinstance(message, ScenarioTriggeredMessage): + scenario_id = message.scenario + else: + self._logger.warn( + "Scenario switch: received unknown message %s", + message) + return + + self._hass.bus.fire( + 'scenario_switch_triggered', { + ATTR_ENTITY_ID: int(self._scs_id), + 'scenario_id': int(scenario_id, 16) + } + ) diff --git a/requirements_all.txt b/requirements_all.txt index 985bc324964..fa96f810e7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -143,6 +143,9 @@ https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip#RFXtrx==0.2 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 +# homeassistant.components.scsgate +scsgate==0.1.0 + # homeassistant.components.sensor.bitcoin blockchain==1.2.1