diff --git a/.coveragerc b/.coveragerc index c37e4f47291..63eb4ea9be1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -26,6 +26,7 @@ omit = homeassistant/components/modbus.py homeassistant/components/*/modbus.py + homeassistant/components/tellstick.py homeassistant/components/*/tellstick.py homeassistant/components/tellduslive.py diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 357a6156c3a..159cd19a96a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -9,7 +9,8 @@ import os import csv from homeassistant.components import ( - group, discovery, wemo, wink, isy994, zwave, insteon_hub, mysensors) + group, discovery, wemo, wink, isy994, + zwave, insteon_hub, mysensors, tellstick) from homeassistant.config import load_yaml_config_file from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, @@ -64,6 +65,7 @@ DISCOVERY_PLATFORMS = { discovery.SERVICE_HUE: 'hue', zwave.DISCOVER_LIGHTS: 'zwave', mysensors.DISCOVER_LIGHTS: 'mysensors', + tellstick.DISCOVER_LIGHTS: 'tellstick', } PROP_TO_ATTR = { diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 8d54ddb1604..3571bd9737d 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -4,127 +4,80 @@ Support for Tellstick lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.tellstick/ """ +from homeassistant.components import tellstick from homeassistant.components.light import ATTR_BRIGHTNESS, Light -from homeassistant.const import EVENT_HOMEASSISTANT_STOP - -REQUIREMENTS = ['tellcore-py==1.1.2'] -SIGNAL_REPETITIONS = 1 +from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS, + ATTR_DISCOVER_DEVICES, + ATTR_DISCOVER_CONFIG) # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick lights.""" - import tellcore.telldus as telldus - from tellcore.library import DirectCallbackDispatcher - import tellcore.constants as tellcore_constants + if (discovery_info is None or + discovery_info[ATTR_DISCOVER_DEVICES] is None or + tellstick.TELLCORE_REGISTRY is None): + return - core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) - signal_repetitions = config.get('signal_repetitions', SIGNAL_REPETITIONS) + signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, + DEFAULT_SIGNAL_REPETITIONS) - switches_and_lights = core.devices() - lights = [] - - for switch in switches_and_lights: - if switch.methods(tellcore_constants.TELLSTICK_DIM): - lights.append(TellstickLight(switch, signal_repetitions)) - - def _device_event_callback(id_, method, data, cid): - """Called from the TelldusCore library to update one device.""" - for light_device in lights: - if light_device.tellstick_device.id == id_: - # Execute the update in another thread - light_device.update_ha_state(True) - break - - callback_id = core.register_device_event(_device_event_callback) - - def unload_telldus_lib(event): - """Un-register the callback bindings.""" - if callback_id is not None: - core.unregister_callback(callback_id) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib) - - add_devices_callback(lights) + add_devices(TellstickLight( + tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions) + for switch_id in discovery_info[ATTR_DISCOVER_DEVICES]) -class TellstickLight(Light): +class TellstickLight(tellstick.TellstickDevice, Light): """Representation of a Tellstick light.""" def __init__(self, tellstick_device, signal_repetitions): """Initialize the light.""" - import tellcore.constants as tellcore_constants - - self.tellstick_device = tellstick_device - self.signal_repetitions = signal_repetitions - self._brightness = 0 - - self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | - tellcore_constants.TELLSTICK_TURNOFF | - tellcore_constants.TELLSTICK_DIM | - tellcore_constants.TELLSTICK_UP | - tellcore_constants.TELLSTICK_DOWN) - self.update() - - @property - def name(self): - """Return the name of the switch if any.""" - return self.tellstick_device.name + self._brightness = 255 + tellstick.TellstickDevice.__init__(self, + tellstick_device, + signal_repetitions) @property def is_on(self): """Return true if switch is on.""" - return self._brightness > 0 + return self._state @property def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness - def turn_off(self, **kwargs): - """Turn the switch off.""" - for _ in range(self.signal_repetitions): + def set_tellstick_state(self, last_command_sent, last_data_sent): + """Update the internal representation of the switch.""" + from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_DIM + if last_command_sent == TELLSTICK_DIM: + if last_data_sent is not None: + self._brightness = int(last_data_sent) + self._state = self._brightness > 0 + else: + self._state = last_command_sent == TELLSTICK_TURNON + + def _send_tellstick_command(self, command, data): + """Handle the turn_on / turn_off commands.""" + from tellcore.constants import (TELLSTICK_TURNOFF, TELLSTICK_DIM) + if command == TELLSTICK_TURNOFF: self.tellstick_device.turn_off() - self._brightness = 0 - self.update_ha_state() + elif command == TELLSTICK_DIM: + self.tellstick_device.dim(self._brightness) + else: + raise NotImplementedError( + "Command not implemented: {}".format(command)) def turn_on(self, **kwargs): """Turn the switch on.""" + from tellcore.constants import TELLSTICK_DIM brightness = kwargs.get(ATTR_BRIGHTNESS) - - if brightness is None: - self._brightness = 255 - else: + if brightness is not None: self._brightness = brightness - for _ in range(self.signal_repetitions): - self.tellstick_device.dim(self._brightness) - self.update_ha_state() + self.call_tellstick(TELLSTICK_DIM, self._brightness) - def update(self): - """Update state of the light.""" - import tellcore.constants as tellcore_constants - - last_command = self.tellstick_device.last_sent_command( - self.last_sent_command_mask) - - if last_command == tellcore_constants.TELLSTICK_TURNON: - self._brightness = 255 - elif last_command == tellcore_constants.TELLSTICK_TURNOFF: - self._brightness = 0 - elif (last_command == tellcore_constants.TELLSTICK_DIM or - last_command == tellcore_constants.TELLSTICK_UP or - last_command == tellcore_constants.TELLSTICK_DOWN): - last_sent_value = self.tellstick_device.last_sent_value() - if last_sent_value is not None: - self._brightness = last_sent_value - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def assumed_state(self): - """Tellstick devices are always assumed state.""" - return True + def turn_off(self, **kwargs): + """Turn the switch off.""" + from tellcore.constants import TELLSTICK_TURNOFF + self.call_tellstick(TELLSTICK_TURNOFF) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 386fb34fa17..1797fc2af8a 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -16,7 +16,8 @@ from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.components import ( - group, wemo, wink, isy994, verisure, zwave, tellduslive, mysensors) + group, wemo, wink, isy994, verisure, + zwave, tellduslive, tellstick, mysensors) DOMAIN = 'switch' SCAN_INTERVAL = 30 @@ -40,6 +41,7 @@ DISCOVERY_PLATFORMS = { zwave.DISCOVER_SWITCHES: 'zwave', tellduslive.DISCOVER_SWITCHES: 'tellduslive', mysensors.DISCOVER_SWITCHES: 'mysensors', + tellstick.DISCOVER_SWITCHES: 'tellstick', } PROP_TO_ATTR = { diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index b55232e14fc..8014244e828 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -4,98 +4,56 @@ Support for Tellstick switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.tellstick/ """ -import logging - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.components import tellstick +from homeassistant.components.tellstick import (ATTR_DISCOVER_DEVICES, + ATTR_DISCOVER_CONFIG) from homeassistant.helpers.entity import ToggleEntity -SIGNAL_REPETITIONS = 1 -REQUIREMENTS = ['tellcore-py==1.1.2'] -_LOGGER = logging.getLogger(__name__) - # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Tellstick switches.""" - import tellcore.telldus as telldus - import tellcore.constants as tellcore_constants - from tellcore.library import DirectCallbackDispatcher + if (discovery_info is None or + discovery_info[ATTR_DISCOVER_DEVICES] is None or + tellstick.TELLCORE_REGISTRY is None): + return - core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) - signal_repetitions = config.get('signal_repetitions', SIGNAL_REPETITIONS) - switches_and_lights = core.devices() + # Allow platform level override, fallback to module config + signal_repetitions = discovery_info.get( + ATTR_DISCOVER_CONFIG, tellstick.DEFAULT_SIGNAL_REPETITIONS) - switches = [] - for switch in switches_and_lights: - if not switch.methods(tellcore_constants.TELLSTICK_DIM): - switches.append( - TellstickSwitchDevice(switch, signal_repetitions)) - - def _device_event_callback(id_, method, data, cid): - """Called from the TelldusCore library to update one device.""" - for switch_device in switches: - if switch_device.tellstick_device.id == id_: - switch_device.update_ha_state() - break - - callback_id = core.register_device_event(_device_event_callback) - - def unload_telldus_lib(event): - """Un-register the callback bindings.""" - if callback_id is not None: - core.unregister_callback(callback_id) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib) - - add_devices_callback(switches) + add_devices(TellstickSwitchDevice( + tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions) + for switch_id in discovery_info[ATTR_DISCOVER_DEVICES]) -class TellstickSwitchDevice(ToggleEntity): +class TellstickSwitchDevice(tellstick.TellstickDevice, ToggleEntity): """Representation of a Tellstick switch.""" - def __init__(self, tellstick_device, signal_repetitions): - """Initialize the Tellstick switch.""" - import tellcore.constants as tellcore_constants - - self.tellstick_device = tellstick_device - self.signal_repetitions = signal_repetitions - - self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | - tellcore_constants.TELLSTICK_TURNOFF) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def assumed_state(self): - """The Tellstick devices are always assumed state.""" - return True - - @property - def name(self): - """Return the name of the switch if any.""" - return self.tellstick_device.name - @property def is_on(self): """Return true if switch is on.""" - import tellcore.constants as tellcore_constants + return self._state - last_command = self.tellstick_device.last_sent_command( - self.last_sent_command_mask) + def set_tellstick_state(self, last_command_sent, last_data_sent): + """Update the internal representation of the switch.""" + from tellcore.constants import TELLSTICK_TURNON + self._state = last_command_sent == TELLSTICK_TURNON - return last_command == tellcore_constants.TELLSTICK_TURNON + def _send_tellstick_command(self, command, data): + """Handle the turn_on / turn_off commands.""" + from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_TURNOFF + if command == TELLSTICK_TURNON: + self.tellstick_device.turn_on() + elif command == TELLSTICK_TURNOFF: + self.tellstick_device.turn_off() def turn_on(self, **kwargs): """Turn the switch on.""" - for _ in range(self.signal_repetitions): - self.tellstick_device.turn_on() - self.update_ha_state() + from tellcore.constants import TELLSTICK_TURNON + self.call_tellstick(TELLSTICK_TURNON) def turn_off(self, **kwargs): """Turn the switch off.""" - for _ in range(self.signal_repetitions): - self.tellstick_device.turn_off() - self.update_ha_state() + from tellcore.constants import TELLSTICK_TURNOFF + self.call_tellstick(TELLSTICK_TURNOFF) diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py new file mode 100644 index 00000000000..04927d9e652 --- /dev/null +++ b/homeassistant/components/tellstick.py @@ -0,0 +1,217 @@ +""" +Tellstick Component. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/Tellstick/ +""" +import logging +import threading + +from homeassistant import bootstrap +from homeassistant.const import ( + ATTR_DISCOVERED, ATTR_SERVICE, + EVENT_PLATFORM_DISCOVERED, EVENT_HOMEASSISTANT_STOP) +from homeassistant.loader import get_component +from homeassistant.helpers.entity import Entity + +DOMAIN = "tellstick" + +REQUIREMENTS = ['tellcore-py==1.1.2'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_SIGNAL_REPETITIONS = "signal_repetitions" +DEFAULT_SIGNAL_REPETITIONS = 1 + +DISCOVER_SWITCHES = "tellstick.switches" +DISCOVER_LIGHTS = "tellstick.lights" +DISCOVERY_TYPES = {"switch": DISCOVER_SWITCHES, + "light": DISCOVER_LIGHTS} + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_DISCOVER_CONFIG = "config" + +# Use a global tellstick domain lock to handle +# tellcore errors then calling to concurrently +TELLSTICK_LOCK = threading.Lock() + +# Keep a reference the the callback registry +# Used from entities that register callback listeners +TELLCORE_REGISTRY = None + + +def _discover(hass, config, found_devices, component_name): + """Setup and send the discovery event.""" + if not len(found_devices): + return + + _LOGGER.info("discovered %d new %s devices", + len(found_devices), component_name) + + component = get_component(component_name) + bootstrap.setup_component(hass, component.DOMAIN, + config) + + signal_repetitions = config[DOMAIN].get( + ATTR_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS) + + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: DISCOVERY_TYPES[component_name], + ATTR_DISCOVERED: {ATTR_DISCOVER_DEVICES: found_devices, + ATTR_DISCOVER_CONFIG: + signal_repetitions}}) + + +def setup(hass, config): + """Setup the Tellstick component.""" + # pylint: disable=global-statement, import-error + global TELLCORE_REGISTRY + + import tellcore.telldus as telldus + import tellcore.constants as tellcore_constants + from tellcore.library import DirectCallbackDispatcher + + core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) + + TELLCORE_REGISTRY = TellstickRegistry(hass, core) + + devices = core.devices() + + # Register devices + TELLCORE_REGISTRY.register_devices(devices) + + # Discover the switches + _discover(hass, config, [switch.id for switch in + devices if not switch.methods( + tellcore_constants.TELLSTICK_DIM)], + "switch") + + # Discover the lights + _discover(hass, config, [light.id for light in + devices if light.methods( + tellcore_constants.TELLSTICK_DIM)], + "light") + + return True + + +class TellstickRegistry: + """Handle everything around tellstick callbacks. + + Keeps a map device ids to home-assistant entities. + Also responsible for registering / cleanup of callbacks. + + All device specific logic should be elsewhere (Entities). + + """ + + def __init__(self, hass, tellcore_lib): + """Init the tellstick mappings and callbacks.""" + self._core_lib = tellcore_lib + # used when map callback device id to ha entities. + self._id_to_entity_map = {} + self._id_to_device_map = {} + self._setup_device_callback(hass, tellcore_lib) + + def _device_callback(self, tellstick_id, method, data, cid): + """Handle the actual callback from tellcore.""" + entity = self._id_to_entity_map.get(tellstick_id, None) + if entity is not None: + entity.set_tellstick_state(method, data) + entity.update_ha_state() + + def _setup_device_callback(self, hass, tellcore_lib): + """Register the callback handler.""" + callback_id = tellcore_lib.register_device_event( + self._device_callback) + + def clean_up_callback(event): + """Unregister the callback bindings.""" + if callback_id is not None: + tellcore_lib.unregister_callback(callback_id) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, clean_up_callback) + + def register_entity(self, tellcore_id, entity): + """Register a new entity to receive callback updates.""" + self._id_to_entity_map[tellcore_id] = entity + + def register_devices(self, devices): + """Register a list of devices.""" + self._id_to_device_map.update({device.id: + device for device in devices}) + + def get_device(self, tellcore_id): + """Return a device by tellcore_id.""" + return self._id_to_device_map.get(tellcore_id, None) + + +class TellstickDevice(Entity): + """Represents a Tellstick device. + + Contains the common logic for all Tellstick devices. + + """ + + def __init__(self, tellstick_device, signal_repetitions): + """Init the tellstick device.""" + self.signal_repetitions = signal_repetitions + self._state = None + self.tellstick_device = tellstick_device + # add to id to entity mapping + TELLCORE_REGISTRY.register_entity(tellstick_device.id, self) + # Query tellcore for the current state + self.update() + + @property + def should_poll(self): + """Tell Home Assistant not to poll this entity.""" + return False + + @property + def assumed_state(self): + """Tellstick devices are always assumed state.""" + return True + + @property + def name(self): + """Return the name of the switch if any.""" + return self.tellstick_device.name + + def set_tellstick_state(self, last_command_sent, last_data_sent): + """Set the private switch state.""" + raise NotImplementedError( + "set_tellstick_state needs to be implemented.") + + def _send_tellstick_command(self, command, data): + """Do the actual call to the tellstick device.""" + raise NotImplementedError( + "_call_tellstick needs to be implemented.") + + def call_tellstick(self, command, data=None): + """Send a command to the device.""" + from tellcore.library import TelldusError + with TELLSTICK_LOCK: + try: + for _ in range(self.signal_repetitions): + self._send_tellstick_command(command, data) + # Update the internal state + self.set_tellstick_state(command, data) + self.update_ha_state() + except TelldusError: + _LOGGER.error(TelldusError) + + def update(self): + """Poll the current state of the device.""" + import tellcore.constants as tellcore_constants + from tellcore.library import TelldusError + try: + last_command = self.tellstick_device.last_sent_command( + tellcore_constants.TELLSTICK_TURNON | + tellcore_constants.TELLSTICK_TURNOFF | + tellcore_constants.TELLSTICK_DIM + ) + last_value = self.tellstick_device.last_sent_value() + self.set_tellstick_state(last_command, last_value) + except TelldusError: + _LOGGER.error(TelldusError) diff --git a/requirements_all.txt b/requirements_all.txt index 56043896810..4c3226938a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -256,9 +256,8 @@ speedtest-cli==0.3.4 # homeassistant.components.sensor.steam_online steamodd==4.21 -# homeassistant.components.light.tellstick +# homeassistant.components.tellstick # homeassistant.components.sensor.tellstick -# homeassistant.components.switch.tellstick tellcore-py==1.1.2 # homeassistant.components.tellduslive