From 6d4b74f8f251afc0cc8f75a96c36c40f69436113 Mon Sep 17 00:00:00 2001 From: Jared Hobbs Date: Sat, 20 Nov 2021 08:22:10 -0700 Subject: [PATCH] Add haa vendor extensions (#59750) Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- .../components/homekit_controller/button.py | 92 +++++++++++++++++++ .../components/homekit_controller/const.py | 2 + .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_haa_fan.py | 28 ++++++ .../homekit_controller/test_button.py | 45 +++++++++ 8 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/homekit_controller/button.py create mode 100644 tests/components/homekit_controller/test_button.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3342f77cd8f..4382543675e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py new file mode 100644 index 00000000000..19dc69c50a8 --- /dev/null +++ b/homeassistant/components/homekit_controller/button.py @@ -0,0 +1,92 @@ +""" +Support for Homekit buttons. + +These are mostly used where a HomeKit accessory exposes additional non-standard +characteristics that don't map to a Home Assistant feature. +""" +from __future__ import annotations + +from dataclasses import dataclass + +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import callback + +from . import KNOWN_DEVICES, CharacteristicEntity + + +@dataclass +class HomeKitButtonEntityDescription(ButtonEntityDescription): + """Describes Homekit button.""" + + write_value: int | str | None = None + + +BUTTON_ENTITIES: dict[str, HomeKitButtonEntityDescription] = { + CharacteristicsTypes.Vendor.HAA_SETUP: HomeKitButtonEntityDescription( + key=CharacteristicsTypes.Vendor.HAA_SETUP, + name="Setup", + icon="mdi:cog", + entity_category=ENTITY_CATEGORY_CONFIG, + write_value="#HAA@trcmd", + ), + CharacteristicsTypes.Vendor.HAA_UPDATE: HomeKitButtonEntityDescription( + key=CharacteristicsTypes.Vendor.HAA_UPDATE, + name="Update", + icon="mdi:update", + entity_category=ENTITY_CATEGORY_CONFIG, + write_value="#HAA@trcmd", + ), +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit buttons.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_characteristic(char: Characteristic): + if not (description := BUTTON_ENTITIES.get(char.type)): + return False + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + async_add_entities([HomeKitButton(conn, info, char, description)], True) + return True + + conn.add_char_factory(async_add_characteristic) + + +class HomeKitButton(CharacteristicEntity, ButtonEntity): + """Representation of a Button control on a homekit accessory.""" + + entity_description = HomeKitButtonEntityDescription + + def __init__( + self, + conn, + info, + char, + description: HomeKitButtonEntityDescription, + ): + """Initialise a HomeKit button control.""" + self.entity_description = description + super().__init__(conn, info, char) + + def get_characteristic_types(self): + """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.""" + if name := super().name: + return f"{name} - {self.entity_description.name}" + return f"{self.entity_description.name}" + + async def async_press(self) -> None: + """Press the button.""" + key = self.entity_description.key + val = self.entity_description.write_value + return await self.async_put_characteristics({key: val}) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 3c9372f96db..6eb507c7214 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -51,6 +51,8 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: "number", + CharacteristicsTypes.Vendor.HAA_SETUP: "button", + CharacteristicsTypes.Vendor.HAA_UPDATE: "button", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: "sensor", CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: "number", diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3a07ae7ec8b..d9645d22a2d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.6.3"], + "requirements": ["aiohomekit==0.6.4"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 0d3c85970b0..ea0da595599 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioguardian==2021.11.0 aioharmony==0.2.8 # homeassistant.components.homekit_controller -aiohomekit==0.6.3 +aiohomekit==0.6.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d87c868f2b5..f784501a351 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -121,7 +121,7 @@ aioguardian==2021.11.0 aioharmony==0.2.8 # homeassistant.components.homekit_controller -aiohomekit==0.6.3 +aiohomekit==0.6.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 9e04434d830..0339c61168f 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -46,3 +46,31 @@ async def test_haa_fan_setup(hass): state = await helper.poll_and_get_state() assert state.attributes["friendly_name"] == "HAA-C718B3" assert round(state.attributes["percentage_step"], 2) == 33.33 + + # Check that custom HAA Setup button is created + entry = entity_registry.async_get("button.haa_c718b3_setup") + assert entry.unique_id == "homekit-C718B3-1-aid:1-sid:1010-cid:1012" + + helper = Helper( + hass, + "button.haa_c718b3_setup", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3 - Setup" + + # Check that custom HAA Update button is created + entry = entity_registry.async_get("button.haa_c718b3_update") + assert entry.unique_id == "homekit-C718B3-1-aid:1-sid:1010-cid:1011" + + helper = Helper( + hass, + "button.haa_c718b3_update", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3 - Update" diff --git a/tests/components/homekit_controller/test_button.py b/tests/components/homekit_controller/test_button.py new file mode 100644 index 00000000000..020f303ffaa --- /dev/null +++ b/tests/components/homekit_controller/test_button.py @@ -0,0 +1,45 @@ +"""Basic checks for HomeKit button.""" +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_switch_with_setup_button(accessory): + """Define setup button characteristics.""" + service = accessory.add_service(ServicesTypes.OUTLET) + + setup = service.add_char(CharacteristicsTypes.Vendor.HAA_SETUP) + + setup.value = "" + setup.format = "string" + + cur_state = service.add_char(CharacteristicsTypes.ON) + cur_state.value = True + + return service + + +async def test_press_button(hass): + """Test a switch service that has a button characteristic is correctly handled.""" + helper = await setup_test_component(hass, create_switch_with_setup_button) + + # Helper will be for the primary entity, which is the outlet. Make a helper for the button. + energy_helper = Helper( + hass, + "button.testdevice_setup", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + setup = outlet[CharacteristicsTypes.Vendor.HAA_SETUP] + + await hass.services.async_call( + "button", + "press", + {"entity_id": "button.testdevice_setup"}, + blocking=True, + ) + assert setup.value == "#HAA@trcmd"