diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index db9c534f708..8c20afcd06f 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -1,4 +1,6 @@ """Constants for the homekit_controller component.""" +from typing import Final + from aiohomekit.model.characteristics import CharacteristicsTypes DOMAIN = "homekit_controller" @@ -62,6 +64,7 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.Vendor.ECOBEE_SLEEP_TARGET_HEAT: "number", CharacteristicsTypes.Vendor.ECOBEE_AWAY_TARGET_COOL: "number", CharacteristicsTypes.Vendor.ECOBEE_AWAY_TARGET_HEAT: "number", + CharacteristicsTypes.Vendor.ECOBEE_CURRENT_MODE: "select", CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: "number", @@ -92,3 +95,7 @@ CHARACTERISTIC_PLATFORMS = { for k, v in list(CHARACTERISTIC_PLATFORMS.items()): value = CHARACTERISTIC_PLATFORMS.pop(k) CHARACTERISTIC_PLATFORMS[CharacteristicsTypes.get_uuid(k)] = value + + +# Device classes +DEVICE_CLASS_ECOBEE_MODE: Final = "homekit_controller__ecobee_mode" diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py new file mode 100644 index 00000000000..55c12c77820 --- /dev/null +++ b/homeassistant/components/homekit_controller/select.py @@ -0,0 +1,71 @@ +"""Support for Homekit select entities.""" +from __future__ import annotations + +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import KNOWN_DEVICES, CharacteristicEntity +from .const import DEVICE_CLASS_ECOBEE_MODE + +_ECOBEE_MODE_TO_TEXT = { + 0: "home", + 1: "sleep", + 2: "away", +} +_ECOBEE_MODE_TO_NUMBERS = {v: k for (k, v) in _ECOBEE_MODE_TO_TEXT.items()} + + +class EcobeeModeSelect(CharacteristicEntity, SelectEntity): + """Represents a ecobee mode select entity.""" + + _attr_options = ["home", "sleep", "away"] + _attr_device_class = DEVICE_CLASS_ECOBEE_MODE + + @property + def name(self) -> str: + """Return the name of the device if any.""" + if name := super().name: + return f"{name} Current Mode" + return "Current Mode" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.Vendor.ECOBEE_CURRENT_MODE, + ] + + @property + def current_option(self) -> str | None: + """Return the current selected option.""" + return _ECOBEE_MODE_TO_TEXT.get(self._char.value) + + async def async_select_option(self, option: str) -> None: + """Set the current mode.""" + option_int = _ECOBEE_MODE_TO_NUMBERS[option] + await self.async_put_characteristics( + {CharacteristicsTypes.Vendor.ECOBEE_SET_HOLD_SCHEDULE: option_int} + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Homekit select entities.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_characteristic(char: Characteristic): + if char.type == CharacteristicsTypes.Vendor.ECOBEE_CURRENT_MODE: + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + async_add_entities([EcobeeModeSelect(conn, info, char)]) + return True + return False + + conn.add_char_factory(async_add_characteristic) diff --git a/homeassistant/components/homekit_controller/strings.select.json b/homeassistant/components/homekit_controller/strings.select.json new file mode 100644 index 00000000000..83f83e56ec2 --- /dev/null +++ b/homeassistant/components/homekit_controller/strings.select.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "home": "Home", + "sleep": "Sleep", + "away": "Away" + } + } +} \ No newline at end of file diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index b2b85c70d6e..83378650b97 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -209,6 +209,13 @@ async def test_ecobee3_setup(hass): unit_of_measurement=TEMP_CELSIUS, state="21.8", ), + EntityTestInfo( + entity_id="select.homew_current_mode", + friendly_name="HomeW Current Mode", + unique_id="homekit-123456789012-aid:1-sid:16-cid:33", + capabilities={"options": ["home", "sleep", "away"]}, + state="home", + ), ], ), ) diff --git a/tests/components/homekit_controller/test_select.py b/tests/components/homekit_controller/test_select.py new file mode 100644 index 00000000000..ea22bf68c54 --- /dev/null +++ b/tests/components/homekit_controller/test_select.py @@ -0,0 +1,90 @@ +"""Basic checks for HomeKit select entities.""" +from aiohomekit.model import Accessory +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from tests.components.homekit_controller.common import Helper, setup_test_component + + +def create_service_with_ecobee_mode(accessory: Accessory): + """Define a thermostat with ecobee mode characteristics.""" + service = accessory.add_service(ServicesTypes.THERMOSTAT, add_required=True) + + current_mode = service.add_char(CharacteristicsTypes.Vendor.ECOBEE_CURRENT_MODE) + current_mode.value = 0 + + service.add_char(CharacteristicsTypes.Vendor.ECOBEE_SET_HOLD_SCHEDULE) + + return service + + +async def test_read_current_mode(hass, utcnow): + """Test that Ecobee mode can be correctly read and show as human readable text.""" + helper = await setup_test_component(hass, create_service_with_ecobee_mode) + service = 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. + energy_helper = Helper( + hass, + "select.testdevice_current_mode", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + mode = service[CharacteristicsTypes.Vendor.ECOBEE_CURRENT_MODE] + + state = await energy_helper.poll_and_get_state() + assert state.state == "home" + + mode.value = 1 + state = await energy_helper.poll_and_get_state() + assert state.state == "sleep" + + mode.value = 2 + state = await energy_helper.poll_and_get_state() + assert state.state == "away" + + +async def test_write_current_mode(hass, utcnow): + """Test can set a specific mode.""" + helper = await setup_test_component(hass, create_service_with_ecobee_mode) + service = 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. + energy_helper = Helper( + hass, + "select.testdevice_current_mode", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + service = energy_helper.accessory.services.first( + service_type=ServicesTypes.THERMOSTAT + ) + mode = service[CharacteristicsTypes.Vendor.ECOBEE_SET_HOLD_SCHEDULE] + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.testdevice_current_mode", "option": "home"}, + blocking=True, + ) + assert mode.value == 0 + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.testdevice_current_mode", "option": "sleep"}, + blocking=True, + ) + assert mode.value == 1 + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.testdevice_current_mode", "option": "away"}, + blocking=True, + ) + assert mode.value == 2