nathanvdh / airtouch2-python

Python client for the Polyaire AirTouch 2 airconditioning system
MIT License
7 stars 6 forks source link

Support airtouch2+ #6

Closed nathanvdh closed 1 year ago

nathanvdh commented 1 year ago

Experimental branch pushed, discuss testing and issues below

Chris112 commented 1 year ago

Hey, keen to get this to work if I can help, here's the steps I've worked through today but will dig further later. Also new to python from javascript so not sure if i'm doing basic things wrong.

I had connection issues at the start and couldn't ping the static IP assigned in the DHCP server (but the airtouch app would still work which is strange) and disconnecting and reconnecting the controller to the wifi seems to have fixed it.

nmap confirms the host is online and port is correct: telnet nmap -p 9200 192.168.20.9

Starting Nmap 7.80 ( https://nmap.org ) at 2023-03-23 15:54 AWST
Nmap scan report for 192.168.20.9
Host is up (0.0050s latency).

PORT     STATE SERVICE
9200/tcp open  wap-wsp

Nmap done: 1 IP address (1 host up) scanned in 0.10 second

At this stage I upgraded python to the latest version (3.11.1) with pyenv and tried to run it but hit a few errors

  1. ModuleNotFoundError: No module named 'airtouch2' Fixed this by adding the following to the top test_programs/at2plus_test.py

    # import sys
    # sys.path.append('../src')
  2. AttributeError: 'AcStatusLogger' object has no attribute 'cleanup_callbacks' Fixed this by initializing acs and cleanup_callbacks to empty lists in the AcStatusLogger class

    acs: list[At2PlusAircon] = []
    cleanup_callbacks: list[Callable] = []
  3. AttributeError: 'At2PlusClient' object has no attribute '_new_ac_callbacks'. Did you mean: 'add_new_ac_callback'? Same deal in the AT2PlusClient class and the _new_ac_callbacks list

    self._new_ac_callbacks: list[Callable] = []

Now running python ./at2plus_test.py outputs the following after I added a print for the bytes read in _read_header of At2PlusClient class as hex):

Connecting to 192.168.20.9 on port 9200
Starting listener task
Sending AcStatusMessage
Enter 'q' to quit: 
>> header_bytes as hex: 55 55 b0 80 01 c0 00 12
Failed reading header, trying again
>> header_bytes as hex: 23 00 00 00 00 0a 00 01
Failed reading header, trying again
>> header_bytes as hex: 10 43 96 c1 03 20 00 00
Failed reading header, trying again

Then I used the Polyaire 2+ communication doc to convert the bytes to information to validate its the correct version and matched what I saw in the app, which it does.

Raw response from AC status request
55 55 b0 80 01 c0 00 12
23 00 00 00 00 0a 00 01

10 43 96 c1 03 2a 00 00

00010000 = AC is on and AC number is 0
01000011 = Mode is set to cool and fan speed is set to medium
10010110 = convert byte to decimal and (VALUE + 100) / 10 is the current temperature. In this case its (150 + 100) / 10 = 25
11000001 = turbo inactive, bypass inactive, spill inactive and timer is set(?)

00000011 = skip
00101010 = combine 2 bytes and convert to decimal to get 810 then current temperature = (810 – 500)/10 = 31

00000000 = unused
00000000 = unused

So it looks like AC status request is being made correctly but breaks down parsing the response, will try again on the weekend.

nathanvdh commented 1 year ago

@Chris112 To install the library you need to run pip install -e . from the project root (where pyproject.toml is located), my bad - should have put that in my post. The -e is for 'editable' which means the installation is just a symlink to the directory so any changes you make are reflected in the installation. With that done, you should be able to remove your manual addition to the PATH.

>> header_bytes as hex: 55 55 b0 80 01 c0 00 12 Failed reading header, trying again

I was parsing the address bytes in the header incorrectly. The spec states:

b. Address Address should 0x80 0xb0 or 0x90 0xb0 (for Extended message) when sending to AirTouch. When receiving from AirTouch, last byte of address will be 0x80 or 0x90 (for Extended message).

I interpreted this as 0x80 0xb0 for send and 0x80 0x80 for receive, when they're actually just swapped. The examples in the spec document do actually show this, I've now fixed it (along with the member initialisation) - have another go.

Chris112 commented 1 year ago

Awesome, working much better now, I was looking for a requirements.txt πŸ˜…

The next issue:

Connecting to 192.168.20.9 on port 9200
Starting listener task
Sending AcStatusMessage
Enter 'q' to quit: 
>> ['0x55', '0x55', '0xb0', '0x80', '0x1', '0xc0', '0x0', '0x12']
Checksum mismatch, ignoring message: Got 2e:9e but expected f0:72
Reading message failed
>> ['0x55', '0x55', '0x9f', '0x80', '0xd5', '0xc0', '0x0', '0x20']
ValueError: Unexpected address byte: expected 0xb0, got 0x9f
Failed reading header, trying again
>> ['0x2b', '0x0', '0x0', '0x0', '0x0', '0x4', '0x0', '0x6']
ValueError: Message header magic is invalid
Failed reading header, trying again
>> ['0x80', '0x80', '0x7', '0xff', '0x81', '0x81', '0x7', '0xff']
ValueError: Message header magic is invalid
Failed reading header, trying again
>> ['0x82', '0x82', '0x7', '0xff', '0x83', '0x83', '0x7', '0xff']
ValueError: Message header magic is invalid
Failed reading header, trying again
>> ['0x90', '0xff', '0x3', '0x3', '0x91', '0xff', '0x7', '0xff']
ValueError: Message header magic is invalid
Failed reading header, trying again

Logs:

2023-03-23 19:51:30,488 MainThread DEBUG: Connecting to 192.168.20.9 on port 9200
2023-03-23 19:51:30,580 MainThread DEBUG: Starting listener task
2023-03-23 19:51:30,581 MainThread DEBUG: Sending AcStatusMessage
2023-03-23 19:51:30,687 MainThread WARNING: Checksum mismatch, ignoring message: Got 2e:9e but expected f0:72
2023-03-23 19:51:30,687 MainThread WARNING: Reading message failed
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Unexpected address byte: expected 0xb0, got 0x9f
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
nathanvdh commented 1 year ago

Awesome, working much better now, I was looking for a requirements.txt πŸ˜…

The next issue:

Connecting to 192.168.20.9 on port 9200
Starting listener task
Sending AcStatusMessage
Enter 'q' to quit: 
>> ['0x55', '0x55', '0xb0', '0x80', '0x1', '0xc0', '0x0', '0x12']
Checksum mismatch, ignoring message: Got 2e:9e but expected f0:72
Reading message failed
>> ['0x55', '0x55', '0x9f', '0x80', '0xd5', '0xc0', '0x0', '0x20']
ValueError: Unexpected address byte: expected 0xb0, got 0x9f
Failed reading header, trying again
>> ['0x2b', '0x0', '0x0', '0x0', '0x0', '0x4', '0x0', '0x6']
ValueError: Message header magic is invalid
Failed reading header, trying again
>> ['0x80', '0x80', '0x7', '0xff', '0x81', '0x81', '0x7', '0xff']
ValueError: Message header magic is invalid
Failed reading header, trying again
>> ['0x82', '0x82', '0x7', '0xff', '0x83', '0x83', '0x7', '0xff']
ValueError: Message header magic is invalid
Failed reading header, trying again
>> ['0x90', '0xff', '0x3', '0x3', '0x91', '0xff', '0x7', '0xff']
ValueError: Message header magic is invalid
Failed reading header, trying again

Logs:

2023-03-23 19:51:30,488 MainThread DEBUG: Connecting to 192.168.20.9 on port 9200
2023-03-23 19:51:30,580 MainThread DEBUG: Starting listener task
2023-03-23 19:51:30,581 MainThread DEBUG: Sending AcStatusMessage
2023-03-23 19:51:30,687 MainThread WARNING: Checksum mismatch, ignoring message: Got 2e:9e but expected f0:72
2023-03-23 19:51:30,687 MainThread WARNING: Reading message failed
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Unexpected address byte: expected 0xb0, got 0x9f
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again
2023-03-23 19:52:03,604 MainThread DEBUG: ValueError: Message header magic is invalid
2023-03-23 19:52:03,604 MainThread DEBUG: Failed reading header, trying again

Hmm looks like the first message the checksum wasn't as expected. Could have been a genuine mismatch? Or my checksum algorithm could be wrong. Can you try changing setpoint a bunch with the app or control pad while the python script is running and see if they all give a checksum mismatch?

The next one is interesting, seems like a message that is different from the spec. The header is very unexpected.

Chris112 commented 1 year ago

It seems that all the checksum validation is consistently failing but the algorithm doesn't look wrong from a quick inspection.

edit: removed irrelevant logs

Chris112 commented 1 year ago

Looks like the checksum is calculated from the whole message minus the header bytes instead of just the message data.

Fixed a couple bugs and now the AC status is returned with ID 0 (I only have 1 AC unit)

  id: 0
  power: 1
  mode: 4
  fan_speed: 4
  set_point: 25.0
  temperature: 25.0
  turbo: False
  bypass: False
  spill: False
  timer: True
  error: 0

and now the request to get the AC ability is being sent but the at2plus_test script hangs on ac_ability = await self._ability_message_queue.get() as it looks like the queue is always empty and is probably a bug I can't brute force with print statements.

Right before it waits forever, I can read the ability payload directly (confirmed by the fixed 0xFF, 0x11 bytes) which indicates that there is at least a response from the controller with the ability of AC0

<< Reading payload of size 8: 55:55:b0:90:01:1f:00:1c
<< Reading payload of size 28: ff:11:00:18:46:75:6a:69:74:73:75:00:00:00:00:00:00:00:00:00:00:08:1f:1d:12:12:10:10
<< Reading payload of size 2: 0e:61

Thanks for all your work πŸ™

nathanvdh commented 1 year ago

Hey man, pushed a few fixes - have another go.

Chris112 commented 1 year ago

Hey, got a little further but hitting a strange error.

In the example doc, the header of the example AC ability message responds with 26 bytes but my AC is responding with 28 bytes in the header and the checksum is passing.

The AC ability response data looks fine except for the maximum set point is being reported as 18 instead of 30. I changed the number of zones and the min/max temps and everything reported the new values correctly except for the max temp.

The additional 2 bytes on the end with no mapping is 0x10 and 0x10 which is unexplained in the doc πŸ˜₯

Looking at [another implementation] (https://github.com/HendX/AirTouch2Plus/blob/main/Sources/AirTouch2Plus/Messages/UnitAbilitiesResponseMessage.swift#LL200C33-L200C33) written in Swift, the repeating length has also been hardcoded to 24 bytes making me think my AC unit is dodgy or nobody else is running the latest firmware.

## Example message
header (8 bytes)
0x55 0x55 0xb0 0x90 0x01 0x1f 0x00 0x1a 

data (26 bytes)
0xff 0x11 0x00 0x16 0x55 0x4e 0x49 0x54 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x04 0x17 0x1d 0x11 0x1f

checksum (2 bytes)
0x6a 0x54

## Actual message
header (8 bytes)
0x55 0x55 0xb0 0x90 0x01 0x1f 0x00 0x1c

data (28 bytes)
0xff 0x11 0x00 0x18 0x46 0x75 0x6a 0x69 0x74 0x73 0x75 0x00 0x00 0x00 0x00 0x00 
0x00 0x00 0x00 0x00 0x00 0x07 0x1f 0x1d 0x13 0x13 0x10 0x10

checksum (2 bytes)
0x32 0xce

## Translated response
number: 0
name: Fujitsu
start_group: 0
group_count: 7
supported_modes: [<AcMode.AUTO: 0>, <AcMode.HEAT: 1>, <AcMode.DRY: 2>, <AcMode.FAN: 3>, <AcMode.COOL: 4>]
supported_fan_speeds: [<AcFanSpeed.AUTO: 0>, <AcFanSpeed.LOW: 2>, <AcFanSpeed.MEDIUM: 3>, <AcFanSpeed.HIGH: 4>]
min_setpoint: 19
max_setpoint: 19
nathanvdh commented 1 year ago

The swift guy is also just doing what I am - implementing what's in that document. He doesn't have an AT2+ unit either. So I suppose you just have a newer version and it's different from the doc. You can see your 4th data byte is 0x18 = 24 bytes of following data, whereas the spec states 22 at this moment, implying it could change - and lo and behold it has changed.

By the way, thanks for continually fixing the runtime mistakes - the downside of interpreted languages is you don't know if a line is really valid until you actually try to interpret it at runtime and of course I can't get past the client.connect() line without a unit to connect to haha.

Could you please investigate the last 4 bytes of the 28 data bytes - adjust your minimum and maximum set points and see if/how those last 4 data bytes change and what their significance is. If changing the maximum changes other bytes, let me know. It's kind of up to you now to reverse engineer what the values mean - because the doc is outdated and I can't do it without a unit. Whenever _read_message() reads a complete message it should dump the bytes to a .dump file, if you want me to help then you can make adjustments to min/max set temp, re-request the AcAbility then post here what you changed, in order, and the subsequent AcAbilityMessage .dump files. I'll make a commit now that allows you to manually re-request the AcAbilityMessage.

Edit: Okay pushed the commit.

Also - I thought the AcAbility stuff was going to be unchanging, i.e. they are fixed properties of the AC. I think my assumption there is wrong now - am I correct in thinking that 'AC Name', 'Start group count', 'Group count', and min/max setpoint can all be adjusted in the control panel? I still assume that the supported modes and fan speeds can't be adjusted, right?

Chris112 commented 1 year ago

There's a few command and status messages firing with undocumented sub message types which look next to impossible to figure out which bits represent what feature/status, I've emailed the generic Polyaire support email and hopefully they can bounce it around to someone with knowledge.

I think the AC ability state is imported initially when it's first connected to one of the supported Polyaire gateways (mine being a Fujitsu for example) representing the physical hardware and should be confirmed by the installer but nothing stops the home owner from modifying it with the default installer password which was just "Polyaire" so it's safe to assume the values can be modified.

nathanvdh commented 1 year ago

Fair enough, I was going to ignore any other messages for now. Will you have a go at working out the AcAbility one though? It's only a couple of extra bytes.

Chris112 commented 1 year ago

Yeah sure, would love to get the basic working still.

The current AC0 ability response:

Read payload of size 8: 55:55:b0:90:01:1f:00:1c
Read payload of size 28: ff:11:00:18:46:75:6a:69:74:73:75:00:00:00:00:00:00:00:00:00:00:07:1f:1d:14:14:1b:1b
Read payload of size 2: 70:38

Under the installer settings I can change the min and max setpoint values for cool and heat and it appears to update in the ability response but the max is now excluded for some reason.

The console inputs are currently set to the following to see if the setpoint range did anything but the response was the same. Min cool (0x14): 20 Max cool(0x1c): 28 Min heat (0x1b): 27 Max heat (0x1b): 27

Of the final 4 bytes, the first 2 always represent the cool min setpoint value set and the last 2 always represent the heat min setpoint value set.

If I had to have a guess after pressing all the buttons, the final 4 bytes are something like

cool_min_setpoint: int
all_cool_min_setpoint: int
heat_min_setpoint: int
all_cool_min_setpoint: int

In the context of a single AC I don't really understand why it would display the min and max of the whole AC unit (max 4 ACs per controller) but it's the only thing I can think of outside of it being a bug.

Let me know if you've got any ideas πŸ‘ŒπŸ‘ŒπŸ‘Œ

Also unrelated, I think the controller is also pushing the data to the cloud, have you ever thought about if it's possible to just pull the data from there?

also unrelated 2: I have this error printed when I change a value on the controller with the script running but struggle to trace through the callbacks

Task exception was never retrieved
future: <Task finished name='Task-4' coro=<At2PlusClient._handle_status_message() done, defined at /home/chris112/projects/airtouch2-python/src/airtouch2/at2plus/AT2PlusClient.py:99> exception=TypeError("log_ac_info() missing 1 required positional argument: 'ac'")>
Traceback (most recent call last):
  File "/home/chris112/projects/airtouch2-python/src/airtouch2/at2plus/AT2PlusClient.py", line 104, in _handle_status_message
    self.aircons_by_id[status.id]._update_status(status)
  File "/home/chris112/projects/airtouch2-python/src/airtouch2/at2plus/AT2PlusAircon.py", line 70, in _update_status
    callback()
TypeError: log_ac_info() missing 1 required positional argument: 'ac'
nathanvdh commented 1 year ago

On unrelated 2: Yeah my bad, the callbacks don't take arguments, although perhaps I should pass in the ID of the AC that was updated. Anyway, just pushed a quick workaround.

Weird that there's those 4 related values on the panel but they don't quite correspond with what we're seeing in the message. Just to confirm - each of the pairs of bytes at the end always change together? They're never different? Seems odd to have the same value twice - like you say perhaps its only important when there's more connected AC units. What's more alarming is that the max values don't seem be there at all....not helpful. The spec definitely says the last two bytes are minimum and maximum - it seems weird they would remove the maximum from the message in this later version.

Alas, if that information has been removed from this AcAbility message we're looking at, I would guess it's now available in a different message? So we're again getting back to either needing a newer version of this spec document, or doing some serious reverse engineering work. E.g. Set up a packet capture of the airtouch2+ app and play around with changing different things on both the panel and app and working out what messages are sent and their content.

Anyway, I can just ignore the max temp thing for now. I'll update the code so the length of the AcAbility isn't hardcoded and then I'll start working on commands and getting you test that.

On unrelated 1: I would not like to do that, at the very least it would be slower and require a login. I much prefer this low-level, local network interaction.

Selmaks commented 1 year ago

Hi Here is a newer version of the API that someone was kind enough to send me, Hopefully it helps. AirTouch 2+ Communication Protocol V1.1.pdf

nathanvdh commented 1 year ago

Yeah so this doc says the last 4 bytes of the Ability message are: cool_min cool_max heat_min heat_max

which makes sense - perhaps you were misreading something Chris? I'm going to implement handling what's in both of these specs now.

It also seems to be the only change in that newer doc

Chris112 commented 1 year ago

Hi Here is a newer version of the API that someone was kind enough to send me, Hopefully it helps. AirTouch 2+ Communication Protocol V1.1.pdf

Fantastic, thanks for sharing.

which makes sense - perhaps you were misreading something Chris? I'm going to implement handling what's in both of these specs now.

I expect it must be a fault in my device, wireshark reporting the same values.

Keen to test further when you've implemented the spec change + group/ac set messages, please let me know if you need any help

nathanvdh commented 1 year ago

Hey @Chris112 I just pushed a commit that attempts to support the 1.1 AcAbility message. See what happens :)

nathanvdh commented 1 year ago

Also just pushed a commit that allows you to control the AC - setpoint, mode, fan speed, toggle on/off.

Untested and likely broken/buggy as my commits in the past have also been...sorry

bigjezza commented 1 year ago

Hey I'm so glad that you're giving this a shot!

I've tried the latest commit c5663b4:

Connecting to 192.168.178.62 on port 9200
Starting listener task
Sending AcStatusMessage with data: 55:55:80:b0:01:c0:00:08:23:00:00:00:00:00:00:0a:7a:30
Waiting for AC0 to be ready
Traceback (most recent call last):
  File "C:\at2\airtouch2-python-at2-plus-support\test_programs\at2plus_test.py", line 135, in <module>
    asyncio.run(main())
  File "C:\Users\jerem\AppData\Local\Programs\Python\Python310\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "C:\Users\jerem\AppData\Local\Programs\Python\Python310\lib\asyncio\base_events.py", line 649, in run_until_complete
    return future.result()
  File "C:\at2\airtouch2-python-at2-plus-support\test_programs\at2plus_test.py", line 101, in main
    await ac0_waiter.wait()
  File "C:\at2\airtouch2-python-at2-plus-support\test_programs\at2plus_test.py", line 70, in wait
    await self.found_ac0.wait()
AttributeError: 'Ac0Waiter' object has no attribute 'found_ac0'

commit 6a8204d:

Connecting to 192.168.178.62 on port 9200
Starting listener task
Sending AcStatusMessage with data: 55:55:80:b0:01:c0:00:08:23:00:00:00:00:00:00:0a:7a:30

Enter: 'q' to quit
       'r' to request AC0 ability
Read payload of size 8: 55:55:b0:80:01:c0:00:12
Read payload of size 18: 23:00:00:00:00:0a:00:01:00:14:78:c9:02:c0:00:00:90:00
Read payload of size 2: 3f:b7
Handling status message
New AC (0) found

            id: 0
            power: 0
            mode: 1
            fan_speed: 4
            set_point: 22.0
            temperature: 20.4
            turbo: True
            bypass: False
            spill: False
            timer: True
            error: 0

Requesting ability of AC0
Sending RequestAcAbilityMessage with data: 55:55:90:b0:01:1f:00:03:ff:11:00:09:83
Waiting for ability message response...
Read payload of size 8: 55:55:b0:90:01:1f:00:1c
Read payload of size 28: ff:11:00:18:4d:48:49:00:00:00:00:00:00:00:00:00:00:00:00:00:00:06:1f:1e:12:12:12:12
Read payload of size 2: b0:89
Creating ability message from 26 bytes
Got ability message response
Task exception was never retrieved
future: <Task finished name='Task-5' coro=<At2PlusClient._handle_status_message() done, defined at C:\at2\airtouch2-python-at2-plus-support\src\airtouch2\at2plus\AT2PlusClient.py:99> exception=AttributeError("'AcAbility' object has no attribute 'number'")>
Traceback (most recent call last):
  File "C:\at2\airtouch2-python-at2-plus-support\src\airtouch2\at2plus\AT2PlusClient.py", line 110, in _handle_status_message
    ability = await self._request_ac_ability(status.id)
  File "C:\at2\airtouch2-python-at2-plus-support\src\airtouch2\at2plus\AT2PlusClient.py", line 93, in _request_ac_ability
    if ac_ability.abilities[0].number != number:
AttributeError: 'AcAbility' object has no attribute 'number'

Earlier commit 519a8b7

Connecting to 192.168.178.62 on port 9200
Starting listener task
Sending AcStatusMessage with data: 55:55:80:b0:01:c0:00:08:23:00:00:00:00:00:00:0a:7a:30

Enter: 'q' to quit
       'r' to request AC0 ability
Read payload of size 8: 55:55:b0:80:01:c0:00:12
Read payload of size 18: 23:00:00:00:00:0a:00:01:00:14:78:c9:02:bb:00:00:90:00
Read payload of size 2: 35:53
Handling status message
New AC (0) found

            id: 0
            power: 0
            mode: 1
            fan_speed: 4
            set_point: 22.0
            temperature: 19.9
            turbo: True
            bypass: False
            spill: False
            timer: True
            error: 0

Requesting ability of AC0
Sending RequestAcAbilityMessage with data: 55:55:90:b0:01:1f:00:03:ff:11:00:09:83
Waiting for ability message response...
Read payload of size 8: 55:55:b0:90:01:1f:00:1c
Read payload of size 28: ff:11:00:18:4d:48:49:00:00:00:00:00:00:00:00:00:00:00:00:00:00:06:1f:1e:12:12:12:12
Read payload of size 2: b0:89
Creating ability message from 26 bytes
Got ability message response
Got ability of AC0:
        number: 0
        name: MHI
        start_group: 0
        group_count: 6
        supported_modes: [<AcMode.AUTO: 0>, <AcMode.HEAT: 1>, <AcMode.DRY: 2>, <AcMode.FAN: 3>, <AcMode.COOL: 4>]
        supported_fan_speeds: [<AcFanSpeed.QUIET: 1>, <AcFanSpeed.LOW: 2>, <AcFanSpeed.MEDIUM: 3>, <AcFanSpeed.HIGH: 4>]
        min_setpoint: 18
        max_setpoint: 18

Finished handling status message
nathanvdh commented 1 year ago

Thanks. I really need to write a server emulator so I can actually run this code myself...

Try again @bigjezza

phardy commented 1 year ago

Following on from the discussion about addresses in #9, I've gone ahead and tried testing the different scenarios where a client will receive a message, to see what addresses we can expect.

The first thing I learned is that a client can use any address it likes, and the controller will respond to it with the same address. If ADDRESS_CONSTANT is changed to 0xb1, commands will be sent with that addr and responses from the controller will include it.

Another thing I've learned is that when a client sends a command packet, then the controller seems to send a corresponding status packet to all connected clients, with the requester's address. But when a client sends a status request packet, the controller only seems to respond directly to the requester.

I've tried testing by connecting to the controller with the at2plus_test.py test client, sending packets from a number of different clients, and logging what addresses were received by the test client. The clients I sent commands from were:

For AC control messages, I turned the AC on and off, and changed the fan speed. For group control messages, I turned zones on and off. The addresses received by the test client for these were:

Group control AC control
same ? b0
remote ? ADDRESS_CONSTANT
official-local 9f 9f
official-cloud 9f 9f
controller 9f 9f
auto ? 9f
schedule 9f 9f

From this I'm concluding that all of the Polyaire client code is using 0x9f for the address, and it makes sense to accept that address. And obviously the address this client uses (0xb0) should be recognised as a valid address. Practically, that's probably all that's really required. But recognising those two and accepting everything else as "some other client connected to the controller" might also be desirable? Not sure.

nathanvdh commented 1 year ago

@phardy Yes yes yes! Awesome work , thank you! You didn't explicitly say this, so I will ask - did you try changing the address constant to random addresses other than 0x9f and 0xb0? Did everything behave as expected? If so, then I probably should allow anything to go in that byte.

You say:

a client can use any address it likes, and the controller will respond to it with the same address

So I would assume you did.

It's great to have someone with the system to actually try stuff out, thanks again.

phardy commented 1 year ago

You didn't explicitly say this, so I will ask - did you try changing the address constant to random addresses other than 0x9f and 0xb0?

Sorry, I did mean to explicitly say that, but finished up later than I expected. I did change the address constant, although when I was testing previously I only tried an address of 0xb1. With that, the sender would log

Sending AcControlMessage with data: 55:55:80:b1:01:c0:00:0c:22:00:00:00:00:04:00:01:f0:f3:00:fb:bf:ee

and both sender and receiver would receive

Read payload of size 8: 55:55:b1:80:01:c0:00:12

But now I'm trying other values for the address constant, and the controller doesn't like everything. 0xb0 - 0xbf seem fine. But other addresses that I've tried like 0x37, 0x88, 0xd5 never get a response from the controller.

I was considering accepting anything in that byte, but logging a debug level message if it's not a known constant, 0x9f or our sending address 0xb0. Does that make sense?

nathanvdh commented 1 year ago

other addresses that I've tried like 0x37, 0x88, 0xd5 never get a response from the controller

Cheers, this is what I really wanted to know. It seems there's some kind of meaning to the value that we don't know.

Does that make sense?

Yes, it makes sense to expect what we've seen works and log a warning or something when it deviates from that, but...

I really want to crash the whole program when we get something unfamiliar (aka fail-fast/fail-early). It means nothing slips by unnoticed - it annoys people and forces them to look at what's happened and raise an issue. When that we happens we gain another piece of the puzzle. The idea is eventually you get enough to actually piece together wtf is going on.

nathanvdh commented 1 year ago

@phardy

Hey just pushed a commit that has it accept both 0xb0 and 0x9f. If anything else comes up in the future, it can be added. Have a play around and let me know if there's any issues.

bigjezza commented 1 year ago

Well I can set the fan speed and setpoints correctly :) The setpoint limits being reported for both cooling and heating are listed as 18/18 After toggling from on to off I get this (the unit does turn off the ac though):

Failed reading header, trying again
Read payload of size 8: 64:02:f8:00:00:45:64:96
ValueError: Message header magic is invalid
nathanvdh commented 1 year ago

@bigjezza did the logs really appear in that order?

If something went wrong reading the header, I would expect them to be in this order:

Read payload of size 8: 64:02:f8:00:00:45:64:96
ValueError: Message header magic is invalid
Failed reading header, trying again

Can you experiment a bit more and see if it happens often? And if it's after specific commands or if there's any kind of pattern to when it happens? Cheers.

phardy commented 1 year ago

The setpoint limits being reported for both cooling and heating are listed as 18/18

I've noticed this as well and, unfortunately, from what I can see it's a bug in the controller not in this code. Looking at the appropriate parts of the payload, all I see is "12 12 12 12", ie 18 in hexadecimal. I see this in logs you've posted, and in logs I've captured experimenting with my controller. When I get time I'll play with changing these settings and see if changing them has any affect in what's sent. But I'm not holding my breath.

nathanvdh commented 1 year ago

@phardy does the airtouch2+ app display the min/max limits anywhere?

phardy commented 1 year ago

@nathanvdh Good point. The app does know about the limits (it won't let you set a temperature outside those limits). And changes to min/max limits via the keypad are immediately reflected in the app. Let me look closer at what's being sent when the limits change.

bigjezza commented 1 year ago

After toggling it off from on, the first message appears almost instantly and in the space of about a minute:

Finished handling status message
Read payload of size 8: 55:55:9f:80:0d:27:00:01
ValueError: 39 is not a valid MessageType
Failed reading header, trying again
Read payload of size 8: 00:2c:4d:55:55:9f:80:0e
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: c0:00:12:23:00:00:00:00
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 0a:00:01:00:12:82:c9:02
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: d6:00:00:90:00:8c:0d:55
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 55:9f:80:0f:c0:00:20:2b
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 00:00:00:00:04:00:06:80
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 80:07:ff:81:81:07:ff:82
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 82:07:ff:83:83:07:ff:90
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: ff:02:d5:91:ff:07:ff:96
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: dc:55:55:9f:80:10:c0:00
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 12:23:00:00:00:00:0a:00
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 01:00:12:82:c9:02:d5:00
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 00:90:00:0d:37:55:55:b0
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 80:bf:c0:00:38:21:00:00
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 00:00:08:00:06:40:64:96
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 64:02:f8:80:00:41:64:96
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 64:02:f8:00:00:42:64:96
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 64:02:f8:00:00:03:64:96
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 00:02:f8:00:00:44:64:96
ValueError: Message header magic is invalid
Failed reading header, trying again
Read payload of size 8: 64:02:f8:00:00:45:64:96
ValueError: Message header magic is invalid
Failed reading header, trying again
nathanvdh commented 1 year ago

The message type of that first message is 0x27 (decimal 39), which is not in the spec. The message is basically only the header and checksum, it has 1 byte of data and its just 0:

               data length=1
                  <--->               
55:55:9f:80:0d:27:00:01 00 2c:4d
<--------header-------> <>
                       data

@bigjezza @phardy are you both able to packet capture from the app to see if you both get this message, and if the app responds with something? The fact that it has no data could mean a couple of things: the message type is all the data the client needs (like a notification that some event has happened), or its requesting some data from the client. Although, it does have 1 byte of data, so maybe it does put some nonzero data in there sometimes. I'll make some improvements so it doesn't fail so horribly and get you to do some more testing.

Also I need to be smarter about reading the message headers after a failure to make sure it can correctly find the next message.

nathanvdh commented 1 year ago

Alrighty, pushed a commit that should handle unrecognised messages, log them, and hopefully continue working. If there's any syntax errors or silly mistakes I've made, please see if you can quickly fix it up and continue testing. Thanks again guys, appreciate the help and the time you're putting in.

phardy commented 1 year ago

I haven't yet seen a sub message type 27, but haven't been running a test client much lately sorry.

I did spend some time looking at the traffic between the Android app and the console when the app starts, though. When it starts the app sends an undocumented extended message request of type 0xff 0xf0. I've been thinking of it as a controller status, because the response from the controller contains:

What I've been able to find in it so far is in https://gist.github.com/phardy/4bde13205c2c4ccf384807f0bf184b94 (maybe easier to read the raw file, github's org mode parser is struggling with my tables). There's a few gaps, especially around the programs and timer that I'll keep working on. But it's mostly complete by now.

ozhound commented 1 year ago

How can i test this in HA? i have an AT2+ system

nathanvdh commented 1 year ago

How can i test this in HA? i have an AT2+ system

I haven't done the HA integration yet. This library is at a stage where it sort of works, I'm going to see if I can add airtouch2+ support to my existing integration. Will be working on that next.

nathanvdh commented 1 year ago

How can i test this in HA? i have an AT2+ system

There is a now https://github.com/nathanvdh/homeassistant-airtouch2plus for you to test.

nathanvdh commented 1 year ago

@phardy @Chris112 are you able to test the homeassistant integration I've made. I've had a couple report some issues and I'm not sure if it's this client or the integration at the moment.

Chris112 commented 1 year ago

Looks good so far after completing the following steps:

  1. Add the custom repo
  2. Download the integration
  3. Add the airtouch2plus integration and enter the ip address of my airtouch 2+ controller
2023-07-12 19:51:28.563 INFO (SyncWorker_4) [homeassistant.util.package] Attempting install of airtouch2==0.7.5
2023-07-12 19:52:25.781 INFO (MainThread) [homeassistant.setup] Setting up airtouch2plus
2023-07-12 19:52:25.781 INFO (MainThread) [homeassistant.setup] Setup of domain airtouch2plus took 0.0 seconds
2023-07-12 19:52:25.960 INFO (MainThread) [homeassistant.components.climate] Setting up climate.airtouch2plus
2023-07-12 19:52:25.963 INFO (MainThread) [homeassistant.helpers.entity_registry] Registered new climate.airtouch2plus entity: climate.at2plus_ac_0

I've added some screenshots below but I'm not too sure where to find the logs just yet, I've enabled debug logging on the integration but couldn't find anything under the Home Assistant Core logs.

Some observations/thoughts:

image

image

https://github.com/nathanvdh/airtouch2-python/assets/1500031/6c0af5c9-3f3e-40f1-9d1f-78f6f2d839c4

ozhound commented 1 year ago

I dont have any of those Elements on the card, the only item i have in the drop down lists is "Off" image image image

nathanvdh commented 1 year ago

@Chris112 for full debug logs you will need to do something like this in configuration.yaml

# Logging setup
logger:
  default: warning
  logs:
    custom_components.airtouch2plus: debug
    airtouch2.at2plus.At2PlusClient: debug
    airtouch2.common.NetClient: debug

Setting the target temperature via HA would use a step of 0.1 which should probably be a whole number which would align with the airtouch 2+ controller + native android app

From what I could see about the protocol it seemed like it allows steps of 0.1deg. Is there any part of the system that shows temperature to 0.1deg accuracy?

When you change stuff outside of home assistant (airtouch panel or app). Does anything ever change inside home assistant?

nathanvdh commented 1 year ago

@Chris112 can you update everything to latest, go back to the test_programs/at2plus_test.py script and verify it's printing out the AC state as you change stuff? Send me the airtouch2plus.log produced by that, and then also the debug logs from the integration.

ctrl-freak commented 1 year ago

I've tested this with my Panasonic and it set itself up seamlessly.

Changing the mode and temperature also reflects the same on my MQTT Climate card through the NodeRED flow, so I'm stoked.

Thanks so much for your work nathanvdh!

For others reference, I added the repository as a custom repository in HACS and installed from there.

From what I could see about the protocol it seemed like it allows steps of 0.1deg. Is there any part of the system that shows temperature to 0.1deg accuracy?

I think the setpoint may take 0.1deg steps, however I don't think my reported actual temperature has that accuracy; it may depend on the brand and model of the AC though? Bumping mine up a .5 degree of setpoint seems to round up/ceiling the actual as an integer.

Chris112 commented 1 year ago

@Chris112 can you update everything to latest, go back to the test_programs/at2plus_test.py script and verify it's printing out the AC state as you change stuff? Send me the airtouch2plus.log produced by that, and then also the debug logs from the integration.

Sure, here are the logs + the steps I took with the native Android airtouch2+ app:

  1. Uninstall homeassistant-airtouch2plus and reinstall
  2. Start program with python3 ./at2plus_test.py
  3. Turn the AC on
  4. Set the temp to 22
  5. Set the operation to cool
  6. Set the operation to heat
  7. Change the fan speed to low
  8. Set the temp to 27
  9. ctrl+c the program

ha_log.txt airtouch2plus.log

From what I could see about the protocol it seemed like it allows steps of 0.1deg.
Is there any part of the system that shows temperature to 0.1deg accuracy?

When I set the AC to a decimal via HA, the at2plus python program reports the new temperature correctly at the decimal value but the Android app + Airtouch2 console both report the floor value e.g, 25.9 displays as 25.

When you change stuff outside of home assistant (airtouch panel or app). Does anything ever change inside home assistant?

Yeah, initially the HA device was responding instantly to updates made via console or android app + creating the correct state change log on the device but then it appears to break at some point (some unhandled header error maybe?) and UI updates no longer occur but it still sends the correct commands to the airtouch controller to update the state then promptly flick back to the previous state (as seen in the gif above)

nathanvdh commented 1 year ago

Thanks for that, it was being broken by an unrecognised status message (0x2B / decimal 43). I'm handling these now (by ignoring them and logging an error). Try the newest beta 5.

Chris112 commented 1 year ago

Hey Nathan, initially after updating there was no real difference, HA would update the console but then flick back to the previous state.

I then noticed my Airtouch 2+ console had a firmware update from 1.2.1 to 1.2.2 and after installing the new version, everything in HA is working as expected ✨🎈

The only problem remaining that I've noticed is setting an operation or hvac mode won't turn on the AC if it's off which I think is the expected behavior. When I set the operation mode to 'off', it turns off the AC but the HA operation flips back to the previous operation.

If I had to guess, the response from the ac turn off request comes back with "mode : x" and that updates the hvac operation mode. If "power:0" returns, maybe the operation mode can be ignored and set to "off" if that's the case.

In the meantime, using the "Climate: Turn on" service is a good work around with a helper variable has the "power" property isn't exposed.

Here's the log from changing the hvac mode from heat to cool which shows power never flips

2023-08-17 10:58:16.965 DEBUG (MainThread) [airtouch2.common.NetClient] Sending AcControlMessage with data: 55:55:80:b0:01:c0:00:0c:22:00:00:00:00:04:00:01:f0:4f:00:fb:cb:7e
2023-08-17 10:58:16.966 DEBUG (MainThread) [airtouch2.common.NetClient] <airtouch2.protocol.at2plus.messages.AcControl.AcControlMessage object at 0x7fd749e8c750>
2023-08-17 10:58:17.199 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 1: 55
2023-08-17 10:58:17.199 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 1: 55
2023-08-17 10:58:17.199 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 6: 9f:80:f5:c0:00:12
2023-08-17 10:58:17.200 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 18: 23:00:00:00:00:0a:00:01:00:43:aa:c1:02:b2:00:00:80:00
2023-08-17 10:58:17.200 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 2: 91:0f
2023-08-17 10:58:17.200 DEBUG (MainThread) [airtouch2.at2plus.At2PlusClient] Handling status message
2023-08-17 10:58:17.201 DEBUG (MainThread) [airtouch2.at2plus.At2PlusClient] Updated AC 0 with value 
            id: 0
            power: 0
            mode: 4
            fan_speed: 3
            set_point: 27.0
            temperature: 19.0
            turbo: False
            bypass: False
            spill: False
            timer: True
            error: 0

2023-08-17 10:58:17.201 DEBUG (MainThread) [airtouch2.at2plus.At2PlusClient] Finished handling status message
2023-08-17 10:58:17.208 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 1: 55
2023-08-17 10:58:17.208 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 1: 55
2023-08-17 10:58:17.208 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 6: b0:80:01:c0:00:12
2023-08-17 10:58:17.209 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 18: 23:00:00:00:00:0a:00:01:00:43:aa:c1:02:b2:00:00:80:00
2023-08-17 10:58:17.209 DEBUG (MainThread) [airtouch2.common.NetClient] Read payload of size 2: 82:5d
2023-08-17 10:58:17.209 DEBUG (MainThread) [airtouch2.at2plus.At2PlusClient] Handling status message
2023-08-17 10:58:17.210 DEBUG (MainThread) [airtouch2.at2plus.At2PlusClient] Updated AC 0 with value 
            id: 0
            power: 0
            mode: 4
            fan_speed: 3
            set_point: 27.0
            temperature: 19.0
            turbo: False
            bypass: False
            spill: False
            timer: True
            error: 0
nathanvdh commented 1 year ago

@Chris112 just found a mistake in the integration. It wasn't checking the on/off state of the AC correctly.

Can you try beta 6?

Chris112 commented 1 year ago

@Chris112 just found a mistake in the integration. It wasn't checking the on/off state of the AC correctly.

Can you try beta 6?

Everything is working as expected now, thanks so much for your awesome work πŸŽ‰

The real icing on the cake now would be the integration creating fan entities to represent the zones and fan speed, then we could ditch the native mobile app πŸš€

mcoops commented 1 year ago

Everything works well! Agreed, separate entities for each zone would be the pièce de résistance ;)

nathanvdh commented 1 year ago

Closing as complete. Will open separate issue for zones.