From 1d3cfd1b1d63302c3d86f4e20ddc3cb73cb45bf7 Mon Sep 17 00:00:00 2001 From: John Boiles Date: Tue, 5 Sep 2017 08:52:15 -0700 Subject: [PATCH 01/15] Basic MQTT vacuum support --- .coveragerc | 1 + homeassistant/components/vacuum/__init__.py | 35 +++ homeassistant/components/vacuum/mqtt.py | 251 ++++++++++++++++++++ tests/components/vacuum/test_mqtt.py | 131 ++++++++++ 4 files changed, 418 insertions(+) create mode 100644 homeassistant/components/vacuum/mqtt.py create mode 100644 tests/components/vacuum/test_mqtt.py diff --git a/.coveragerc b/.coveragerc index d5eb32e670c28..598667da0fd13 100644 --- a/.coveragerc +++ b/.coveragerc @@ -581,6 +581,7 @@ omit = homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py homeassistant/components/zwave/util.py + homeassistant/components/vacuum/mqtt.py [report] diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index a3d963624336e..a47f60c5ff77d 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -94,6 +94,41 @@ SUPPORT_MAP = 2048 +SERVICE_TO_STRING = { + SUPPORT_TURN_ON: 'turn_on', + SUPPORT_TURN_OFF: 'turn_off', + SUPPORT_PAUSE: 'pause', + SUPPORT_STOP: 'stop', + SUPPORT_RETURN_HOME: 'return_home', + SUPPORT_FAN_SPEED: 'fan_speed', + SUPPORT_BATTERY: 'battery', + SUPPORT_STATUS: 'status', + SUPPORT_SEND_COMMAND: 'send_command', + SUPPORT_LOCATE: 'locate', + SUPPORT_CLEAN_SPOT: 'clean_spot', + SUPPORT_MAP: 'map', +} + +STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} + + +def services_to_strings(services): + """Convert SUPPORT_* service bitmask to list of service strings.""" + strings = [] + for service in SERVICE_TO_STRING: + if service & services: + strings.append(SERVICE_TO_STRING[service]) + return strings + + +def strings_to_services(strings): + """Convert service strings to SUPPORT_* service bitmask.""" + services = 0 + for string in strings: + services |= STRING_TO_SERVICE[string] + return services + + @bind_hass def is_on(hass, entity_id=None): """Return if the vacuum is on based on the statemachine.""" diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py new file mode 100644 index 0000000000000..95e5999684328 --- /dev/null +++ b/homeassistant/components/vacuum/mqtt.py @@ -0,0 +1,251 @@ +""" +Support for a generic MQTT vacuum. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/vacuum/ +""" +import asyncio +import json +import logging + +import voluptuous as vol + +import homeassistant.components.mqtt as mqtt +import homeassistant.helpers.config_validation as cv +from homeassistant.components.vacuum import ( + DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, + SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, VacuumDevice, services_to_strings, strings_to_services) +from homeassistant.const import CONF_NAME, ATTR_SUPPORTED_FEATURES,\ + ATTR_BATTERY_LEVEL, ATTR_STATE +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mqtt'] + +DEFAULT_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STOP |\ + SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY |\ + SUPPORT_CLEAN_SPOT +ALL_SERVICES = DEFAULT_SERVICES | SUPPORT_PAUSE | SUPPORT_LOCATE +DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES) + +DEFAULT_COMMAND_TOPIC = 'vacuum/command' +DEFAULT_STATE_TOPIC = 'vacuum/state' + +DEFAULT_NAME = 'MQTT Vacuum' + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(ATTR_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(mqtt.CONF_COMMAND_TOPIC, default=DEFAULT_COMMAND_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(mqtt.CONF_STATE_TOPIC, default=DEFAULT_STATE_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the vacuum.""" + name = config.get(CONF_NAME) + command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) + state_topic = config.get(mqtt.CONF_STATE_TOPIC) + supported_feature_strings = config.get(ATTR_SUPPORTED_FEATURES) + supported_features = strings_to_services(supported_feature_strings) + qos = config.get(mqtt.CONF_QOS) + state_template = cv.template('{{value_json.battery_level}}') + state_template.hass = hass + + add_devices([ + MqttVacuum(name, supported_features, command_topic, state_topic, qos, + state_template), + ]) + + +class MqttVacuum(VacuumDevice): + """Representation of a MQTT-controlled vacuum.""" + + # pylint: disable=no-self-use + def __init__(self, name, supported_features, command_topic, state_topic, + qos, state_template): + """Initialize the vacuum.""" + self._name = name + self._command_topic = command_topic + self._state_topic = state_topic + self._supported_features = supported_features + self._qos = qos + + self._payload_turn_on = "turn_on" + self._payload_turn_off = "turn_off" + self._payload_return_to_base = "return_to_base" + self._payload_stop = "stop" + self._payload_clean_spot = "clean_spot" + self._payload_locate = "locate" + self._payload_start_pause = "start_pause" + self._retain = False + + self._state_template = state_template + + self._state = False + self._status = 'Unknown' + self._battery_level = 0 + + @asyncio.coroutine + def async_added_to_hass(self): + """Subscribe MQTT events. + + This method is a coroutine. + """ + @callback + def message_received(topic, payload, qos): + """Handle new MQTT message.""" + try: + payload = json.loads(payload) + except ValueError: # e.g. json.decoder.JSONDecodeError: + _LOGGER.warning("JSONDecodeError in vacuum.mqtt payload: %s", + payload) + return + + battery_level = payload.get(ATTR_BATTERY_LEVEL) + if battery_level is not None: + self._battery_level = int(battery_level) + + charging = payload.get("charging") + state = payload.get(ATTR_STATE) + if payload[ATTR_STATE]: + if state == "cleaning": + self._state = True + self._status = "Cleaning" + if state == "docked": + self._state = False + if charging: + self._status = "Docked & Charging" + else: + self._status = "Docked" + if state == "stopped": + self._state = False + self._status = "Stopped" + + self.hass.async_add_job(self.async_update_ha_state()) + + yield from mqtt.async_subscribe( + self.hass, self._state_topic, message_received, self._qos) + + @property + def name(self): + """Return the name of the vacuum.""" + return self._name + + @property + def icon(self): + """Return the icon for the vacuum.""" + return DEFAULT_ICON + + @property + def should_poll(self): + """No polling needed for an MQTT vacuum.""" + return False + + @property + def is_on(self): + """Return true if vacuum is on.""" + return self._state + + @property + def status(self): + """Return the status of the vacuum.""" + if self.supported_features & SUPPORT_STATUS == 0: + return + + return self._status + + @property + def battery_level(self): + """Return the status of the vacuum.""" + if self.supported_features & SUPPORT_BATTERY == 0: + return + + return max(0, min(100, self._battery_level)) + + @property + def device_state_attributes(self): + """Return device state attributes.""" + return {} + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features + + def turn_on(self, **kwargs): + """Turn the vacuum on.""" + if self.supported_features & SUPPORT_TURN_ON == 0: + return + + mqtt.async_publish(self.hass, self._command_topic, + self._payload_turn_on, self._qos, self._retain) + self._status = 'Cleaning' + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the vacuum off.""" + if self.supported_features & SUPPORT_TURN_OFF == 0: + return + + mqtt.async_publish(self.hass, self._command_topic, + self._payload_turn_off, self._qos, self._retain) + self._status = 'Turning Off' + self.schedule_update_ha_state() + + def stop(self, **kwargs): + """Turn the vacuum off.""" + if self.supported_features & SUPPORT_STOP == 0: + return + + mqtt.async_publish(self.hass, self._command_topic, self._payload_stop, + self._qos, self._retain) + self._status = 'Stopping the current task' + self.schedule_update_ha_state() + + def clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + if self.supported_features & SUPPORT_CLEAN_SPOT == 0: + return + + mqtt.async_publish(self.hass, self._command_topic, + self._payload_clean_spot, self._qos, self._retain) + self._status = "Cleaning spot" + self.schedule_update_ha_state() + + def locate(self, **kwargs): + """Turn the vacuum off.""" + if self.supported_features & SUPPORT_LOCATE == 0: + return + + mqtt.async_publish(self.hass, self._command_topic, + self._payload_locate, self._qos, self._retain) + self._status = "Hi, I'm over here!" + self.schedule_update_ha_state() + + def start_pause(self, **kwargs): + """Start, pause or resume the cleaning task.""" + if self.supported_features & SUPPORT_PAUSE == 0: + return + + mqtt.async_publish(self.hass, self._command_topic, + self._payload_start_pause, self._qos, self._retain) + self._status = 'Pausing/Resuming cleaning...' + self.schedule_update_ha_state() + + def return_to_base(self, **kwargs): + """Tell the vacuum to return to its dock.""" + if self.supported_features & SUPPORT_RETURN_HOME == 0: + return + + mqtt.async_publish(self.hass, self._command_topic, + self._payload_return_to_base, self._qos, + self._retain) + self._status = 'Returning home...' + self.schedule_update_ha_state() diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py new file mode 100644 index 0000000000000..196c7877ab0f6 --- /dev/null +++ b/tests/components/vacuum/test_mqtt.py @@ -0,0 +1,131 @@ +"""The tests for the Demo vacuum platform.""" +import unittest + +from homeassistant.components import vacuum +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, services_to_strings, ATTR_BATTERY_ICON, ATTR_STATUS) +from homeassistant.components.vacuum.mqtt import ALL_SERVICES +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME) +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, mock_mqtt_component,\ + fire_mqtt_message + + +class TestVacuumMQTT(unittest.TestCase): + """MQTT vacuum component test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_default_supported_features(self): + """Test that the correct supported features.""" + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + } + })) + entity = self.hass.states.get('vacuum.mqtttest') + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + self.assertListEqual(sorted(services_to_strings(entity_features)), + sorted(['turn_on', 'turn_off', 'stop', + 'return_home', 'battery', 'status', + 'clean_spot'])) + + def test_all_commands(self): + """Test simple commands to the vacuum.""" + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + ATTR_SUPPORTED_FEATURES: services_to_strings(ALL_SERVICES), + } + })) + + vacuum.turn_on(self.hass, 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual(('vacuum/command', 'turn_on', 0, False), + self.mock_publish.mock_calls[-2][1]) + + vacuum.turn_off(self.hass, 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual(('vacuum/command', 'turn_off', 0, False), + self.mock_publish.mock_calls[-2][1]) + + vacuum.stop(self.hass, 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual(('vacuum/command', 'stop', 0, False), + self.mock_publish.mock_calls[-2][1]) + + vacuum.clean_spot(self.hass, 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual(('vacuum/command', 'clean_spot', 0, False), + self.mock_publish.mock_calls[-2][1]) + + vacuum.locate(self.hass, 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual(('vacuum/command', 'locate', 0, False), + self.mock_publish.mock_calls[-2][1]) + + vacuum.start_pause(self.hass, 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual(('vacuum/command', 'start_pause', 0, False), + self.mock_publish.mock_calls[-2][1]) + + vacuum.return_to_base(self.hass, 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual(('vacuum/command', 'return_to_base', 0, False), + self.mock_publish.mock_calls[-2][1]) + + def test_status(self): + """Test status updates from the vacuum.""" + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + } + })) + + message = """ + {"battery_level": 54, "state": "cleaning", "charging": false} + """ + fire_mqtt_message(self.hass, 'vacuum/state', message) + self.hass.block_till_done() + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(state.attributes.get(ATTR_BATTERY_ICON), + 'mdi:battery-50') + self.assertEqual(54, state.attributes.get(ATTR_BATTERY_LEVEL)) + + message = """ + {"battery_level": 61, "state": "docked", "charging": true} + """ + fire_mqtt_message(self.hass, 'vacuum/state', message) + self.hass.block_till_done() + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual(state.attributes.get(ATTR_BATTERY_ICON), + 'mdi:battery-charging-60') + self.assertEqual(61, state.attributes.get(ATTR_BATTERY_LEVEL)) + + def test_status_invalid_json(self): + """Test to make sure nothing breaks if the vacuum sends bad JSON.""" + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + } + })) + + fire_mqtt_message(self.hass, 'vacuum/state', '{"asdfasas false}') + self.hass.block_till_done() + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(STATE_OFF, state.state) + self.assertEqual("Unknown", state.attributes.get(ATTR_STATUS)) From 43dc1a6c088198c0c6170e87b11589b97fcfe805 Mon Sep 17 00:00:00 2001 From: John Boiles Date: Tue, 12 Sep 2017 22:38:02 -0700 Subject: [PATCH 02/15] PR feedback --- homeassistant/components/vacuum/demo.py | 57 +++--- homeassistant/components/vacuum/mqtt.py | 240 +++++++++++++++++------- tests/components/vacuum/test_mqtt.py | 4 +- 3 files changed, 205 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index 54415b59db087..5668072db1cdd 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ +import asyncio import logging from homeassistant.components.vacuum import ( @@ -36,9 +37,10 @@ DEMO_VACUUM_NONE = '4_Fourth_floor' -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Demo vacuums.""" - add_devices([ + async_add_devices([ DemoVacuum(DEMO_VACUUM_COMPLETE, SUPPORT_ALL_SERVICES), DemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES), DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES), @@ -121,7 +123,8 @@ def supported_features(self): """Flag supported features.""" return self._supported_features - def turn_on(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): """Turn the vacuum on.""" if self.supported_features & SUPPORT_TURN_ON == 0: return @@ -130,27 +133,30 @@ def turn_on(self, **kwargs): self._cleaned_area += 5.32 self._battery_level -= 2 self._status = 'Cleaning' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Turn the vacuum off.""" if self.supported_features & SUPPORT_TURN_OFF == 0: return self._state = False self._status = 'Charging' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def stop(self, **kwargs): - """Turn the vacuum off.""" + @asyncio.coroutine + def async_stop(self, **kwargs): + """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return self._state = False self._status = 'Stopping the current task' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def clean_spot(self, **kwargs): + @asyncio.coroutine + def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return @@ -159,17 +165,19 @@ def clean_spot(self, **kwargs): self._cleaned_area += 1.32 self._battery_level -= 1 self._status = "Cleaning spot" - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def locate(self, **kwargs): - """Turn the vacuum off.""" + @asyncio.coroutine + def async_locate(self, **kwargs): + """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return self._status = "Hi, I'm over here!" - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def start_pause(self, **kwargs): + @asyncio.coroutine + def async_start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" if self.supported_features & SUPPORT_PAUSE == 0: return @@ -181,18 +189,20 @@ def start_pause(self, **kwargs): self._battery_level -= 1 else: self._status = 'Pausing the current task' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_fan_speed(self, fan_speed, **kwargs): - """Tell the vacuum to return to its dock.""" + @asyncio.coroutine + def async_set_fan_speed(self, fan_speed, **kwargs): + """Set the vacuum's fan speed.""" if self.supported_features & SUPPORT_FAN_SPEED == 0: return if fan_speed in self.fan_speed_list: self._fan_speed = fan_speed - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def return_to_base(self, **kwargs): + @asyncio.coroutine + def async_return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: return @@ -200,13 +210,14 @@ def return_to_base(self, **kwargs): self._state = False self._status = 'Returning home...' self._battery_level += 5 - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def send_command(self, command, params=None, **kwargs): + @asyncio.coroutine + def async_send_command(self, command, params=None, **kwargs): """Send a command to the vacuum.""" if self.supported_features & SUPPORT_SEND_COMMAND == 0: return self._status = 'Executing {}({})'.format(command, params) self._state = True - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 95e5999684328..a01a8f7172e61 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -2,10 +2,9 @@ Support for a generic MQTT vacuum. For more details about this platform, please refer to the documentation -https://home-assistant.io/components/vacuum/ +https://home-assistant.io/components/vacuum.mqtt/ """ import asyncio -import json import logging import voluptuous as vol @@ -13,12 +12,11 @@ import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv from homeassistant.components.vacuum import ( - DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, - SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, - SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, VacuumDevice, services_to_strings, strings_to_services) -from homeassistant.const import CONF_NAME, ATTR_SUPPORTED_FEATURES,\ - ATTR_BATTERY_LEVEL, ATTR_STATE + DEFAULT_ICON, services_to_strings, strings_to_services, SUPPORT_BATTERY, + SUPPORT_CLEAN_SPOT, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, + SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + VacuumDevice, STRING_TO_SERVICE) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback _LOGGER = logging.getLogger(__name__) @@ -29,38 +27,126 @@ SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY |\ SUPPORT_CLEAN_SPOT ALL_SERVICES = DEFAULT_SERVICES | SUPPORT_PAUSE | SUPPORT_LOCATE -DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES) +CONF_PAYLOAD_TURN_ON = 'payload_turn_on' +CONF_PAYLOAD_TURN_OFF = 'payload_turn_off' +CONF_PAYLOAD_RETURN_TO_BASE = 'payload_return_to_base' +CONF_PAYLOAD_STOP = 'payload_stop' +CONF_PAYLOAD_CLEAN_SPOT = 'payload_clean_spot' +CONF_PAYLOAD_LOCATE = 'payload_locate' +CONF_PAYLOAD_START_PAUSE = 'payload_start_pause' +CONF_BATTERY_LEVEL_TOPIC = 'battery_level_topic' +CONF_BATTERY_LEVEL_TEMPLATE = 'battery_level_template' +CONF_CHARGING_TOPIC = 'charging_topic' +CONF_CHARGING_TEMPLATE = 'charging_template' +CONF_STATE_TOPIC = 'state_topic' +CONF_STATE_TEMPLATE = 'state_template' + +DEFAULT_NAME = 'MQTT Vacuum' +DEFAULT_RETAIN = False +DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES) DEFAULT_COMMAND_TOPIC = 'vacuum/command' +DEFAULT_PAYLOAD_TURN_ON = 'turn_on' +DEFAULT_PAYLOAD_TURN_OFF = 'turn_off' +DEFAULT_PAYLOAD_RETURN_TO_BASE = 'return_to_base' +DEFAULT_PAYLOAD_STOP = 'stop' +DEFAULT_PAYLOAD_CLEAN_SPOT = 'clean_spot' +DEFAULT_PAYLOAD_LOCATE = 'locate' +DEFAULT_PAYLOAD_START_PAUSE = 'start_pause' +DEFAULT_BATTERY_LEVEL_TOPIC = 'vacuum/state' +DEFAULT_BATTERY_LEVEL_TEMPLATE = cv.template('{{ value_json.battery_level }}') +DEFAULT_CHARGING_TOPIC = 'vacuum/state' +DEFAULT_CHARGING_TEMPLATE = cv.template('{{ value_json.charging }}') DEFAULT_STATE_TOPIC = 'vacuum/state' +DEFAULT_STATE_TEMPLATE = cv.template('{{ value_json.state }}') -DEFAULT_NAME = 'MQTT Vacuum' PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(ATTR_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS): - vol.All(cv.ensure_list, [cv.string]), + vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), + + vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(mqtt.CONF_COMMAND_TOPIC, default=DEFAULT_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(mqtt.CONF_STATE_TOPIC, default=DEFAULT_STATE_TOPIC): + vol.Optional(CONF_PAYLOAD_TURN_ON, + default=DEFAULT_PAYLOAD_TURN_ON): cv.string, + vol.Optional(CONF_PAYLOAD_TURN_OFF, + default=DEFAULT_PAYLOAD_TURN_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_RETURN_TO_BASE, + default=DEFAULT_PAYLOAD_RETURN_TO_BASE): cv.string, + vol.Optional(CONF_PAYLOAD_STOP, + default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_PAYLOAD_CLEAN_SPOT, + default=DEFAULT_PAYLOAD_CLEAN_SPOT): cv.string, + vol.Optional(CONF_PAYLOAD_LOCATE, + default=DEFAULT_PAYLOAD_LOCATE): cv.string, + vol.Optional(CONF_PAYLOAD_START_PAUSE, + default=DEFAULT_PAYLOAD_START_PAUSE): cv.string, + + vol.Optional(CONF_BATTERY_LEVEL_TOPIC, + default=DEFAULT_BATTERY_LEVEL_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE, + default=DEFAULT_BATTERY_LEVEL_TEMPLATE): + cv.template, + + vol.Optional(CONF_CHARGING_TOPIC, + default=DEFAULT_CHARGING_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(CONF_CHARGING_TEMPLATE, + default=DEFAULT_CHARGING_TEMPLATE): + cv.template, + + vol.Optional(CONF_STATE_TOPIC, + default=DEFAULT_STATE_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(CONF_STATE_TEMPLATE, + default=DEFAULT_STATE_TEMPLATE): + cv.template, }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the vacuum.""" name = config.get(CONF_NAME) - command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) - state_topic = config.get(mqtt.CONF_STATE_TOPIC) supported_feature_strings = config.get(ATTR_SUPPORTED_FEATURES) supported_features = strings_to_services(supported_feature_strings) + qos = config.get(mqtt.CONF_QOS) - state_template = cv.template('{{value_json.battery_level}}') + retain = config.get(mqtt.CONF_RETAIN) + + command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) + payload_turn_on = config.get(CONF_PAYLOAD_TURN_ON) + payload_turn_off = config.get(CONF_PAYLOAD_TURN_OFF) + payload_return_to_base = config.get(CONF_PAYLOAD_RETURN_TO_BASE) + payload_stop = config.get(CONF_PAYLOAD_STOP) + payload_clean_spot = config.get(CONF_PAYLOAD_CLEAN_SPOT) + payload_locate = config.get(CONF_PAYLOAD_LOCATE) + payload_start_pause = config.get(CONF_PAYLOAD_START_PAUSE) + + battery_level_topic = config.get(CONF_BATTERY_LEVEL_TOPIC) + battery_level_template = config.get(CONF_BATTERY_LEVEL_TEMPLATE) + battery_level_template.hass = hass + + charging_topic = config.get(CONF_CHARGING_TOPIC) + charging_template = config.get(CONF_CHARGING_TEMPLATE) + charging_template.hass = hass + + state_topic = config.get(mqtt.CONF_STATE_TOPIC) + state_template = config.get(CONF_STATE_TEMPLATE) state_template.hass = hass - add_devices([ - MqttVacuum(name, supported_features, command_topic, state_topic, qos, - state_template), + async_add_devices([ + MqttVacuum(name, supported_features, qos, retain, command_topic, + payload_turn_on, payload_turn_off, + payload_return_to_base, payload_stop, + payload_clean_spot, payload_locate, + payload_start_pause, battery_level_topic, + battery_level_template, charging_topic, charging_template, + state_topic, state_template), ]) @@ -68,24 +154,34 @@ class MqttVacuum(VacuumDevice): """Representation of a MQTT-controlled vacuum.""" # pylint: disable=no-self-use - def __init__(self, name, supported_features, command_topic, state_topic, - qos, state_template): + def __init__(self, name, supported_features, qos, retain, command_topic, + payload_turn_on, payload_turn_off, payload_return_to_base, + payload_stop, payload_clean_spot, payload_locate, + payload_start_pause, battery_level_topic, + battery_level_template, charging_topic, charging_template, + state_topic, state_template): """Initialize the vacuum.""" self._name = name - self._command_topic = command_topic - self._state_topic = state_topic self._supported_features = supported_features self._qos = qos + self._retain = retain + + self._command_topic = command_topic + self._payload_turn_on = payload_turn_on + self._payload_turn_off = payload_turn_off + self._payload_return_to_base = payload_return_to_base + self._payload_stop = payload_stop + self._payload_clean_spot = payload_clean_spot + self._payload_locate = payload_locate + self._payload_start_pause = payload_start_pause + + self._battery_level_topic = battery_level_topic + self._battery_level_template = battery_level_template - self._payload_turn_on = "turn_on" - self._payload_turn_off = "turn_off" - self._payload_return_to_base = "return_to_base" - self._payload_stop = "stop" - self._payload_clean_spot = "clean_spot" - self._payload_locate = "locate" - self._payload_start_pause = "start_pause" - self._retain = False + self._charging_topic = charging_topic + self._charging_template = charging_template + self._state_topic = state_topic self._state_template = state_template self._state = False @@ -101,37 +197,37 @@ def async_added_to_hass(self): @callback def message_received(topic, payload, qos): """Handle new MQTT message.""" - try: - payload = json.loads(payload) - except ValueError: # e.g. json.decoder.JSONDecodeError: - _LOGGER.warning("JSONDecodeError in vacuum.mqtt payload: %s", - payload) - return - - battery_level = payload.get(ATTR_BATTERY_LEVEL) + battery_level = self._battery_level_template\ + .async_render_with_possible_json_value(payload, + error_value=None) if battery_level is not None: self._battery_level = int(battery_level) - charging = payload.get("charging") - state = payload.get(ATTR_STATE) - if payload[ATTR_STATE]: - if state == "cleaning": + charging = self._charging_template\ + .async_render_with_possible_json_value(payload, + error_value=False) + state = self._state_template\ + .async_render_with_possible_json_value(payload, + error_value=None) + + if state: + if state == 'cleaning': self._state = True self._status = "Cleaning" - if state == "docked": + if state == 'docked': self._state = False if charging: self._status = "Docked & Charging" else: self._status = "Docked" - if state == "stopped": + if state == 'stopped': self._state = False self._status = "Stopped" - self.hass.async_add_job(self.async_update_ha_state()) + self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( - self.hass, self._state_topic, message_received, self._qos) + yield from self.hass.components.mqtt.async_subscribe( + self._state_topic, message_received, self._qos) @property def name(self): @@ -155,7 +251,7 @@ def is_on(self): @property def status(self): - """Return the status of the vacuum.""" + """Return the battery level of the vacuum.""" if self.supported_features & SUPPORT_STATUS == 0: return @@ -169,17 +265,13 @@ def battery_level(self): return max(0, min(100, self._battery_level)) - @property - def device_state_attributes(self): - """Return device state attributes.""" - return {} - @property def supported_features(self): """Flag supported features.""" return self._supported_features - def turn_on(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): """Turn the vacuum on.""" if self.supported_features & SUPPORT_TURN_ON == 0: return @@ -187,9 +279,10 @@ def turn_on(self, **kwargs): mqtt.async_publish(self.hass, self._command_topic, self._payload_turn_on, self._qos, self._retain) self._status = 'Cleaning' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Turn the vacuum off.""" if self.supported_features & SUPPORT_TURN_OFF == 0: return @@ -197,19 +290,21 @@ def turn_off(self, **kwargs): mqtt.async_publish(self.hass, self._command_topic, self._payload_turn_off, self._qos, self._retain) self._status = 'Turning Off' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def stop(self, **kwargs): - """Turn the vacuum off.""" + @asyncio.coroutine + def async_stop(self, **kwargs): + """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return mqtt.async_publish(self.hass, self._command_topic, self._payload_stop, self._qos, self._retain) self._status = 'Stopping the current task' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def clean_spot(self, **kwargs): + @asyncio.coroutine + def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return @@ -217,19 +312,21 @@ def clean_spot(self, **kwargs): mqtt.async_publish(self.hass, self._command_topic, self._payload_clean_spot, self._qos, self._retain) self._status = "Cleaning spot" - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def locate(self, **kwargs): - """Turn the vacuum off.""" + @asyncio.coroutine + def async_locate(self, **kwargs): + """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return mqtt.async_publish(self.hass, self._command_topic, self._payload_locate, self._qos, self._retain) self._status = "Hi, I'm over here!" - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def start_pause(self, **kwargs): + @asyncio.coroutine + def async_start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" if self.supported_features & SUPPORT_PAUSE == 0: return @@ -237,9 +334,10 @@ def start_pause(self, **kwargs): mqtt.async_publish(self.hass, self._command_topic, self._payload_start_pause, self._qos, self._retain) self._status = 'Pausing/Resuming cleaning...' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def return_to_base(self, **kwargs): + @asyncio.coroutine + def async_return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: return @@ -248,4 +346,4 @@ def return_to_base(self, **kwargs): self._payload_return_to_base, self._qos, self._retain) self._status = 'Returning home...' - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index 196c7877ab0f6..634c2e73631b2 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -8,8 +8,8 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME) from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, mock_mqtt_component,\ - fire_mqtt_message +from tests.common import ( + fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) class TestVacuumMQTT(unittest.TestCase): From 2009409e59977a7a0261cda6522fc42c95d729df Mon Sep 17 00:00:00 2001 From: John Boiles Date: Tue, 12 Sep 2017 23:35:34 -0700 Subject: [PATCH 03/15] Support for fan_speed and send_command services --- homeassistant/components/vacuum/mqtt.py | 202 ++++++++++++++++++------ tests/components/vacuum/test_mqtt.py | 39 ++++- 2 files changed, 186 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index a01a8f7172e61..0afd233ac6aad 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -12,10 +12,10 @@ import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv from homeassistant.components.vacuum import ( - DEFAULT_ICON, services_to_strings, strings_to_services, SUPPORT_BATTERY, - SUPPORT_CLEAN_SPOT, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, - SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumDevice, STRING_TO_SERVICE) + DEFAULT_ICON, services_to_strings, strings_to_services, STRING_TO_SERVICE, + SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, + SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VacuumDevice) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback @@ -26,7 +26,8 @@ DEFAULT_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STOP |\ SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY |\ SUPPORT_CLEAN_SPOT -ALL_SERVICES = DEFAULT_SERVICES | SUPPORT_PAUSE | SUPPORT_LOCATE +ALL_SERVICES = DEFAULT_SERVICES | SUPPORT_PAUSE | SUPPORT_LOCATE |\ + SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND CONF_PAYLOAD_TURN_ON = 'payload_turn_on' CONF_PAYLOAD_TURN_OFF = 'payload_turn_off' @@ -41,6 +42,11 @@ CONF_CHARGING_TEMPLATE = 'charging_template' CONF_STATE_TOPIC = 'state_topic' CONF_STATE_TEMPLATE = 'state_template' +CONF_FAN_SPEED_TOPIC = 'fan_speed_topic' +CONF_FAN_SPEED_TEMPLATE = 'fan_speed_template' +CONF_SET_FAN_SPEED_TOPIC = 'set_fan_speed_topic' +CONF_FAN_SPEED_LIST = 'fan_speed_list' +CONF_SEND_COMMAND_TOPIC = 'send_command_topic' DEFAULT_NAME = 'MQTT Vacuum' DEFAULT_RETAIN = False @@ -59,7 +65,11 @@ DEFAULT_CHARGING_TEMPLATE = cv.template('{{ value_json.charging }}') DEFAULT_STATE_TOPIC = 'vacuum/state' DEFAULT_STATE_TEMPLATE = cv.template('{{ value_json.state }}') - +DEFAULT_FAN_SPEED_TOPIC = 'vacuum/state' +DEFAULT_FAN_SPEED_TEMPLATE = cv.template('{{ value_json.fan_speed }}') +DEFAULT_SET_FAN_SPEED_TOPIC = 'vacuum/set_fan_speed' +DEFAULT_FAN_SPEED_LIST = ['min', 'medium', 'high', 'max'] +DEFAULT_SEND_COMMAND_TOPIC = 'vacuum/send_command' PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -105,6 +115,23 @@ vol.Optional(CONF_STATE_TEMPLATE, default=DEFAULT_STATE_TEMPLATE): cv.template, + + vol.Optional(CONF_FAN_SPEED_TOPIC, + default=DEFAULT_FAN_SPEED_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_SPEED_TEMPLATE, + default=DEFAULT_FAN_SPEED_TEMPLATE): + cv.template, + + vol.Optional(CONF_SET_FAN_SPEED_TOPIC, + default=DEFAULT_SET_FAN_SPEED_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_SPEED_LIST, default=DEFAULT_FAN_SPEED_LIST): + cv.ensure_list, + + vol.Optional(CONF_SEND_COMMAND_TOPIC, + default=DEFAULT_SEND_COMMAND_TOPIC): + mqtt.valid_publish_topic, }) @@ -139,14 +166,24 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): state_template = config.get(CONF_STATE_TEMPLATE) state_template.hass = hass + fan_speed_topic = config.get(CONF_FAN_SPEED_TOPIC) + fan_speed_template = config.get(CONF_FAN_SPEED_TEMPLATE) + fan_speed_template.hass = hass + set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) + fan_speed_list = config.get(CONF_FAN_SPEED_LIST) + + send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) + async_add_devices([ - MqttVacuum(name, supported_features, qos, retain, command_topic, - payload_turn_on, payload_turn_off, - payload_return_to_base, payload_stop, - payload_clean_spot, payload_locate, - payload_start_pause, battery_level_topic, - battery_level_template, charging_topic, charging_template, - state_topic, state_template), + MqttVacuum( + name, supported_features, qos, retain, command_topic, + payload_turn_on, payload_turn_off, payload_return_to_base, + payload_stop, payload_clean_spot, payload_locate, + payload_start_pause, battery_level_topic, battery_level_template, + charging_topic, charging_template, state_topic, state_template, + fan_speed_topic, fan_speed_template, set_fan_speed_topic, + fan_speed_list, send_command_topic + ), ]) @@ -154,12 +191,14 @@ class MqttVacuum(VacuumDevice): """Representation of a MQTT-controlled vacuum.""" # pylint: disable=no-self-use - def __init__(self, name, supported_features, qos, retain, command_topic, - payload_turn_on, payload_turn_off, payload_return_to_base, - payload_stop, payload_clean_spot, payload_locate, - payload_start_pause, battery_level_topic, - battery_level_template, charging_topic, charging_template, - state_topic, state_template): + def __init__( + self, name, supported_features, qos, retain, command_topic, + payload_turn_on, payload_turn_off, payload_return_to_base, + payload_stop, payload_clean_spot, payload_locate, + payload_start_pause, battery_level_topic, battery_level_template, + charging_topic, charging_template, state_topic, state_template, + fan_speed_topic, fan_speed_template, set_fan_speed_topic, + fan_speed_list, send_command_topic): """Initialize the vacuum.""" self._name = name self._supported_features = supported_features @@ -184,9 +223,18 @@ def __init__(self, name, supported_features, qos, retain, command_topic, self._state_topic = state_topic self._state_template = state_template - self._state = False + self._fan_speed_topic = fan_speed_topic + self._fan_speed_template = fan_speed_template + + self._set_fan_speed_topic = set_fan_speed_topic + self._fan_speed_list = fan_speed_list + self._send_command_topic = send_command_topic + + self._is_cleaning = False + self._charging = False self._status = 'Unknown' self._battery_level = 0 + self._fan_speed = 'unknown' @asyncio.coroutine def async_added_to_hass(self): @@ -197,32 +245,49 @@ def async_added_to_hass(self): @callback def message_received(topic, payload, qos): """Handle new MQTT message.""" - battery_level = self._battery_level_template\ - .async_render_with_possible_json_value(payload, - error_value=None) - if battery_level is not None: - self._battery_level = int(battery_level) - - charging = self._charging_template\ - .async_render_with_possible_json_value(payload, - error_value=False) - state = self._state_template\ - .async_render_with_possible_json_value(payload, - error_value=None) - - if state: - if state == 'cleaning': - self._state = True - self._status = "Cleaning" - if state == 'docked': - self._state = False - if charging: - self._status = "Docked & Charging" - else: - self._status = "Docked" - if state == 'stopped': - self._state = False - self._status = "Stopped" + + if topic == self._battery_level_topic: + battery_level = self._battery_level_template\ + .async_render_with_possible_json_value( + payload, + error_value=None) + if battery_level is not None: + self._battery_level = int(battery_level) + + if topic == self._charging_topic: + charging = self._charging_template\ + .async_render_with_possible_json_value( + payload, + error_value=None) + if charging is not None: + self._charging = charging + + if topic == self._state_topic: + state = self._state_template\ + .async_render_with_possible_json_value( + payload, + error_value=None) + if state: + if state == 'cleaning': + self._is_cleaning = True + self._status = "Cleaning" + if state == 'docked': + self._is_cleaning = False + if self._charging: + self._status = "Docked & Charging" + else: + self._status = "Docked" + if state == 'stopped': + self._is_cleaning = False + self._status = "Stopped" + + if topic == self._fan_speed_topic: + fan_speed = self._fan_speed_template\ + .async_render_with_possible_json_value( + payload, + error_value=None) + if fan_speed is not None: + self._fan_speed = fan_speed self.async_schedule_update_ha_state() @@ -247,16 +312,31 @@ def should_poll(self): @property def is_on(self): """Return true if vacuum is on.""" - return self._state + return self._is_cleaning @property def status(self): - """Return the battery level of the vacuum.""" + """Return a status string for the vacuum.""" if self.supported_features & SUPPORT_STATUS == 0: return return self._status + @property + def fan_speed(self): + """Return the status of the vacuum.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return + + return self._fan_speed + + @property + def fan_speed_list(self): + """Return the status of the vacuum.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return [] + return self._fan_speed_list + @property def battery_level(self): """Return the status of the vacuum.""" @@ -347,3 +427,29 @@ def async_return_to_base(self, **kwargs): self._retain) self._status = 'Returning home...' self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_set_fan_speed(self, fan_speed, **kwargs): + """Set fan speed.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return + if not self._fan_speed_list or fan_speed not in self._fan_speed_list: + return + + mqtt.async_publish( + self.hass, self._set_fan_speed_topic, fan_speed, self._qos, + self._retain) + self._status = "Setting fan to {}...".format(fan_speed) + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" + if self.supported_features & SUPPORT_SEND_COMMAND == 0: + return + + mqtt.async_publish( + self.hass, self._send_command_topic, command, self._qos, + self._retain) + self._status = "Sending command {}...".format(command) + self.async_schedule_update_ha_state() diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index 634c2e73631b2..2bedb090bf04b 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -3,7 +3,8 @@ from homeassistant.components import vacuum from homeassistant.components.vacuum import ( - ATTR_BATTERY_LEVEL, services_to_strings, ATTR_BATTERY_ICON, ATTR_STATUS) + ATTR_BATTERY_LEVEL, services_to_strings, ATTR_BATTERY_ICON, ATTR_STATUS, + ATTR_FAN_SPEED) from homeassistant.components.vacuum.mqtt import ALL_SERVICES from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME) @@ -84,18 +85,36 @@ def test_all_commands(self): self.assertEqual(('vacuum/command', 'return_to_base', 0, False), self.mock_publish.mock_calls[-2][1]) + vacuum.set_fan_speed(self.hass, 'high', 'vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual( + ('vacuum/set_fan_speed', 'high', 0, False), + self.mock_publish.mock_calls[-2][1] + ) + + vacuum.send_command(self.hass, '44 FE 93', entity_id='vacuum.mqtttest') + self.hass.block_till_done() + self.assertEqual( + ('vacuum/send_command', '44 FE 93', 0, False), + self.mock_publish.mock_calls[-2][1] + ) + def test_status(self): """Test status updates from the vacuum.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { vacuum.DOMAIN: { CONF_PLATFORM: 'mqtt', CONF_NAME: 'mqtttest', + ATTR_SUPPORTED_FEATURES: services_to_strings(ALL_SERVICES), } })) - message = """ - {"battery_level": 54, "state": "cleaning", "charging": false} - """ + message = """{ + "battery_level": 54, + "state": "cleaning", + "charging": false, + "fan_speed": "max" + }""" fire_mqtt_message(self.hass, 'vacuum/state', message) self.hass.block_till_done() state = self.hass.states.get('vacuum.mqtttest') @@ -103,10 +122,15 @@ def test_status(self): self.assertEqual(state.attributes.get(ATTR_BATTERY_ICON), 'mdi:battery-50') self.assertEqual(54, state.attributes.get(ATTR_BATTERY_LEVEL)) + self.assertEqual('max', state.attributes.get(ATTR_FAN_SPEED)) + + message = """{ + "battery_level": 61, + "state": "docked", + "charging": true, + "fan_speed": "min" + }""" - message = """ - {"battery_level": 61, "state": "docked", "charging": true} - """ fire_mqtt_message(self.hass, 'vacuum/state', message) self.hass.block_till_done() state = self.hass.states.get('vacuum.mqtttest') @@ -114,6 +138,7 @@ def test_status(self): self.assertEqual(state.attributes.get(ATTR_BATTERY_ICON), 'mdi:battery-charging-60') self.assertEqual(61, state.attributes.get(ATTR_BATTERY_LEVEL)) + self.assertEqual('min', state.attributes.get(ATTR_FAN_SPEED)) def test_status_invalid_json(self): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" From d2bd84a83e43767ad4c6d21567ecb7f4ac69b18b Mon Sep 17 00:00:00 2001 From: John Boiles Date: Wed, 13 Sep 2017 00:22:21 -0700 Subject: [PATCH 04/15] Fix configurable topics --- homeassistant/components/vacuum/mqtt.py | 9 ++++++--- tests/components/vacuum/test_mqtt.py | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 0afd233ac6aad..9719783dbb4ba 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -245,7 +245,6 @@ def async_added_to_hass(self): @callback def message_received(topic, payload, qos): """Handle new MQTT message.""" - if topic == self._battery_level_topic: battery_level = self._battery_level_template\ .async_render_with_possible_json_value( @@ -291,8 +290,12 @@ def message_received(topic, payload, qos): self.async_schedule_update_ha_state() - yield from self.hass.components.mqtt.async_subscribe( - self._state_topic, message_received, self._qos) + topics_set = { + self._battery_level_topic, self._charging_topic, self._state_topic, + self._fan_speed_topic} + for topic in topics_set: + yield from self.hass.components.mqtt.async_subscribe( + topic, message_received, self._qos) @property def name(self): diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index 2bedb090bf04b..8093a41ccd7c1 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -5,7 +5,8 @@ from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, services_to_strings, ATTR_BATTERY_ICON, ATTR_STATUS, ATTR_FAN_SPEED) -from homeassistant.components.vacuum.mqtt import ALL_SERVICES +from homeassistant.components.vacuum.mqtt import ALL_SERVICES, \ + CONF_BATTERY_LEVEL_TOPIC, CONF_BATTERY_LEVEL_TEMPLATE from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME) from homeassistant.setup import setup_component @@ -140,6 +141,25 @@ def test_status(self): self.assertEqual(61, state.attributes.get(ATTR_BATTERY_LEVEL)) self.assertEqual('min', state.attributes.get(ATTR_FAN_SPEED)) + def test_battery_template(self): + """Tests that you can use non-default templates for battery_level.""" + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + ATTR_SUPPORTED_FEATURES: services_to_strings(ALL_SERVICES), + CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", + CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" + } + })) + + fire_mqtt_message(self.hass, 'retroroomba/battery_level', '54') + self.hass.block_till_done() + state = self.hass.states.get('vacuum.mqtttest') + self.assertEqual(54, state.attributes.get(ATTR_BATTERY_LEVEL)) + self.assertEqual(state.attributes.get(ATTR_BATTERY_ICON), + 'mdi:battery-50') + def test_status_invalid_json(self): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { From d04eac8f29abab0c94a870219a0c889bf3fb07fe Mon Sep 17 00:00:00 2001 From: John Boiles Date: Wed, 13 Sep 2017 01:00:10 -0700 Subject: [PATCH 05/15] Use configurable bools for cleaning/docked/stopped state --- homeassistant/components/vacuum/mqtt.py | 113 +++++++++++++++++------- tests/components/vacuum/test_mqtt.py | 20 +++-- 2 files changed, 95 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 9719783dbb4ba..1a3ada6d01812 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -18,6 +18,7 @@ SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VacuumDevice) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback +from homeassistant.util.icon import icon_for_battery_level _LOGGER = logging.getLogger(__name__) @@ -29,6 +30,8 @@ ALL_SERVICES = DEFAULT_SERVICES | SUPPORT_PAUSE | SUPPORT_LOCATE |\ SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND +BOOL_TRUE_STRINGS = {'true', '1', 'yes', 'on'} + CONF_PAYLOAD_TURN_ON = 'payload_turn_on' CONF_PAYLOAD_TURN_OFF = 'payload_turn_off' CONF_PAYLOAD_RETURN_TO_BASE = 'payload_return_to_base' @@ -40,6 +43,10 @@ CONF_BATTERY_LEVEL_TEMPLATE = 'battery_level_template' CONF_CHARGING_TOPIC = 'charging_topic' CONF_CHARGING_TEMPLATE = 'charging_template' +CONF_CLEANING_TOPIC = 'cleaning_topic' +CONF_CLEANING_TEMPLATE = 'cleaning_template' +CONF_DOCKED_TOPIC = 'docked_topic' +CONF_DOCKED_TEMPLATE = 'docked_template' CONF_STATE_TOPIC = 'state_topic' CONF_STATE_TEMPLATE = 'state_template' CONF_FAN_SPEED_TOPIC = 'fan_speed_topic' @@ -63,6 +70,10 @@ DEFAULT_BATTERY_LEVEL_TEMPLATE = cv.template('{{ value_json.battery_level }}') DEFAULT_CHARGING_TOPIC = 'vacuum/state' DEFAULT_CHARGING_TEMPLATE = cv.template('{{ value_json.charging }}') +DEFAULT_CLEANING_TOPIC = 'vacuum/state' +DEFAULT_CLEANING_TEMPLATE = cv.template('{{ value_json.cleaning }}') +DEFAULT_DOCKED_TOPIC = 'vacuum/state' +DEFAULT_DOCKED_TEMPLATE = cv.template('{{ value_json.docked }}') DEFAULT_STATE_TOPIC = 'vacuum/state' DEFAULT_STATE_TEMPLATE = cv.template('{{ value_json.state }}') DEFAULT_FAN_SPEED_TOPIC = 'vacuum/state' @@ -109,6 +120,20 @@ default=DEFAULT_CHARGING_TEMPLATE): cv.template, + vol.Optional(CONF_CLEANING_TOPIC, + default=DEFAULT_CLEANING_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(CONF_CLEANING_TEMPLATE, + default=DEFAULT_CLEANING_TEMPLATE): + cv.template, + + vol.Optional(CONF_DOCKED_TOPIC, + default=DEFAULT_DOCKED_TOPIC): + mqtt.valid_publish_topic, + vol.Optional(CONF_DOCKED_TEMPLATE, + default=DEFAULT_DOCKED_TEMPLATE): + cv.template, + vol.Optional(CONF_STATE_TOPIC, default=DEFAULT_STATE_TOPIC): mqtt.valid_publish_topic, @@ -162,9 +187,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): charging_template = config.get(CONF_CHARGING_TEMPLATE) charging_template.hass = hass - state_topic = config.get(mqtt.CONF_STATE_TOPIC) - state_template = config.get(CONF_STATE_TEMPLATE) - state_template.hass = hass + cleaning_topic = config.get(CONF_CLEANING_TOPIC) + cleaning_template = config.get(CONF_CLEANING_TEMPLATE) + cleaning_template.hass = hass + + docked_topic = config.get(CONF_DOCKED_TOPIC) + docked_template = config.get(CONF_DOCKED_TEMPLATE) + docked_template.hass = hass fan_speed_topic = config.get(CONF_FAN_SPEED_TOPIC) fan_speed_template = config.get(CONF_FAN_SPEED_TEMPLATE) @@ -180,9 +209,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): payload_turn_on, payload_turn_off, payload_return_to_base, payload_stop, payload_clean_spot, payload_locate, payload_start_pause, battery_level_topic, battery_level_template, - charging_topic, charging_template, state_topic, state_template, - fan_speed_topic, fan_speed_template, set_fan_speed_topic, - fan_speed_list, send_command_topic + charging_topic, charging_template, cleaning_topic, + cleaning_template, docked_topic, docked_template, fan_speed_topic, + fan_speed_template, set_fan_speed_topic, fan_speed_list, + send_command_topic ), ]) @@ -196,9 +226,10 @@ def __init__( payload_turn_on, payload_turn_off, payload_return_to_base, payload_stop, payload_clean_spot, payload_locate, payload_start_pause, battery_level_topic, battery_level_template, - charging_topic, charging_template, state_topic, state_template, - fan_speed_topic, fan_speed_template, set_fan_speed_topic, - fan_speed_list, send_command_topic): + charging_topic, charging_template, cleaning_topic, + cleaning_template, docked_topic, docked_template, fan_speed_topic, + fan_speed_template, set_fan_speed_topic, fan_speed_list, + send_command_topic): """Initialize the vacuum.""" self._name = name self._supported_features = supported_features @@ -220,8 +251,11 @@ def __init__( self._charging_topic = charging_topic self._charging_template = charging_template - self._state_topic = state_topic - self._state_template = state_template + self._cleaning_topic = cleaning_topic + self._cleaning_template = cleaning_template + + self._docked_topic = docked_topic + self._docked_template = docked_template self._fan_speed_topic = fan_speed_topic self._fan_speed_template = fan_speed_template @@ -230,8 +264,9 @@ def __init__( self._fan_speed_list = fan_speed_list self._send_command_topic = send_command_topic - self._is_cleaning = False + self._cleaning = False self._charging = False + self._docked = False self._status = 'Unknown' self._battery_level = 0 self._fan_speed = 'unknown' @@ -259,26 +294,33 @@ def message_received(topic, payload, qos): payload, error_value=None) if charging is not None: - self._charging = charging + self._charging = str(charging).lower() in BOOL_TRUE_STRINGS - if topic == self._state_topic: - state = self._state_template\ + if topic == self._cleaning_topic: + cleaning = self._cleaning_template \ .async_render_with_possible_json_value( payload, error_value=None) - if state: - if state == 'cleaning': - self._is_cleaning = True - self._status = "Cleaning" - if state == 'docked': - self._is_cleaning = False - if self._charging: - self._status = "Docked & Charging" - else: - self._status = "Docked" - if state == 'stopped': - self._is_cleaning = False - self._status = "Stopped" + if cleaning is not None: + self._cleaning = str(cleaning).lower() in BOOL_TRUE_STRINGS + + if topic == self._docked_topic: + docked = self._cleaning_template \ + .async_render_with_possible_json_value( + payload, + error_value=None) + if docked is not None: + self._docked = str(docked).lower() in BOOL_TRUE_STRINGS + + if self._docked: + if self._charging: + self._status = "Docked & Charging" + else: + self._status = "Docked" + elif self._cleaning: + self._status = "Cleaning" + else: + self._status = "Stopped" if topic == self._fan_speed_topic: fan_speed = self._fan_speed_template\ @@ -291,8 +333,8 @@ def message_received(topic, payload, qos): self.async_schedule_update_ha_state() topics_set = { - self._battery_level_topic, self._charging_topic, self._state_topic, - self._fan_speed_topic} + self._battery_level_topic, self._charging_topic, + self._cleaning_topic, self._docked_topic, self._fan_speed_topic} for topic in topics_set: yield from self.hass.components.mqtt.async_subscribe( topic, message_received, self._qos) @@ -315,7 +357,7 @@ def should_poll(self): @property def is_on(self): """Return true if vacuum is on.""" - return self._is_cleaning + return self._cleaning @property def status(self): @@ -348,6 +390,15 @@ def battery_level(self): return max(0, min(100, self._battery_level)) + @property + def battery_icon(self): + """Return the battery icon for the vacuum cleaner.""" + if self.supported_features & SUPPORT_BATTERY == 0: + return + + return icon_for_battery_level( + battery_level=self.battery_level, charging=self._charging) + @property def supported_features(self): """Flag supported features.""" diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index 8093a41ccd7c1..c5a3cab3b41ee 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -112,7 +112,8 @@ def test_status(self): message = """{ "battery_level": 54, - "state": "cleaning", + "cleaning": true, + "docked": false, "charging": false, "fan_speed": "max" }""" @@ -120,14 +121,17 @@ def test_status(self): self.hass.block_till_done() state = self.hass.states.get('vacuum.mqtttest') self.assertEqual(STATE_ON, state.state) - self.assertEqual(state.attributes.get(ATTR_BATTERY_ICON), - 'mdi:battery-50') + self.assertEqual( + 'mdi:battery-50', + state.attributes.get(ATTR_BATTERY_ICON) + ) self.assertEqual(54, state.attributes.get(ATTR_BATTERY_LEVEL)) self.assertEqual('max', state.attributes.get(ATTR_FAN_SPEED)) message = """{ "battery_level": 61, - "state": "docked", + "docked": true, + "cleaning": false, "charging": true, "fan_speed": "min" }""" @@ -136,8 +140,10 @@ def test_status(self): self.hass.block_till_done() state = self.hass.states.get('vacuum.mqtttest') self.assertEqual(STATE_OFF, state.state) - self.assertEqual(state.attributes.get(ATTR_BATTERY_ICON), - 'mdi:battery-charging-60') + self.assertEqual( + 'mdi:battery-charging-60', + state.attributes.get(ATTR_BATTERY_ICON) + ) self.assertEqual(61, state.attributes.get(ATTR_BATTERY_LEVEL)) self.assertEqual('min', state.attributes.get(ATTR_FAN_SPEED)) @@ -173,4 +179,4 @@ def test_status_invalid_json(self): self.hass.block_till_done() state = self.hass.states.get('vacuum.mqtttest') self.assertEqual(STATE_OFF, state.state) - self.assertEqual("Unknown", state.attributes.get(ATTR_STATUS)) + self.assertEqual("Stopped", state.attributes.get(ATTR_STATUS)) From 50793a39cc10e856fd27e9ebfead6ff1cd54da56 Mon Sep 17 00:00:00 2001 From: John Boiles Date: Wed, 13 Sep 2017 01:22:19 -0700 Subject: [PATCH 06/15] Fix language in docstring --- tests/components/vacuum/test_mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index c5a3cab3b41ee..cb5be9c2d7cb7 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -148,7 +148,7 @@ def test_status(self): self.assertEqual('min', state.attributes.get(ATTR_FAN_SPEED)) def test_battery_template(self): - """Tests that you can use non-default templates for battery_level.""" + """Test that you can use non-default templates for battery_level.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { vacuum.DOMAIN: { CONF_PLATFORM: 'mqtt', From 639391c5b06f1652e4795fb914621474a6afd0f5 Mon Sep 17 00:00:00 2001 From: John Boiles Date: Wed, 13 Sep 2017 08:56:56 -0700 Subject: [PATCH 07/15] PR feedback --- homeassistant/components/vacuum/__init__.py | 35 -------------- homeassistant/components/vacuum/demo.py | 51 ++++++++------------- homeassistant/components/vacuum/mqtt.py | 42 +++++++++++++++-- tests/components/vacuum/test_mqtt.py | 7 +-- 4 files changed, 62 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index a47f60c5ff77d..a3d963624336e 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -94,41 +94,6 @@ SUPPORT_MAP = 2048 -SERVICE_TO_STRING = { - SUPPORT_TURN_ON: 'turn_on', - SUPPORT_TURN_OFF: 'turn_off', - SUPPORT_PAUSE: 'pause', - SUPPORT_STOP: 'stop', - SUPPORT_RETURN_HOME: 'return_home', - SUPPORT_FAN_SPEED: 'fan_speed', - SUPPORT_BATTERY: 'battery', - SUPPORT_STATUS: 'status', - SUPPORT_SEND_COMMAND: 'send_command', - SUPPORT_LOCATE: 'locate', - SUPPORT_CLEAN_SPOT: 'clean_spot', - SUPPORT_MAP: 'map', -} - -STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} - - -def services_to_strings(services): - """Convert SUPPORT_* service bitmask to list of service strings.""" - strings = [] - for service in SERVICE_TO_STRING: - if service & services: - strings.append(SERVICE_TO_STRING[service]) - return strings - - -def strings_to_services(strings): - """Convert service strings to SUPPORT_* service bitmask.""" - services = 0 - for string in strings: - services |= STRING_TO_SERVICE[string] - return services - - @bind_hass def is_on(hass, entity_id=None): """Return if the vacuum is on based on the statemachine.""" diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index 5668072db1cdd..668e3ca37e6e0 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -import asyncio import logging from homeassistant.components.vacuum import ( @@ -37,10 +36,9 @@ DEMO_VACUUM_NONE = '4_Fourth_floor' -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo vacuums.""" - async_add_devices([ + add_devices([ DemoVacuum(DEMO_VACUUM_COMPLETE, SUPPORT_ALL_SERVICES), DemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES), DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES), @@ -123,8 +121,7 @@ def supported_features(self): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + def turn_on(self, **kwargs): """Turn the vacuum on.""" if self.supported_features & SUPPORT_TURN_ON == 0: return @@ -133,30 +130,27 @@ def async_turn_on(self, **kwargs): self._cleaned_area += 5.32 self._battery_level -= 2 self._status = 'Cleaning' - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + def turn_off(self, **kwargs): """Turn the vacuum off.""" if self.supported_features & SUPPORT_TURN_OFF == 0: return self._state = False self._status = 'Charging' - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_stop(self, **kwargs): + def stop(self, **kwargs): """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return self._state = False self._status = 'Stopping the current task' - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_clean_spot(self, **kwargs): + def clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return @@ -165,19 +159,17 @@ def async_clean_spot(self, **kwargs): self._cleaned_area += 1.32 self._battery_level -= 1 self._status = "Cleaning spot" - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_locate(self, **kwargs): + def locate(self, **kwargs): """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return self._status = "Hi, I'm over here!" - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_start_pause(self, **kwargs): + def start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" if self.supported_features & SUPPORT_PAUSE == 0: return @@ -189,20 +181,18 @@ def async_start_pause(self, **kwargs): self._battery_level -= 1 else: self._status = 'Pausing the current task' - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed, **kwargs): """Set the vacuum's fan speed.""" if self.supported_features & SUPPORT_FAN_SPEED == 0: return if fan_speed in self.fan_speed_list: self._fan_speed = fan_speed - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_return_to_base(self, **kwargs): + def return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: return @@ -210,14 +200,13 @@ def async_return_to_base(self, **kwargs): self._state = False self._status = 'Returning home...' self._battery_level += 5 - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() - @asyncio.coroutine - def async_send_command(self, command, params=None, **kwargs): + def send_command(self, command, params=None, **kwargs): """Send a command to the vacuum.""" if self.supported_features & SUPPORT_SEND_COMMAND == 0: return self._status = 'Executing {}({})'.format(command, params) self._state = True - self.async_schedule_update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 1a3ada6d01812..f95bf164de6ad 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -12,10 +12,10 @@ import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv from homeassistant.components.vacuum import ( - DEFAULT_ICON, services_to_strings, strings_to_services, STRING_TO_SERVICE, - SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, - SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, - SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VacuumDevice) + DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, + SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + VacuumDevice) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback from homeassistant.util.icon import icon_for_battery_level @@ -24,6 +24,40 @@ DEPENDENCIES = ['mqtt'] +SERVICE_TO_STRING = { + SUPPORT_TURN_ON: 'turn_on', + SUPPORT_TURN_OFF: 'turn_off', + SUPPORT_PAUSE: 'pause', + SUPPORT_STOP: 'stop', + SUPPORT_RETURN_HOME: 'return_home', + SUPPORT_FAN_SPEED: 'fan_speed', + SUPPORT_BATTERY: 'battery', + SUPPORT_STATUS: 'status', + SUPPORT_SEND_COMMAND: 'send_command', + SUPPORT_LOCATE: 'locate', + SUPPORT_CLEAN_SPOT: 'clean_spot', +} + +STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} + + +def services_to_strings(services): + """Convert SUPPORT_* service bitmask to list of service strings.""" + strings = [] + for service in SERVICE_TO_STRING: + if service & services: + strings.append(SERVICE_TO_STRING[service]) + return strings + + +def strings_to_services(strings): + """Convert service strings to SUPPORT_* service bitmask.""" + services = 0 + for string in strings: + services |= STRING_TO_SERVICE[string] + return services + + DEFAULT_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STOP |\ SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY |\ SUPPORT_CLEAN_SPOT diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index cb5be9c2d7cb7..72eea5e1530ec 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -3,10 +3,11 @@ from homeassistant.components import vacuum from homeassistant.components.vacuum import ( - ATTR_BATTERY_LEVEL, services_to_strings, ATTR_BATTERY_ICON, ATTR_STATUS, + ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, ATTR_FAN_SPEED) -from homeassistant.components.vacuum.mqtt import ALL_SERVICES, \ - CONF_BATTERY_LEVEL_TOPIC, CONF_BATTERY_LEVEL_TEMPLATE +from homeassistant.components.vacuum.mqtt import ( + ALL_SERVICES, CONF_BATTERY_LEVEL_TOPIC, CONF_BATTERY_LEVEL_TEMPLATE, + services_to_strings) from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME) from homeassistant.setup import setup_component From 0f4cefdbb81d71036613302a0265e826992cb557 Mon Sep 17 00:00:00 2001 From: John Boiles Date: Wed, 13 Sep 2017 09:17:58 -0700 Subject: [PATCH 08/15] Remove duplicate vacuum/state topic defaults --- homeassistant/components/vacuum/mqtt.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index f95bf164de6ad..66cbd683d4838 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -102,15 +102,11 @@ def strings_to_services(strings): DEFAULT_PAYLOAD_START_PAUSE = 'start_pause' DEFAULT_BATTERY_LEVEL_TOPIC = 'vacuum/state' DEFAULT_BATTERY_LEVEL_TEMPLATE = cv.template('{{ value_json.battery_level }}') -DEFAULT_CHARGING_TOPIC = 'vacuum/state' DEFAULT_CHARGING_TEMPLATE = cv.template('{{ value_json.charging }}') -DEFAULT_CLEANING_TOPIC = 'vacuum/state' DEFAULT_CLEANING_TEMPLATE = cv.template('{{ value_json.cleaning }}') -DEFAULT_DOCKED_TOPIC = 'vacuum/state' DEFAULT_DOCKED_TEMPLATE = cv.template('{{ value_json.docked }}') DEFAULT_STATE_TOPIC = 'vacuum/state' DEFAULT_STATE_TEMPLATE = cv.template('{{ value_json.state }}') -DEFAULT_FAN_SPEED_TOPIC = 'vacuum/state' DEFAULT_FAN_SPEED_TEMPLATE = cv.template('{{ value_json.fan_speed }}') DEFAULT_SET_FAN_SPEED_TOPIC = 'vacuum/set_fan_speed' DEFAULT_FAN_SPEED_LIST = ['min', 'medium', 'high', 'max'] @@ -148,21 +144,21 @@ def strings_to_services(strings): cv.template, vol.Optional(CONF_CHARGING_TOPIC, - default=DEFAULT_CHARGING_TOPIC): + default=DEFAULT_STATE_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_CHARGING_TEMPLATE, default=DEFAULT_CHARGING_TEMPLATE): cv.template, vol.Optional(CONF_CLEANING_TOPIC, - default=DEFAULT_CLEANING_TOPIC): + default=DEFAULT_STATE_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_CLEANING_TEMPLATE, default=DEFAULT_CLEANING_TEMPLATE): cv.template, vol.Optional(CONF_DOCKED_TOPIC, - default=DEFAULT_DOCKED_TOPIC): + default=DEFAULT_STATE_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_DOCKED_TEMPLATE, default=DEFAULT_DOCKED_TEMPLATE): @@ -176,7 +172,7 @@ def strings_to_services(strings): cv.template, vol.Optional(CONF_FAN_SPEED_TOPIC, - default=DEFAULT_FAN_SPEED_TOPIC): + default=DEFAULT_STATE_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_FAN_SPEED_TEMPLATE, default=DEFAULT_FAN_SPEED_TEMPLATE): From fb24fdf19af0b49328e8daa656a63f3fcd398cda Mon Sep 17 00:00:00 2001 From: John Boiles Date: Wed, 13 Sep 2017 12:32:33 -0700 Subject: [PATCH 09/15] Fix incorrect template for docked value --- homeassistant/components/vacuum/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 66cbd683d4838..fc3748c9100d0 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -335,7 +335,7 @@ def message_received(topic, payload, qos): self._cleaning = str(cleaning).lower() in BOOL_TRUE_STRINGS if topic == self._docked_topic: - docked = self._cleaning_template \ + docked = self._docked_template \ .async_render_with_possible_json_value( payload, error_value=None) From 60d9f71386aa597e4ed39b5a86022f7e66c6d412 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Sep 2017 12:17:10 +0200 Subject: [PATCH 10/15] Move direction like default mqtt platfom/components --- homeassistant/components/vacuum/mqtt.py | 88 ++++++------------------- 1 file changed, 19 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index fc3748c9100d0..4f546d8fb99d0 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -66,6 +66,7 @@ def strings_to_services(strings): BOOL_TRUE_STRINGS = {'true', '1', 'yes', 'on'} +CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES CONF_PAYLOAD_TURN_ON = 'payload_turn_on' CONF_PAYLOAD_TURN_OFF = 'payload_turn_off' CONF_PAYLOAD_RETURN_TO_BASE = 'payload_return_to_base' @@ -92,7 +93,6 @@ def strings_to_services(strings): DEFAULT_NAME = 'MQTT Vacuum' DEFAULT_RETAIN = False DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES) -DEFAULT_COMMAND_TOPIC = 'vacuum/command' DEFAULT_PAYLOAD_TURN_ON = 'turn_on' DEFAULT_PAYLOAD_TURN_OFF = 'turn_off' DEFAULT_PAYLOAD_RETURN_TO_BASE = 'return_to_base' @@ -100,27 +100,14 @@ def strings_to_services(strings): DEFAULT_PAYLOAD_CLEAN_SPOT = 'clean_spot' DEFAULT_PAYLOAD_LOCATE = 'locate' DEFAULT_PAYLOAD_START_PAUSE = 'start_pause' -DEFAULT_BATTERY_LEVEL_TOPIC = 'vacuum/state' -DEFAULT_BATTERY_LEVEL_TEMPLATE = cv.template('{{ value_json.battery_level }}') -DEFAULT_CHARGING_TEMPLATE = cv.template('{{ value_json.charging }}') -DEFAULT_CLEANING_TEMPLATE = cv.template('{{ value_json.cleaning }}') -DEFAULT_DOCKED_TEMPLATE = cv.template('{{ value_json.docked }}') -DEFAULT_STATE_TOPIC = 'vacuum/state' -DEFAULT_STATE_TEMPLATE = cv.template('{{ value_json.state }}') -DEFAULT_FAN_SPEED_TEMPLATE = cv.template('{{ value_json.fan_speed }}') -DEFAULT_SET_FAN_SPEED_TOPIC = 'vacuum/set_fan_speed' DEFAULT_FAN_SPEED_LIST = ['min', 'medium', 'high', 'max'] -DEFAULT_SEND_COMMAND_TOPIC = 'vacuum/send_command' PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(ATTR_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS): + vol.Optional(CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - - vol.Optional(mqtt.CONF_COMMAND_TOPIC, default=DEFAULT_COMMAND_TOPIC): - mqtt.valid_publish_topic, + vol.Optional(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_PAYLOAD_TURN_ON, default=DEFAULT_PAYLOAD_TURN_ON): cv.string, vol.Optional(CONF_PAYLOAD_TURN_OFF, @@ -135,58 +122,21 @@ def strings_to_services(strings): default=DEFAULT_PAYLOAD_LOCATE): cv.string, vol.Optional(CONF_PAYLOAD_START_PAUSE, default=DEFAULT_PAYLOAD_START_PAUSE): cv.string, - - vol.Optional(CONF_BATTERY_LEVEL_TOPIC, - default=DEFAULT_BATTERY_LEVEL_TOPIC): - mqtt.valid_publish_topic, - vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE, - default=DEFAULT_BATTERY_LEVEL_TEMPLATE): - cv.template, - - vol.Optional(CONF_CHARGING_TOPIC, - default=DEFAULT_STATE_TOPIC): - mqtt.valid_publish_topic, - vol.Optional(CONF_CHARGING_TEMPLATE, - default=DEFAULT_CHARGING_TEMPLATE): - cv.template, - - vol.Optional(CONF_CLEANING_TOPIC, - default=DEFAULT_STATE_TOPIC): - mqtt.valid_publish_topic, - vol.Optional(CONF_CLEANING_TEMPLATE, - default=DEFAULT_CLEANING_TEMPLATE): - cv.template, - - vol.Optional(CONF_DOCKED_TOPIC, - default=DEFAULT_STATE_TOPIC): - mqtt.valid_publish_topic, - vol.Optional(CONF_DOCKED_TEMPLATE, - default=DEFAULT_DOCKED_TEMPLATE): - cv.template, - - vol.Optional(CONF_STATE_TOPIC, - default=DEFAULT_STATE_TOPIC): - mqtt.valid_publish_topic, - vol.Optional(CONF_STATE_TEMPLATE, - default=DEFAULT_STATE_TEMPLATE): - cv.template, - - vol.Optional(CONF_FAN_SPEED_TOPIC, - default=DEFAULT_STATE_TOPIC): - mqtt.valid_publish_topic, - vol.Optional(CONF_FAN_SPEED_TEMPLATE, - default=DEFAULT_FAN_SPEED_TEMPLATE): - cv.template, - - vol.Optional(CONF_SET_FAN_SPEED_TOPIC, - default=DEFAULT_SET_FAN_SPEED_TOPIC): - mqtt.valid_publish_topic, - vol.Optional(CONF_FAN_SPEED_LIST, default=DEFAULT_FAN_SPEED_LIST): - cv.ensure_list, - - vol.Optional(CONF_SEND_COMMAND_TOPIC, - default=DEFAULT_SEND_COMMAND_TOPIC): - mqtt.valid_publish_topic, + vol.Optional(CONF_BATTERY_LEVEL_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, + vol.Optional(CONF_CHARGING_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_CHARGING_TEMPLATE): cv.template, + vol.Optional(CONF_CLEANING_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_CLEANING_TEMPLATE): cv.template, + vol.Optional(CONF_DOCKED_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_DOCKED_TEMPLATE): cv.template, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_SPEED_LIST): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, }) @@ -194,7 +144,7 @@ def strings_to_services(strings): def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the vacuum.""" name = config.get(CONF_NAME) - supported_feature_strings = config.get(ATTR_SUPPORTED_FEATURES) + supported_feature_strings = config.get(CONF_SUPPORTED_FEATURES) supported_features = strings_to_services(supported_feature_strings) qos = config.get(mqtt.CONF_QOS) From 0a17fbe8494bd1427aad1bf91a9b08a6d1bb7608 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Sep 2017 12:32:37 +0200 Subject: [PATCH 11/15] fix None on templates --- homeassistant/components/vacuum/mqtt.py | 31 +++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 4f546d8fb99d0..abc0a0c20c809 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -100,7 +100,6 @@ def strings_to_services(strings): DEFAULT_PAYLOAD_CLEAN_SPOT = 'clean_spot' DEFAULT_PAYLOAD_LOCATE = 'locate' DEFAULT_PAYLOAD_START_PAUSE = 'start_pause' -DEFAULT_FAN_SPEED_LIST = ['min', 'medium', 'high', 'max'] PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -135,7 +134,8 @@ def strings_to_services(strings): vol.Optional(CONF_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_FAN_SPEED_LIST): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_FAN_SPEED_LIST, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, }) @@ -161,23 +161,29 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): battery_level_topic = config.get(CONF_BATTERY_LEVEL_TOPIC) battery_level_template = config.get(CONF_BATTERY_LEVEL_TEMPLATE) - battery_level_template.hass = hass + if battery_level_template: + battery_level_template.hass = hass charging_topic = config.get(CONF_CHARGING_TOPIC) charging_template = config.get(CONF_CHARGING_TEMPLATE) - charging_template.hass = hass + if charging_template: + charging_template.hass = hass cleaning_topic = config.get(CONF_CLEANING_TOPIC) cleaning_template = config.get(CONF_CLEANING_TEMPLATE) - cleaning_template.hass = hass + if cleaning_template: + cleaning_template.hass = hass docked_topic = config.get(CONF_DOCKED_TOPIC) docked_template = config.get(CONF_DOCKED_TEMPLATE) - docked_template.hass = hass + if docked_template: + docked_template.hass = hass fan_speed_topic = config.get(CONF_FAN_SPEED_TOPIC) fan_speed_template = config.get(CONF_FAN_SPEED_TEMPLATE) - fan_speed_template.hass = hass + if fan_speed_template: + fan_speed_template.hass = hass + set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) fan_speed_list = config.get(CONF_FAN_SPEED_LIST) @@ -260,7 +266,8 @@ def async_added_to_hass(self): @callback def message_received(topic, payload, qos): """Handle new MQTT message.""" - if topic == self._battery_level_topic: + if topic == self._battery_level_topic and \ + self._battery_level_template: battery_level = self._battery_level_template\ .async_render_with_possible_json_value( payload, @@ -268,7 +275,7 @@ def message_received(topic, payload, qos): if battery_level is not None: self._battery_level = int(battery_level) - if topic == self._charging_topic: + if topic == self._charging_topic and self._charging_template: charging = self._charging_template\ .async_render_with_possible_json_value( payload, @@ -276,7 +283,7 @@ def message_received(topic, payload, qos): if charging is not None: self._charging = str(charging).lower() in BOOL_TRUE_STRINGS - if topic == self._cleaning_topic: + if topic == self._cleaning_topic and self._cleaning_template: cleaning = self._cleaning_template \ .async_render_with_possible_json_value( payload, @@ -284,7 +291,7 @@ def message_received(topic, payload, qos): if cleaning is not None: self._cleaning = str(cleaning).lower() in BOOL_TRUE_STRINGS - if topic == self._docked_topic: + if topic == self._docked_topic and self._docked_template: docked = self._docked_template \ .async_render_with_possible_json_value( payload, @@ -302,7 +309,7 @@ def message_received(topic, payload, qos): else: self._status = "Stopped" - if topic == self._fan_speed_topic: + if topic == self._fan_speed_topic and self._fan_speed_template: fan_speed = self._fan_speed_template\ .async_render_with_possible_json_value( payload, From cf94712544363402e415a924fac492ae7093c3cc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Sep 2017 14:34:31 +0200 Subject: [PATCH 12/15] fix tests --- tests/components/vacuum/test_mqtt.py | 70 +++++++++++++++------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index 72eea5e1530ec..f341f48c60a50 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -4,12 +4,8 @@ from homeassistant.components import vacuum from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, - ATTR_FAN_SPEED) -from homeassistant.components.vacuum.mqtt import ( - ALL_SERVICES, CONF_BATTERY_LEVEL_TOPIC, CONF_BATTERY_LEVEL_TEMPLATE, - services_to_strings) -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME) + ATTR_FAN_SPEED, mqtt) +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME from homeassistant.setup import setup_component from tests.common import ( fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) @@ -23,6 +19,22 @@ def setUp(self): # pylint: disable=invalid-name self.hass = get_test_home_assistant() self.mock_publish = mock_mqtt_component(self.hass) + self.default_config = { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + mqtt.CONF_COMMAND_TOPIC = 'vacuum/command' + mqtt.CONF_SEND_COMMAND_TOPIC = 'vacuum/send_command' + mqtt.CONF_BATTERY_LEVEL_TOPIC = 'vacuum/state' + mqtt.CONF_BATTERY_LEVEL_TEMPLATE = '{{ value_json.battery_level }}' + mqtt.CONF_CHARGING_TEMPLATE = '{{ value_json.charging }}' + mqtt.CONF_CLEANING_TEMPLATE = '{{ value_json.cleaning }}' + mqtt.CONF_DOCKED_TEMPLATE = '{{ value_json.docked }}' + mqtt.CONF_STATE_TOPIC = 'vacuum/state' + mqtt.CONF_STATE_TEMPLATE = '{{ value_json.state }}' + mqtt.CONF_FAN_SPEED_TEMPLATE = '{{ value_json.fan_speed }}' + mqtt.CONF_SET_FAN_SPEED_TOPIC = 'vacuum/set_fan_speed' + } + def tearDown(self): # pylint: disable=invalid-name """Stop down everything that was started.""" self.hass.stop() @@ -30,14 +42,12 @@ def tearDown(self): # pylint: disable=invalid-name def test_default_supported_features(self): """Test that the correct supported features.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: { - CONF_PLATFORM: 'mqtt', - CONF_NAME: 'mqtttest', - } + vacuum.DOMAIN: self.default_config, })) entity = self.hass.states.get('vacuum.mqtttest') - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - self.assertListEqual(sorted(services_to_strings(entity_features)), + entity_features = \ + entity.attributes.get(mqtt.CONF_SUPPORTED_FEATURES, 0) + self.assertListEqual(sorted(mqtt.services_to_strings(entity_features)), sorted(['turn_on', 'turn_off', 'stop', 'return_home', 'battery', 'status', 'clean_spot'])) @@ -45,11 +55,10 @@ def test_default_supported_features(self): def test_all_commands(self): """Test simple commands to the vacuum.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: { - CONF_PLATFORM: 'mqtt', - CONF_NAME: 'mqtttest', - ATTR_SUPPORTED_FEATURES: services_to_strings(ALL_SERVICES), - } + vacuum.DOMAIN: self.default_config.update({ + mqtt.CONF_SUPPORTED_FEATURES: + mqtt.services_to_strings(mqtt.ALL_SERVICES), + }) })) vacuum.turn_on(self.hass, 'vacuum.mqtttest') @@ -104,11 +113,10 @@ def test_all_commands(self): def test_status(self): """Test status updates from the vacuum.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: { - CONF_PLATFORM: 'mqtt', - CONF_NAME: 'mqtttest', - ATTR_SUPPORTED_FEATURES: services_to_strings(ALL_SERVICES), - } + vacuum.DOMAIN: self.default_config.update({ + mqtt.CONF_SUPPORTED_FEATURES: + mqtt.services_to_strings(mqtt.ALL_SERVICES), + }) })) message = """{ @@ -151,13 +159,12 @@ def test_status(self): def test_battery_template(self): """Test that you can use non-default templates for battery_level.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: { - CONF_PLATFORM: 'mqtt', - CONF_NAME: 'mqtttest', - ATTR_SUPPORTED_FEATURES: services_to_strings(ALL_SERVICES), - CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", - CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" - } + vacuum.DOMAIN: self.default_config.update({ + mqtt.CONF_SUPPORTED_FEATURES: + mqtt.services_to_strings(mqtt.ALL_SERVICES), + mqtt.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", + mqtt.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" + }) })) fire_mqtt_message(self.hass, 'retroroomba/battery_level', '54') @@ -170,10 +177,7 @@ def test_battery_template(self): def test_status_invalid_json(self): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: { - CONF_PLATFORM: 'mqtt', - CONF_NAME: 'mqtttest', - } + vacuum.DOMAIN: self.default_config, })) fire_mqtt_message(self.hass, 'vacuum/state', '{"asdfasas false}') From bbe2b4b68757c183a46d0239971f4f9347ce74f6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Sep 2017 14:41:09 +0200 Subject: [PATCH 13/15] fix int --- tests/components/vacuum/test_mqtt.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index f341f48c60a50..b204cd0c5bdb5 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -22,17 +22,18 @@ def setUp(self): # pylint: disable=invalid-name self.default_config = { CONF_PLATFORM: 'mqtt', CONF_NAME: 'mqtttest', - mqtt.CONF_COMMAND_TOPIC = 'vacuum/command' - mqtt.CONF_SEND_COMMAND_TOPIC = 'vacuum/send_command' - mqtt.CONF_BATTERY_LEVEL_TOPIC = 'vacuum/state' - mqtt.CONF_BATTERY_LEVEL_TEMPLATE = '{{ value_json.battery_level }}' - mqtt.CONF_CHARGING_TEMPLATE = '{{ value_json.charging }}' - mqtt.CONF_CLEANING_TEMPLATE = '{{ value_json.cleaning }}' - mqtt.CONF_DOCKED_TEMPLATE = '{{ value_json.docked }}' - mqtt.CONF_STATE_TOPIC = 'vacuum/state' - mqtt.CONF_STATE_TEMPLATE = '{{ value_json.state }}' - mqtt.CONF_FAN_SPEED_TEMPLATE = '{{ value_json.fan_speed }}' - mqtt.CONF_SET_FAN_SPEED_TOPIC = 'vacuum/set_fan_speed' + mqtt.CONF_COMMAND_TOPIC: 'vacuum/command', + mqtt.CONF_SEND_COMMAND_TOPIC: 'vacuum/send_command', + mqtt.CONF_BATTERY_LEVEL_TOPIC: 'vacuum/state', + mqtt.CONF_BATTERY_LEVEL_TEMPLATE: + '{{ value_json.battery_level }}', + mqtt.CONF_CHARGING_TEMPLATE: '{{ value_json.charging }}', + mqtt.CONF_CLEANING_TEMPLATE: '{{ value_json.cleaning }}', + mqtt.CONF_DOCKED_TEMPLATE: '{{ value_json.docked }}', + mqtt.CONF_STATE_TOPIC: 'vacuum/state', + mqtt.CONF_STATE_TEMPLATE: '{{ value_json.state }}', + mqtt.CONF_FAN_SPEED_TEMPLATE: '{{ value_json.fan_speed }}', + mqtt.CONF_SET_FAN_SPEED_TOPIC: 'vacuum/set_fan_speed', } def tearDown(self): # pylint: disable=invalid-name From 15c74e9f8063049c417da455f4347aa82b6f0fda Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Sep 2017 15:19:16 +0200 Subject: [PATCH 14/15] fix tests --- homeassistant/components/vacuum/mqtt.py | 8 ++++-- tests/components/vacuum/test_mqtt.py | 37 +++++++++++++++---------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index abc0a0c20c809..853c50369a256 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -319,9 +319,11 @@ def message_received(topic, payload, qos): self.async_schedule_update_ha_state() - topics_set = { - self._battery_level_topic, self._charging_topic, - self._cleaning_topic, self._docked_topic, self._fan_speed_topic} + topics_set = [topic for topic in (self._battery_level_topic, + self._charging_topic, + self._cleaning_topic, + self._docked_topic, + self._fan_speed_topic) if topic] for topic in topics_set: yield from self.hass.components.mqtt.async_subscribe( topic, message_received, self._qos) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index b204cd0c5bdb5..513c28c23a1c0 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -5,6 +5,7 @@ from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, ATTR_FAN_SPEED, mqtt) +from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, CONF_NAME from homeassistant.setup import setup_component from tests.common import ( @@ -22,7 +23,7 @@ def setUp(self): # pylint: disable=invalid-name self.default_config = { CONF_PLATFORM: 'mqtt', CONF_NAME: 'mqtttest', - mqtt.CONF_COMMAND_TOPIC: 'vacuum/command', + CONF_COMMAND_TOPIC: 'vacuum/command', mqtt.CONF_SEND_COMMAND_TOPIC: 'vacuum/send_command', mqtt.CONF_BATTERY_LEVEL_TOPIC: 'vacuum/state', mqtt.CONF_BATTERY_LEVEL_TEMPLATE: @@ -34,6 +35,7 @@ def setUp(self): # pylint: disable=invalid-name mqtt.CONF_STATE_TEMPLATE: '{{ value_json.state }}', mqtt.CONF_FAN_SPEED_TEMPLATE: '{{ value_json.fan_speed }}', mqtt.CONF_SET_FAN_SPEED_TOPIC: 'vacuum/set_fan_speed', + mqtt.CONF_FAN_SPEED_LIST: ['min', 'medium', 'high', 'max'], } def tearDown(self): # pylint: disable=invalid-name @@ -55,11 +57,11 @@ def test_default_supported_features(self): def test_all_commands(self): """Test simple commands to the vacuum.""" + self.default_config[mqtt.CONF_SUPPORTED_FEATURES] = \ + mqtt.services_to_strings(mqtt.ALL_SERVICES) + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config.update({ - mqtt.CONF_SUPPORTED_FEATURES: - mqtt.services_to_strings(mqtt.ALL_SERVICES), - }) + vacuum.DOMAIN: self.default_config, })) vacuum.turn_on(self.hass, 'vacuum.mqtttest') @@ -113,11 +115,11 @@ def test_all_commands(self): def test_status(self): """Test status updates from the vacuum.""" + self.default_config[mqtt.CONF_SUPPORTED_FEATURES] = \ + mqtt.services_to_strings(mqtt.ALL_SERVICES) + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config.update({ - mqtt.CONF_SUPPORTED_FEATURES: - mqtt.services_to_strings(mqtt.ALL_SERVICES), - }) + vacuum.DOMAIN: self.default_config, })) message = """{ @@ -159,13 +161,15 @@ def test_status(self): def test_battery_template(self): """Test that you can use non-default templates for battery_level.""" + self.default_config.update({ + mqtt.CONF_SUPPORTED_FEATURES: + mqtt.services_to_strings(mqtt.ALL_SERVICES), + mqtt.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", + mqtt.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" + }) + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config.update({ - mqtt.CONF_SUPPORTED_FEATURES: - mqtt.services_to_strings(mqtt.ALL_SERVICES), - mqtt.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", - mqtt.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" - }) + vacuum.DOMAIN: self.default_config, })) fire_mqtt_message(self.hass, 'retroroomba/battery_level', '54') @@ -177,6 +181,9 @@ def test_battery_template(self): def test_status_invalid_json(self): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" + self.default_config[mqtt.CONF_SUPPORTED_FEATURES] = \ + mqtt.services_to_strings(mqtt.ALL_SERVICES) + self.assertTrue(setup_component(self.hass, vacuum.DOMAIN, { vacuum.DOMAIN: self.default_config, })) From 7fd1c10dac6923d636d1ad06794403a2d93eb0b6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 15 Sep 2017 15:22:46 +0200 Subject: [PATCH 15/15] ready to merge --- tests/components/vacuum/test_mqtt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index 513c28c23a1c0..f4c63d6370804 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -28,11 +28,15 @@ def setUp(self): # pylint: disable=invalid-name mqtt.CONF_BATTERY_LEVEL_TOPIC: 'vacuum/state', mqtt.CONF_BATTERY_LEVEL_TEMPLATE: '{{ value_json.battery_level }}', + mqtt.CONF_CHARGING_TOPIC: 'vacuum/state', mqtt.CONF_CHARGING_TEMPLATE: '{{ value_json.charging }}', + mqtt.CONF_CLEANING_TOPIC: 'vacuum/state', mqtt.CONF_CLEANING_TEMPLATE: '{{ value_json.cleaning }}', + mqtt.CONF_DOCKED_TOPIC: 'vacuum/state', mqtt.CONF_DOCKED_TEMPLATE: '{{ value_json.docked }}', mqtt.CONF_STATE_TOPIC: 'vacuum/state', mqtt.CONF_STATE_TEMPLATE: '{{ value_json.state }}', + mqtt.CONF_FAN_SPEED_TOPIC: 'vacuum/state', mqtt.CONF_FAN_SPEED_TEMPLATE: '{{ value_json.fan_speed }}', mqtt.CONF_SET_FAN_SPEED_TOPIC: 'vacuum/set_fan_speed', mqtt.CONF_FAN_SPEED_LIST: ['min', 'medium', 'high', 'max'],