Improve fans in homekit_controller (#74440)
This commit is contained in:
parent
809f101f55
commit
f6cb2833ca
9 changed files with 205 additions and 198 deletions
|
@ -25,6 +25,8 @@ from homeassistant.components.climate.const import (
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_TARGET_TEMP_HIGH,
|
ATTR_TARGET_TEMP_HIGH,
|
||||||
ATTR_TARGET_TEMP_LOW,
|
ATTR_TARGET_TEMP_LOW,
|
||||||
|
FAN_AUTO,
|
||||||
|
FAN_ON,
|
||||||
SWING_OFF,
|
SWING_OFF,
|
||||||
SWING_VERTICAL,
|
SWING_VERTICAL,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
|
@ -72,6 +74,7 @@ TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS = {
|
||||||
TargetHeaterCoolerStateValues.COOL: HVACMode.COOL,
|
TargetHeaterCoolerStateValues.COOL: HVACMode.COOL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Map of hass operation modes to homekit modes
|
# Map of hass operation modes to homekit modes
|
||||||
MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()}
|
MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()}
|
||||||
|
|
||||||
|
@ -104,19 +107,65 @@ async def async_setup_entry(
|
||||||
conn.add_listener(async_add_service)
|
conn.add_listener(async_add_service)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
|
class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
"""Representation of a Homekit climate device."""
|
"""The base HomeKit Controller climate entity."""
|
||||||
|
|
||||||
|
_attr_temperature_unit = TEMP_CELSIUS
|
||||||
|
|
||||||
def get_characteristic_types(self) -> list[str]:
|
def get_characteristic_types(self) -> list[str]:
|
||||||
"""Define the homekit characteristics the entity cares about."""
|
"""Define the homekit characteristics the entity cares about."""
|
||||||
return [
|
return [
|
||||||
|
CharacteristicsTypes.TEMPERATURE_CURRENT,
|
||||||
|
CharacteristicsTypes.FAN_STATE_TARGET,
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self) -> float | None:
|
||||||
|
"""Return the current temperature."""
|
||||||
|
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_modes(self) -> list[str] | None:
|
||||||
|
"""Return the available fan modes."""
|
||||||
|
if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET):
|
||||||
|
return [FAN_ON, FAN_AUTO]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_mode(self) -> str | None:
|
||||||
|
"""Return the current fan mode."""
|
||||||
|
fan_mode = self.service.value(CharacteristicsTypes.FAN_STATE_TARGET)
|
||||||
|
return FAN_AUTO if fan_mode else FAN_ON
|
||||||
|
|
||||||
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
|
"""Turn fan to manual/auto."""
|
||||||
|
await self.async_put_characteristics(
|
||||||
|
{CharacteristicsTypes.FAN_STATE_TARGET: int(fan_mode == FAN_AUTO)}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> int:
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
features = 0
|
||||||
|
|
||||||
|
if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET):
|
||||||
|
features |= ClimateEntityFeature.FAN_MODE
|
||||||
|
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
|
||||||
|
"""Representation of a Homekit climate device."""
|
||||||
|
|
||||||
|
def get_characteristic_types(self) -> list[str]:
|
||||||
|
"""Define the homekit characteristics the entity cares about."""
|
||||||
|
return super().get_characteristic_types() + [
|
||||||
CharacteristicsTypes.ACTIVE,
|
CharacteristicsTypes.ACTIVE,
|
||||||
CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE,
|
CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE,
|
||||||
CharacteristicsTypes.TARGET_HEATER_COOLER_STATE,
|
CharacteristicsTypes.TARGET_HEATER_COOLER_STATE,
|
||||||
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD,
|
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD,
|
||||||
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD,
|
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD,
|
||||||
CharacteristicsTypes.SWING_MODE,
|
CharacteristicsTypes.SWING_MODE,
|
||||||
CharacteristicsTypes.TEMPERATURE_CURRENT,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
|
@ -162,11 +211,6 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float:
|
|
||||||
"""Return the current temperature."""
|
|
||||||
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
|
@ -321,7 +365,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
features = 0
|
features = super().supported_features
|
||||||
|
|
||||||
if self.service.has(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD):
|
if self.service.has(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD):
|
||||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
@ -334,22 +378,16 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity):
|
||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
@property
|
|
||||||
def temperature_unit(self) -> str:
|
|
||||||
"""Return the unit of measurement."""
|
|
||||||
return TEMP_CELSIUS
|
|
||||||
|
|
||||||
|
class HomeKitClimateEntity(HomeKitBaseClimateEntity):
|
||||||
class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
|
||||||
"""Representation of a Homekit climate device."""
|
"""Representation of a Homekit climate device."""
|
||||||
|
|
||||||
def get_characteristic_types(self) -> list[str]:
|
def get_characteristic_types(self) -> list[str]:
|
||||||
"""Define the homekit characteristics the entity cares about."""
|
"""Define the homekit characteristics the entity cares about."""
|
||||||
return [
|
return super().get_characteristic_types() + [
|
||||||
CharacteristicsTypes.HEATING_COOLING_CURRENT,
|
CharacteristicsTypes.HEATING_COOLING_CURRENT,
|
||||||
CharacteristicsTypes.HEATING_COOLING_TARGET,
|
CharacteristicsTypes.HEATING_COOLING_TARGET,
|
||||||
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD,
|
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD,
|
||||||
CharacteristicsTypes.TEMPERATURE_CURRENT,
|
|
||||||
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD,
|
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD,
|
||||||
CharacteristicsTypes.TEMPERATURE_TARGET,
|
CharacteristicsTypes.TEMPERATURE_TARGET,
|
||||||
CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
|
CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
|
||||||
|
@ -411,11 +449,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float | None:
|
|
||||||
"""Return the current temperature."""
|
|
||||||
return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
|
@ -558,7 +591,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
features = 0
|
features = super().supported_features
|
||||||
|
|
||||||
if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET):
|
if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET):
|
||||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
@ -573,11 +606,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
|
||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
@property
|
|
||||||
def temperature_unit(self) -> str:
|
|
||||||
"""Return the unit of measurement."""
|
|
||||||
return TEMP_CELSIUS
|
|
||||||
|
|
||||||
|
|
||||||
ENTITY_TYPES = {
|
ENTITY_TYPES = {
|
||||||
ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity,
|
ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity,
|
||||||
|
|
|
@ -15,6 +15,10 @@ from homeassistant.components.fan import (
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.util.percentage import (
|
||||||
|
percentage_to_ranged_value,
|
||||||
|
ranged_value_to_percentage,
|
||||||
|
)
|
||||||
|
|
||||||
from . import KNOWN_DEVICES, HomeKitEntity
|
from . import KNOWN_DEVICES, HomeKitEntity
|
||||||
|
|
||||||
|
@ -48,13 +52,32 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
|
||||||
"""Return true if device is on."""
|
"""Return true if device is on."""
|
||||||
return self.service.value(self.on_characteristic) == 1
|
return self.service.value(self.on_characteristic) == 1
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def percentage(self) -> int:
|
def percentage(self) -> int:
|
||||||
"""Return the current speed percentage."""
|
"""Return the current speed percentage."""
|
||||||
if not self.is_on:
|
if not self.is_on:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
return self.service.value(CharacteristicsTypes.ROTATION_SPEED)
|
return ranged_value_to_percentage(
|
||||||
|
self._speed_range, self.service.value(CharacteristicsTypes.ROTATION_SPEED)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_direction(self) -> str:
|
def current_direction(self) -> str:
|
||||||
|
@ -88,7 +111,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
|
||||||
def speed_count(self) -> int:
|
def speed_count(self) -> int:
|
||||||
"""Speed count for the fan."""
|
"""Speed count for the fan."""
|
||||||
return round(
|
return round(
|
||||||
min(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100, 100)
|
min(self._max_speed, 100)
|
||||||
/ max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0)
|
/ max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -104,7 +127,11 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
|
||||||
return await self.async_turn_off()
|
return await self.async_turn_off()
|
||||||
|
|
||||||
await self.async_put_characteristics(
|
await self.async_put_characteristics(
|
||||||
{CharacteristicsTypes.ROTATION_SPEED: percentage}
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: round(
|
||||||
|
percentage_to_ranged_value(self._speed_range, percentage)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_oscillate(self, oscillating: bool) -> None:
|
async def async_oscillate(self, oscillating: bool) -> None:
|
||||||
|
@ -129,7 +156,9 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity):
|
||||||
percentage is not None
|
percentage is not None
|
||||||
and self.supported_features & FanEntityFeature.SET_SPEED
|
and self.supported_features & FanEntityFeature.SET_SPEED
|
||||||
):
|
):
|
||||||
characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage
|
characteristics[CharacteristicsTypes.ROTATION_SPEED] = round(
|
||||||
|
percentage_to_ranged_value(self._speed_range, percentage)
|
||||||
|
)
|
||||||
|
|
||||||
if characteristics:
|
if characteristics:
|
||||||
await self.async_put_characteristics(characteristics)
|
await self.async_put_characteristics(characteristics)
|
||||||
|
|
|
@ -67,8 +67,6 @@ async def async_setup_entry(
|
||||||
|
|
||||||
if description := NUMBER_ENTITIES.get(char.type):
|
if description := NUMBER_ENTITIES.get(char.type):
|
||||||
entities.append(HomeKitNumber(conn, info, char, description))
|
entities.append(HomeKitNumber(conn, info, char, description))
|
||||||
elif entity_type := NUMBER_ENTITY_CLASSES.get(char.type):
|
|
||||||
entities.append(entity_type(conn, info, char))
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -130,72 +128,3 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity):
|
||||||
self._char.type: value,
|
self._char.type: value,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity):
|
|
||||||
"""Representation of a Number control for Ecobee Fan Mode request."""
|
|
||||||
|
|
||||||
def get_characteristic_types(self) -> list[str]:
|
|
||||||
"""Define the homekit characteristics the entity is tracking."""
|
|
||||||
return [self._char.type]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""Return the name of the device if any."""
|
|
||||||
prefix = ""
|
|
||||||
if name := super().name:
|
|
||||||
prefix = name
|
|
||||||
return f"{prefix} Fan Mode"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_min_value(self) -> float:
|
|
||||||
"""Return the minimum value."""
|
|
||||||
return self._char.minValue or DEFAULT_MIN_VALUE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_max_value(self) -> float:
|
|
||||||
"""Return the maximum value."""
|
|
||||||
return self._char.maxValue or DEFAULT_MAX_VALUE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_step(self) -> float:
|
|
||||||
"""Return the increment/decrement step."""
|
|
||||||
return self._char.minStep or DEFAULT_STEP
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float:
|
|
||||||
"""Return the current characteristic value."""
|
|
||||||
return self._char.value
|
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
|
||||||
"""Set the characteristic to this value."""
|
|
||||||
|
|
||||||
# Sending the fan mode request sometimes ends up getting ignored by ecobee
|
|
||||||
# and this might be because it the older value instead of newer, and ecobee
|
|
||||||
# thinks there is nothing to do.
|
|
||||||
# So in order to make sure that the request is executed by ecobee, we need
|
|
||||||
# to send a different value before sending the target value.
|
|
||||||
# Fan mode value is a value from 0 to 100. We send a value off by 1 first.
|
|
||||||
|
|
||||||
if value > self.min_value:
|
|
||||||
other_value = value - 1
|
|
||||||
else:
|
|
||||||
other_value = self.min_value + 1
|
|
||||||
|
|
||||||
if value != other_value:
|
|
||||||
await self.async_put_characteristics(
|
|
||||||
{
|
|
||||||
self._char.type: other_value,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.async_put_characteristics(
|
|
||||||
{
|
|
||||||
self._char.type: value,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
NUMBER_ENTITY_CLASSES: dict[str, type] = {
|
|
||||||
CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: HomeKitEcobeeFanModeNumber,
|
|
||||||
}
|
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
"perms": ["pr", "pw", "ev"],
|
"perms": ["pr", "pw", "ev"],
|
||||||
"ev": true,
|
"ev": true,
|
||||||
"format": "bool",
|
"format": "bool",
|
||||||
"value": false
|
"value": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aid": 1,
|
"aid": 1,
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
"minValue": 0,
|
"minValue": 0,
|
||||||
"maxValue": 3,
|
"maxValue": 3,
|
||||||
"minStep": 1,
|
"minStep": 1,
|
||||||
"value": 3
|
"value": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
|
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
|
SUPPORT_FAN_MODE,
|
||||||
SUPPORT_TARGET_HUMIDITY,
|
SUPPORT_TARGET_HUMIDITY,
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
@ -43,9 +44,11 @@ async def test_ecobee501_setup(hass):
|
||||||
SUPPORT_TARGET_TEMPERATURE
|
SUPPORT_TARGET_TEMPERATURE
|
||||||
| SUPPORT_TARGET_TEMPERATURE_RANGE
|
| SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||||
| SUPPORT_TARGET_HUMIDITY
|
| SUPPORT_TARGET_HUMIDITY
|
||||||
|
| SUPPORT_FAN_MODE
|
||||||
),
|
),
|
||||||
capabilities={
|
capabilities={
|
||||||
"hvac_modes": ["off", "heat", "cool", "heat_cool"],
|
"hvac_modes": ["off", "heat", "cool", "heat_cool"],
|
||||||
|
"fan_modes": ["on", "auto"],
|
||||||
"min_temp": 7.2,
|
"min_temp": 7.2,
|
||||||
"max_temp": 33.3,
|
"max_temp": 33.3,
|
||||||
"min_humidity": 20,
|
"min_humidity": 20,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Make sure that a H.A.A. fan can be setup."""
|
"""Make sure that a H.A.A. fan can be setup."""
|
||||||
|
|
||||||
from homeassistant.components.fan import SUPPORT_SET_SPEED
|
from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
|
|
||||||
from tests.components.homekit_controller.common import (
|
from tests.components.homekit_controller.common import (
|
||||||
|
@ -18,7 +18,9 @@ async def test_haa_fan_setup(hass):
|
||||||
accessories = await setup_accessories_from_file(hass, "haa_fan.json")
|
accessories = await setup_accessories_from_file(hass, "haa_fan.json")
|
||||||
await setup_test_accessories(hass, accessories)
|
await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
# FIXME: assert round(state.attributes["percentage_step"], 2) == 33.33
|
haa_fan_state = hass.states.get("fan.haa_c718b3")
|
||||||
|
attributes = haa_fan_state.attributes
|
||||||
|
assert attributes[ATTR_PERCENTAGE] == 66
|
||||||
|
|
||||||
await assert_devices_and_entities_created(
|
await assert_devices_and_entities_created(
|
||||||
hass,
|
hass,
|
||||||
|
@ -55,7 +57,7 @@ async def test_haa_fan_setup(hass):
|
||||||
entity_id="fan.haa_c718b3",
|
entity_id="fan.haa_c718b3",
|
||||||
friendly_name="HAA-C718B3",
|
friendly_name="HAA-C718B3",
|
||||||
unique_id="homekit-C718B3-1-8",
|
unique_id="homekit-C718B3-1-8",
|
||||||
state="off",
|
state="on",
|
||||||
supported_features=SUPPORT_SET_SPEED,
|
supported_features=SUPPORT_SET_SPEED,
|
||||||
capabilities={
|
capabilities={
|
||||||
"preset_modes": None,
|
"preset_modes": None,
|
||||||
|
|
|
@ -10,6 +10,7 @@ from aiohomekit.model.services import ServicesTypes
|
||||||
|
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
SERVICE_SET_HUMIDITY,
|
SERVICE_SET_HUMIDITY,
|
||||||
SERVICE_SET_HVAC_MODE,
|
SERVICE_SET_HVAC_MODE,
|
||||||
SERVICE_SET_SWING_MODE,
|
SERVICE_SET_SWING_MODE,
|
||||||
|
@ -32,6 +33,9 @@ def create_thermostat_service(accessory):
|
||||||
char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT)
|
char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT)
|
||||||
char.value = 0
|
char.value = 0
|
||||||
|
|
||||||
|
char = service.add_char(CharacteristicsTypes.FAN_STATE_TARGET)
|
||||||
|
char.value = 0
|
||||||
|
|
||||||
char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD)
|
char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD)
|
||||||
char.minValue = 15
|
char.minValue = 15
|
||||||
char.maxValue = 40
|
char.maxValue = 40
|
||||||
|
@ -144,6 +148,32 @@ async def test_climate_change_thermostat_state(hass, utcnow):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "fan_mode": "on"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.THERMOSTAT,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.FAN_STATE_TARGET: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "fan_mode": "auto"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.THERMOSTAT,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.FAN_STATE_TARGET: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_climate_check_min_max_values_per_mode(hass, utcnow):
|
async def test_climate_check_min_max_values_per_mode(hass, utcnow):
|
||||||
"""Test that we we get the appropriate min/max values for each mode."""
|
"""Test that we we get the appropriate min/max values for each mode."""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Basic checks for HomeKit motion sensors and contact sensors."""
|
"""Basic checks for HomeKit fans."""
|
||||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
from aiohomekit.model.characteristics import CharacteristicsTypes
|
||||||
from aiohomekit.model.services import ServicesTypes
|
from aiohomekit.model.services import ServicesTypes
|
||||||
|
|
||||||
|
@ -41,6 +41,20 @@ def create_fanv2_service(accessory):
|
||||||
swing_mode.value = 0
|
swing_mode.value = 0
|
||||||
|
|
||||||
|
|
||||||
|
def create_fanv2_service_non_standard_rotation_range(accessory):
|
||||||
|
"""Define fan v2 with a non-standard rotation range."""
|
||||||
|
service = accessory.add_service(ServicesTypes.FAN_V2)
|
||||||
|
|
||||||
|
cur_state = service.add_char(CharacteristicsTypes.ACTIVE)
|
||||||
|
cur_state.value = 0
|
||||||
|
|
||||||
|
speed = service.add_char(CharacteristicsTypes.ROTATION_SPEED)
|
||||||
|
speed.value = 0
|
||||||
|
speed.minValue = 0
|
||||||
|
speed.maxValue = 3
|
||||||
|
speed.minStep = 1
|
||||||
|
|
||||||
|
|
||||||
def create_fanv2_service_with_min_step(accessory):
|
def create_fanv2_service_with_min_step(accessory):
|
||||||
"""Define fan v2 characteristics as per HAP spec."""
|
"""Define fan v2 characteristics as per HAP spec."""
|
||||||
service = accessory.add_service(ServicesTypes.FAN_V2)
|
service = accessory.add_service(ServicesTypes.FAN_V2)
|
||||||
|
@ -730,3 +744,64 @@ async def test_v2_oscillate_read(hass, utcnow):
|
||||||
ServicesTypes.FAN_V2, {CharacteristicsTypes.SWING_MODE: 1}
|
ServicesTypes.FAN_V2, {CharacteristicsTypes.SWING_MODE: 1}
|
||||||
)
|
)
|
||||||
assert state.attributes["oscillating"] is True
|
assert state.attributes["oscillating"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_v2_set_percentage_non_standard_rotation_range(hass, utcnow):
|
||||||
|
"""Test that we set fan speed with a non-standard rotation range."""
|
||||||
|
helper = await setup_test_component(
|
||||||
|
hass, create_fanv2_service_non_standard_rotation_range
|
||||||
|
)
|
||||||
|
|
||||||
|
await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1})
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"fan",
|
||||||
|
"set_percentage",
|
||||||
|
{"entity_id": "fan.testdevice", "percentage": 100},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.FAN_V2,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"fan",
|
||||||
|
"set_percentage",
|
||||||
|
{"entity_id": "fan.testdevice", "percentage": 66},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.FAN_V2,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"fan",
|
||||||
|
"set_percentage",
|
||||||
|
{"entity_id": "fan.testdevice", "percentage": 33},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.FAN_V2,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"fan",
|
||||||
|
"set_percentage",
|
||||||
|
{"entity_id": "fan.testdevice", "percentage": 0},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.FAN_V2,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ACTIVE: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
@ -26,26 +26,6 @@ def create_switch_with_spray_level(accessory):
|
||||||
return service
|
return service
|
||||||
|
|
||||||
|
|
||||||
def create_switch_with_ecobee_fan_mode(accessory):
|
|
||||||
"""Define battery level characteristics."""
|
|
||||||
service = accessory.add_service(ServicesTypes.OUTLET)
|
|
||||||
|
|
||||||
ecobee_fan_mode = service.add_char(
|
|
||||||
CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED
|
|
||||||
)
|
|
||||||
|
|
||||||
ecobee_fan_mode.value = 0
|
|
||||||
ecobee_fan_mode.minStep = 1
|
|
||||||
ecobee_fan_mode.minValue = 0
|
|
||||||
ecobee_fan_mode.maxValue = 100
|
|
||||||
ecobee_fan_mode.format = "float"
|
|
||||||
|
|
||||||
cur_state = service.add_char(CharacteristicsTypes.ON)
|
|
||||||
cur_state.value = True
|
|
||||||
|
|
||||||
return service
|
|
||||||
|
|
||||||
|
|
||||||
async def test_read_number(hass, utcnow):
|
async def test_read_number(hass, utcnow):
|
||||||
"""Test a switch service that has a sensor characteristic is correctly handled."""
|
"""Test a switch service that has a sensor characteristic is correctly handled."""
|
||||||
helper = await setup_test_component(hass, create_switch_with_spray_level)
|
helper = await setup_test_component(hass, create_switch_with_spray_level)
|
||||||
|
@ -106,72 +86,3 @@ async def test_write_number(hass, utcnow):
|
||||||
ServicesTypes.OUTLET,
|
ServicesTypes.OUTLET,
|
||||||
{CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 3},
|
{CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 3},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_write_ecobee_fan_mode_number(hass, utcnow):
|
|
||||||
"""Test a switch service that has a sensor characteristic is correctly handled."""
|
|
||||||
helper = await setup_test_component(hass, create_switch_with_ecobee_fan_mode)
|
|
||||||
|
|
||||||
# Helper will be for the primary entity, which is the outlet. Make a helper for the sensor.
|
|
||||||
fan_mode = Helper(
|
|
||||||
hass,
|
|
||||||
"number.testdevice_fan_mode",
|
|
||||||
helper.pairing,
|
|
||||||
helper.accessory,
|
|
||||||
helper.config_entry,
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
"number",
|
|
||||||
"set_value",
|
|
||||||
{"entity_id": "number.testdevice_fan_mode", "value": 1},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
fan_mode.async_assert_service_values(
|
|
||||||
ServicesTypes.OUTLET,
|
|
||||||
{CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 1},
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
"number",
|
|
||||||
"set_value",
|
|
||||||
{"entity_id": "number.testdevice_fan_mode", "value": 2},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
fan_mode.async_assert_service_values(
|
|
||||||
ServicesTypes.OUTLET,
|
|
||||||
{CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 2},
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
"number",
|
|
||||||
"set_value",
|
|
||||||
{"entity_id": "number.testdevice_fan_mode", "value": 99},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
fan_mode.async_assert_service_values(
|
|
||||||
ServicesTypes.OUTLET,
|
|
||||||
{CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 99},
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
"number",
|
|
||||||
"set_value",
|
|
||||||
{"entity_id": "number.testdevice_fan_mode", "value": 100},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
fan_mode.async_assert_service_values(
|
|
||||||
ServicesTypes.OUTLET,
|
|
||||||
{CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 100},
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
"number",
|
|
||||||
"set_value",
|
|
||||||
{"entity_id": "number.testdevice_fan_mode", "value": 0},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
fan_mode.async_assert_service_values(
|
|
||||||
ServicesTypes.OUTLET,
|
|
||||||
{CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 0},
|
|
||||||
)
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue