From dda4f8415073129984a44eaaf123e26c17b4191f Mon Sep 17 00:00:00 2001 From: Flyte Date: Sun, 24 Jan 2016 08:02:14 +0000 Subject: [PATCH] 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