diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 9fa5f5ea210..d7f8746de5a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -10,7 +10,7 @@ import logging import os import csv -from homeassistant.components import group, discovery, wink, isy994 +from homeassistant.components import group, discovery, wink, isy994, zwave from homeassistant.config import load_yaml_config_file from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) @@ -60,6 +60,7 @@ DISCOVERY_PLATFORMS = { wink.DISCOVER_LIGHTS: 'wink', isy994.DISCOVER_LIGHTS: 'isy994', discovery.SERVICE_HUE: 'hue', + zwave.DISCOVER_LIGHTS: 'zwave', } PROP_TO_ATTR = { diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py new file mode 100644 index 00000000000..10e75125e12 --- /dev/null +++ b/homeassistant/components/light/zwave.py @@ -0,0 +1,121 @@ +""" +homeassistant.components.light.zwave +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +""" +# pylint: disable=import-error +from openzwave.network import ZWaveNetwork +from pydispatch import dispatcher + +import homeassistant.components.zwave as zwave + +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.light import (Light, ATTR_BRIGHTNESS) +from threading import Timer + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Find and add zwave lights. """ + if discovery_info is None: + return + + node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] + value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] + + if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL: + return + if value.type != zwave.TYPE_BYTE: + return + if value.genre != zwave.GENRE_USER: + return + + value.set_change_verified(False) + add_devices([ZwaveDimmer(value)]) + + +def brightness_state(value): + """ Returns the brightness and state according to the current + data of given value. """ + if value.data > 0: + return (value.data / 99) * 255, STATE_ON + else: + return 255, STATE_OFF + + +class ZwaveDimmer(Light): + """ Provides a zwave dimmer. """ + # pylint: disable=too-many-arguments + def __init__(self, value): + self._value = value + self._node = value.node + + self._brightness, self._state = brightness_state(value) + + # Used for value change event handling + self._refreshing = False + self._timer = None + + dispatcher.connect( + self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + + def _value_changed(self, value): + """ Called when a value has changed on the network. """ + if self._value.value_id != value.value_id: + return + + if self._refreshing: + self._refreshing = False + self._brightness, self._state = brightness_state(value) + else: + def _refresh_value(): + """Used timer callback for delayed value refresh.""" + self._refreshing = True + self._value.refresh() + + if self._timer is not None and self._timer.isAlive(): + self._timer.cancel() + + self._timer = Timer(2, _refresh_value) + self._timer.start() + + self.update_ha_state() + + @property + def should_poll(self): + """ No polling needed for a light. """ + return False + + @property + def name(self): + """ Returns the name of the device if any. """ + name = self._node.name or "{}".format(self._node.product_name) + + return "{}".format(name or self._value.label) + + @property + def brightness(self): + """ Brightness of this light between 0..255. """ + return self._brightness + + @property + def is_on(self): + """ True if device is on. """ + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """ Turn the device on. """ + + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + # Zwave multilevel switches use a range of [0, 99] to control + # brightness. + brightness = (self._brightness / 255) * 99 + + if self._node.set_dimmer(self._value.value_id, brightness): + self._state = STATE_ON + + def turn_off(self, **kwargs): + """ Turn the device off. """ + if self._node.set_dimmer(self._value.value_id, 0): + self._state = STATE_OFF diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 65034f0fb17..0c93b3d8cf1 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -31,6 +31,16 @@ DEVICE_MAPPINGS = { def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up Z-Wave sensors. """ + + # Return on empty `discovery_info`. Given you configure HA with: + # + # sensor: + # platform: zwave + # + # `setup_platform` will be called without `discovery_info`. + if discovery_info is None: + return + node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index daa323c6378..e5b321c989d 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -22,15 +22,33 @@ DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" CONF_DEBUG = "debug" DISCOVER_SENSORS = "zwave.sensors" +DISCOVER_LIGHTS = "zwave.light" +COMMAND_CLASS_SWITCH_MULTILEVEL = 38 COMMAND_CLASS_SENSOR_BINARY = 48 COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_BATTERY = 128 -# list of tuple (DOMAIN, discovered service, supported command classes) +GENRE_WHATEVER = None +GENRE_USER = "User" + +TYPE_WHATEVER = None +TYPE_BYTE = "Byte" +TYPE_BOOL = "Bool" + +# list of tuple (DOMAIN, discovered service, supported command +# classes, value type) DISCOVERY_COMPONENTS = [ - ('sensor', DISCOVER_SENSORS, - [COMMAND_CLASS_SENSOR_BINARY, COMMAND_CLASS_SENSOR_MULTILEVEL]), + ('sensor', + DISCOVER_SENSORS, + [COMMAND_CLASS_SENSOR_BINARY, COMMAND_CLASS_SENSOR_MULTILEVEL], + TYPE_WHATEVER, + GENRE_WHATEVER), + ('light', + DISCOVER_LIGHTS, + [COMMAND_CLASS_SWITCH_MULTILEVEL], + TYPE_BYTE, + GENRE_USER), ] ATTR_NODE_ID = "node_id" @@ -110,19 +128,31 @@ def setup(hass, config): def value_added(node, value): """ Called when a value is added to a node on the network. """ - for component, discovery_service, command_ids in DISCOVERY_COMPONENTS: - if value.command_class in command_ids: - # Ensure component is loaded - bootstrap.setup_component(hass, component, config) - # Fire discovery event - hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_service, - ATTR_DISCOVERED: { - ATTR_NODE_ID: node.node_id, - ATTR_VALUE_ID: value.value_id, - } - }) + for (component, + discovery_service, + command_ids, + value_type, + value_genre) in DISCOVERY_COMPONENTS: + + if value.command_class not in command_ids: + continue + if value_type is not None and value_type != value.type: + continue + if value_genre is not None and value_genre != value.genre: + continue + + # Ensure component is loaded + bootstrap.setup_component(hass, component, config) + + # Fire discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_service, + ATTR_DISCOVERED: { + ATTR_NODE_ID: node.node_id, + ATTR_VALUE_ID: value.value_id, + } + }) dispatcher.connect( value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False)