diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 99aa68d1975..8b116210a10 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -9,7 +9,7 @@ from homeassistant.components.fan import ( SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, - CONF_STATE, STATE_OFF, STATE_ON) + CONF_STATE) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -32,6 +32,7 @@ CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' CONF_OSCILLATION_VALUE_TEMPLATE = 'oscillation_value_template' CONF_PAYLOAD_OSCILLATION_ON = 'payload_oscillation_on' CONF_PAYLOAD_OSCILLATION_OFF = 'payload_oscillation_off' +CONF_PAYLOAD_OFF_SPEED = 'payload_off_speed' CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed' CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed' CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed' @@ -57,12 +58,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, + vol.Optional(CONF_PAYLOAD_OFF_SPEED, default=SPEED_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OSCILLATION_OFF, - default=DEFAULT_PAYLOAD_OFF): cv.string, + default=OSCILLATE_OFF_PAYLOAD): cv.string, vol.Optional(CONF_PAYLOAD_OSCILLATION_ON, - default=DEFAULT_PAYLOAD_ON): cv.string, + default=OSCILLATE_ON_PAYLOAD): cv.string, vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_SPEED_LIST, default=[SPEED_OFF, SPEED_LOW, @@ -172,13 +174,14 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE) } self._payload = { - STATE_ON: config[CONF_PAYLOAD_ON], - STATE_OFF: config[CONF_PAYLOAD_OFF], - OSCILLATE_ON_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_ON], - OSCILLATE_OFF_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_OFF], - SPEED_LOW: config[CONF_PAYLOAD_LOW_SPEED], - SPEED_MEDIUM: config[CONF_PAYLOAD_MEDIUM_SPEED], - SPEED_HIGH: config[CONF_PAYLOAD_HIGH_SPEED], + 'STATE_ON': config[CONF_PAYLOAD_ON], + 'STATE_OFF': config[CONF_PAYLOAD_OFF], + 'OSCILLATE_ON_PAYLOAD': config[CONF_PAYLOAD_OSCILLATION_ON], + 'OSCILLATE_OFF_PAYLOAD': config[CONF_PAYLOAD_OSCILLATION_OFF], + 'SPEED_LOW': config[CONF_PAYLOAD_LOW_SPEED], + 'SPEED_MEDIUM': config[CONF_PAYLOAD_MEDIUM_SPEED], + 'SPEED_HIGH': config[CONF_PAYLOAD_HIGH_SPEED], + 'SPEED_OFF': config[CONF_PAYLOAD_OFF_SPEED], } optimistic = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None @@ -208,9 +211,9 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def state_received(msg): """Handle new received MQTT message.""" payload = templates[CONF_STATE](msg.payload) - if payload == self._payload[STATE_ON]: + if payload == self._payload['STATE_ON']: self._state = True - elif payload == self._payload[STATE_OFF]: + elif payload == self._payload['STATE_OFF']: self._state = False self.async_write_ha_state() @@ -224,12 +227,14 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def speed_received(msg): """Handle new received MQTT message for the speed.""" payload = templates[ATTR_SPEED](msg.payload) - if payload == self._payload[SPEED_LOW]: + if payload == self._payload['SPEED_LOW']: self._speed = SPEED_LOW - elif payload == self._payload[SPEED_MEDIUM]: + elif payload == self._payload['SPEED_MEDIUM']: self._speed = SPEED_MEDIUM - elif payload == self._payload[SPEED_HIGH]: + elif payload == self._payload['SPEED_HIGH']: self._speed = SPEED_HIGH + elif payload == self._payload['SPEED_OFF']: + self._speed = SPEED_OFF self.async_write_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: @@ -243,9 +248,9 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" payload = templates[OSCILLATION](msg.payload) - if payload == self._payload[OSCILLATE_ON_PAYLOAD]: + if payload == self._payload['OSCILLATE_ON_PAYLOAD']: self._oscillation = True - elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]: + elif payload == self._payload['OSCILLATE_OFF_PAYLOAD']: self._oscillation = False self.async_write_ha_state() @@ -314,10 +319,13 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, """ mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], - self._payload[STATE_ON], self._config[CONF_QOS], + self._payload['STATE_ON'], self._config[CONF_QOS], self._config[CONF_RETAIN]) if speed: await self.async_set_speed(speed) + if self._optimistic: + self._state = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn off the entity. @@ -326,8 +334,11 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, """ mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], - self._payload[STATE_OFF], self._config[CONF_QOS], + self._payload['STATE_OFF'], self._config[CONF_QOS], self._config[CONF_RETAIN]) + if self._optimistic: + self._state = False + self.async_write_ha_state() async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan. @@ -338,11 +349,13 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, return if speed == SPEED_LOW: - mqtt_payload = self._payload[SPEED_LOW] + mqtt_payload = self._payload['SPEED_LOW'] elif speed == SPEED_MEDIUM: - mqtt_payload = self._payload[SPEED_MEDIUM] + mqtt_payload = self._payload['SPEED_MEDIUM'] elif speed == SPEED_HIGH: - mqtt_payload = self._payload[SPEED_HIGH] + mqtt_payload = self._payload['SPEED_HIGH'] + elif speed == SPEED_OFF: + mqtt_payload = self._payload['SPEED_OFF'] else: mqtt_payload = speed @@ -364,9 +377,9 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, return if oscillating is False: - payload = self._payload[OSCILLATE_OFF_PAYLOAD] + payload = self._payload['OSCILLATE_OFF_PAYLOAD'] else: - payload = self._payload[OSCILLATE_ON_PAYLOAD] + payload = self._payload['OSCILLATE_ON_PAYLOAD'] mqtt.async_publish( self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC], diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index b7f8b8338a0..c00de8522b9 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -4,12 +4,14 @@ from unittest.mock import ANY from homeassistant.components import fan, mqtt from homeassistant.components.mqtt.discovery import async_start -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, mock_registry) +from tests.components.fan import common async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): @@ -23,6 +25,349 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert hass.states.get('fan.test') is None +async def test_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_off': 'StAtE_OfF', + 'payload_on': 'StAtE_On', + 'oscillation_state_topic': 'oscillation-state-topic', + 'oscillation_command_topic': 'oscillation-command-topic', + 'payload_oscillation_off': 'OsC_OfF', + 'payload_oscillation_on': 'OsC_On', + 'speed_state_topic': 'speed-state-topic', + 'speed_command_topic': 'speed-command-topic', + 'payload_off_speed': 'speed_OfF', + 'payload_low_speed': 'speed_lOw', + 'payload_medium_speed': 'speed_mEdium', + 'payload_high_speed': 'speed_High', + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, 'state-topic', 'StAtE_On') + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + + async_fire_mqtt_message(hass, 'state-topic', 'StAtE_OfF') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get('oscillating') is False + + async_fire_mqtt_message(hass, 'oscillation-state-topic', 'OsC_On') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is True + + async_fire_mqtt_message(hass, 'oscillation-state-topic', 'OsC_OfF') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is False + + assert fan.SPEED_OFF == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_lOw') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_LOW == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_mEdium') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_MEDIUM == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_High') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_HIGH == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_OfF') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_OFF == state.attributes.get('speed') + + +async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): + """Test the controlling state via topic and JSON message.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'oscillation_state_topic': 'oscillation-state-topic', + 'oscillation_command_topic': 'oscillation-command-topic', + 'speed_state_topic': 'speed-state-topic', + 'speed_command_topic': 'speed-command-topic', + 'state_value_template': '{{ value_json.val }}', + 'oscillation_value_template': '{{ value_json.val }}', + 'speed_value_template': '{{ value_json.val }}', + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, 'state-topic', '{"val":"ON"}') + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + + async_fire_mqtt_message(hass, 'state-topic', '{"val":"OFF"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get('oscillating') is False + + async_fire_mqtt_message( + hass, 'oscillation-state-topic', '{"val":"oscillate_on"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is True + + async_fire_mqtt_message( + hass, 'oscillation-state-topic', '{"val":"oscillate_off"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is False + + assert fan.SPEED_OFF == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"low"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_LOW == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"medium"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_MEDIUM == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"high"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_HIGH == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"off"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_OFF == state.attributes.get('speed') + + +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test optimistic mode without state topic.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'command-topic', + 'payload_off': 'StAtE_OfF', + 'payload_on': 'StAtE_On', + 'oscillation_command_topic': 'oscillation-command-topic', + 'payload_oscillation_off': 'OsC_OfF', + 'payload_oscillation_on': 'OsC_On', + 'speed_command_topic': 'speed-command-topic', + 'payload_off_speed': 'speed_OfF', + 'payload_low_speed': 'speed_lOw', + 'payload_medium_speed': 'speed_mEdium', + 'payload_high_speed': 'speed_High', + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_on(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'StAtE_On', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_off(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'StAtE_OfF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', True) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'OsC_On', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', False) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'OsC_OfF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_lOw', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_mEdium', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_High', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_OfF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): + """Test optimistic mode with state topic.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'oscillation_state_topic': 'oscillation-state-topic', + 'oscillation_command_topic': 'oscillation-command-topic', + 'speed_state_topic': 'speed-state-topic', + 'speed_command_topic': 'speed-command-topic', + 'optimistic': True + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_on(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'ON', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_off(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'OFF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', True) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'oscillate_on', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', False) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'oscillate_off', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'low', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'medium', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'high', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'off', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_default_availability_payload(hass, mqtt_mock): """Test the availability payload.""" assert await async_setup_component(hass, fan.DOMAIN, {