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 Starkvind fan capabilities and modes being incorrectly exposed #87

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
42 changes: 28 additions & 14 deletions tests/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,13 @@ async def test_fan_ikea(
call({"fan_mode": 1}, manufacturer=None)
]

# turn on with set speed from HA
cluster.write_attributes.reset_mock()
await async_turn_on(zha_gateway, entity, speed="high")
assert cluster.write_attributes.mock_calls == [
call({"fan_mode": 10}, manufacturer=None)
]

# turn off from HA
cluster.write_attributes.reset_mock()
await async_turn_off(zha_gateway, entity)
Expand All @@ -547,6 +554,13 @@ async def test_fan_ikea(
call({"fan_mode": 10}, manufacturer=None)
]

# skip 10% when set from HA
cluster.write_attributes.reset_mock()
await async_set_percentage(zha_gateway, entity, percentage=10)
assert cluster.write_attributes.mock_calls == [
call({"fan_mode": 2}, manufacturer=None)
]

# change preset_mode from HA
cluster.write_attributes.reset_mock()
await async_set_preset_mode(zha_gateway, entity, preset_mode=PRESET_MODE_AUTO)
Expand Down Expand Up @@ -574,17 +588,17 @@ async def test_fan_ikea(
),
[
(None, False, None, None),
({"fan_mode": 0}, False, 0, None),
({"fan_mode": 1}, True, 10, PRESET_MODE_AUTO),
({"fan_mode": 10}, True, 20, "Speed 1"),
({"fan_mode": 15}, True, 30, "Speed 1.5"),
({"fan_mode": 20}, True, 40, "Speed 2"),
({"fan_mode": 25}, True, 50, "Speed 2.5"),
({"fan_mode": 30}, True, 60, "Speed 3"),
({"fan_mode": 35}, True, 70, "Speed 3.5"),
({"fan_mode": 40}, True, 80, "Speed 4"),
({"fan_mode": 45}, True, 90, "Speed 4.5"),
({"fan_mode": 50}, True, 100, "Speed 5"),
({"fan_mode": 0, "fan_speed": 0}, False, 0, None),
({"fan_mode": 1, "fan_speed": 30}, True, 60, PRESET_MODE_AUTO),
({"fan_mode": 10, "fan_speed": 10}, True, 20, None),
({"fan_mode": 15, "fan_speed": 15}, True, 30, None),
({"fan_mode": 20, "fan_speed": 20}, True, 40, None),
({"fan_mode": 25, "fan_speed": 25}, True, 50, None),
({"fan_mode": 30, "fan_speed": 30}, True, 60, None),
({"fan_mode": 35, "fan_speed": 35}, True, 70, None),
({"fan_mode": 40, "fan_speed": 40}, True, 80, None),
({"fan_mode": 45, "fan_speed": 45}, True, 90, None),
({"fan_mode": 50, "fan_speed": 50}, True, 100, None),
],
)
async def test_fan_ikea_init(
Expand Down Expand Up @@ -613,7 +627,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 device_joined(zigpy_device_ikea)
entity = get_entity(zha_device, platform=Platform.FAN)
Expand All @@ -623,13 +637,13 @@ async def test_fan_ikea_update_entity(
assert entity.state[ATTR_PRESET_MODE] is None
assert entity.percentage_step == 100 / 10

cluster.PLUGGED_ATTR_READS = {"fan_mode": 1}
cluster.PLUGGED_ATTR_READS = {"fan_mode": 1, "fan_speed": 6}

await entity.async_update()
await zha_gateway.async_block_till_done()

assert entity.state["is_on"] is True
assert entity.state[ATTR_PERCENTAGE] == 10
assert entity.state[ATTR_PERCENTAGE] == 60
assert entity.state[ATTR_PRESET_MODE] is PRESET_MODE_AUTO
assert entity.percentage_step == 100 / 10

Expand Down
68 changes: 47 additions & 21 deletions zha/application/platforms/fan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,28 +378,20 @@ def async_update(self, _: Any = None) -> None:
self.maybe_emit_state_changed_event()


IKEA_SPEED_RANGE = (1, 10) # off is not included
IKEA_PRESET_MODES_TO_NAME = {
TheJulianJES marked this conversation as resolved.
Show resolved Hide resolved
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(Fan):
"""Representation of an Ikea fan."""

_attr_supported_features: FanEntityFeature = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)

def __init__(
self,
unique_id: str,
Expand All @@ -421,20 +413,54 @@ def __init__(
@functools.cached_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}

@functools.cached_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."""
self.debug(f"fan_speed is {self._fan_cluster_handler.fan_speed}")
TheJulianJES marked this conversation as resolved.
Show resolved Hide resolved
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,
speed: str | None = None,
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 speed is None and percentage is None and preset_mode is None:
await self.async_set_preset_mode("auto")
else:
await super().async_turn_on(speed, percentage, preset_mode)

async def async_set_percentage(self, percentage: int) -> None:
TheJulianJES marked this conversation as resolved.
Show resolved Hide resolved
"""Set the speed percentage of the fan."""

self.debug(f"Set percentage {percentage}")
TheJulianJES marked this conversation as resolved.
Show resolved Hide resolved
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
6 changes: 6 additions & 0 deletions zha/zigbee/cluster_handlers/manufacturerspecific.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,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 mode."""
TheJulianJES marked this conversation as resolved.
Show resolved Hide resolved
return self.cluster.get("fan_speed")

@property
def fan_mode_sequence(self) -> int | None:
"""Return possible fan mode speeds."""
Expand All @@ -421,6 +426,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)


@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(IKEA_REMOTE_CLUSTER)
Expand Down
Loading