Skip to content

Add HomeKit support for media players#14446

Merged
cdce8p merged 7 commits intohome-assistant:devfrom
schmittx:homekit-media-player-support
May 25, 2018
Merged

Add HomeKit support for media players#14446
cdce8p merged 7 commits intohome-assistant:devfrom
schmittx:homekit-media-player-support

Conversation

@schmittx
Copy link
Copy Markdown
Contributor

@schmittx schmittx commented May 13, 2018

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, and toggle_mute. Uses supported_features to determine the default mode but can be customized in entity_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):

homekit:
  entity_config:
    media_player.living_room:
      mode:
        - on_off
        - play_pause
        - play_stop
        - toggle_mute

Checklist:

  • The code change is tested and works locally.
  • Local tests pass with tox. Your PR cannot be merged unless tests pass

If user exposed functionality or configuration variables are added/changed:

If the code does not interact with devices:

  • Tests have been added to verify that the new code works.

@cdce8p
Copy link
Copy Markdown
Member

cdce8p commented May 13, 2018

Would be best if you create a separate type for it, either in the type_switch or even in a new module.

@quthla
Copy link
Copy Markdown
Contributor

quthla commented May 13, 2018

This is already possible with templates. Why would we hack it into the HomeKit component?

@cdce8p
Copy link
Copy Markdown
Member

cdce8p commented May 13, 2018

@quthla It might be, however official support would make it easier for inexperienced users.

@cdce8p cdce8p self-assigned this May 14, 2018
@schmittx schmittx force-pushed the homekit-media-player-support branch from 922e28e to 5fc1713 Compare May 15, 2018 03:09

This comment was marked as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'homeassistant.core.split_entity_id' imported but unused

@schmittx schmittx changed the title [WIP] Add HomeKit support for media players Add HomeKit support for media players May 16, 2018
Copy link
Copy Markdown
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to use: if features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Named Tuples instead of the list?

Copy link
Copy Markdown
Contributor Author

@schmittx schmittx May 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_service

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, exactly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split the line before the else. Would improve readability.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

@schmittx schmittx May 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@schmittx
Copy link
Copy Markdown
Contributor Author

schmittx commented May 16, 2018

@cdce8p

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.

Sorry, I don’t understand the question. Can you clarify?

@cdce8p
Copy link
Copy Markdown
Member

cdce8p commented May 16, 2018

Most media_player entites support more then just one mode. I don't think it's far fetched to think that users also want to be able to control all of those from the Home App. Currently it's limited to just on swich per entity or am I getting something wrong?

@schmittx
Copy link
Copy Markdown
Contributor Author

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.

@schmittx schmittx force-pushed the homekit-media-player-support branch from ea206f7 to 9ed60cd Compare May 17, 2018 06:48
@schmittx schmittx force-pushed the homekit-media-player-support branch from 9ed60cd to f004f50 Compare May 19, 2018 04:24
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)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@schmittx schmittx closed this May 19, 2018
@schmittx schmittx force-pushed the homekit-media-player-support branch from 05d8fb0 to e88fc33 Compare May 19, 2018 15:28
@schmittx schmittx reopened this May 19, 2018
@schmittx schmittx changed the title Add HomeKit support for media players [WIP] Add HomeKit support for media players May 19, 2018
@schmittx schmittx closed this May 19, 2018
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the validation to the MediaPlayer class. get_accessory is designed to handle choosing the right type only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do before the super call, how to get supported_features since self.hass isn't initialized yet?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Accessory class does that for you, but the parameter is called self.display_name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sort all imports alphabetically. I'm working on doing the same for all other modules. That applies to the tests as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A dictionary for the flag vars might be a better solution. Take a look at the Light class type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A dict might be better here as well.

self._chars = {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used self.chars so that we can check it in the tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove the keyword, it fits on one line. Also for the other cases.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A tuple is more efficient for immutable data structures.

hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sort the import alphabetically.

Copy link
Copy Markdown
Member

@cdce8p cdce8p May 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move them belove STRING CONSTANTS and maybe create a new section Media_player Modes.

This comment was marked as outdated.

@schmittx schmittx changed the title [WIP] Add HomeKit support for media players Add HomeKit support for media players May 20, 2018
Copy link
Copy Markdown
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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('......')

Copy link
Copy Markdown
Contributor Author

@schmittx schmittx May 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
    ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't reproduce the error. Can you push your changes to Github, so I can try them?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.chars should be private. (The empty line above can be deleted.)

Copy link
Copy Markdown
Contributor Author

@schmittx schmittx May 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 == 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, you're right. Please ignore that comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Through the change CONF_MODE will always be set modes = self.config[CONF_MODE]. (The empty line above can be deleted as well.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just use current_state below.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just remove the empty lines in between. Also done by #14556

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import should be above homeassistant.const

@schmittx schmittx force-pushed the homekit-media-player-support branch from 7ca4a1c to e905a6e Compare May 21, 2018 14:09
Copy link
Copy Markdown
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/components/homekit/test_util.py Outdated
{'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'}}]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'invalid_mode' instead of 'on_on'

Comment thread tests/components/homekit/test_util.py Outdated
"""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, {})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]}

Comment thread tests/components/homekit/test_util.py Outdated

attrs = {ATTR_SUPPORTED_FEATURES: 384}
entity_state = State('media_player.demo', 'on', attrs)
config = {CONF_MODE: [ON_OFF, PLAY_PAUSE]}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    entity_state = State('media_player.demo', 'on')
    config = {CONF_MODE: [ON_OFF]}
    with pytest.raises(vol.Invalid):
        validate_media_player_modes(entity_state, config)

Comment thread tests/components/homekit/test_util.py Outdated


def test_validate_media_player_modes():
"""Test validate modes for media playeres."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

media players


from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)
from homeassistant.components.homekit.type_media_players import (MediaPlayer)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STATE_IDLE before STATE_OFF

Copy link
Copy Markdown
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Merging this till next weekend. Thanks 🥇

@cdce8p cdce8p mentioned this pull request May 23, 2018
4 tasks
@cdce8p cdce8p merged commit a9f19a1 into home-assistant:dev May 25, 2018
@schmittx schmittx deleted the homekit-media-player-support branch June 2, 2018 00:04
@balloob balloob mentioned this pull request Jun 8, 2018
girlpunk pushed a commit to girlpunk/home-assistant that referenced this pull request Sep 4, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Sep 5, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants