From ccbc231d3ae42adf05e1fa1612e96142b18dd090 Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Sun, 6 Jan 2019 17:05:04 +0100 Subject: [PATCH] [2/3] vacuum mqtt-discovery (#19478) * add discoverability to mqtt-vacuum --- homeassistant/components/mqtt/discovery.py | 18 +- homeassistant/components/vacuum/__init__.py | 12 +- homeassistant/components/vacuum/mqtt.py | 47 +- tests/components/vacuum/common.py | 121 ++++- tests/components/vacuum/test_mqtt.py | 492 ++++++++++++-------- 5 files changed, 464 insertions(+), 226 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 92e7ffdaf4f..790007dc0e8 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -25,7 +25,7 @@ TOPIC_MATCHER = re.compile( SUPPORTED_COMPONENTS = [ 'binary_sensor', 'camera', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock', 'climate', - 'alarm_control_panel'] + 'alarm_control_panel', 'vacuum'] CONFIG_ENTRY_COMPONENTS = [ 'binary_sensor', @@ -38,6 +38,7 @@ CONFIG_ENTRY_COMPONENTS = [ 'climate', 'alarm_control_panel', 'fan', + 'vacuum', ] DEPRECATED_PLATFORM_TO_SCHEMA = { @@ -69,12 +70,25 @@ ABBREVIATIONS = { 'bri_stat_t': 'brightness_state_topic', 'bri_val_tpl': 'brightness_value_template', 'clr_temp_cmd_tpl': 'color_temp_command_template', + 'bat_lev_t': 'battery_level_topic', + 'bat_lev_tpl': 'battery_level_template', + 'chrg_t': 'charging_topic', + 'chrg_tpl': 'charging_template', 'clr_temp_cmd_t': 'color_temp_command_topic', 'clr_temp_stat_t': 'color_temp_state_topic', 'clr_temp_val_tpl': 'color_temp_value_template', + 'cln_t': 'cleaning_topic', + 'cln_tpl': 'cleaning_template', 'cmd_t': 'command_topic', 'curr_temp_t': 'current_temperature_topic', 'dev_cla': 'device_class', + 'dock_t': 'docked_topic', + 'dock_tpl': 'docked_template', + 'err_t': 'error_topic', + 'err_tpl': 'error_template', + 'fanspd_t': 'fan_speed_topic', + 'fanspd_tpl': 'fan_speed_template', + 'fanspd_lst': 'fan_speed_list', 'fx_cmd_t': 'effect_command_topic', 'fx_list': 'effect_list', 'fx_stat_t': 'effect_state_topic', @@ -124,6 +138,7 @@ ABBREVIATIONS = { 'rgb_cmd_t': 'rgb_command_topic', 'rgb_stat_t': 'rgb_state_topic', 'rgb_val_tpl': 'rgb_value_template', + 'send_cmd_t': 'send_command_topic', 'send_if_off': 'send_if_off', 'set_pos_tpl': 'set_position_template', 'set_pos_t': 'set_position_topic', @@ -137,6 +152,7 @@ ABBREVIATIONS = { 'stat_open': 'state_open', 'stat_t': 'state_topic', 'stat_val_tpl': 'state_value_template', + 'sup_feat': 'supported_features', 'swing_mode_cmd_t': 'swing_mode_command_topic', 'swing_mode_stat_tpl': 'swing_mode_state_template', 'swing_mode_stat_t': 'swing_mode_state_topic', diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 4799e945be0..6341c9661ed 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -95,7 +95,7 @@ def is_on(hass, entity_id=None): async def async_setup(hass, config): """Set up the vacuum component.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS) await component.async_setup(config) @@ -152,6 +152,16 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class _BaseVacuum(Entity): """Representation of a base vacuum. diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index ac15f4ce048..eeed7090ebf 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -10,15 +10,18 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import ( - MqttAvailability, subscription) + ATTR_DISCOVERY_HASH, MqttAvailability, MqttDiscoveryUpdate, + subscription) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.vacuum import ( 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) + VacuumDevice, DOMAIN) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.icon import icon_for_battery_level _LOGGER = logging.getLogger(__name__) @@ -145,11 +148,30 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the vacuum.""" - async_add_entities([MqttVacuum(config, discovery_info)]) + """Set up MQTT vacuum through configuration.yaml.""" + await _async_setup_entity(config, async_add_entities, + discovery_info) -class MqttVacuum(MqttAvailability, VacuumDevice): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT vacuum dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT vacuum.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(DOMAIN, 'mqtt'), async_discover) + + +async def _async_setup_entity(config, async_add_entities, + discovery_hash=None): + """Set up the MQTT vacuum.""" + async_add_entities([MqttVacuum(config, discovery_hash)]) + + +class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" def __init__(self, config, discovery_info): @@ -174,6 +196,8 @@ class MqttVacuum(MqttAvailability, VacuumDevice): MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_info, + self.discovery_update) def _setup_from_config(self, config): self._name = config.get(CONF_NAME) @@ -221,11 +245,24 @@ class MqttVacuum(MqttAvailability, VacuumDevice): ) } + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA(discovery_payload) + self._setup_from_config(config) + await self.availability_discovery_update(config) + await self._subscribe_topics() + self.async_schedule_update_ha_state() + async def async_added_to_hass(self): """Subscribe MQTT events.""" await super().async_added_to_hass() await self._subscribe_topics() + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + await subscription.async_unsubscribe_topics(self.hass, self._sub_state) + await MqttAvailability.async_will_remove_from_hass(self) + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 436f23f5546..62a0e429c0a 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -10,92 +10,189 @@ from homeassistant.components.vacuum import ( from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass def turn_on(hass, entity_id=None): + """Turn all or specified vacuum on.""" + hass.add_job(async_turn_on, hass, entity_id) + + +@callback +@bind_hass +def async_turn_on(hass, entity_id=None): """Turn all or specified vacuum on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data)) @bind_hass def turn_off(hass, entity_id=None): + """Turn all or specified vacuum off.""" + hass.add_job(async_turn_off, hass, entity_id) + + +@callback +@bind_hass +def async_turn_off(hass, entity_id=None): """Turn all or specified vacuum off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data)) @bind_hass def toggle(hass, entity_id=None): + """Toggle all or specified vacuum.""" + hass.add_job(async_toggle, hass, entity_id) + + +@callback +@bind_hass +def async_toggle(hass, entity_id=None): """Toggle all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, data)) @bind_hass def locate(hass, entity_id=None): + """Locate all or specified vacuum.""" + hass.add_job(async_locate, hass, entity_id) + + +@callback +@bind_hass +def async_locate(hass, entity_id=None): """Locate all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_LOCATE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_LOCATE, data)) @bind_hass def clean_spot(hass, entity_id=None): + """Tell all or specified vacuum to perform a spot clean-up.""" + hass.add_job(async_clean_spot, hass, entity_id) + + +@callback +@bind_hass +def async_clean_spot(hass, entity_id=None): """Tell all or specified vacuum to perform a spot clean-up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLEAN_SPOT, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, data)) @bind_hass def return_to_base(hass, entity_id=None): + """Tell all or specified vacuum to return to base.""" + hass.add_job(async_return_to_base, hass, entity_id) + + +@callback +@bind_hass +def async_return_to_base(hass, entity_id=None): """Tell all or specified vacuum to return to base.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_RETURN_TO_BASE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, data)) @bind_hass def start_pause(hass, entity_id=None): + """Tell all or specified vacuum to start or pause the current task.""" + hass.add_job(async_start_pause, hass, entity_id) + + +@callback +@bind_hass +def async_start_pause(hass, entity_id=None): """Tell all or specified vacuum to start or pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_START_PAUSE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_START_PAUSE, data)) @bind_hass def start(hass, entity_id=None): + """Tell all or specified vacuum to start or resume the current task.""" + hass.add_job(async_start, hass, entity_id) + + +@callback +@bind_hass +def async_start(hass, entity_id=None): """Tell all or specified vacuum to start or resume the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_START, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_START, data)) @bind_hass def pause(hass, entity_id=None): + """Tell all or the specified vacuum to pause the current task.""" + hass.add_job(async_pause, hass, entity_id) + + +@callback +@bind_hass +def async_pause(hass, entity_id=None): """Tell all or the specified vacuum to pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_PAUSE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_PAUSE, data)) @bind_hass def stop(hass, entity_id=None): + """Stop all or specified vacuum.""" + hass.add_job(async_stop, hass, entity_id) + + +@callback +@bind_hass +def async_stop(hass, entity_id=None): """Stop all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_STOP, data)) @bind_hass def set_fan_speed(hass, fan_speed, entity_id=None): + """Set fan speed for all or specified vacuum.""" + hass.add_job(async_set_fan_speed, hass, fan_speed, entity_id) + + +@callback +@bind_hass +def async_set_fan_speed(hass, fan_speed, entity_id=None): """Set fan speed for all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FAN_SPEED] = fan_speed - hass.services.call(DOMAIN, SERVICE_SET_FAN_SPEED, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_SET_FAN_SPEED, data)) @bind_hass def send_command(hass, command, params=None, entity_id=None): + """Send command to all or specified vacuum.""" + hass.add_job(async_send_command, hass, command, params, entity_id) + + +@callback +@bind_hass +def async_send_command(hass, command, params=None, entity_id=None): """Send command to all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_COMMAND] = command if params is not None: data[ATTR_PARAMS] = params - hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_SEND_COMMAND, data)) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index ba2c1866807..3df6235f85c 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -1,254 +1,332 @@ -"""The tests for the Demo vacuum platform.""" -import unittest +"""The tests for the Mqtt vacuum platform.""" +import pytest -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.setup import async_setup_component from homeassistant.const import ( CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, CONF_NAME) -from homeassistant.setup import setup_component - +from homeassistant.components import vacuum, mqtt +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, + ATTR_FAN_SPEED, mqtt as mqttvacuum) +from homeassistant.components.mqtt import CONF_COMMAND_TOPIC +from homeassistant.components.mqtt.discovery import async_start from tests.common import ( - fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) + async_mock_mqtt_component, + async_fire_mqtt_message, MockConfigEntry) from tests.components.vacuum import common -class TestVacuumMQTT(unittest.TestCase): - """MQTT vacuum component test class.""" +default_config = { + CONF_PLATFORM: 'mqtt', + CONF_NAME: 'mqtttest', + CONF_COMMAND_TOPIC: 'vacuum/command', + mqttvacuum.CONF_SEND_COMMAND_TOPIC: 'vacuum/send_command', + mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: 'vacuum/state', + mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: + '{{ value_json.battery_level }}', + mqttvacuum.CONF_CHARGING_TOPIC: 'vacuum/state', + mqttvacuum.CONF_CHARGING_TEMPLATE: '{{ value_json.charging }}', + mqttvacuum.CONF_CLEANING_TOPIC: 'vacuum/state', + mqttvacuum.CONF_CLEANING_TEMPLATE: '{{ value_json.cleaning }}', + mqttvacuum.CONF_DOCKED_TOPIC: 'vacuum/state', + mqttvacuum.CONF_DOCKED_TEMPLATE: '{{ value_json.docked }}', + mqttvacuum.CONF_STATE_TOPIC: 'vacuum/state', + mqttvacuum.CONF_STATE_TEMPLATE: '{{ value_json.state }}', + mqttvacuum.CONF_FAN_SPEED_TOPIC: 'vacuum/state', + mqttvacuum.CONF_FAN_SPEED_TEMPLATE: '{{ value_json.fan_speed }}', + mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: 'vacuum/set_fan_speed', + mqttvacuum.CONF_FAN_SPEED_LIST: ['min', 'medium', 'high', 'max'], +} - 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'], - } +@pytest.fixture +def mock_publish(hass): + """Initialize components.""" + yield hass.loop.run_until_complete(async_mock_mqtt_component(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.""" - assert 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) - assert sorted(mqtt.services_to_strings(entity_features)) == \ - sorted(['turn_on', 'turn_off', 'stop', - 'return_home', 'battery', 'status', - 'clean_spot']) +async def test_default_supported_features(hass, mock_publish): + """Test that the correct supported features.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) + entity = hass.states.get('vacuum.mqtttest') + entity_features = \ + entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) + assert sorted(mqttvacuum.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) - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) +async def test_all_commands(hass, mock_publish): + """Test simple commands to the vacuum.""" + default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) - common.turn_on(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'turn_on', 0, False) - self.mock_publish.async_publish.reset_mock() + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - common.turn_off(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'turn_off', 0, False) - self.mock_publish.async_publish.reset_mock() + common.turn_on(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'turn_on', 0, False) + mock_publish.async_publish.reset_mock() - common.stop(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'stop', 0, False) - self.mock_publish.async_publish.reset_mock() + common.turn_off(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'turn_off', 0, False) + mock_publish.async_publish.reset_mock() - common.clean_spot(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'clean_spot', 0, False) - self.mock_publish.async_publish.reset_mock() + common.stop(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'stop', 0, False) + mock_publish.async_publish.reset_mock() - common.locate(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'locate', 0, False) - self.mock_publish.async_publish.reset_mock() + common.clean_spot(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'clean_spot', 0, False) + mock_publish.async_publish.reset_mock() - common.start_pause(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'start_pause', 0, False) - self.mock_publish.async_publish.reset_mock() + common.locate(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'locate', 0, False) + mock_publish.async_publish.reset_mock() - common.return_to_base(self.hass, 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/command', 'return_to_base', 0, False) - self.mock_publish.async_publish.reset_mock() + common.start_pause(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'start_pause', 0, False) + mock_publish.async_publish.reset_mock() - common.set_fan_speed(self.hass, 'high', 'vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/set_fan_speed', 'high', 0, False) - self.mock_publish.async_publish.reset_mock() + common.return_to_base(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/command', 'return_to_base', 0, False) + mock_publish.async_publish.reset_mock() - common.send_command(self.hass, '44 FE 93', entity_id='vacuum.mqtttest') - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'vacuum/send_command', '44 FE 93', 0, False) + common.set_fan_speed(hass, 'high', 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/set_fan_speed', 'high', 0, False) + mock_publish.async_publish.reset_mock() - def test_status(self): - """Test status updates from the vacuum.""" - self.default_config[mqtt.CONF_SUPPORTED_FEATURES] = \ - mqtt.services_to_strings(mqtt.ALL_SERVICES) + common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_publish.async_publish.assert_called_once_with( + 'vacuum/send_command', '44 FE 93', 0, False) - assert 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') - assert STATE_ON == state.state - assert 'mdi:battery-50' == \ - state.attributes.get(ATTR_BATTERY_ICON) - assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert 'max' == state.attributes.get(ATTR_FAN_SPEED) +async def test_status(hass, mock_publish): + """Test status updates from the vacuum.""" + default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) - message = """{ - "battery_level": 61, - "docked": true, - "cleaning": false, - "charging": true, - "fan_speed": "min" - }""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - fire_mqtt_message(self.hass, 'vacuum/state', message) - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state - assert 'mdi:battery-charging-60' == \ - state.attributes.get(ATTR_BATTERY_ICON) - assert 61 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert 'min' == state.attributes.get(ATTR_FAN_SPEED) + message = """{ + "battery_level": 54, + "cleaning": true, + "docked": false, + "charging": false, + "fan_speed": "max" + }""" + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert STATE_ON == state.state + assert 'mdi:battery-50' == \ + state.attributes.get(ATTR_BATTERY_ICON) + assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) + assert 'max' == 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 }}" - }) + message = """{ + "battery_level": 61, + "docked": true, + "cleaning": false, + "charging": true, + "fan_speed": "min" + }""" - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert STATE_OFF == state.state + assert 'mdi:battery-charging-60' == \ + state.attributes.get(ATTR_BATTERY_ICON) + assert 61 == state.attributes.get(ATTR_BATTERY_LEVEL) + assert 'min' == state.attributes.get(ATTR_FAN_SPEED) - fire_mqtt_message(self.hass, 'retroroomba/battery_level', '54') - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert 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) +async def test_battery_template(hass, mock_publish): + """Test that you can use non-default templates for battery_level.""" + default_config.update({ + mqttvacuum.CONF_SUPPORTED_FEATURES: + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES), + mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", + mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" + }) - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - fire_mqtt_message(self.hass, 'vacuum/state', '{"asdfasas false}') - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state - assert "Stopped" == state.attributes.get(ATTR_STATUS) + async_fire_mqtt_message(hass, 'retroroomba/battery_level', '54') + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) + assert state.attributes.get(ATTR_BATTERY_ICON) == \ + 'mdi:battery-50' - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - self.default_config.update({ - 'availability_topic': 'availability-topic' - }) - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) +async def test_status_invalid_json(hass, mock_publish): + """Test to make sure nothing breaks if the vacuum sends bad JSON.""" + default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'vacuum/state', '{"asdfasas false}') + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert STATE_OFF == state.state + assert "Stopped" == state.attributes.get(ATTR_STATUS) - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE != state.state - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() +async def test_default_availability_payload(hass, mock_publish): + """Test availability by default payload with defined topic.""" + default_config.update({ + 'availability_topic': 'availability-topic' + }) - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - self.default_config.update({ - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - }) + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state - assert setup_component(self.hass, vacuum.DOMAIN, { - vacuum.DOMAIN: self.default_config, - }) + async_fire_mqtt_message(hass, 'availability-topic', 'online') + await hass.async_block_till_done() + await hass.async_block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE != state.state - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'availability-topic', 'offline') + await hass.async_block_till_done() + await hass.async_block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE != state.state + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - state = self.hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state +async def test_custom_availability_payload(hass, mock_publish): + """Test availability by custom payload with defined topic.""" + default_config.update({ + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + }) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: default_config, + }) + + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE == state.state + + +async def test_discovery_removal_vacuum(hass, mock_publish): + """Test removal of discovered vacuum.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is None + + +async def test_discovery_update_vacuum(hass, mock_publish): + """Test update of discovered vacuum.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('vacuum.milk') + assert state is None