py-air-control: AC2889/10 no longer working

I was using py-air-control for some months to control my AC2889/10 without problems. Since today it does not work any longer although I have changed absolutely nothing. I can switch on and switch off the air purifier using

airctrl --ipaddr 192.168.0.84 --protocol coap --pwr (0|1)

But when calling airctrl --ipaddr 192.168.0.84 --protocol coap I now get the following almost on every call:

2021-07-05 13:44:32,524 - MainThread - coapthon.layers.messagelayer - DEBUG - send_request - From None, To ('192.168.0.84', 5683), None-None, POST-kR, [Uri-Path: sys, Uri-Path: dev, Uri-Path: sync, ] A0852A76...8 bytes
2021-07-05 13:44:32,525 - MainThread - coapthon.client.coap - DEBUG - send_datagram - From None, To ('192.168.0.84', 5683), CON-49660, POST-kR, [Uri-Path: sys, Uri-Path: dev, Uri-Path: sync, ] A0852A76...8 bytes
2021-07-05 13:44:32,526 - Thread-1   - coapthon.client.coap - DEBUG - Start receiver Thread
2021-07-05 13:44:32,527 - MainThread-Retry-49660 - coapthon.client.coap - DEBUG - retransmit loop ... enter
2021-07-05 13:44:34,837 - MainThread-Retry-49660 - coapthon.client.coap - DEBUG - retransmit loop ... retransmit Request
2021-07-05 13:44:34,837 - MainThread-Retry-49660 - coapthon.client.coap - DEBUG - send_datagram - From None, To ('192.168.0.84', 5683), CON-49660, POST-kR, [Uri-Path: sys, Uri-Path: dev, Uri-Path: sync, ] A0852A76...8 bytes
2021-07-05 13:44:37,532 - MainThread-Retry-49660 - coapthon.client.coap - WARNING - Give up on message From None, To ('192.168.0.84', 5683), CON-49660, POST-kR, [Uri-Path: sys, Uri-Path: dev, Uri-Path: sync, ] A0852A76...8 bytes
2021-07-05 13:44:37,534 - MainThread-Retry-49660 - coapthon.client.coap - DEBUG - retransmit loop ... exit
2021-07-05 13:44:37,535 - Thread-1   - coapthon.client.coap - DEBUG - Exiting receiver Thread due to request
Traceback (most recent call last):
  File "/home/pi/.local/bin/airctrl", line 10, in <module>
    sys.exit(main())
  File "/home/pi/.local/lib/python3.7/site-packages/pyairctrl/airctrl.py", line 448, in main
    c = CoAPCli(device["ip"], debug=args.debug)
  File "/home/pi/.local/lib/python3.7/site-packages/pyairctrl/airctrl.py", line 15, in __init__
    self._client = CoAPAirClient(host, port, debug)
  File "/home/pi/.local/lib/python3.7/site-packages/pyairctrl/coap_client.py", line 80, in __init__
    self._sync()
  File "/home/pi/.local/lib/python3.7/site-packages/pyairctrl/coap_client.py", line 96, in _sync
    raise Exception("sync timeout")
Exception: sync timeout

But this does not happen on every call. A small number of calls (roundabout 1 of 20) succeed and return the correct status:

[name]                        Name: Luftreiniger
[type]                        Type: AC2889
[modelid]                     ModelId: AC2889/10
[swversion]                   Version: 1.0.7
[range]                       range: Comfort
[Runtime]                     Runtime: 0.84 hours
[WifiVersion]                 WifiVersion: AWS_Philips_AIR@64.3
[ProductId]                   ProductId: be10acb2e62411e8a1e3061302926720
[DeviceId]                    DeviceId: 47bf972690a911eb8f480aa926c8d24c
[StatusType]                  StatusType: status
[ConnectType]                 ConnectType: Online
[om]                          Fan speed: 0
[pwr]                         Power: OFF
[cl]                          Child lock: False
[aqil]                        Light brightness: 100
[uil]                         Buttons light: ON
[mode]                        Mode: allergen
[pm25]                        PM25: 4
[iaql]                        Allergen index: 1
[aqit]                        Air quality notification threshold: 7
[ddp]                         Used index: IAI
[fltt1]                       HEPA filter type: NanoProtect Filter Series 3 (FY2422)
[fltt2]                       Active carbon filter type: NanoProtect Filter AC (FY2420)
[fltsts0]                     Pre-filter and Wick: clean in 233 hours
[fltsts1]                     HEPA filter: replace in 4313 hours
[fltsts2]                     Active carbon filter: replace in 1913 hours

Is this a known problem and there is a solution for that?

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 2
  • Comments: 30

Most upvoted comments

I have a workaround… using another package. Looks like the FW update from Philips is indeed a bugfix and not a bug. Looks like the purifier only sends a new message if something changed, els it will repeat the last message. So if you have good air quality the values will not change often and py-aircontrol will timeout.

I am now using this package https://github.com/betaboon/aioairctrl which has a small bug which i fixed here https://github.com/Peter-J/aioairctrl (output was not flushed so piping it to another script did not work). You can install it using

pip3 install -U git+https://github.com/......./.........

Biggest feature is that aioairctrl can continously listen to the air purifyer. Again, if your airquality is too god / does not change you will have to wait a long time for the first message. Changing your fanspeed will force new messages to be send.

I included my scripts in the examples folder. Don’t expect too much in regards of code quality / readability and so on but i have not a single faild send or recieve. Zero user intervention since.

If you find any bugs / have suggestions please let me know. Will make a pull-request in a few days.

Best regards, Peter

  1. You have to acquire a package capture. If you do not use a fritzbox router you have to google how to capture packages on your router or within linux running your airctrl script. If your router is a fritzbox go to http://fritz.box/support.lua and login, scroll down to “Paketmitschnitte” and click the link, scroll down to “WLAN” and click “Start” in the row containing “2.4 GHz”. A download will start, save it to your disk.
  2. execute your airctrl command (in my case airctrl --ipaddr 192.168.6.151 --protocol coap )
  3. click “Stopp” in your fritzbox browser window, the download will stop
  4. if you don’t have wireshark: download and install it, you do not need any of those package sniffing drivers wireshark could install
  5. open the downloaded file in wireshark
  6. apply a suitable filter by specifying the ip address of your aircleaner and your airctrl running machine, (in my case it was ip.addr == 192.168.6.151 && ip.addr == 192.168.6.140 ) grafik
  7. left click any coap package, left klick (or double click??) to expand “Constrained Application Protocol …”, right click on the Message ID, left klick “Apply as Column” (Als Spalte anwenden) grafik now you can sort by Message ID by clicking on the corresponding column label
  8. to color the aircleaner when it is sending: click “view” -> “Coloring Rules…” -> “+” in the bottom left corner, name it however you want to, as filter add “ip.src == 192.168.6.151” (replace the ip address with the ip address of your aircleaner), hit enter, highlight the row containing the new rule, click “Background” in the bottom left corner and choose a color that is not light blue, hit “OK” to close the color selection window, hit “OK” to close the Coloring Rules window.
  9. make sure your messages are sorted by message id, look for a /sys/dev/sync with a /sys/dev/sync response from your aircleaner followed by a /sys/dev/status from your airctrl and then … the package we are looking for. I highlighted two unsuccessful examples for you grafik
  10. post a screenshot of the result

What ever you do, do not post the package capture file itself to the internet, it contains ANYTHING transmitted by WLAN in the regarding timeframe.

Best regards, Peter

P.S.: I really hope you don’t hit a roadblock at step 1

https://github.com/betaboon/aioairctrl still works. But because its using the observer pattern as opposed to continously poll. Big reason I had to switch to this library instead. At least if my memory serves correctly…

The first couple of lines were just some trailing whitespace which was triggering syntastic, but the remainder is the relevant part:

index 9d5b142..d2f76eb 100644
--- a/pyairctrl/coap_client.py
+++ b/pyairctrl/coap_client.py
@@ -83,15 +83,15 @@ class CoAPAirClient(HTTPAirClientBase):
     def __del__(self):
         # TODO call a close method explicitly instead
         if self.response:
-            self.client.cancel_observing(self.response, True)        
-        self.client.stop()        
+            self.client.cancel_observing(self.response, True)
+        self.client.stop()
 
     def _create_coap_client(self, host, port):
         return HelperClient(server=(host, port))
 
     def _sync(self):
         self.syncrequest = binascii.hexlify(os.urandom(4)).decode("utf8").upper()
-        resp = self.client.post("/sys/dev/sync", self.syncrequest, timeout=5)
+        resp = self.client.post("/sys/dev/sync", self.syncrequest, timeout=10)
         if resp:
             self.client_key = resp.payload
         else:
@@ -145,7 +145,7 @@ class CoAPAirClient(HTTPAirClientBase):
         try:
             request = self.client.mk_request(defines.Codes.GET, path)
             request.observe = 0
-            self.response = self.client.send_request(request, None, 2)
+            self.response = self.client.send_request(request, None, 10)
             encrypted_payload = self.response.payload
             decrypted_payload = self._decrypt_payload(encrypted_payload)
         except WrongDigestException:
@@ -174,7 +174,7 @@ class CoAPAirClient(HTTPAirClientBase):
                 }
             }
             encrypted_payload = self._encrypt_payload(json.dumps(payload))
-            response = self.client.post(path, encrypted_payload)
+            response = self.client.post(path, encrypted_payload, timeout=10)
             if self.debug:
                 print(response)
             return response.payload == '{"status":"success"}'

Ideally, I’d suggest using a defined constant but I thought I’d see what people thought before proposing something more concrete.

I ping airtctl 10 times and 2 times I got Unexpected error:'NoneType' object has no attribute 'payload' I changed to 15 sec timeout and 10 times ping airtctl and got 0 error.

Thanks.

The first couple of lines were just some trailing whitespace which was triggering syntastic, but the remainder is the relevant part:

index 9d5b142..d2f76eb 100644
--- a/pyairctrl/coap_client.py
+++ b/pyairctrl/coap_client.py
@@ -83,15 +83,15 @@ class CoAPAirClient(HTTPAirClientBase):
     def __del__(self):
         # TODO call a close method explicitly instead
         if self.response:
-            self.client.cancel_observing(self.response, True)        
-        self.client.stop()        
+            self.client.cancel_observing(self.response, True)
+        self.client.stop()
 
     def _create_coap_client(self, host, port):
         return HelperClient(server=(host, port))
 
     def _sync(self):
         self.syncrequest = binascii.hexlify(os.urandom(4)).decode("utf8").upper()
-        resp = self.client.post("/sys/dev/sync", self.syncrequest, timeout=5)
+        resp = self.client.post("/sys/dev/sync", self.syncrequest, timeout=10)
         if resp:
             self.client_key = resp.payload
         else:
@@ -145,7 +145,7 @@ class CoAPAirClient(HTTPAirClientBase):
         try:
             request = self.client.mk_request(defines.Codes.GET, path)
             request.observe = 0
-            self.response = self.client.send_request(request, None, 2)
+            self.response = self.client.send_request(request, None, 10)
             encrypted_payload = self.response.payload
             decrypted_payload = self._decrypt_payload(encrypted_payload)
         except WrongDigestException:
@@ -174,7 +174,7 @@ class CoAPAirClient(HTTPAirClientBase):
                 }
             }
             encrypted_payload = self._encrypt_payload(json.dumps(payload))
-            response = self.client.post(path, encrypted_payload)
+            response = self.client.post(path, encrypted_payload, timeout=10)
             if self.debug:
                 print(response)
             return response.payload == '{"status":"success"}'

Ideally, I’d suggest using a defined constant but I thought I’d see what people thought before proposing something more concrete.