Closed brianyulke closed 2 years ago
+1 - I have the LC1 lights; is there anyway I can help I have basic python skills
+1 - I have the LC1 lights; is there anyway I can help I have basic python skills
Python 3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import broadlink
>>> device = broadlink.hello('192.168.1.99')
>>> device.auth()
True
>>> device.get_type()
'S3'
>>> device.get_state()
{'pwr': 0, 'indicator': 1, 'maxworktime': 0, 'childlock': 0}
Where get_state() is more or less a copy of what is in light.py; I'm at a loss now as there is no documentation from broadlink and I have no idea how to toggle sub devices or even list them.. This document gives me some idea, but nothing in terms of how to send the data to the hub https://docs-ibroadlink-com.translate.goog/public/appsdk_en/appsdk_05/?_x_tr_sl=zh-CN&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=sc
Here is the implementation hub.py - the sub device ids (did) can be found in the app listed as MAC addresses
>>> import broadlink
>>> device = broadlink.hello('192.168.1.99')
>>> device.auth()
True
>>> device.print_payload()
{'did': '00000000000000000000a043b0d0783a'}
{'pwr1': 0, 'pwr2': 0, 'plugmode': 0, 'hb_timeout': 180, 'lb_online1': 1}
>>> device.set_state("00000000000000000000a043b0d0783a",1)
{'pwr1': 1, 'pwr2': 0, 'plugmode': 0, 'hb_timeout': 180, 'lb_online1': 1}
>>> device.set_state("00000000000000000000a043b0d0783a",0,1)
{'pwr1': 0, 'pwr2': 1, 'plugmode': 0, 'hb_timeout': 180, 'lb_online1': 1}
>>> device.set_state("00000000000000000000a043b0d0783a",None,1)
{'pwr1': 0, 'pwr2': 1, 'plugmode': 0, 'hb_timeout': 180, 'lb_online1': 1}
"""Support for hubs."""
import struct
import enum
import json
from . import exceptions as e
from .device import Device
class s3(Device):
"""Controls a Broadlink S3."""
TYPE = "S3"
def print_payload(self) -> None:
hex_string = "80958b19c3cdbbf4e44a07285a1f64a860dda677568d5369134ebef0345244d55dd827fdf7227d6424c73159bc311d0a336eec48a57830d4ccec3ebd06c176a8"
payload = self.decrypt(bytearray.fromhex(hex_string))
js_len = struct.unpack_from("<I", payload, 0x08)[0]
print(json.loads(payload[0x0C : 0x0C + js_len]))
hex_string = "1dd713e6bf12fee9a7e98a29148b1f4ba569e3106ead7fb38214fa66584a3c4e98669e027ba94df43b8015719ee1b0feb8d83f7d14b28170357094e283024bc68eddfceca43e6376e5ad3a713ad04714"
payload = self.decrypt(bytearray.fromhex(hex_string))
js_len = struct.unpack_from("<I", payload, 0x08)[0]
print(json.loads(payload[0x0C : 0x0C + js_len]))
def set_state(
self,
did: str = None,
pwr1: bool = None,
pwr2: bool = None,
pwr3: bool = None,
) -> dict:
"""Set the power state of the device."""
state = {}
if did is not None:
state["did"] = did
if pwr1 is not None:
state["pwr1"] = int(bool(pwr1))
if pwr2 is not None:
state["pwr2"] = int(bool(pwr2))
if pwr3 is not None:
state["pwr1"] = int(bool(pwr3))
packet = self._encode(2, state)
response = self.send_packet(0x6A, packet)
e.check_error(response[0x22:0x24])
return self._decode(response)
def _encode(self, flag: int, state: dict) -> bytes:
"""Encode a JSON packet."""
# flag: 1 for reading, 2 for writing.
packet = bytearray(12)
data = json.dumps(state, separators=(",", ":")).encode()
struct.pack_into("<HHHBBI", packet, 0, 0xA5A5, 0x5A5A, 0, flag, 0x0B, len(data))
packet.extend(data)
checksum = sum(packet, 0xBEAF) & 0xFFFF
packet[0x04:0x06] = checksum.to_bytes(2, "little")
return packet
def _decode(self, response: bytes) -> dict:
"""Decode a JSON packet."""
payload = self.decrypt(response[0x38:])
js_len = struct.unpack_from("<I", payload, 0x08)[0]
state = json.loads(payload[0x0C : 0x0C + js_len])
return state
Hello guys, i tried the fork of stevendodd, i also added all the files needed to turn this to a Home assistant addons, and it work well. I can connect to my Broadlink S3 Hub and get the actual state of the connected button Broadlink SR3-4KEY.
device.get_state() return a json object with the latest state that the connected button sent last time a button was clicked; so for example if button2 was clicked then state = {...parameter, 'button1':0,'button2':1,'button3':0,'button4':0 }
until another button will be clicked. So my quesion is how can i listen to button click on the connected button? i'm a software engineer so i can write the code my self and help this library.
Can you please share the output from device.get_state(did)
Yes of course
{
"datatype": "fw_snapshot_v1",
"heartbeat": 0,
"lb_online1": 1,
"lowbattery": 0,
"scenarioswitch_1": 0,
"scenarioswitch_2": 1,
"scenarioswitch_3": 0,
"scenarioswitch_4": 0
}
this is a sample when the last button clicked was 2
Does it stay like this until another button is pressed? What did you actually get working in home assistant have you also got light switches or other sub devices?
And if you press button two again does it turn it off?
Oh yes sorry, so
i started from the example addon on the HA github repo and then integrate it with your docker file, so i can run the s3 rest api server directly inside the addon (i use HA OS)
Does that mean that if you press button to for a second time the scenario that you've hooked up in the broadlink app doesn't play for the second time?
There must be some way to reset the button. I could extend my code so that you can turn the buttons on and off from the python libraries and the rest API. Then your logic would be something like if button state equals 1 do something; then set button state equal to 0
Could you also post the output from when you initialised the hub on the first request to the rest API
The documentation from broadlink as always is pretty weak; I suspect you have not got any routines or triggers set up in the app and the button stays pressed. If you triggered a function in the app I believe the button will probably be reset to 0 after the function has triggered. Could you please test this theory for me.
I already tested this, in fact i added push notification on the app settings, so if i click button 1 i will receive a notification on the phone. But after the notification the json response is the same
this is the sr3-4key if you have't see one before https://ae01.alicdn.com/kf/Hac952d828193462fbbefbef7df11b83fB/Broadlink-SR3-4-Key-Smart-Button-Switch-Wireless-Works-With-Alexa-Google-Home-IFTTT-Need-S3.jpg_640x640.jpg
If you press button one twice in a row do you get a second notification?
What happens if you press button one walk away for 5-minutes and then press button one again do you get two notifications
Hello steven, so I made a video that show you that every time i click a button i will receive a notification on the phone, https://we.tl/t-jsGE0Wdjlr
Sorry to be a pain with all the questions, but once you click the 'I know it' on the notification can you check the json again to see if the button state is reset to 0. If it is not then the switch is sending a message to the hub rather than the hub polling the switch.. As I said above I can update the Python libraries to try and set the button state on or off however it may not trigger a routine in the broadlink app. Let's try it and see... I'll update the code in a bit and let you know
Could you also post the output from when you initialised the hub on the first request to the rest API
No, don't worry for the questions, we are trying to help each other, like i said before the button state not reset to 0. I used Wireshark to see the comunication of the hub, and every time i click the button, the S3 sends an udp message to the broadlink server i think.
this is the output of homeassistant.local:5000/?hub=192.168.1.74
[{'did': '00000000000000000000ec0bae371ee1', 'pid': '00000000000000000000000048650000', 'name': '', 'offline': 0}]
Your switch must send a response to the hub when the button is pushed, the app can then monitor some sort of state on the hub in order to trigger a routine.
Try this: https://hub.docker.com/r/stevendodd/s3-rest-switch-api you should be able to set the state of the buttons on your switch with a POST to: homeassistant.local:5000/00000000000000000000ec0bae371ee1/1 for example. The post body needs to contain - {"active": "true"} to turn the button on or {"active": "false"}' for off.
I would be interested to know if you are able to turn the button on/off and when you turn it on is the routine triggered
Just tested this, but not with your premade docker image, but with your repo, i made a POST request with raw body
{
"active":true
}
but i got a network timeout for no response
Traceback (most recent call last):
File "/usr/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
response = self.full_dispatch_request()
File "/usr/lib/python3.9/site-packages/flask/app.py", line 1518, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/lib/python3.9/site-packages/flask/app.py", line 1516, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/lib/python3.9/site-packages/flask/app.py", line 1502, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
File "/test/s3_rest.py", line 105, in dynamic
return handle_request(request, did, int(gang))
File "/test/s3_rest.py", line 38, in handle_request
return create_resp(device.set_state(did, pwr[1], pwr[2], pwr[3]), gang)
File "/test/broadlink/hub.py", line 66, in set_state
response = self.send_packet(0x6A, packet)
File "/test/broadlink/device.py", line 309, in send_packet
raise e.NetworkTimeoutError(
broadlink.exceptions.NetworkTimeoutError: [Errno -4000] Network timeout: No response received within 10s
I think that a device like the sr3-4key doesn't have the capability to turn on or off, i mean this is something that does not have sense on a button. we need to know how to intercept the call that the hub does to the broadlink backend
If you try with my Docker alternate image and then use a switch from home assistant to send the post data - let's see
The alternate image has new code to support your switch
def set_state(
self,
did: str = None,
pwr1: bool = None,
pwr2: bool = None,
pwr3: bool = None,
pwr4: bool = None,
) -> dict:
"""Set the power state of the device."""
state = {}
if did is not None:
state["did"] = did
if pwr1 is not None:
state["pwr1"] = int(bool(pwr1))
state["scenarioswitch_1"] = int(bool(pwr1))
if pwr2 is not None:
state["pwr2"] = int(bool(pwr2))
state["scenarioswitch_2"] = int(bool(pwr2))
if pwr3 is not None:
state["pwr3"] = int(bool(pwr3))
state["scenarioswitch_3"] = int(bool(pwr3))
if pwr4 is not None:
state["scenarioswitch_4"] = int(bool(pwr4))
packet = self._encode(2, state)
response = self.send_packet(0x6A, packet)
e.check_error(response[0x22:0x24])
return self._decode(response)
just tried it but nothing changed, same error:
File "/test/broadlink/device.py", line 309, in send_packet
raise e.NetworkTimeoutError(
broadlink.exceptions.NetworkTimeoutError: [Errno -4000] Network timeout: No response received within 10s
this is the json that i send (i put a print on your code):
{'did': '00000000000000000000ec0bae371ee1', 'pwr2': 0, 'scenarioswitch_2': 0}
Does the get state method it still work?
Now a GET request to homeassistant.local:5000/00000000000000000000ec0bae371ee1 give me this json response:
{ "status": 0 }
same for GET on homeassistant.local:5000/00000000000000000000ec0bae371ee1/1
i tried clicked a button and the response change back to the "original" one:
{
"datatype": "fw_snapshot_v1",
"heartbeat": 0,
"lb_online1": 1,
"lowbattery": 0,
"scenarioswitch_1": 1,
"scenarioswitch_2": 0,
"scenarioswitch_3": 0,
"scenarioswitch_4": 0
}
I want to emphasize that the sr3-4key is not a switch, it is a portable button, so it is no sense to set state on a stateless button, we need, in my opinion, focus on the hub that receive the signal from the button
Can you reproduce the same failure; the fact that it keeps state means it acts like a switch.
The only way to further is to listen to packets from the hub to/from your phone https://play.google.com/store/apps/details?id=app.greyshirts.sslcapture&hl=en_GB&gl=US
i checked with Wireshark and i can say that the hub and my phone didn't comunicate one to each other, the notification system go through the broadlink server. i tried with a router with no internet and when i click the button no notification is sent to my phone until i came back online and then all notifications comes
Can you reproduce the same failure; the fact that it keeps state means it acts like a switch.
no i disagree on this, i think that because of the power management the hub take a snapshot of the state of the button and respond with this, and the button work only when a button is clicked The fact here is to preserve battery life, so when a button is clicked -> send the state to the hub -> go to sleep
its not all UDP - this is how I figured out the LC1 wall light switches; I also thought the servers were involved with the local comms; a lot of the app state is on the servers; rather than relying on the notification routine is it possible to see just a button press in the app?
no i can't have a notification without setting an app notification
Well there's not much more I can do at the moment I'm sorry, I am fairly sure that your button does not update the hub and it is in fact your app that is polling the button via the hub somehow. The only way to figure it out will be to intercept network traffic as you have been doing and then decoding the packets as per the protocol specified in this repository. I have looked through the packet captures that allowed me to reverse engineer the LC one switch and all of the traffic between the phone and hub was via UDP; the only TCP traffic was between the application and the broadlink servers
If you look above those 256 B UDP responses was each of the sub devices getting queried from my application via the hub. It does it constantly in the background
just tried with your app, here it is!
This is the packets sniffed when i click the button!
Should be able to find the Mac addresses of the hub in reverse order in those hex dumps fairly easily
Please can you help me? I have never done anything like this I can find the mac address of the hub thanks to my router, i don't undestand what you mean
Yeah I'll give it a go tomorrow and see if I can decode the first request / response
In the broadlink app when you click on device info could you tell me what device has the Mac address EC b0 ae 38 FC 83 it will either be the hub or the smart button
Here is the info: HUB
SR3-4KEY
Now a GET request to homeassistant.local:5000/00000000000000000000ec0bae371ee1 give me this json response:
{ "status": 0 }
same for GET on homeassistant.local:5000/00000000000000000000ec0bae371ee1/1i tried clicked a button and the response change back to the "original" one:
{ "datatype": "fw_snapshot_v1", "heartbeat": 0, "lb_online1": 1, "lowbattery": 0, "scenarioswitch_1": 1, "scenarioswitch_2": 0, "scenarioswitch_3": 0, "scenarioswitch_4": 0 }
I want to emphasize that the sr3-4key is not a switch, it is a portable button, so it is no sense to set state on a stateless button, we need, in my opinion, focus on the hub that receive the signal from the button
It looks like just from the traffic pattern above your app is broadcasting without a specific address and then your hub is responding to the broadcast when the button is pressed. Looking at your quote above perhaps we also need to set the heartbeat parameter; but first I would love you to try:
set_state(did,0,1,0,0)
Ok so this is my test:
Starting from all disconnected
Put battery on the buttons
Connect the hub to the electrical socket
Connect to the hub -> http://homeassistant.local:5000/?hub=192.168.1.74 -> [{'did': '00000000000000000000ec0bae371ee1', 'pid': '00000000000000000000000048650000', 'name': '', 'offline': 0}]
Run the get state method -> http://homeassistant.local:5000/00000000000000000000ec0bae371ee1 -> {"status": 0}
Press button one
Run the get state method -> http://homeassistant.local:5000/00000000000000000000ec0bae371ee1 ->
{
"datatype": "fw_snapshot_v1",
"heartbeat": 0,
"lb_online1": 1,
"lowbattery": 0,
"scenarioswitch_1": 1,
"scenarioswitch_2": 0,
"scenarioswitch_3": 0,
"scenarioswitch_4": 0
}
turn on packet capture
run set state -> POST http://homeassistant.local:5000/00000000000000000000ec0bae371ee1/2 con {"active":"true"}
-> payload to the hub {'did': '00000000000000000000ec0bae371ee1', 'scenarioswitch_3': 1}
with no packet recorded and same python error:
Traceback (most recent call last):
File "/test/broadlink/device.py", line 305, in send_packet
resp = conn.recvfrom(2048)[0]
socket.timeout: timed out
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
response = self.full_dispatch_request()
File "/usr/lib/python3.9/site-packages/flask/app.py", line 1518, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/lib/python3.9/site-packages/flask/app.py", line 1516, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/lib/python3.9/site-packages/flask/app.py", line 1502, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
File "/test/s3_rest.py", line 107, in dynamic
return handle_request(request, did, int(gang))
File "/test/s3_rest.py", line 40, in handle_request
return create_resp(device.set_state(did, pwr[0], pwr[1], pwr[2], pwr[3]), gang)
File "/test/broadlink/hub.py", line 74, in set_state
response = self.send_packet(0x6A, packet)
File "/test/broadlink/device.py", line 309, in send_packet
raise e.NetworkTimeoutError(
broadlink.exceptions.NetworkTimeoutError: [Errno -4000] Network timeout: No response received within 10s
run get state -> {"status": 0}
Edit: Tried with a simple python script, no different, set_state give NetworkTimeoutError, no response received within 10s
import broadlink
buttonsDid = "00000000000000000000ec0bae371ee1"
def main():
device = broadlink.hello('192.168.1.74')
device.auth()
subD = device.get_subdevices()
print(subD)
state = device.get_state(buttonsDid)
print(state)
device.set_state(buttonsDid, 0, 1, 0, 0)
if __name__ == "__main__":
main()
Thank you for doing that, I will look at decoding those packets for you, of interest it is not the heartbeat either.. https://docs.ibroadlink.com/public/configuration-sdk+ctc/openproxy/
if you can link me a guide i will try too, 4 eyes is better than 2 👍
It's all in the protocol.md file in this repository
yes, i saw protocol.md, but what type of messages could that be? command type?
My working theory is that the hub send a broadcast request and the button responds like what you captured above. The hub then potentially updates the broadlink servers directly and executes any routines that have been configured.
Out of interest could you please let me know what your intention is in terms of using the library? Do you want to poll the button for a button press?
All of the code I have written so far is command type however as we have tested and seen above that doesn't work and so I think it will be a discovery/broadcast message with a response from the button potentially via the hub. The library could be extended with a listen function that periodically sends out the broadcast message and does something if it gets a response.
If you look in device.py it does a broadcast message when trying to discover devices on the network
Please add support for the S3 hub - 0xa59c. This hub accompanies the Smart Light Switch TC3 (in my case, the TC3-US-1).