Skip to content
Merged
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
7 changes: 7 additions & 0 deletions homeassistant/components/knx/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ class FanZeroMode(StrEnum):
Platform.CLIMATE,
Platform.COVER,
Platform.DATE,
Platform.FAN,
Platform.DATETIME,
Platform.LIGHT,
Platform.SWITCH,
Expand Down Expand Up @@ -217,3 +218,9 @@ class ClimateConf:
FAN_MAX_STEP: Final = "fan_max_step"
FAN_SPEED_MODE: Final = "fan_speed_mode"
FAN_ZERO_MODE: Final = "fan_zero_mode"


class FanConf:
"""Common config keys for fan."""

MAX_STEP: Final = "max_step"
149 changes: 115 additions & 34 deletions homeassistant/components/knx/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,36 @@
import math
from typing import Any, Final

from propcache.api import cached_property
from xknx.devices import Fan as XknxFan

from homeassistant import config_entries
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage,
)
from homeassistant.util.scaling import int_states_in_range

from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .const import CONF_SYNC_STATE, DOMAIN, KNX_ADDRESS, KNX_MODULE_KEY, FanConf
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .schema import FanSchema
from .storage.const import (
CONF_ENTITY,
CONF_GA_OSCILLATION,
CONF_GA_SPEED,
CONF_GA_STEP,
CONF_SPEED,
)
from .storage.util import ConfigExtractor

DEFAULT_PERCENTAGE: Final = 50

Expand All @@ -34,40 +46,36 @@ async def async_setup_entry(
) -> None:
"""Set up fan(s) for KNX platform."""
knx_module = hass.data[KNX_MODULE_KEY]
config: list[ConfigType] = knx_module.config_yaml[Platform.FAN]

async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config)
platform = async_get_current_platform()
knx_module.config_store.add_platform(
platform=Platform.FAN,
controller=KnxUiEntityPlatformController(
knx_module=knx_module,
entity_platform=platform,
entity_class=KnxUiFan,
),
)

entities: list[_KnxFan] = []
if yaml_platform_config := knx_module.config_yaml.get(Platform.FAN):
entities.extend(
KnxYamlFan(knx_module, entity_config)
for entity_config in yaml_platform_config
)
if ui_config := knx_module.config_store.data["entities"].get(Platform.FAN):
entities.extend(
KnxUiFan(knx_module, unique_id, config)
for unique_id, config in ui_config.items()
)
if entities:
async_add_entities(entities)


class KNXFan(KnxYamlEntity, FanEntity):
class _KnxFan(FanEntity):
"""Representation of a KNX fan."""

_device: XknxFan

def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX fan."""
max_step = config.get(FanSchema.CONF_MAX_STEP)
super().__init__(
knx_module=knx_module,
device=XknxFan(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_speed=config.get(KNX_ADDRESS),
group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS),
group_address_oscillation=config.get(
FanSchema.CONF_OSCILLATION_ADDRESS
),
group_address_oscillation_state=config.get(
FanSchema.CONF_OSCILLATION_STATE_ADDRESS
),
max_step=max_step,
),
)
# FanSpeedMode.STEP if max_step is set
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)

self._attr_unique_id = str(self._device.speed.group_address)
_step_range: tuple[int, int] | None

async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
Expand All @@ -77,7 +85,7 @@ async def async_set_percentage(self, percentage: int) -> None:
else:
await self._device.set_speed(percentage)

@property
@cached_property
def supported_features(self) -> FanEntityFeature:
"""Flag supported features."""
flags = (
Expand All @@ -103,7 +111,7 @@ def percentage(self) -> int | None:
)
return self._device.current_speed

@property
@cached_property
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
if self._step_range is None:
Expand Down Expand Up @@ -134,3 +142,76 @@ async def async_oscillate(self, oscillating: bool) -> None:
def oscillating(self) -> bool | None:
"""Return whether or not the fan is currently oscillating."""
return self._device.current_oscillation


class KnxYamlFan(_KnxFan, KnxYamlEntity):
"""Representation of a KNX fan configured from YAML."""

_device: XknxFan

def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize of KNX fan."""
max_step = config.get(FanConf.MAX_STEP)
super().__init__(
knx_module=knx_module,
device=XknxFan(
xknx=knx_module.xknx,
name=config[CONF_NAME],
group_address_speed=config.get(KNX_ADDRESS),
group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS),
group_address_oscillation=config.get(
FanSchema.CONF_OSCILLATION_ADDRESS
),
group_address_oscillation_state=config.get(
FanSchema.CONF_OSCILLATION_STATE_ADDRESS
),
max_step=max_step,
),
)
# FanSpeedMode.STEP if max_step is set
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)

self._attr_unique_id = str(self._device.speed.group_address)


class KnxUiFan(_KnxFan, KnxUiEntity):
"""Representation of a KNX fan configured from UI."""

_device: XknxFan

def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
) -> None:
"""Initialize of KNX fan."""
knx_conf = ConfigExtractor(config[DOMAIN])
# max_step is required for step mode, thus can be used to differentiate modes
max_step: int | None = knx_conf.get(CONF_SPEED, FanConf.MAX_STEP)
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
if max_step:
# step control
speed_write = knx_conf.get_write(CONF_SPEED, CONF_GA_STEP)
speed_state = knx_conf.get_state_and_passive(CONF_SPEED, CONF_GA_STEP)
else:
# percentage control
speed_write = knx_conf.get_write(CONF_SPEED, CONF_GA_SPEED)
speed_state = knx_conf.get_state_and_passive(CONF_SPEED, CONF_GA_SPEED)

self._device = XknxFan(
xknx=knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address_speed=speed_write,
group_address_speed_state=speed_state,
group_address_oscillation=knx_conf.get_write(CONF_GA_OSCILLATION),
group_address_oscillation_state=knx_conf.get_state_and_passive(
CONF_GA_OSCILLATION
),
max_step=max_step,
sync_state=knx_conf.get(CONF_SYNC_STATE),
)
# FanSpeedMode.STEP if max_step is set
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
4 changes: 2 additions & 2 deletions homeassistant/components/knx/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
ClimateConf,
ColorTempModes,
CoverConf,
FanConf,
FanZeroMode,
)
from .validation import (
Expand Down Expand Up @@ -575,7 +576,6 @@ class FanSchema(KNXPlatformSchema):
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
CONF_OSCILLATION_ADDRESS = "oscillation_address"
CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address"
CONF_MAX_STEP = "max_step"

DEFAULT_NAME = "KNX Fan"

Expand All @@ -586,7 +586,7 @@ class FanSchema(KNXPlatformSchema):
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(CONF_MAX_STEP): cv.byte,
vol.Optional(FanConf.MAX_STEP): cv.byte,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/knx/storage/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
CONF_GA_DATETIME: Final = "ga_datetime"
CONF_GA_TIME: Final = "ga_time"

CONF_GA_STEP: Final = "ga_step"

# Climate
CONF_GA_TEMPERATURE_CURRENT: Final = "ga_temperature_current"
CONF_GA_HUMIDITY_CURRENT: Final = "ga_humidity_current"
Expand All @@ -42,11 +44,15 @@
# Cover
CONF_GA_UP_DOWN: Final = "ga_up_down"
CONF_GA_STOP: Final = "ga_stop"
CONF_GA_STEP: Final = "ga_step"
CONF_GA_POSITION_SET: Final = "ga_position_set"
CONF_GA_POSITION_STATE: Final = "ga_position_state"
CONF_GA_ANGLE: Final = "ga_angle"

# Fan
CONF_SPEED: Final = "speed"
CONF_GA_SPEED: Final = "ga_speed"
CONF_GA_OSCILLATION: Final = "ga_oscillation"

# Light
CONF_COLOR_TEMP_MIN: Final = "color_temp_min"
CONF_COLOR_TEMP_MAX: Final = "color_temp_max"
Expand Down
41 changes: 41 additions & 0 deletions homeassistant/components/knx/storage/entity_store_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ClimateConf,
ColorTempModes,
CoverConf,
FanConf,
FanZeroMode,
)
from .const import (
Expand Down Expand Up @@ -62,13 +63,15 @@
CONF_GA_OP_MODE_PROTECTION,
CONF_GA_OP_MODE_STANDBY,
CONF_GA_OPERATION_MODE,
CONF_GA_OSCILLATION,
CONF_GA_POSITION_SET,
CONF_GA_POSITION_STATE,
CONF_GA_RED_BRIGHTNESS,
CONF_GA_RED_SWITCH,
CONF_GA_SATURATION,
CONF_GA_SENSOR,
CONF_GA_SETPOINT_SHIFT,
CONF_GA_SPEED,
CONF_GA_STEP,
CONF_GA_STOP,
CONF_GA_SWITCH,
Expand All @@ -80,6 +83,7 @@
CONF_GA_WHITE_BRIGHTNESS,
CONF_GA_WHITE_SWITCH,
CONF_IGNORE_AUTO_MODE,
CONF_SPEED,
CONF_TARGET_TEMPERATURE,
)
from .knx_selector import (
Expand Down Expand Up @@ -220,6 +224,42 @@
}
)

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"
),
},
),
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,
),
vol.Optional(CONF_GA_OSCILLATION): GASelector(
write_required=True, valid_dpt="1"
),
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),
}
)


@unique
class LightColorMode(StrEnum):
Expand Down Expand Up @@ -513,6 +553,7 @@ class ConfClimateFanSpeedMode(StrEnum):
Platform.COVER: COVER_KNX_SCHEMA,
Platform.DATE: DATE_KNX_SCHEMA,
Platform.DATETIME: DATETIME_KNX_SCHEMA,
Platform.FAN: FAN_KNX_SCHEMA,
Platform.LIGHT: LIGHT_KNX_SCHEMA,
Platform.SWITCH: SWITCH_KNX_SCHEMA,
Platform.TIME: TIME_KNX_SCHEMA,
Expand Down
35 changes: 35 additions & 0 deletions homeassistant/components/knx/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,41 @@
}
}
},
"fan": {
"description": "The KNX fan platform is used as an interface to fan actuators.",
"knx": {
"ga_oscillation": {
"description": "Toggle oscillation of the fan.",
"label": "Oscillation"
},
"speed": {
"description": "Control the speed of the fan.",
"ga_speed": {
"description": "Group address to control the current speed of the fan as a percentage value.",
"label": "Speed"
},
"ga_step": {
"description": "Group address to control the current speed step.",
"label": "Step"
},
"max_step": {
"description": "Number of discrete fan speed steps (Off excluded).",
"label": "Fan steps"
},
"options": {
"percentage_mode": {
"description": "Set the fan speed as a percentage value (0-100%).",
"label": "Percentage"
},
"step_mode": {
"description": "Set the fan speed in discrete steps.",
"label": "Steps"
}
},
"title": "Fan speed"
}
}
},
"header": "Create new entity",
"light": {
"description": "The KNX light platform is used as an interface to dimming actuators, LED controllers, DALI gateways and similar.",
Expand Down
Loading
Loading