Add HomeKit Television functionality (#22968)
This commit is contained in:
parent
8da600adff
commit
e24d56aa5b
5 changed files with 447 additions and 18 deletions
|
@ -6,6 +6,7 @@ from zlib import adler32
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import cover
|
from homeassistant.components import cover
|
||||||
|
from homeassistant.components.media_player import DEVICE_CLASS_TV
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||||
CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY,
|
CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY,
|
||||||
|
@ -99,7 +100,7 @@ async def async_setup(hass, config):
|
||||||
def get_accessory(hass, driver, state, aid, config):
|
def get_accessory(hass, driver, state, aid, config):
|
||||||
"""Take state and return an accessory object if supported."""
|
"""Take state and return an accessory object if supported."""
|
||||||
if not aid:
|
if not aid:
|
||||||
_LOGGER.warning('The entitiy "%s" is not supported, since it '
|
_LOGGER.warning('The entity "%s" is not supported, since it '
|
||||||
'generates an invalid aid, please change it.',
|
'generates an invalid aid, please change it.',
|
||||||
state.entity_id)
|
state.entity_id)
|
||||||
return None
|
return None
|
||||||
|
@ -138,10 +139,15 @@ def get_accessory(hass, driver, state, aid, config):
|
||||||
a_type = 'Lock'
|
a_type = 'Lock'
|
||||||
|
|
||||||
elif state.domain == 'media_player':
|
elif state.domain == 'media_player':
|
||||||
|
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
feature_list = config.get(CONF_FEATURE_LIST)
|
feature_list = config.get(CONF_FEATURE_LIST)
|
||||||
if feature_list and \
|
|
||||||
validate_media_player_features(state, feature_list):
|
if device_class == DEVICE_CLASS_TV:
|
||||||
a_type = 'MediaPlayer'
|
a_type = 'TelevisionMediaPlayer'
|
||||||
|
else:
|
||||||
|
if feature_list and \
|
||||||
|
validate_media_player_features(state, feature_list):
|
||||||
|
a_type = 'MediaPlayer'
|
||||||
|
|
||||||
elif state.domain == 'sensor':
|
elif state.domain == 'sensor':
|
||||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
|
|
|
@ -61,6 +61,7 @@ SERV_CONTACT_SENSOR = 'ContactSensor'
|
||||||
SERV_FANV2 = 'Fanv2'
|
SERV_FANV2 = 'Fanv2'
|
||||||
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
|
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
|
||||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
||||||
|
SERV_INPUT_SOURCE = 'InputSource'
|
||||||
SERV_LEAK_SENSOR = 'LeakSensor'
|
SERV_LEAK_SENSOR = 'LeakSensor'
|
||||||
SERV_LIGHT_SENSOR = 'LightSensor'
|
SERV_LIGHT_SENSOR = 'LightSensor'
|
||||||
SERV_LIGHTBULB = 'Lightbulb'
|
SERV_LIGHTBULB = 'Lightbulb'
|
||||||
|
@ -71,6 +72,8 @@ SERV_OUTLET = 'Outlet'
|
||||||
SERV_SECURITY_SYSTEM = 'SecuritySystem'
|
SERV_SECURITY_SYSTEM = 'SecuritySystem'
|
||||||
SERV_SMOKE_SENSOR = 'SmokeSensor'
|
SERV_SMOKE_SENSOR = 'SmokeSensor'
|
||||||
SERV_SWITCH = 'Switch'
|
SERV_SWITCH = 'Switch'
|
||||||
|
SERV_TELEVISION = 'Television'
|
||||||
|
SERV_TELEVISION_SPEAKER = 'TelevisionSpeaker'
|
||||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||||
SERV_THERMOSTAT = 'Thermostat'
|
SERV_THERMOSTAT = 'Thermostat'
|
||||||
SERV_VALVE = 'Valve'
|
SERV_VALVE = 'Valve'
|
||||||
|
@ -78,6 +81,7 @@ SERV_WINDOW_COVERING = 'WindowCovering'
|
||||||
|
|
||||||
# #### Characteristics ####
|
# #### Characteristics ####
|
||||||
CHAR_ACTIVE = 'Active'
|
CHAR_ACTIVE = 'Active'
|
||||||
|
CHAR_ACTIVE_IDENTIFIER = 'ActiveIdentifier'
|
||||||
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
||||||
CHAR_AIR_QUALITY = 'AirQuality'
|
CHAR_AIR_QUALITY = 'AirQuality'
|
||||||
CHAR_BATTERY_LEVEL = 'BatteryLevel'
|
CHAR_BATTERY_LEVEL = 'BatteryLevel'
|
||||||
|
@ -90,6 +94,7 @@ CHAR_CARBON_MONOXIDE_LEVEL = 'CarbonMonoxideLevel'
|
||||||
CHAR_CARBON_MONOXIDE_PEAK_LEVEL = 'CarbonMonoxidePeakLevel'
|
CHAR_CARBON_MONOXIDE_PEAK_LEVEL = 'CarbonMonoxidePeakLevel'
|
||||||
CHAR_CHARGING_STATE = 'ChargingState'
|
CHAR_CHARGING_STATE = 'ChargingState'
|
||||||
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
|
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
|
||||||
|
CHAR_CONFIGURED_NAME = 'ConfiguredName'
|
||||||
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
|
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
|
||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||||
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
|
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
|
||||||
|
@ -99,10 +104,14 @@ CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity'
|
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity'
|
||||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||||
|
CHAR_CURRENT_VISIBILITY_STATE = 'CurrentVisibilityState'
|
||||||
CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
|
CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||||
CHAR_HUE = 'Hue'
|
CHAR_HUE = 'Hue'
|
||||||
|
CHAR_IDENTIFIER = 'Identifier'
|
||||||
CHAR_IN_USE = 'InUse'
|
CHAR_IN_USE = 'InUse'
|
||||||
|
CHAR_INPUT_SOURCE_TYPE = 'InputSourceType'
|
||||||
|
CHAR_IS_CONFIGURED = 'IsConfigured'
|
||||||
CHAR_LEAK_DETECTED = 'LeakDetected'
|
CHAR_LEAK_DETECTED = 'LeakDetected'
|
||||||
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
|
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
|
||||||
CHAR_LOCK_TARGET_STATE = 'LockTargetState'
|
CHAR_LOCK_TARGET_STATE = 'LockTargetState'
|
||||||
|
@ -110,15 +119,18 @@ CHAR_LINK_QUALITY = 'LinkQuality'
|
||||||
CHAR_MANUFACTURER = 'Manufacturer'
|
CHAR_MANUFACTURER = 'Manufacturer'
|
||||||
CHAR_MODEL = 'Model'
|
CHAR_MODEL = 'Model'
|
||||||
CHAR_MOTION_DETECTED = 'MotionDetected'
|
CHAR_MOTION_DETECTED = 'MotionDetected'
|
||||||
|
CHAR_MUTE = 'Mute'
|
||||||
CHAR_NAME = 'Name'
|
CHAR_NAME = 'Name'
|
||||||
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
|
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
|
||||||
CHAR_ON = 'On'
|
CHAR_ON = 'On'
|
||||||
CHAR_OUTLET_IN_USE = 'OutletInUse'
|
CHAR_OUTLET_IN_USE = 'OutletInUse'
|
||||||
CHAR_POSITION_STATE = 'PositionState'
|
CHAR_POSITION_STATE = 'PositionState'
|
||||||
|
CHAR_REMOTE_KEY = 'RemoteKey'
|
||||||
CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
||||||
CHAR_ROTATION_SPEED = 'RotationSpeed'
|
CHAR_ROTATION_SPEED = 'RotationSpeed'
|
||||||
CHAR_SATURATION = 'Saturation'
|
CHAR_SATURATION = 'Saturation'
|
||||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||||
|
CHAR_SLEEP_DISCOVER_MODE = 'SleepDiscoveryMode'
|
||||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||||
CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery'
|
CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery'
|
||||||
CHAR_SWING_MODE = 'SwingMode'
|
CHAR_SWING_MODE = 'SwingMode'
|
||||||
|
@ -129,6 +141,10 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
|
||||||
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
||||||
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
||||||
CHAR_VALVE_TYPE = 'ValveType'
|
CHAR_VALVE_TYPE = 'ValveType'
|
||||||
|
CHAR_VOLUME = 'Volume'
|
||||||
|
CHAR_VOLUME_SELECTOR = 'VolumeSelector'
|
||||||
|
CHAR_VOLUME_CONTROL_TYPE = 'VolumeControlType'
|
||||||
|
|
||||||
|
|
||||||
# #### Properties ####
|
# #### Properties ####
|
||||||
PROP_MAX_VALUE = 'maxValue'
|
PROP_MAX_VALUE = 'maxValue'
|
||||||
|
|
|
@ -1,23 +1,49 @@
|
||||||
"""Class to hold all media player accessories."""
|
"""Class to hold all media player accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyhap.const import CATEGORY_SWITCH
|
from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)
|
ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_MUTED,
|
||||||
|
ATTR_MEDIA_VOLUME_LEVEL, SERVICE_SELECT_SOURCE, DOMAIN, SUPPORT_PAUSE,
|
||||||
|
SUPPORT_PLAY, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP,
|
||||||
|
SUPPORT_SELECT_SOURCE)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE,
|
||||||
SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE,
|
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_STOP,
|
||||||
STATE_OFF, STATE_PLAYING, STATE_UNKNOWN)
|
SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP,
|
||||||
|
SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, STATE_OFF, STATE_PLAYING,
|
||||||
|
STATE_PAUSED, STATE_UNKNOWN)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory
|
from .accessories import HomeAccessory
|
||||||
from .const import (
|
from .const import (
|
||||||
CHAR_NAME, CHAR_ON, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
CHAR_ACTIVE, CHAR_ACTIVE_IDENTIFIER, CHAR_CONFIGURED_NAME,
|
||||||
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH)
|
CHAR_CURRENT_VISIBILITY_STATE, CHAR_IDENTIFIER, CHAR_INPUT_SOURCE_TYPE,
|
||||||
|
CHAR_IS_CONFIGURED, CHAR_NAME, CHAR_SLEEP_DISCOVER_MODE, CHAR_MUTE,
|
||||||
|
CHAR_ON, CHAR_REMOTE_KEY, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR,
|
||||||
|
CHAR_VOLUME, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
||||||
|
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH, SERV_TELEVISION,
|
||||||
|
SERV_TELEVISION_SPEAKER, SERV_INPUT_SOURCE)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MEDIA_PLAYER_KEYS = {
|
||||||
|
# 0: "Rewind",
|
||||||
|
# 1: "FastForward",
|
||||||
|
# 2: "NextTrack",
|
||||||
|
# 3: "PreviousTrack",
|
||||||
|
# 4: "ArrowUp",
|
||||||
|
# 5: "ArrowDown",
|
||||||
|
# 6: "ArrowLeft",
|
||||||
|
# 7: "ArrowRight",
|
||||||
|
# 8: "Select",
|
||||||
|
# 9: "Back",
|
||||||
|
# 10: "Exit",
|
||||||
|
11: SERVICE_MEDIA_PLAY_PAUSE,
|
||||||
|
# 15: "Information",
|
||||||
|
}
|
||||||
|
|
||||||
MODE_FRIENDLY_NAME = {
|
MODE_FRIENDLY_NAME = {
|
||||||
FEATURE_ON_OFF: 'Power',
|
FEATURE_ON_OFF: 'Power',
|
||||||
FEATURE_PLAY_PAUSE: 'Play/Pause',
|
FEATURE_PLAY_PAUSE: 'Play/Pause',
|
||||||
|
@ -142,3 +168,185 @@ class MediaPlayer(HomeAccessory):
|
||||||
self.entity_id, current_state)
|
self.entity_id, current_state)
|
||||||
self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state)
|
self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state)
|
||||||
self._flag[FEATURE_TOGGLE_MUTE] = False
|
self._flag[FEATURE_TOGGLE_MUTE] = False
|
||||||
|
|
||||||
|
|
||||||
|
@TYPES.register('TelevisionMediaPlayer')
|
||||||
|
class TelevisionMediaPlayer(HomeAccessory):
|
||||||
|
"""Generate a Television Media Player accessory."""
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
"""Initialize a Switch accessory object."""
|
||||||
|
super().__init__(*args, category=CATEGORY_TELEVISION)
|
||||||
|
|
||||||
|
self._flag = {CHAR_ACTIVE: False, CHAR_ACTIVE_IDENTIFIER: False,
|
||||||
|
CHAR_MUTE: False}
|
||||||
|
self.support_select_source = False
|
||||||
|
|
||||||
|
self.sources = []
|
||||||
|
|
||||||
|
# Add additional characteristics if volume or input selection supported
|
||||||
|
self.chars_tv = []
|
||||||
|
self.chars_speaker = []
|
||||||
|
features = self.hass.states.get(self.entity_id) \
|
||||||
|
.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
|
if features & (SUPPORT_PLAY | SUPPORT_PAUSE):
|
||||||
|
self.chars_tv.append(CHAR_REMOTE_KEY)
|
||||||
|
if features & SUPPORT_VOLUME_MUTE or features & SUPPORT_VOLUME_STEP:
|
||||||
|
self.chars_speaker.extend((CHAR_NAME, CHAR_ACTIVE,
|
||||||
|
CHAR_VOLUME_CONTROL_TYPE,
|
||||||
|
CHAR_VOLUME_SELECTOR))
|
||||||
|
if features & SUPPORT_VOLUME_SET:
|
||||||
|
self.chars_speaker.append(CHAR_VOLUME)
|
||||||
|
|
||||||
|
if features & SUPPORT_SELECT_SOURCE:
|
||||||
|
self.support_select_source = True
|
||||||
|
|
||||||
|
serv_tv = self.add_preload_service(SERV_TELEVISION, self.chars_tv)
|
||||||
|
self.set_primary_service(serv_tv)
|
||||||
|
serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name)
|
||||||
|
serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True)
|
||||||
|
self.char_active = serv_tv.configure_char(
|
||||||
|
CHAR_ACTIVE, setter_callback=self.set_on_off)
|
||||||
|
|
||||||
|
if CHAR_REMOTE_KEY in self.chars_tv:
|
||||||
|
self.char_remote_key = serv_tv.configure_char(
|
||||||
|
CHAR_REMOTE_KEY, setter_callback=self.set_remote_key)
|
||||||
|
|
||||||
|
if CHAR_VOLUME_SELECTOR in self.chars_speaker:
|
||||||
|
serv_speaker = self.add_preload_service(
|
||||||
|
SERV_TELEVISION_SPEAKER, self.chars_speaker)
|
||||||
|
serv_tv.add_linked_service(serv_speaker)
|
||||||
|
|
||||||
|
name = '{} {}'.format(self.display_name, 'Volume')
|
||||||
|
serv_speaker.configure_char(CHAR_NAME, value=name)
|
||||||
|
serv_speaker.configure_char(CHAR_ACTIVE, value=1)
|
||||||
|
|
||||||
|
self.char_mute = serv_speaker.configure_char(
|
||||||
|
CHAR_MUTE, value=False, setter_callback=self.set_mute)
|
||||||
|
|
||||||
|
volume_control_type = 1 if CHAR_VOLUME in self.chars_speaker else 2
|
||||||
|
serv_speaker.configure_char(CHAR_VOLUME_CONTROL_TYPE,
|
||||||
|
value=volume_control_type)
|
||||||
|
|
||||||
|
self.char_volume_selector = serv_speaker.configure_char(
|
||||||
|
CHAR_VOLUME_SELECTOR, setter_callback=self.set_volume_step)
|
||||||
|
|
||||||
|
if CHAR_VOLUME in self.chars_speaker:
|
||||||
|
self.char_volume = serv_speaker.configure_char(
|
||||||
|
CHAR_VOLUME, setter_callback=self.set_volume)
|
||||||
|
|
||||||
|
if self.support_select_source:
|
||||||
|
self.sources = self.hass.states.get(self.entity_id) \
|
||||||
|
.attributes.get(ATTR_INPUT_SOURCE_LIST, [])
|
||||||
|
self.char_input_source = serv_tv.configure_char(
|
||||||
|
CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source)
|
||||||
|
for index, source in enumerate(self.sources):
|
||||||
|
serv_input = self.add_preload_service(
|
||||||
|
SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME])
|
||||||
|
serv_tv.add_linked_service(serv_input)
|
||||||
|
serv_input.configure_char(
|
||||||
|
CHAR_CONFIGURED_NAME, value=source)
|
||||||
|
serv_input.configure_char(CHAR_NAME, value=source)
|
||||||
|
serv_input.configure_char(CHAR_IDENTIFIER, value=index)
|
||||||
|
serv_input.configure_char(CHAR_IS_CONFIGURED, value=True)
|
||||||
|
input_type = 3 if "hdmi" in source.lower() else 0
|
||||||
|
serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE,
|
||||||
|
value=input_type)
|
||||||
|
serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE,
|
||||||
|
value=False)
|
||||||
|
_LOGGER.debug('%s: Added source %s.', self.entity_id, source)
|
||||||
|
|
||||||
|
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[CHAR_ACTIVE] = True
|
||||||
|
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||||
|
self.call_service(DOMAIN, service, params)
|
||||||
|
|
||||||
|
def set_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[CHAR_MUTE] = True
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||||
|
ATTR_MEDIA_VOLUME_MUTED: value}
|
||||||
|
self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params)
|
||||||
|
|
||||||
|
def set_volume(self, value):
|
||||||
|
"""Send volume step value if call came from HomeKit."""
|
||||||
|
_LOGGER.debug('%s: Set volume to %s', self.entity_id, value)
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||||
|
ATTR_MEDIA_VOLUME_LEVEL: value}
|
||||||
|
self.call_service(DOMAIN, SERVICE_VOLUME_SET, params)
|
||||||
|
|
||||||
|
def set_volume_step(self, value):
|
||||||
|
"""Send volume step value if call came from HomeKit."""
|
||||||
|
_LOGGER.debug('%s: Step volume by %s',
|
||||||
|
self.entity_id, value)
|
||||||
|
service = SERVICE_VOLUME_DOWN if value else SERVICE_VOLUME_UP
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||||
|
self.call_service(DOMAIN, service, params)
|
||||||
|
|
||||||
|
def set_input_source(self, value):
|
||||||
|
"""Send input set value if call came from HomeKit."""
|
||||||
|
_LOGGER.debug('%s: Set current input to %s',
|
||||||
|
self.entity_id, value)
|
||||||
|
source = self.sources[value]
|
||||||
|
self._flag[CHAR_ACTIVE_IDENTIFIER] = True
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||||
|
ATTR_INPUT_SOURCE: source}
|
||||||
|
self.call_service(DOMAIN, SERVICE_SELECT_SOURCE, params)
|
||||||
|
|
||||||
|
def set_remote_key(self, value):
|
||||||
|
"""Send remote key value if call came from HomeKit."""
|
||||||
|
_LOGGER.debug('%s: Set remote key to %s', self.entity_id, value)
|
||||||
|
service = MEDIA_PLAYER_KEYS.get(value)
|
||||||
|
if service:
|
||||||
|
# Handle Play Pause
|
||||||
|
if service == SERVICE_MEDIA_PLAY_PAUSE:
|
||||||
|
state = self.hass.states.get(self.entity_id).state
|
||||||
|
if state in (STATE_PLAYING, STATE_PAUSED):
|
||||||
|
service = SERVICE_MEDIA_PLAY if state == STATE_PAUSED \
|
||||||
|
else SERVICE_MEDIA_PAUSE
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||||
|
self.call_service(DOMAIN, service, params)
|
||||||
|
|
||||||
|
def update_state(self, new_state):
|
||||||
|
"""Update Television state after state changed."""
|
||||||
|
current_state = new_state.state
|
||||||
|
|
||||||
|
# Power state television
|
||||||
|
hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN)
|
||||||
|
if not self._flag[CHAR_ACTIVE]:
|
||||||
|
_LOGGER.debug('%s: Set current active state to %s',
|
||||||
|
self.entity_id, hk_state)
|
||||||
|
self.char_active.set_value(hk_state)
|
||||||
|
self._flag[CHAR_ACTIVE] = False
|
||||||
|
|
||||||
|
# Set mute state
|
||||||
|
if CHAR_VOLUME_SELECTOR in self.chars_speaker:
|
||||||
|
current_mute_state = new_state.attributes.get(
|
||||||
|
ATTR_MEDIA_VOLUME_MUTED)
|
||||||
|
if not self._flag[CHAR_MUTE]:
|
||||||
|
_LOGGER.debug('%s: Set current mute state to %s',
|
||||||
|
self.entity_id, current_mute_state)
|
||||||
|
self.char_mute.set_value(current_mute_state)
|
||||||
|
self._flag[CHAR_MUTE] = False
|
||||||
|
|
||||||
|
# Set active input
|
||||||
|
if self.support_select_source:
|
||||||
|
source_name = new_state.attributes.get(ATTR_INPUT_SOURCE)
|
||||||
|
if self.sources and not self._flag[CHAR_ACTIVE_IDENTIFIER]:
|
||||||
|
_LOGGER.debug('%s: Set current input to %s', self.entity_id,
|
||||||
|
source_name)
|
||||||
|
if source_name in self.sources:
|
||||||
|
index = self.sources.index(source_name)
|
||||||
|
self.char_input_source.set_value(index)
|
||||||
|
else:
|
||||||
|
_LOGGER.warning('%s: Sources out of sync. '
|
||||||
|
'Restart HomeAssistant', self.entity_id)
|
||||||
|
self.char_input_source.set_value(0)
|
||||||
|
self._flag[CHAR_ACTIVE_IDENTIFIER] = False
|
||||||
|
|
|
@ -59,10 +59,6 @@ def test_customize_options(config, name):
|
||||||
('Fan', 'fan.test', 'on', {}, {}),
|
('Fan', 'fan.test', 'on', {}, {}),
|
||||||
('Light', 'light.test', 'on', {}, {}),
|
('Light', 'light.test', 'on', {}, {}),
|
||||||
('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}),
|
('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}),
|
||||||
('MediaPlayer', 'media_player.test', 'on',
|
|
||||||
{ATTR_SUPPORTED_FEATURES: media_player_c.SUPPORT_TURN_ON |
|
|
||||||
media_player_c.SUPPORT_TURN_OFF}, {CONF_FEATURE_LIST:
|
|
||||||
{FEATURE_ON_OFF: None}}),
|
|
||||||
('SecuritySystem', 'alarm_control_panel.test', 'armed_away', {},
|
('SecuritySystem', 'alarm_control_panel.test', 'armed_away', {},
|
||||||
{ATTR_CODE: '1234'}),
|
{ATTR_CODE: '1234'}),
|
||||||
('Thermostat', 'climate.test', 'auto', {}, {}),
|
('Thermostat', 'climate.test', 'auto', {}, {}),
|
||||||
|
@ -101,6 +97,26 @@ def test_type_covers(type_name, entity_id, state, attrs):
|
||||||
assert mock_type.called
|
assert mock_type.called
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [
|
||||||
|
('MediaPlayer', 'media_player.test', 'on',
|
||||||
|
{ATTR_SUPPORTED_FEATURES: media_player_c.SUPPORT_TURN_ON |
|
||||||
|
media_player_c.SUPPORT_TURN_OFF}, {CONF_FEATURE_LIST:
|
||||||
|
{FEATURE_ON_OFF: None}}),
|
||||||
|
('TelevisionMediaPlayer', 'media_player.tv', 'on',
|
||||||
|
{ATTR_DEVICE_CLASS: 'tv'}, {}),
|
||||||
|
])
|
||||||
|
def test_type_media_player(type_name, entity_id, state, attrs, config):
|
||||||
|
"""Test if media_player types are associated correctly."""
|
||||||
|
mock_type = Mock()
|
||||||
|
with patch.dict(TYPES, {type_name: mock_type}):
|
||||||
|
entity_state = State(entity_id, state, attrs)
|
||||||
|
get_accessory(None, None, entity_state, 2, config)
|
||||||
|
assert mock_type.called
|
||||||
|
|
||||||
|
if config:
|
||||||
|
assert mock_type.call_args[0][-1] == config
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('type_name, entity_id, state, attrs', [
|
@pytest.mark.parametrize('type_name, entity_id, state, attrs', [
|
||||||
('BinarySensor', 'binary_sensor.opening', 'on',
|
('BinarySensor', 'binary_sensor.opening', 'on',
|
||||||
{ATTR_DEVICE_CLASS: 'opening'}),
|
{ATTR_DEVICE_CLASS: 'opening'}),
|
||||||
|
|
|
@ -3,12 +3,15 @@
|
||||||
from homeassistant.components.homekit.const import (
|
from homeassistant.components.homekit.const import (
|
||||||
ATTR_VALUE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
ATTR_VALUE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
||||||
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE)
|
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE)
|
||||||
from homeassistant.components.homekit.type_media_players import MediaPlayer
|
from homeassistant.components.media_player import DEVICE_CLASS_TV
|
||||||
|
from homeassistant.components.homekit.type_media_players import (
|
||||||
|
MediaPlayer, TelevisionMediaPlayer)
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
|
ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL,
|
||||||
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)
|
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_IDLE, STATE_OFF, STATE_ON,
|
ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_IDLE,
|
||||||
STATE_PAUSED, STATE_PLAYING)
|
STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING)
|
||||||
|
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
|
@ -129,3 +132,183 @@ async def test_media_player_set_state(hass, hk_driver, events):
|
||||||
assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False
|
assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False
|
||||||
assert len(events) == 8
|
assert len(events) == 8
|
||||||
assert events[-1].data[ATTR_VALUE] is None
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_media_player_television(hass, hk_driver, events, caplog):
|
||||||
|
"""Test if television accessory and HA are updated accordingly."""
|
||||||
|
entity_id = 'media_player.television'
|
||||||
|
|
||||||
|
# Supports 'select_source', 'volume_step', 'turn_on', 'turn_off',
|
||||||
|
# 'volume_mute', 'volume_set', 'pause'
|
||||||
|
hass.states.async_set(entity_id, None, {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, ATTR_SUPPORTED_FEATURES: 3469,
|
||||||
|
ATTR_MEDIA_VOLUME_MUTED: False, ATTR_INPUT_SOURCE_LIST: [
|
||||||
|
'HDMI 1', 'HDMI 2', 'HDMI 3', 'HDMI 4']})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = TelevisionMediaPlayer(hass, hk_driver, 'MediaPlayer', entity_id, 2,
|
||||||
|
None)
|
||||||
|
await hass.async_add_job(acc.run)
|
||||||
|
|
||||||
|
assert acc.aid == 2
|
||||||
|
assert acc.category == 31 # Television
|
||||||
|
|
||||||
|
assert acc.char_active.value == 0
|
||||||
|
assert acc.char_remote_key.value == 0
|
||||||
|
assert acc.char_input_source.value == 0
|
||||||
|
assert acc.char_mute.value is False
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_active.value == 1
|
||||||
|
assert acc.char_mute.value is True
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_OFF)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_active.value == 0
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: 'HDMI 2'})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_input_source.value == 1
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: 'HDMI 3'})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_input_source.value == 2
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: 'HDMI 5'})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_input_source.value == 0
|
||||||
|
assert caplog.records[-2].levelname == 'WARNING'
|
||||||
|
|
||||||
|
# Set from HomeKit
|
||||||
|
call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on')
|
||||||
|
call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off')
|
||||||
|
call_media_play = async_mock_service(hass, DOMAIN, 'media_play')
|
||||||
|
call_media_pause = async_mock_service(hass, DOMAIN, 'media_pause')
|
||||||
|
call_media_play_pause = async_mock_service(hass, DOMAIN,
|
||||||
|
'media_play_pause')
|
||||||
|
call_toggle_mute = async_mock_service(hass, DOMAIN, 'volume_mute')
|
||||||
|
call_select_source = async_mock_service(hass, DOMAIN, 'select_source')
|
||||||
|
call_volume_up = async_mock_service(hass, DOMAIN, 'volume_up')
|
||||||
|
call_volume_down = async_mock_service(hass, DOMAIN, 'volume_down')
|
||||||
|
call_volume_set = async_mock_service(hass, DOMAIN, 'volume_set')
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_active.client_update_value, 1)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_turn_on
|
||||||
|
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_active.client_update_value, 0)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_turn_off
|
||||||
|
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert len(events) == 2
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_remote_key.client_update_value, 11)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_media_play_pause
|
||||||
|
assert call_media_play_pause[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert len(events) == 3
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_add_job(acc.char_remote_key.client_update_value, 11)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_media_pause
|
||||||
|
assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert len(events) == 4
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_remote_key.client_update_value, 10)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(events) == 4
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_PAUSED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_add_job(acc.char_remote_key.client_update_value, 11)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_media_play
|
||||||
|
assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert len(events) == 5
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_mute.client_update_value, True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_toggle_mute
|
||||||
|
assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True
|
||||||
|
assert len(events) == 6
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_mute.client_update_value, False)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_toggle_mute
|
||||||
|
assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False
|
||||||
|
assert len(events) == 7
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_input_source.client_update_value, 1)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_select_source
|
||||||
|
assert call_select_source[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert call_select_source[0].data[ATTR_INPUT_SOURCE] == 'HDMI 2'
|
||||||
|
assert len(events) == 8
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_volume_selector.client_update_value, 0)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_volume_up
|
||||||
|
assert call_volume_up[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert len(events) == 9
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_volume_selector.client_update_value, 1)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_volume_down
|
||||||
|
assert call_volume_down[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert len(events) == 10
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
await hass.async_add_job(acc.char_volume.client_update_value, 20)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert call_volume_set[0]
|
||||||
|
assert call_volume_set[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert call_volume_set[0].data[ATTR_MEDIA_VOLUME_LEVEL] == 20
|
||||||
|
assert len(events) == 11
|
||||||
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_media_player_television_basic(hass, hk_driver, events, caplog):
|
||||||
|
"""Test if basic television accessory and HA are updated accordingly."""
|
||||||
|
entity_id = 'media_player.television'
|
||||||
|
|
||||||
|
# Supports turn_on', 'turn_off'
|
||||||
|
hass.states.async_set(entity_id, None, {
|
||||||
|
ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, ATTR_SUPPORTED_FEATURES: 384})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = TelevisionMediaPlayer(hass, hk_driver, 'MediaPlayer', entity_id, 2,
|
||||||
|
None)
|
||||||
|
await hass.async_add_job(acc.run)
|
||||||
|
|
||||||
|
assert acc.chars_tv == []
|
||||||
|
assert acc.chars_speaker == []
|
||||||
|
assert acc.support_select_source is False
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_active.value == 1
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_OFF)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_active.value == 0
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: 'HDMI 3'})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_active.value == 1
|
||||||
|
|
||||||
|
assert 'Error' not in caplog.messages[-1]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue