Skip to content

Commit bd882fa

Browse files
bdracoballoob
authored andcommitted
Improve stability of homekit media players (#35080)
1 parent a083eb3 commit bd882fa

File tree

5 files changed

+125
-35
lines changed

5 files changed

+125
-35
lines changed

homeassistant/components/homekit/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,12 @@ def get_accessory(hass, driver, state, aid, config):
275275

276276
elif state.domain == "media_player":
277277
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
278-
feature_list = config.get(CONF_FEATURE_LIST)
278+
feature_list = config.get(CONF_FEATURE_LIST, [])
279279

280280
if device_class == DEVICE_CLASS_TV:
281281
a_type = "TelevisionMediaPlayer"
282-
else:
283-
if feature_list and validate_media_player_features(state, feature_list):
284-
a_type = "MediaPlayer"
282+
elif validate_media_player_features(state, feature_list):
283+
a_type = "MediaPlayer"
285284

286285
elif state.domain == "sensor":
287286
device_class = state.attributes.get(ATTR_DEVICE_CLASS)

homeassistant/components/homekit/type_media_players.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
SERV_TELEVISION,
6666
SERV_TELEVISION_SPEAKER,
6767
)
68+
from .util import get_media_player_features
6869

6970
_LOGGER = logging.getLogger(__name__)
7071

@@ -84,10 +85,12 @@
8485
# 15: "Information",
8586
}
8687

88+
# Names may not contain special characters
89+
# or emjoi (/ is a special character for Apple)
8790
MODE_FRIENDLY_NAME = {
8891
FEATURE_ON_OFF: "Power",
89-
FEATURE_PLAY_PAUSE: "Play/Pause",
90-
FEATURE_PLAY_STOP: "Play/Stop",
92+
FEATURE_PLAY_PAUSE: "Play-Pause",
93+
FEATURE_PLAY_STOP: "Play-Stop",
9194
FEATURE_TOGGLE_MUTE: "Mute",
9295
}
9396

@@ -106,7 +109,9 @@ def __init__(self, *args):
106109
FEATURE_PLAY_STOP: None,
107110
FEATURE_TOGGLE_MUTE: None,
108111
}
109-
feature_list = self.config[CONF_FEATURE_LIST]
112+
feature_list = self.config.get(
113+
CONF_FEATURE_LIST, get_media_player_features(state)
114+
)
110115

111116
if FEATURE_ON_OFF in feature_list:
112117
name = self.generate_service_name(FEATURE_ON_OFF)
@@ -214,7 +219,7 @@ def update_state(self, new_state):
214219
self.chars[FEATURE_PLAY_STOP].set_value(hk_state)
215220

216221
if self.chars[FEATURE_TOGGLE_MUTE]:
217-
current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)
222+
current_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED))
218223
_LOGGER.debug(
219224
'%s: Set current state for "toggle_mute" to %s',
220225
self.entity_id,
@@ -240,9 +245,7 @@ def __init__(self, *args):
240245
# Add additional characteristics if volume or input selection supported
241246
self.chars_tv = []
242247
self.chars_speaker = []
243-
features = self.hass.states.get(self.entity_id).attributes.get(
244-
ATTR_SUPPORTED_FEATURES, 0
245-
)
248+
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
246249

247250
if features & (SUPPORT_PLAY | SUPPORT_PAUSE):
248251
self.chars_tv.append(CHAR_REMOTE_KEY)
@@ -253,7 +256,8 @@ def __init__(self, *args):
253256
if features & SUPPORT_VOLUME_SET:
254257
self.chars_speaker.append(CHAR_VOLUME)
255258

256-
if features & SUPPORT_SELECT_SOURCE:
259+
source_list = state.attributes.get(ATTR_INPUT_SOURCE_LIST, [])
260+
if source_list and features & SUPPORT_SELECT_SOURCE:
257261
self.support_select_source = True
258262

259263
serv_tv = self.add_preload_service(SERV_TELEVISION, self.chars_tv)
@@ -298,9 +302,7 @@ def __init__(self, *args):
298302
)
299303

300304
if self.support_select_source:
301-
self.sources = self.hass.states.get(self.entity_id).attributes.get(
302-
ATTR_INPUT_SOURCE_LIST, []
303-
)
305+
self.sources = source_list
304306
self.char_input_source = serv_tv.configure_char(
305307
CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source
306308
)
@@ -380,35 +382,30 @@ def update_state(self, new_state):
380382
hk_state = 0
381383
if current_state not in ("None", STATE_OFF, STATE_UNKNOWN):
382384
hk_state = 1
383-
384385
_LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state)
385386
if self.char_active.value != hk_state:
386387
self.char_active.set_value(hk_state)
387388

388389
# Set mute state
389390
if CHAR_VOLUME_SELECTOR in self.chars_speaker:
390-
current_mute_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)
391+
current_mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED))
391392
_LOGGER.debug(
392393
"%s: Set current mute state to %s", self.entity_id, current_mute_state,
393394
)
394395
if self.char_mute.value != current_mute_state:
395396
self.char_mute.set_value(current_mute_state)
396397

397398
# Set active input
398-
if self.support_select_source:
399+
if self.support_select_source and self.sources:
399400
source_name = new_state.attributes.get(ATTR_INPUT_SOURCE)
400-
if self.sources:
401-
_LOGGER.debug(
402-
"%s: Set current input to %s", self.entity_id, source_name
401+
_LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name)
402+
if source_name in self.sources:
403+
index = self.sources.index(source_name)
404+
if self.char_input_source.value != index:
405+
self.char_input_source.set_value(index)
406+
else:
407+
_LOGGER.warning(
408+
"%s: Sources out of sync. Restart Home Assistant", self.entity_id,
403409
)
404-
if source_name in self.sources:
405-
index = self.sources.index(source_name)
406-
if self.char_input_source.value != index:
407-
self.char_input_source.set_value(index)
408-
else:
409-
_LOGGER.warning(
410-
"%s: Sources out of sync. Restart Home Assistant",
411-
self.entity_id,
412-
)
413-
if self.char_input_source.value != 0:
414-
self.char_input_source.set_value(0)
410+
if self.char_input_source.value != 0:
411+
self.char_input_source.set_value(0)

homeassistant/components/homekit/util.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,40 @@
9696
)
9797

9898

99+
HOMEKIT_CHAR_TRANSLATIONS = {
100+
0: " ", # nul
101+
10: " ", # nl
102+
13: " ", # cr
103+
33: "-", # !
104+
34: " ", # "
105+
36: "-", # $
106+
37: "-", # %
107+
40: "-", # (
108+
41: "-", # )
109+
42: "-", # *
110+
43: "-", # +
111+
47: "-", # /
112+
58: "-", # :
113+
59: "-", # ;
114+
60: "-", # <
115+
61: "-", # =
116+
62: "-", # >
117+
63: "-", # ?
118+
64: "-", # @
119+
91: "-", # [
120+
92: "-", # \
121+
93: "-", # ]
122+
94: "-", # ^
123+
95: " ", # _
124+
96: "-", # `
125+
123: "-", # {
126+
124: "-", # |
127+
125: "-", # }
128+
126: "-", # ~
129+
127: "-", # del
130+
}
131+
132+
99133
def validate_entity_config(values):
100134
"""Validate config entry for CONF_ENTITY."""
101135
if not isinstance(values, dict):
@@ -133,8 +167,8 @@ def validate_entity_config(values):
133167
return entities
134168

135169

136-
def validate_media_player_features(state, feature_list):
137-
"""Validate features for media players."""
170+
def get_media_player_features(state):
171+
"""Determine features for media players."""
138172
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
139173

140174
supported_modes = []
@@ -148,14 +182,30 @@ def validate_media_player_features(state, feature_list):
148182
supported_modes.append(FEATURE_PLAY_STOP)
149183
if features & media_player.const.SUPPORT_VOLUME_MUTE:
150184
supported_modes.append(FEATURE_TOGGLE_MUTE)
185+
return supported_modes
186+
187+
188+
def validate_media_player_features(state, feature_list):
189+
"""Validate features for media players."""
190+
supported_modes = get_media_player_features(state)
191+
192+
if not supported_modes:
193+
_LOGGER.error("%s does not support any media_player features", state.entity_id)
194+
return False
195+
196+
if not feature_list:
197+
# Auto detected
198+
return True
151199

152200
error_list = []
153201
for feature in feature_list:
154202
if feature not in supported_modes:
155203
error_list.append(feature)
156204

157205
if error_list:
158-
_LOGGER.error("%s does not support features: %s", state.entity_id, error_list)
206+
_LOGGER.error(
207+
"%s does not support media_player features: %s", state.entity_id, error_list
208+
)
159209
return False
160210
return True
161211

@@ -247,6 +297,16 @@ def convert_to_float(state):
247297
return None
248298

249299

300+
def cleanup_name_for_homekit(name):
301+
"""Ensure the name of the device will not crash homekit."""
302+
#
303+
# This is not a security measure.
304+
#
305+
# UNICODE_EMOJI is also not allowed but that
306+
# likely isn't a problem
307+
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)
308+
309+
250310
def temperature_to_homekit(temperature, unit):
251311
"""Convert temperature to Celsius for HomeKit."""
252312
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)

tests/components/homekit/test_type_media_players.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,26 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog):
368368
assert not caplog.messages or "Error" not in caplog.messages[-1]
369369

370370

371+
async def test_media_player_television_supports_source_select_no_sources(
372+
hass, hk_driver, events, caplog
373+
):
374+
"""Test if basic tv that supports source select but is missing a source list."""
375+
entity_id = "media_player.television"
376+
377+
# Supports turn_on', 'turn_off'
378+
hass.states.async_set(
379+
entity_id,
380+
None,
381+
{ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, ATTR_SUPPORTED_FEATURES: 3469},
382+
)
383+
await hass.async_block_till_done()
384+
acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
385+
await acc.run_handler()
386+
await hass.async_block_till_done()
387+
388+
assert acc.support_select_source is False
389+
390+
371391
async def test_tv_restore(hass, hk_driver, events):
372392
"""Test setting up an entity from state in the event registry."""
373393
hass.state = CoreState.not_running

tests/components/homekit/test_util.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from homeassistant.components.homekit.util import (
2323
HomeKitSpeedMapping,
2424
SpeedRange,
25+
cleanup_name_for_homekit,
2526
convert_to_float,
2627
density_to_air_quality,
2728
dismiss_setup_message,
@@ -172,6 +173,19 @@ def test_convert_to_float():
172173
assert convert_to_float(None) is None
173174

174175

176+
def test_cleanup_name_for_homekit():
177+
"""Ensure name sanitize works as expected."""
178+
179+
assert cleanup_name_for_homekit("abc") == "abc"
180+
assert cleanup_name_for_homekit("a b c") == "a b c"
181+
assert cleanup_name_for_homekit("ab_c") == "ab c"
182+
assert (
183+
cleanup_name_for_homekit('ab!@#$%^&*()-=":.,><?//\\ frog')
184+
== "ab--#---&----- -.,------ frog"
185+
)
186+
assert cleanup_name_for_homekit("の日本_語文字セット") == "の日本 語文字セット"
187+
188+
175189
def test_temperature_to_homekit():
176190
"""Test temperature conversion from HA to HomeKit."""
177191
assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5

0 commit comments

Comments
 (0)