Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix zha ikea starkvind capabilities and modes being incorrectly exposed #114854

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,11 @@ def fan_mode(self) -> int | None:
"""Return current fan mode."""
return self.cluster.get("fan_mode")

@property
def fan_speed(self) -> int | None:
"""Return current fan speed."""
return self.cluster.get("fan_speed")

@property
def fan_mode_sequence(self) -> int | None:
"""Return possible fan mode speeds."""
Expand All @@ -412,6 +417,7 @@ async def async_set_speed(self, value) -> None:
async def async_update(self) -> None:
"""Retrieve latest state."""
await self.get_attribute_value("fan_mode", from_cache=False)
await self.get_attribute_value("fan_speed", from_cache=False)

@callback
def attribute_updated(self, attrid: int, value: Any, _: Any) -> None:
Expand All @@ -420,7 +426,7 @@ def attribute_updated(self, attrid: int, value: Any, _: Any) -> None:
self.debug(
"Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value
)
if attr_name == "fan_mode":
if attr_name in ("fan_mode", "fan_speed"):
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value
)
Expand Down
62 changes: 41 additions & 21 deletions homeassistant/components/zha/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,28 +251,15 @@ async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()


IKEA_SPEED_RANGE = (1, 10) # off is not included
IKEA_PRESET_MODES_TO_NAME = {
1: PRESET_MODE_AUTO,
2: "Speed 1",
3: "Speed 1.5",
4: "Speed 2",
5: "Speed 2.5",
6: "Speed 3",
7: "Speed 3.5",
8: "Speed 4",
9: "Speed 4.5",
10: "Speed 5",
}


@MULTI_MATCH(
cluster_handler_names="ikea_airpurifier",
models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
)
class IkeaFan(ZhaFan):
"""Representation of an Ikea fan."""

_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE

def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None:
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
Expand All @@ -281,20 +268,53 @@ def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None:
@property
def preset_modes_to_name(self) -> dict[int, str]:
"""Return a dict from preset mode to name."""
return IKEA_PRESET_MODES_TO_NAME
return {1: PRESET_MODE_AUTO}

@property
def speed_range(self) -> tuple[int, int]:
"""Return the range of speeds the fan supports. Off is not included."""
return IKEA_SPEED_RANGE

# 1 is not a speed, but auto mode and is filtered out in async_set_percentage
return (1, 10)

@property
def default_on_percentage(self) -> int:
"""Return the default on percentage."""
return int(
(100 / self.speed_count) * self.preset_name_to_mode[PRESET_MODE_AUTO]
def percentage(self) -> int | None:
"""Return the current speed percentage."""
if self._fan_cluster_handler.fan_speed is None:
return None
if self._fan_cluster_handler.fan_speed == 0:
return 0
return ranged_value_to_percentage(
# Starkvind has an additional fan_speed attribute that we can use to
# get the speed even if fan_mode is set to auto.
self.speed_range,
self._fan_cluster_handler.fan_speed,
)

async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the entity on."""
# Starkvind turns on in auto mode by default.
if percentage is None:
if preset_mode is None:
preset_mode = "auto"
await self.async_set_preset_mode(preset_mode)
else:
await self.async_set_percentage(percentage)

async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""

fan_mode = math.ceil(percentage_to_ranged_value(self.speed_range, percentage))
# 1 is a mode, not a speed, so we skip to 2 instead.
if fan_mode == 1:
fan_mode = 2
await self._async_set_fan_mode(fan_mode)


@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_FAN,
Expand Down
36 changes: 18 additions & 18 deletions tests/components/zha/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,17 +647,17 @@ async def test_fan_ikea(
),
[
(None, STATE_OFF, None, None),
({"fan_mode": 0}, STATE_OFF, 0, None),
({"fan_mode": 1}, STATE_ON, 10, PRESET_MODE_AUTO),
({"fan_mode": 10}, STATE_ON, 20, "Speed 1"),
({"fan_mode": 15}, STATE_ON, 30, "Speed 1.5"),
({"fan_mode": 20}, STATE_ON, 40, "Speed 2"),
({"fan_mode": 25}, STATE_ON, 50, "Speed 2.5"),
({"fan_mode": 30}, STATE_ON, 60, "Speed 3"),
({"fan_mode": 35}, STATE_ON, 70, "Speed 3.5"),
({"fan_mode": 40}, STATE_ON, 80, "Speed 4"),
({"fan_mode": 45}, STATE_ON, 90, "Speed 4.5"),
({"fan_mode": 50}, STATE_ON, 100, "Speed 5"),
({"fan_mode": 0, "fan_speed": 0}, STATE_OFF, 0, None),
({"fan_mode": 1, "fan_speed": 6}, STATE_ON, 60, PRESET_MODE_AUTO),
({"fan_mode": 2, "fan_speed": 2}, STATE_ON, 20, None),
({"fan_mode": 3, "fan_speed": 3}, STATE_ON, 30, None),
({"fan_mode": 4, "fan_speed": 4}, STATE_ON, 40, None),
({"fan_mode": 5, "fan_speed": 5}, STATE_ON, 50, None),
({"fan_mode": 6, "fan_speed": 6}, STATE_ON, 60, None),
({"fan_mode": 7, "fan_speed": 7}, STATE_ON, 70, None),
({"fan_mode": 8, "fan_speed": 8}, STATE_ON, 80, None),
({"fan_mode": 9, "fan_speed": 9}, STATE_ON, 90, None),
({"fan_mode": 10, "fan_speed": 10}, STATE_ON, 100, None),
],
)
async def test_fan_ikea_init(
Expand Down Expand Up @@ -691,7 +691,7 @@ async def test_fan_ikea_update_entity(
) -> None:
"""Test ZHA fan platform."""
cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier
cluster.PLUGGED_ATTR_READS = {"fan_mode": 0}
cluster.PLUGGED_ATTR_READS = {"fan_mode": 0, "fan_speed": 0}

zha_device = await zha_device_joined_restored(zigpy_device_ikea)
entity_id = find_entity_id(Platform.FAN, zha_device, hass)
Expand All @@ -713,22 +713,22 @@ async def test_fan_ikea_update_entity(
)
assert hass.states.get(entity_id).state == STATE_OFF
if zha_device_joined_restored.name == "zha_device_joined":
assert cluster.read_attributes.await_count == 4
assert cluster.read_attributes.await_count == 5
else:
assert cluster.read_attributes.await_count == 7
assert cluster.read_attributes.await_count == 8

cluster.PLUGGED_ATTR_READS = {"fan_mode": 1}
cluster.PLUGGED_ATTR_READS = {"fan_mode": 1, "fan_speed": 6}
await hass.services.async_call(
"homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
)
assert hass.states.get(entity_id).state == STATE_ON
assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 10
assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 60
assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is PRESET_MODE_AUTO
assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 10
if zha_device_joined_restored.name == "zha_device_joined":
assert cluster.read_attributes.await_count == 5
assert cluster.read_attributes.await_count == 7
else:
assert cluster.read_attributes.await_count == 8
assert cluster.read_attributes.await_count == 10


@pytest.fixture
Expand Down