Map heatercooler rotation speed as 3 level fan speed in homekit controller (#98291)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
207e3f90a6
commit
b8086f3c21
4 changed files with 370 additions and 0 deletions
|
@ -23,6 +23,10 @@ from homeassistant.components.climate import (
|
||||||
DEFAULT_MAX_TEMP,
|
DEFAULT_MAX_TEMP,
|
||||||
DEFAULT_MIN_TEMP,
|
DEFAULT_MIN_TEMP,
|
||||||
FAN_AUTO,
|
FAN_AUTO,
|
||||||
|
FAN_HIGH,
|
||||||
|
FAN_LOW,
|
||||||
|
FAN_MEDIUM,
|
||||||
|
FAN_OFF,
|
||||||
FAN_ON,
|
FAN_ON,
|
||||||
SWING_OFF,
|
SWING_OFF,
|
||||||
SWING_VERTICAL,
|
SWING_VERTICAL,
|
||||||
|
@ -35,6 +39,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature
|
||||||
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
|
from . import KNOWN_DEVICES
|
||||||
from .connection import HKDevice
|
from .connection import HKDevice
|
||||||
|
@ -86,6 +94,16 @@ SWING_MODE_HASS_TO_HOMEKIT = {v: k for k, v in SWING_MODE_HOMEKIT_TO_HASS.items(
|
||||||
|
|
||||||
DEFAULT_MIN_STEP: Final = 1.0
|
DEFAULT_MIN_STEP: Final = 1.0
|
||||||
|
|
||||||
|
ROTATION_SPEED_LOW = 33
|
||||||
|
ROTATION_SPEED_MEDIUM = 66
|
||||||
|
ROTATION_SPEED_HIGH = 100
|
||||||
|
|
||||||
|
HASS_FAN_MODE_TO_HOMEKIT_ROTATION = {
|
||||||
|
FAN_LOW: ROTATION_SPEED_LOW,
|
||||||
|
FAN_MEDIUM: ROTATION_SPEED_MEDIUM,
|
||||||
|
FAN_HIGH: ROTATION_SPEED_HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -170,8 +188,45 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
|
||||||
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD,
|
CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD,
|
||||||
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD,
|
CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD,
|
||||||
CharacteristicsTypes.SWING_MODE,
|
CharacteristicsTypes.SWING_MODE,
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _get_rotation_speed_range(self) -> tuple[float, float]:
|
||||||
|
rotation_speed = self.service[CharacteristicsTypes.ROTATION_SPEED]
|
||||||
|
return round(rotation_speed.minValue or 0) + 1, round(
|
||||||
|
rotation_speed.maxValue or 100
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_modes(self) -> list[str]:
|
||||||
|
"""Return the available fan modes."""
|
||||||
|
return [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_mode(self) -> str | None:
|
||||||
|
"""Return the current fan mode."""
|
||||||
|
speed_range = self._get_rotation_speed_range()
|
||||||
|
speed_percentage = ranged_value_to_percentage(
|
||||||
|
speed_range, self.service.value(CharacteristicsTypes.ROTATION_SPEED)
|
||||||
|
)
|
||||||
|
# homekit value 0 33 66 100
|
||||||
|
if speed_percentage > ROTATION_SPEED_MEDIUM:
|
||||||
|
return FAN_HIGH
|
||||||
|
if speed_percentage > ROTATION_SPEED_LOW:
|
||||||
|
return FAN_MEDIUM
|
||||||
|
if speed_percentage > 0:
|
||||||
|
return FAN_LOW
|
||||||
|
return FAN_OFF
|
||||||
|
|
||||||
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
|
"""Set new target fan mode."""
|
||||||
|
rotation = HASS_FAN_MODE_TO_HOMEKIT_ROTATION.get(fan_mode, 0)
|
||||||
|
speed_range = self._get_rotation_speed_range()
|
||||||
|
speed = round(percentage_to_ranged_value(speed_range, rotation))
|
||||||
|
await self.async_put_characteristics(
|
||||||
|
{CharacteristicsTypes.ROTATION_SPEED: speed}
|
||||||
|
)
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
@ -387,6 +442,9 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
|
||||||
if self.service.has(CharacteristicsTypes.SWING_MODE):
|
if self.service.has(CharacteristicsTypes.SWING_MODE):
|
||||||
features |= ClimateEntityFeature.SWING_MODE
|
features |= ClimateEntityFeature.SWING_MODE
|
||||||
|
|
||||||
|
if self.service.has(CharacteristicsTypes.ROTATION_SPEED):
|
||||||
|
features |= ClimateEntityFeature.FAN_MODE
|
||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"aid": 1,
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"iid": 1,
|
||||||
|
"type": "0000003E-0000-1000-8000-0026BB765291",
|
||||||
|
"characteristics": [
|
||||||
|
{
|
||||||
|
"type": "00000014-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 2,
|
||||||
|
"perms": ["pw"],
|
||||||
|
"format": "bool",
|
||||||
|
"description": "Identify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000052-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 3,
|
||||||
|
"perms": ["pr", "ev"],
|
||||||
|
"format": "string",
|
||||||
|
"value": "1.0.0",
|
||||||
|
"description": "Firmware Revision",
|
||||||
|
"maxLen": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000053-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 4,
|
||||||
|
"perms": ["pr"],
|
||||||
|
"format": "string",
|
||||||
|
"value": "1.0.0",
|
||||||
|
"description": "Hardware Revision",
|
||||||
|
"maxLen": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000020-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 5,
|
||||||
|
"perms": ["pr"],
|
||||||
|
"format": "string",
|
||||||
|
"value": "Garzola Marco",
|
||||||
|
"description": "Manufacturer",
|
||||||
|
"maxLen": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000021-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 6,
|
||||||
|
"perms": ["pr"],
|
||||||
|
"format": "string",
|
||||||
|
"value": "Daikin-fwec3a-esp32-homekit-bridge",
|
||||||
|
"description": "Model",
|
||||||
|
"maxLen": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000023-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 7,
|
||||||
|
"perms": ["pr"],
|
||||||
|
"format": "string",
|
||||||
|
"value": "Air Conditioner",
|
||||||
|
"description": "Name",
|
||||||
|
"maxLen": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000030-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 8,
|
||||||
|
"perms": ["pr"],
|
||||||
|
"format": "string",
|
||||||
|
"value": "00000001",
|
||||||
|
"description": "Serial Number",
|
||||||
|
"maxLen": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iid": 9,
|
||||||
|
"type": "000000BC-0000-1000-8000-0026BB765291",
|
||||||
|
"characteristics": [
|
||||||
|
{
|
||||||
|
"type": "000000B0-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 10,
|
||||||
|
"perms": ["pr", "pw", "ev"],
|
||||||
|
"format": "uint8",
|
||||||
|
"value": 1,
|
||||||
|
"description": "Active"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000011-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 11,
|
||||||
|
"perms": ["pr", "ev"],
|
||||||
|
"format": "float",
|
||||||
|
"value": 27.9,
|
||||||
|
"description": "Current Temperature",
|
||||||
|
"unit": "celsius",
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 99,
|
||||||
|
"minStep": 0.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "000000B1-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 12,
|
||||||
|
"perms": ["pr", "ev"],
|
||||||
|
"format": "uint8",
|
||||||
|
"value": 3,
|
||||||
|
"description": "Current Heater Cooler State"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "000000B2-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 13,
|
||||||
|
"perms": ["pr", "pw", "ev"],
|
||||||
|
"format": "uint8",
|
||||||
|
"value": 2,
|
||||||
|
"description": "Target Heater Cooler State"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "0000000D-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 14,
|
||||||
|
"perms": ["pr", "pw", "ev"],
|
||||||
|
"format": "float",
|
||||||
|
"value": 24.5,
|
||||||
|
"description": "Cooling Threshold Temperature",
|
||||||
|
"unit": "celsius",
|
||||||
|
"minValue": 18,
|
||||||
|
"maxValue": 32,
|
||||||
|
"minStep": 0.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000012-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 15,
|
||||||
|
"perms": ["pr", "pw", "ev"],
|
||||||
|
"format": "float",
|
||||||
|
"value": 24.5,
|
||||||
|
"description": "Heating Threshold Temperature",
|
||||||
|
"unit": "celsius",
|
||||||
|
"minValue": 13,
|
||||||
|
"maxValue": 27,
|
||||||
|
"minStep": 0.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000029-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 16,
|
||||||
|
"perms": ["pr", "pw", "ev"],
|
||||||
|
"format": "float",
|
||||||
|
"value": 100,
|
||||||
|
"description": "Rotation Speed",
|
||||||
|
"unit": "percentage",
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 100,
|
||||||
|
"minStep": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "00000023-0000-1000-8000-0026BB765291",
|
||||||
|
"iid": 17,
|
||||||
|
"perms": ["pr"],
|
||||||
|
"format": "string",
|
||||||
|
"value": "SlaveID 1",
|
||||||
|
"description": "Name",
|
||||||
|
"maxLen": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""Tests for handling accessories on a Homespan esp32 daikin bridge."""
|
||||||
|
from homeassistant.components.climate import ClimateEntityFeature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from ..common import (
|
||||||
|
HUB_TEST_ACCESSORY_ID,
|
||||||
|
DeviceTestInfo,
|
||||||
|
EntityTestInfo,
|
||||||
|
assert_devices_and_entities_created,
|
||||||
|
setup_accessories_from_file,
|
||||||
|
setup_test_accessories,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_homespan_daikin_bridge_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that aHomespan esp32 daikin bridge can be correctly setup in HA via HomeKit."""
|
||||||
|
accessories = await setup_accessories_from_file(hass, "homespan_daikin_bridge.json")
|
||||||
|
await setup_test_accessories(hass, accessories)
|
||||||
|
|
||||||
|
await assert_devices_and_entities_created(
|
||||||
|
hass,
|
||||||
|
DeviceTestInfo(
|
||||||
|
unique_id=HUB_TEST_ACCESSORY_ID,
|
||||||
|
name="Air Conditioner",
|
||||||
|
model="Daikin-fwec3a-esp32-homekit-bridge",
|
||||||
|
manufacturer="Garzola Marco",
|
||||||
|
sw_version="1.0.0",
|
||||||
|
hw_version="1.0.0",
|
||||||
|
serial_number="00000001",
|
||||||
|
devices=[],
|
||||||
|
entities=[
|
||||||
|
EntityTestInfo(
|
||||||
|
entity_id="climate.air_conditioner_slaveid_1",
|
||||||
|
friendly_name="Air Conditioner SlaveID 1",
|
||||||
|
unique_id="00:00:00:00:00:00_1_9",
|
||||||
|
supported_features=(
|
||||||
|
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| ClimateEntityFeature.FAN_MODE
|
||||||
|
),
|
||||||
|
capabilities={
|
||||||
|
"hvac_modes": ["heat_cool", "heat", "cool", "off"],
|
||||||
|
"min_temp": 18,
|
||||||
|
"max_temp": 32,
|
||||||
|
"target_temp_step": 0.5,
|
||||||
|
"fan_modes": ["off", "low", "medium", "high"],
|
||||||
|
},
|
||||||
|
state="cool",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
|
@ -691,6 +691,9 @@ def create_heater_cooler_service(accessory):
|
||||||
char = service.add_char(CharacteristicsTypes.SWING_MODE)
|
char = service.add_char(CharacteristicsTypes.SWING_MODE)
|
||||||
char.value = 0
|
char.value = 0
|
||||||
|
|
||||||
|
char = service.add_char(CharacteristicsTypes.ROTATION_SPEED)
|
||||||
|
char.value = 100
|
||||||
|
|
||||||
|
|
||||||
# Test heater-cooler devices
|
# Test heater-cooler devices
|
||||||
def create_heater_cooler_service_min_max(accessory):
|
def create_heater_cooler_service_min_max(accessory):
|
||||||
|
@ -867,6 +870,103 @@ async def test_heater_cooler_change_thermostat_temperature(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_heater_cooler_change_fan_speed(hass: HomeAssistant, utcnow) -> None:
|
||||||
|
"""Test that we can change the target fan speed."""
|
||||||
|
helper = await setup_test_component(hass, create_heater_cooler_service)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_HVAC_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "hvac_mode": HVACMode.COOL},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "fan_mode": "low"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.HEATER_COOLER,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 33,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "fan_mode": "medium"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.HEATER_COOLER,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 66,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
|
{"entity_id": "climate.testdevice", "fan_mode": "high"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
helper.async_assert_service_values(
|
||||||
|
ServicesTypes.HEATER_COOLER,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_heater_cooler_read_fan_speed(hass: HomeAssistant, utcnow) -> None:
|
||||||
|
"""Test that we can read the state of a HomeKit thermostat accessory."""
|
||||||
|
helper = await setup_test_component(hass, create_heater_cooler_service)
|
||||||
|
|
||||||
|
# Simulate that fan speed is off
|
||||||
|
await helper.async_update(
|
||||||
|
ServicesTypes.HEATER_COOLER,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = await helper.poll_and_get_state()
|
||||||
|
assert state.attributes["fan_mode"] == "off"
|
||||||
|
|
||||||
|
# Simulate that fan speed is low
|
||||||
|
await helper.async_update(
|
||||||
|
ServicesTypes.HEATER_COOLER,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 33,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = await helper.poll_and_get_state()
|
||||||
|
assert state.attributes["fan_mode"] == "low"
|
||||||
|
|
||||||
|
# Simulate that fan speed is medium
|
||||||
|
await helper.async_update(
|
||||||
|
ServicesTypes.HEATER_COOLER,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 66,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = await helper.poll_and_get_state()
|
||||||
|
assert state.attributes["fan_mode"] == "medium"
|
||||||
|
|
||||||
|
# Simulate that fan speed is high
|
||||||
|
await helper.async_update(
|
||||||
|
ServicesTypes.HEATER_COOLER,
|
||||||
|
{
|
||||||
|
CharacteristicsTypes.ROTATION_SPEED: 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = await helper.poll_and_get_state()
|
||||||
|
assert state.attributes["fan_mode"] == "high"
|
||||||
|
|
||||||
|
|
||||||
async def test_heater_cooler_read_thermostat_state(hass: HomeAssistant, utcnow) -> None:
|
async def test_heater_cooler_read_thermostat_state(hass: HomeAssistant, utcnow) -> None:
|
||||||
"""Test that we can read the state of a HomeKit thermostat accessory."""
|
"""Test that we can read the state of a HomeKit thermostat accessory."""
|
||||||
helper = await setup_test_component(hass, create_heater_cooler_service)
|
helper = await setup_test_component(hass, create_heater_cooler_service)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue