mirror of
https://github.com/home-assistant/core.git
synced 2025-12-19 18:38:58 +00:00
KNX Fan: Add support for switch addresses (#159367)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, Final
|
||||
from typing import Any
|
||||
|
||||
from propcache.api import cached_property
|
||||
from xknx.devices import Fan as XknxFan
|
||||
@@ -32,12 +32,11 @@ from .storage.const import (
|
||||
CONF_GA_OSCILLATION,
|
||||
CONF_GA_SPEED,
|
||||
CONF_GA_STEP,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_SPEED,
|
||||
)
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
DEFAULT_PERCENTAGE: Final = 50
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -77,26 +76,24 @@ class _KnxFan(FanEntity):
|
||||
_device: XknxFan
|
||||
_step_range: tuple[int, int] | None
|
||||
|
||||
def _get_knx_speed(self, percentage: int) -> int:
|
||||
"""Convert percentage to KNX speed value."""
|
||||
if self._step_range is not None:
|
||||
return math.ceil(percentage_to_ranged_value(self._step_range, percentage))
|
||||
return percentage
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed of the fan, as a percentage."""
|
||||
if self._step_range:
|
||||
step = math.ceil(percentage_to_ranged_value(self._step_range, percentage))
|
||||
await self._device.set_speed(step)
|
||||
else:
|
||||
await self._device.set_speed(percentage)
|
||||
await self._device.set_speed(self._get_knx_speed(percentage))
|
||||
|
||||
@cached_property
|
||||
def supported_features(self) -> FanEntityFeature:
|
||||
"""Flag supported features."""
|
||||
flags = (
|
||||
FanEntityFeature.SET_SPEED
|
||||
| FanEntityFeature.TURN_ON
|
||||
| FanEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
flags = FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
|
||||
if self._device.speed.initialized:
|
||||
flags |= FanEntityFeature.SET_SPEED
|
||||
if self._device.supports_oscillation:
|
||||
flags |= FanEntityFeature.OSCILLATE
|
||||
|
||||
return flags
|
||||
|
||||
@property
|
||||
@@ -118,6 +115,11 @@ class _KnxFan(FanEntity):
|
||||
return super().speed_count
|
||||
return int_states_in_range(self._step_range)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the current fan state of the device."""
|
||||
return self._device.is_on
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
@@ -125,14 +127,12 @@ class _KnxFan(FanEntity):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn on the fan."""
|
||||
if percentage is None:
|
||||
await self.async_set_percentage(DEFAULT_PERCENTAGE)
|
||||
else:
|
||||
await self.async_set_percentage(percentage)
|
||||
speed = self._get_knx_speed(percentage) if percentage is not None else None
|
||||
await self._device.turn_on(speed)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
await self.async_set_percentage(0)
|
||||
await self._device.turn_off()
|
||||
|
||||
async def async_oscillate(self, oscillating: bool) -> None:
|
||||
"""Oscillate the fan."""
|
||||
@@ -165,7 +165,12 @@ class KnxYamlFan(_KnxFan, KnxYamlEntity):
|
||||
group_address_oscillation_state=config.get(
|
||||
FanSchema.CONF_OSCILLATION_STATE_ADDRESS
|
||||
),
|
||||
group_address_switch=config.get(FanSchema.CONF_SWITCH_ADDRESS),
|
||||
group_address_switch_state=config.get(
|
||||
FanSchema.CONF_SWITCH_STATE_ADDRESS
|
||||
),
|
||||
max_step=max_step,
|
||||
sync_state=config.get(CONF_SYNC_STATE, True),
|
||||
),
|
||||
)
|
||||
# FanSpeedMode.STEP if max_step is set
|
||||
@@ -210,6 +215,8 @@ class KnxUiFan(_KnxFan, KnxUiEntity):
|
||||
group_address_oscillation_state=knx_conf.get_state_and_passive(
|
||||
CONF_GA_OSCILLATION
|
||||
),
|
||||
group_address_switch=knx_conf.get_write(CONF_GA_SWITCH),
|
||||
group_address_switch_state=knx_conf.get_state_and_passive(CONF_GA_SWITCH),
|
||||
max_step=max_step,
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
)
|
||||
|
||||
@@ -576,19 +576,40 @@ class FanSchema(KNXPlatformSchema):
|
||||
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
|
||||
CONF_OSCILLATION_ADDRESS = "oscillation_address"
|
||||
CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address"
|
||||
CONF_SWITCH_ADDRESS = "switch_address"
|
||||
CONF_SWITCH_STATE_ADDRESS = "switch_state_address"
|
||||
|
||||
DEFAULT_NAME = "KNX Fan"
|
||||
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(FanConf.MAX_STEP): cv.byte,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_SWITCH_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_SWITCH_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(FanConf.MAX_STEP): cv.byte,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
}
|
||||
),
|
||||
vol.Any(
|
||||
vol.Schema(
|
||||
{vol.Required(KNX_ADDRESS): object},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
vol.Schema(
|
||||
{vol.Required(CONF_SWITCH_ADDRESS): object},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
msg=(
|
||||
f"At least one of '{KNX_ADDRESS}' or"
|
||||
f" '{CONF_SWITCH_ADDRESS}' is required."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -224,40 +224,58 @@ DATETIME_KNX_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
FAN_KNX_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SPEED): GroupSelect(
|
||||
GroupSelectOption(
|
||||
translation_key="percentage_mode",
|
||||
schema={
|
||||
vol.Required(CONF_GA_SPEED): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
},
|
||||
FAN_KNX_SCHEMA = AllSerializeFirst(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_GA_SWITCH): GASelector(
|
||||
write_required=True, valid_dpt="1"
|
||||
),
|
||||
GroupSelectOption(
|
||||
translation_key="step_mode",
|
||||
schema={
|
||||
vol.Required(CONF_GA_STEP): GASelector(
|
||||
write_required=True, valid_dpt="5.010"
|
||||
),
|
||||
vol.Required(FanConf.MAX_STEP, default=3): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=1,
|
||||
max=100,
|
||||
step=1,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
)
|
||||
),
|
||||
},
|
||||
vol.Optional(CONF_SPEED): GroupSelect(
|
||||
GroupSelectOption(
|
||||
translation_key="percentage_mode",
|
||||
schema={
|
||||
vol.Required(CONF_GA_SPEED): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
},
|
||||
),
|
||||
GroupSelectOption(
|
||||
translation_key="step_mode",
|
||||
schema={
|
||||
vol.Required(CONF_GA_STEP): GASelector(
|
||||
write_required=True, valid_dpt="5.010"
|
||||
),
|
||||
vol.Required(
|
||||
FanConf.MAX_STEP, default=3
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=1,
|
||||
max=100,
|
||||
step=1,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
)
|
||||
),
|
||||
},
|
||||
),
|
||||
collapsible=False,
|
||||
),
|
||||
collapsible=False,
|
||||
vol.Optional(CONF_GA_OSCILLATION): GASelector(
|
||||
write_required=True, valid_dpt="1"
|
||||
),
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),
|
||||
}
|
||||
),
|
||||
vol.Any(
|
||||
vol.Schema(
|
||||
{vol.Required(CONF_GA_SWITCH): object},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
vol.Optional(CONF_GA_OSCILLATION): GASelector(
|
||||
write_required=True, valid_dpt="1"
|
||||
vol.Schema(
|
||||
{vol.Required(CONF_SPEED): object},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),
|
||||
}
|
||||
msg=("At least one of 'Switch' or 'Fan speed' is required."),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -467,6 +467,10 @@
|
||||
"description": "Toggle oscillation of the fan.",
|
||||
"label": "Oscillation"
|
||||
},
|
||||
"ga_switch": {
|
||||
"description": "Group address to turn the fan on/off.",
|
||||
"label": "Switch"
|
||||
},
|
||||
"speed": {
|
||||
"description": "Control the speed of the fan.",
|
||||
"ga_speed": {
|
||||
|
||||
@@ -1037,10 +1037,32 @@
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': list([
|
||||
dict({
|
||||
'name': 'ga_switch',
|
||||
'optional': True,
|
||||
'options': dict({
|
||||
'passive': True,
|
||||
'state': dict({
|
||||
'required': False,
|
||||
}),
|
||||
'validDPTs': list([
|
||||
dict({
|
||||
'main': 1,
|
||||
'sub': None,
|
||||
}),
|
||||
]),
|
||||
'write': dict({
|
||||
'required': True,
|
||||
}),
|
||||
}),
|
||||
'required': False,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'collapsible': False,
|
||||
'name': 'speed',
|
||||
'required': True,
|
||||
'optional': True,
|
||||
'required': False,
|
||||
'schema': list([
|
||||
dict({
|
||||
'schema': list([
|
||||
|
||||
@@ -109,6 +109,72 @@ async def test_fan_step(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
await knx.assert_telegram_count(0)
|
||||
|
||||
|
||||
async def test_fan_switch(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
"""Test KNX fan with switch only."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
FanSchema.PLATFORM: {
|
||||
CONF_NAME: "test",
|
||||
FanSchema.CONF_SWITCH_ADDRESS: "1/2/3",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# turn on fan
|
||||
await hass.services.async_call(
|
||||
"fan", "turn_on", {"entity_id": "fan.test"}, blocking=True
|
||||
)
|
||||
await knx.assert_write("1/2/3", True)
|
||||
|
||||
# turn off fan
|
||||
await hass.services.async_call(
|
||||
"fan", "turn_off", {"entity_id": "fan.test"}, blocking=True
|
||||
)
|
||||
await knx.assert_write("1/2/3", False)
|
||||
|
||||
|
||||
async def test_fan_switch_step(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
"""Test KNX fan with speed steps and switch address."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
FanSchema.PLATFORM: {
|
||||
CONF_NAME: "test",
|
||||
KNX_ADDRESS: "1/1/1",
|
||||
FanSchema.CONF_SWITCH_ADDRESS: "2/2/2",
|
||||
FanConf.MAX_STEP: 4,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# turn on fan without percentage - actuator sets default speed
|
||||
await hass.services.async_call(
|
||||
"fan", "turn_on", {"entity_id": "fan.test"}, blocking=True
|
||||
)
|
||||
await knx.assert_write("2/2/2", True)
|
||||
|
||||
# turn on with speed 75% - step 3 - turn_on sends switch ON again
|
||||
await hass.services.async_call(
|
||||
"fan", "turn_on", {"entity_id": "fan.test", "percentage": 75}, blocking=True
|
||||
)
|
||||
await knx.assert_write("2/2/2", True)
|
||||
await knx.assert_write("1/1/1", (3,))
|
||||
|
||||
# set speed to 25% - step 1 - set_percentage doesn't send switch ON
|
||||
await hass.services.async_call(
|
||||
"fan",
|
||||
"set_percentage",
|
||||
{"entity_id": "fan.test", "percentage": 25},
|
||||
blocking=True,
|
||||
)
|
||||
await knx.assert_write("1/1/1", (1,))
|
||||
|
||||
# turn off fan - no percentage change sent
|
||||
await hass.services.async_call(
|
||||
"fan", "turn_off", {"entity_id": "fan.test"}, blocking=True
|
||||
)
|
||||
await knx.assert_write("2/2/2", False)
|
||||
|
||||
|
||||
async def test_fan_oscillation(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
"""Test KNX fan oscillation."""
|
||||
await knx.setup_integration(
|
||||
@@ -153,7 +219,7 @@ async def test_fan_oscillation(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("knx_data", "expected_read_response", "expected_state"),
|
||||
[
|
||||
(
|
||||
( # percent mode fan with oscillation
|
||||
{
|
||||
"speed": {
|
||||
"ga_speed": {"write": "1/1/0", "state": "1/1/1"},
|
||||
@@ -164,7 +230,7 @@ async def test_fan_oscillation(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
[("1/1/1", (0x55,)), ("2/2/2", True)],
|
||||
{"state": STATE_ON, "percentage": 33, "oscillating": True},
|
||||
),
|
||||
(
|
||||
( # step only fan
|
||||
{
|
||||
"speed": {
|
||||
"ga_step": {"write": "1/1/0", "state": "1/1/1"},
|
||||
@@ -175,6 +241,14 @@ async def test_fan_oscillation(hass: HomeAssistant, knx: KNXTestKit) -> None:
|
||||
[("1/1/1", (2,))],
|
||||
{"state": STATE_ON, "percentage": 66},
|
||||
),
|
||||
( # switch only fan
|
||||
{
|
||||
"ga_switch": {"write": "1/1/0", "state": "1/1/1"},
|
||||
"sync_state": True,
|
||||
},
|
||||
[("1/1/1", True)],
|
||||
{"state": STATE_ON, "percentage": None},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_fan_ui_create(
|
||||
|
||||
Reference in New Issue
Block a user