Skip to content

Commit

Permalink
Add support for fan_speed on climate device (#1572)
Browse files Browse the repository at this point in the history
* add support for fan_speed on climate device

* Update test/devices_tests/climate_test.py

Co-authored-by: Matthias Alphart <[email protected]>

* fix review comments

---------

Co-authored-by: Matthias Alphart <[email protected]>
  • Loading branch information
somdoron and farmio authored Sep 18, 2024
1 parent fe974e3 commit a94b860
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 1 deletion.
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ nav_order: 2

# Changelog

### Features

- Added fan speed support to climate

# 3.1.1 Fix Eberle status 2024-08-19

### Bugfixes
Expand Down
6 changes: 6 additions & 0 deletions docs/climate.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Climate are representations of KNX HVAC/Climate controls.
- `group_address_target_temperature_state` KNX address for reading the target temperature from the KNX bus. Used in for setpoint_shift calculations as base temperature. *DPT 9.001*
- `group_address_setpoint_shift` KNX address to set setpoint_shift (base temperature deviation). *DPT 6.010* or *DPT 9.002*
- `group_address_setpoint_shift_state` KNX address to read current setpoint_shift. *DPT 6.010* or *DPT 9.002*
- `group_address_fan_speed` KNX address for the fan speed. *DPT 5.001 / 5.010*
- `group_address_fan_speed_state` KNX address for reading fan speed. *DPT 5.001 / 5.010*
- `setpoint_shift_mode` SetpointShiftMode Enum for setpoint_shift payload encoding. When `None` it is inferred from first incoming payload. Default: `None`
- `setpoint_shift_max` Maximum value for setpoint_shift.
- `setpoint_shift_min` Minimum value for setpoint_shift.
Expand All @@ -32,6 +34,7 @@ Climate are representations of KNX HVAC/Climate controls.
- `sync_state` defines if and how often the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
- `max_temp` Maximum value for target temperature.
- `min_temp` Minimum value for target temperature.
- `fan_max_step` Maximum step amount for fans which are controlled with steps and not percentage. If this attribute is set, the fan is controlled by sending the step value in the range `0` and `max_step`. In that case, the group address DPT changes from *DPT 5.001* to *DPT 5.010*. Default: None.
- `mode` ClimateMode instance for this climate device
- `group_address_operation_mode` KNX address for operation mode. *DPT 20.102*
- `group_address_operation_mode_state` KNX address for operation mode status. *DPT 20.102*
Expand Down Expand Up @@ -78,6 +81,8 @@ climate = Climate(
group_address_target_temperature_state='',
group_address_setpoint_shift='',
group_address_setpoint_shift_state='',
group_address_fan_speed=None,
group_address_fan_speed_state=None,
temperature_step=0.1,
setpoint_shift_max=6,
setpoint_shift_min=-6,
Expand All @@ -88,6 +93,7 @@ climate = Climate(
max_temp=26,
mode=climate_mode,
device_updated_cb=None,
fan_max_step=None,
)
xknx.devices.async_add(climate)
xknx.devices.async_add(climate_mode)
Expand Down
74 changes: 74 additions & 0 deletions test/devices_tests/climate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1562,3 +1562,77 @@ async def test_is_active(self):
# only command initialized
climate_active_command.active.value = None
assert climate_active_command.is_active is False

async def test_fan_speed(self):
"""Test fan speed functionality."""
xknx = XKNX()
climate_step = Climate(
xknx,
name="TestClimate",
group_address_fan_speed="1/2/3",
group_address_fan_speed_state="1/2/4",
fan_max_step=3,
)
xknx.devices.async_add(climate_step)
climate_percent = Climate(
xknx,
name="TestClimate",
group_address_fan_speed="1/2/5",
group_address_fan_speed_state="1/2/6",
)
xknx.devices.async_add(climate_percent)

# Test initial state
assert climate_step.current_fan_speed is None
assert climate_percent.current_fan_speed is None

xknx.devices.process(
Telegram(
destination_address=GroupAddress("1/2/3"),
payload=GroupValueWrite(DPTArray(2)),
)
)
assert climate_step.current_fan_speed == 2

# 140 is 55% as byte (0...255)
xknx.devices.process(
Telegram(
destination_address=GroupAddress("1/2/5"),
payload=GroupValueWrite(DPTArray(140)),
)
)
assert climate_percent.current_fan_speed == 55

await climate_step.set_fan_speed(3)
assert xknx.telegrams.qsize() == 1
telegram = xknx.telegrams.get_nowait()
assert telegram == Telegram(
destination_address=GroupAddress("1/2/3"),
payload=GroupValueWrite(DPTArray(3)),
)

await climate_percent.set_fan_speed(45)
assert xknx.telegrams.qsize() == 1
telegram = xknx.telegrams.get_nowait()
# 115 is 45% as byte (0...255)
assert telegram == Telegram(
destination_address=GroupAddress("1/2/5"),
payload=GroupValueWrite(DPTArray(115)),
)

xknx.devices.process(
Telegram(
destination_address=GroupAddress("1/2/4"),
payload=GroupValueWrite(DPTArray(2)),
)
)
assert climate_step.current_fan_speed == 2

# 140 is 55% as byte (0...255)
xknx.devices.process(
Telegram(
destination_address=GroupAddress("1/2/6"),
payload=GroupValueWrite(DPTArray(140)),
)
)
assert climate_percent.current_fan_speed == 55
5 changes: 4 additions & 1 deletion test/str_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def test_climate(self):
setpoint_shift_min=-20,
group_address_on_off="1/2/14",
group_address_on_off_state="1/2/15",
group_address_fan_speed="1/2/16",
group_address_fan_speed_state="1/2/17",
)
assert (
str(climate) == '<Climate name="Wohnzimmer" '
Expand All @@ -139,7 +141,8 @@ def test_climate(self):
'temperature_step="0.1" '
"setpoint_shift=<1/2/3, 1/2/4, [], None /> "
'setpoint_shift_max="20" setpoint_shift_min="-20" '
"group_address_on_off=<1/2/14, 1/2/15, [], None /> />"
"group_address_on_off=<1/2/14, 1/2/15, [], None /> "
"group_address_fan_speed=<1/2/16, 1/2/17, [], None /> />"
)

def test_climate_mode(self):
Expand Down
43 changes: 43 additions & 0 deletions xknx/devices/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import logging
from typing import TYPE_CHECKING, Any

from xknx.devices.fan import FanSpeedMode
from xknx.remote_value import (
GroupAddressesType,
RemoteValue,
RemoteValueDptValue1Ucount,
RemoteValueScaling,
RemoteValueSetpointShift,
RemoteValueSwitch,
Expand Down Expand Up @@ -63,6 +65,9 @@ def __init__(
max_temp: float | None = None,
mode: ClimateMode | None = None,
device_updated_cb: DeviceCallbackType[Climate] | None = None,
group_address_fan_speed: GroupAddressesType = None,
group_address_fan_speed_state: GroupAddressesType = None,
fan_max_step: int | None = None,
):
"""Initialize Climate class."""
super().__init__(xknx, name, device_updated_cb)
Expand Down Expand Up @@ -135,6 +140,33 @@ def __init__(
after_update_cb=self.after_update,
)

self.fan_speed: RemoteValueDptValue1Ucount | RemoteValueScaling
self.fan_mode = FanSpeedMode.STEP if fan_max_step else FanSpeedMode.PERCENT
self.fan_max_step = fan_max_step

if self.fan_mode == FanSpeedMode.STEP:
self.fan_speed = RemoteValueDptValue1Ucount(
xknx,
group_address_fan_speed,
group_address_fan_speed_state,
sync_state=sync_state,
device_name=self.name,
feature_name="Fan Speed",
after_update_cb=self.after_update,
)
else:
self.fan_speed = RemoteValueScaling(
xknx,
group_address_fan_speed,
group_address_fan_speed_state,
sync_state=sync_state,
device_name=self.name,
feature_name="Fan Speed",
after_update_cb=self.after_update,
range_from=0,
range_to=100,
)

self.mode = mode

def _iter_remote_values(self) -> Iterator[RemoteValue[Any]]:
Expand All @@ -145,6 +177,7 @@ def _iter_remote_values(self) -> Iterator[RemoteValue[Any]]:
yield self.on
yield self.active
yield self.command_value
yield self.fan_speed

def has_group_address(self, group_address: DeviceGroupAddress) -> bool:
"""Test if device has given group address."""
Expand Down Expand Up @@ -197,6 +230,10 @@ async def set_target_temperature(self, target_temperature: float) -> None:
)
self.target_temperature.set(validated_temp)

async def set_fan_speed(self, speed: int) -> None:
"""Set the fan to a designated speed."""
self.fan_speed.set(speed)

@property
def base_temperature(self) -> float | None:
"""
Expand Down Expand Up @@ -262,6 +299,11 @@ def target_temperature_min(self) -> float | None:
return self.base_temperature + self.setpoint_shift_min
return None

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

def process_group_write(self, telegram: Telegram) -> None:
"""Process incoming and outgoing GROUP WRITE telegram."""
for remote_value in self._iter_remote_values():
Expand All @@ -287,5 +329,6 @@ def __str__(self) -> str:
f'setpoint_shift_max="{self.setpoint_shift_max}" '
f'setpoint_shift_min="{self.setpoint_shift_min}" '
f"group_address_on_off={self.on.group_addr_str()} "
f"group_address_fan_speed={self.fan_speed.group_addr_str()} "
"/>"
)

0 comments on commit a94b860

Please sign in to comment.