Allow adding new devices to an Aqara hub via homekit_controller (#62600)

This commit is contained in:
Jc2k 2021-12-22 18:49:58 +00:00 committed by GitHub
parent 6e13605cad
commit 06eec7adfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 869 additions and 10 deletions

View file

@ -48,6 +48,9 @@ HOMEKIT_ACCESSORY_DISPATCH = {
CHARACTERISTIC_PLATFORMS = {
CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME: "number",
CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME: "number",
CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE: "switch",
CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE: "switch",
CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor",
CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: "sensor",
CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: "number",

View file

@ -75,10 +75,9 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity):
@property
def name(self) -> str:
"""Return the name of the device if any."""
prefix = ""
if name := super().name:
prefix = f"{name} -"
return f"{prefix} {self.entity_description.name}"
if prefix := super().name:
return f"{prefix} {self.entity_description.name}"
return self.entity_description.name
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""

View file

@ -1,15 +1,21 @@
"""Support for Homekit switches."""
from __future__ import annotations
from dataclasses import dataclass
from aiohomekit.model.characteristics import (
Characteristic,
CharacteristicsTypes,
InUseValues,
IsConfiguredValues,
)
from aiohomekit.model.services import ServicesTypes
from homeassistant.components.switch import SwitchEntity
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory
from . import KNOWN_DEVICES, HomeKitEntity
from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity
OUTLET_IN_USE = "outlet_in_use"
@ -18,6 +24,30 @@ ATTR_IS_CONFIGURED = "is_configured"
ATTR_REMAINING_DURATION = "remaining_duration"
@dataclass
class DeclarativeSwitchEntityDescription(SwitchEntityDescription):
"""Describes Homekit button."""
true_value: bool = True
false_value: bool = False
SWITCH_ENTITIES: dict[str, DeclarativeSwitchEntityDescription] = {
CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE: DeclarativeSwitchEntityDescription(
key=CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE,
name="Pairing Mode",
icon="mdi:lock-open",
entity_category=EntityCategory.CONFIG,
),
CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE: DeclarativeSwitchEntityDescription(
key=CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE,
name="Pairing Mode",
icon="mdi:lock-open",
entity_category=EntityCategory.CONFIG,
),
}
class HomeKitSwitch(HomeKitEntity, SwitchEntity):
"""Representation of a Homekit switch."""
@ -96,6 +126,49 @@ class HomeKitValve(HomeKitEntity, SwitchEntity):
return attrs
class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity):
"""Representation of a Homekit switch backed by a single characteristic."""
def __init__(
self,
conn,
info,
char,
description: DeclarativeSwitchEntityDescription,
):
"""Initialise a HomeKit switch."""
self.entity_description = description
super().__init__(conn, info, char)
@property
def name(self) -> str:
"""Return the name of the device if any."""
if prefix := super().name:
return f"{prefix} {self.entity_description.name}"
return self.entity_description.name
def get_characteristic_types(self):
"""Define the homekit characteristics the entity cares about."""
return [self._char.type]
@property
def is_on(self):
"""Return true if device is on."""
return self._char.value == self.entity_description.true_value
async def async_turn_on(self, **kwargs):
"""Turn the specified switch on."""
await self.async_put_characteristics(
{self._char.type: self.entity_description.true_value}
)
async def async_turn_off(self, **kwargs):
"""Turn the specified switch off."""
await self.async_put_characteristics(
{self._char.type: self.entity_description.false_value}
)
ENTITY_TYPES = {
ServicesTypes.SWITCH: HomeKitSwitch,
ServicesTypes.OUTLET: HomeKitSwitch,
@ -117,3 +190,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
return True
conn.add_listener(async_add_service)
@callback
def async_add_characteristic(char: Characteristic):
if not (description := SWITCH_ENTITIES.get(char.type)):
return False
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
async_add_entities(
[DeclarativeCharacteristicSwitch(conn, info, char, description)], True
)
return True
conn.add_char_factory(async_add_characteristic)

View file

@ -0,0 +1,646 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "0000003E-0000-1000-8000-0026BB765291",
"primary": false,
"hidden": false,
"characteristics": [
{
"iid": 65537,
"type": "00000014-0000-1000-8000-0026BB765291",
"format": "bool",
"perms": [
"pw"
]
},
{
"iid": 65538,
"type": "00000020-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Aqara",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65539,
"type": "00000021-0000-1000-8000-0026BB765291",
"format": "string",
"value": "HE1-G01",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65540,
"type": "00000023-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Aqara-Hub-E1-00A0",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65541,
"type": "00000030-0000-1000-8000-0026BB765291",
"format": "string",
"value": "00aa00000a0",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65542,
"type": "00000052-0000-1000-8000-0026BB765291",
"format": "string",
"value": "3.3.0",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65543,
"type": "00000053-0000-1000-8000-0026BB765291",
"format": "string",
"value": "1.0",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65544,
"type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B",
"format": "string",
"value": "5.0;dfeceb3a",
"perms": [
"pr",
"hd"
],
"ev": false
},
{
"iid": 65545,
"type": "220",
"format": "data",
"value": "xDsGO4QdTEA=",
"perms": [
"pr"
],
"ev": false,
"maxDataLen": 8
}
]
},
{
"iid": 2,
"type": "000000A2-0000-1000-8000-0026BB765291",
"primary": false,
"hidden": false,
"characteristics": [
{
"iid": 131074,
"type": "00000037-0000-1000-8000-0026BB765291",
"format": "string",
"value": "1.1.0",
"perms": [
"pr"
],
"ev": false
}
]
},
{
"iid": 4,
"type": "22A",
"primary": false,
"hidden": false,
"characteristics": [
{
"iid": 262145,
"type": "22B",
"format": "bool",
"value": 1,
"perms": [
"pr"
],
"ev": false
},
{
"iid": 262146,
"type": "22C",
"format": "uint32",
"value": 9,
"perms": [
"pr"
],
"ev": false,
"minValue": 0,
"maxValue": 15,
"minStep": 1
},
{
"iid": 262147,
"type": "22D",
"format": "tlv8",
"value": "",
"perms": [
"pr",
"pw",
"ev",
"tw",
"wr"
],
"ev": false
}
]
},
{
"iid": 16,
"type": "0000007E-0000-1000-8000-0026BB765291",
"primary": true,
"hidden": false,
"characteristics": [
{
"iid": 1048578,
"type": "00000023-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Security System",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 1048579,
"type": "00000066-0000-1000-8000-0026BB765291",
"format": "uint8",
"value": 3,
"perms": [
"pr",
"ev"
],
"ev": true,
"minValue": 0,
"maxValue": 4,
"minStep": 1,
"valid-values": [
0,
1,
2,
3,
4
]
},
{
"iid": 1048580,
"type": "00000067-0000-1000-8000-0026BB765291",
"format": "uint8",
"value": 3,
"perms": [
"pr",
"pw",
"ev"
],
"ev": true,
"minValue": 0,
"maxValue": 3,
"minStep": 1,
"valid-values": [
0,
1,
2,
3
]
},
{
"iid": 1048581,
"type": "60CDDE6C-42B6-4C72-9719-AB2740EABE2A",
"format": "tlv8",
"value": "AAA=",
"perms": [
"pr",
"pw"
],
"ev": false,
"description": "Stay Arm Trigger Devices"
},
{
"iid": 1048582,
"type": "4AB2460A-41E4-4F05-97C3-CCFDAE1BE324",
"format": "tlv8",
"value": "AAA=",
"perms": [
"pr",
"pw"
],
"ev": false,
"description": "Alarm Trigger Devices"
},
{
"iid": 1048583,
"type": "F8296386-5A30-4AA7-838C-ED0DA9D807DF",
"format": "tlv8",
"value": "AAA=",
"perms": [
"pr",
"pw"
],
"ev": false,
"description": "Night Arm Trigger Devices"
}
]
},
{
"iid": 17,
"type": "9715BF53-AB63-4449-8DC7-2785D617390A",
"primary": false,
"hidden": true,
"characteristics": [
{
"iid": 1114114,
"type": "00000023-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Gateway",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 1114115,
"type": "4CB28907-66DF-4D9C-962C-9971ABF30EDC",
"format": "string",
"value": "1970-01-01 21:01:22+8",
"perms": [
"pr",
"pw",
"hd"
],
"ev": false,
"description": "Date and Time"
},
{
"iid": 1114116,
"type": "EE56B186-B0D3-488E-8C79-C21FC9BCF437",
"format": "int",
"value": 40,
"perms": [
"pr",
"pw",
"ev",
"hd"
],
"ev": false,
"description": "Gateway Volume",
"unit": "percentage",
"minValue": 0,
"maxValue": 100,
"minStep": 1
},
{
"iid": 1114117,
"type": "B1C09E4C-E202-4827-B863-B0F32F727CFF",
"format": "bool",
"value": 0,
"perms": [
"pr",
"pw",
"ev",
"hd"
],
"ev": false,
"description": "New Accessory Permission"
},
{
"iid": 1114118,
"type": "2CB22739-1E4C-4798-A761-BC2FAF51AFC3",
"format": "string",
"value": "",
"perms": [
"pr",
"ev",
"hd"
],
"ev": false,
"description": "Accessory Joined"
},
{
"iid": 1114119,
"type": "75D19FA9-218B-4943-997E-341E5D1C60CC",
"format": "string",
"perms": [
"pw",
"hd"
],
"description": "Remove Accessory"
},
{
"iid": 1114120,
"type": "7D943F6A-E052-4E96-A176-D17BF00E32CB",
"format": "int",
"value": -1,
"perms": [
"pr",
"ev",
"hd"
],
"ev": false,
"description": "Firmware Update Status",
"minValue": -65535,
"maxValue": 65535,
"minStep": 1
},
{
"iid": 1114121,
"type": "A45EFD52-0DB5-4C1A-9727-513FBCD8185F",
"format": "string",
"perms": [
"pw",
"hd"
],
"description": "Firmware Update URL",
"maxLen": 256
},
{
"iid": 1114122,
"type": "40F0124A-579D-40E4-865E-0EF6740EA64B",
"format": "string",
"perms": [
"pw",
"hd"
],
"description": "Firmware Update Checksum"
},
{
"iid": 1114123,
"type": "E1C20B22-E3A7-4B92-8BA3-C16E778648A7",
"format": "string",
"value": "",
"perms": [
"pr",
"ev",
"hd"
],
"ev": false,
"description": "Identify Accessory"
},
{
"iid": 1114124,
"type": "4CF1436A-755C-4377-BDB8-30BE29EB8620",
"format": "string",
"value": "Chinese",
"perms": [
"pr",
"pw",
"ev",
"hd"
],
"ev": false,
"description": "Language"
},
{
"iid": 1114125,
"type": "25D889CB-7135-4A29-B5B4-C1FFD6D2DD5C",
"format": "string",
"value": "",
"perms": [
"pr",
"pw",
"hd"
],
"ev": false,
"description": "Country Domain"
},
{
"iid": 1114126,
"type": "C7EECAA7-91D9-40EB-AD0C-FFDDE3143CB9",
"format": "string",
"value": "lumi1.00aa00000a0",
"perms": [
"pr",
"hd"
],
"ev": false,
"description": "Lumi Did"
},
{
"iid": 1114127,
"type": "80FA747E-CB45-45A4-B7BE-AA7D9964859E",
"format": "string",
"perms": [
"pw",
"hd"
],
"description": "Lumi Bindkey"
},
{
"iid": 1114128,
"type": "C3B8A329-EF0C-4739-B773-E5B7AEA52C71",
"format": "bool",
"value": 0,
"perms": [
"pr",
"hd"
],
"ev": false,
"description": "Lumi Bindstate"
}
]
}
]
},
{
"aid": 33,
"services": [
{
"iid": 1,
"type": "0000003E-0000-1000-8000-0026BB765291",
"primary": false,
"hidden": false,
"characteristics": [
{
"iid": 65537,
"type": "00000014-0000-1000-8000-0026BB765291",
"format": "bool",
"perms": [
"pw"
]
},
{
"iid": 65538,
"type": "00000020-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Aqara",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65539,
"type": "00000021-0000-1000-8000-0026BB765291",
"format": "string",
"value": "AS006",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65540,
"type": "00000023-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Contact Sensor",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65541,
"type": "00000030-0000-1000-8000-0026BB765291",
"format": "string",
"value": "158d0007c59c6a",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65542,
"type": "00000052-0000-1000-8000-0026BB765291",
"format": "string",
"value": "0",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 65543,
"type": "00000053-0000-1000-8000-0026BB765291",
"format": "string",
"value": "1.0",
"perms": [
"pr"
],
"ev": false
}
]
},
{
"iid": 4,
"type": "00000080-0000-1000-8000-0026BB765291",
"primary": true,
"hidden": false,
"characteristics": [
{
"iid": 262146,
"type": "00000023-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Contact Sensor",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 262147,
"type": "0000006A-0000-1000-8000-0026BB765291",
"format": "uint8",
"value": 0,
"perms": [
"pr",
"ev"
],
"ev": true,
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [
0,
1
]
}
]
},
{
"iid": 5,
"type": "00000096-0000-1000-8000-0026BB765291",
"primary": false,
"hidden": false,
"characteristics": [
{
"iid": 327682,
"type": "00000023-0000-1000-8000-0026BB765291",
"format": "string",
"value": "Battery Sensor",
"perms": [
"pr"
],
"ev": false
},
{
"iid": 327683,
"type": "00000068-0000-1000-8000-0026BB765291",
"format": "uint8",
"value": 100,
"perms": [
"pr",
"ev"
],
"ev": true,
"unit": "percentage",
"minValue": 0,
"maxValue": 100,
"minStep": 1
},
{
"iid": 327685,
"type": "00000079-0000-1000-8000-0026BB765291",
"format": "uint8",
"value": 0,
"perms": [
"pr",
"ev"
],
"ev": true,
"minValue": 0,
"maxValue": 1,
"minStep": 1,
"valid-values": [
0,
1
]
},
{
"iid": 327684,
"type": "0000008F-0000-1000-8000-0026BB765291",
"format": "uint8",
"value": 2,
"perms": [
"pr",
"ev"
],
"ev": true,
"minValue": 2,
"maxValue": 2,
"minStep": 1,
"valid-values": [
2
]
}
]
}
]
}
]

View file

@ -45,7 +45,14 @@ async def test_aqara_gateway_setup(hass):
(
"number.aqara_hub_1563_volume",
"homekit-0000000123456789-aid:1-sid:65536-cid:65541",
"Aqara Hub-1563 - Volume",
"Aqara Hub-1563 Volume",
None,
EntityCategory.CONFIG,
),
(
"switch.aqara_hub_1563_pairing_mode",
"homekit-0000000123456789-aid:1-sid:65536-cid:65538",
"Aqara Hub-1563 Pairing Mode",
None,
EntityCategory.CONFIG,
),
@ -80,3 +87,66 @@ async def test_aqara_gateway_setup(hass):
# All entities should be part of same device
assert len(device_ids) == 1
async def test_aqara_gateway_e1_setup(hass):
"""Test that an Aqara E1 Gateway can be correctly setup in HA."""
accessories = await setup_accessories_from_file(hass, "aqara_e1.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
sensors = [
(
"alarm_control_panel.aqara_hub_e1_00a0",
"homekit-00aa00000a0-16",
"Aqara-Hub-E1-00A0",
SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY,
None,
),
(
"number.aqara_hub_e1_00a0_volume",
"homekit-00aa00000a0-aid:1-sid:17-cid:1114116",
"Aqara-Hub-E1-00A0 Volume",
None,
EntityCategory.CONFIG,
),
(
"switch.aqara_hub_e1_00a0_pairing_mode",
"homekit-00aa00000a0-aid:1-sid:17-cid:1114117",
"Aqara-Hub-E1-00A0 Pairing Mode",
None,
EntityCategory.CONFIG,
),
]
device_ids = set()
for (entity_id, unique_id, friendly_name, supported_features, category) in sensors:
entry = entity_registry.async_get(entity_id)
assert entry.unique_id == unique_id
assert entry.entity_category == category
helper = Helper(
hass,
entity_id,
pairing,
accessories[0],
config_entry,
)
state = await helper.poll_and_get_state()
assert state.attributes["friendly_name"] == friendly_name
assert state.attributes.get("supported_features") == supported_features
device = device_registry.async_get(entry.device_id)
assert device.manufacturer == "Aqara"
assert device.name == "Aqara-Hub-E1-00A0"
assert device.model == "HE1-G01"
assert device.sw_version == "3.3.0"
assert device.via_device_id is None
device_ids.add(entry.device_id)
# All entities should be part of same device
assert len(device_ids) == 1

View file

@ -41,7 +41,7 @@ async def test_eve_degree_setup(hass):
(
"number.eve_degree_aa11_elevation",
"homekit-AA00A0A00000-aid:1-sid:30-cid:33",
"Eve Degree AA11 - Elevation",
"Eve Degree AA11 Elevation",
),
]

View file

@ -31,8 +31,7 @@ async def test_vocolinc_flowerbud_setup(hass):
)
state = await helper.poll_and_get_state()
assert (
state.attributes["friendly_name"]
== "VOCOlinc-Flowerbud-0d324b - Spray Quantity"
state.attributes["friendly_name"] == "VOCOlinc-Flowerbud-0d324b Spray Quantity"
)
device = device_registry.async_get(entry.device_id)

View file

@ -38,6 +38,14 @@ def create_valve_service(accessory):
remaining.value = 99
def create_char_switch_service(accessory):
"""Define swtch characteristics."""
service = accessory.add_service(ServicesTypes.OUTLET)
on_char = service.add_char(CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE)
on_char.value = False
async def test_switch_change_outlet_state(hass, utcnow):
"""Test that we can turn a HomeKit outlet on and off again."""
helper = await setup_test_component(hass, create_switch_service)
@ -122,3 +130,51 @@ async def test_valve_read_state(hass, utcnow):
helper.characteristics[("valve", "in-use")].value = InUseValues.NOT_IN_USE
switch_1 = await helper.poll_and_get_state()
assert switch_1.attributes["in_use"] is False
async def test_char_switch_change_state(hass, utcnow):
"""Test that we can turn a characteristic on and off again."""
helper = await setup_test_component(
hass, create_char_switch_service, suffix="pairing_mode"
)
svc = helper.accessory.services.first(service_type=ServicesTypes.OUTLET)
pairing_mode = svc[CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE]
await hass.services.async_call(
"switch",
"turn_on",
{"entity_id": "switch.testdevice_pairing_mode"},
blocking=True,
)
assert pairing_mode.value is True
await hass.services.async_call(
"switch",
"turn_off",
{"entity_id": "switch.testdevice_pairing_mode"},
blocking=True,
)
assert pairing_mode.value is False
async def test_char_switch_read_state(hass, utcnow):
"""Test that we can read the state of a HomeKit characteristic switch."""
helper = await setup_test_component(
hass, create_char_switch_service, suffix="pairing_mode"
)
svc = helper.accessory.services.first(service_type=ServicesTypes.OUTLET)
pairing_mode = svc[CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE]
# Initial state is that the switch is off
switch_1 = await helper.poll_and_get_state()
assert switch_1.state == "off"
# Simulate that someone switched on the device in the real world not via HA
pairing_mode.set_value(True)
switch_1 = await helper.poll_and_get_state()
assert switch_1.state == "on"
# Simulate that device switched off in the real world not via HA
pairing_mode.set_value(False)
switch_1 = await helper.poll_and_get_state()
assert switch_1.state == "off"