1
mirror of https://github.com/home-assistant/core synced 2024-09-25 00:41:32 +02:00

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.FILTER_LIFE_LEVEL: "sensor",
CharacteristicsTypes.VENDOR_AIRVERSA_SLEEP_MODE: "switch",
CharacteristicsTypes.TEMPERATURE_UNITS: "select",
}
STARTUP_EXCEPTIONS = (

View File

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

View File

@ -1,18 +1,54 @@
"""Support for Homekit select entities."""
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.const import Platform
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNOWN_DEVICES
from .connection import HKDevice
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 = {
0: "home",
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()}
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."""
_attr_options = ["home", "sleep", "away"]
@ -64,14 +151,23 @@ async def async_setup_entry(
@callback
def async_add_characteristic(char: Characteristic) -> bool:
if char.type == CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE:
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
entity = EcobeeModeSelect(conn, info, char)
entities: list[BaseHomeKitSelect] = []
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
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(
entity.old_unique_id, entity.unique_id, Platform.SELECT
)
async_add_entities([entity])
return True
return False
async_add_entities(entities)
return True
conn.add_char_factory(async_add_characteristic)

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
"""Basic checks for HomeKit select entities."""
from aiohomekit.model import Accessory
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics.const import TemperatureDisplayUnits
from aiohomekit.model.services import ServicesTypes
from homeassistant.core import HomeAssistant
@ -22,6 +23,16 @@ def create_service_with_ecobee_mode(accessory: Accessory):
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:
"""Test we can migrate a select unique id."""
entity_registry = er.async_get(hass)
@ -125,3 +136,76 @@ async def test_write_current_mode(hass: HomeAssistant, utcnow) -> None:
ServicesTypes.THERMOSTAT,
{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},
)