2020-01-05 14:56:46 +00:00
|
|
|
"""Support for Homekit fans."""
|
2022-02-01 19:30:37 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
2020-02-24 09:55:33 +00:00
|
|
|
from aiohomekit.model.characteristics import CharacteristicsTypes
|
2022-02-01 19:30:37 +00:00
|
|
|
from aiohomekit.model.services import Service, ServicesTypes
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
from homeassistant.components.fan import (
|
|
|
|
DIRECTION_FORWARD,
|
|
|
|
DIRECTION_REVERSE,
|
|
|
|
FanEntity,
|
2022-04-06 10:04:58 +00:00
|
|
|
FanEntityFeature,
|
2020-01-05 14:56:46 +00:00
|
|
|
)
|
2021-12-28 18:24:40 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2022-10-11 21:26:03 +00:00
|
|
|
from homeassistant.const import Platform
|
2021-12-28 18:24:40 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
2022-07-05 14:25:30 +00:00
|
|
|
from homeassistant.util.percentage import (
|
|
|
|
percentage_to_ranged_value,
|
|
|
|
ranged_value_to_percentage,
|
|
|
|
)
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2022-08-06 18:10:26 +00:00
|
|
|
from . import KNOWN_DEVICES
|
2022-10-11 21:26:03 +00:00
|
|
|
from .connection import HKDevice
|
2022-08-06 18:10:26 +00:00
|
|
|
from .entity import HomeKitEntity
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
# 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that
|
|
|
|
# its consistent with homeassistant.components.homekit.
|
|
|
|
DIRECTION_TO_HK = {
|
|
|
|
DIRECTION_REVERSE: 1,
|
|
|
|
DIRECTION_FORWARD: 0,
|
|
|
|
}
|
|
|
|
HK_DIRECTION_TO_HA = {v: k for (k, v) in DIRECTION_TO_HK.items()}
|
|
|
|
|
|
|
|
|
|
|
|
class BaseHomeKitFan(HomeKitEntity, FanEntity):
|
|
|
|
"""Representation of a Homekit fan."""
|
|
|
|
|
|
|
|
# This must be set in subclasses to the name of a boolean characteristic
|
|
|
|
# that controls whether the fan is on or off.
|
2022-02-01 19:30:37 +00:00
|
|
|
on_characteristic: str
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2022-02-01 19:30:37 +00:00
|
|
|
def get_characteristic_types(self) -> list[str]:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Define the homekit characteristics the entity cares about."""
|
|
|
|
return [
|
|
|
|
CharacteristicsTypes.SWING_MODE,
|
|
|
|
CharacteristicsTypes.ROTATION_DIRECTION,
|
|
|
|
CharacteristicsTypes.ROTATION_SPEED,
|
2020-03-11 16:27:20 +00:00
|
|
|
self.on_characteristic,
|
2020-01-05 14:56:46 +00:00
|
|
|
]
|
|
|
|
|
2020-03-11 16:27:20 +00:00
|
|
|
@property
|
2022-02-01 19:30:37 +00:00
|
|
|
def is_on(self) -> bool:
|
2020-03-11 16:27:20 +00:00
|
|
|
"""Return true if device is on."""
|
|
|
|
return self.service.value(self.on_characteristic) == 1
|
|
|
|
|
2022-07-05 14:25:30 +00:00
|
|
|
@property
|
|
|
|
def _speed_range(self) -> tuple[int, int]:
|
|
|
|
"""Return the speed range."""
|
|
|
|
return (self._min_speed, self._max_speed)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def _min_speed(self) -> int:
|
|
|
|
"""Return the minimum speed."""
|
|
|
|
return (
|
|
|
|
round(self.service[CharacteristicsTypes.ROTATION_SPEED].minValue or 0) + 1
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def _max_speed(self) -> int:
|
|
|
|
"""Return the minimum speed."""
|
|
|
|
return round(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100)
|
|
|
|
|
2020-01-05 14:56:46 +00:00
|
|
|
@property
|
2022-02-01 19:30:37 +00:00
|
|
|
def percentage(self) -> int:
|
2021-01-28 10:43:43 +00:00
|
|
|
"""Return the current speed percentage."""
|
2020-01-05 14:56:46 +00:00
|
|
|
if not self.is_on:
|
2021-01-28 10:43:43 +00:00
|
|
|
return 0
|
2020-03-11 11:40:47 +00:00
|
|
|
|
2022-07-05 14:25:30 +00:00
|
|
|
return ranged_value_to_percentage(
|
|
|
|
self._speed_range, self.service.value(CharacteristicsTypes.ROTATION_SPEED)
|
|
|
|
)
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
@property
|
2022-02-01 19:30:37 +00:00
|
|
|
def current_direction(self) -> str:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Return the current direction of the fan."""
|
2020-03-11 11:40:47 +00:00
|
|
|
direction = self.service.value(CharacteristicsTypes.ROTATION_DIRECTION)
|
|
|
|
return HK_DIRECTION_TO_HA[direction]
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
@property
|
2022-02-01 19:30:37 +00:00
|
|
|
def oscillating(self) -> bool:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Return whether or not the fan is currently oscillating."""
|
2020-03-11 11:40:47 +00:00
|
|
|
oscillating = self.service.value(CharacteristicsTypes.SWING_MODE)
|
|
|
|
return oscillating == 1
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
@property
|
2022-11-17 09:30:39 +00:00
|
|
|
def supported_features(self) -> FanEntityFeature | int:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Flag supported features."""
|
2020-03-18 15:12:55 +00:00
|
|
|
features = 0
|
|
|
|
|
|
|
|
if self.service.has(CharacteristicsTypes.ROTATION_DIRECTION):
|
2022-04-06 10:04:58 +00:00
|
|
|
features |= FanEntityFeature.DIRECTION
|
2020-03-18 15:12:55 +00:00
|
|
|
|
|
|
|
if self.service.has(CharacteristicsTypes.ROTATION_SPEED):
|
2022-04-06 10:04:58 +00:00
|
|
|
features |= FanEntityFeature.SET_SPEED
|
2020-03-18 15:12:55 +00:00
|
|
|
|
|
|
|
if self.service.has(CharacteristicsTypes.SWING_MODE):
|
2022-04-06 10:04:58 +00:00
|
|
|
features |= FanEntityFeature.OSCILLATE
|
2020-03-18 15:12:55 +00:00
|
|
|
|
|
|
|
return features
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2021-03-08 18:43:22 +00:00
|
|
|
@property
|
2022-02-01 19:30:37 +00:00
|
|
|
def speed_count(self) -> int:
|
2021-03-08 18:43:22 +00:00
|
|
|
"""Speed count for the fan."""
|
|
|
|
return round(
|
2022-07-05 14:25:30 +00:00
|
|
|
min(self._max_speed, 100)
|
2021-05-25 21:06:17 +00:00
|
|
|
/ max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0)
|
2021-03-08 18:43:22 +00:00
|
|
|
)
|
|
|
|
|
2022-02-01 19:30:37 +00:00
|
|
|
async def async_set_direction(self, direction: str) -> None:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Set the direction of the fan."""
|
2020-03-11 16:27:20 +00:00
|
|
|
await self.async_put_characteristics(
|
|
|
|
{CharacteristicsTypes.ROTATION_DIRECTION: DIRECTION_TO_HK[direction]}
|
2020-01-06 16:10:51 +00:00
|
|
|
)
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2022-02-01 19:30:37 +00:00
|
|
|
async def async_set_percentage(self, percentage: int) -> None:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Set the speed of the fan."""
|
2021-01-28 10:43:43 +00:00
|
|
|
if percentage == 0:
|
2020-01-05 14:56:46 +00:00
|
|
|
return await self.async_turn_off()
|
|
|
|
|
2020-03-11 16:27:20 +00:00
|
|
|
await self.async_put_characteristics(
|
2022-07-05 14:25:30 +00:00
|
|
|
{
|
|
|
|
CharacteristicsTypes.ROTATION_SPEED: round(
|
|
|
|
percentage_to_ranged_value(self._speed_range, percentage)
|
|
|
|
)
|
|
|
|
}
|
2020-01-06 16:10:51 +00:00
|
|
|
)
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2022-02-01 19:30:37 +00:00
|
|
|
async def async_oscillate(self, oscillating: bool) -> None:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Oscillate the fan."""
|
2020-03-11 16:27:20 +00:00
|
|
|
await self.async_put_characteristics(
|
|
|
|
{CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0}
|
2020-01-06 16:10:51 +00:00
|
|
|
)
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2021-01-27 23:44:36 +00:00
|
|
|
async def async_turn_on(
|
2022-02-01 19:30:37 +00:00
|
|
|
self,
|
|
|
|
percentage: int | None = None,
|
|
|
|
preset_mode: str | None = None,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> None:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Turn the specified fan on."""
|
2022-02-01 19:30:37 +00:00
|
|
|
characteristics: dict[str, Any] = {}
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
if not self.is_on:
|
2020-03-11 16:27:20 +00:00
|
|
|
characteristics[self.on_characteristic] = True
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2022-04-06 10:04:58 +00:00
|
|
|
if (
|
|
|
|
percentage is not None
|
|
|
|
and self.supported_features & FanEntityFeature.SET_SPEED
|
|
|
|
):
|
2022-07-05 14:25:30 +00:00
|
|
|
characteristics[CharacteristicsTypes.ROTATION_SPEED] = round(
|
|
|
|
percentage_to_ranged_value(self._speed_range, percentage)
|
|
|
|
)
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2020-03-11 16:27:20 +00:00
|
|
|
if characteristics:
|
|
|
|
await self.async_put_characteristics(characteristics)
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2022-02-01 19:30:37 +00:00
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Turn the specified fan off."""
|
2020-03-11 16:27:20 +00:00
|
|
|
await self.async_put_characteristics({self.on_characteristic: False})
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HomeKitFanV1(BaseHomeKitFan):
|
|
|
|
"""Implement fan support for public.hap.service.fan."""
|
|
|
|
|
2020-03-11 16:27:20 +00:00
|
|
|
on_characteristic = CharacteristicsTypes.ON
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HomeKitFanV2(BaseHomeKitFan):
|
|
|
|
"""Implement fan support for public.hap.service.fanv2."""
|
|
|
|
|
2020-03-11 16:27:20 +00:00
|
|
|
on_characteristic = CharacteristicsTypes.ACTIVE
|
2020-01-05 14:56:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
ENTITY_TYPES = {
|
2020-11-16 23:11:39 +00:00
|
|
|
ServicesTypes.FAN: HomeKitFanV1,
|
|
|
|
ServicesTypes.FAN_V2: HomeKitFanV2,
|
2020-01-05 14:56:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-12-28 18:24:40 +00:00
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
config_entry: ConfigEntry,
|
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
) -> None:
|
2020-01-05 14:56:46 +00:00
|
|
|
"""Set up Homekit fans."""
|
2022-10-11 21:26:03 +00:00
|
|
|
hkid: str = config_entry.data["AccessoryPairingID"]
|
|
|
|
conn: HKDevice = hass.data[KNOWN_DEVICES][hkid]
|
2020-01-05 14:56:46 +00:00
|
|
|
|
2020-01-29 21:59:45 +00:00
|
|
|
@callback
|
2022-02-01 19:30:37 +00:00
|
|
|
def async_add_service(service: Service) -> bool:
|
2022-01-31 22:48:16 +00:00
|
|
|
if not (entity_class := ENTITY_TYPES.get(service.type)):
|
2020-01-05 14:56:46 +00:00
|
|
|
return False
|
2020-11-16 23:11:39 +00:00
|
|
|
info = {"aid": service.accessory.aid, "iid": service.iid}
|
2022-10-11 21:26:03 +00:00
|
|
|
entity: HomeKitEntity = entity_class(conn, info)
|
|
|
|
conn.async_migrate_unique_id(
|
|
|
|
entity.old_unique_id, entity.unique_id, Platform.FAN
|
|
|
|
)
|
|
|
|
async_add_entities([entity])
|
2020-01-05 14:56:46 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
conn.add_listener(async_add_service)
|