Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HDMI CEC - support for devices and commands #4781

Merged
merged 104 commits into from
Jan 20, 2017
Merged

HDMI CEC - support for devices and commands #4781

merged 104 commits into from
Jan 20, 2017

Conversation

konikvranik
Copy link
Contributor

@konikvranik konikvranik commented Dec 6, 2016

Description:

This pullrequest extends functionality of previous hdmi_cec platform so now detected HDMI devices are handled as switch entites, reports name, vendor, address and state, could be turned on/off.
Also adds services to pass arbitraty CEC command and control volume.

Pull request in home-assistant.github.io with documentation: home-assistant/home-assistant.io#1642

Example entry for configuration.yaml (if applicable):

hdmi_cec:

Checklist:

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • New dependencies have been added to the REQUIREMENTS variable ([example][ex-requir]).
  • New dependencies are only imported inside functions that use them ([example][ex-import]).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.

@mention-bot
Copy link

@konikvranik, thanks for your PR! By analyzing the history of the files in this pull request, we identified @fabaff, @happyleavesaoc and @balloob to be potential reviewers.

_LOGGER.info("Creating %s switch %d", DOMAIN, logical)
self._state = False
CecDevice.__init__(self, hass, cec_client, logical)
self.entity_id = "%s.%s_%s" % (DOMAIN, 'hdmi', hex(self._logical_address)[2:])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (86 > 79 characters)

if ATTR_NEW in discovery_info:
_LOGGER.info("Setting up devices %s", discovery_info[ATTR_NEW])
add_devices(CecSwitch(hass, CEC_CLIENT, device) for device in
filter(lambda x: x not in DEVICE_PRESENCE or not DEVICE_PRESENCE[x], discovery_info[ATTR_NEW]))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (115 > 79 characters)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use filter. It does not give readable code.

"""
import logging

from homeassistant.components.hdmi_cec import CecDevice, CEC_CLIENT, ATTR_NEW, DEVICE_PRESENCE

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (94 > 79 characters)

"""
import logging

from homeassistant.components.hdmi_cec import CecDevice, CEC_DEVICES, CEC_CLIENT

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (80 > 79 characters)

def cec_key_press_callback(self, key, duration):
"""key press callback"""
_LOGGER.info("[key pressed] " + str(key))
self.hass.bus.fire(EVENT_CEC_KEYPRESS_RECEIVED, {ATTR_KEY: key, ATTR_DUR: duration})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (92 > 79 characters)

self.lib_cec = cec.ICECAdapter.Create(self.cecconfig)
# print libCEC version and compilation information
_LOGGER.info("libCEC version " + self.lib_cec.VersionToString(
self.cecconfig.serverVersion) + " loaded: " + self.lib_cec.GetLibInfo())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (84 > 79 characters)

def available(self):
return self._available

@property

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redefinition of unused 'device_state_attributes' from line 342


@property
def icon(self):
return icon_by_type(self._cec_type_id) if self._icon is None else self._icon

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (84 > 79 characters)


@property
def vendor_name(self):
return VENDORS[self._vendor_id] if self._vendor_id in VENDORS else VENDORS[0]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (85 > 79 characters)

and self.vendor_name != 'Unknown' \
else "%s %d" % (DEVICE_TYPE_NAMES[self._cec_type_id], self._logical_address) if self._name is None \
else "%s %d (%s)" % (
DEVICE_TYPE_NAMES[self._cec_type_id], self._logical_address, self._name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (84 > 79 characters)

def name(self):
"""Return the name of the device."""
return "%s %s" % (self.vendor_name, self._name) if self._name is not None and self.vendor_name is not None \
and self.vendor_name != 'Unknown' \

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line over-indented for visual indent

self._physical_address = "%d.%d.%d.%d" % (
cmd_chain[0] / 0x10, cmd_chain[0] % 0x10, cmd_chain[1] / 0x10, cmd_chain[1] % 0x10)
self._cec_type_id = cmd_chain[2]
_LOGGER.info("Got physical address and type for device %x -> %x, %s, %s",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (89 > 79 characters)

_LOGGER.info("Got vendor for device %x -> %x, %s: %s", src, dst, self.vendor_id, self.vendor_name)
elif cmd == 0x84:
self._physical_address = "%d.%d.%d.%d" % (
cmd_chain[0] / 0x10, cmd_chain[0] % 0x10, cmd_chain[1] / 0x10, cmd_chain[1] % 0x10)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (103 > 79 characters)

for i in cmd_chain:
self._vendor_id *= 0x100
self._vendor_id += i
_LOGGER.info("Got vendor for device %x -> %x, %s: %s", src, dst, self.vendor_id, self.vendor_name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (114 > 79 characters)

self._name = ''
for c in cmd_chain:
self._name += chr(c)
_LOGGER.info("Got name for device %x -> %x, %s", src, dst, self._name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (86 > 79 characters)

self._state = STATE_OFF
else:
self._state = STATE_UNKNOWN
_LOGGER.info("Got status for device %x -> %x, %s", src, dst, self._state)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (89 > 79 characters)

@balloob balloob changed the base branch from master to dev December 7, 2016 05:08
Copy link
Member

@balloob balloob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of changes needed. Biggest one is that all the CEC protocol/vendor specific information will have to be extracted into a third party lib.

_LOGGER.debug("CEC setup")
config = base_config.get(DOMAIN)

global CEC_CLIENT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use a global but use an entry in the dictionary hass.data instead.

self._available = False
self._hidden = False
self._name = None
DEVICE_PRESENCE[logical] = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use hass.data


MAX_DEPTH = 4
VENDORS = {0x000039: 'Toshiba',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is too much protocol specific logic in this component. This should be moved to a third party lib on which Home Assistant can depend.

dev_type = 'switch'

while True:
new_devices = filter(lambda x: CEC_CLIENT.lib_cec.PollDevice(x),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rewrite this using for loops to keep this code understandable.

global CEC_CLIENT
CEC_CLIENT = CecClient(hass)
exclude = config.get(CONF_EXCLUDE)
stop = threading.Event()

def _start_cec(event):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You cannot have an event handler that never returns, you are then stealing a thread from HASS. If you want to run in a thread, start your own.


def turn_on(self, **kwargs) -> None:
"""Turn device on."""
_LOGGER.info("***************** turn_on *****************")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove these logging

_LOGGER.info("***************** turn_on *****************")
self.cec_client.lib_cec.PowerOnDevices(self._logical_address)
self._state = STATE_ON
self.async_update()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't call async_update from outside the event loop


def turn_off(self, **kwargs) -> None:
"""Turn device off."""
_LOGGER.info("***************** turn_off *****************")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

_LOGGER.info("***************** turn_off *****************")
self.cec_client.lib_cec.StandbyDevices(self._logical_address)
self._state = STATE_STANDBY
self.async_update()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No async_update from outside event loop

self.async_update()
self.schedule_update_ha_state()

def __init__(self, hass, cec_client, logical):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put init methods on the top of a class definition.

@balloob balloob self-assigned this Dec 7, 2016
def available(self):
return self._available

@property

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redefinition of unused 'device_state_attributes' from line 339

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' \

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line over-indented for visual indent

@konikvranik
Copy link
Contributor Author

As you asked, I moved CEC specific stuff into separate library https://github.com/konikvranik/pycec

@rytilahti
Copy link
Member

Thanks a lot for your work, I'm eagerly waiting for this to get merged into dev. It'd be nice if you'd merge related commits (even for this published PR, if it's okay for HASS developers) or at least for the future ones. :)

@konikvranik
Copy link
Contributor Author

@rytilahti Thank you. I created PR to master. So should I create PR into dev branch?

@kellerza
Copy link
Member

We can squash before we merge, but you should really be running local tests! Try the shortcut:

$ script/lint --changed

If you add new files without tests, you should add them to coveragerc

@kellerza
Copy link
Member

The PR is against homeassistant:dev so no worries. You can have it in any branch in your repo (but makes your life easier if you work in a new branch in your repo)

@konikvranik
Copy link
Contributor Author

@kellerza So I added also switch/hdmi_cec into .coveragerc.

$ script/lint --changed

Now passing without error, but I have had to modify it to check python files only. It failed on "syntax error" in services.yaml.

I also run whole tox lint and also passed.

Only I suspect to miss now is to slightly update documentation on home-assistant.github.io

Copy link
Member

@kellerza kellerza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not looked at your custom library, but you should make sure that the component you created can function (and most importantly that you can test it!) outside of HASS


CONFIG_SCHEMA = vol.Schema({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should always have a CONFIG_SCHEMA in the component to validate your config.

CONFIG_SCHEMA allows extra keys

@@ -172,6 +172,7 @@ omit =
homeassistant/components/feedreader.py
homeassistant/components/foursquare.py
homeassistant/components/hdmi_cec.py
homeassistant/components/switch/hdmi_cec.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you please sort 😄

vol.Optional(CMD_UNMUTE): None,
vol.Optional(CMD_MUTE_TOGGLE): None
})
}, extra=vol.REMOVE_EXTRA)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For services SCHEMAS you probably want to fail on additional keys?
Not sure if you really need the DOMAIN

DOMAIN: vol.Schema({
vol.Optional(ATTR_DST): vol.Coerce(int),
})
}, extra=vol.REMOVE_EXTRA)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the Volume schema

def _new_device(device):
"""Called when new device is detected by HDMI network."""
discovery.load_platform(hass, "switch", DOMAIN,
discovered={ATTR_NEW: [device]},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything sent over the hass bus, like device must be serializable (i.e. a str/int/dict). It does not seem so from here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should I put device into hasss.data with my unique key and then retrieve it by this key which will be send over load_platform?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can make your DOMAIN part of the key, then you know it should be unique

hass.services.register(DOMAIN, SERVICE_VOLUME, _volume,
descriptions[SERVICE_VOLUME])
hass.services.register(DOMAIN, SERVICE_UPDATE_DEVICES, _update,
descriptions[SERVICE_UPDATE_DEVICES])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add the schemas you defined when registering these services

self._state = STATE_UNKNOWN
self._logical_address = logical
self.entity_id = "%s.%d" % (DOMAIN, self._logical_address)
device.set_update_callback(self._update)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also indicates the data you sent over the buss is not serializable.

Rather send non-serializable data in the hass.data dictionary.

@@ -4,7 +4,7 @@
# performs roughly what this test did in the past.

if [ "$1" = "--changed" ]; then
export files="`git diff upstream/dev --name-only | grep -v requirements_all.txt`"
export files="`git diff upstream/dev --name-only | grep -e '\.py$'`"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I like this, but do you mind doing it in a separate PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, in #5018

def _volume(call):
"""Increase/decrease volume and mute/unmute system."""
for cmd, att in call.data.items():
att = int(att)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you have the SCHEMAs validating your input, this type of coercing variables should not be needed

vranikp and others added 23 commits January 20, 2017 12:04
* 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
* code formatting
* code formatting
* import order
* 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
* services:
power_on
standby
active_source
* newer version of pyCEC
* getting device name from config
* correct call on shutdown
* 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
* 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)
* accept hexadecimal format of commands
* support for media player
* platform customization
* type constants
* accept hexadecimal format of commands
* support for media player
* platform customization
* accept hexadecimal format of commands
* support for media player
* platform customization
* preparing tcp support
* cleanup imports
* cleanup and enhance services description
* removed unwanted file
* pyCEC v0.4.6
* pined dependency version
* tighten service schemas
@konikvranik
Copy link
Contributor Author

@rytilahti: pycec dependency is present in version 0.4.6. Otherwise checks fail.

@balloob balloob merged commit 067e11e into home-assistant:dev Jan 20, 2017
@kellerza
Copy link
Member

@konikvranik Thanks for the PR, it took a while to get it in, but you added many features and worth it in the end! 🐬 !!

@michaelhenningersrb
Copy link

Hey could you tell me when this giant feature is available in a new release.... Great... thank you guys

@kellerza
Copy link
Member

It should be the coming weekend

@home-assistant home-assistant locked and limited conversation to collaborators Apr 30, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants