From e7b9b86c64b3bd3f1b326ff24c8c283eb17232bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Feb 2015 17:36:28 -0800 Subject: [PATCH] Get Z-Wave sensors to work with Home Assistant --- Dockerfile | 15 +- homeassistant/components/sensor/__init__.py | 29 +--- homeassistant/components/sensor/zwave.py | 154 ++++++++++++++++++++ homeassistant/components/zwave.py | 97 ++++++++++++ homeassistant/const.py | 7 + scripts/build_python_openzwave | 28 ++++ scripts/dev_docker | 7 +- scripts/dev_openzwave_docker | 18 +++ 8 files changed, 313 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/sensor/zwave.py create mode 100644 homeassistant/components/zwave.py create mode 100755 scripts/build_python_openzwave create mode 100755 scripts/dev_openzwave_docker diff --git a/Dockerfile b/Dockerfile index 6d34e1e4506b..a78322bbd80d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,19 +7,6 @@ RUN apt-get update && \ apt-get install -y cython3 libudev-dev python-sphinx python3-setuptools mercurial && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ pip3 install cython && \ - cd .. && \ - git clone https://github.com/Artanis/louie.git && \ - cd louie && \ - python setup.py install && \ - cd .. && \ - hg clone https://code.google.com/r/balloob-python-openzwave/ && \ - cd balloob-python-openzwave && \ - ./update.sh && \ - sed -i '253s/.*//' openzwave/cpp/src/value_classes/ValueID.h && \ - ./compile.sh && \ - ./install.sh - -# L18 sed is to apply a patch to make openzwave compile -# L19 2to3 to have the api code work in Python 3 + scripts/build_python_openzwave CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 768b98432b34..59061aa812bb 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -8,19 +8,13 @@ from datetime import timedelta from homeassistant.loader import get_component import homeassistant.util as util -from homeassistant.const import ( - STATE_OPEN) from homeassistant.helpers import ( - platform_devices_from_config) -from homeassistant.components import group, discovery, wink + platform_devices_from_config, generate_entity_id) +from homeassistant.components import discovery, wink, zwave DOMAIN = 'sensor' DEPENDENCIES = [] -GROUP_NAME_ALL_SENSORS = 'all_sensors' -ENTITY_ID_ALL_SENSORS = group.ENTITY_ID_FORMAT.format( - GROUP_NAME_ALL_SENSORS) - ENTITY_ID_FORMAT = DOMAIN + '.{}' MIN_TIME_BETWEEN_SCANS = timedelta(seconds=1) @@ -28,18 +22,12 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=1) # Maps discovered services to their platforms DISCOVERY_PLATFORMS = { wink.DISCOVER_SENSORS: 'wink', + zwave.DISCOVER_SENSORS: 'zwave', } _LOGGER = logging.getLogger(__name__) -def is_on(hass, entity_id=None): - """ Returns if the sensor is open based on the statemachine. """ - entity_id = entity_id or ENTITY_ID_ALL_SENSORS - - return hass.states.is_state(entity_id, STATE_OPEN) - - def setup(hass, config): """ Track states and offer events for sensors. """ logger = logging.getLogger(__name__) @@ -58,10 +46,6 @@ def setup(hass, config): update_sensor_states(None) - # Track all sensors in a group - sensor_group = group.Group( - hass, GROUP_NAME_ALL_SENSORS, sensors.keys(), False) - def sensor_discovered(service, info): """ Called when a sensor is discovered. """ platform = get_component("{}.{}".format( @@ -71,16 +55,13 @@ def setup(hass, config): for sensor in discovered: if sensor is not None and sensor not in sensors.values(): - sensor.entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format(util.slugify(sensor.name)), - sensors.keys()) + sensor.entity_id = generate_entity_id( + ENTITY_ID_FORMAT, sensor.name, sensors.keys()) sensors[sensor.entity_id] = sensor sensor.update_ha_state(hass) - sensor_group.update_tracked_entity_ids(sensors.keys()) - discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), sensor_discovered) # Fire every 3 seconds diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py new file mode 100644 index 000000000000..82ba8d31e399 --- /dev/null +++ b/homeassistant/components/sensor/zwave.py @@ -0,0 +1,154 @@ +import homeassistant.components.zwave as zwave +from homeassistant.helpers import Device +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_BATTERY_LEVEL, ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELCIUS, TEMP_FAHRENHEIT, LIGHT_LUX, ATTR_LOCATION) + + +def devices_discovered(hass, config, info): + """ """ + from louie import connect + from openzwave.network import ZWaveNetwork + + VALUE_CLASS_MAP = { + zwave.VALUE_TEMPERATURE: ZWaveTemperatureSensor, + zwave.VALUE_LUMINANCE: ZWaveLuminanceSensor, + zwave.VALUE_RELATIVE_HUMIDITY: ZWaveRelativeHumiditySensor, + } + + sensors = [] + + for node in zwave.NETWORK.nodes.values(): + for value, klass in VALUE_CLASS_MAP.items(): + if value in node.values: + sensors.append(klass(node)) + + if sensors[-1] is None: + print("") + print("WTF BBQ") + print(node, klass) + print("") + continue + + def value_changed(network, node, value): + """ """ + print("") + print("") + print("") + print("ValueChanged in sensor !!", node, value) + print("") + print("") + print("") + + # triggered when sensors have updated + connect(value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED, weak=False) + + return sensors + + +class ZWaveSensor(Device): + def __init__(self, node, sensor_value): + self._node = node + self._value = node.values[sensor_value] + + @property + def unique_id(self): + """ Returns a unique id. """ + return "ZWAVE-{}-{}".format(self._node.node_id, self._value) + + @property + def name(self): + """ Returns the name of the device. """ + name = self._node.name or "{} {}".format( + self._node.manufacturer_name, self._node.product_name) + + return "{} {}".format(name, self._value.label) + + @property + def state(self): + """ Returns the state of the sensor. """ + return self._value.data + + @property + def state_attributes(self): + """ Returns the state attributes. """ + attrs = { + ATTR_FRIENDLY_NAME: self.name + } + + battery_level = zwave.get_node_value( + self._node, zwave.VALUE_BATTERY_LEVEL) + + if battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = battery_level + + unit = self.unit + + if unit is not None: + attrs[ATTR_UNIT_OF_MEASUREMENT] = unit + + location = self._node.location + + if location: + attrs[ATTR_LOCATION] = location + + attrs.update(self.get_sensor_attributes()) + + return attrs + + @property + def unit(self): + """ Unit if sensor has one. """ + return None + + def get_sensor_attributes(self): + """ Get sensor attributes. """ + return {} + + +class ZWaveTemperatureSensor(ZWaveSensor): + """ Represents a ZWave Temperature Sensor. """ + + def __init__(self, node): + super().__init__(node, zwave.VALUE_TEMPERATURE) + + @property + def state(self): + """ Returns the state of the sensor. """ + return round(self._value.data/1000, 1) + + @property + def unit(self): + """ Unit of this sensor. """ + unit = self._value.units + + if unit == 'C': + return TEMP_CELCIUS + elif unit == 'F': + return TEMP_FAHRENHEIT + else: + return None + + +class ZWaveRelativeHumiditySensor(ZWaveSensor): + """ Represents a ZWave Relative Humidity Sensor. """ + + def __init__(self, node): + super().__init__(node, zwave.VALUE_RELATIVE_HUMIDITY) + + @property + def unit(self): + """ Unit of this sensor. """ + return '%' + + +class ZWaveLuminanceSensor(ZWaveSensor): + """ Represents a ZWave luminance Sensor. """ + + def __init__(self, node): + super().__init__(node, zwave.VALUE_LUMINANCE) + + @property + def unit(self): + """ Unit of this sensor. """ + return LIGHT_LUX diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py new file mode 100644 index 000000000000..3381f6772a83 --- /dev/null +++ b/homeassistant/components/zwave.py @@ -0,0 +1,97 @@ +from homeassistant import bootstrap +from homeassistant.loader import get_component +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED) + +DOMAIN = "zwave" +DEPENDENCIES = [] + +CONF_USB_STICK_PATH = "usb_path" +DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" +CONF_DEBUG = "debug" + +DISCOVER_SENSORS = "zwave.sensors" + +VALUE_SENSOR = 72057594076463104 +VALUE_TEMPERATURE = 72057594076479506 +VALUE_LUMINANCE = 72057594076479538 +VALUE_RELATIVE_HUMIDITY = 72057594076479570 +VALUE_BATTERY_LEVEL = 72057594077773825 + +NETWORK = None + + +def get_node_value(node, key): + """ Helper function to get a node value. """ + return node.values[key].data if key in node.values else None + + +def setup(hass, config): + """ + Setup Z-wave. + Will automatically load components to support devices found on the network. + """ + global NETWORK + + from louie import connect + from openzwave.option import ZWaveOption + from openzwave.network import ZWaveNetwork + + # Setup options + options = ZWaveOption( + config[DOMAIN].get(CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH), + user_path=hass.config_dir) + + if config[DOMAIN].get(CONF_DEBUG) == '1': + options.set_console_output(True) + + options.lock() + + NETWORK = ZWaveNetwork(options, autostart=False) + + def log_all(signal): + print("") + print("LOG ALL") + print(signal) + print("") + print("") + print("") + + connect(log_all, weak=False) + + def zwave_init_done(network): + """ Called when Z-Wave has initialized. """ + init_sensor = False + + # This should be rewritten more efficient when supporting more types + for node in network.nodes.values(): + if get_node_value(node, VALUE_SENSOR) and not init_sensor: + init_sensor = True + + component = get_component('sensor') + + # Ensure component is loaded + if component.DOMAIN not in hass.components: + bootstrap.setup_component(hass, component.DOMAIN, config) + + # Fire discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: DISCOVER_SENSORS, + ATTR_DISCOVERED: {} + }) + + connect( + zwave_init_done, ZWaveNetwork.SIGNAL_NETWORK_READY, weak=False) + + def stop_zwave(event): + """ Stop Z-wave. """ + NETWORK.stop() + + def start_zwave(event): + """ Called when Home Assistant starts up. """ + NETWORK.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zwave) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8b8372aaf615..b0ec0a04c31b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -71,6 +71,13 @@ TEMP_FAHRENHEIT = "°F" # Contains the information that is discovered ATTR_DISCOVERED = "discovered" +# Location of the device/sensor +ATTR_LOCATION = "location" + +ATTR_BATTERY_LEVEL = "battery_level" + +LIGHT_LUX = "LUX" + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" diff --git a/scripts/build_python_openzwave b/scripts/build_python_openzwave new file mode 100755 index 000000000000..a5810eb784df --- /dev/null +++ b/scripts/build_python_openzwave @@ -0,0 +1,28 @@ +# Sets up and builds python open zwave to be used with Home Assistant +# Dependencies that need to be installed: +# apt-get install cython3 libudev-dev python-sphinx python3-setuptools mercurial +# pip3 install cython + +# If current pwd is scripts, go 1 up. +if [ ${PWD##*/} == "scripts" ]; then + cd .. +fi + +cd .. + +# We need to install louie here or else python-openzwave install +# will download louie from PIP and that one is not compatible with Python 3 +git clone https://github.com/balloob/louie.git +cd louie +python setup.py install +cd .. + +hg clone https://code.google.com/r/balloob-python-openzwave/ +cd balloob-python-openzwave +./update.sh + +# Fix an issue with openzwave +sed -i '253s/.*//' openzwave/cpp/src/value_classes/ValueID.h + +./compile.sh +./install.sh diff --git a/scripts/dev_docker b/scripts/dev_docker index 26cdc7beed94..b3672e56095f 100755 --- a/scripts/dev_docker +++ b/scripts/dev_docker @@ -1,3 +1,5 @@ +# Build and run Home Assinstant in Docker + # Optional: pass in a timezone as first argument # If not given will attempt to mount /etc/localtime @@ -8,8 +10,6 @@ fi docker build -t home-assistant-dev . -# TODO set device via command line, remove /bin/bash - if [ $# -gt 0 ] then docker run \ @@ -18,8 +18,7 @@ then -e "TZ=$1" \ -v `pwd`:/usr/src/app \ -v `pwd`/config:/config \ - -t -i home-assistant-dev \ - /bin/bash + -t -i home-assistant-dev else docker run \ diff --git a/scripts/dev_openzwave_docker b/scripts/dev_openzwave_docker new file mode 100755 index 000000000000..9e9d5ba5441c --- /dev/null +++ b/scripts/dev_openzwave_docker @@ -0,0 +1,18 @@ +# Open a docker that can be used to debug/dev python-openzwave +# Pass in a command line argument to skip build + +# If current pwd is scripts, go 1 up. +if [ ${PWD##*/} == "scripts" ]; then + cd .. +fi + +if [ $# -gt 0 ] +then + docker build -t home-assistant-dev . +fi + +docker run \ + --device=/dev/ttyUSB0:/zwaveusbstick:rwm \ + -v `pwd`:/usr/src/app \ + -t -i home-assistant-dev \ + /bin/bash