diff --git a/.coveragerc b/.coveragerc index 5bd097e8abb..ea0530aa8f7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -214,6 +214,7 @@ omit = homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py + homeassistant/components/media_player/hdmi_cec.py homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py @@ -356,6 +357,7 @@ omit = homeassistant/components/switch/digitalloggers.py homeassistant/components/switch/dlink.py homeassistant/components/switch/edimax.py + homeassistant/components/switch/hdmi_cec.py homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hook.py homeassistant/components/switch/kankun.py diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index 4fab7f84bd3..44b205993b6 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -1,27 +1,110 @@ """ -CEC component. +HDMI CEC component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/hdmi_cec/ """ import logging +import multiprocessing +import os +from collections import defaultdict +from functools import reduce import voluptuous as vol -from homeassistant.const import (EVENT_HOMEASSISTANT_START, CONF_DEVICES) import homeassistant.helpers.config_validation as cv +from homeassistant import core +from homeassistant.components import discovery +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER +from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.config import load_yaml_config_file +from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, + EVENT_HOMEASSISTANT_STOP, STATE_ON, + STATE_OFF, CONF_DEVICES, CONF_PLATFORM, + CONF_CUSTOMIZE, STATE_PLAYING, STATE_IDLE, + STATE_PAUSED, CONF_HOST) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity -_CEC = None -_LOGGER = logging.getLogger(__name__) - -ATTR_DEVICE = 'device' +REQUIREMENTS = ['pyCEC==0.4.6'] DOMAIN = 'hdmi_cec' -MAX_DEPTH = 4 +_LOGGER = logging.getLogger(__name__) + +ICON_UNKNOWN = 'mdi:help' +ICON_AUDIO = 'mdi:speaker' +ICON_PLAYER = 'mdi:play' +ICON_TUNER = 'mdi:nest-thermostat' +ICON_RECORDER = 'mdi:microphone' +ICON_TV = 'mdi:television' +ICONS_BY_TYPE = { + 0: ICON_TV, + 1: ICON_RECORDER, + 3: ICON_TUNER, + 4: ICON_PLAYER, + 5: ICON_AUDIO +} + +CEC_DEVICES = defaultdict(list) + +CMD_UP = 'up' +CMD_DOWN = 'down' +CMD_MUTE = 'mute' +CMD_UNMUTE = 'unmute' +CMD_MUTE_TOGGLE = 'toggle mute' +CMD_PRESS = 'press' +CMD_RELEASE = 'release' + +EVENT_CEC_COMMAND_RECEIVED = 'cec_command_received' +EVENT_CEC_KEYPRESS_RECEIVED = 'cec_keypress_received' + +ATTR_PHYSICAL_ADDRESS = 'physical_address' +ATTR_TYPE_ID = 'type_id' +ATTR_VENDOR_NAME = 'vendor_name' +ATTR_VENDOR_ID = 'vendor_id' +ATTR_DEVICE = 'device' +ATTR_COMMAND = 'command' +ATTR_TYPE = 'type' +ATTR_KEY = 'key' +ATTR_DUR = 'dur' +ATTR_SRC = 'src' +ATTR_DST = 'dst' +ATTR_CMD = 'cmd' +ATTR_ATT = 'att' +ATTR_RAW = 'raw' +ATTR_DIR = 'dir' +ATTR_ABT = 'abt' +ATTR_NEW = 'new' + +_VOL_HEX = vol.Any(vol.Coerce(int), lambda x: int(x, 16)) + +SERVICE_SEND_COMMAND = 'send_command' +SERVICE_SEND_COMMAND_SCHEMA = vol.Schema({ + vol.Optional(ATTR_CMD): _VOL_HEX, + vol.Optional(ATTR_SRC): _VOL_HEX, + vol.Optional(ATTR_DST): _VOL_HEX, + vol.Optional(ATTR_ATT): _VOL_HEX, + vol.Optional(ATTR_RAW): vol.Coerce(str) +}, extra=vol.PREVENT_EXTRA) + +SERVICE_VOLUME = 'volume' +SERVICE_VOLUME_SCHEMA = vol.Schema({ + vol.Optional(CMD_UP): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), + vol.Optional(CMD_DOWN): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)), + vol.Optional(CMD_MUTE): None, + vol.Optional(CMD_UNMUTE): None, + vol.Optional(CMD_MUTE_TOGGLE): None +}, extra=vol.PREVENT_EXTRA) + +SERVICE_UPDATE_DEVICES = 'update' +SERVICE_UPDATE_DEVICES_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({}) +}, extra=vol.PREVENT_EXTRA) + +SERVICE_SELECT_DEVICE = 'select_device' SERVICE_POWER_ON = 'power_on' -SERVICE_SELECT_DEVICE = 'select_device' SERVICE_STANDBY = 'standby' # pylint: disable=unnecessary-lambda @@ -30,92 +113,304 @@ DEVICE_SCHEMA = vol.Schema({ cv.string) }) +CUSTOMIZE_SCHEMA = vol.Schema({ + vol.Optional(CONF_PLATFORM, default=MEDIA_PLAYER): vol.Any(MEDIA_PLAYER, + SWITCH) +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_DEVICES): DEVICE_SCHEMA + vol.Optional(CONF_DEVICES): vol.Any(DEVICE_SCHEMA, + vol.Schema({ + vol.All(cv.string): vol.Any( + cv.string) + })), + vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER), + vol.Optional(CONF_HOST): cv.string, }) }, extra=vol.ALLOW_EXTRA) +def pad_physical_address(addr): + """Right-pad a physical address.""" + return addr + [0] * (4 - len(addr)) + + def parse_mapping(mapping, parents=None): """Parse configuration device mapping.""" if parents is None: parents = [] for addr, val in mapping.items(): - cur = parents + [str(addr)] - if isinstance(val, dict): - yield from parse_mapping(val, cur) - elif isinstance(val, str): - yield (val, cur) + if isinstance(addr, (str,)) and isinstance(val, (str,)): + from pycec.network import PhysicalAddress + yield (addr, PhysicalAddress(val)) + else: + cur = parents + [addr] + if isinstance(val, dict): + yield from parse_mapping(val, cur) + elif isinstance(val, str): + yield (val, pad_physical_address(cur)) -def pad_physical_address(addr): - """Right-pad a physical address.""" - return addr + ['0'] * (MAX_DEPTH - len(addr)) - - -def setup(hass, config): +def setup(hass: HomeAssistant, base_config): """Setup CEC capability.""" - global _CEC - - try: - import cec - except ImportError: - _LOGGER.error("libcec must be installed") - return False + from pycec.network import HDMINetwork + from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand + from pycec.const import KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_MUTE, \ + ADDR_AUDIOSYSTEM, ADDR_BROADCAST, ADDR_UNREGISTERED + from pycec.cec import CecAdapter + from pycec.tcp import TcpAdapter # Parse configuration into a dict of device name to physical address # represented as a list of four elements. - flat = {} - for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})): - flat[pair[0]] = pad_physical_address(pair[1]) + device_aliases = {} + devices = base_config[DOMAIN].get(CONF_DEVICES, {}) + _LOGGER.debug("Parsing config %s", devices) + device_aliases.update(parse_mapping(devices)) + _LOGGER.debug("Parsed devices: %s", device_aliases) - # Configure libcec. - cfg = cec.libcec_configuration() - cfg.strDeviceName = 'HASS' - cfg.bActivateSource = 0 - cfg.bMonitorOnly = 1 - cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT + platform = base_config[DOMAIN].get(CONF_PLATFORM, SWITCH) - # Setup CEC adapter. - _CEC = cec.ICECAdapter.Create(cfg) + loop = ( + # Create own thread if more than 1 CPU + hass.loop if multiprocessing.cpu_count() < 2 else None) + host = base_config[DOMAIN].get(CONF_HOST, None) + if host: + adapter = TcpAdapter(host, name="HASS", activate_source=False) + else: + adapter = CecAdapter(name="HASS", activate_source=False) + hdmi_network = HDMINetwork(adapter, loop=loop) - def _power_on(call): - """Power on all devices.""" - _CEC.PowerOnDevices() + def _volume(call): + """Increase/decrease volume and mute/unmute system.""" + for cmd, att in call.data.items(): + if cmd == CMD_UP: + _process_volume(KEY_VOLUME_UP, att) + elif cmd == CMD_DOWN: + _process_volume(KEY_VOLUME_DOWN, att) + elif cmd == CMD_MUTE: + hdmi_network.send_command( + KeyPressCommand(KEY_MUTE, dst=ADDR_AUDIOSYSTEM)) + hdmi_network.send_command( + KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + _LOGGER.info("Audio muted") + else: + _LOGGER.warning("Unknown command %s", cmd) + def _process_volume(cmd, att): + if isinstance(att, (str,)): + att = att.strip() + if att == CMD_PRESS: + hdmi_network.send_command( + KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) + elif att == CMD_RELEASE: + hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + else: + att = 1 if att == "" else int(att) + for _ in range(1, att): + hdmi_network.send_command( + KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) + hdmi_network.send_command( + KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) + + def _tx(call): + """Send CEC command.""" + data = call.data + if ATTR_RAW in data: + command = CecCommand(data[ATTR_RAW]) + else: + if ATTR_SRC in data: + src = data[ATTR_SRC] + else: + src = ADDR_UNREGISTERED + if ATTR_DST in data: + dst = data[ATTR_DST] + else: + dst = ADDR_BROADCAST + if ATTR_CMD in data: + cmd = data[ATTR_CMD] + else: + _LOGGER.error("Attribute 'cmd' is missing") + return False + if ATTR_ATT in data: + if isinstance(data[ATTR_ATT], (list,)): + att = data[ATTR_ATT] + else: + att = reduce(lambda x, y: "%s:%x" % (x, y), data[ATTR_ATT]) + else: + att = "" + command = CecCommand(cmd, dst, src, att) + hdmi_network.send_command(command) + + @callback def _standby(call): - """Standby all devices.""" - _CEC.StandbyDevices() + hdmi_network.standby() + + @callback + def _power_on(call): + hdmi_network.power_on() def _select_device(call): """Select the active device.""" - path = flat.get(call.data[ATTR_DEVICE]) - if not path: + from pycec.network import PhysicalAddress + + addr = call.data[ATTR_DEVICE] + if not addr: _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) - cmds = [] - for i in range(1, MAX_DEPTH - 1): - addr = pad_physical_address(path[:i]) - cmds.append('1f:82:{}{}:{}{}'.format(*addr)) - cmds.append('1f:86:{}{}:{}{}'.format(*addr)) - for cmd in cmds: - _CEC.Transmit(_CEC.CommandFromString(cmd)) - _LOGGER.info("Selected %s", call.data[ATTR_DEVICE]) + return + if addr in device_aliases: + addr = device_aliases[addr] + else: + entity = hass.states.get(addr) + _LOGGER.debug("Selecting entity %s", entity) + if entity is not None: + addr = entity.attributes['physical_address'] + _LOGGER.debug("Address acquired: %s", addr) + if addr is None: + _LOGGER.error("Device %s has not physical address.", + call.data[ATTR_DEVICE]) + return + if not isinstance(addr, (PhysicalAddress,)): + addr = PhysicalAddress(addr) + hdmi_network.active_source(addr) + _LOGGER.info("Selected %s (%s)", call.data[ATTR_DEVICE], addr) + + def _update(call): + """ + Callback called when device update is needed. + + - called by service, requests CEC network to update data. + """ + hdmi_network.scan() + + @callback + def _new_device(device): + """Called when new device is detected by HDMI network.""" + key = DOMAIN + '.' + device.name + hass.data[key] = device + discovery.load_platform(hass, base_config.get(core.DOMAIN).get( + CONF_CUSTOMIZE, {}).get(key, {}).get(CONF_PLATFORM, platform), + DOMAIN, discovered={ATTR_NEW: [key]}, + hass_config=base_config) + + def _shutdown(call): + hdmi_network.stop() def _start_cec(event): - """Open CEC adapter.""" - adapters = _CEC.DetectAdapters() - if len(adapters) == 0: - _LOGGER.error("No CEC adapter found") - return + """Register services and start HDMI network to watch for devices.""" + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml'))[DOMAIN] + hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, _tx, + descriptions[SERVICE_SEND_COMMAND], + SERVICE_SEND_COMMAND_SCHEMA) + hass.services.register(DOMAIN, SERVICE_VOLUME, _volume, + descriptions[SERVICE_VOLUME], + SERVICE_VOLUME_SCHEMA) + hass.services.register(DOMAIN, SERVICE_UPDATE_DEVICES, _update, + descriptions[SERVICE_UPDATE_DEVICES], + SERVICE_UPDATE_DEVICES_SCHEMA) + hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) + hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) + hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, _select_device) - if _CEC.Open(adapters[0].strComName): - hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) - hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) - hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, - _select_device) - else: - _LOGGER.error("Failed to open adapter") + hdmi_network.set_new_device_callback(_new_device) + hdmi_network.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) return True + + +class CecDevice(Entity): + """Representation of a HDMI CEC device entity.""" + + def __init__(self, hass: HomeAssistant, device, logical): + """Initialize the device.""" + self._device = device + self.hass = hass + self._icon = None + self._state = STATE_UNKNOWN + self._logical_address = logical + self.entity_id = "%s.%d" % (DOMAIN, self._logical_address) + device.set_update_callback(self._update) + + def update(self): + """Update device status.""" + self._update() + + def _update(self, device=None): + """Update device status.""" + if device: + from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ + POWER_OFF, POWER_ON + if device.power_status == POWER_OFF: + self._state = STATE_OFF + elif device.status == STATUS_PLAY: + self._state = STATE_PLAYING + elif device.status == STATUS_STOP: + self._state = STATE_IDLE + elif device.status == STATUS_STILL: + self._state = STATE_PAUSED + elif device.power_status == POWER_ON: + self._state = STATE_ON + else: + _LOGGER.warning("Unknown state: %d", device.power_status) + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the device.""" + return ( + "%s %s" % (self.vendor_name, self._device.osd_name) + if (self._device.osd_name is not None and + self.vendor_name is not None and self.vendor_name != 'Unknown') + else "%s %d" % (self._device.type_name, self._logical_address) + if self._device.osd_name is None + else "%s %d (%s)" % (self._device.type_name, self._logical_address, + self._device.osd_name)) + + @property + def vendor_id(self): + """ID of device's vendor.""" + return self._device.vendor_id + + @property + def vendor_name(self): + """Name of device's vendor.""" + return self._device.vendor + + @property + def physical_address(self): + """Physical address of device in HDMI network.""" + return str(self._device.physical_address) + + @property + def type(self): + """String representation of device's type.""" + return self._device.type_name + + @property + def type_id(self): + """Type ID of device.""" + return self._device.type + + @property + def icon(self): + """Icon for device by its type.""" + return (self._icon if self._icon is not None else + ICONS_BY_TYPE.get(self._device.type) + if self._device.type in ICONS_BY_TYPE else ICON_UNKNOWN) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state_attr = {} + if self.vendor_id is not None: + state_attr[ATTR_VENDOR_ID] = self.vendor_id + state_attr[ATTR_VENDOR_NAME] = self.vendor_name + if self.type_id is not None: + state_attr[ATTR_TYPE_ID] = self.type_id + state_attr[ATTR_TYPE] = self.type + if self.physical_address is not None: + state_attr[ATTR_PHYSICAL_ADDRESS] = self.physical_address + return state_attr diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py new file mode 100644 index 00000000000..4998072018e --- /dev/null +++ b/homeassistant/components/media_player/hdmi_cec.py @@ -0,0 +1,175 @@ +""" +Support for HDMI CEC devices as media players. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/hdmi_cec/ +""" +import logging + +from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice +from homeassistant.components.media_player import MediaPlayerDevice, DOMAIN, \ + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, \ + SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_STOP, \ + SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE +from homeassistant.const import STATE_ON, STATE_OFF, STATE_PLAYING, \ + STATE_IDLE, STATE_PAUSED +from homeassistant.core import HomeAssistant + +DEPENDENCIES = ['hdmi_cec'] + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return HDMI devices as +switches.""" + if ATTR_NEW in discovery_info: + _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) + add_devices(CecPlayerDevice(hass, hass.data.get(device), + hass.data.get(device).logical_address) for + device in discovery_info[ATTR_NEW]) + + +class CecPlayerDevice(CecDevice, MediaPlayerDevice): + """Representation of a HDMI device as a Media palyer.""" + + def __init__(self, hass: HomeAssistant, device, logical): + """Initialize the HDMI device.""" + CecDevice.__init__(self, hass, device, logical) + self.entity_id = "%s.%s_%s" % ( + DOMAIN, 'hdmi', hex(self._logical_address)[2:]) + self.update() + + def send_keypress(self, key): + """Send keypress to CEC adapter.""" + from pycec.commands import KeyPressCommand, KeyReleaseCommand + _LOGGER.debug("Sending keypress %s to device %s", hex(key), + hex(self._logical_address)) + self._device.send_command( + KeyPressCommand(key, dst=self._logical_address)) + self._device.send_command( + KeyReleaseCommand(dst=self._logical_address)) + + def send_playback(self, key): + """Send playback status to CEC adapter.""" + from pycec.commands import CecCommand + self._device.async_send_command( + CecCommand(key, dst=self._logical_address)) + + def mute_volume(self, mute): + """Mute volume.""" + from pycec.const import KEY_MUTE + self.send_keypress(KEY_MUTE) + + def media_previous_track(self): + """Go to previous track.""" + from pycec.const import KEY_BACKWARD + self.send_keypress(KEY_BACKWARD) + + def turn_on(self): + """Turn device on.""" + self._device.turn_on() + self._state = STATE_ON + + def clear_playlist(self): + """Clear players playlist.""" + raise NotImplementedError() + + def turn_off(self): + """Turn device off.""" + self._device.turn_off() + self._state = STATE_OFF + + def media_stop(self): + """Stop playback.""" + from pycec.const import KEY_STOP + self.send_keypress(KEY_STOP) + self._state = STATE_IDLE + + def play_media(self, media_type, media_id): + """Not supported.""" + raise NotImplementedError() + + def media_next_track(self): + """Skip to next track.""" + from pycec.const import KEY_FORWARD + self.send_keypress(KEY_FORWARD) + + def media_seek(self, position): + """Not supported.""" + raise NotImplementedError() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + raise NotImplementedError() + + def media_pause(self): + """Pause playback.""" + from pycec.const import KEY_PAUSE + self.send_keypress(KEY_PAUSE) + self._state = STATE_PAUSED + + def select_source(self, source): + """Not supported.""" + raise NotImplementedError() + + def media_play(self): + """Start playback.""" + from pycec.const import KEY_PLAY + self.send_keypress(KEY_PLAY) + self._state = STATE_PLAYING + + def volume_up(self): + """Increase volume.""" + from pycec.const import KEY_VOLUME_UP + _LOGGER.debug("%s: volume up", self._logical_address) + self.send_keypress(KEY_VOLUME_UP) + + def volume_down(self): + """Decrease volume.""" + from pycec.const import KEY_VOLUME_DOWN + _LOGGER.debug("%s: volume down", self._logical_address) + self.send_keypress(KEY_VOLUME_DOWN) + + @property + def state(self) -> str: + """Cached state of device.""" + return self._state + + def _update(self, device=None): + """Update device status.""" + if device: + from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \ + POWER_OFF, POWER_ON + if device.power_status == POWER_OFF: + self._state = STATE_OFF + elif not self.support_pause: + if device.power_status == POWER_ON: + self._state = STATE_ON + elif device.status == STATUS_PLAY: + self._state = STATE_PLAYING + elif device.status == STATUS_STOP: + self._state = STATE_IDLE + elif device.status == STATUS_STILL: + self._state = STATE_PAUSED + else: + _LOGGER.warning("Unknown state: %s", device.status) + self.schedule_update_ha_state() + + @property + def supported_media_commands(self): + """Flag media commands that are supported.""" + from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, \ + TYPE_AUDIO + if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | + SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_PREVIOUS_TRACK | + SUPPORT_NEXT_TRACK) + if self.type == TYPE_TUNER: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | + SUPPORT_PAUSE | SUPPORT_STOP) + if self.type_id == TYPE_AUDIO: + return (SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | + SUPPORT_VOLUME_MUTE) + return SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 54c0e18a3ee..53f82d5c059 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -153,3 +153,58 @@ verisure: device_serial: description: The serial number of the smartcam you want to capture an image from. example: '2DEU AT5Z' + +hdmi_cec: + send_command: + description: Sends CEC command into HDMI CEC capable adapter. + + fields: + raw: + description: 'Raw CEC command in format "00:00:00:00" where first two digits are source and destination, second byte is command and optional other bytes are command parameters. If raw command specified, other params are ignored.' + example: '"10:36"' + + src: + desctiption: 'Source of command. Could be decimal number or string with hexadeximal notation: "0x10".' + example: '12 or "0xc"' + + dst: + description: 'Destination for command. Could be decimal number or string with hexadeximal notation: "0x10".' + example: '5 or "0x5"' + + cmd: + description: 'Command itself. Could be decimal number or string with hexadeximal notation: "0x10".' + example: '144 or "0x90"' + + att: + description: Optional parameters. + example: [0, 2] + + update: + description: Update devices state from network. + + volume: + description: Increase or decrease volume of system. + + fields: + up: + description: Increases volume x levels. + example: 3 + down: + description: Decreases volume x levels. + example: 3 + mute: Mutes audio system. Value is ignored. + unmute: Unmutes audio system. Value is ignored. + toggle mute: Toggles mute of audio system. Value is ignored. + + select_device: + description: Select HDMI device. + fields: + device: + description: Addres of device to select. Can be entity_id, physical address or alias from confuguration. + example: '"switch.hdmi_1" or "1.1.0.0" or "01:10"' + + power_on: + description: Power on all devices which supports it. + + standby: + description: Standby all devices which supports it. diff --git a/homeassistant/components/switch/hdmi_cec.py b/homeassistant/components/switch/hdmi_cec.py new file mode 100644 index 00000000000..bd1f9ea6578 --- /dev/null +++ b/homeassistant/components/switch/hdmi_cec.py @@ -0,0 +1,63 @@ +""" +Support for HDMI CEC devices as switches. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/hdmi_cec/ +""" +import logging + +from homeassistant.components.hdmi_cec import CecDevice, ATTR_NEW +from homeassistant.components.switch import SwitchDevice, DOMAIN +from homeassistant.const import STATE_OFF, STATE_STANDBY, STATE_ON +from homeassistant.core import HomeAssistant + +DEPENDENCIES = ['hdmi_cec'] + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return HDMI devices as switches.""" + if ATTR_NEW in discovery_info: + _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) + add_devices(CecSwitchDevice(hass, hass.data.get(device), + hass.data.get(device).logical_address) for + device in discovery_info[ATTR_NEW]) + + +class CecSwitchDevice(CecDevice, SwitchDevice): + """Representation of a HDMI device as a Switch.""" + + def __init__(self, hass: HomeAssistant, device, logical): + """Initialize the HDMI device.""" + CecDevice.__init__(self, hass, device, logical) + self.entity_id = "%s.%s_%s" % ( + DOMAIN, 'hdmi', hex(self._logical_address)[2:]) + self.update() + + def turn_on(self, **kwargs) -> None: + """Turn device on.""" + self._device.turn_on() + self._state = STATE_ON + + def turn_off(self, **kwargs) -> None: + """Turn device off.""" + self._device.turn_off() + self._state = STATE_ON + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._state == STATE_ON + + @property + def is_standby(self): + """Return true if device is in standby.""" + return self._state == STATE_OFF or self._state == STATE_STANDBY + + @property + def state(self) -> str: + """Cached state of device.""" + return self._state diff --git a/requirements_all.txt b/requirements_all.txt index ec328eca44d..59927a3672b 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -377,6 +377,9 @@ pwaqi==1.3 # homeassistant.components.sensor.cpuspeed py-cpuinfo==0.2.3 +# homeassistant.components.hdmi_cec +pyCEC==0.4.6 + # homeassistant.components.switch.tplink pyHS100==0.2.3