From bf940bd1f39e0587b46c7b69706a8730d4abdde7 Mon Sep 17 00:00:00 2001 From: rubund Date: Sun, 29 May 2016 23:28:03 +0200 Subject: [PATCH] Initial support for EnOcean (#2177) * Initial support for EnOcean Tested to work with: - Eltako FUD61 dimmer - Eltako FT55 battery-less switch - Permundo PSC234 (switch and power monitor) * Rerun gen_requirements_all.py --- .coveragerc | 3 + .../components/binary_sensor/enocean.py | 63 ++++++++++ homeassistant/components/enocean.py | 117 ++++++++++++++++++ homeassistant/components/light/enocean.py | 92 ++++++++++++++ homeassistant/components/sensor/enocean.py | 55 ++++++++ homeassistant/components/switch/enocean.py | 76 ++++++++++++ requirements_all.txt | 3 + 7 files changed, 409 insertions(+) create mode 100644 homeassistant/components/binary_sensor/enocean.py create mode 100644 homeassistant/components/enocean.py create mode 100644 homeassistant/components/light/enocean.py create mode 100644 homeassistant/components/sensor/enocean.py create mode 100644 homeassistant/components/switch/enocean.py diff --git a/.coveragerc b/.coveragerc index 734a5c7b78d..d13ae0d13c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -75,6 +75,9 @@ omit = homeassistant/components/zwave.py homeassistant/components/*/zwave.py + homeassistant/components/enocean.py + homeassistant/components/*/enocean.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/binary_sensor/arest.py diff --git a/homeassistant/components/binary_sensor/enocean.py b/homeassistant/components/binary_sensor/enocean.py new file mode 100644 index 00000000000..12f073f9e85 --- /dev/null +++ b/homeassistant/components/binary_sensor/enocean.py @@ -0,0 +1,63 @@ +""" +Support for EnOcean binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.enocean/ +""" + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components import enocean +from homeassistant.const import CONF_NAME + +DEPENDENCIES = ["enocean"] + +CONF_ID = "id" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Binary Sensor platform fo EnOcean.""" + dev_id = config.get(CONF_ID, None) + devname = config.get(CONF_NAME, "EnOcean binary sensor") + add_devices([EnOceanBinarySensor(dev_id, devname)]) + + +class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): + """Representation of EnOcean binary sensors such as wall switches.""" + + def __init__(self, dev_id, devname): + """Initialize the EnOcean binary sensor.""" + enocean.EnOceanDevice.__init__(self) + self.stype = "listener" + self.dev_id = dev_id + self.which = -1 + self.onoff = -1 + self.devname = devname + + @property + def name(self): + """The default name for the binary sensor.""" + return self.devname + + def value_changed(self, value, value2): + """Fire an event with the data that have changed. + + This method is called when there is an incoming packet associated + with this platform. + """ + self.update_ha_state() + if value2 == 0x70: + self.which = 0 + self.onoff = 0 + elif value2 == 0x50: + self.which = 0 + self.onoff = 1 + elif value2 == 0x30: + self.which = 1 + self.onoff = 0 + elif value2 == 0x10: + self.which = 1 + self.onoff = 1 + self.hass.bus.fire('button_pressed', {"id": self.dev_id, + 'pushed': value, + 'which': self.which, + 'onoff': self.onoff}) diff --git a/homeassistant/components/enocean.py b/homeassistant/components/enocean.py new file mode 100644 index 00000000000..1e70e537c59 --- /dev/null +++ b/homeassistant/components/enocean.py @@ -0,0 +1,117 @@ +""" +EnOcean Component. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/EnOcean/ +""" + +DOMAIN = "enocean" + +REQUIREMENTS = ['enocean==0.31'] + +CONF_DEVICE = "device" + +ENOCEAN_DONGLE = None + + +def setup(hass, config): + """Setup the EnOcean component.""" + global ENOCEAN_DONGLE + + serial_dev = config[DOMAIN].get(CONF_DEVICE, "/dev/ttyUSB0") + + ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev) + return True + + +class EnOceanDongle: + """Representation of an EnOcean dongle.""" + + def __init__(self, hass, ser): + """Initialize the EnOcean dongle.""" + from enocean.communicators.serialcommunicator import SerialCommunicator + self.__communicator = SerialCommunicator(port=ser, + callback=self.callback) + self.__communicator.start() + self.__devices = [] + + def register_device(self, dev): + """Register another device.""" + self.__devices.append(dev) + + def send_command(self, command): + """Send a command from the EnOcean dongle.""" + self.__communicator.send(command) + + def _combine_hex(self, data): # pylint: disable=no-self-use + """Combine list of integer values to one big integer.""" + output = 0x00 + for i, j in enumerate(reversed(data)): + output |= (j << i * 8) + return output + + # pylint: disable=too-many-branches + def callback(self, temp): + """Callback function for EnOcean Device. + + This is the callback function called by + python-enocan whenever there is an incoming + packet. + """ + from enocean.protocol.packet import RadioPacket + if isinstance(temp, RadioPacket): + rxtype = None + value = None + if temp.data[6] == 0x30: + rxtype = "wallswitch" + value = 1 + elif temp.data[6] == 0x20: + rxtype = "wallswitch" + value = 0 + elif temp.data[4] == 0x0c: + rxtype = "power" + value = temp.data[3] + (temp.data[2] << 8) + elif temp.data[2] == 0x60: + rxtype = "switch_status" + if temp.data[3] == 0xe4: + value = 1 + elif temp.data[3] == 0x80: + value = 0 + elif temp.data[0] == 0xa5 and temp.data[1] == 0x02: + rxtype = "dimmerstatus" + value = temp.data[2] + for device in self.__devices: + if rxtype == "wallswitch" and device.stype == "listener": + if temp.sender == self._combine_hex(device.dev_id): + device.value_changed(value, temp.data[1]) + if rxtype == "power" and device.stype == "powersensor": + if temp.sender == self._combine_hex(device.dev_id): + device.value_changed(value) + if rxtype == "power" and device.stype == "switch": + if temp.sender == self._combine_hex(device.dev_id): + if value > 10: + device.value_changed(1) + if rxtype == "switch_status" and device.stype == "switch": + if temp.sender == self._combine_hex(device.dev_id): + device.value_changed(value) + if rxtype == "dimmerstatus" and device.stype == "dimmer": + if temp.sender == self._combine_hex(device.dev_id): + device.value_changed(value) + + +# pylint: disable=too-few-public-methods +class EnOceanDevice(): + """Parent class for all devices associated with the EnOcean component.""" + + def __init__(self): + """Initialize the device.""" + ENOCEAN_DONGLE.register_device(self) + self.stype = "" + self.sensorid = [0x00, 0x00, 0x00, 0x00] + + # pylint: disable=no-self-use + def send_command(self, data, optional, packet_type): + """Send a command via the EnOcean dongle.""" + from enocean.protocol.packet import Packet + packet = Packet(packet_type, data=data, optional=optional) + ENOCEAN_DONGLE.send_command(packet) diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/light/enocean.py new file mode 100644 index 00000000000..adb10a20fda --- /dev/null +++ b/homeassistant/components/light/enocean.py @@ -0,0 +1,92 @@ +""" +Support for EnOcean light sources. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.enocean/ +""" + +import logging +import math + +from homeassistant.components.light import Light, ATTR_BRIGHTNESS +from homeassistant.const import CONF_NAME +from homeassistant.components import enocean + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ["enocean"] + +CONF_ID = "id" +CONF_SENDER_ID = "sender_id" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the EnOcean light platform.""" + sender_id = config.get(CONF_SENDER_ID, None) + devname = config.get(CONF_NAME, "Enocean actuator") + dev_id = config.get(CONF_ID, [0x00, 0x00, 0x00, 0x00]) + + add_devices([EnOceanLight(sender_id, devname, dev_id)]) + + +class EnOceanLight(enocean.EnOceanDevice, Light): + """Representation of an EnOcean light source.""" + + def __init__(self, sender_id, devname, dev_id): + """Initialize the EnOcean light source.""" + enocean.EnOceanDevice.__init__(self) + self._on_state = False + self._brightness = 50 + self._sender_id = sender_id + self.dev_id = dev_id + self._devname = devname + self.stype = "dimmer" + + @property + def name(self): + """Return the name of the device if any.""" + return self._devname + + @property + def brightness(self): + """Brightness of the light. + + This method is optional. Removing it indicates to Home Assistant + that brightness is not supported for this light. + """ + return self._brightness + + @property + def is_on(self): + """If light is on.""" + return self._on_state + + def turn_on(self, **kwargs): + """Turn the light source on or sets a specific dimmer value.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness is not None: + self._brightness = brightness + + bval = math.floor(self._brightness / 256.0 * 100.0) + if bval == 0: + bval = 1 + command = [0xa5, 0x02, bval, 0x01, 0x09] + command.extend(self._sender_id) + command.extend([0x00]) + self.send_command(command, [], 0x01) + self._on_state = True + + def turn_off(self, **kwargs): + """Turn the light source off.""" + command = [0xa5, 0x02, 0x00, 0x01, 0x09] + command.extend(self._sender_id) + command.extend([0x00]) + self.send_command(command, [], 0x01) + self._on_state = False + + def value_changed(self, val): + """Update the internal state of this device in HA.""" + self._brightness = math.floor(val / 100.0 * 256.0) + self._on_state = bool(val != 0) + self.update_ha_state() diff --git a/homeassistant/components/sensor/enocean.py b/homeassistant/components/sensor/enocean.py new file mode 100644 index 00000000000..23a59fb5ece --- /dev/null +++ b/homeassistant/components/sensor/enocean.py @@ -0,0 +1,55 @@ +""" +Support for EnOcean sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.enocean/ +""" + +from homeassistant.const import CONF_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.components import enocean + +DEPENDENCIES = ["enocean"] + +CONF_ID = "id" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup an EnOcean sensor device.""" + dev_id = config.get(CONF_ID, None) + devname = config.get(CONF_NAME, None) + add_devices([EnOceanSensor(dev_id, devname)]) + + +class EnOceanSensor(enocean.EnOceanDevice, Entity): + """Representation of an EnOcean sensor device such as a power meter.""" + + def __init__(self, dev_id, devname): + """Initialize the EnOcean sensor device.""" + enocean.EnOceanDevice.__init__(self) + self.stype = "powersensor" + self.power = None + self.dev_id = dev_id + self.which = -1 + self.onoff = -1 + self.devname = devname + + @property + def name(self): + """Return the name of the device.""" + return 'Power %s' % self.devname + + def value_changed(self, value): + """Update the internal state of the device.""" + self.power = value + self.update_ha_state() + + @property + def state(self): + """Return the state of the device.""" + return self.power + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "W" diff --git a/homeassistant/components/switch/enocean.py b/homeassistant/components/switch/enocean.py new file mode 100644 index 00000000000..f0ae26100c3 --- /dev/null +++ b/homeassistant/components/switch/enocean.py @@ -0,0 +1,76 @@ +""" +Support for EnOcean switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.enocean/ +""" + +import logging + +from homeassistant.const import CONF_NAME +from homeassistant.components import enocean +from homeassistant.helpers.entity import ToggleEntity + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ["enocean"] + +CONF_ID = "id" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the EnOcean switch platform.""" + dev_id = config.get(CONF_ID, None) + devname = config.get(CONF_NAME, "Enocean actuator") + + add_devices([EnOceanSwitch(dev_id, devname)]) + + +class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): + """Representation of an EnOcean switch device.""" + + def __init__(self, dev_id, devname): + """Initialize the EnOcean switch device.""" + enocean.EnOceanDevice.__init__(self) + self.dev_id = dev_id + self._devname = devname + self._light = None + self._on_state = False + self._on_state2 = False + self.stype = "switch" + + @property + def is_on(self): + """Return whether the switch is on or off.""" + return self._on_state + + @property + def name(self): + """Return the device name.""" + return self._devname + + def turn_on(self, **kwargs): + """Turn on the switch.""" + optional = [0x03, ] + optional.extend(self.dev_id) + optional.extend([0xff, 0x00]) + self.send_command(data=[0xD2, 0x01, 0x00, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00], optional=optional, + packet_type=0x01) + self._on_state = True + + def turn_off(self, **kwargs): + """Turn off the switch.""" + optional = [0x03, ] + optional.extend(self.dev_id) + optional.extend([0xff, 0x00]) + self.send_command(data=[0xD2, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00], optional=optional, + packet_type=0x01) + self._on_state = False + + def value_changed(self, val): + """Update the internal state of the switch.""" + self._on_state = val + self.update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index a4b7df30835..f9a479fae9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -55,6 +55,9 @@ dweepy==0.2.0 # homeassistant.components.sensor.eliqonline eliqonline==1.0.12 +# homeassistant.components.enocean +enocean==0.31 + # homeassistant.components.http eventlet==0.19.0