diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 4863cc24f1eb..4981f7aadc4c 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -39,6 +39,12 @@ FAN_MODE_MAP_REVERSE = {v: k for k, v in FAN_MODE_MAP.items()} PRESET_NATURAL_WIND = "natural_wind" PRESET_SLEEP_WIND = "sleep_wind" +# Some devices report 255 for PercentCurrent instead of their actual speed +# while FanMode is Auto. This is not part of the Matter spec (which says the +# device should keep reporting its real current speed, see spec 4.4.6.1.2 and +# 4.4.6.4), but we need to work around it regardless. +PERCENT_CURRENT_AUTO_QUIRK = 255 + async def async_setup_entry( hass: HomeAssistant, @@ -207,9 +213,9 @@ class MatterFan(MatterEntity, FanEntity): current_percent = self.get_matter_attribute_value( clusters.FanControl.Attributes.PercentCurrent ) - # NOTE that a device may give back 255 as a special value to indicate that - # the speed is under automatic control and not set to a specific value. - self._attr_percentage = None if current_percent == 255 else current_percent + self._attr_percentage = ( + None if current_percent == PERCENT_CURRENT_AUTO_QUIRK else current_percent + ) # get preset mode from fan mode (and wind feature if available) wind_setting = self.get_matter_attribute_value( @@ -242,7 +248,7 @@ class MatterFan(MatterEntity, FanEntity): # keep track of the last known mode for turn_on commands without preset if self._attr_preset_mode is not None: self._last_known_preset_mode = self._attr_preset_mode - if current_percent and current_percent != 255: + if current_percent and current_percent != PERCENT_CURRENT_AUTO_QUIRK: self._last_known_percentage = current_percent @callback diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 573a8d2698c2..67e5f9614230 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -134,10 +134,11 @@ async def test_fan_turn_on_with_percentage( attribute_path="1/514/2", value=50, ) - # test again where percentage is omitted in the service call - # PercentCurrent is 255 (auto) on this fixture, which is not a valid - # value to write back, so it should fall back to the last known preset - # mode instead of blindly replaying the sentinel value. + # test again where percentage is omitted in the service call. + # This fixture's PercentCurrent is 255, a device-specific quirk value + # (not part of the Matter spec) that is not valid to write back, so it + # should fall back to the last known preset mode instead of blindly + # replaying that sentinel value. matter_client.write_attribute.reset_mock() await hass.services.async_call( FAN_DOMAIN, @@ -153,6 +154,33 @@ async def test_fan_turn_on_with_percentage( ) +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"]) +async def test_fan_turn_on_replays_last_known_percentage( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test that a bare turn_on replays a real last known percentage.""" + entity_id = "fan.mock_air_purifier" + # simulate the device confirming a real (non-quirk) speed + set_node_attribute(matter_node, 1, 514, 3, 70) + await trigger_subscription_callback(hass, matter_client) + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=matter_node.node_id, + attribute_path="1/514/2", + value=70, + ) + + @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("node_fixture", ["mock_fan"]) async def test_fan_turn_on_with_preset_mode(