diff --git a/.coveragerc b/.coveragerc index 274d6260078..0ed94e62199 100644 --- a/.coveragerc +++ b/.coveragerc @@ -582,6 +582,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/demo.py b/homeassistant/components/vacuum/demo.py index 54415b59db0..668e3ca37e6 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -142,7 +142,7 @@ class DemoVacuum(VacuumDevice): self.schedule_update_ha_state() def stop(self, **kwargs): - """Turn the vacuum off.""" + """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return @@ -162,7 +162,7 @@ class DemoVacuum(VacuumDevice): self.schedule_update_ha_state() def locate(self, **kwargs): - """Turn the vacuum off.""" + """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return @@ -184,7 +184,7 @@ class DemoVacuum(VacuumDevice): self.schedule_update_ha_state() def set_fan_speed(self, fan_speed, **kwargs): - """Tell the vacuum to return to its dock.""" + """Set the vacuum's fan speed.""" if self.supported_features & SUPPORT_FAN_SPEED == 0: return diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py new file mode 100644 index 00000000000..853c50369a2 --- /dev/null +++ b/homeassistant/components/vacuum/mqtt.py @@ -0,0 +1,498 @@ +""" +Support for a generic MQTT vacuum. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/vacuum.mqtt/ +""" +import asyncio +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_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 + +_LOGGER = logging.getLogger(__name__) + +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 +ALL_SERVICES = DEFAULT_SERVICES | SUPPORT_PAUSE | SUPPORT_LOCATE |\ + SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND + +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' +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_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' +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 +DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES) +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' + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + 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): mqtt.valid_publish_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): 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, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, +}) + + +@asyncio.coroutine +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(CONF_SUPPORTED_FEATURES) + supported_features = strings_to_services(supported_feature_strings) + + qos = config.get(mqtt.CONF_QOS) + 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) + if battery_level_template: + battery_level_template.hass = hass + + charging_topic = config.get(CONF_CHARGING_TOPIC) + charging_template = config.get(CONF_CHARGING_TEMPLATE) + if charging_template: + charging_template.hass = hass + + cleaning_topic = config.get(CONF_CLEANING_TOPIC) + cleaning_template = config.get(CONF_CLEANING_TEMPLATE) + if cleaning_template: + cleaning_template.hass = hass + + docked_topic = config.get(CONF_DOCKED_TOPIC) + docked_template = config.get(CONF_DOCKED_TEMPLATE) + 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) + 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) + + 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, cleaning_topic, + cleaning_template, docked_topic, docked_template, fan_speed_topic, + fan_speed_template, set_fan_speed_topic, fan_speed_list, + send_command_topic + ), + ]) + + +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, 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 + 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._charging_topic = charging_topic + self._charging_template = charging_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 + + self._set_fan_speed_topic = set_fan_speed_topic + self._fan_speed_list = fan_speed_list + self._send_command_topic = send_command_topic + + self._cleaning = False + self._charging = False + self._docked = False + self._status = 'Unknown' + self._battery_level = 0 + self._fan_speed = 'unknown' + + @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.""" + if topic == self._battery_level_topic and \ + self._battery_level_template: + 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 and self._charging_template: + charging = self._charging_template\ + .async_render_with_possible_json_value( + payload, + error_value=None) + if charging is not None: + self._charging = str(charging).lower() in BOOL_TRUE_STRINGS + + if topic == self._cleaning_topic and self._cleaning_template: + cleaning = self._cleaning_template \ + .async_render_with_possible_json_value( + payload, + error_value=None) + if cleaning is not None: + self._cleaning = str(cleaning).lower() in BOOL_TRUE_STRINGS + + if topic == self._docked_topic and self._docked_template: + docked = self._docked_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 and self._fan_speed_template: + 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() + + 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) + + @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._cleaning + + @property + def status(self): + """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.""" + if self.supported_features & SUPPORT_BATTERY == 0: + return + + 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.""" + return self._supported_features + + @asyncio.coroutine + def async_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.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_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.async_schedule_update_ha_state() + + @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.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_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.async_schedule_update_ha_state() + + @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.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_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.async_schedule_update_ha_state() + + @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 + + mqtt.async_publish(self.hass, self._command_topic, + self._payload_return_to_base, self._qos, + 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 new file mode 100644 index 00000000000..f4c63d63708 --- /dev/null +++ b/tests/components/vacuum/test_mqtt.py @@ -0,0 +1,199 @@ +"""The tests for the Demo vacuum platform.""" +import unittest + +from homeassistant.components import vacuum +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 ( + fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) + + +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) + + self.default_config = { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + 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_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'], + } + + 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: self.default_config, + })) + entity = self.hass.states.get('vacuum.mqtttest') + 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'])) + + 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, + })) + + 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]) + + 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.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, + })) + + message = """{ + "battery_level": 54, + "cleaning": true, + "docked": false, + "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') + self.assertEqual(STATE_ON, state.state) + 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, + "docked": true, + "cleaning": false, + "charging": true, + "fan_speed": "min" + }""" + + 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( + '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)) + + 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, + })) + + 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.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, + })) + + 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("Stopped", state.attributes.get(ATTR_STATUS))