Add a select entity for homekit temperature display units (#100853)

This commit is contained in:
Jc2k 2023-09-25 14:53:01 +01:00 committed by GitHub
parent 23b239ba77
commit 7334bc7c9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 200 additions and 13 deletions

View file

@ -101,6 +101,7 @@ CHARACTERISTIC_PLATFORMS = {
CharacteristicsTypes.MUTE: "switch", CharacteristicsTypes.MUTE: "switch",
CharacteristicsTypes.FILTER_LIFE_LEVEL: "sensor", CharacteristicsTypes.FILTER_LIFE_LEVEL: "sensor",
CharacteristicsTypes.VENDOR_AIRVERSA_SLEEP_MODE: "switch", CharacteristicsTypes.VENDOR_AIRVERSA_SLEEP_MODE: "switch",
CharacteristicsTypes.TEMPERATURE_UNITS: "select",
} }
STARTUP_EXCEPTIONS = ( STARTUP_EXCEPTIONS = (

View file

@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"], "loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.0.3"], "requirements": ["aiohomekit==3.0.4"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View file

@ -1,18 +1,54 @@
"""Support for Homekit select entities.""" """Support for Homekit select entities."""
from __future__ import annotations from __future__ import annotations
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from dataclasses import dataclass
from enum import IntEnum
from homeassistant.components.select import SelectEntity from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.characteristics.const import TemperatureDisplayUnits
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import EntityCategory, Platform
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.helpers.typing import ConfigType
from . import KNOWN_DEVICES from . import KNOWN_DEVICES
from .connection import HKDevice from .connection import HKDevice
from .entity import CharacteristicEntity from .entity import CharacteristicEntity
@dataclass
class HomeKitSelectEntityDescriptionRequired:
"""Required fields for HomeKitSelectEntityDescription."""
choices: dict[str, IntEnum]
@dataclass
class HomeKitSelectEntityDescription(
SelectEntityDescription, HomeKitSelectEntityDescriptionRequired
):
"""A generic description of a select entity backed by a single characteristic."""
name: str | None = None
SELECT_ENTITIES: dict[str, HomeKitSelectEntityDescription] = {
CharacteristicsTypes.TEMPERATURE_UNITS: HomeKitSelectEntityDescription(
key="temperature_display_units",
translation_key="temperature_display_units",
name="Temperature Display Units",
icon="mdi:thermometer",
entity_category=EntityCategory.CONFIG,
choices={
"celsius": TemperatureDisplayUnits.CELSIUS,
"fahrenheit": TemperatureDisplayUnits.FAHRENHEIT,
},
),
}
_ECOBEE_MODE_TO_TEXT = { _ECOBEE_MODE_TO_TEXT = {
0: "home", 0: "home",
1: "sleep", 1: "sleep",
@ -21,7 +57,58 @@ _ECOBEE_MODE_TO_TEXT = {
_ECOBEE_MODE_TO_NUMBERS = {v: k for (k, v) in _ECOBEE_MODE_TO_TEXT.items()} _ECOBEE_MODE_TO_NUMBERS = {v: k for (k, v) in _ECOBEE_MODE_TO_TEXT.items()}
class EcobeeModeSelect(CharacteristicEntity, SelectEntity): class BaseHomeKitSelect(CharacteristicEntity, SelectEntity):
"""Base entity for select entities backed by a single characteristics."""
class HomeKitSelect(BaseHomeKitSelect):
"""Representation of a select control on a homekit accessory."""
entity_description: HomeKitSelectEntityDescription
def __init__(
self,
conn: HKDevice,
info: ConfigType,
char: Characteristic,
description: HomeKitSelectEntityDescription,
) -> None:
"""Initialise a HomeKit select control."""
self.entity_description = description
self._choice_to_enum = self.entity_description.choices
self._enum_to_choice = {
v: k for (k, v) in self.entity_description.choices.items()
}
self._attr_options = list(self.entity_description.choices.keys())
super().__init__(conn, info, char)
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [self._char.type]
@property
def name(self) -> str | None:
"""Return the name of the device if any."""
if name := self.accessory.name:
return f"{name} {self.entity_description.name}"
return self.entity_description.name
@property
def current_option(self) -> str | None:
"""Return the current selected option."""
return self._enum_to_choice.get(self._char.value)
async def async_select_option(self, option: str) -> None:
"""Set the current option."""
await self.async_put_characteristics(
{self._char.type: self._choice_to_enum[option]}
)
class EcobeeModeSelect(BaseHomeKitSelect):
"""Represents a ecobee mode select entity.""" """Represents a ecobee mode select entity."""
_attr_options = ["home", "sleep", "away"] _attr_options = ["home", "sleep", "away"]
@ -64,14 +151,23 @@ async def async_setup_entry(
@callback @callback
def async_add_characteristic(char: Characteristic) -> bool: def async_add_characteristic(char: Characteristic) -> bool:
if char.type == CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE: entities: list[BaseHomeKitSelect] = []
info = {"aid": char.service.accessory.aid, "iid": char.service.iid} info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
entity = EcobeeModeSelect(conn, info, char)
if description := SELECT_ENTITIES.get(char.type):
entities.append(HomeKitSelect(conn, info, char, description))
elif char.type == CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE:
entities.append(EcobeeModeSelect(conn, info, char))
if not entities:
return False
for entity in entities:
conn.async_migrate_unique_id( conn.async_migrate_unique_id(
entity.old_unique_id, entity.unique_id, Platform.SELECT entity.old_unique_id, entity.unique_id, Platform.SELECT
) )
async_add_entities([entity])
return True async_add_entities(entities)
return False return True
conn.add_char_factory(async_add_characteristic) conn.add_char_factory(async_add_characteristic)

View file

@ -102,6 +102,12 @@
"home": "[%key:common::state::home%]", "home": "[%key:common::state::home%]",
"sleep": "Sleep" "sleep": "Sleep"
} }
},
"temperature_display_units": {
"state": {
"celsius": "Celsius",
"fahrenheit": "Fahrenheit"
}
} }
}, },
"sensor": { "sensor": {

View file

@ -250,7 +250,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.0.3 aiohomekit==3.0.4
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http

View file

@ -228,7 +228,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.0.3 aiohomekit==3.0.4
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http

View file

@ -1,6 +1,7 @@
"""Basic checks for HomeKit select entities.""" """Basic checks for HomeKit select entities."""
from aiohomekit.model import Accessory from aiohomekit.model import Accessory
from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics.const import TemperatureDisplayUnits
from aiohomekit.model.services import ServicesTypes from aiohomekit.model.services import ServicesTypes
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -22,6 +23,16 @@ def create_service_with_ecobee_mode(accessory: Accessory):
return service return service
def create_service_with_temperature_units(accessory: Accessory):
"""Define a thermostat with ecobee mode characteristics."""
service = accessory.add_service(ServicesTypes.TEMPERATURE_SENSOR, add_required=True)
units = service.add_char(CharacteristicsTypes.TEMPERATURE_UNITS)
units.value = 0
return service
async def test_migrate_unique_id(hass: HomeAssistant, utcnow) -> None: async def test_migrate_unique_id(hass: HomeAssistant, utcnow) -> None:
"""Test we can migrate a select unique id.""" """Test we can migrate a select unique id."""
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -125,3 +136,76 @@ async def test_write_current_mode(hass: HomeAssistant, utcnow) -> None:
ServicesTypes.THERMOSTAT, ServicesTypes.THERMOSTAT,
{CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE: 2}, {CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE: 2},
) )
async def test_read_select(hass: HomeAssistant, utcnow) -> None:
"""Test the generic select can read the current value."""
helper = await setup_test_component(hass, create_service_with_temperature_units)
# Helper will be for the primary entity, which is the service. Make a helper for the sensor.
select_entity = Helper(
hass,
"select.testdevice_temperature_display_units",
helper.pairing,
helper.accessory,
helper.config_entry,
)
state = await select_entity.async_update(
ServicesTypes.TEMPERATURE_SENSOR,
{
CharacteristicsTypes.TEMPERATURE_UNITS: 0,
},
)
assert state.state == "celsius"
state = await select_entity.async_update(
ServicesTypes.TEMPERATURE_SENSOR,
{
CharacteristicsTypes.TEMPERATURE_UNITS: 1,
},
)
assert state.state == "fahrenheit"
async def test_write_select(hass: HomeAssistant, utcnow) -> None:
"""Test can set a value."""
helper = await setup_test_component(hass, create_service_with_temperature_units)
helper.accessory.services.first(service_type=ServicesTypes.THERMOSTAT)
# Helper will be for the primary entity, which is the service. Make a helper for the sensor.
current_mode = Helper(
hass,
"select.testdevice_temperature_display_units",
helper.pairing,
helper.accessory,
helper.config_entry,
)
await hass.services.async_call(
"select",
"select_option",
{
"entity_id": "select.testdevice_temperature_display_units",
"option": "fahrenheit",
},
blocking=True,
)
current_mode.async_assert_service_values(
ServicesTypes.TEMPERATURE_SENSOR,
{CharacteristicsTypes.TEMPERATURE_UNITS: TemperatureDisplayUnits.FAHRENHEIT},
)
await hass.services.async_call(
"select",
"select_option",
{
"entity_id": "select.testdevice_temperature_display_units",
"option": "celsius",
},
blocking=True,
)
current_mode.async_assert_service_values(
ServicesTypes.TEMPERATURE_SENSOR,
{CharacteristicsTypes.TEMPERATURE_UNITS: TemperatureDisplayUnits.CELSIUS},
)