Ezlo device state events?

Does the Ezlo API provide any sort of event notification when a device changes state?

So, instead of having to poll every “x” seconds to see if a device state has changed, the hub / API could automatically notify a client via an event (webhook, etc).

Thanks.

1 Like

Hello @robotman,
Ezlo controllers send a message to the servers when some events take place, including the change in the status of a device. You can visualize this on the Ezlo API tool under “Responses” → “Broadcast messages” (see image below) after logging in to your account and connecting to the controller.

image

Regards!

Thanks, I just tested via the web tool and I see the change in state.

But how would one go implementing a listener in code using the API? Do you have an example, pseudo code? documentation, etc.?

Thank you.

Hi @robotman

This is a plugin that runs on openLuup and includes what you are looking for GitHub - reneboer/EzloBridge: A bridge running on openLuup to make the Ezlo controled devices availble on openLuup with all its great plugins..

All is event based. that is, a commands is send and the event handlers handles the responses and/or broadcast messages.

Cheers Rene

Thanks Rene, but it appears that this functionality is inherent in the new Ezlos? I suspect it’s a Websocket event of some sort… but haven’t heard back from the dev team… @Oscar.Morales can you see my question re: this? Thanks.

OK, I figured this out… you can listen for event by creating a websocket that connects to the hub.

Here’s an example in Python. It opens a websocket and whenever device state changes, the hub will broadcast it and you can act on these events.

This is much better than polling the hub constantly

# Listen to Ezlo broadcast events.
uri = "ws://192.168.1.176:17000/"
async def wsrun(uri):
    async with websockets.connect(uri) as websocket:
        # Listen forever
        while True:
            print("Got: " + await websocket.recv())

asyncio.get_event_loop().run_until_complete(wsrun(uri))

In my example, if I turn a switch on or off, or open / close an open/close sensor, I get an event (JSON) that I can then parse.

Sample output:

A switch:

Got: {"id":"ui_broadcast","msg_id":"60d0cfd3077e90125777dc2c","msg_subclass":"hub.item.updated","result":{"_id":"60aaef71077e903f4a354448","deviceCategory":"switch","deviceId":"60aaef71077e903f4a354447","deviceName":"Plug-in Switch","deviceSubcategory":"interior_plugin","name":"switch","notifications":null,"roomName":"Office","serviceNotification":false,"syncNotification":false,"userNotification":true,"value":true,"valueFormatted":"true","valueType":"bool"}}

And, an open/close sensor:
Got: {"id":"ui_broadcast","msg_id":"60d0cfe4077e90125777dc2f","msg_subclass":"hub.item.updated","result":{"_id":"60c697bd077e9017345fa704","deviceArmed":false,"deviceCategory":"security_sensor","deviceId":"60c697bd077e9017345fa6fe","deviceName":"Door/Window Sensor","deviceSubcategory":"door","name":"security_threat","notifications":null,"roomName":"Office","serviceNotification":false,"syncNotification":false,"userNotification":false,"value":true,"valueFormatted":"true","valueType":"bool"}}

3 Likes

Hello @robotman,
I was not on duty yesterday but this is a brilliant solution. I’m really glad you could figure it out.
Regards!

I’m wondering if there’s a bug in the WebSockets server on the hub or I am doing something wrong?

If I use the simple client I mentioned above, and have logging turned on, I see ping/pong traffic as expected via the WebSockets protocol:

DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.PING: 9>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=<Opcode.PONG: 10>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - event = data_received(<2 bytes>)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.PING: 9>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=<Opcode.PONG: 10>, data=b'', rsv1=False, rsv2=False, rsv3=False)

BUT, after a few hours, the hub simply stops responding. No more broadcasts, no more ping/pong.

Here’s the code:

import asyncio
import websockets
import logging

async def hello():
    uri = "ws://192.168.1.176:17000/"
    async with websockets.connect(uri, ping_interval=None) as websocket:

        while (True):

            greeting = await websocket.recv()
            print ("Hub says: " + greeting)
            await asyncio.sleep(0.5)

# Turn on debugging to see the ping/pong frames.
logging.basicConfig(level=logging.DEBUG)

Any thoughts? Thanks.

@Oscar.Morales any thoughts on this? ?Thanks.

Hello @robotman,
I have been testing this code and it seems to be working fine for me. I think it performs better without the line that makes it wait 0.5 seconds. I’m curious to know why it was added.
I would recommend trying this approach in a host connected to the same IP segment as the controller. It would be good to check the controller is not losing power or changing its IP address during the process.
Also, make sure the insecure access is enabled on the controller with this call:

{
    "method": "hub.offline.insecure_access.enabled.set",
    "id": "_ID_",
    "params": {
        "enabled": true
    }
}

If you want to try a different approach, you may try using the WebSocket Request beta tool in Postman. This will also allow you to send calls and not only receive them.
image
Worst case scenario, it would be necessary to add a code that refreshes the communication every certain time or amount of responses.
Regards.

Thanks.

I added the offline.insecure_access option, but there is no difference.

How long did you run the code for?

Below are three tests I did - it lasts for about 20 minutes then no longer responds.

date 
Wed Jul  7 08:47:59 PDT 2021
/usr/local/bin/python3 ./hubClient2.py

DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 192.168.1.176:17000
DEBUG:urllib3.connectionpool:http://192.168.1.176:17000 "POST /v1/method/hub.offline.insecure_access.enabled.set HTTP/1.1" 400 None
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:websockets.protocol:client - state = CONNECTING
DEBUG:websockets.protocol:client - event = connection_made(<_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>)
DEBUG:websockets.server:client > GET / HTTP/1.1
DEBUG:websockets.server:client > Headers([('Host', '192.168.1.176:17000'), ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Key', 'fa8sTLRcdXB2A2/RzOb5jw=='), ('Sec-WebSocket-Version', '13'), ('Sec-WebSocket-Extensions', 'permessage-deflate; client_max_window_bits'), ('User-Agent', 'Python/3.9 websockets/9.1')])
DEBUG:websockets.protocol:client - event = data_received(<129 bytes>)
DEBUG:websockets.server:client < HTTP/1.1 101 Switching Protocols
DEBUG:websockets.server:client < Headers([('Upgrade', 'WebSocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Accept', '7d+r0/EqHLouRUtdetnUDZT0IQE=')])
DEBUG:websockets.protocol:client - state = OPEN
DEBUG:websockets.protocol:client - event = data_received(<2 bytes>)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.PING: 9>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
.
.
.
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.PING: 9>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=<Opcode.PONG: 10>, data=b'', rsv1=False, rsv2=False, rsv3=False)
**** FROZE HERE
date
Wed Jul  7 09:20:38 PDT 2021


Second Test
date
Wed Jul  7 09:21:10 PDT 2021
/usr/local/bin/python3 ./hubClient2.py
DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 192.168.1.176:17000
DEBUG:urllib3.connectionpool:http://192.168.1.176:17000 "POST /v1/method/hub.offline.insecure_access.enabled.set HTTP/1.1" 400 None
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:websockets.protocol:client - state = CONNECTING
DEBUG:websockets.protocol:client - event = connection_made(<_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>)
DEBUG:websockets.server:client > GET / HTTP/1.1
DEBUG:websockets.server:client > Headers([('Host', '192.168.1.176:17000'), ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Key', 'NDCSxM2v1BJfctf9gEgPoQ=='), ('Sec-WebSocket-Version', '13'), ('Sec-WebSocket-Extensions', 'permessage-deflate; client_max_window_bits'), ('User-Agent', 'Python/3.9 websockets/9.1')])
DEBUG:websockets.protocol:client - event = data_received(<129 bytes>)
DEBUG:websockets.server:client < HTTP/1.1 101 Switching Protocols
DEBUG:websockets.server:client < Headers([('Upgrade', 'WebSocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Accept', '4mO0uC9Ni62kRq0NYcbP05B9tXw=')])
DEBUG:websockets.protocol:client - state = OPEN
DEBUG:websockets.protocol:client - event = data_received(<2 bytes>)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.PING: 9>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=<Opcode.PONG: 10>, data=b'', rsv1=False, rsv2=False, rsv3=False)
.
.
.
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.PING: 9>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=<Opcode.PONG: 10>, data=b'', rsv1=False, rsv2=False, rsv3=False)
**** FROZE HERE
date
Wed Jul  7 09:40:17 PDT 2021



Third test

date
Wed Jul  7 09:41:07 PDT 2021
dave@Daves-MBP WS Server and Client8 % /usr/local/bin/python3 ./hubClient2.py
DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 192.168.1.176:17000
DEBUG:urllib3.connectionpool:http://192.168.1.176:17000 "POST /v1/method/hub.offline.insecure_access.enabled.set HTTP/1.1" 400 None
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:websockets.protocol:client - state = CONNECTING
DEBUG:websockets.protocol:client - event = connection_made(<_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>)
DEBUG:websockets.server:client > GET / HTTP/1.1
DEBUG:websockets.server:client > Headers([('Host', '192.168.1.176:17000'), ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Key', 'Kd93f9ocFWIyqHvZZxcPXA=='), ('Sec-WebSocket-Version', '13'), ('Sec-WebSocket-Extensions', 'permessage-deflate; client_max_window_bits'), ('User-Agent', 'Python/3.9 websockets/9.1')])
DEBUG:websockets.protocol:client - event = data_received(<129 bytes>)
DEBUG:websockets.server:client < HTTP/1.1 101 Switching Protocols
DEBUG:websockets.server:client < Headers([('Upgrade', 'WebSocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Accept', 'fgwDz19psM5QIGb+XUapWAuNt6E=')])
DEBUG:websockets.protocol:client - state = OPEN
DEBUG:websockets.protocol:client - event = data_received(<505 bytes>)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.TEXT: 1>, data=b'{"id":"ui_broadcast","msg_id":"60e5d928077e90401a651ba4","msg_subclass":"hub.item.updated","result":{"_id":"60c697bd077e9017345fa701","deviceArmed":false,"deviceCategory":"security_sensor","deviceId":"60c697bd077e9017345fa6fe","deviceName":"Door/Window Sensor","deviceSubcategory":"door","name":"dw_state","notifications":null,"roomName":"Office","serviceNotification":false,"syncNotification":false,"userNotification":false,"value":"dw_is_opened","valueFormatted":"dw_is_opened","valueType":"token"}}', rsv1=False, rsv2=False, rsv3=False)
Hub says: {"id":"ui_broadcast","msg_id":"60e5d928077e90401a651ba4","msg_subclass":"hub.item.updated","result":{"_id":"60c697bd077e9017345fa701","deviceArmed":false,"deviceCategory":"security_sensor","deviceId":"60c697bd077e9017345fa6fe","deviceName":"Door/Window Sensor","deviceSubcategory":"door","name":"dw_state","notifications":null,"roomName":"Office","serviceNotification":false,"syncNotification":false,"userNotification":false,"value":"dw_is_opened","valueFormatted":"dw_is_opened","valueType":"token"}}
DEBUG:websockets.protocol:client - event = data_received(<493 bytes>)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=<Opcode.PING: 9>, data=b'', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=<Opcode.PONG: 10>, data=b'', rsv1=False, rsv2=False, rsv3=False)
.
.
.
date
Wed Jul  7 10:15:39 PDT 2021

I should add that I have done all the following:
"I would recommend trying this approach in a host connected to the same IP segment as the controller. It would be good to check the controller is not losing power or changing its IP address during the process.
Also, make sure the insecure access is enabled on the controller with this call:

{
    "method": "hub.offline.insecure_access.enabled.set",
    "id": "_ID_",
    "params": {
        "enabled": true
    }
}

"

Hi,

I have code in Lua that talks to the Ezlo hub and that does not only listen, but also send data occasionally. I do a device status refresh once every few hours in case an event gets missed or something. It seems that you must also send to the hub or it will close the listen only connection as you experience.

Cheers Rene

I have an integration running as well, and haven’t observed any spontaneous disconnects on 2.0.14.1570.3. I’ve left the unit undisturbed (no development work) overnight, about 18 hours so far, and no restarts/disconnects/dead channel. My integration also sent no requests/commands during that time, so my results are a little different from @reneboer (not sending anything doesn’t seem to disconnect me). The only activity on the channel during that time was pings about every 20s from the hub (for which pongs were sent).

My connection is currently running in anonymous mode (but still secure/wss), but I’ll test auth/token-controlled access during the overnight quiet period later tonight. I need to test what happens at token expiration anyway.

1 Like

Thanks Rene, that’s exactly what the ping/pong action (RFC 6455) is supposed to take care of. It maintains a connection between a WebSocket client and server by sending ping / pong frames back and forth. In testing, the server does respond with pongs to my pings:

DEBUG:websockets.protocol:client - received ping, sending pong: [empty]
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=<Opcode.PONG: 10>, data=b'', rsv1=False, rsv2=False, rsv3=False)

However, in my tests, the connection is closed about every 20 minutes…

Thanks - any chance you’re interested in trying the same Python code so see if you get different results?

import asyncio
import websockets
import logging

async def hello():
    uri = "ws://192.168.1.176:17000/"
    async with websockets.connect(uri, ping_interval=None) as websocket:

        while (True):

            greeting = await websocket.recv()
            print ("Hub says: " + greeting)

# Turn on debugging to see the ping/pong frames.
logging.basicConfig(level=logging.DEBUG)

Hi robotman,

Your code does nothing but listen. What if you first send a command to the hub and then start to listen. The ws API is mainly written for the Vera Apps and they send all kinds of requests. I would try that and see what happens.

Cheers Rene

Thanks, actually it does more than listen. It sends ping events and listens for ping events (note the ping_interval) and the debug output - normally for WebSockets, that should be enough. But, to your point, that might not be enough. I’ll try making hub requests periodically and see what happens.

Good idea!

I’ll report back shortly…

As promised, reporting back… @reneboer it looks like you were right! Thank you. I re-wrote the sample code and had it do a hub.items.list periodically. It kept the session alive. Previously it ran for about 20 minutes, it’s been running for about 3 hours now.

I wrongly assumed that a ping/pong would keep the session open (which I thought was the point to the ping/pong), but in digging deeper, I ran across this:

"NOTE: A Ping frame may serve either as a keepalive or as a means to verify that the remote endpoint is still responsive.

Only messages sent from a user keep the session alive. This is because only messages coming from a user imply user activity. Received messages do not imply activity and, thus, do not renew the session expiration."

See: how to keep your websocket session alive | Shanhe Yi

I learned something today. Thank you Rene. I hope this helps others too. (You now have a simple means to listen to broadcast events from the hub.)

1 Like

Interesting, because I keep sessions open for hours without sending anything.

That is. (Is your code Javascript by chance?)