Use configured names in HomeKit for child accessories (#142531)

This commit is contained in:
J. Nick Koston 2025-04-14 02:24:43 -10:00 committed by GitHub
parent f84f6aa713
commit 514363f1c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 135 additions and 40 deletions

View File

@ -35,6 +35,7 @@ from homeassistant.core import State, callback
from .accessories import TYPES, HomeAccessory
from .const import (
CHAR_ACTIVE,
CHAR_CONFIGURED_NAME,
CHAR_NAME,
CHAR_ON,
CHAR_ROTATION_DIRECTION,
@ -120,7 +121,9 @@ class Fan(HomeAccessory):
continue
preset_serv = self.add_preload_service(
SERV_SWITCH, CHAR_NAME, unique_id=preset_mode
SERV_SWITCH,
[CHAR_NAME, CHAR_CONFIGURED_NAME],
unique_id=preset_mode,
)
serv_fan.add_linked_service(preset_serv)
preset_serv.configure_char(
@ -129,6 +132,9 @@ class Fan(HomeAccessory):
f"{self.display_name} {preset_mode}"
),
)
preset_serv.configure_char(
CHAR_CONFIGURED_NAME, value=cleanup_name_for_homekit(preset_mode)
)
def setter_callback(value: int, preset_mode: str = preset_mode) -> None:
self.set_preset_mode(value, preset_mode)

View File

@ -41,6 +41,7 @@ from .const import (
ATTR_KEY_NAME,
CATEGORY_RECEIVER,
CHAR_ACTIVE,
CHAR_CONFIGURED_NAME,
CHAR_MUTE,
CHAR_NAME,
CHAR_ON,
@ -100,41 +101,67 @@ class MediaPlayer(HomeAccessory):
)
if FEATURE_ON_OFF in feature_list:
name = self.generate_service_name(FEATURE_ON_OFF)
serv_on_off = self.add_preload_service(
SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_ON_OFF
SERV_SWITCH, [CHAR_CONFIGURED_NAME, CHAR_NAME], unique_id=FEATURE_ON_OFF
)
serv_on_off.configure_char(
CHAR_NAME, value=self.generate_service_name(FEATURE_ON_OFF)
)
serv_on_off.configure_char(
CHAR_CONFIGURED_NAME,
value=self.generated_configured_name(FEATURE_ON_OFF),
)
serv_on_off.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char(
CHAR_ON, value=False, setter_callback=self.set_on_off
)
if FEATURE_PLAY_PAUSE in feature_list:
name = self.generate_service_name(FEATURE_PLAY_PAUSE)
serv_play_pause = self.add_preload_service(
SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_PLAY_PAUSE
SERV_SWITCH,
[CHAR_CONFIGURED_NAME, CHAR_NAME],
unique_id=FEATURE_PLAY_PAUSE,
)
serv_play_pause.configure_char(
CHAR_NAME, value=self.generate_service_name(FEATURE_PLAY_PAUSE)
)
serv_play_pause.configure_char(
CHAR_CONFIGURED_NAME,
value=self.generated_configured_name(FEATURE_PLAY_PAUSE),
)
serv_play_pause.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char(
CHAR_ON, value=False, setter_callback=self.set_play_pause
)
if FEATURE_PLAY_STOP in feature_list:
name = self.generate_service_name(FEATURE_PLAY_STOP)
serv_play_stop = self.add_preload_service(
SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_PLAY_STOP
SERV_SWITCH,
[CHAR_CONFIGURED_NAME, CHAR_NAME],
unique_id=FEATURE_PLAY_STOP,
)
serv_play_stop.configure_char(
CHAR_NAME, value=self.generate_service_name(FEATURE_PLAY_STOP)
)
serv_play_stop.configure_char(
CHAR_CONFIGURED_NAME,
value=self.generated_configured_name(FEATURE_PLAY_STOP),
)
serv_play_stop.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char(
CHAR_ON, value=False, setter_callback=self.set_play_stop
)
if FEATURE_TOGGLE_MUTE in feature_list:
name = self.generate_service_name(FEATURE_TOGGLE_MUTE)
serv_toggle_mute = self.add_preload_service(
SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_TOGGLE_MUTE
SERV_SWITCH,
[CHAR_CONFIGURED_NAME, CHAR_NAME],
unique_id=FEATURE_TOGGLE_MUTE,
)
serv_toggle_mute.configure_char(
CHAR_NAME, value=self.generate_service_name(FEATURE_TOGGLE_MUTE)
)
serv_toggle_mute.configure_char(
CHAR_CONFIGURED_NAME,
value=self.generated_configured_name(FEATURE_TOGGLE_MUTE),
)
serv_toggle_mute.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char(
CHAR_ON, value=False, setter_callback=self.set_toggle_mute
)
@ -146,6 +173,10 @@ class MediaPlayer(HomeAccessory):
f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}"
)
def generated_configured_name(self, mode: str) -> str:
"""Generate name for individual service."""
return cleanup_name_for_homekit(MODE_FRIENDLY_NAME[mode])
def set_on_off(self, value: bool) -> None:
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value)

View File

@ -49,6 +49,7 @@ from homeassistant.helpers.event import async_call_later
from .accessories import TYPES, HomeAccessory, HomeDriver
from .const import (
CHAR_ACTIVE,
CHAR_CONFIGURED_NAME,
CHAR_IN_USE,
CHAR_NAME,
CHAR_ON,
@ -360,11 +361,13 @@ class SelectSwitch(HomeAccessory):
options = state.attributes[ATTR_OPTIONS]
for option in options:
serv_option = self.add_preload_service(
SERV_OUTLET, [CHAR_NAME, CHAR_IN_USE], unique_id=option
)
serv_option.configure_char(
CHAR_NAME, value=cleanup_name_for_homekit(option)
SERV_OUTLET,
[CHAR_NAME, CHAR_CONFIGURED_NAME, CHAR_IN_USE],
unique_id=option,
)
name = cleanup_name_for_homekit(option)
serv_option.configure_char(CHAR_NAME, value=name)
serv_option.configure_char(CHAR_CONFIGURED_NAME, value=name)
serv_option.configure_char(CHAR_IN_USE, value=False)
self.select_chars[option] = serv_option.configure_char(
CHAR_ON,

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.trigger import async_initialize_triggers
from .accessories import TYPES, HomeAccessory
from .aidmanager import get_system_unique_id
from .const import (
CHAR_CONFIGURED_NAME,
CHAR_NAME,
CHAR_PROGRAMMABLE_SWITCH_EVENT,
CHAR_SERVICE_LABEL_INDEX,
@ -66,7 +67,7 @@ class DeviceTriggerAccessory(HomeAccessory):
trigger_name = cleanup_name_for_homekit(" ".join(trigger_name_parts))
serv_stateless_switch = self.add_preload_service(
SERV_STATELESS_PROGRAMMABLE_SWITCH,
[CHAR_NAME, CHAR_SERVICE_LABEL_INDEX],
[CHAR_NAME, CHAR_CONFIGURED_NAME, CHAR_SERVICE_LABEL_INDEX],
unique_id=unique_id,
)
self.triggers.append(
@ -77,6 +78,9 @@ class DeviceTriggerAccessory(HomeAccessory):
)
)
serv_stateless_switch.configure_char(CHAR_NAME, value=trigger_name)
serv_stateless_switch.configure_char(
CHAR_CONFIGURED_NAME, value=trigger_name
)
serv_stateless_switch.configure_char(
CHAR_SERVICE_LABEL_INDEX, value=idx + 1
)

View File

@ -453,7 +453,7 @@ async def test_config_entry_with_trigger_accessory(
"iid": 6,
"perms": ["pr"],
"type": "30",
"value": ANY,
"value": device_id,
},
{
"format": "string",
@ -484,8 +484,15 @@ async def test_config_entry_with_trigger_accessory(
"value": "Ceiling Lights Changed States",
},
{
"format": "uint8",
"format": "string",
"iid": 11,
"perms": ["pr", "pw", "ev"],
"type": "E3",
"value": "Ceiling Lights Changed States",
},
{
"format": "uint8",
"iid": 12,
"maxValue": 255,
"minStep": 1,
"minValue": 1,
@ -495,28 +502,28 @@ async def test_config_entry_with_trigger_accessory(
},
],
"iid": 8,
"linked": [12],
"linked": [13],
"type": "89",
},
{
"characteristics": [
{
"format": "uint8",
"iid": 13,
"iid": 14,
"perms": ["pr"],
"type": "CD",
"valid-values": [0, 1],
"value": 1,
}
],
"iid": 12,
"iid": 13,
"type": "CC",
},
{
"characteristics": [
{
"format": "uint8",
"iid": 15,
"iid": 16,
"perms": ["pr", "ev"],
"type": "73",
"valid-values": [0],
@ -524,14 +531,21 @@ async def test_config_entry_with_trigger_accessory(
},
{
"format": "string",
"iid": 16,
"iid": 17,
"perms": ["pr"],
"type": "23",
"value": "Ceiling Lights Turned Off",
},
{
"format": "string",
"iid": 18,
"perms": ["pr", "pw", "ev"],
"type": "E3",
"value": "Ceiling Lights Turned Off",
},
{
"format": "uint8",
"iid": 17,
"iid": 19,
"maxValue": 255,
"minStep": 1,
"minValue": 1,
@ -540,29 +554,29 @@ async def test_config_entry_with_trigger_accessory(
"value": 2,
},
],
"iid": 14,
"linked": [18],
"iid": 15,
"linked": [20],
"type": "89",
},
{
"characteristics": [
{
"format": "uint8",
"iid": 19,
"iid": 21,
"perms": ["pr"],
"type": "CD",
"valid-values": [0, 1],
"value": 1,
}
],
"iid": 18,
"iid": 20,
"type": "CC",
},
{
"characteristics": [
{
"format": "uint8",
"iid": 21,
"iid": 23,
"perms": ["pr", "ev"],
"type": "73",
"valid-values": [0],
@ -570,14 +584,21 @@ async def test_config_entry_with_trigger_accessory(
},
{
"format": "string",
"iid": 22,
"iid": 24,
"perms": ["pr"],
"type": "23",
"value": "Ceiling Lights Turned On",
},
{
"format": "string",
"iid": 25,
"perms": ["pr", "pw", "ev"],
"type": "E3",
"value": "Ceiling Lights Turned On",
},
{
"format": "uint8",
"iid": 23,
"iid": 26,
"maxValue": 255,
"minStep": 1,
"minValue": 1,
@ -586,22 +607,22 @@ async def test_config_entry_with_trigger_accessory(
"value": 3,
},
],
"iid": 20,
"linked": [24],
"iid": 22,
"linked": [27],
"type": "89",
},
{
"characteristics": [
{
"format": "uint8",
"iid": 25,
"iid": 28,
"perms": ["pr"],
"type": "CD",
"valid-values": [0, 1],
"value": 1,
}
],
"iid": 24,
"iid": 27,
"type": "CC",
},
],
@ -626,6 +647,7 @@ async def test_config_entry_with_trigger_accessory(
"pairing_id": ANY,
"status": 1,
}
with (
patch("pyhap.accessory_driver.AccessoryDriver.async_start"),
patch("homeassistant.components.homekit.HomeKit.async_stop"),

View File

@ -14,7 +14,13 @@ from homeassistant.components.fan import (
DOMAIN as FAN_DOMAIN,
FanEntityFeature,
)
from homeassistant.components.homekit.const import ATTR_VALUE, PROP_MIN_STEP
from homeassistant.components.homekit.accessories import HomeDriver
from homeassistant.components.homekit.const import (
ATTR_VALUE,
CHAR_CONFIGURED_NAME,
PROP_MIN_STEP,
SERV_SWITCH,
)
from homeassistant.components.homekit.type_fans import Fan
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -603,7 +609,7 @@ async def test_fan_restore(
async def test_fan_multiple_preset_modes(
hass: HomeAssistant, hk_driver, events: list[Event]
hass: HomeAssistant, hk_driver: HomeDriver, events: list[Event]
) -> None:
"""Test fan with multiple preset modes."""
entity_id = "fan.demo"
@ -623,6 +629,9 @@ async def test_fan_multiple_preset_modes(
assert acc.preset_mode_chars["auto"].value == 1
assert acc.preset_mode_chars["smart"].value == 0
switch_service = acc.get_service(SERV_SWITCH)
configured_name_char = switch_service.get_characteristic(CHAR_CONFIGURED_NAME)
assert configured_name_char.value == "auto"
acc.run()
await hass.async_block_till_done()

View File

@ -6,6 +6,7 @@ from homeassistant.components.homekit.accessories import HomeDriver
from homeassistant.components.homekit.const import (
ATTR_KEY_NAME,
ATTR_VALUE,
CHAR_CONFIGURED_NAME,
CHAR_REMOTE_KEY,
CONF_FEATURE_LIST,
EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
@ -14,6 +15,7 @@ from homeassistant.components.homekit.const import (
FEATURE_PLAY_STOP,
FEATURE_TOGGLE_MUTE,
KEY_ARROW_RIGHT,
SERV_SWITCH,
)
from homeassistant.components.homekit.type_media_players import (
MediaPlayer,
@ -74,6 +76,10 @@ async def test_media_player_set_state(
assert acc.aid == 2
assert acc.category == 8 # Switch
switch_service = acc.get_service(SERV_SWITCH)
configured_name_char = switch_service.get_characteristic(CHAR_CONFIGURED_NAME)
assert configured_name_char.value == "Power"
assert acc.chars[FEATURE_ON_OFF].value is False
assert acc.chars[FEATURE_PLAY_PAUSE].value is False
assert acc.chars[FEATURE_PLAY_STOP].value is False

View File

@ -6,6 +6,8 @@ import pytest
from homeassistant.components.homekit.const import (
ATTR_VALUE,
CHAR_CONFIGURED_NAME,
SERV_OUTLET,
TYPE_FAUCET,
TYPE_SHOWER,
TYPE_SPRINKLER,
@ -568,6 +570,10 @@ async def test_input_select_switch(
acc.run()
await hass.async_block_till_done()
switch_service = acc.get_service(SERV_OUTLET)
configured_name_char = switch_service.get_characteristic(CHAR_CONFIGURED_NAME)
assert configured_name_char.value == "option1"
assert acc.select_chars["option1"].value is True
assert acc.select_chars["option2"].value is False
assert acc.select_chars["option3"].value is False

View File

@ -3,7 +3,11 @@
from unittest.mock import MagicMock
from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.homekit.const import CHAR_PROGRAMMABLE_SWITCH_EVENT
from homeassistant.components.homekit.const import (
CHAR_CONFIGURED_NAME,
CHAR_PROGRAMMABLE_SWITCH_EVENT,
SERV_STATELESS_PROGRAMMABLE_SWITCH,
)
from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
@ -55,6 +59,10 @@ async def test_programmable_switch_button_fires_on_trigger(
assert acc.device_id is device_id
assert acc.available is True
switch_service = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH)
configured_name_char = switch_service.get_characteristic(CHAR_CONFIGURED_NAME)
assert configured_name_char.value == "ceiling lights Changed States"
hk_driver.publish.reset_mock()
hass.states.async_set("light.ceiling_lights", STATE_ON)
await hass.async_block_till_done()