Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
CONF_IP_ADDRESS, CONF_NAME, CONF_PORT,
CONF_IP_ADDRESS, CONF_MODE, CONF_NAME, CONF_PORT,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
Expand All @@ -25,7 +25,8 @@
CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START,
DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE,
SERVICE_HOMEKIT_START)
from .util import show_setup_message, validate_entity_config
from .util import (
show_setup_message, validate_entity_config, validate_media_player_modes)

TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -125,6 +126,11 @@ def get_accessory(hass, state, aid, config):
elif state.domain == 'lock':
a_type = 'Lock'

elif state.domain == 'media_player':
validate_media_player_modes(state, config)
if config.get(CONF_MODE):
a_type = 'MediaPlayer'

elif state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
Expand Down Expand Up @@ -208,8 +214,8 @@ def start(self, *args):
# pylint: disable=unused-variable
from . import ( # noqa F401
type_covers, type_fans, type_lights, type_locks,
type_security_systems, type_sensors, type_switches,
type_thermostats)
type_media_players, type_security_systems, type_sensors,
type_switches, type_thermostats)

for state in self.hass.states.all():
self.add_bridge_accessory(state)
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/homekit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
MANUFACTURER = 'Home Assistant'

# #### Media Player Modes ####
ON_OFF = 'on_off'
PLAY_PAUSE = 'play_pause'
PLAY_STOP = 'play_stop'
TOGGLE_MUTE = 'toggle_mute'

# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
Expand Down
142 changes: 142 additions & 0 deletions homeassistant/components/homekit/type_media_players.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Class to hold all media player accessories."""
import logging

from pyhap.const import CATEGORY_SWITCH

from homeassistant.const import (
ATTR_ENTITY_ID, CONF_MODE, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE,
STATE_OFF, STATE_PLAYING, STATE_UNKNOWN)
from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)

from . import TYPES
from .accessories import HomeAccessory
from .const import (
CHAR_NAME, CHAR_ON, ON_OFF, PLAY_PAUSE, PLAY_STOP, SERV_SWITCH,
TOGGLE_MUTE)

_LOGGER = logging.getLogger(__name__)

MODE_FRIENDLY_NAME = {ON_OFF: 'Power',
PLAY_PAUSE: 'Play/Pause',
PLAY_STOP: 'Play/Stop',
TOGGLE_MUTE: 'Mute'}


@TYPES.register('MediaPlayer')
class MediaPlayer(HomeAccessory):
"""Generate a Media Player accessory."""

def __init__(self, *args):
"""Initialize a Switch accessory object."""
super().__init__(*args, category=CATEGORY_SWITCH)
self._flag = {ON_OFF: False, PLAY_PAUSE: False,
PLAY_STOP: False, TOGGLE_MUTE: False}
self.chars = {ON_OFF: None, PLAY_PAUSE: None,
PLAY_STOP: None, TOGGLE_MUTE: None}
modes = self.config[CONF_MODE]

if ON_OFF in modes:
serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_on_off.configure_char(
CHAR_NAME, value=self.generate_service_name(ON_OFF))
self.chars[ON_OFF] = serv_on_off.configure_char(
CHAR_ON, value=False, setter_callback=self.set_on_off)

if PLAY_PAUSE in modes:
serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_play_pause.configure_char(
CHAR_NAME, value=self.generate_service_name(PLAY_PAUSE))
self.chars[PLAY_PAUSE] = serv_play_pause.configure_char(
CHAR_ON, value=False, setter_callback=self.set_play_pause)

if PLAY_STOP in modes:
serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_play_stop.configure_char(
CHAR_NAME, value=self.generate_service_name(PLAY_STOP))
self.chars[PLAY_STOP] = serv_play_stop.configure_char(
CHAR_ON, value=False, setter_callback=self.set_play_stop)

if TOGGLE_MUTE in modes:
serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_toggle_mute.configure_char(
CHAR_NAME, value=self.generate_service_name(TOGGLE_MUTE))
self.chars[TOGGLE_MUTE] = serv_toggle_mute.configure_char(
CHAR_ON, value=False, setter_callback=self.set_toggle_mute)

def generate_service_name(self, mode):
"""Generate name for individual service."""
return '{} {}'.format(self.display_name, MODE_FRIENDLY_NAME[mode])

def set_on_off(self, value):
"""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)
self._flag[ON_OFF] = True
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call(DOMAIN, service, params)

def set_play_pause(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "play_pause" to %s',
self.entity_id, value)
self._flag[PLAY_PAUSE] = True
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call(DOMAIN, service, params)

def set_play_stop(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "play_stop" to %s',
self.entity_id, value)
self._flag[PLAY_STOP] = True
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call(DOMAIN, service, params)

def set_toggle_mute(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "toggle_mute" to %s',
self.entity_id, value)
self._flag[TOGGLE_MUTE] = True
params = {ATTR_ENTITY_ID: self.entity_id,
ATTR_MEDIA_VOLUME_MUTED: value}
self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params)

def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = new_state.state

if self.chars[ON_OFF]:
hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None')
if not self._flag[ON_OFF]:
_LOGGER.debug('%s: Set current state for "on_off" to %s',
self.entity_id, hk_state)
self.chars[ON_OFF].set_value(hk_state)
self._flag[ON_OFF] = False

if self.chars[PLAY_PAUSE]:
hk_state = current_state == STATE_PLAYING
if not self._flag[PLAY_PAUSE]:
_LOGGER.debug('%s: Set current state for "play_pause" to %s',
self.entity_id, hk_state)
self.chars[PLAY_PAUSE].set_value(hk_state)
self._flag[PLAY_PAUSE] = False

if self.chars[PLAY_STOP]:
hk_state = current_state == STATE_PLAYING
if not self._flag[PLAY_STOP]:
_LOGGER.debug('%s: Set current state for "play_stop" to %s',
self.entity_id, hk_state)
self.chars[PLAY_STOP].set_value(hk_state)
self._flag[PLAY_STOP] = False

if self.chars[TOGGLE_MUTE]:
current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)
if not self._flag[TOGGLE_MUTE]:
_LOGGER.debug('%s: Set current state for "toggle_mute" to %s',
self.entity_id, current_state)
self.chars[TOGGLE_MUTE].set_value(current_state)
self._flag[TOGGLE_MUTE] = False
43 changes: 41 additions & 2 deletions homeassistant/components/homekit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@

import voluptuous as vol

from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE)
from homeassistant.core import split_entity_id
from homeassistant.const import (
ATTR_CODE, CONF_NAME, TEMP_CELSIUS)
ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.temperature as temp_util
from .const import HOMEKIT_NOTIFY_ID
from .const import (
HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE)

_LOGGER = logging.getLogger(__name__)

MEDIA_PLAYER_MODES = (ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE)


def validate_entity_config(values):
"""Validate config entry for CONF_ENTITY."""
Expand All @@ -34,10 +40,43 @@ def validate_entity_config(values):
code = config.get(ATTR_CODE)
params[ATTR_CODE] = cv.string(code) if code else None

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

entities[entity] = params
return entities


def validate_media_player_modes(state, config):
"""Validate modes for media playeres."""
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)

supported_modes = []
if features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF):
supported_modes.append(ON_OFF)
if features & (SUPPORT_PLAY | SUPPORT_PAUSE):
supported_modes.append(PLAY_PAUSE)
if features & (SUPPORT_PLAY | SUPPORT_STOP):
supported_modes.append(PLAY_STOP)
if features & SUPPORT_VOLUME_MUTE:
supported_modes.append(TOGGLE_MUTE)

if not config.get(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))


def show_setup_message(hass, pincode):
"""Display persistent notification with setup information."""
pin = pincode.decode()
Expand Down
22 changes: 21 additions & 1 deletion tests/components/homekit/test_get_accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
from unittest.mock import patch, Mock

import pytest
import voluptuous as vol

from homeassistant.core import State
from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON)
from homeassistant.components.homekit import get_accessory, TYPES
from homeassistant.components.homekit.const import ON_OFF
from homeassistant.const import (
ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
ATTR_UNIT_OF_MEASUREMENT, CONF_MODE, CONF_NAME, TEMP_CELSIUS,
TEMP_FAHRENHEIT)


def test_not_supported(caplog):
Expand All @@ -24,6 +29,18 @@ def test_not_supported(caplog):
assert 'invalid aid' in caplog.records[0].msg


def test_not_supported_media_player():
"""Test if mode isn't supported and if no supported modes."""
# selected mode for entity not supported
with pytest.raises(vol.Invalid):
entity_state = State('media_player.demo', 'on')
get_accessory(None, entity_state, 2, {CONF_MODE: [ON_OFF]})

# no supported modes for entity
entity_state = State('media_player.demo', 'on')
assert get_accessory(None, entity_state, 2, {}) is None


@pytest.mark.parametrize('config, name', [
({CONF_NAME: 'Customize Name'}, 'Customize Name'),
])
Expand All @@ -40,6 +57,9 @@ def test_customize_options(config, name):
('Fan', 'fan.test', 'on', {}, {}),
('Light', 'light.test', 'on', {}, {}),
('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}),
('MediaPlayer', 'media_player.test', 'on',
{ATTR_SUPPORTED_FEATURES: SUPPORT_TURN_ON | SUPPORT_TURN_OFF},
{CONF_MODE: [ON_OFF]}),
('SecuritySystem', 'alarm_control_panel.test', 'armed', {},
{ATTR_CODE: '1234'}),
('Thermostat', 'climate.test', 'auto', {}, {}),
Expand Down
Loading