diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 37b6f0aeabd5..d0f76b01c716 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -554,7 +554,7 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase): @property @override - def fan_mode(self) -> str: + def fan_mode(self) -> str | None: """Return the fan setting.""" fan_speed = self._device.current_fan_speed @@ -563,6 +563,10 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase): return self.fan_zero_mode if self._device.fan_speed_mode is FanSpeedMode.STEP: + # DPT 5.010 fits any 1-byte value (0-255), so a gateway may report + # a step beyond the configured fan_max_step + if fan_speed >= len(self._attr_fan_modes): + return None return self._attr_fan_modes[fan_speed] # Find the closest fan mode percentage diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 154681fc33cc..2d01aae29b2f 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -502,6 +502,45 @@ async def test_fan_speed_3_steps(hass: HomeAssistant, knx: KNXTestKit) -> None: knx.assert_state("climate.test", HVACMode.HEAT, fan_mode="off") +@pytest.mark.parametrize("raw_value", [0x04, 0xFF]) +async def test_fan_speed_step_out_of_range( + hass: HomeAssistant, knx: KNXTestKit, raw_value: int +) -> None: + """Test that fan step values beyond fan_max_step don't crash the entity.""" + await knx.setup_integration( + { + ClimateSchema.PLATFORM: { + CONF_NAME: "test", + ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6", + ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7", + ClimateConf.FAN_SPEED_MODE: "step", + ClimateConf.FAN_MAX_STEP: 3, + } + } + ) + + # read states state updater + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + + # StateUpdater initialize state + await knx.receive_response("1/2/5", RAW_FLOAT_22_0) + await knx.receive_response("1/2/3", RAW_FLOAT_21_0) + + # Query status + await knx.assert_read("1/2/7") + + # a gateway may report a step value beyond the configured fan_max_step + await knx.receive_write("1/2/7", (raw_value,)) + await hass.async_block_till_done() + + # the invalid telegram is published without breaking the state write + knx.assert_state("climate.test", HVACMode.HEAT, fan_mode=None) + + async def test_fan_speed_2_steps(hass: HomeAssistant, knx: KNXTestKit) -> None: """Test KNX climate fan speed 2 steps.""" await knx.setup_integration(