From 8269e843f2a38ea9bf6da904e7a60d559ee29ba6 Mon Sep 17 00:00:00 2001 From: sfam Date: Mon, 23 Nov 2015 00:22:43 +0000 Subject: [PATCH 1/8] 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) From d4b3af327d0dfab571df7c411e26f95a4612fa2f Mon Sep 17 00:00:00 2001 From: sfam Date: Mon, 23 Nov 2015 00:25:10 +0000 Subject: [PATCH 2/8] Initial commit for rollershutter component --- homeassistant/const.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5b0b5a5e214..047f66ef924 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -53,8 +53,6 @@ STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' STATE_ALARM_PENDING = 'pending' STATE_ALARM_TRIGGERED = 'triggered' -STATE_LOCKED = 'locked' -STATE_UNLOCKED = 'unlocked' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -98,9 +96,6 @@ ATTR_BATTERY_LEVEL = "battery_level" # For devices which support an armed state ATTR_ARMED = "device_armed" -# For devices which support a locked state -ATTR_LOCKED = "locked" - # For sensors that support 'tripping', eg. motion and door sensors ATTR_TRIPPED = "device_tripped" @@ -140,8 +135,9 @@ SERVICE_ALARM_ARM_HOME = "alarm_arm_home" SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" SERVICE_ALARM_TRIGGER = "alarm_trigger" -SERVICE_LOCK = "lock" -SERVICE_UNLOCK = "unlock" +SERVICE_MOVE_UP = 'move_up' +SERVICE_MOVE_DOWN = 'move_down' +SERVICE_MOVE_STOP = 'move_stop' # #### API / REMOTE #### SERVER_PORT = 8123 From 68ff9dd74f61fce99adfeaba3dc4519bbc0dc997 Mon Sep 17 00:00:00 2001 From: sfam Date: Mon, 23 Nov 2015 00:35:22 +0000 Subject: [PATCH 3/8] rollershutter component - fix const --- homeassistant/const.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index 047f66ef924..add1848b72c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -53,6 +53,8 @@ STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' STATE_ALARM_PENDING = 'pending' STATE_ALARM_TRIGGERED = 'triggered' +STATE_LOCKED = 'locked' +STATE_UNLOCKED = 'unlocked' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -96,6 +98,9 @@ ATTR_BATTERY_LEVEL = "battery_level" # For devices which support an armed state ATTR_ARMED = "device_armed" +# For devices which support a locked state +ATTR_LOCKED = "locked" + # For sensors that support 'tripping', eg. motion and door sensors ATTR_TRIPPED = "device_tripped" @@ -135,6 +140,9 @@ SERVICE_ALARM_ARM_HOME = "alarm_arm_home" SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" SERVICE_ALARM_TRIGGER = "alarm_trigger" +SERVICE_LOCK = "lock" +SERVICE_UNLOCK = "unlock" + SERVICE_MOVE_UP = 'move_up' SERVICE_MOVE_DOWN = 'move_down' SERVICE_MOVE_STOP = 'move_stop' From e001ea913a6132571a1b79cad50e07ac6d288190 Mon Sep 17 00:00:00 2001 From: sfam Date: Tue, 24 Nov 2015 08:03:02 +0000 Subject: [PATCH 4/8] add __init__.py to test folder --- tests/components/alarm_control_panel/__init__.py | 0 tests/components/rollershutter/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/components/alarm_control_panel/__init__.py create mode 100644 tests/components/rollershutter/__init__.py diff --git a/tests/components/alarm_control_panel/__init__.py b/tests/components/alarm_control_panel/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/rollershutter/__init__.py b/tests/components/rollershutter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 351430c1b38415ec790701b7c303d2a8ad1d3a04 Mon Sep 17 00:00:00 2001 From: sfam Date: Tue, 24 Nov 2015 10:41:39 +0000 Subject: [PATCH 5/8] move current_position to RollershutterDevice class --- .../components/rollershutter/__init__.py | 17 ++++++- .../components/rollershutter/mqtt.py | 46 +++++++++---------- tests/components/rollershutter/test_mqtt.py | 12 +++-- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/rollershutter/__init__.py b/homeassistant/components/rollershutter/__init__.py index 461a34200b2..5ebcf8a3d21 100644 --- a/homeassistant/components/rollershutter/__init__.py +++ b/homeassistant/components/rollershutter/__init__.py @@ -13,7 +13,7 @@ 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) + STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID) DOMAIN = 'rollershutter' @@ -98,6 +98,21 @@ class RollershutterDevice(Entity): """ Represents a rollershutter within Home Assistant. """ # pylint: disable=no-self-use + @property + def current_position(self): + """ Return current position of rollershutter. + None is unknown, 0 is closed, 100 is fully open. """ + raise NotImplementedError() + + @property + def state(self): + current = self.current_position + + if current is None: + return STATE_UNKNOWN + + return STATE_CLOSED if current == 0 else STATE_OPEN + @property def state_attributes(self): """ Returns optional state attributes. """ diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py index ad5a4346bc9..6128a4d9819 100644 --- a/homeassistant/components/rollershutter/mqtt.py +++ b/homeassistant/components/rollershutter/mqtt.py @@ -8,7 +8,6 @@ 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'] @@ -47,7 +46,7 @@ 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._state = None self._hass = hass self._name = name self._state_topic = state_topic @@ -58,17 +57,20 @@ class MqttRollershutter(RollershutterDevice): 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() + if self._state_topic is None: + return - mqtt.subscribe(hass, self._state_topic, message_received, - self._qos) + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + value = self._parse(payload) + if value.isnumeric() and 0 <= int(value) <= 100: + self._state = int(value) + self.update_ha_state() + else: + _LOGGER.warning( + "Payload is expected to be an integer between 0 and 100") + + mqtt.subscribe(hass, self._state_topic, message_received, self._qos) @property def should_poll(self): @@ -81,19 +83,15 @@ class MqttRollershutter(RollershutterDevice): 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 + def current_position(self): + """ Return current position of rollershutter. + None is unknown, 0 is closed, 100 is fully open. """ + return self._state @property def is_open(self): """ True if device is open. """ - return self.state == STATE_OPEN + return self._state > 0 def move_up(self, **kwargs): """ Moves the device UP. """ @@ -113,7 +111,7 @@ class MqttRollershutter(RollershutterDevice): @property def state_attributes(self): """ Return the state attributes. """ - state_attr = { - ATTR_CURRENT_POSITION: self._state, - } + state_attr = {} + if self._state is not None: + state_attr[ATTR_CURRENT_POSITION] = self._state return state_attr diff --git a/tests/components/rollershutter/test_mqtt.py b/tests/components/rollershutter/test_mqtt.py index 099d8fad1ea..2a71163c50f 100644 --- a/tests/components/rollershutter/test_mqtt.py +++ b/tests/components/rollershutter/test_mqtt.py @@ -93,9 +93,9 @@ class TestRollershutterMQTT(unittest.TestCase): } })) - current_position = self.hass.states.get( - 'rollershutter.test').attributes['current_position'] - self.assertEqual(-1, current_position) + state_attributes_dict = self.hass.states.get( + 'rollershutter.test').attributes + self.assertFalse('current_position' in state_attributes_dict) fire_mqtt_message(self.hass, 'state-topic', '0') self.hass.pool.block_till_done() @@ -114,3 +114,9 @@ class TestRollershutterMQTT(unittest.TestCase): current_position = self.hass.states.get( 'rollershutter.test').attributes['current_position'] self.assertEqual(50, current_position) + + fire_mqtt_message(self.hass, 'state-topic', 'non-numeric') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(50, current_position) From 08ba71a35931f4f37ae3a094dcf3623c65c5b9da Mon Sep 17 00:00:00 2001 From: sfam Date: Wed, 25 Nov 2015 18:13:39 +0000 Subject: [PATCH 6/8] rename component to motor and services to open/close/stop --- homeassistant/components/motor/__init__.py | 130 +++++++++++++++++ .../{rollershutter => motor}/mqtt.py | 52 +++---- .../{rollershutter => motor}/services.yaml | 0 .../components/rollershutter/__init__.py | 131 ------------------ .../{rollershutter => motor}/__init__.py | 0 .../{rollershutter => motor}/test_mqtt.py | 56 ++++---- 6 files changed, 184 insertions(+), 185 deletions(-) create mode 100644 homeassistant/components/motor/__init__.py rename homeassistant/components/{rollershutter => motor}/mqtt.py (71%) rename homeassistant/components/{rollershutter => motor}/services.yaml (100%) delete mode 100644 homeassistant/components/rollershutter/__init__.py rename tests/components/{rollershutter => motor}/__init__.py (100%) rename tests/components/{rollershutter => motor}/test_mqtt.py (67%) diff --git a/homeassistant/components/motor/__init__.py b/homeassistant/components/motor/__init__.py new file mode 100644 index 00000000000..feea836a0dd --- /dev/null +++ b/homeassistant/components/motor/__init__.py @@ -0,0 +1,130 @@ +""" +homeassistant.components.motor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Motor 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_OPEN, SERVICE_CLOSE, SERVICE_STOP, + STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID) + + +DOMAIN = 'motor' +DEPENDENCIES = [] +SCAN_INTERVAL = 15 + +GROUP_NAME_ALL_MOTORS = 'all motors' +ENTITY_ID_ALL_MOTORS = group.ENTITY_ID_FORMAT.format('all_motors') + +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 motor is open based on the statemachine. """ + entity_id = entity_id or ENTITY_ID_ALL_MOTORS + return hass.states.is_state(entity_id, STATE_OPEN) + + +def call_open(hass, entity_id=None): + """ Open all or specified motor. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_OPEN, data) + + +def call_close(hass, entity_id=None): + """ Close all or specified motor. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_CLOSE, data) + + +def call_stop(hass, entity_id=None): + """ Stops all or specified motor. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_STOP, data) + + +def setup(hass, config): + """ Track states and offer events for motors. """ + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, + GROUP_NAME_ALL_MOTORS) + component.setup(config) + + def handle_motor_service(service): + """ Handles calls to the motor services. """ + target_motors = component.extract_from_service(service) + + for motor in target_motors: + if service.service == SERVICE_OPEN: + motor.open() + elif service.service == SERVICE_CLOSE: + motor.close() + elif service.service == SERVICE_STOP: + motor.stop() + + if motor.should_poll: + motor.update_ha_state(True) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + hass.services.register(DOMAIN, SERVICE_OPEN, + handle_motor_service, + descriptions.get(SERVICE_OPEN)) + hass.services.register(DOMAIN, SERVICE_CLOSE, + handle_motor_service, + descriptions.get(SERVICE_CLOSE)) + hass.services.register(DOMAIN, SERVICE_STOP, + handle_motor_service, + descriptions.get(SERVICE_STOP)) + + return True + + +class MotorDevice(Entity): + """ Represents a motor within Home Assistant. """ + # pylint: disable=no-self-use + + @property + def current_position(self): + """ Return current position of motor. + None is unknown, 0 is closed, 100 is fully open. """ + raise NotImplementedError() + + @property + def state(self): + current = self.current_position + + if current is None: + return STATE_UNKNOWN + + return STATE_CLOSED if current == 0 else STATE_OPEN + + @property + def state_attributes(self): + """ Returns optional state attributes. """ + return None + + def open(self, **kwargs): + """ Open the device. """ + raise NotImplementedError() + + def close(self, **kwargs): + """ Close the device. """ + raise NotImplementedError() + + def stop(self, **kwargs): + """ Stop the device. """ + raise NotImplementedError() diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/motor/mqtt.py similarity index 71% rename from homeassistant/components/rollershutter/mqtt.py rename to homeassistant/components/motor/mqtt.py index 6128a4d9819..48a20d5a3b6 100644 --- a/homeassistant/components/rollershutter/mqtt.py +++ b/homeassistant/components/motor/mqtt.py @@ -1,21 +1,21 @@ """ -homeassistant.components.rollershutter.mqtt +homeassistant.components.motor.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allows to configure a MQTT rollershutter. +Allows to configure a MQTT motor. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rollershutter.mqtt/ +https://home-assistant.io/components/motor.mqtt/ """ import logging import homeassistant.components.mqtt as mqtt -from homeassistant.components.rollershutter import RollershutterDevice +from homeassistant.components.motor import MotorDevice _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -DEFAULT_NAME = "MQTT Shutter" +DEFAULT_NAME = "MQTT Motor" DEFAULT_QOS = 0 -DEFAULT_PAYLOAD_UP = "UP" -DEFAULT_PAYLOAD_DOWN = "DOWN" +DEFAULT_PAYLOAD_OPEN = "OPEN" +DEFAULT_PAYLOAD_CLOSE = "CLOSE" DEFAULT_PAYLOAD_STOP = "STOP" ATTR_CURRENT_POSITION = 'current_position' @@ -23,37 +23,37 @@ ATTR_CURRENT_POSITION = 'current_position' # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """ Add MQTT Roller Shutter """ + """ Add MQTT Motor """ if config.get('command_topic') is None: _LOGGER.error("Missing required variable: command_topic") return False - add_devices_callback([MqttRollershutter( + add_devices_callback([MqttMotor( 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_open', DEFAULT_PAYLOAD_OPEN), + config.get('payload_close', DEFAULT_PAYLOAD_CLOSE), 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 """ +class MqttMotor(MotorDevice): + """ Represents a motor that can be controlled using MQTT """ def __init__(self, hass, name, state_topic, command_topic, qos, - payload_up, payload_down, payload_stop, state_format): + payload_open, payload_close, payload_stop, state_format): self._state = None 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_open = payload_open + self._payload_close = payload_close self._payload_stop = payload_stop self._parse = mqtt.FmtParser(state_format) @@ -79,12 +79,12 @@ class MqttRollershutter(RollershutterDevice): @property def name(self): - """ The name of the rollershutter """ + """ The name of the motor """ return self._name @property def current_position(self): - """ Return current position of rollershutter. + """ Return current position of motor. None is unknown, 0 is closed, 100 is fully open. """ return self._state @@ -93,18 +93,18 @@ class MqttRollershutter(RollershutterDevice): """ True if device is open. """ return self._state > 0 - def move_up(self, **kwargs): - """ Moves the device UP. """ - mqtt.publish(self.hass, self._command_topic, self._payload_up, + def open(self, **kwargs): + """ Close the device. """ + mqtt.publish(self.hass, self._command_topic, self._payload_open, self._qos) - def move_down(self, **kwargs): - """ Moves the device DOWN. """ - mqtt.publish(self.hass, self._command_topic, self._payload_down, + def close(self, **kwargs): + """ Open the device. """ + mqtt.publish(self.hass, self._command_topic, self._payload_close, self._qos) - def move_stop(self, **kwargs): - """ Moves the device to STOP. """ + def stop(self, **kwargs): + """ Stop the device. """ mqtt.publish(self.hass, self._command_topic, self._payload_stop, self._qos) diff --git a/homeassistant/components/rollershutter/services.yaml b/homeassistant/components/motor/services.yaml similarity index 100% rename from homeassistant/components/rollershutter/services.yaml rename to homeassistant/components/motor/services.yaml diff --git a/homeassistant/components/rollershutter/__init__.py b/homeassistant/components/rollershutter/__init__.py deleted file mode 100644 index 5ebcf8a3d21..00000000000 --- a/homeassistant/components/rollershutter/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -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, STATE_CLOSED, STATE_UNKNOWN, 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 current_position(self): - """ Return current position of rollershutter. - None is unknown, 0 is closed, 100 is fully open. """ - raise NotImplementedError() - - @property - def state(self): - current = self.current_position - - if current is None: - return STATE_UNKNOWN - - return STATE_CLOSED if current == 0 else STATE_OPEN - - @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/tests/components/rollershutter/__init__.py b/tests/components/motor/__init__.py similarity index 100% rename from tests/components/rollershutter/__init__.py rename to tests/components/motor/__init__.py diff --git a/tests/components/rollershutter/test_mqtt.py b/tests/components/motor/test_mqtt.py similarity index 67% rename from tests/components/rollershutter/test_mqtt.py rename to tests/components/motor/test_mqtt.py index 2a71163c50f..4d7af04ae6c 100644 --- a/tests/components/rollershutter/test_mqtt.py +++ b/tests/components/motor/test_mqtt.py @@ -1,19 +1,19 @@ """ -tests.components.rollershutter.test_mqtt +tests.components.motor.test_mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Tests mqtt rollershutter. +Tests mqtt motor. """ import unittest from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN import homeassistant.core as ha -import homeassistant.components.rollershutter as rollershutter +import homeassistant.components.motor as motor from tests.common import mock_mqtt_component, fire_mqtt_message -class TestRollershutterMQTT(unittest.TestCase): - """ Test the MQTT rollershutter. """ +class TestMotorMQTT(unittest.TestCase): + """ Test the MQTT motor. """ def setUp(self): # pylint: disable=invalid-name self.hass = ha.HomeAssistant() @@ -24,43 +24,43 @@ class TestRollershutterMQTT(unittest.TestCase): self.hass.stop() def test_controlling_state_via_topic(self): - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.assertTrue(motor.setup(self.hass, { + 'motor': { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', 'command_topic': 'command-topic', 'qos': 0, - 'payload_up': 'UP', - 'payload_down': 'DOWN', + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', 'payload_stop': 'STOP' } })) - state = self.hass.states.get('rollershutter.test') + state = self.hass.states.get('motor.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') + state = self.hass.states.get('motor.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') + state = self.hass.states.get('motor.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') + state = self.hass.states.get('motor.test') self.assertEqual(STATE_OPEN, state.state) def test_sending_mqtt_commands(self): - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.assertTrue(motor.setup(self.hass, { + 'motor': { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', @@ -69,54 +69,54 @@ class TestRollershutterMQTT(unittest.TestCase): } })) - state = self.hass.states.get('rollershutter.test') + state = self.hass.states.get('motor.test') self.assertEqual(STATE_UNKNOWN, state.state) - rollershutter.move_up(self.hass, 'rollershutter.test') + motor.call_open(self.hass, 'motor.test') self.hass.pool.block_till_done() - self.assertEqual(('command-topic', 'UP', 2), + self.assertEqual(('command-topic', 'OPEN', 2), self.mock_publish.mock_calls[-1][1]) - state = self.hass.states.get('rollershutter.test') + state = self.hass.states.get('motor.test') self.assertEqual(STATE_UNKNOWN, state.state) def test_state_attributes_current_position(self): - self.assertTrue(rollershutter.setup(self.hass, { - 'rollershutter': { + self.assertTrue(motor.setup(self.hass, { + 'motor': { 'platform': 'mqtt', 'name': 'test', 'state_topic': 'state-topic', 'command_topic': 'command-topic', - 'payload_up': 'UP', - 'payload_down': 'DOWN', + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', 'payload_stop': 'STOP' } })) state_attributes_dict = self.hass.states.get( - 'rollershutter.test').attributes + 'motor.test').attributes self.assertFalse('current_position' in state_attributes_dict) 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'] + 'motor.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'] + 'motor.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'] + 'motor.test').attributes['current_position'] self.assertEqual(50, current_position) fire_mqtt_message(self.hass, 'state-topic', 'non-numeric') self.hass.pool.block_till_done() current_position = self.hass.states.get( - 'rollershutter.test').attributes['current_position'] + 'motor.test').attributes['current_position'] self.assertEqual(50, current_position) From 0dbf4b3c10e92ec207c3e5288646186b12196b90 Mon Sep 17 00:00:00 2001 From: sfam Date: Wed, 25 Nov 2015 18:20:16 +0000 Subject: [PATCH 7/8] add const.py changes to commit --- homeassistant/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index add1848b72c..1513c188cc2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -143,9 +143,9 @@ SERVICE_ALARM_TRIGGER = "alarm_trigger" SERVICE_LOCK = "lock" SERVICE_UNLOCK = "unlock" -SERVICE_MOVE_UP = 'move_up' -SERVICE_MOVE_DOWN = 'move_down' -SERVICE_MOVE_STOP = 'move_stop' +SERVICE_OPEN = 'open' +SERVICE_CLOSE = 'close' +SERVICE_STOP = 'stop' # #### API / REMOTE #### SERVER_PORT = 8123 From 4f75286f64dde2cbfa429eb19167169f88c2baa1 Mon Sep 17 00:00:00 2001 From: sfam Date: Thu, 26 Nov 2015 09:38:25 +0000 Subject: [PATCH 8/8] fix some comments --- homeassistant/components/motor/mqtt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motor/mqtt.py b/homeassistant/components/motor/mqtt.py index 48a20d5a3b6..2aac5e37c16 100644 --- a/homeassistant/components/motor/mqtt.py +++ b/homeassistant/components/motor/mqtt.py @@ -90,16 +90,16 @@ class MqttMotor(MotorDevice): @property def is_open(self): - """ True if device is open. """ + """ True if device is current position is not zero. """ return self._state > 0 def open(self, **kwargs): - """ Close the device. """ + """ Open the device. """ mqtt.publish(self.hass, self._command_topic, self._payload_open, self._qos) def close(self, **kwargs): - """ Open the device. """ + """ Close the device. """ mqtt.publish(self.hass, self._command_topic, self._payload_close, self._qos)