Add support for Faucet services in HomeKit Controller (#129094)

This commit is contained in:
Jacob Feisley 2024-10-25 05:15:13 -04:00 committed by GitHub
parent d0f685183d
commit 7f9e5e29a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 854 additions and 2 deletions

View File

@ -50,6 +50,7 @@ HOMEKIT_ACCESSORY_DISPATCH = {
ServicesTypes.FAN_V2: "fan",
ServicesTypes.OCCUPANCY_SENSOR: "binary_sensor",
ServicesTypes.TELEVISION: "media_player",
ServicesTypes.FAUCET: "switch",
ServicesTypes.VALVE: "switch",
ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT: "camera",
ServicesTypes.DOORBELL: "event",

View File

@ -102,6 +102,27 @@ class HomeKitSwitch(HomeKitEntity, SwitchEntity):
return None
class HomeKitFaucet(HomeKitEntity, SwitchEntity):
"""Representation of a Homekit faucet."""
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity cares about."""
return [CharacteristicsTypes.ACTIVE]
@property
def is_on(self) -> bool:
"""Return true if device is on."""
return self.service.value(CharacteristicsTypes.ACTIVE)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified faucet on."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified faucet off."""
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
class HomeKitValve(HomeKitEntity, SwitchEntity):
"""Represents a valve in an irrigation system."""
@ -192,9 +213,10 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity):
)
ENTITY_TYPES: dict[str, type[HomeKitSwitch | HomeKitValve]] = {
ENTITY_TYPES: dict[str, type[HomeKitSwitch | HomeKitFaucet | HomeKitValve]] = {
ServicesTypes.SWITCH: HomeKitSwitch,
ServicesTypes.OUTLET: HomeKitSwitch,
ServicesTypes.FAUCET: HomeKitFaucet,
ServicesTypes.VALVE: HomeKitValve,
}
@ -213,7 +235,7 @@ async def async_setup_entry(
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
entity: HomeKitSwitch | HomeKitValve = entity_class(conn, info)
entity: HomeKitSwitch | HomeKitFaucet | HomeKitValve = entity_class(conn, info)
conn.async_migrate_unique_id(
entity.old_unique_id, entity.unique_id, Platform.SWITCH
)

View File

@ -0,0 +1,378 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "0000003E-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pr"],
"format": "string",
"value": "U by Moen-015F44",
"description": "Name",
"maxLen": 64
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 3,
"perms": ["pr"],
"format": "string",
"value": "Moen Incorporated",
"description": "Manufacturer",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 4,
"perms": ["pr"],
"format": "string",
"value": "TS3304",
"description": "Model",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 5,
"perms": ["pr"],
"format": "string",
"value": "**REDACTED**",
"description": "Serial Number",
"maxLen": 64
},
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 6,
"perms": ["pw"],
"format": "bool",
"description": "Identify"
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"perms": ["pr"],
"format": "string",
"value": "3.3.0",
"description": "Firmware Revision",
"maxLen": 64
}
]
},
{
"iid": 8,
"type": "000000D7-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 9,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 0,
"description": "Active",
"minValue": 0,
"maxValue": 1,
"minStep": 1
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 10,
"perms": ["pr"],
"format": "string",
"value": "u by moen",
"description": "Name",
"maxLen": 64
}
],
"linked": [11, 17, 22, 27, 32]
},
{
"iid": 11,
"type": "000000BC-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 12,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 0,
"description": "Active",
"minValue": 0,
"maxValue": 1,
"minStep": 1
},
{
"type": "00000011-0000-1000-8000-0026BB765291",
"iid": 13,
"perms": ["pr", "ev"],
"format": "float",
"value": 21.66666,
"description": "Current Temperature",
"unit": "celsius",
"minValue": 0.0,
"maxValue": 100.0,
"minStep": 0.1
},
{
"type": "000000B1-0000-1000-8000-0026BB765291",
"iid": 14,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "Current Heater Cooler State",
"minValue": 0,
"maxValue": 3,
"minStep": 1
},
{
"type": "000000B2-0000-1000-8000-0026BB765291",
"iid": 15,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 0,
"description": "Target Heater Cooler State",
"minValue": 0,
"maxValue": 2,
"minStep": 1
},
{
"type": "00000012-0000-1000-8000-0026BB765291",
"iid": 16,
"perms": ["pr", "pw", "ev"],
"format": "float",
"value": 37.77777,
"description": "Heating Threshold Temperature",
"unit": "celsius",
"minValue": 15.55556,
"maxValue": 48.88888,
"minStep": 0.1
}
]
},
{
"iid": 17,
"type": "000000D0-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 18,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 0,
"description": "Active",
"minValue": 0,
"maxValue": 1,
"minStep": 1
},
{
"type": "000000D2-0000-1000-8000-0026BB765291",
"iid": 19,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "In Use"
},
{
"type": "000000D5-0000-1000-8000-0026BB765291",
"iid": 20,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 2,
"description": "Valve Type"
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 21,
"perms": ["pr"],
"format": "string",
"value": "Outlet 1",
"description": "Name",
"maxLen": 64
}
]
},
{
"iid": 22,
"type": "000000D0-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 23,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 0,
"description": "Active",
"minValue": 0,
"maxValue": 1,
"minStep": 1
},
{
"type": "000000D2-0000-1000-8000-0026BB765291",
"iid": 24,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "In Use"
},
{
"type": "000000D5-0000-1000-8000-0026BB765291",
"iid": 25,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 2,
"description": "Valve Type"
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 26,
"perms": ["pr"],
"format": "string",
"value": "Outlet 2",
"description": "Name",
"maxLen": 64
}
]
},
{
"iid": 27,
"type": "000000D0-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 28,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 0,
"description": "Active",
"minValue": 0,
"maxValue": 1,
"minStep": 1
},
{
"type": "000000D2-0000-1000-8000-0026BB765291",
"iid": 29,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "In Use"
},
{
"type": "000000D5-0000-1000-8000-0026BB765291",
"iid": 30,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 2,
"description": "Valve Type"
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 31,
"perms": ["pr"],
"format": "string",
"value": "Outlet 3",
"description": "Name",
"maxLen": 64
}
]
},
{
"iid": 32,
"type": "000000D0-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "000000B0-0000-1000-8000-0026BB765291",
"iid": 33,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 0,
"description": "Active",
"minValue": 0,
"maxValue": 1,
"minStep": 1
},
{
"type": "000000D2-0000-1000-8000-0026BB765291",
"iid": 34,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "In Use"
},
{
"type": "000000D5-0000-1000-8000-0026BB765291",
"iid": 35,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 2,
"description": "Valve Type"
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 36,
"perms": ["pr"],
"format": "string",
"value": "Outlet 4",
"description": "Name",
"maxLen": 64
}
]
},
{
"iid": 37,
"type": "00000010-0000-1000-8000-001D4B474349",
"characteristics": [
{
"type": "00000011-0000-1000-8000-001D4B474349",
"iid": 38,
"perms": ["pr", "ev", "hd"],
"format": "uint8",
"value": 1
},
{
"type": "00000012-0000-1000-8000-001D4B474349",
"iid": 39,
"perms": ["pw", "hd"],
"format": "uint8"
},
{
"type": "00000013-0000-1000-8000-001D4B474349",
"iid": 40,
"perms": ["pw", "hd"],
"format": "string",
"maxLen": 64
},
{
"type": "00000014-0000-1000-8000-001D4B474349",
"iid": 41,
"perms": ["pw", "hd"],
"format": "string",
"maxLen": 64
},
{
"type": "00000015-0000-1000-8000-001D4B474349",
"iid": 42,
"perms": ["pw", "hd"],
"format": "string",
"maxLen": 64
}
]
},
{
"iid": 43,
"type": "000000A2-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 44,
"perms": ["pr"],
"format": "string",
"value": "1.1.0",
"description": "Version",
"maxLen": 64
}
]
}
]
}
]

View File

@ -17758,6 +17758,397 @@
}),
])
# ---
# name: test_snapshots[u_by_moen_ts3304]
list([
dict({
'device': dict({
'area_id': None,
'config_entries': list([
'TestData',
]),
'configuration_url': None,
'connections': list([
]),
'disabled_by': None,
'entry_type': None,
'hw_version': '',
'identifiers': list([
list([
'homekit_controller:accessory-id',
'00:00:00:00:00:00:aid:1',
]),
]),
'is_new': False,
'labels': list([
]),
'manufacturer': 'Moen Incorporated',
'model': 'TS3304',
'model_id': None,
'name': 'U by Moen-015F44',
'name_by_user': None,
'primary_config_entry': 'TestData',
'serial_number': '**REDACTED**',
'suggested_area': None,
'sw_version': '3.3.0',
}),
'entities': list([
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': None,
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.u_by_moen_015f44_identify',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'U by Moen-015F44 Identify',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:00:00:00:00:00_1_1_6',
'unit_of_measurement': None,
}),
'state': dict({
'attributes': dict({
'device_class': 'identify',
'friendly_name': 'U by Moen-015F44 Identify',
}),
'entity_id': 'button.u_by_moen_015f44_identify',
'state': 'unknown',
}),
}),
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
'target_temp_step': 1.0,
}),
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.u_by_moen_015f44',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'U by Moen-015F44',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 385>,
'translation_key': None,
'unique_id': '00:00:00:00:00:00_1_11',
'unit_of_measurement': None,
}),
'state': dict({
'attributes': dict({
'current_temperature': 21.7,
'friendly_name': 'U by Moen-015F44',
'hvac_action': <HVACAction.OFF: 'off'>,
'hvac_modes': list([
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 385>,
'target_temp_step': 1.0,
'temperature': None,
}),
'entity_id': 'climate.u_by_moen_015f44',
'state': 'off',
}),
}),
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.u_by_moen_015f44_current_temperature',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'U by Moen-015F44 Current Temperature',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:00:00:00:00:00_1_11_13',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'state': dict({
'attributes': dict({
'device_class': 'temperature',
'friendly_name': 'U by Moen-015F44 Current Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'entity_id': 'sensor.u_by_moen_015f44_current_temperature',
'state': '21.66666',
}),
}),
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': None,
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.u_by_moen_015f44',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'U by Moen-015F44',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:00:00:00:00:00_1_8',
'unit_of_measurement': None,
}),
'state': dict({
'attributes': dict({
'friendly_name': 'U by Moen-015F44',
}),
'entity_id': 'switch.u_by_moen_015f44',
'state': 'off',
}),
}),
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': None,
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.u_by_moen_015f44_outlet_1',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'U by Moen-015F44 Outlet 1',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'valve',
'unique_id': '00:00:00:00:00:00_1_17',
'unit_of_measurement': None,
}),
'state': dict({
'attributes': dict({
'friendly_name': 'U by Moen-015F44 Outlet 1',
'in_use': False,
}),
'entity_id': 'switch.u_by_moen_015f44_outlet_1',
'state': 'off',
}),
}),
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': None,
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.u_by_moen_015f44_outlet_2',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'U by Moen-015F44 Outlet 2',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'valve',
'unique_id': '00:00:00:00:00:00_1_22',
'unit_of_measurement': None,
}),
'state': dict({
'attributes': dict({
'friendly_name': 'U by Moen-015F44 Outlet 2',
'in_use': False,
}),
'entity_id': 'switch.u_by_moen_015f44_outlet_2',
'state': 'off',
}),
}),
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': None,
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.u_by_moen_015f44_outlet_3',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'U by Moen-015F44 Outlet 3',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'valve',
'unique_id': '00:00:00:00:00:00_1_27',
'unit_of_measurement': None,
}),
'state': dict({
'attributes': dict({
'friendly_name': 'U by Moen-015F44 Outlet 3',
'in_use': False,
}),
'entity_id': 'switch.u_by_moen_015f44_outlet_3',
'state': 'off',
}),
}),
dict({
'entry': dict({
'aliases': list([
]),
'area_id': None,
'capabilities': None,
'categories': dict({
}),
'config_entry_id': 'TestData',
'device_class': None,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.u_by_moen_015f44_outlet_4',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'labels': list([
]),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'U by Moen-015F44 Outlet 4',
'platform': 'homekit_controller',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'valve',
'unique_id': '00:00:00:00:00:00_1_32',
'unit_of_measurement': None,
}),
'state': dict({
'attributes': dict({
'friendly_name': 'U by Moen-015F44 Outlet 4',
'in_use': False,
}),
'entity_id': 'switch.u_by_moen_015f44_outlet_4',
'state': 'off',
}),
}),
]),
}),
])
# ---
# name: test_snapshots[velux_active_netatmo_co2]
list([
dict({

View File

@ -27,6 +27,14 @@ def create_switch_service(accessory: Accessory) -> None:
outlet_in_use.value = False
def create_faucet_service(accessory: Accessory) -> None:
"""Define faucet characteristics."""
service = accessory.add_service(ServicesTypes.FAUCET)
active_char = service.add_char(CharacteristicsTypes.ACTIVE)
active_char.value = False
def create_valve_service(accessory: Accessory) -> None:
"""Define valve characteristics."""
service = accessory.add_service(ServicesTypes.VALVE)
@ -115,6 +123,58 @@ async def test_switch_read_outlet_state(
assert switch_1.attributes["outlet_in_use"] is True
async def test_faucet_change_active_state(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can turn a HomeKit outlet on and off again."""
helper = await setup_test_component(hass, get_next_aid(), create_faucet_service)
await hass.services.async_call(
"switch", "turn_on", {"entity_id": "switch.testdevice"}, blocking=True
)
helper.async_assert_service_values(
ServicesTypes.FAUCET,
{
CharacteristicsTypes.ACTIVE: 1,
},
)
await hass.services.async_call(
"switch", "turn_off", {"entity_id": "switch.testdevice"}, blocking=True
)
helper.async_assert_service_values(
ServicesTypes.FAUCET,
{
CharacteristicsTypes.ACTIVE: 0,
},
)
async def test_faucet_read_active_state(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can read the state of a HomeKit outlet accessory."""
helper = await setup_test_component(hass, get_next_aid(), create_faucet_service)
# Initial state is that the switch is off and the outlet isn't in use
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
switch_1 = await helper.async_update(
ServicesTypes.FAUCET,
{CharacteristicsTypes.ACTIVE: True},
)
assert switch_1.state == "on"
# Simulate that device switched off in the real world not via HA
switch_1 = await helper.async_update(
ServicesTypes.FAUCET,
{CharacteristicsTypes.ACTIVE: False},
)
assert switch_1.state == "off"
async def test_valve_change_active_state(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None: