From 8269e843f2a38ea9bf6da904e7a60d559ee29ba6 Mon Sep 17 00:00:00 2001 From: sfam Date: Mon, 23 Nov 2015 00:22:43 +0000 Subject: [PATCH] Initial commit for rollershutter component --- .../components/rollershutter/__init__.py | 116 +++++++++++++++++ .../components/rollershutter/mqtt.py | 119 ++++++++++++++++++ .../components/rollershutter/services.yaml | 0 tests/components/rollershutter/test_mqtt.py | 116 +++++++++++++++++ 4 files changed, 351 insertions(+) create mode 100644 homeassistant/components/rollershutter/__init__.py create mode 100644 homeassistant/components/rollershutter/mqtt.py create mode 100644 homeassistant/components/rollershutter/services.yaml create mode 100644 tests/components/rollershutter/test_mqtt.py diff --git a/homeassistant/components/rollershutter/__init__.py b/homeassistant/components/rollershutter/__init__.py new file mode 100644 index 00000000000..461a34200b2 --- /dev/null +++ b/homeassistant/components/rollershutter/__init__.py @@ -0,0 +1,116 @@ +""" +homeassistant.components.rollershutter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Rollershutter component. + +""" +import os +import logging + +from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity +from homeassistant.components import group +from homeassistant.const import ( + SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_MOVE_STOP, + STATE_OPEN, ATTR_ENTITY_ID) + + +DOMAIN = 'rollershutter' +DEPENDENCIES = [] +SCAN_INTERVAL = 15 + +GROUP_NAME_ALL_ROLLERSHUTTERS = 'all rollershutters' +ENTITY_ID_ALL_ROLLERSHUTTERS = group.ENTITY_ID_FORMAT.format( + 'all_rollershutters') + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = {} + +_LOGGER = logging.getLogger(__name__) + + +def is_open(hass, entity_id=None): + """ Returns if the rollershutter is open based on the statemachine. """ + entity_id = entity_id or ENTITY_ID_ALL_ROLLERSHUTTERS + return hass.states.is_state(entity_id, STATE_OPEN) + + +def move_up(hass, entity_id=None): + """ Moves all or specified rollershutter up. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_MOVE_UP, data) + + +def move_down(hass, entity_id=None): + """ Moves all or specified rollershutter down. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_MOVE_DOWN, data) + + +def move_stop(hass, entity_id=None): + """ Stops all or specified rollershutter. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_MOVE_STOP, data) + + +def setup(hass, config): + """ Track states and offer events for rollershutters. """ + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, + GROUP_NAME_ALL_ROLLERSHUTTERS) + component.setup(config) + + def handle_rollershutter_service(service): + """ Handles calls to the rollershutter services. """ + target_rollershutters = component.extract_from_service(service) + + for rollershutter in target_rollershutters: + if service.service == SERVICE_MOVE_UP: + rollershutter.move_up() + elif service.service == SERVICE_MOVE_DOWN: + rollershutter.move_down() + elif service.service == SERVICE_MOVE_STOP: + rollershutter.move_stop() + + if rollershutter.should_poll: + rollershutter.update_ha_state(True) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + hass.services.register(DOMAIN, SERVICE_MOVE_UP, + handle_rollershutter_service, + descriptions.get(SERVICE_MOVE_UP)) + hass.services.register(DOMAIN, SERVICE_MOVE_DOWN, + handle_rollershutter_service, + descriptions.get(SERVICE_MOVE_DOWN)) + hass.services.register(DOMAIN, SERVICE_MOVE_STOP, + handle_rollershutter_service, + descriptions.get(SERVICE_MOVE_STOP)) + + return True + + +class RollershutterDevice(Entity): + """ Represents a rollershutter within Home Assistant. """ + # pylint: disable=no-self-use + + @property + def state_attributes(self): + """ Returns optional state attributes. """ + return None + + def move_up(self, **kwargs): + """ Moves the device UP. """ + raise NotImplementedError() + + def move_down(self, **kwargs): + """ Moves the device DOWN. """ + raise NotImplementedError() + + def move_stop(self, **kwargs): + """ Moves the device to STOP. """ + raise NotImplementedError() diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py new file mode 100644 index 00000000000..ad5a4346bc9 --- /dev/null +++ b/homeassistant/components/rollershutter/mqtt.py @@ -0,0 +1,119 @@ +""" +homeassistant.components.rollershutter.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a MQTT rollershutter. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rollershutter.mqtt/ +""" +import logging +import homeassistant.components.mqtt as mqtt +from homeassistant.components.rollershutter import RollershutterDevice +from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mqtt'] + +DEFAULT_NAME = "MQTT Shutter" +DEFAULT_QOS = 0 +DEFAULT_PAYLOAD_UP = "UP" +DEFAULT_PAYLOAD_DOWN = "DOWN" +DEFAULT_PAYLOAD_STOP = "STOP" + +ATTR_CURRENT_POSITION = 'current_position' + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add MQTT Roller Shutter """ + + if config.get('command_topic') is None: + _LOGGER.error("Missing required variable: command_topic") + return False + + add_devices_callback([MqttRollershutter( + hass, + config.get('name', DEFAULT_NAME), + config.get('state_topic'), + config.get('command_topic'), + config.get('qos', DEFAULT_QOS), + config.get('payload_up', DEFAULT_PAYLOAD_UP), + config.get('payload_down', DEFAULT_PAYLOAD_DOWN), + config.get('payload_stop', DEFAULT_PAYLOAD_STOP), + config.get('state_format'))]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class MqttRollershutter(RollershutterDevice): + """ Represents a rollershutter that can be togggled using MQTT """ + def __init__(self, hass, name, state_topic, command_topic, qos, + payload_up, payload_down, payload_stop, state_format): + self._state = -1 + self._hass = hass + self._name = name + self._state_topic = state_topic + self._command_topic = command_topic + self._qos = qos + self._payload_up = payload_up + self._payload_down = payload_down + self._payload_stop = payload_stop + self._parse = mqtt.FmtParser(state_format) + + if self._state_topic: + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + value = self._parse(payload) + if value.isnumeric(): + if 0 <= int(value) <= 100: + self._state = int(value) + self.update_ha_state() + + mqtt.subscribe(hass, self._state_topic, message_received, + self._qos) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the rollershutter """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if self._state == -1: + return STATE_UNKNOWN + elif self._state == 0: + return STATE_CLOSED + else: + return STATE_OPEN + + @property + def is_open(self): + """ True if device is open. """ + return self.state == STATE_OPEN + + def move_up(self, **kwargs): + """ Moves the device UP. """ + mqtt.publish(self.hass, self._command_topic, self._payload_up, + self._qos) + + def move_down(self, **kwargs): + """ Moves the device DOWN. """ + mqtt.publish(self.hass, self._command_topic, self._payload_down, + self._qos) + + def move_stop(self, **kwargs): + """ Moves the device to STOP. """ + mqtt.publish(self.hass, self._command_topic, self._payload_stop, + self._qos) + + @property + def state_attributes(self): + """ Return the state attributes. """ + state_attr = { + ATTR_CURRENT_POSITION: self._state, + } + return state_attr diff --git a/homeassistant/components/rollershutter/services.yaml b/homeassistant/components/rollershutter/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/rollershutter/test_mqtt.py b/tests/components/rollershutter/test_mqtt.py new file mode 100644 index 00000000000..099d8fad1ea --- /dev/null +++ b/tests/components/rollershutter/test_mqtt.py @@ -0,0 +1,116 @@ +""" +tests.components.rollershutter.test_mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests mqtt rollershutter. +""" +import unittest + +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN +import homeassistant.core as ha +import homeassistant.components.rollershutter as rollershutter +from tests.common import mock_mqtt_component, fire_mqtt_message + + +class TestRollershutterMQTT(unittest.TestCase): + """ Test the MQTT rollershutter. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_controlling_state_via_topic(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_up': 'UP', + 'payload_down': 'DOWN', + 'payload_stop': 'STOP' + } + })) + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '0') + self.hass.pool.block_till_done() + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_CLOSED, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '50') + self.hass.pool.block_till_done() + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_OPEN, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '100') + self.hass.pool.block_till_done() + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_OPEN, state.state) + + def test_sending_mqtt_commands(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 2 + } + })) + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + rollershutter.move_up(self.hass, 'rollershutter.test') + self.hass.pool.block_till_done() + + self.assertEqual(('command-topic', 'UP', 2), + self.mock_publish.mock_calls[-1][1]) + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + def test_state_attributes_current_position(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_up': 'UP', + 'payload_down': 'DOWN', + 'payload_stop': 'STOP' + } + })) + + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(-1, current_position) + + fire_mqtt_message(self.hass, 'state-topic', '0') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(0, current_position) + + fire_mqtt_message(self.hass, 'state-topic', '50') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(50, current_position) + + fire_mqtt_message(self.hass, 'state-topic', '101') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(50, current_position)