diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1492e5df7b7..1879d9a6415 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -45,6 +45,7 @@ from .schema import ( ConnectionSchema, CoverSchema, ExposeSchema, + FanSchema, LightSchema, NotifySchema, SceneSchema, @@ -136,6 +137,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(SupportedPlatforms.weather.value): vol.All( cv.ensure_list, [WeatherSchema.SCHEMA] ), + vol.Optional(SupportedPlatforms.fan.value): vol.All( + cv.ensure_list, [FanSchema.SCHEMA] + ), } ), ) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index e434aed395d..6a76de6a97f 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -33,14 +33,15 @@ class ColorTempModes(Enum): class SupportedPlatforms(Enum): """Supported platforms.""" - cover = "cover" - light = "light" binary_sensor = "binary_sensor" climate = "climate" - switch = "switch" + cover = "cover" + fan = "fan" + light = "light" notify = "notify" scene = "scene" sensor = "sensor" + switch = "switch" weather = "weather" diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index c1e73733b22..20a887b628d 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -8,6 +8,7 @@ from xknx.devices import ( ClimateMode as XknxClimateMode, Cover as XknxCover, Device as XknxDevice, + Fan as XknxFan, Light as XknxLight, Notification as XknxNotification, Scene as XknxScene, @@ -24,6 +25,7 @@ from .schema import ( BinarySensorSchema, ClimateSchema, CoverSchema, + FanSchema, LightSchema, SceneSchema, SensorSchema, @@ -65,6 +67,9 @@ def create_knx_device( if platform is SupportedPlatforms.weather: return _create_weather(knx_module, config) + if platform is SupportedPlatforms.fan: + return _create_fan(knx_module, config) + def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: """Return a KNX Cover device to be used within XKNX.""" @@ -353,3 +358,20 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: ), group_address_humidity=config.get(WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS), ) + + +def _create_fan(knx_module: XKNX, config: ConfigType) -> XknxFan: + """Return a KNX Fan device to be used within XKNX.""" + + fan = XknxFan( + knx_module, + name=config[CONF_NAME], + group_address_speed=config.get(CONF_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=config.get(FanSchema.CONF_MAX_STEP), + ) + return fan diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py new file mode 100644 index 00000000000..d5dfb25ccd4 --- /dev/null +++ b/homeassistant/components/knx/fan.py @@ -0,0 +1,93 @@ +"""Support for KNX/IP fans.""" +import math +from typing import Any, Optional + +from xknx.devices import Fan as XknxFan +from xknx.devices.fan import FanSpeedMode + +from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .const import DOMAIN +from .knx_entity import KnxEntity + +DEFAULT_PERCENTAGE = 50 + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up fans for KNX platform.""" + entities = [] + for device in hass.data[DOMAIN].xknx.devices: + if isinstance(device, XknxFan): + entities.append(KNXFan(device)) + async_add_entities(entities) + + +class KNXFan(KnxEntity, FanEntity): + """Representation of a KNX fan.""" + + def __init__(self, device: XknxFan): + """Initialize of KNX fan.""" + super().__init__(device) + + if self._device.mode == FanSpeedMode.Step: + self._step_range = (1, device.max_step) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + if self._device.mode == FanSpeedMode.Step: + step = math.ceil(percentage_to_ranged_value(self._step_range, percentage)) + await self._device.set_speed(step) + else: + await self._device.set_speed(percentage) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + flags = SUPPORT_SET_SPEED + + if self._device.supports_oscillation: + flags |= SUPPORT_OSCILLATE + + return flags + + @property + def percentage(self) -> Optional[int]: + """Return the current speed as a percentage.""" + if self._device.current_speed is None: + return None + + if self._device.mode == FanSpeedMode.Step: + return ranged_value_to_percentage( + self._step_range, self._device.current_speed + ) + return self._device.current_speed + + async def async_turn_on( + self, + speed: Optional[str] = None, + percentage: Optional[int] = None, + preset_mode: Optional[str] = None, + **kwargs, + ) -> None: + """Turn on the fan.""" + if percentage is None: + await self.async_set_percentage(DEFAULT_PERCENTAGE) + else: + await self.async_set_percentage(percentage) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + await self.async_set_percentage(0) + + async def async_oscillate(self, oscillating: bool) -> None: + """Oscillate the fan.""" + await self._device.set_oscillation(oscillating) + + @property + def oscillating(self): + """Return whether or not the fan is currently oscillating.""" + return self._device.current_oscillation diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 22599014d0f..a9b65b85352 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -430,3 +430,25 @@ class WeatherSchema: vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): cv.string, } ) + + +class FanSchema: + """Voluptuous schema for KNX fans.""" + + 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" + + SCHEMA = vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_OSCILLATION_ADDRESS): cv.string, + vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): cv.string, + vol.Optional(CONF_MAX_STEP): cv.byte, + } + )