diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 76ad5e3f12c..a37b085c0dc 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -6,6 +6,7 @@ from zlib import adler32 import voluptuous as vol from homeassistant.components import cover +from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, 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): """Take state and return an accessory object if supported.""" 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.', state.entity_id) return None @@ -138,10 +139,15 @@ def get_accessory(hass, driver, state, aid, config): a_type = 'Lock' elif state.domain == 'media_player': + device_class = state.attributes.get(ATTR_DEVICE_CLASS) feature_list = config.get(CONF_FEATURE_LIST) - if feature_list and \ - validate_media_player_features(state, feature_list): - a_type = 'MediaPlayer' + + if device_class == DEVICE_CLASS_TV: + a_type = 'TelevisionMediaPlayer' + else: + if feature_list and \ + validate_media_player_features(state, feature_list): + a_type = 'MediaPlayer' elif state.domain == 'sensor': device_class = state.attributes.get(ATTR_DEVICE_CLASS) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index a88cd7fc430..11c0314abf2 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -61,6 +61,7 @@ SERV_CONTACT_SENSOR = 'ContactSensor' SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' SERV_HUMIDITY_SENSOR = 'HumiditySensor' +SERV_INPUT_SOURCE = 'InputSource' SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHT_SENSOR = 'LightSensor' SERV_LIGHTBULB = 'Lightbulb' @@ -71,6 +72,8 @@ SERV_OUTLET = 'Outlet' SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' +SERV_TELEVISION = 'Television' +SERV_TELEVISION_SPEAKER = 'TelevisionSpeaker' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_VALVE = 'Valve' @@ -78,6 +81,7 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_ACTIVE = 'Active' +CHAR_ACTIVE_IDENTIFIER = 'ActiveIdentifier' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' CHAR_BATTERY_LEVEL = 'BatteryLevel' @@ -90,6 +94,7 @@ CHAR_CARBON_MONOXIDE_LEVEL = 'CarbonMonoxideLevel' CHAR_CARBON_MONOXIDE_PEAK_LEVEL = 'CarbonMonoxidePeakLevel' CHAR_CHARGING_STATE = 'ChargingState' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' +CHAR_CONFIGURED_NAME = 'ConfiguredName' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' @@ -99,10 +104,14 @@ CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' +CHAR_CURRENT_VISIBILITY_STATE = 'CurrentVisibilityState' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' +CHAR_IDENTIFIER = 'Identifier' CHAR_IN_USE = 'InUse' +CHAR_INPUT_SOURCE_TYPE = 'InputSourceType' +CHAR_IS_CONFIGURED = 'IsConfigured' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -110,15 +119,18 @@ CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' +CHAR_MUTE = 'Mute' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' CHAR_OUTLET_IN_USE = 'OutletInUse' CHAR_POSITION_STATE = 'PositionState' +CHAR_REMOTE_KEY = 'RemoteKey' CHAR_ROTATION_DIRECTION = 'RotationDirection' CHAR_ROTATION_SPEED = 'RotationSpeed' CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' +CHAR_SLEEP_DISCOVER_MODE = 'SleepDiscoveryMode' CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery' CHAR_SWING_MODE = 'SwingMode' @@ -129,6 +141,10 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' CHAR_VALVE_TYPE = 'ValveType' +CHAR_VOLUME = 'Volume' +CHAR_VOLUME_SELECTOR = 'VolumeSelector' +CHAR_VOLUME_CONTROL_TYPE = 'VolumeControlType' + # #### Properties #### PROP_MAX_VALUE = 'maxValue' diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index f8f4ef96992..b0c4be35e1b 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -1,23 +1,49 @@ """Class to hold all media player accessories.""" import logging -from pyhap.const import CATEGORY_SWITCH +from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION 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 ( - ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, - STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_STOP, + 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 .accessories import HomeAccessory from .const import ( - CHAR_NAME, CHAR_ON, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, - FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH) + CHAR_ACTIVE, CHAR_ACTIVE_IDENTIFIER, CHAR_CONFIGURED_NAME, + 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__) +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 = { FEATURE_ON_OFF: 'Power', FEATURE_PLAY_PAUSE: 'Play/Pause', @@ -142,3 +168,185 @@ class MediaPlayer(HomeAccessory): self.entity_id, current_state) self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) 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 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index fb46cf33404..a04f5906fef 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -59,10 +59,6 @@ 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: 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', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), @@ -101,6 +97,26 @@ def test_type_covers(type_name, entity_id, state, attrs): 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', [ ('BinarySensor', 'binary_sensor.opening', 'on', {ATTR_DEVICE_CLASS: 'opening'}), diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 065d1845fdb..3c00867a2cf 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -3,12 +3,15 @@ from homeassistant.components.homekit.const import ( ATTR_VALUE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, 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 ( + ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_IDLE, STATE_OFF, STATE_ON, - STATE_PAUSED, STATE_PLAYING) + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_IDLE, + STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) 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 len(events) == 8 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]