From fff269e790b1e741e43a360ec6502d166b6b73b2 Mon Sep 17 00:00:00 2001 From: Thomas Delaet Date: Wed, 26 Jul 2017 14:03:29 +0200 Subject: [PATCH] Velbus (#8076) * add Velbus changes * update library version * fix python-velbus version * bug fix and update python-velbus * change config handling * update velbus components/platforms * add support for Velbus switches * fix bugs * typo * add velbus fan * update velbus library * bug fix in logic of fan handling of speed settings * add Velbus changes change config handling update velbus components/platforms add support for Velbus switches add velbus fan * remove duplicate entry * fix documentation links * fix linting error * regen requirements_all.txt * add support for Velbus cover * bugfix in cover component * bugfix in cover component * remove unused imports * Travis fixes * fix style * fix style * Update velbus.py * Update velbus.py * Update velbus.py * Update requirements_all.txt * Update velbus.py * Update velbus.py * Update velbus.py * Update velbus.py * fix style * Update velbus.py * Update velbus.py * Update velbus.py * Update velbus.py * Update velbus.py * Update velbus.py --- .coveragerc | 3 + .../components/binary_sensor/velbus.py | 96 +++++++++ homeassistant/components/cover/velbus.py | 160 +++++++++++++++ homeassistant/components/fan/velbus.py | 187 ++++++++++++++++++ homeassistant/components/light/velbus.py | 104 ++++++++++ homeassistant/components/switch/velbus.py | 111 +++++++++++ homeassistant/components/velbus.py | 43 ++++ requirements_all.txt | 3 + 8 files changed, 707 insertions(+) create mode 100644 homeassistant/components/binary_sensor/velbus.py create mode 100644 homeassistant/components/cover/velbus.py create mode 100644 homeassistant/components/fan/velbus.py create mode 100644 homeassistant/components/light/velbus.py create mode 100644 homeassistant/components/switch/velbus.py create mode 100644 homeassistant/components/velbus.py diff --git a/.coveragerc b/.coveragerc index c9bd40dfc65..c4051af5136 100644 --- a/.coveragerc +++ b/.coveragerc @@ -172,6 +172,9 @@ omit = homeassistant/components/twilio.py homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py + + homeassistant/components/velbus.py + homeassistant/components/*/velbus.py homeassistant/components/velux.py homeassistant/components/*/velux.py diff --git a/homeassistant/components/binary_sensor/velbus.py b/homeassistant/components/binary_sensor/velbus.py new file mode 100644 index 00000000000..214edcf9463 --- /dev/null +++ b/homeassistant/components/binary_sensor/velbus.py @@ -0,0 +1,96 @@ +""" +Support for Velbus Binary Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.velbus/ +""" +import asyncio +import logging + + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_DEVICES +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA +from homeassistant.components.velbus import DOMAIN +import homeassistant.helpers.config_validation as cv + + +DEPENDENCIES = ['velbus'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ + { + vol.Required('module'): cv.positive_int, + vol.Required('channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional('is_pushbutton'): cv.boolean + } + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Velbus binary sensors.""" + velbus = hass.data[DOMAIN] + + add_devices(VelbusBinarySensor(sensor, velbus) + for sensor in config[CONF_DEVICES]) + + +class VelbusBinarySensor(BinarySensorDevice): + """Representation of a Velbus Binary Sensor.""" + + def __init__(self, binary_sensor, velbus): + """Initialize a Velbus light.""" + self._velbus = velbus + self._name = binary_sensor[CONF_NAME] + self._module = binary_sensor['module'] + self._channel = binary_sensor['channel'] + self._is_pushbutton = 'is_pushbutton' in binary_sensor \ + and binary_sensor['is_pushbutton'] + self._state = False + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + yield from self.hass.async_add_job( + self._velbus.subscribe, self._on_message) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.PushButtonStatusMessage): + if message.address == self._module and \ + self._channel in message.get_channels(): + if self._is_pushbutton: + if self._channel in message.closed: + self._toggle() + else: + pass + else: + self._toggle() + + def _toggle(self): + if self._state is True: + self._state = False + else: + self._state = True + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the display name of this sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the sensor is on.""" + return self._state diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/cover/velbus.py new file mode 100644 index 00000000000..ab5d6e8ef79 --- /dev/null +++ b/homeassistant/components/cover/velbus.py @@ -0,0 +1,160 @@ +""" +Support for Velbus covers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.velbus/ +""" +import logging +import asyncio +import time + +import voluptuous as vol + +from homeassistant.components.cover import ( + CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_STOP) +from homeassistant.components.velbus import DOMAIN +from homeassistant.const import (CONF_COVERS, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +COVER_SCHEMA = vol.Schema({ + vol.Required('module'): cv.positive_int, + vol.Required('open_channel'): cv.positive_int, + vol.Required('close_channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), +}) + +DEPENDENCIES = ['velbus'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up cover controlled by Velbus.""" + devices = config.get(CONF_COVERS, {}) + covers = [] + + velbus = hass.data[DOMAIN] + for device_name, device_config in devices.items(): + covers.append( + VelbusCover( + velbus, + device_config.get(CONF_NAME, device_name), + device_config.get('module'), + device_config.get('open_channel'), + device_config.get('close_channel') + ) + ) + + if not covers: + _LOGGER.error("No covers added") + return False + + add_devices(covers) + + +class VelbusCover(CoverDevice): + """Representation a Velbus cover.""" + + def __init__(self, velbus, name, module, open_channel, close_channel): + """Initialize the cover.""" + self._velbus = velbus + self._name = name + self._close_channel_state = None + self._open_channel_state = None + self._module = module + self._open_channel = open_channel + self._close_channel = close_channel + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage): + if message.address == self._module: + if message.channel == self._close_channel: + self._close_channel_state = message.is_on() + self.schedule_update_ha_state() + if message.channel == self._open_channel: + self._open_channel_state = message.is_on() + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._close_channel_state + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown. + """ + return None + + def _relay_off(self, channel): + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def _relay_on(self, channel): + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def open_cover(self, **kwargs): + """Open the cover.""" + self._relay_off(self._close_channel) + time.sleep(0.3) + self._relay_on(self._open_channel) + + def close_cover(self, **kwargs): + """Close the cover.""" + self._relay_off(self._open_channel) + time.sleep(0.3) + self._relay_on(self._close_channel) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self._relay_off(self._open_channel) + time.sleep(0.3) + self._relay_off(self._close_channel) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = [self._open_channel, self._close_channel] + self._velbus.send(message) diff --git a/homeassistant/components/fan/velbus.py b/homeassistant/components/fan/velbus.py new file mode 100644 index 00000000000..c0d125aa5ab --- /dev/null +++ b/homeassistant/components/fan/velbus.py @@ -0,0 +1,187 @@ +""" +Support for Velbus platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/fan.velbus/ +""" +import asyncio +import logging +import voluptuous as vol + +from homeassistant.components.fan import ( + SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, + PLATFORM_SCHEMA) +from homeassistant.components.velbus import DOMAIN +from homeassistant.const import CONF_NAME, CONF_DEVICES, STATE_OFF +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['velbus'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ + { + vol.Required('module'): cv.positive_int, + vol.Required('channel_low'): cv.positive_int, + vol.Required('channel_medium'): cv.positive_int, + vol.Required('channel_high'): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + } + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Fans.""" + velbus = hass.data[DOMAIN] + add_devices(VelbusFan(fan, velbus) for fan in config[CONF_DEVICES]) + + +class VelbusFan(FanEntity): + """Representation of a Velbus Fan.""" + + def __init__(self, fan, velbus): + """Initialize a Velbus light.""" + self._velbus = velbus + self._name = fan[CONF_NAME] + self._module = fan['module'] + self._channel_low = fan['channel_low'] + self._channel_medium = fan['channel_medium'] + self._channel_high = fan['channel_high'] + self._channels = [self._channel_low, self._channel_medium, + self._channel_high] + self._channels_state = [False, False, False] + self._speed = STATE_OFF + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage) and \ + message.address == self._module and \ + message.channel in self._channels: + if message.channel == self._channel_low: + self._channels_state[0] = message.is_on() + elif message.channel == self._channel_medium: + self._channels_state[1] = message.is_on() + elif message.channel == self._channel_high: + self._channels_state[2] = message.is_on() + self._calculate_speed() + self.schedule_update_ha_state() + + def _calculate_speed(self): + if self._is_off(): + self._speed = STATE_OFF + elif self._is_low(): + self._speed = SPEED_LOW + elif self._is_medium(): + self._speed = SPEED_MEDIUM + elif self._is_high(): + self._speed = SPEED_HIGH + + def _is_off(self): + return self._channels_state[0] is False and \ + self._channels_state[1] is False and \ + self._channels_state[2] is False + + def _is_low(self): + return self._channels_state[0] is True and \ + self._channels_state[1] is False and \ + self._channels_state[2] is False + + def _is_medium(self): + return self._channels_state[0] is True and \ + self._channels_state[1] is True and \ + self._channels_state[2] is False + + def _is_high(self): + return self._channels_state[0] is True and \ + self._channels_state[1] is False and \ + self._channels_state[2] is True + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def speed(self): + """Return the current speed.""" + return self._speed + + @property + def speed_list(self): + """Get the list of available speeds.""" + return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + def turn_on(self, speed, **kwargs): + """Turn on the entity.""" + if speed is None: + speed = SPEED_MEDIUM + self.set_speed(speed) + + def turn_off(self): + """Turn off the entity.""" + self.set_speed(STATE_OFF) + + def set_speed(self, speed): + """Set the speed of the fan.""" + channels_off = [] + channels_on = [] + if speed == STATE_OFF: + channels_off = self._channels + elif speed == SPEED_LOW: + channels_off = [self._channel_medium, self._channel_high] + channels_on = [self._channel_low] + elif speed == SPEED_MEDIUM: + channels_off = [self._channel_high] + channels_on = [self._channel_low, self._channel_medium] + elif speed == SPEED_HIGH: + channels_off = [self._channel_medium] + channels_on = [self._channel_low, self._channel_high] + for channel in channels_off: + self._relay_off(channel) + for channel in channels_on: + self._relay_on(channel) + self.schedule_update_ha_state() + + def _relay_on(self, channel): + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def _relay_off(self, channel): + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = self._channels + self._velbus.send(message) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_SET_SPEED diff --git a/homeassistant/components/light/velbus.py b/homeassistant/components/light/velbus.py new file mode 100644 index 00000000000..8a02b36b75f --- /dev/null +++ b/homeassistant/components/light/velbus.py @@ -0,0 +1,104 @@ +""" +Support for Velbus lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.velbus/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_DEVICES +from homeassistant.components.light import Light, PLATFORM_SCHEMA +from homeassistant.components.velbus import DOMAIN +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['velbus'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ + { + vol.Required('module'): cv.positive_int, + vol.Required('channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string + } + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Lights.""" + velbus = hass.data[DOMAIN] + add_devices(VelbusLight(light, velbus) for light in config[CONF_DEVICES]) + + +class VelbusLight(Light): + """Representation of a Velbus Light.""" + + def __init__(self, light, velbus): + """Initialize a Velbus light.""" + self._velbus = velbus + self._name = light[CONF_NAME] + self._module = light['module'] + self._channel = light['channel'] + self._state = False + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage) and \ + message.address == self._module and \ + message.channel == self._channel: + self._state = message.is_on() + self.schedule_update_ha_state() + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def is_on(self): + """Return true if the light is on.""" + return self._state + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = [self._channel] + self._velbus.send(message) diff --git a/homeassistant/components/switch/velbus.py b/homeassistant/components/switch/velbus.py new file mode 100644 index 00000000000..15090091a52 --- /dev/null +++ b/homeassistant/components/switch/velbus.py @@ -0,0 +1,111 @@ +""" +Support for Velbus switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.velbus/ +""" + +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_DEVICES +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.velbus import DOMAIN +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +SWITCH_SCHEMA = { + vol.Required('module'): cv.positive_int, + vol.Required('channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): + vol.All(cv.ensure_list, [SWITCH_SCHEMA]) +}) + +DEPENDENCIES = ['velbus'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Switch.""" + velbus = hass.data[DOMAIN] + devices = [] + + for switch in config[CONF_DEVICES]: + devices.append(VelbusSwitch(switch, velbus)) + add_devices(devices) + return True + + +class VelbusSwitch(SwitchDevice): + """Representation of a switch.""" + + def __init__(self, switch, velbus): + """Initialize a Velbus switch.""" + self._velbus = velbus + self._name = switch[CONF_NAME] + self._module = switch['module'] + self._channel = switch['channel'] + self._state = False + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage) and \ + message.address == self._module and \ + message.channel == self._channel: + self._state = message.is_on() + self.schedule_update_ha_state() + + @property + def name(self): + """Return the display name of this switch.""" + return self._name + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def is_on(self): + """Return true if the switch is on.""" + return self._state + + def turn_on(self, **kwargs): + """Instruct the switch to turn on.""" + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def turn_off(self, **kwargs): + """Instruct the switch to turn off.""" + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = [self._channel] + self._velbus.send(message) diff --git a/homeassistant/components/velbus.py b/homeassistant/components/velbus.py new file mode 100644 index 00000000000..ff2db955d31 --- /dev/null +++ b/homeassistant/components/velbus.py @@ -0,0 +1,43 @@ +""" +Support for Velbus platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/velbus/ +""" +import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT + +REQUIREMENTS = ['python-velbus==2.0.11'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'velbus' + + +VELBUS_MESSAGE = 'velbus.message' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PORT): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Velbus platform.""" + import velbus + port = config[DOMAIN].get(CONF_PORT) + connection = velbus.VelbusUSBConnection(port) + controller = velbus.Controller(connection) + hass.data[DOMAIN] = controller + + def stop_velbus(event): + """Disconnect from serial port.""" + _LOGGER.debug("Shutting down ") + connection.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus) + return True diff --git a/requirements_all.txt b/requirements_all.txt index 9820d0888cd..aad16af8592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -761,6 +761,9 @@ python-telegram-bot==6.1.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 +# homeassistant.components.velbus +python-velbus==2.0.11 + # homeassistant.components.media_player.vlc python-vlc==1.1.2