mjg59 / python-broadlink

Python module for controlling Broadlink RM2/3 (Pro) remote controls, A1 sensor platforms and SP2/3 smartplugs
MIT License
1.39k stars 479 forks source link

Add Support for S3 Hub 0xa59c #647

Closed brianyulke closed 2 years ago

brianyulke commented 2 years ago

Please add support for the S3 hub - 0xa59c. This hub accompanies the Smart Light Switch TC3 (in my case, the TC3-US-1).

dj-fiorex commented 2 years ago

I think that the most important thing here is not what I want, we are here to work for the community, this buttons are cheap and very versatile so in my opinion the best way to handle this devices is to have realtime notification when a button is clicked, so you can do almost anything when a button is clicked. What do you think?

stevendodd commented 2 years ago

Code in device.py should work for the listener almost out of the box. If you run

devices = broadlink.discover()

And while you are discovering press a button could you please print the response. Could you try doing it a couple of times using different buttons

stevendodd commented 2 years ago
def scan(
    timeout: int = DEFAULT_TIMEOUT,
    local_ip_address: str = None,
    discover_ip_address: str = DEFAULT_BCAST_ADDR,
    discover_ip_port: int = DEFAULT_PORT,
) -> t.Generator[HelloResponse, None, None]:
    """Broadcast a hello message and yield responses."""
    conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    conn.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

    if local_ip_address:
        conn.bind((local_ip_address, 0))
        port = conn.getsockname()[1]
    else:
        local_ip_address = "0.0.0.0"
        port = 0

    packet = bytearray(0x30)
    packet[0x08:0x14] = Datetime.pack(Datetime.now())
    packet[0x18:0x1C] = socket.inet_aton(local_ip_address)[::-1]
    packet[0x1C:0x1E] = port.to_bytes(2, "little")
    packet[0x26] = 6

    checksum = sum(packet, 0xBEAF) & 0xFFFF
    packet[0x20:0x22] = checksum.to_bytes(2, "little")

    start_time = time.time()
    discovered = []

    try:
        while (time.time() - start_time) < timeout:
            time_left = timeout - (time.time() - start_time)
            conn.settimeout(min(DEFAULT_RETRY_INTVL, time_left))
            conn.sendto(packet, (discover_ip_address, discover_ip_port))

            while True:
                try:
                    resp, host = conn.recvfrom(1024)
                except socket.timeout:
                    break

                devtype = resp[0x34] | resp[0x35] << 8
                mac = resp[0x3A:0x40][::-1]

                if (host, mac, devtype) in discovered:
                    continue
                discovered.append((host, mac, devtype))

                name = resp[0x40:].split(b"\x00")[0].decode()
                is_locked = bool(resp[0x7F])
                yield devtype, host, mac, name, is_locked
    finally:
        conn.close()

It will print that it's found the S3 hub but it's the payload that is interesting - In the case of the response you received above 34 32 35 37 33 which is decoded into the variable name above perhaps you could print that out because the code discards duplicate responses that finds based on host, device type and MAC address

dj-fiorex commented 2 years ago

Ok, i changed scan function and i tried your code:

import broadlink

buttonsDid = "00000000000000000000ec0bae371ee1"

def main():
    print("Hello World!")
    devices = broadlink.discover()
    print(devices)

if __name__ == "__main__":
    main()

this is what it print:

[broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False)]
stevendodd commented 2 years ago

Did you allow multiple entries based on name as well? Did you press a couple of buttons during discovery? Could you also please print packet.tohex() before it is sent

dj-fiorex commented 2 years ago

what you mean with "Did you allow multiple entries based on name as well?" Yes i clicked all buttons, and when i clicked the button 1 i received the notification on the phone.

this is packet.hex() 000000000000000001000000e607281316050b0200000000000000000000000006c00000000006000000000000000000 this is packet bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x07)\x13\x16\x05\x0b\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xc0\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00')

stevendodd commented 2 years ago
if (host, mac, devtype) in discovered:
                    continue
discovered.append((host, mac, devtype))

Will discard multiple entries even if the name is different.

I think we might need to take a step back. Could you try the packet capture again and see if there is a difference in the response when a different button is pressed

stevendodd commented 2 years ago

Look at the packet.hex and compare it to the request from your number one capture above, close but not quite the same..

We know how to send the packet to request a button press but we don't know how to fully populate the packet; notice how datetime and checksums are added to the packet and so that will be some of the differences but it doesn't account for all of it.

It would be great if you could copy and paste the hex request responses rather than using an image

dj-fiorex commented 2 years ago

oh sure, this is the number 1 image export file: https://we.tl/t-VQyDta74lu

stevendodd commented 2 years ago

Something went wrong, you can just copy and paste the hex strings using normal copy and paste,

dj-fiorex commented 2 years ago
if (host, mac, devtype) in discovered:
                    continue
discovered.append((host, mac, devtype))

Will discard multiple entries even if the name is different.

I think we might need to take a step back. Could you try the packet capture again and see if there is a difference in the response when a different button is pressed

Wait, wait, wait, i removed the code that skip if a device with the same name already exist and boom:

[broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False), broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False), broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False), broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False), broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False), broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False), broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False), broadlink.hub.s3(('192.168.1.74', 80), mac=b'\xec\x0b\xae8\xfc\x83', devtype=42573, timeout=10, name='42573', model='S3', manufacturer='Broadlink', is_locked=False)]

I know i clicked too many times lol

dj-fiorex commented 2 years ago
5a a5 aa 55 5a a5 aa 55  01 00 00 00 e6 07 28 16  Z..UZ..U ......(
14 04 0a 02 00 00 00 00  01 08 00 0a 3a a1 00 00  ....... ....:...
ef c4 00 00 00 00 06 00  00 00 00 00 00 00 00 00  ........ ........

5a a5 aa 55 5a a5 aa 55  01 00 00 00 e6 07 28 16  Z..UZ..U ......(
14 04 0a 02 00 00 00 00  01 08 00 0a 3a a1 00 00  ....... ....:...
05 cd 00 00 00 00 07 00  00 00 00 00 00 00 00 00  ........ ........
4d 59 0e 58 4d a6 4a 01  a8 c0 83 fc 38 ae 0b ec  MY.XM.J. ....8...
34 32 35 37 33 00 00 00  00 00 00 00 00 00 00 00  42573... ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ........ ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ........ ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 02 00  ........ ........

5a a5 aa 55 5a a5 aa 55  01 00 00 00 e6 07 29 16  Z..UZ..U ......)
14 04 0a 02 00 00 00 00  01 08 00 0a 3a a1 00 00  ....... ....:...
f0 c4 00 00 00 00 06 00  00 00 00 00 00 00 00 00  ........ ........

5a a5 aa 55 5a a5 aa 55  01 00 00 00 e6 07 29 16  Z..UZ..U ......)
14 04 0a 02 00 00 00 00  01 08 00 0a 3a a1 00 00  ....... ....:...
06 cd 00 00 00 00 07 00  00 00 00 00 00 00 00 00  ........ ........
4d 59 0e 58 4d a6 4a 01  a8 c0 83 fc 38 ae 0b ec  MY.XM.J. ....8...
34 32 35 37 33 00 00 00  00 00 00 00 00 00 00 00  42573... ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ........ ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ........ ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 02 00  ........ ........

5a a5 aa 55 5a a5 aa 55  01 00 00 00 e6 07 2a 16  Z..UZ..U ......*
14 04 0a 02 00 00 00 00  01 08 00 0a 3a a1 00 00  ....... ....:...
f1 c4 00 00 00 00 06 00  00 00 00 00 00 00 00 00  ........ ........

5a a5 aa 55 5a a5 aa 55  01 00 00 00 e6 07 2a 16  Z..UZ..U ......*
14 04 0a 02 00 00 00 00  01 08 00 0a 3a a1 00 00  ....... ....:...
07 cd 00 00 00 00 07 00  00 00 00 00 00 00 00 00  ........ ........
4d 59 0e 58 4d a6 4a 01  a8 c0 83 fc 38 ae 0b ec  MY.XM.J. ....8...
34 32 35 37 33 00 00 00  00 00 00 00 00 00 00 00  42573... ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ........ ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ........ ........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 02 00  ........ ........
dj-fiorex commented 2 years ago

No, I cheered for nothing, tried to run again the program, this time without click a button and the response is the same :( So is the same packet

stevendodd commented 2 years ago

Yes, can you go back to capturing packets and seeing if you can see a difference between one button press or another, you may need to go back to Wireshark although I think you should try the phone packet capture first

stevendodd commented 2 years ago
                 date-time                         local-ip         cs          6
0000000000000000 01000000e6072813 16050b0200000000 0000000000000000 06c0000000000600 0000000000000000
5aa5aa555aa5aa55 01000000e6072816 14040a0200000000 0108000a3aa10000 efc4000000000600 0000000000000000
packet = bytearray(0x30)
    packet[0x08:0x14] = Datetime.pack(Datetime.now())
    packet[0x18:0x1C] = socket.inet_aton(local_ip_address)[::-1]
    packet[0x1C:0x1E] = port.to_bytes(2, "little")
    packet[0x26] = 6

    checksum = sum(packet, 0xBEAF) & 0xFFFF
    packet[0x20:0x22] = checksum.to_bytes(2, "little")

Think you captured a discovery packet on the first attempt

stevendodd commented 2 years ago

Is the payload in the response: 0x3432353733 - is that cheeky Chinese character doing there

Screenshot 2022-02-11 at 20 54 32
dj-fiorex commented 2 years ago

sorry for the late reply, I am continuing to test reading with packet capture, but I am unable to bind a package to the click of a button. Very good job @stevendodd, so what does it mean? LOL

stevendodd commented 2 years ago

It's the name of the S3 hub you printed above in ASCII

dj-fiorex commented 2 years ago

I can't reproduce the same thing when a button is clicked, how can i export to another format?

stevendodd commented 2 years ago

If the button click is not going to the phone/app then the only way we'll be to route traffic from the button via somewhere where you can intercept it to the hub. You might try listening for udp on wireshark but I'm not sure it's going to be broadcast

dj-fiorex commented 2 years ago

The button uses a Broadlink BL-3358-P that is a simple wifi card, but i don't find any info online. With wireshark i can't intercept any message to and from the ip of the hub, i don't know why

stevendodd commented 2 years ago

The button uses a Broadlink BL-3358-P that is a simple wifi card, but i don't find any info online. With wireshark i can't intercept any message to and from the ip of the hub, i don't know why

How did you find out about the Wi-Fi card is the documentation somewhere - even just stating that it uses this chip?

The fact that the button uses the smart hub means that there is some communication between them, perhaps it's just setting up the Wi-Fi connection? Perhaps the button communicates directly to the Broadlink servers. I'm wondering if my smart light switches also have the same Wi-Fi card

stevendodd commented 2 years ago

https://usermanual.wiki/Hangzhou-BroadLink-Technology/BL3358-P supports WEP/WPA so certainly has the ability to communicate directly with the servers

stevendodd commented 2 years ago

If there is no internal traffic between the hub and the smart button then you may have to resort to https://ifttt.com

dj-fiorex commented 2 years ago

i just opened the SR3-4KEY and inside i saw this chip. the chip is a simple 2.4ghz wireless but i think that it send and receive commands from the hub via a channel different from simple wifi, so the hub is needed to comunicate to the server. I saw this user manual but i don't know how to change fw in the button

You smart light will turn on from the app even when there is not an active internet connection right?

stevendodd commented 2 years ago

I suspect the hub when you pair it it will set up the Wi-Fi and then do nothing else unless you can capture a packet, we are stuck

dj-fiorex commented 2 years ago

but if the button were connected directly to the home wifi I think I would see it in my router, among the connected devices

stevendodd commented 2 years ago

That's true; you need to intercept the packet between the hub and the button

dj-fiorex commented 2 years ago

i can confirm that the device is not connected directly with my wifi, i don't know how to intercept something that is not standard wifi LOL

You smart light will turn on from the app even when there is not an active internet connection right?

please reply

stevendodd commented 2 years ago

Yes, just needs Wi-Fi

dj-fiorex commented 2 years ago

i think we have to stick with a server that poll every x ms the hub and have a copy of the previous state of the buttons. then it can say if the last state was 0 and now is 1, the button was clicked. i don't like this type of solutions but...

dj-fiorex commented 2 years ago

i didn't want to give up so i restart again! So i spin up a router and connected only my phone and the hub, started wireshark to sniff the packet and i think that this time i was able to mark the message to the button click, so this is the pcap file from wireshark, in the comments of the packetyou will find something like "Starting button1" and "finish button1" this is to mark when i clicked the buttons https://we.tl/t-VQ9onb1xsX

stevendodd commented 2 years ago

Those downloads don't work they seem to be just zero byte files

dj-fiorex commented 2 years ago

Do you have wireshark installed? because i tried downloading the file and it works try this https://we.tl/t-2rutsoqyml

stevendodd commented 2 years ago

That works what is the IP address of the hub and button please

dj-fiorex commented 2 years ago

This is the ip address of the hub: 192.168.137.219 like i said before the button DID NOT connect to the standard WiFi protocol, directly to the router, so it doesn't have an ip address

just to be more clear it think that the button send a msg to the hub via a non standard 2.4ghz protocol, than the hub broadcast a msg to the local network

stevendodd commented 2 years ago

how did you mark the before/finish packets

Screenshot 2022-02-12 at 17 07 50
dj-fiorex commented 2 years ago

Before Button# means that this is the last capture BEFORE i clicked the button, so you see Before -> go to the next nsg and you will see the msg from when i clicked the button Unlike Before, Finish means that this is the last packet captured AFTER the button was clicked but before some time so this is the last msg in the ButtonClick msgs I hope I explained myself

stevendodd commented 2 years ago

How do you know did you put a comment on a live feed press the button and then comment the live feed after the press, was your phone 192.168.137.64

dj-fiorex commented 2 years ago

yes, my phone is .64. this file is the record of this actions:

stevendodd commented 2 years ago
Screenshot 2022-02-12 at 17 20 09
dj-fiorex commented 2 years ago

Please start with looking at packet 1 and 4. there is 3 packets for each button press

stevendodd commented 2 years ago

If I remove your phone from the traffic then there is only this UDP traffic in between where you have marked the button presses. The packets don't seem to conform to the protocol that is documented in this repository but I'm not 100% sure about that yet. I still don't understand how you can be sure that you marked the before/after correctly

dj-fiorex commented 2 years ago

i'm 99% sure because for some time no packets show up in this record, than i clicked the button and these 3 packet show up. i know this is not a scientific test

stevendodd commented 2 years ago

The packets seem to start with the Mac addresses of each of your devices can you please confirm Mac address of the button it should be in your app as the DID

dj-fiorex commented 2 years ago

this is the did of the button: 00000000000000000000ec0bae371ee1

stevendodd commented 2 years ago

rather than trying to decode it why don't you just try sending this packet to your hub via the Python libraries - It may have a timestamp inclued it so it might not work because of it:

ec0bae38fc8306ea56f0fcb70800450000b867f8400030117328037a2117c0a889db0714400200a478a6d27fdf0201009c00868f13a7392170aaf3d990fd30d313a7878f13a50555681886ac47fc653a48c372132d0e50e4c343429702b5592f01830f4ba79abf120492fdd8dc585fba6d5a7135e7d6df9c5132c297845d91cdbb4d6ec51601dbc6b291f048a9faa5009ada12cf98e468d3dc20a4faca36bf5a9b544487fad24aa88a1688a9d1a4454601914d9524994bd344f85253174b0fae64a7868c972f
dj-fiorex commented 2 years ago

sorry, i don't understand you, my goal is to be able to write a program that can listen to these messages and understand which button was pressed, what do i get by sending this message to the hub?

stevendodd commented 2 years ago

trying to simulate a button press

dj-fiorex commented 2 years ago

Ok so i created a new function in device.py that send a packet as argument to the function:

    def send_fake(self, packet: bytes):
        with self.lock and socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as conn:
            timeout = self.timeout
            start_time = time.time()

            while True:
                time_left = timeout - (time.time() - start_time)
                conn.settimeout(min(DEFAULT_RETRY_INTVL, time_left))
                conn.sendto(packet, self.host)

                try:
                    resp = conn.recvfrom(2048)[0]
                    break
                except socket.timeout as err:
                    if (time.time() - start_time) > timeout:
                        raise e.NetworkTimeoutError(
                            -4000,
                            "Network timeout",
                            f"No response received within {timeout}s",
                        ) from err

        if len(resp) < 0x30:
            raise e.DataValidationError(
                -4007,
                "Received data packet length error",
                f"Expected at least 48 bytes and received {len(resp)}",
            )

        nom_checksum = int.from_bytes(resp[0x20:0x22], "little")
        real_checksum = sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF

        if nom_checksum != real_checksum:
            raise e.DataValidationError(
                -4008,
                "Received data packet check error",
                f"Expected a checksum of {nom_checksum} and received {real_checksum}",
            )

        return resp

and them a script to execute:

import broadlink

buttonsDid = "00000000000000000000ec0bae371ee1"

def main():
    print("Hello World!")
    device = broadlink.hello('192.168.137.219')
    device.auth()
    subD = device.get_subdevices()
    print(device.send_fake(bytes.fromhex("ec0bae38fc8306ea56f0fcb70800450000b867f8400030117328037a2117c0a889db0714400200a478a6d27fdf0201009c00868f13a7392170aaf3d990fd30d313a7878f13a50555681886ac47fc653a48c372132d0e50e4c343429702b5592f01830f4ba79abf120492fdd8dc585fba6d5a7135e7d6df9c5132c297845d91cdbb4d6ec51601dbc6b291f048a9faa5009ada12cf98e468d3dc20a4faca36bf5a9b544487fad24aa88a1688a9d1a4454601914d9524994bd344f85253174b0fae64a7868c972f")))

if __name__ == "__main__":
    main()

this is the last print output:

b'\xec\x0b\xae8\xfc\x83\x06\xeaV\xf0\xfc\xb7\x08\x00E\x00\x00\xb8g\xf8@\x000\x11s(\x03z!\x17\xc0\xa8\x0c\xd5\xf9\xff@\x02\x84\xa7x\xa6\xd2\x7f\xdf\x02\x01\x00\x9c\x00\x86\x8f\x13\xa79!'