diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 1d7aa9f38cb..77b4532c001 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -1,24 +1,27 @@ """Support for Tasmota fans.""" +from typing import Optional + from hatasmota import const as tasmota_const from homeassistant.components import fan from homeassistant.components.fan import FanEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate -HA_TO_TASMOTA_SPEED_MAP = { - fan.SPEED_OFF: tasmota_const.FAN_SPEED_OFF, - fan.SPEED_LOW: tasmota_const.FAN_SPEED_LOW, - fan.SPEED_MEDIUM: tasmota_const.FAN_SPEED_MEDIUM, - fan.SPEED_HIGH: tasmota_const.FAN_SPEED_HIGH, -} - -TASMOTA_TO_HA_SPEED_MAP = {v: k for k, v in HA_TO_TASMOTA_SPEED_MAP.items()} +ORDERED_NAMED_FAN_SPEEDS = [ + tasmota_const.FAN_SPEED_LOW, + tasmota_const.FAN_SPEED_MEDIUM, + tasmota_const.FAN_SPEED_HIGH, +] # off is not included async def async_setup_entry(hass, config_entry, async_add_entities): @@ -56,42 +59,45 @@ class TasmotaFan( ) @property - def speed(self): - """Return the current speed.""" - return TASMOTA_TO_HA_SPEED_MAP.get(self._state) + def speed_count(self) -> Optional[int]: + """Return the number of speeds the fan supports.""" + return len(ORDERED_NAMED_FAN_SPEEDS) @property - def speed_list(self): - """Get the list of available speeds.""" - return list(HA_TO_TASMOTA_SPEED_MAP) + def percentage(self): + """Return the current speed percentage.""" + if self._state is None: + return None + if self._state == 0: + return 0 + return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, self._state) @property def supported_features(self): """Flag supported features.""" return fan.SUPPORT_SET_SPEED - async def async_set_speed(self, speed): + async def async_set_percentage(self, percentage): """Set the speed of the fan.""" - if speed not in HA_TO_TASMOTA_SPEED_MAP: - raise ValueError(f"Unsupported speed {speed}") - if speed == fan.SPEED_OFF: + if percentage == 0: await self.async_turn_off() else: - self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed]) + tasmota_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + self._tasmota_entity.set_speed(tasmota_speed) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed=None, percentage=None, preset_mode=None, **kwargs ): """Turn the fan on.""" # Tasmota does not support turning a fan on with implicit speed - await self.async_set_speed(speed or fan.SPEED_MEDIUM) + await self.async_set_percentage( + percentage + or ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, tasmota_const.FAN_SPEED_MEDIUM + ) + ) async def async_turn_off(self, **kwargs): """Turn the fan off.""" diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 4035c877bb8..a64c5e9c5e4 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -52,6 +52,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["speed"] is None + assert state.attributes["percentage"] is None assert state.attributes["speed_list"] == ["off", "low", "medium", "high"] assert state.attributes["supported_features"] == fan.SUPPORT_SET_SPEED assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -60,31 +61,37 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "low" + assert state.attributes["percentage"] == 33 async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":2}') state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "medium" + assert state.attributes["percentage"] == 66 async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":3}') state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "high" + assert state.attributes["percentage"] == 100 async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":0}') state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["speed"] == "off" + assert state.attributes["percentage"] == 0 async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":1}') state = hass.states.get("fan.tasmota") assert state.state == STATE_ON assert state.attributes["speed"] == "low" + assert state.attributes["percentage"] == 33 async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":0}') state = hass.states.get("fan.tasmota") assert state.state == STATE_OFF assert state.attributes["speed"] == "off" + assert state.attributes["percentage"] == 0 async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): @@ -151,6 +158,34 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 15) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "1", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 50) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "2", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set speed percentage and verify MQTT message is sent + await common.async_set_percentage(hass, "fan.tasmota", 90) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False + ) async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota): @@ -176,7 +211,7 @@ async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota): # Set an unsupported speed and verify MQTT message is not sent with pytest.raises(ValueError) as excinfo: await common.async_set_speed(hass, "fan.tasmota", "no_such_speed") - assert "Unsupported speed no_such_speed" in str(excinfo.value) + assert "no_such_speed" in str(excinfo.value) mqtt_mock.async_publish.assert_not_called()