Add HomeKit support for media players#14446
Conversation
|
Would be best if you create a separate type for it, either in the |
|
This is already possible with templates. Why would we hack it into the HomeKit component? |
|
@quthla It might be, however official support would make it easier for inexperienced users. |
922e28e to
5fc1713
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
'homeassistant.components.homekit.const.ON_OFF' imported but unused
'homeassistant.components.homekit.const.PLAY_PAUSE' imported but unused
'homeassistant.components.homekit.const.PLAY_STOP' imported but unused
'homeassistant.components.homekit.const.TOGGLE_MUTE' imported but unused
line too long (93 > 79 characters)
There was a problem hiding this comment.
'homeassistant.core.split_entity_id' imported but unused
cdce8p
left a comment
There was a problem hiding this comment.
Left a few comments.
How do you plan to cover the case the switches for all modes should be added? IMO this should be the default as well. Can be excluded via the config.
There was a problem hiding this comment.
You should be able to use: if features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF).
There was a problem hiding this comment.
Maybe Named Tuples instead of the list?
There was a problem hiding this comment.
I'm new to namedtuple, you're proposing something like this?
Mode = namedtuple('Mode', ['on_state', 'on_service', 'off_service'])
STATE_SERVICE_MAP = {
ON_OFF: Mode(STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF),
PLAY_PAUSE: Mode(STATE_PLAYING, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE),
PLAY_STOP: Mode(STATE_PLAYING, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP),
TOGGLE_MUTE: Mode(True, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE)
}then
self.state_service_map = STATE_SERVICE_MAP[self.mode]finally
service = self.state_service_map.on_service if value \
else self.state_service_map.off_serviceThere was a problem hiding this comment.
Instead of assigning those with the named tuple assigning it just to one var and accessing it later through self.mode_map.on_service (mode_map might not be the best name).
There was a problem hiding this comment.
Split the line before the else. Would improve readability.
There was a problem hiding this comment.
I don't like those three lines, and they currently don't work either.
For hk_state: What if it changes from off to playing (just an example that doesn't work yet)?
For current_state: Maybe use an explicit if instead. Currently it's quite complicated and wouldn't get any easier.
current_state = new_state.state
if self.mode == TOGGLE_MUTE:
current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)There was a problem hiding this comment.
For hk_state: Valid point, perhaps for on_off mode its best to compare with state_off; and for the other modes compare with state_on? This is the same approach that homebridge-homeassistant uses.
For current_state: Agreed, if statement is good.
Sorry, I don’t understand the question. Can you clarify? |
|
Most |
|
OK, I thought that’s what you meant. Valid point, I agree that we should support multiple modes for one entity if possible. I’ll look into it. |
ea206f7 to
9ed60cd
Compare
9ed60cd to
f004f50
Compare
| hk_state = current_state == self.state_service_map.on_state | ||
| if self.mode == ON_OFF: | ||
| hk_state = current_state not in [self.state_service_map.off_state, | ||
| STATE_UNKNOWN, str(None)] |
There was a problem hiding this comment.
A tuple would be better here: (self.state_service_map.off_state, STATE_UNKNOWN, 'None').
I'm not sure though if 'None' is really necessary here, but wouldn't be a big deal to leave it in either.
05d8fb0 to
e88fc33
Compare
c005ad0 to
e88fc33
Compare
There was a problem hiding this comment.
Move the validation to the MediaPlayer class. get_accessory is designed to handle choosing the right type only.
There was a problem hiding this comment.
How should we handle the case if none of the config_modes are supported? Or if entity has no supported_modes? Both would result in validated_modes being empty. That’s why I initially put it in get_accessory.
There was a problem hiding this comment.
Good question. I haven't tested it, but you should be able to do these checks before the super call in the type. If the setup should be aborted, a return should do the trick.
There was a problem hiding this comment.
If we do before the super call, how to get supported_features since self.hass isn't initialized yet?
There was a problem hiding this comment.
You should be able to access all necessary attributes through args. However this would be pretty ugly.
What do think of only moving it to a separate function in util instead, so it doesn't clutter __init__.py and get_accessory that much?
There was a problem hiding this comment.
The Accessory class does that for you, but the parameter is called self.display_name
There was a problem hiding this comment.
Please sort all imports alphabetically. I'm working on doing the same for all other modules. That applies to the tests as well.
There was a problem hiding this comment.
A dictionary for the flag vars might be a better solution. Take a look at the Light class type.
There was a problem hiding this comment.
A dict might be better here as well.
self._chars = {}There was a problem hiding this comment.
I used self.chars so that we can check it in the tests.
There was a problem hiding this comment.
If you remove the keyword, it fits on one line. Also for the other cases.
There was a problem hiding this comment.
A tuple is more efficient for immutable data structures.
hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None')There was a problem hiding this comment.
Please sort the import alphabetically.
There was a problem hiding this comment.
Move them belove STRING CONSTANTS and maybe create a new section Media_player Modes.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
cdce8p
left a comment
There was a problem hiding this comment.
Left you another round of comments, some really small ones. What I'm really thinking about is in which case, given a valid config, the type won't be added. I couldn't came up with an example. Nevertheless validate_media_player_modes can stay in util.
There was a problem hiding this comment.
Since you already pass config to validate_media_player_modes, why not override config[CONF_MODE] there? Also the is not None check can be abbreviated.
validate_media_player_modes(state, config)
if config[CONF_MODE]:There was a problem hiding this comment.
We don't need this check. cv.ensure_list(value) returns an empty list if the value is None. Additionally I think changing the for loop mode to key might be a bit better.
There was a problem hiding this comment.
Since you only check if a mode is in the list, it isn't necessary to filter out duplicates with and mode not in validated_modes. Through the change in vec we guaranty that config[CONF_MODE] is always a list at the time of execution for this function, so we can directly iterate over it. Furthermore I think we should raise vol.Invalid if a mode is not supported.
if not config[CONF_MODE]:
config[CONF_MODE] = supported_modes
return
for mode in config[CONF_MODE]:
if mode not in supported_modes:
raise vol.Invalid('......')There was a problem hiding this comment.
With this in validate_entity_config
if domain == 'media_player':
mode = config.get(CONF_MODE)
params[CONF_MODE] = cv.ensure_list(mode)
for key in params[CONF_MODE]:
if key not in MEDIA_PLAYER_MODES:
raise vol.Invalid(
'Invalid mode: "{}", valid modes are: "{}".'
.format(key, MEDIA_PLAYER_MODES))And this in validate_media_player_modes
if not config[CONF_MODE]:
config[CONF_MODE] = supported_modes
return
for mode in config[CONF_MODE]:
if mode not in supported_modes:
raise vol.Invalid('"{}" does not support mode: "{}".'
.format(state.entity_id, mode))An error is produced if mode is not given in entity_config
KeyError: 'mode'
I know that the change below works, but is there a better solution?
if not config.get(CONF_MODE):
...There was a problem hiding this comment.
I couldn't reproduce the error. Can you push your changes to Github, so I can try them?
There was a problem hiding this comment.
self.chars should be private. (The empty line above can be deleted.)
There was a problem hiding this comment.
I think it cannot be private in order for the tests to function? Correct?
All other components (type_lights, etc.) use non-private self.chars.
assert acc.chars[ON_OFF].value == 0There was a problem hiding this comment.
Yea, you're right. Please ignore that comment.
There was a problem hiding this comment.
Through the change CONF_MODE will always be set modes = self.config[CONF_MODE]. (The empty line above can be deleted as well.)
There was a problem hiding this comment.
You can just use current_state below.
There was a problem hiding this comment.
We should just remove the empty lines in between. Also done by #14556
There was a problem hiding this comment.
We need an extra test case for when the media_player should not be added. While thinking about it: What are the use cases for this / the user input (for entity_config/mode) to reach it.
There was a problem hiding this comment.
You're right, now that we added this to validate_media_player_modes
if not config[CONF_MODE]:
config[CONF_MODE] = supported_modes
return
for mode in config[CONF_MODE]:
if mode not in supported_modes:
raise vol.Invalid('"{}" does not support mode: "{}".'
.format(state.entity_id, mode))Then this in get_accessory
elif state.domain == 'media_player':
validate_media_player_modes(state, config)
if config[CONF_MODE]:
a_type = 'MediaPlayer'Can be re-written as
elif state.domain == 'media_player':
validate_media_player_modes(state, config)
a_type = 'MediaPlayer'I also added a test in test_not_supported in the event that the media_player entity has no supported modes (i.e. Universal) .
with pytest.raises(vol.Invalid):
attrs = {ATTR_SUPPORTED_FEATURES: SUPPORT_PAUSE | SUPPORT_SEEK}
entity_state = State('media_player.demo', 'on', attrs)
get_accessory(None, entity_state, 2, {CONF_MODE: [ON_OFF]})There was a problem hiding this comment.
I would need to see the final version for this. If there are entities that don't match any supported modes, we might as well need the change if config[CONF_MODE]: a_type = 'MediaPlayer'. Could be possible that I'm missing something.
There was a problem hiding this comment.
If an entity has no supported modes, then supported_modes will be empty and a vol.Invalid will be raised from if mode not in supported_modes:. Anyway, I pushed my latest version so you can review.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
This import should be above homeassistant.const
7ca4a1c to
e905a6e
Compare
cdce8p
left a comment
There was a problem hiding this comment.
Some last style changes and test improvements
| with pytest.raises(vol.Invalid): | ||
| attrs = {ATTR_SUPPORTED_FEATURES: SUPPORT_PAUSE | SUPPORT_SEEK} | ||
| entity_state = State('media_player.demo', 'on', attrs) | ||
| get_accessory(None, entity_state, 2, {CONF_MODE: [ON_OFF]}) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| {'demo.test': 'test'}, {'demo.test': [1, 2]}, | ||
| {'demo.test': None}, {'demo.test': {CONF_NAME: None}}] | ||
| {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, | ||
| {'media_player.test': {CONF_MODE: 'on_on'}}] |
There was a problem hiding this comment.
'invalid_mode' instead of 'on_on'
| """Test validate modes for media playeres.""" | ||
| attrs = {ATTR_SUPPORTED_FEATURES: 20873} | ||
| entity_state = State('media_player.demo', 'on', attrs) | ||
| validate_media_player_modes(entity_state, {}) |
There was a problem hiding this comment.
Add an assert statement to make sure config is as expected:
config = {}
attrs = {ATTR_SUPPORTED_FEATURES: 20873}
entity_state = State('media_player.demo', 'on', attrs)
validate_media_player_modes(entity_state, config)
assert config == {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]}|
|
||
| attrs = {ATTR_SUPPORTED_FEATURES: 384} | ||
| entity_state = State('media_player.demo', 'on', attrs) | ||
| config = {CONF_MODE: [ON_OFF, PLAY_PAUSE]} |
There was a problem hiding this comment.
entity_state = State('media_player.demo', 'on')
config = {CONF_MODE: [ON_OFF]}
with pytest.raises(vol.Invalid):
validate_media_player_modes(entity_state, config)|
|
||
|
|
||
| def test_validate_media_player_modes(): | ||
| """Test validate modes for media playeres.""" |
|
|
||
| from homeassistant.components.media_player import ( | ||
| ATTR_MEDIA_VOLUME_MUTED, DOMAIN) | ||
| from homeassistant.components.homekit.type_media_players import (MediaPlayer) |
There was a problem hiding this comment.
import MediaPlayer (without parentheses)
| from homeassistant.const import ( | ||
| ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_MODE, SERVICE_MEDIA_PAUSE, | ||
| SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, | ||
| SERVICE_VOLUME_MUTE, STATE_OFF, STATE_ON, STATE_IDLE, STATE_PAUSED, |
cdce8p
left a comment
There was a problem hiding this comment.
Looks good. Merging this till next weekend. Thanks 🥇
Description:
Adds support for media players in HomeKit. Entities will be shown as switches within HomeKit. Available modes are
on_off,play_pause,play_stop, andtoggle_mute. Usessupported_featuresto determine the default mode but can be customized inentity_config.Related issue (if applicable): N/A
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#5378
Example entry for
configuration.yaml(if applicable):Checklist:
tox. Your PR cannot be merged unless tests passIf user exposed functionality or configuration variables are added/changed:
If the code does not interact with devices: