mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Fix Matter fan and light dividing by zero on out-of-spec device ranges (#182645)
This commit is contained in:
@@ -270,9 +270,13 @@ class MatterFan(MatterEntity, FanEntity):
|
||||
# does not leave a stale speed_count / percentage_step.
|
||||
self._attr_speed_count = 100
|
||||
if feature_map & FanControlFeature.kMultiSpeed:
|
||||
self._attr_speed_count = int(
|
||||
speed_max = int(
|
||||
self.get_matter_attribute_value(clusters.FanControl.Attributes.SpeedMax)
|
||||
)
|
||||
# the step size divides by the speed count, so a device reporting
|
||||
# no speeds keeps the default
|
||||
if speed_max > 0:
|
||||
self._attr_speed_count = speed_max
|
||||
if feature_map & FanControlFeature.kRocking:
|
||||
# NOTE: the Matter model allows that a device can have multiple/different
|
||||
# rock directions while HA doesn't allow this in the entity model.
|
||||
|
||||
@@ -84,6 +84,19 @@ TRANSITION_BLOCKLIST = (
|
||||
)
|
||||
|
||||
|
||||
def _level_range(level_control: clusters.LevelControl) -> tuple[int, int]:
|
||||
"""Return the level range of the device.
|
||||
|
||||
Brightness scaling divides by the width of the range, so a device that
|
||||
reports a range without width gets the default range instead.
|
||||
"""
|
||||
min_level = level_control.minLevel or 1
|
||||
max_level = level_control.maxLevel or 254
|
||||
if max_level <= min_level:
|
||||
return (1, 254)
|
||||
return (min_level, max_level)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MatterConfigEntry,
|
||||
@@ -177,13 +190,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
|
||||
assert level_control is not None
|
||||
|
||||
level = round(
|
||||
renormalize(
|
||||
brightness,
|
||||
(0, 255),
|
||||
(level_control.minLevel or 1, level_control.maxLevel or 254),
|
||||
)
|
||||
)
|
||||
level = round(renormalize(brightness, (0, 255), _level_range(level_control)))
|
||||
|
||||
await self.send_device_command(
|
||||
clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
||||
@@ -280,9 +287,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
|
||||
return round(
|
||||
renormalize(
|
||||
level_control.currentLevel,
|
||||
(level_control.minLevel or 1, level_control.maxLevel or 254),
|
||||
(0, 255),
|
||||
level_control.currentLevel, _level_range(level_control), (0, 255)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -504,3 +504,24 @@ async def test_fan_set_percentage_without_multispeed(
|
||||
attribute_path="1/514/2",
|
||||
value=75,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_air_purifier"])
|
||||
@pytest.mark.parametrize("attributes", [{"1/514/4": 0}])
|
||||
async def test_fan_without_speed_steps(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test a fan reporting no speed steps keeps the default step size."""
|
||||
entity_id = "fan.mock_air_purifier"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["percentage_step"] == 1
|
||||
|
||||
# a proper speed count is picked up when the features are recalculated
|
||||
set_node_attribute(matter_node, 1, 514, 4, 5)
|
||||
set_node_attribute(matter_node, 1, 514, 65532, 47)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["percentage_step"] == 20
|
||||
|
||||
@@ -565,3 +565,36 @@ async def test_light_unexpected_color_mode(
|
||||
transitionTime=0,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_dimmable_light"])
|
||||
async def test_dimmable_light_without_level_range(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test a light reporting a level range without width uses the default range."""
|
||||
entity_id = "light.mock_dimmable_light"
|
||||
set_node_attribute(matter_node, 1, 8, 2, 5)
|
||||
set_node_attribute(matter_node, 1, 8, 3, 5)
|
||||
set_node_attribute(matter_node, 1, 8, 0, 5)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["brightness"] == 4
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": entity_id, "brightness": 128},
|
||||
blocking=True,
|
||||
)
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
||||
level=128,
|
||||
transitionTime=0,
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user