Skip to content

WIP - convert zha remotes from binary sensors to events#14902

Closed
dmulcahey wants to merge 9 commits intohome-assistant:devfrom
dmulcahey:dm/zha-remote-events
Closed

WIP - convert zha remotes from binary sensors to events#14902
dmulcahey wants to merge 9 commits intohome-assistant:devfrom
dmulcahey:dm/zha-remote-events

Conversation

@dmulcahey
Copy link
Copy Markdown
Contributor

Description:

This PR removes zha binary_sensor entities and converts them to events based on feedback from this PR: #14795

EventOrigin.remote)
self._level = level
self._hass.bus.fire('zha.level_change', { 'device': self._id,
'level': self._level }, EventOrigin.remote)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

continuation line under-indented for visual indent
whitespace before '}'

self._hass.bus.fire('zha.on', { 'device': self._id },
EventOrigin.remote)
self._level = level
self._hass.bus.fire('zha.level_change', { 'device': self._id,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

whitespace after '{'

self._hass.bus.fire('zha.off', { 'device': self._id },
EventOrigin.remote)
elif level > 0 and self._level == 0:
self._hass.bus.fire('zha.on', { 'device': self._id },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

whitespace after '{'
whitespace before '}'

def set_level(self, level):
"""Set the level."""
if level == 0 and self._level > 0:
self._hass.bus.fire('zha.off', { 'device': self._id },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

whitespace after '{'
whitespace before '}'

def __init__(self, hass, cluster, discovery_info):
"""Initialize Switch."""
super().__init__(hass, cluster, discovery_info)
self._level = 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

indentation is not a multiple of four


def __init__(self, hass, cluster, discovery_info):
"""Initialize Switch."""
super().__init__(hass, cluster, discovery_info)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

indentation is not a multiple of four

"""Switch / remote event for zha"""

def __init__(self, hass, cluster, discovery_info):
"""Initialize Switch."""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

indentation is not a multiple of four

self._ieee = discovery_info['endpoint'].device.ieee
self._ieeetail = ''.join(['%02x' % (o, ) for o in self._ieee[-4:]])
if discovery_info['manufacturer'] and discovery_info['model'] is not \
None:
Copy link
Copy Markdown

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

await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
self._hass.data[DATA_ZHA_EVENT].append(ZHALevelEvent(self._hass,
cluster, discovery_info))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

continuation line under-indented for visual indent

await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
self._hass.data[DATA_ZHA_EVENT].append(ZHASwitchEvent(self._hass,
cluster, discovery_info))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

continuation line under-indented for visual indent

EventOrigin.remote)
self._level = level
self._hass.bus.fire('zha.level_change', {'device': self._id,
'level': self._level}, EventOrigin.remote)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

line too long (91 > 79 characters)

await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
self._hass.data[DATA_ZHA_EVENT].append(ZHALevelEvent(self._hass,
cluster, discovery_info))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

line too long (90 > 79 characters)

await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
self._hass.data[DATA_ZHA_EVENT].append(ZHASwitchEvent(self._hass,
cluster, discovery_info))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

line too long (91 > 79 characters)

self._ieeetail = ''.join(['%02x' % (o, ) for o in self._ieee[-4:]])
if discovery_info['manufacturer'] and discovery_info['model'] is not \
None:
self._id = "{}.{}_{}_{}_{}{}".format(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is the ID so complicated ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I kept it what it was before to try to minimize the impact of the breaking change. Zigbee devices consist of endpoints that expose functionality on clusters as attributes. device -> endpoints -> clusters -> attributes and commands. 1 physical device may expose several functions. so what has been done up to this point is:

domain.manufacturer_model_short_address_endpoint_id_cluster_id

here is an example of a single device that exposes motion and temperature sensors:
binary_sensor.centralite_3326l_0e35a368_1_1280
sensor.centralite_3326l_0e35a368_1_1026

does this make sense?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes.

)
else:
self._id = "{}.zha_{}_{}{}".format(
'zha',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So eh, now it will be zha.zha_ ? Anything that's not dynamic you can just put in the string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll take a look at this case. Pretty sure that is supposed to be domain and I messed something up there.

def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0040):
self._hass.bus.fire('zha.off', {'device': self._id},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please extract the event names as EVENT_XXX constants at the top

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will do

@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would love to see all things like 0x0000 and 0x0004 be extracted into constants with names like MOVE_TO_LEVEL etc. These constants should at least live at the top of the file, but preferably they are moved to ZHA.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed. I can try to tackle that this week in separate PR's to both repos.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you start with putting them in constants defined at the top of this file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will do

Copy link
Copy Markdown
Contributor

@Adminiuga Adminiuga Jun 11, 2018

Choose a reason for hiding this comment

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

something like?

cmd = zigpy.zcl.clusters.general.LevelControl.server_commands.get(command_id, ('unknown'))[0]
if cmd in ('move_to_level', 'move_to_level_with_on_off'):
     pass

EventOrigin.remote)
self._level = level
self._hass.bus.fire(LEVEL_CHANGE_EVENT_KEY, {DEVICE: self._id,
LEVEL: self._level}, EventOrigin.remote)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

continuation line under-indented for visual indent

self._cluster.add_listener(self)
self._ieee = discovery_info['endpoint'].device.ieee
self._ieeetail = ''.join(['%02x' % (o, ) for o in self._ieee[-4:]])
self._domain = domain;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

statement ends with a semicolon

await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
self._hass.data[DATA_ZHA_EVENT].append(ZHALevelEvent(self._hass,
cluster, discovery_info))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

continuation line under-indented for visual indent

"""Increment the level."""
self.set_level(min(255, max(0, self._level + change)))

def set_level(self, level):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is called from a method annotated with @callback. This means that it is run inside the event loop. Please also annotate this method with callback to indicate that. Preferably you prefix all methods likeasync_set_level, to indicate it's inside the event loop.

Copy link
Copy Markdown
Contributor Author

@dmulcahey dmulcahey Jun 10, 2018

Choose a reason for hiding this comment

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

cluster_command is a function called by zigpy. I may have added @callback incorrectly. Is there a way to determine if this is the case?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that zigpy runs inside the event loop, check the internals of the module or look at how the other code that interacts with zigpy does it.

def set_level(self, level):
"""Set the level."""
if level == 0 and self._level > 0:
self._hass.bus.fire(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Inside the event loop, you need to call bus.async_fire

self._cluster.add_listener(self)
self._ieee = discovery_info['endpoint'].device.ieee
self._ieeetail = ''.join(['%02x' % (o, ) for o in self._ieee[-4:]])
self._domain = domain
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This instance variable is not used.

self._hass = hass
self._cluster = cluster
self._cluster.add_listener(self)
self._ieee = discovery_info['endpoint'].device.ieee
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This instance variable is not used.

)
self._level = level
self._hass.bus.fire(
LEVEL_CHANGE_EVENT_KEY,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wouldn't something that has a level be a sensor ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think that's why this was originally added as a binary sensor. the binary sensor was trying to track on vs. off and level was an attribute.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure how to feel about "level". On one hand I'd expect event to be stateless, so no actual level value, just "level_up" or "level_down" events, but on the other hand it is nice to have everything wired together out of the box.

if args[0] == 0xff:
rate = 10 # Should read default move rate
self.move_level(-rate if args[0] else rate)
elif command_id == 0x0002: # step
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there's 'step' and 'step_with_on_off' == '0x06. We should handle both of those. Check #14756

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will do, ty

self._hass.bus.fire(
OFF_EVENT_KEY,
{DEVICE: self._id},
EventOrigin.remote
Copy link
Copy Markdown
Contributor

@Adminiuga Adminiuga Jun 11, 2018

Choose a reason for hiding this comment

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

What about either having an attribute indicating how many times the key was pressed in the last second or two (clicks_per_second), or maybe a separate event indicating double/triple clicks? eg for the 1st "off" command received in the last 2s time window we fire OFF_EVENT_KEY, for the 2nd command received in the last 2s windows, we fire OFF_DOUBLE_EVEN_KEY or anything like this?
I could get some fancy automation with events like this, like controlling group of lights with a single remote.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe in a subsequent PR. For now I just want to focus on the functionality that was already here.

cluster = out_clusters[LevelControl.cluster_id]
if discovery_info['new_join']:
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure having "min_report_interval" at such low value is a good idea. Two reasons:

  1. remotes are unlikely to send attribute reports anyway, as those usually just send "cluster commands"
  2. if for some reason it would send attribute reports (which I still have to see), you are risking to put too much traffic onto network. I've tried it with the lights and when dimming more then one light at the same time, those reports were overloading the network to the point it was causing Delivery Timeout exceptions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All of this came from the binary_sensor component of zha. It is all pre-existing logic. I’ll look into it as well though.

)
from zigpy.zcl.clusters.general import OnOff
if component == 'binary_sensor' and OnOff.cluster_id == \
cluster.cluster_id:
Copy link
Copy Markdown
Contributor

@Adminiuga Adminiuga Jun 11, 2018

Choose a reason for hiding this comment

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

Should we better add something like SINGLE_OUTPUT_CLUSTER_EVENT_CLASS to zha/const.py and handle those in a separate pass from handling SINGLE_INPUT/OUTPUT_DEVICE_CLASSes??? in current implementation, a "hypothetical" IAS device with an OnOff input cluster would cause the event handler to be instantiated, even if there's no OnOff output cluster, wouldn't it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, was speaking to @damarco about this as well. I am going to fix this. I ordered some Xiaomi sensors to test this case specifically because apparently they work this way. I have to add a sensor class back into binary_sensor to fix this as well.

@dmulcahey dmulcahey requested review from a team and syssi as code owners July 2, 2018 11:05
@dmulcahey dmulcahey requested a review from a team July 2, 2018 11:05
@dmulcahey dmulcahey requested a review from a team as a code owner July 2, 2018 11:05
@cdce8p cdce8p removed their request for review July 2, 2018 12:34
@balloob
Copy link
Copy Markdown
Member

balloob commented Sep 21, 2018

What's the status of this, merge conflicts and it is still marked as WIP

@dmulcahey
Copy link
Copy Markdown
Contributor Author

I’ll try to pick this back up this weekend

self._config,
)

async def _async_setup_remote(self, discovery_info):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we move this into a separate file, just to keep __init__.py less cluttered?

pass


class ZHAEvent(object):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same as above

@dmulcahey
Copy link
Copy Markdown
Contributor Author

closing this and starting fresh off of current dev. will reopen and reference this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants