Add support for a wider variety of EnOcean devices (#22052)

* Implement EnOcean temperature and humidity sensors.

* Bump EnOcean version to 0.50
* Refactor components for more generic device handling
* Move radio packet data interpretation to specific devices

* Update CODEOWNERS

* Implement code review changes
This commit is contained in:
Beat 2019-04-25 00:30:46 +02:00 committed by Martin Hjelmare
parent fef1dc8c54
commit 96735e41af
10 changed files with 305 additions and 153 deletions

View File

@ -66,6 +66,7 @@ homeassistant/components/egardia/* @jeroenterheerdt
homeassistant/components/eight_sleep/* @mezz64
homeassistant/components/emby/* @mezz64
homeassistant/components/enigma2/* @fbradyirl
homeassistant/components/enocean/* @bdurrer
homeassistant/components/ephember/* @ttroy50
homeassistant/components/epsonworkforce/* @ThaStealth
homeassistant/components/eq3btsmart/* @rytilahti

View File

@ -4,13 +4,13 @@ import logging
import voluptuous as vol
from homeassistant.const import CONF_DEVICE
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'enocean'
ENOCEAN_DONGLE = None
DATA_ENOCEAN = 'enocean'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -18,14 +18,15 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
SIGNAL_RECEIVE_MESSAGE = 'enocean.receive_message'
SIGNAL_SEND_MESSAGE = 'enocean.send_message'
def setup(hass, config):
"""Set up the EnOcean component."""
global ENOCEAN_DONGLE
serial_dev = config[DOMAIN].get(CONF_DEVICE)
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
dongle = EnOceanDongle(hass, serial_dev)
hass.data[DATA_ENOCEAN] = dongle
return True
@ -39,87 +40,53 @@ class EnOceanDongle:
self.__communicator = SerialCommunicator(
port=ser, callback=self.callback)
self.__communicator.start()
self.__devices = []
self.hass = hass
self.hass.helpers.dispatcher.dispatcher_connect(
SIGNAL_SEND_MESSAGE, self._send_message_callback)
def register_device(self, dev):
"""Register another device."""
self.__devices.append(dev)
def send_command(self, command):
"""Send a command from the EnOcean dongle."""
def _send_message_callback(self, command):
"""Send a command through the EnOcean dongle."""
self.__communicator.send(command)
# pylint: disable=no-self-use
def _combine_hex(self, data):
"""Combine list of integer values to one big integer."""
output = 0x00
for i, j in enumerate(reversed(data)):
output |= (j << i * 8)
return output
def callback(self, temp):
def callback(self, packet):
"""Handle EnOcean device's callback.
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):
_LOGGER.debug("Received radio packet: %s", temp)
rxtype = None
value = None
channel = 0
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 == 0x60:
rxtype = "switch_status"
channel = temp.data[2] & 0x1F
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_int == self._combine_hex(device.dev_id):
device.value_changed(value, temp.data[1])
if rxtype == "power" and device.stype == "powersensor":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "power" and device.stype == "switch":
if temp.sender_int == self._combine_hex(device.dev_id):
if value > 10:
device.value_changed(1)
if rxtype == "switch_status" and device.stype == "switch" and \
channel == device.channel:
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "dimmerstatus" and device.stype == "dimmer":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if isinstance(packet, RadioPacket):
_LOGGER.debug("Received radio packet: %s", packet)
self.hass.helpers.dispatcher.dispatcher_send(
SIGNAL_RECEIVE_MESSAGE, packet)
class EnOceanDevice():
class EnOceanDevice(Entity):
"""Parent class for all devices associated with the EnOcean component."""
def __init__(self):
def __init__(self, dev_id, dev_name="EnOcean device"):
"""Initialize the device."""
ENOCEAN_DONGLE.register_device(self)
self.stype = ""
self.sensorid = [0x00, 0x00, 0x00, 0x00]
self.dev_id = dev_id
self.dev_name = dev_name
async def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RECEIVE_MESSAGE, self._message_received_callback)
def _message_received_callback(self, packet):
"""Handle incoming packets."""
from enocean.utils import combine_hex
if packet.sender_int == combine_hex(self.dev_id):
self.value_changed(packet)
def value_changed(self, packet):
"""Update the internal state of the device when a packet arrives."""
# 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)
self.hass.helpers.dispatcher.dispatcher_send(
SIGNAL_SEND_MESSAGE, packet)

View File

@ -3,16 +3,17 @@ import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
from homeassistant.components import enocean
from homeassistant.const import (
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS)
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'EnOcean binary sensor'
DEPENDENCIES = ['enocean']
EVENT_BUTTON_PRESSED = 'button_pressed'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
@ -24,61 +25,80 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Binary Sensor platform for EnOcean."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
dev_name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS)
add_entities([EnOceanBinarySensor(dev_id, devname, device_class)])
add_entities([EnOceanBinarySensor(dev_id, dev_name, device_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
"""Representation of EnOcean binary sensors such as wall switches."""
"""Representation of EnOcean binary sensors such as wall switches.
def __init__(self, dev_id, devname, device_class):
Supported EEPs (EnOcean Equipment Profiles):
- F6-02-01 (Light and Blind Control - Application Style 2)
- F6-02-02 (Light and Blind Control - Application Style 1)
"""
def __init__(self, dev_id, dev_name, device_class):
"""Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self)
self.stype = 'listener'
self.dev_id = dev_id
super().__init__(dev_id, dev_name)
self._device_class = device_class
self.which = -1
self.onoff = -1
self.devname = devname
self._device_class = device_class
@property
def name(self):
"""Return the default name for the binary sensor."""
return self.devname
return self.dev_name
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
def value_changed(self, value, value2):
def value_changed(self, packet):
"""Fire an event with the data that have changed.
This method is called when there is an incoming packet associated
with this platform.
Example packet data:
- 2nd button pressed
['0xf6', '0x10', '0x00', '0x2d', '0xcf', '0x45', '0x30']
- button released
['0xf6', '0x00', '0x00', '0x2d', '0xcf', '0x45', '0x20']
"""
# Energy Bow
pushed = None
if packet.data[6] == 0x30:
pushed = 1
elif packet.data[6] == 0x20:
pushed = 0
self.schedule_update_ha_state()
if value2 == 0x70:
action = packet.data[1]
if action == 0x70:
self.which = 0
self.onoff = 0
elif value2 == 0x50:
elif action == 0x50:
self.which = 0
self.onoff = 1
elif value2 == 0x30:
elif action == 0x30:
self.which = 1
self.onoff = 0
elif value2 == 0x10:
elif action == 0x10:
self.which = 1
self.onoff = 1
elif value2 == 0x37:
elif action == 0x37:
self.which = 10
self.onoff = 0
elif value2 == 0x15:
elif action == 0x15:
self.which = 10
self.onoff = 1
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
'pushed': value,
'which': self.which,
'onoff': self.onoff})
self.hass.bus.fire(EVENT_BUTTON_PRESSED,
{'id': self.dev_id,
'pushed': pushed,
'which': self.which,
'onoff': self.onoff})

View File

@ -4,10 +4,10 @@ import math
import voluptuous as vol
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_ID)
from homeassistant.components import enocean
from homeassistant.components.light import (
ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@ -28,29 +28,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the EnOcean light platform."""
sender_id = config.get(CONF_SENDER_ID)
devname = config.get(CONF_NAME)
dev_name = config.get(CONF_NAME)
dev_id = config.get(CONF_ID)
add_entities([EnOceanLight(sender_id, devname, dev_id)])
add_entities([EnOceanLight(sender_id, dev_id, dev_name)])
class EnOceanLight(enocean.EnOceanDevice, Light):
"""Representation of an EnOcean light source."""
def __init__(self, sender_id, devname, dev_id):
def __init__(self, sender_id, dev_id, dev_name):
"""Initialize the EnOcean light source."""
enocean.EnOceanDevice.__init__(self)
super().__init__(dev_id, dev_name)
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
return self.dev_name
@property
def brightness(self):
@ -94,8 +91,14 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
self.send_command(command, [], 0x01)
self._on_state = False
def value_changed(self, val):
"""Update the internal state of this device."""
self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0)
self.schedule_update_ha_state()
def value_changed(self, packet):
"""Update the internal state of this device.
Dimmer devices like Eltako FUD61 send telegram in different RORGs.
We only care about the 4BS (0xA5).
"""
if packet.data[0] == 0xa5 and packet.data[1] == 0x02:
val = packet.data[2]
self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0)
self.schedule_update_ha_state()

View File

@ -3,8 +3,8 @@
"name": "Enocean",
"documentation": "https://www.home-assistant.io/components/enocean",
"requirements": [
"enocean==0.40"
"enocean==0.50"
],
"dependencies": [],
"codeowners": []
"codeowners": ["@bdurrer"]
}

View File

@ -3,58 +3,201 @@ import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, CONF_ID, POWER_WATT)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components import enocean
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, POWER_WATT)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_MAX_TEMP = 'max_temp'
CONF_MIN_TEMP = 'min_temp'
CONF_RANGE_FROM = 'range_from'
CONF_RANGE_TO = 'range_to'
DEFAULT_NAME = 'EnOcean sensor'
DEVICE_CLASS_POWER = 'powersensor'
SENSOR_TYPES = {
DEVICE_CLASS_HUMIDITY: {
'name': 'Humidity',
'unit': '%',
'icon': 'mdi:water-percent',
'class': DEVICE_CLASS_HUMIDITY,
},
DEVICE_CLASS_POWER: {
'name': 'Power',
'unit': POWER_WATT,
'icon': 'mdi:power-plug',
'class': DEVICE_CLASS_POWER,
},
DEVICE_CLASS_TEMPERATURE: {
'name': 'Temperature',
'unit': TEMP_CELSIUS,
'icon': 'mdi:thermometer',
'class': DEVICE_CLASS_TEMPERATURE,
},
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string,
vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int),
vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int),
vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int,
vol.Optional(CONF_RANGE_TO, default=0): cv.positive_int,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an EnOcean sensor device."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
dev_name = config.get(CONF_NAME)
dev_class = config.get(CONF_DEVICE_CLASS)
add_entities([EnOceanSensor(dev_id, devname)])
if dev_class == DEVICE_CLASS_TEMPERATURE:
temp_min = config.get(CONF_MIN_TEMP)
temp_max = config.get(CONF_MAX_TEMP)
range_from = config.get(CONF_RANGE_FROM)
range_to = config.get(CONF_RANGE_TO)
add_entities([EnOceanTemperatureSensor(
dev_id, dev_name, temp_min, temp_max, range_from, range_to)])
elif dev_class == DEVICE_CLASS_HUMIDITY:
add_entities([EnOceanHumiditySensor(dev_id, dev_name)])
elif dev_class == DEVICE_CLASS_POWER:
add_entities([EnOceanPowerSensor(dev_id, dev_name)])
class EnOceanSensor(enocean.EnOceanDevice, Entity):
"""Representation of an EnOcean sensor device such as a power meter."""
class EnOceanSensor(enocean.EnOceanDevice):
"""Representation of an EnOcean sensor device such as a power meter."""
def __init__(self, dev_id, devname):
def __init__(self, dev_id, dev_name, sensor_type):
"""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
super().__init__(dev_id, dev_name)
self._sensor_type = sensor_type
self._device_class = SENSOR_TYPES[self._sensor_type]['class']
self._dev_name = '{} {}'.format(
SENSOR_TYPES[self._sensor_type]['name'], dev_name)
self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit']
self._icon = SENSOR_TYPES[self._sensor_type]['icon']
self._state = None
@property
def name(self):
"""Return the name of the device."""
return 'Power %s' % self.devname
return self._dev_name
def value_changed(self, value):
"""Update the internal state of the device."""
self.power = value
self.schedule_update_ha_state()
@property
def icon(self):
"""Icon to use in the frontend."""
return self._icon
@property
def device_class(self):
"""Return the device class of the sensor."""
return self._device_class
@property
def state(self):
"""Return the state of the device."""
return self.power
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return POWER_WATT
return self._unit_of_measurement
def value_changed(self, packet):
"""Update the internal state of the sensor."""
class EnOceanPowerSensor(EnOceanSensor):
"""Representation of an EnOcean power sensor.
EEPs (EnOcean Equipment Profiles):
- A5-12-01 (Automated Meter Reading, Electricity)
"""
def __init__(self, dev_id, dev_name):
"""Initialize the EnOcean power sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_POWER)
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.rorg != 0xA5:
return
packet.parse_eep(0x12, 0x01)
if packet.parsed['DT']['raw_value'] == 1:
# this packet reports the current value
raw_val = packet.parsed['MR']['raw_value']
divisor = packet.parsed['DIV']['raw_value']
self._state = raw_val / (10 ** divisor)
self.schedule_update_ha_state()
class EnOceanTemperatureSensor(EnOceanSensor):
"""Representation of an EnOcean temperature sensor device.
EEPs (EnOcean Equipment Profiles):
- A5-02-01 to A5-02-1B All 8 Bit Temperature Sensors of A5-02
- A5-10-01 to A5-10-14 (Room Operating Panels)
- A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%)
- A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%)
- A5-10-10 (Temp. and Humidity Sensor and Set Point)
- A5-10-12 (Temp. and Humidity Sensor, Set Point and Occupancy Control)
- 10 Bit Temp. Sensors are not supported (A5-02-20, A5-02-30)
For the following EEPs the scales must be set to "0 to 250":
- A5-04-01
- A5-04-02
- A5-10-10 to A5-10-14
"""
def __init__(self, dev_id, dev_name, scale_min, scale_max,
range_from, range_to):
"""Initialize the EnOcean temperature sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE)
self._scale_min = scale_min
self._scale_max = scale_max
self.range_from = range_from
self.range_to = range_to
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.data[0] != 0xa5:
return
temp_scale = self._scale_max - self._scale_min
temp_range = self.range_to - self.range_from
raw_val = packet.data[3]
temperature = temp_scale / temp_range * (raw_val - self.range_from)
temperature += self._scale_min
self._state = round(temperature, 1)
self.schedule_update_ha_state()
class EnOceanHumiditySensor(EnOceanSensor):
"""Representation of an EnOcean humidity sensor device.
EEPs (EnOcean Equipment Profiles):
- A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%)
- A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%)
- A5-10-10 to A5-10-14 (Room Operating Panels)
"""
def __init__(self, dev_id, dev_name):
"""Initialize the EnOcean humidity sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_HUMIDITY)
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.rorg != 0xA5:
return
humidity = packet.data[2] * 100 / 250
self._state = round(humidity, 1)
self.schedule_update_ha_state()

View File

@ -3,16 +3,16 @@ import logging
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, CONF_ID)
from homeassistant.components import enocean
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'EnOcean Switch'
CONF_CHANNEL = 'channel'
DEFAULT_NAME = 'EnOcean Switch'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
@ -23,26 +23,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the EnOcean switch platform."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
channel = config.get(CONF_CHANNEL)
dev_id = config.get(CONF_ID)
dev_name = config.get(CONF_NAME)
add_entities([EnOceanSwitch(dev_id, devname, channel)])
add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
"""Representation of an EnOcean switch device."""
def __init__(self, dev_id, devname, channel):
def __init__(self, dev_id, dev_name, channel):
"""Initialize the EnOcean switch device."""
enocean.EnOceanDevice.__init__(self)
self.dev_id = dev_id
self._devname = devname
super().__init__(dev_id, dev_name)
self._light = None
self._on_state = False
self._on_state2 = False
self.channel = channel
self.stype = "switch"
@property
def is_on(self):
@ -52,7 +49,7 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
@property
def name(self):
"""Return the device name."""
return self._devname
return self.dev_name
def turn_on(self, **kwargs):
"""Turn on the switch."""
@ -74,7 +71,24 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
packet_type=0x01)
self._on_state = False
def value_changed(self, val):
def value_changed(self, packet):
"""Update the internal state of the switch."""
self._on_state = val
self.schedule_update_ha_state()
if packet.data[0] == 0xa5:
# power meter telegram, turn on if > 10 watts
packet.parse_eep(0x12, 0x01)
if packet.parsed['DT']['raw_value'] == 1:
raw_val = packet.parsed['MR']['raw_value']
divisor = packet.parsed['DIV']['raw_value']
watts = raw_val / (10 ** divisor)
if watts > 1:
self._on_state = True
self.schedule_update_ha_state()
elif packet.data[0] == 0xd2:
# actuator status telegram
packet.parse_eep(0x01, 0x01)
if packet.parsed['CMD']['raw_value'] == 4:
channel = packet.parsed['IO']['raw_value']
output = packet.parsed['OV']['raw_value']
if channel == self.channel:
self._on_state = output > 0
self.schedule_update_ha_state()

View File

@ -383,7 +383,7 @@ elkm1-lib==0.7.13
emulated_roku==0.1.8
# homeassistant.components.enocean
enocean==0.40
enocean==0.50
# homeassistant.components.entur_public_transport
enturclient==0.2.0

View File

@ -90,6 +90,9 @@ eebrightbox==0.0.4
# homeassistant.components.emulated_roku
emulated_roku==0.1.8
# homeassistant.components.enocean
enocean==0.50
# homeassistant.components.season
ephem==3.7.6.0

View File

@ -58,6 +58,7 @@ TEST_REQUIREMENTS = (
'dsmr_parser',
'eebrightbox',
'emulated_roku',
'enocean',
'ephem',
'evohomeclient',
'feedparser-homeassistant',