HDMI CEC - support for devices and commands (#4781)
* cec client object * cec command structure * autodetect source * volume support and native source select * switch device * media player device * detecting of state * friendly names * hdmi cec properties * presence detection * simplified callbacks * stable names * renamed methods * code cleanup * name with vendor * fixed standby call name * fake standby/poweron * domain switch * domain switch * async updating * update separated * cec -> hass event bridge * fixed name generation * code cleanup * code cleanup * icon constants * code cleanup * do not register unavailable devices * discovery of deevices * code cleanup * cec device discovery * moved method implementation into child * service descriptions * service descriptions * service descriptions * changed entity init sequence * logging cleanup * add remove as job * closing cec, no service schemas * correct iterate over dictionary * Volume by commands * threading * logging minimized * get load out of main thread * naming cleanup * get load out of main thread * optimized discovery * async where possible * cleanup logging, constructors first * pydoc * formatting * no async_update from out of loop no hiding entities removed redundant device_state_attributes async updating presence * no async * working async cec * cec in thirdparty lib * cec initialized oudsice * working without SIGSEGV * rollbacked file changed by mistake * sending of commands * working with ha * using hass loop and device driven updates * version up * version up * Command types in pycec, cleanup for HA integration * Removed media player, state moved to switch * service descriptions * requirements: pyCEC * line width to 79 * doc * doc * overindentation solved * HDMI to uppercase * minimal dependency on cec * removed unwanted line * doc wording * margin 79 * line continuation indent * imperative doc * lint: indentation * fixed overindented * fixed overindented * fixed overindented * fixed overindented * order of imports * PEP8 * keep signature of overriding * removed redundant blank line * fixed update call method (#4) * Preparation for merge to upstream (#5) * newer version of pyCEC * updated services.yaml * fixed lint scrpt to operate only on python files * pycec version up * update services * no coverage report * exclude non python files from lint * lint only on python files * Dev (#6) * reordered * sending nonserialized data through hass.data * code formatting * code formatting * import order * Dev (#7) * newer version of pyCEC * updated services.yaml * fixed lint scrpt to operate only on python files * pycec version up * update services * no coverage report * exclude non python files from lint * lint only on python files * reordered * sending nonserialized data through hass.data * import order * fixed object handling * code formatting * Backwards compatibility of hdmi_cec (#10) * services: power_on standby active_source * new version of pyCEC (#12) * newer version of pyCEC * devices config (#13) * getting device name from config * shutdown fix (#14) * correct call on shutdown * remove misplaced annotations (#15) * Preparation for merge to upstream (#5) * newer version of pyCEC * updated services.yaml * reordered * sending nonserialized data through hass.data * services: power_on standby active_source * code formatting * getting device name from config * correct call on shutdown * pyCEC version 0.3.6 (#18) * newer version of pyCEC * updated services.yaml * sending nonserialized data through hass.data * services: ** power_on ** standby ** active_source * getting device name from config * correct call on shutdown * fork new thread on multicore machines * support both config schemas: original and new (#16) * volume press and release support (#17) * support for media_player (#21) * accept hexadecimal format of commands * support for media player * platform customization * type constants * Dev (#23) * accept hexadecimal format of commands * support for media player * platform customization * TCP CEC support (#24) * accept hexadecimal format of commands * support for media player * platform customization * preparing tcp support * volume handling (#25) * Incorporated CR remarks (#26) * cleanup imports * cleanup and enhance services description * removed unwanted file * implemented CR remarks (#27) * pyCEC v0.4.6 * pined dependency version * tighten service schemas * requirements (#28)
This commit is contained in:
parent
cb47d16282
commit
067e11ea5c
6 changed files with 658 additions and 65 deletions
|
@ -214,6 +214,7 @@ omit =
|
||||||
homeassistant/components/media_player/emby.py
|
homeassistant/components/media_player/emby.py
|
||||||
homeassistant/components/media_player/firetv.py
|
homeassistant/components/media_player/firetv.py
|
||||||
homeassistant/components/media_player/gpmdp.py
|
homeassistant/components/media_player/gpmdp.py
|
||||||
|
homeassistant/components/media_player/hdmi_cec.py
|
||||||
homeassistant/components/media_player/itunes.py
|
homeassistant/components/media_player/itunes.py
|
||||||
homeassistant/components/media_player/kodi.py
|
homeassistant/components/media_player/kodi.py
|
||||||
homeassistant/components/media_player/lg_netcast.py
|
homeassistant/components/media_player/lg_netcast.py
|
||||||
|
@ -356,6 +357,7 @@ omit =
|
||||||
homeassistant/components/switch/digitalloggers.py
|
homeassistant/components/switch/digitalloggers.py
|
||||||
homeassistant/components/switch/dlink.py
|
homeassistant/components/switch/dlink.py
|
||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
|
homeassistant/components/switch/hdmi_cec.py
|
||||||
homeassistant/components/switch/hikvisioncam.py
|
homeassistant/components/switch/hikvisioncam.py
|
||||||
homeassistant/components/switch/hook.py
|
homeassistant/components/switch/hook.py
|
||||||
homeassistant/components/switch/kankun.py
|
homeassistant/components/switch/kankun.py
|
||||||
|
|
|
@ -1,27 +1,110 @@
|
||||||
"""
|
"""
|
||||||
CEC component.
|
HDMI CEC component.
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/hdmi_cec/
|
https://home-assistant.io/components/hdmi_cec/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START, CONF_DEVICES)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
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
|
REQUIREMENTS = ['pyCEC==0.4.6']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ATTR_DEVICE = 'device'
|
|
||||||
|
|
||||||
DOMAIN = 'hdmi_cec'
|
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_POWER_ON = 'power_on'
|
||||||
SERVICE_SELECT_DEVICE = 'select_device'
|
|
||||||
SERVICE_STANDBY = 'standby'
|
SERVICE_STANDBY = 'standby'
|
||||||
|
|
||||||
# pylint: disable=unnecessary-lambda
|
# pylint: disable=unnecessary-lambda
|
||||||
|
@ -30,92 +113,304 @@ DEVICE_SCHEMA = vol.Schema({
|
||||||
cv.string)
|
cv.string)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_PLATFORM, default=MEDIA_PLAYER): vol.Any(MEDIA_PLAYER,
|
||||||
|
SWITCH)
|
||||||
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: 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)
|
}, 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):
|
def parse_mapping(mapping, parents=None):
|
||||||
"""Parse configuration device mapping."""
|
"""Parse configuration device mapping."""
|
||||||
if parents is None:
|
if parents is None:
|
||||||
parents = []
|
parents = []
|
||||||
for addr, val in mapping.items():
|
for addr, val in mapping.items():
|
||||||
cur = parents + [str(addr)]
|
if isinstance(addr, (str,)) and isinstance(val, (str,)):
|
||||||
if isinstance(val, dict):
|
from pycec.network import PhysicalAddress
|
||||||
yield from parse_mapping(val, cur)
|
yield (addr, PhysicalAddress(val))
|
||||||
elif isinstance(val, str):
|
else:
|
||||||
yield (val, cur)
|
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):
|
def setup(hass: HomeAssistant, base_config):
|
||||||
"""Right-pad a physical address."""
|
|
||||||
return addr + ['0'] * (MAX_DEPTH - len(addr))
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Setup CEC capability."""
|
"""Setup CEC capability."""
|
||||||
global _CEC
|
from pycec.network import HDMINetwork
|
||||||
|
from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand
|
||||||
try:
|
from pycec.const import KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_MUTE, \
|
||||||
import cec
|
ADDR_AUDIOSYSTEM, ADDR_BROADCAST, ADDR_UNREGISTERED
|
||||||
except ImportError:
|
from pycec.cec import CecAdapter
|
||||||
_LOGGER.error("libcec must be installed")
|
from pycec.tcp import TcpAdapter
|
||||||
return False
|
|
||||||
|
|
||||||
# Parse configuration into a dict of device name to physical address
|
# Parse configuration into a dict of device name to physical address
|
||||||
# represented as a list of four elements.
|
# represented as a list of four elements.
|
||||||
flat = {}
|
device_aliases = {}
|
||||||
for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})):
|
devices = base_config[DOMAIN].get(CONF_DEVICES, {})
|
||||||
flat[pair[0]] = pad_physical_address(pair[1])
|
_LOGGER.debug("Parsing config %s", devices)
|
||||||
|
device_aliases.update(parse_mapping(devices))
|
||||||
|
_LOGGER.debug("Parsed devices: %s", device_aliases)
|
||||||
|
|
||||||
# Configure libcec.
|
platform = base_config[DOMAIN].get(CONF_PLATFORM, SWITCH)
|
||||||
cfg = cec.libcec_configuration()
|
|
||||||
cfg.strDeviceName = 'HASS'
|
|
||||||
cfg.bActivateSource = 0
|
|
||||||
cfg.bMonitorOnly = 1
|
|
||||||
cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT
|
|
||||||
|
|
||||||
# Setup CEC adapter.
|
loop = (
|
||||||
_CEC = cec.ICECAdapter.Create(cfg)
|
# 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):
|
def _volume(call):
|
||||||
"""Power on all devices."""
|
"""Increase/decrease volume and mute/unmute system."""
|
||||||
_CEC.PowerOnDevices()
|
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):
|
def _standby(call):
|
||||||
"""Standby all devices."""
|
hdmi_network.standby()
|
||||||
_CEC.StandbyDevices()
|
|
||||||
|
@callback
|
||||||
|
def _power_on(call):
|
||||||
|
hdmi_network.power_on()
|
||||||
|
|
||||||
def _select_device(call):
|
def _select_device(call):
|
||||||
"""Select the active device."""
|
"""Select the active device."""
|
||||||
path = flat.get(call.data[ATTR_DEVICE])
|
from pycec.network import PhysicalAddress
|
||||||
if not path:
|
|
||||||
|
addr = call.data[ATTR_DEVICE]
|
||||||
|
if not addr:
|
||||||
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
|
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
|
||||||
cmds = []
|
return
|
||||||
for i in range(1, MAX_DEPTH - 1):
|
if addr in device_aliases:
|
||||||
addr = pad_physical_address(path[:i])
|
addr = device_aliases[addr]
|
||||||
cmds.append('1f:82:{}{}:{}{}'.format(*addr))
|
else:
|
||||||
cmds.append('1f:86:{}{}:{}{}'.format(*addr))
|
entity = hass.states.get(addr)
|
||||||
for cmd in cmds:
|
_LOGGER.debug("Selecting entity %s", entity)
|
||||||
_CEC.Transmit(_CEC.CommandFromString(cmd))
|
if entity is not None:
|
||||||
_LOGGER.info("Selected %s", call.data[ATTR_DEVICE])
|
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):
|
def _start_cec(event):
|
||||||
"""Open CEC adapter."""
|
"""Register services and start HDMI network to watch for devices."""
|
||||||
adapters = _CEC.DetectAdapters()
|
descriptions = load_yaml_config_file(
|
||||||
if len(adapters) == 0:
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))[DOMAIN]
|
||||||
_LOGGER.error("No CEC adapter found")
|
hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, _tx,
|
||||||
return
|
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):
|
hdmi_network.set_new_device_callback(_new_device)
|
||||||
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on)
|
hdmi_network.start()
|
||||||
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
|
|
||||||
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE,
|
|
||||||
_select_device)
|
|
||||||
else:
|
|
||||||
_LOGGER.error("Failed to open adapter")
|
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||||
return True
|
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
|
||||||
|
|
175
homeassistant/components/media_player/hdmi_cec.py
Normal file
175
homeassistant/components/media_player/hdmi_cec.py
Normal file
|
@ -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
|
|
@ -153,3 +153,58 @@ verisure:
|
||||||
device_serial:
|
device_serial:
|
||||||
description: The serial number of the smartcam you want to capture an image from.
|
description: The serial number of the smartcam you want to capture an image from.
|
||||||
example: '2DEU AT5Z'
|
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.
|
||||||
|
|
63
homeassistant/components/switch/hdmi_cec.py
Normal file
63
homeassistant/components/switch/hdmi_cec.py
Normal file
|
@ -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
|
|
@ -377,6 +377,9 @@ pwaqi==1.3
|
||||||
# homeassistant.components.sensor.cpuspeed
|
# homeassistant.components.sensor.cpuspeed
|
||||||
py-cpuinfo==0.2.3
|
py-cpuinfo==0.2.3
|
||||||
|
|
||||||
|
# homeassistant.components.hdmi_cec
|
||||||
|
pyCEC==0.4.6
|
||||||
|
|
||||||
# homeassistant.components.switch.tplink
|
# homeassistant.components.switch.tplink
|
||||||
pyHS100==0.2.3
|
pyHS100==0.2.3
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue