From dda4f8415073129984a44eaaf123e26c17b4191f Mon Sep 17 00:00:00 2001 From: Flyte Date: Sun, 24 Jan 2016 08:02:14 +0000 Subject: [PATCH 1/6] Add zigbee components. --- .coveragerc | 3 + .../components/binary_sensor/zigbee.py | 19 ++ homeassistant/components/light/zigbee.py | 19 ++ homeassistant/components/sensor/zigbee.py | 68 +++++ homeassistant/components/switch/zigbee.py | 19 ++ homeassistant/components/zigbee.py | 272 ++++++++++++++++++ requirements_all.txt | 3 + 7 files changed, 403 insertions(+) create mode 100644 homeassistant/components/binary_sensor/zigbee.py create mode 100644 homeassistant/components/light/zigbee.py create mode 100644 homeassistant/components/sensor/zigbee.py create mode 100644 homeassistant/components/switch/zigbee.py create mode 100644 homeassistant/components/zigbee.py diff --git a/.coveragerc b/.coveragerc index b7a8e3ef1e3..4cf3dbaee49 100644 --- a/.coveragerc +++ b/.coveragerc @@ -35,6 +35,9 @@ omit = homeassistant/components/wink.py homeassistant/components/*/wink.py + homeassistant/components/zigbee.py + homeassistant/components/*/zigbee.py + homeassistant/components/zwave.py homeassistant/components/*/zwave.py diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py new file mode 100644 index 00000000000..8fccb777e9b --- /dev/null +++ b/homeassistant/components/binary_sensor/zigbee.py @@ -0,0 +1,19 @@ +""" +homeassistant.components.binary_sensor.zigbee + +Contains functionality to use a ZigBee device as a binary sensor. +""" +from homeassistant.components.zigbee import ( + ZigBeeDigitalIn, ZigBeeDigitalInConfig) + + +DEPENDENCIES = ["zigbee"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ + Create and add an entity based on the configuration. + """ + add_entities([ + ZigBeeDigitalIn(hass, ZigBeeDigitalInConfig(config)) + ]) diff --git a/homeassistant/components/light/zigbee.py b/homeassistant/components/light/zigbee.py new file mode 100644 index 00000000000..1d846ce89a8 --- /dev/null +++ b/homeassistant/components/light/zigbee.py @@ -0,0 +1,19 @@ +""" +homeassistant.components.light.zigbee + +Contains functionality to use a ZigBee device as a light. +""" +from homeassistant.components.zigbee import ( + ZigBeeDigitalOut, ZigBeeDigitalOutConfig) + + +DEPENDENCIES = ["zigbee"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ + Create and add an entity based on the configuration. + """ + add_entities([ + ZigBeeDigitalOut(hass, ZigBeeDigitalOutConfig(config)) + ]) diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py new file mode 100644 index 00000000000..c69c7efa6fa --- /dev/null +++ b/homeassistant/components/sensor/zigbee.py @@ -0,0 +1,68 @@ +""" +homeassistant.components.sensor.zigbee + +Contains functionality to use a ZigBee device as a sensor. +""" + +import logging + +from homeassistant.core import JobPriority +from homeassistant.const import TEMP_CELCIUS +from homeassistant.helpers.entity import Entity +from homeassistant.components import zigbee + + +DEPENDENCIES = ["zigbee"] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ + Uses the 'type' config value to work out which type of ZigBee sensor we're + dealing with and instantiates the relevant classes to handle it. + """ + typ = config.get("type", "").lower() + if not typ: + _LOGGER.exception( + "Must include 'type' when configuring a ZigBee sensor.") + return + try: + sensor_class, config_class = TYPE_CLASSES[typ] + except KeyError: + _LOGGER.exception("Unknown ZigBee sensor type: %s", typ) + return + add_entities([sensor_class(hass, config_class(config))]) + + +class ZigBeeTemperatureSensor(Entity): + """ + Allows usage of an XBee Pro as a temperature sensor. + """ + def __init__(self, hass, config): + self._config = config + self._temp = None + if config.should_poll: + hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) + + @property + def name(self): + return self._config.name + + @property + def state(self): + return self._temp + + @property + def unit_of_measurement(self): + return TEMP_CELCIUS + + def update(self, *args): + self._temp = zigbee.DEVICE.get_temperature(self._config.address) + self.update_ha_state() + + +# This must be below the ZigBeeTemperatureSensor which it references. +TYPE_CLASSES = { + "temperature": (ZigBeeTemperatureSensor, zigbee.ZigBeeConfig), + "analog": (zigbee.ZigBeeAnalogIn, zigbee.ZigBeeAnalogInConfig) +} diff --git a/homeassistant/components/switch/zigbee.py b/homeassistant/components/switch/zigbee.py new file mode 100644 index 00000000000..7e13593a94e --- /dev/null +++ b/homeassistant/components/switch/zigbee.py @@ -0,0 +1,19 @@ +""" +homeassistant.components.switch.zigbee + +Contains functionality to use a ZigBee device as a switch. +""" +from homeassistant.components.zigbee import ( + ZigBeeDigitalOut, ZigBeeDigitalOutConfig) + + +DEPENDENCIES = ["zigbee"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ + Create and add an entity based on the configuration. + """ + add_entities([ + ZigBeeDigitalOut(hass, ZigBeeDigitalOutConfig(config)) + ]) diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py new file mode 100644 index 00000000000..22565b61b18 --- /dev/null +++ b/homeassistant/components/zigbee.py @@ -0,0 +1,272 @@ +""" +homeassistant.components.zigbee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sets up and provides access to a ZigBee device and contains generic entity +classes. +""" + +from binascii import unhexlify + +import xbee_helper.const as xb_const +from xbee_helper import ZigBee + +from homeassistant.core import JobPriority +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity, ToggleEntity + + +DOMAIN = "zigbee" +REQUIREMENTS = ("xbee-helper==0.0.6",) + +CONF_DEVICE = "device" +CONF_BAUD = "baud" + +DEFAULT_DEVICE = "/dev/ttyUSB0" +DEFAULT_BAUD = 9600 +DEFAULT_ADC_MAX_VOLTS = 1.2 + +DEVICE = None + + +def setup(hass, config): + """ + Set up the connection to the ZigBee device and instantiate the helper + class for it. + """ + global DEVICE + + from serial import Serial + + usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) + baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) + ser = Serial(usb_device, baud) + DEVICE = ZigBee(ser) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port) + return True + + +def close_serial_port(*args): + """ + Close the serial port we're using to communicate with the ZigBee. + """ + DEVICE.zb.serial.close() + + +class ZigBeeConfig(object): + """ + Handles the fetching of configuration from the config file for any ZigBee + entity. + """ + def __init__(self, config): + self._config = config + self._should_poll = config.get("poll", True) + + @property + def name(self): + """ + The name given to the entity. + """ + return self._config["name"] + + @property + def address(self): + """ + If an address has been provided, unhexlify it, otherwise return None + as we're talking to our local ZigBee device. + """ + address = self._config.get("address") + if address is not None: + address = unhexlify(address) + return address + + @property + def should_poll(self): + """ + A bool depicting whether HA should repeatedly poll this device for its + value. + """ + return self._should_poll + + +class ZigBeePinConfig(ZigBeeConfig): + """ + Handles the fetching of configuration from the config file for a ZigBee + GPIO pin. + """ + @property + def pin(self): + """ + The GPIO pin number. + """ + return self._config["pin"] + + +class ZigBeeDigitalPinConfig(ZigBeePinConfig): + """ + Handles the fetching of configuration from the config file for a ZigBee + GPIO pin set to digital in or out. + """ + def __init__(self, config): + super(ZigBeeDigitalPinConfig, self).__init__(config) + self._bool2state, self._state2bool = self.boolean_maps + + @property + def boolean_maps(self): + """ + Create dicts to map booleans to pin high/low and vice versa. Depends on + the config item "on_state" which should be set to "low" or "high". + """ + if self._config.get("on_state", "").lower() == "low": + bool2state = { + True: xb_const.GPIO_DIGITAL_OUTPUT_LOW, + False: xb_const.GPIO_DIGITAL_OUTPUT_HIGH + } + else: + bool2state = { + True: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, + False: xb_const.GPIO_DIGITAL_OUTPUT_LOW + } + state2bool = {v: k for k, v in bool2state.items()} + return bool2state, state2bool + + @property + def bool2state(self): + """ + A dictionary mapping booleans to GPIOSetting objects to translate + on/off as being pin high or low. + """ + return self._bool2state + + @property + def state2bool(self): + """ + A dictionary mapping GPIOSetting objects to booleans to translate + pin high/low as being on or off. + """ + return self._state2bool + +# Create an alias so that ZigBeeDigitalOutConfig has a logical opposite. +ZigBeeDigitalInConfig = ZigBeeDigitalPinConfig + + +class ZigBeeDigitalOutConfig(ZigBeeDigitalPinConfig): + """ + A subclass of ZigBeeDigitalPinConfig which sets _should_poll to default as + False instead of True. The value will still be overridden by the presence + of a 'poll' config entry. + """ + def __init__(self, config): + super(ZigBeeDigitalOutConfig, self).__init__(config) + self._should_poll = config.get("poll", False) + + +class ZigBeeAnalogInConfig(ZigBeePinConfig): + """ + Handles the fetching of configuration from the config file for a ZigBee + GPIO pin set to analog in. + """ + @property + def max_voltage(self): + """ + The voltage at which the ADC will report its highest value. + """ + return float(self._config.get("max_volts", DEFAULT_ADC_MAX_VOLTS)) + + +class ZigBeeDigitalIn(ToggleEntity): + """ + ToggleEntity to represent a GPIO pin configured as a digital input. + """ + def __init__(self, hass, config): + self._config = config + self._state = False + if config.should_poll: + hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) + + @property + def name(self): + return self._config.name + + @property + def should_poll(self): + return self._config.should_poll + + @property + def is_on(self): + return self._state + + def update(self, *args): + """ + Ask the ZigBee device what its output is set to. + """ + pin_state = DEVICE.get_gpio_pin( + self._config.pin, + self._config.address) + self._state = self._config.state2bool[pin_state] + self.update_ha_state() + + +class ZigBeeDigitalOut(ZigBeeDigitalIn): + """ + Adds functionality to ZigBeeDigitalIn to control an output. + """ + def __init__(self, hass, config): + super(ZigBeeDigitalOut, self).__init__(hass, config) + # Get initial value regardless of whether we should_poll. + # If config.should_poll is True, then it's already been handled in + # our parent class __init__(). + if not config.should_poll: + hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) + + def _set_state(self, state): + DEVICE.set_gpio_pin( + self._config.pin, + self._config.bool2state[state], + self._config.address) + self._state = state + self.update_ha_state() + + def turn_on(self, **kwargs): + self._set_state(True) + + def turn_off(self, **kwargs): + self._set_state(False) + + +class ZigBeeAnalogIn(Entity): + """ + Entity to represent a GPIO pin configured as an analog input. + """ + def __init__(self, hass, config): + self._config = config + self._value = None + if config.should_poll: + hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) + + @property + def name(self): + return self._config.name + + @property + def should_poll(self): + return self._config.should_poll + + @property + def state(self): + return self._value + + @property + def unit_of_measurement(self): + return "%" + + def update(self, *args): + """ + Get the latest reading from the ADC. + """ + self._value = DEVICE.read_analog_pin( + self._config.pin, + self._config.max_voltage, + self._config.address, + xb_const.ADC_PERCENTAGE) + self.update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 2241aaecdda..736a887ad68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -213,5 +213,8 @@ radiotherm==1.2 # homeassistant.components.verisure vsure==0.4.5 +# homeassistant.components.zigbee +xbee-helper==0.0.6 + # homeassistant.components.zwave pydispatcher==2.0.5 From 4045fb686255a870916b6e1eb3677da56cca0e05 Mon Sep 17 00:00:00 2001 From: Flyte Date: Thu, 28 Jan 2016 13:44:37 +0000 Subject: [PATCH 2/6] Fix import-before-install of xbee-helper dependency. --- homeassistant/components/zigbee.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 22565b61b18..738ef012f0a 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -8,9 +8,6 @@ classes. from binascii import unhexlify -import xbee_helper.const as xb_const -from xbee_helper import ZigBee - from homeassistant.core import JobPriority from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity, ToggleEntity @@ -26,6 +23,11 @@ DEFAULT_DEVICE = "/dev/ttyUSB0" DEFAULT_BAUD = 9600 DEFAULT_ADC_MAX_VOLTS = 1.2 +# Copied from xbee_helper.const during setup() +GPIO_DIGITAL_OUTPUT_LOW = None +GPIO_DIGITAL_OUTPUT_HIGH = None +ADC_PERCENTAGE = None + DEVICE = None @@ -35,9 +37,18 @@ def setup(hass, config): class for it. """ global DEVICE + global GPIO_DIGITAL_OUTPUT_LOW + global GPIO_DIGITAL_OUTPUT_HIGH + global ADC_PERCENTAGE + import xbee_helper.const as xb_const + from xbee_helper import ZigBee from serial import Serial + GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW + GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH + ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE + usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) ser = Serial(usb_device, baud) @@ -119,13 +130,13 @@ class ZigBeeDigitalPinConfig(ZigBeePinConfig): """ if self._config.get("on_state", "").lower() == "low": bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_LOW, - False: xb_const.GPIO_DIGITAL_OUTPUT_HIGH + True: GPIO_DIGITAL_OUTPUT_LOW, + False: GPIO_DIGITAL_OUTPUT_HIGH } else: bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, - False: xb_const.GPIO_DIGITAL_OUTPUT_LOW + True: GPIO_DIGITAL_OUTPUT_HIGH, + False: GPIO_DIGITAL_OUTPUT_LOW } state2bool = {v: k for k, v in bool2state.items()} return bool2state, state2bool @@ -268,5 +279,5 @@ class ZigBeeAnalogIn(Entity): self._config.pin, self._config.max_voltage, self._config.address, - xb_const.ADC_PERCENTAGE) + ADC_PERCENTAGE) self.update_ha_state() From 72cca0a91adca13881ebd870489270a85c4a64f2 Mon Sep 17 00:00:00 2001 From: Flyte Date: Fri, 29 Jan 2016 10:47:28 +0000 Subject: [PATCH 3/6] ZigBee: Handle case in which Serial port is unable to open --- homeassistant/components/zigbee.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 738ef012f0a..e153201e027 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -6,6 +6,7 @@ Sets up and provides access to a ZigBee device and contains generic entity classes. """ +import logging from binascii import unhexlify from homeassistant.core import JobPriority @@ -30,6 +31,8 @@ ADC_PERCENTAGE = None DEVICE = None +_LOGGER = logging.getLogger(__name__) + def setup(hass, config): """ @@ -43,7 +46,7 @@ def setup(hass, config): import xbee_helper.const as xb_const from xbee_helper import ZigBee - from serial import Serial + from serial import Serial, SerialException GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH @@ -51,7 +54,11 @@ def setup(hass, config): usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) - ser = Serial(usb_device, baud) + try: + ser = Serial(usb_device, baud) + except SerialException as exc: + _LOGGER.exception("Unable to open serial port for ZigBee: %s", exc) + return False DEVICE = ZigBee(ser) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port) return True From 241a7689832c504f18d9ea3bbb71e4f58f4d8fe5 Mon Sep 17 00:00:00 2001 From: Flyte Date: Fri, 29 Jan 2016 11:33:15 +0000 Subject: [PATCH 4/6] ZigBee: Remove extraneous update_ha_state() usage and use it more appropriately for initial state update. --- homeassistant/components/sensor/zigbee.py | 7 ++--- homeassistant/components/zigbee.py | 35 +++++++++++------------ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py index c69c7efa6fa..732e4e97792 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/sensor/zigbee.py @@ -41,8 +41,9 @@ class ZigBeeTemperatureSensor(Entity): def __init__(self, hass, config): self._config = config self._temp = None - if config.should_poll: - hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) + # Get initial state + hass.pool.add_job( + JobPriority.EVENT_STATE, (self.update_ha_state, True)) @property def name(self): @@ -58,8 +59,6 @@ class ZigBeeTemperatureSensor(Entity): def update(self, *args): self._temp = zigbee.DEVICE.get_temperature(self._config.address) - self.update_ha_state() - # This must be below the ZigBeeTemperatureSensor which it references. TYPE_CLASSES = { diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index e153201e027..41a9d02f014 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -10,7 +10,7 @@ import logging from binascii import unhexlify from homeassistant.core import JobPriority -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON, STATE_OFF from homeassistant.helpers.entity import Entity, ToggleEntity @@ -192,15 +192,16 @@ class ZigBeeAnalogInConfig(ZigBeePinConfig): return float(self._config.get("max_volts", DEFAULT_ADC_MAX_VOLTS)) -class ZigBeeDigitalIn(ToggleEntity): +class ZigBeeDigitalIn(Entity): """ ToggleEntity to represent a GPIO pin configured as a digital input. """ def __init__(self, hass, config): self._config = config self._state = False - if config.should_poll: - hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) + # Get initial state + hass.pool.add_job( + JobPriority.EVENT_STATE, (self.update_ha_state, True)) @property def name(self): @@ -210,11 +211,15 @@ class ZigBeeDigitalIn(ToggleEntity): def should_poll(self): return self._config.should_poll + @property + def state(self): + return STATE_ON if self.is_on else STATE_OFF + @property def is_on(self): return self._state - def update(self, *args): + def update(self): """ Ask the ZigBee device what its output is set to. """ @@ -222,28 +227,20 @@ class ZigBeeDigitalIn(ToggleEntity): self._config.pin, self._config.address) self._state = self._config.state2bool[pin_state] - self.update_ha_state() -class ZigBeeDigitalOut(ZigBeeDigitalIn): +class ZigBeeDigitalOut(ZigBeeDigitalIn, ToggleEntity): """ Adds functionality to ZigBeeDigitalIn to control an output. """ - def __init__(self, hass, config): - super(ZigBeeDigitalOut, self).__init__(hass, config) - # Get initial value regardless of whether we should_poll. - # If config.should_poll is True, then it's already been handled in - # our parent class __init__(). - if not config.should_poll: - hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) - def _set_state(self, state): DEVICE.set_gpio_pin( self._config.pin, self._config.bool2state[state], self._config.address) self._state = state - self.update_ha_state() + if not self.should_poll: + self.update_ha_state() def turn_on(self, **kwargs): self._set_state(True) @@ -259,8 +256,9 @@ class ZigBeeAnalogIn(Entity): def __init__(self, hass, config): self._config = config self._value = None - if config.should_poll: - hass.pool.add_job(JobPriority.EVENT_STATE, (self.update, None)) + # Get initial state + hass.pool.add_job( + JobPriority.EVENT_STATE, (self.update_ha_state, True)) @property def name(self): @@ -287,4 +285,3 @@ class ZigBeeAnalogIn(Entity): self._config.max_voltage, self._config.address, ADC_PERCENTAGE) - self.update_ha_state() From c17a4fca8030d550d3192d5ff96c674413a891e0 Mon Sep 17 00:00:00 2001 From: Flyte Date: Fri, 29 Jan 2016 12:00:53 +0000 Subject: [PATCH 5/6] ZigBee: Ensure correct entity types are used for each component. --- .../components/binary_sensor/zigbee.py | 12 +++++++++- homeassistant/components/light/zigbee.py | 12 +++++++++- homeassistant/components/sensor/zigbee.py | 3 ++- homeassistant/components/switch/zigbee.py | 11 ++++++++- homeassistant/components/zigbee.py | 23 +++++++++++-------- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py index 8fccb777e9b..72b2499b190 100644 --- a/homeassistant/components/binary_sensor/zigbee.py +++ b/homeassistant/components/binary_sensor/zigbee.py @@ -3,6 +3,8 @@ homeassistant.components.binary_sensor.zigbee Contains functionality to use a ZigBee device as a binary sensor. """ + +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.zigbee import ( ZigBeeDigitalIn, ZigBeeDigitalInConfig) @@ -15,5 +17,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): Create and add an entity based on the configuration. """ add_entities([ - ZigBeeDigitalIn(hass, ZigBeeDigitalInConfig(config)) + ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config)) ]) + + +class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice): + """ + Use multiple inheritance to turn a ZigBeeDigitalIn into a + BinarySensorDevice. + """ + pass diff --git a/homeassistant/components/light/zigbee.py b/homeassistant/components/light/zigbee.py index 1d846ce89a8..6e1831d79bd 100644 --- a/homeassistant/components/light/zigbee.py +++ b/homeassistant/components/light/zigbee.py @@ -3,6 +3,8 @@ homeassistant.components.light.zigbee Contains functionality to use a ZigBee device as a light. """ + +from homeassistant.components.light import Light from homeassistant.components.zigbee import ( ZigBeeDigitalOut, ZigBeeDigitalOutConfig) @@ -15,5 +17,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): Create and add an entity based on the configuration. """ add_entities([ - ZigBeeDigitalOut(hass, ZigBeeDigitalOutConfig(config)) + ZigBeeLight(hass, ZigBeeDigitalOutConfig(config)) ]) + + +class ZigBeeLight(ZigBeeDigitalOut, Light): + """ + Use multiple inheritance to turn an instance of ZigBeeDigitalOut into a + Light. + """ + pass diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py index 732e4e97792..33d4f72ed76 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/sensor/zigbee.py @@ -60,7 +60,8 @@ class ZigBeeTemperatureSensor(Entity): def update(self, *args): self._temp = zigbee.DEVICE.get_temperature(self._config.address) -# This must be below the ZigBeeTemperatureSensor which it references. + +# This must be below the classes to which it refers. TYPE_CLASSES = { "temperature": (ZigBeeTemperatureSensor, zigbee.ZigBeeConfig), "analog": (zigbee.ZigBeeAnalogIn, zigbee.ZigBeeAnalogInConfig) diff --git a/homeassistant/components/switch/zigbee.py b/homeassistant/components/switch/zigbee.py index 7e13593a94e..3570db8f2ed 100644 --- a/homeassistant/components/switch/zigbee.py +++ b/homeassistant/components/switch/zigbee.py @@ -3,6 +3,8 @@ homeassistant.components.switch.zigbee Contains functionality to use a ZigBee device as a switch. """ + +from homeassistant.components.switch import SwitchDevice from homeassistant.components.zigbee import ( ZigBeeDigitalOut, ZigBeeDigitalOutConfig) @@ -15,5 +17,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): Create and add an entity based on the configuration. """ add_entities([ - ZigBeeDigitalOut(hass, ZigBeeDigitalOutConfig(config)) + ZigBeeSwitch(hass, ZigBeeDigitalOutConfig(config)) ]) + + +class ZigBeeSwitch(ZigBeeDigitalOut, SwitchDevice): + """ + Use multiple inheritance to turn a ZigBeeDigitalOut into a SwitchDevice. + """ + pass diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 41a9d02f014..2d7c87d7e5f 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -10,8 +10,8 @@ import logging from binascii import unhexlify from homeassistant.core import JobPriority -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON, STATE_OFF -from homeassistant.helpers.entity import Entity, ToggleEntity +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity DOMAIN = "zigbee" @@ -194,7 +194,7 @@ class ZigBeeAnalogInConfig(ZigBeePinConfig): class ZigBeeDigitalIn(Entity): """ - ToggleEntity to represent a GPIO pin configured as a digital input. + Represents a GPIO pin configured as a digital input. """ def __init__(self, hass, config): self._config = config @@ -211,12 +211,11 @@ class ZigBeeDigitalIn(Entity): def should_poll(self): return self._config.should_poll - @property - def state(self): - return STATE_ON if self.is_on else STATE_OFF - @property def is_on(self): + """ + Returns True if the Entity is on, else False. + """ return self._state def update(self): @@ -229,7 +228,7 @@ class ZigBeeDigitalIn(Entity): self._state = self._config.state2bool[pin_state] -class ZigBeeDigitalOut(ZigBeeDigitalIn, ToggleEntity): +class ZigBeeDigitalOut(ZigBeeDigitalIn): """ Adds functionality to ZigBeeDigitalIn to control an output. """ @@ -243,15 +242,21 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn, ToggleEntity): self.update_ha_state() def turn_on(self, **kwargs): + """ + Set the digital output to its 'on' state. + """ self._set_state(True) def turn_off(self, **kwargs): + """ + Set the digital output to its 'off' state. + """ self._set_state(False) class ZigBeeAnalogIn(Entity): """ - Entity to represent a GPIO pin configured as an analog input. + Represents a GPIO pin configured as an analog input. """ def __init__(self, hass, config): self._config = config From 902077d78b2c56a32676c22f1bddb35083aea856 Mon Sep 17 00:00:00 2001 From: Flyte Date: Fri, 29 Jan 2016 16:43:01 +0000 Subject: [PATCH 6/6] Catch ZigBee exceptions when communicating with the devices and log appropriate messages. --- homeassistant/components/sensor/zigbee.py | 11 +++- homeassistant/components/zigbee.py | 65 +++++++++++++++++------ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py index 33d4f72ed76..49da890923c 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/sensor/zigbee.py @@ -5,6 +5,7 @@ Contains functionality to use a ZigBee device as a sensor. """ import logging +from binascii import hexlify from homeassistant.core import JobPriority from homeassistant.const import TEMP_CELCIUS @@ -58,7 +59,15 @@ class ZigBeeTemperatureSensor(Entity): return TEMP_CELCIUS def update(self, *args): - self._temp = zigbee.DEVICE.get_temperature(self._config.address) + try: + self._temp = zigbee.DEVICE.get_temperature(self._config.address) + except zigbee.ZIGBEE_TX_FAILURE: + _LOGGER.warning( + "Transmission failure when attempting to get sample from " + "ZigBee device at address: %s", hexlify(self._config.address)) + except zigbee.ZIGBEE_EXCEPTION as exc: + _LOGGER.exception( + "Unable to get sample from ZigBee device: %s", exc) # This must be below the classes to which it refers. diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 2d7c87d7e5f..7c876738859 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -7,7 +7,7 @@ classes. """ import logging -from binascii import unhexlify +from binascii import hexlify, unhexlify from homeassistant.core import JobPriority from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -24,10 +24,12 @@ DEFAULT_DEVICE = "/dev/ttyUSB0" DEFAULT_BAUD = 9600 DEFAULT_ADC_MAX_VOLTS = 1.2 -# Copied from xbee_helper.const during setup() +# Copied from xbee_helper during setup() GPIO_DIGITAL_OUTPUT_LOW = None GPIO_DIGITAL_OUTPUT_HIGH = None ADC_PERCENTAGE = None +ZIGBEE_EXCEPTION = None +ZIGBEE_TX_FAILURE = None DEVICE = None @@ -43,14 +45,19 @@ def setup(hass, config): global GPIO_DIGITAL_OUTPUT_LOW global GPIO_DIGITAL_OUTPUT_HIGH global ADC_PERCENTAGE + global ZIGBEE_EXCEPTION + global ZIGBEE_TX_FAILURE import xbee_helper.const as xb_const from xbee_helper import ZigBee + from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from serial import Serial, SerialException GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE + ZIGBEE_EXCEPTION = ZigBeeException + ZIGBEE_TX_FAILURE = ZigBeeTxFailure usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) @@ -222,9 +229,19 @@ class ZigBeeDigitalIn(Entity): """ Ask the ZigBee device what its output is set to. """ - pin_state = DEVICE.get_gpio_pin( - self._config.pin, - self._config.address) + try: + pin_state = DEVICE.get_gpio_pin( + self._config.pin, + self._config.address) + except ZIGBEE_TX_FAILURE: + _LOGGER.warning( + "Transmission failure when attempting to get sample from " + "ZigBee device at address: %s", hexlify(self._config.address)) + return + except ZIGBEE_EXCEPTION as exc: + _LOGGER.exception( + "Unable to get sample from ZigBee device: %s", exc) + return self._state = self._config.state2bool[pin_state] @@ -233,10 +250,20 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): Adds functionality to ZigBeeDigitalIn to control an output. """ def _set_state(self, state): - DEVICE.set_gpio_pin( - self._config.pin, - self._config.bool2state[state], - self._config.address) + try: + DEVICE.set_gpio_pin( + self._config.pin, + self._config.bool2state[state], + self._config.address) + except ZIGBEE_TX_FAILURE: + _LOGGER.warning( + "Transmission failure when attempting to set output pin on " + "ZigBee device at address: %s", hexlify(self._config.address)) + return + except ZIGBEE_EXCEPTION as exc: + _LOGGER.exception( + "Unable to set digital pin on ZigBee device: %s", exc) + return self._state = state if not self.should_poll: self.update_ha_state() @@ -281,12 +308,20 @@ class ZigBeeAnalogIn(Entity): def unit_of_measurement(self): return "%" - def update(self, *args): + def update(self): """ Get the latest reading from the ADC. """ - self._value = DEVICE.read_analog_pin( - self._config.pin, - self._config.max_voltage, - self._config.address, - ADC_PERCENTAGE) + try: + self._value = DEVICE.read_analog_pin( + self._config.pin, + self._config.max_voltage, + self._config.address, + ADC_PERCENTAGE) + except ZIGBEE_TX_FAILURE: + _LOGGER.warning( + "Transmission failure when attempting to get sample from " + "ZigBee device at address: %s", hexlify(self._config.address)) + except ZIGBEE_EXCEPTION as exc: + _LOGGER.exception( + "Unable to get sample from ZigBee device: %s", exc)