Skip to content

Commit

Permalink
zha: ikea: starkvind: Fix capabilities and modes being incorrectly ex…
Browse files Browse the repository at this point in the history
…posed

Fixes home-assistant#97440
Previously starkvind exposed 10 speed settings and no modes, where 10% corresponded to auto mode
and 20%-100% corresponded to fixed speeds.

This patch correctly exposes auto mode as a mode. It also adds support for showing the actual
fan speed while auto mode is enabled.
Starkvind supports 9 fan speeds. Because 9 doesn't neatly fit into 100% I cheated a bit and divided
the 100% into 10% increments, where trying to set the fan to 10% sets it to 20% instead. I believe
that this gives the overall better user experience compared to having 11.11% increments.
The 5 speed modes present on the physical interface of the device correspond to HA speed settings 20%,
40%, 60% and 100%.

This patch depends on zigpy/zha-device-handlers#3088 being merged and released.
  • Loading branch information
freundTech committed Jul 3, 2024
1 parent 61f1c8d commit 180f656
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 40 deletions.
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

0 comments on commit 180f656

Please sign in to comment.