[<< | Prev | Index | Next | >>]

Tuesday, December 10, 2019

Airthings Wave+ Defacto API With History



[Technical entry: Home Automation, Airthings Wave Plus, Python, Bluetooth LE.]

I have a couple Airthings Wave+ multi sensors which read radon, temp, humidity, VOCs, CO2, and--despite that they don't mention it anywhere--ambient light. The radon updates hourly and the rest every five minutes. Unfortunately the only local API Airthings provides is for getting a current snapshot of the sensors, no historic values, and if you polled for that every five minutes you'd probably kill the batteries in weeks or days, not to mention ending up with gaps due to down times or connection failures. They do have an app and a cloud service that work together to eventually collect and archive the full history on their servers, but I find that occasionally hours behind, not to mention I just prefer for many reasons to keep the data local. (To be fair, the app and website work great and would be fine for most people--I have special needs.)

Toward that end, I worked out enough of their API to get the historic data from the unit locally, as well how to flash its light, set its clock, tell whether it's in pairing mode, and whether anybody's been waving at it recently (which one does because it responds by glowing with a color to indicate the air quality). The one thing I haven't figured out is how to get the battery state. (If anybody knows, please do tell.)

I've been using matplotlib to co-plot the various sensors from multiple Waves along with a hand-entered log of events, which has been useful in seeing how opening and closing windows and such affects the radon in multiple parts of the house. One thing I learned early in the process is that my garage, which is the only part of the house directly on slab, fills quickly with radon which then flows to the rest of the house, and consequently that ventilating the garage keeps the radon levels considerably lower in the house. I would have never figured this out without the Airthings, and considering these radon levels are equivalent to smoking half a pack of cigarettes a day in terms of lung cancer risk, I'd say the gamble in investing this much time and money into it has paid off.

In the process I also wrote a small but handy Bluetooth LE library for python, but I'll talk about that in a separate post.

I haven't formally GPL'd the code or whatnot but you're welcome to use it for non-commercial purposes, with no warranty implied or otherwise of course. I just cobbled this together for my own use, and there's a lot of cleanup that needs doing like handling exceptions and error logging better and so on. And, there's no telling when Airthings might change the API, since it is not published; nor whether something in the way I'm accessing the device might strain or brick it, so use at your own risk! That said, so far it's working for me. Please do drop me a note if you use it and tell me how it goes.

Currently you should just need these three files (and python3--sorry, didn't make them backward compatible): AirthingsWavePlusBtle.py, Btle.py, and Units.py.

Note, as of 2019-12-06, you have to fix a bug in the bluepy btle.py library, which typically resides somewhere like /usr/local/lib/python3.6/dist-packages/bluepy/btle.py: Comment out the _getResp in disconnect() or it can hang on readline when a 'stat disc' reply from the helper is overlooked (as in during a call to setMTU). I've reported the bug but it's just crickets over there.

I'm not providing a main application (up to you what to do with the library), but the Airthings... file has a test/demo mode that works like this, which gives you an idea of what information it extracts (it asks for just one hour of history by default):

> python3 AirthingsWavePlusBtle.py 2930012345 a9:cf:21:3c:b6:53 # serial number and mac addy of the device

{'current': {'ambientlight': 0,
             'co2': 504,
             'humidity': 42.0,
             'mode': 0,
             'pressure': 1010.1,
             'radon': 2.027027027027027,
             'radon-lt': 2.4054054054054053,
             'raw': b'\x01T\x00\x00K\x00Y\x00\xe5\x07I\xc5\xf8\x01\x85\x00'
                    b'\x00\x003\x11',
             'temperature': 68.378,
             'time': 1576042356.9542477,
             'time_': datetime.datetime(2019, 12, 10, 21, 32, 36, 954248),
             'voc': 133,
             'waves': 0,
             'x3': 4403},
 'hist': [{'ambientlight': (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
           'co2': (505, 509, 511, 506, 502, 497, 502, 497, 509, 515, 506, 522),
           'humidity': [41.5,
                        41.5,
                        41.5,
                        41.5,
                        41.5,
                        41.0,
                        41.0,
                        41.5,
                        41.5,
                        41.5,
                        41.5,
                        41.5],
           'pressure': [1010.02,
                        1010.06,
                        1010.02,
                        1010.0,
                        1010.02,
                        1010.04,
                        1010.02,
                        1010.0,
                        1009.96,
                        1009.94,
                        1009.98,
                        1010.0],
           'radon': (75, 89),
           'recno': 4130,
           'series_start': 1561169841,
           'series_start_': datetime.datetime(2019, 6, 21, 19, 17, 21),
           'start_time': 1576037841,
           'start_time_': datetime.datetime(2019, 12, 10, 20, 17, 21),
           'temperature': [20.64,
                           20.61,
                           20.5,
                           20.48,
                           20.54,
                           20.54,
                           20.43,
                           20.33,
                           20.39,
                           20.41,
                           20.38,
                           20.27],
           'unused': [(0, 0, 0, 0, 1, 2, 3, 3),
                      (0, 0, 0, 32, 0, 0),
                      (0, 2826, 3056)],
           'voc': [140, 142, 152, 136, 144, 146, 129, 144, 140, 142, 148, 152],
           'x3': [4363.7109375,
                  4363.7109375,
                  4352.86328125,
                  4392.8984375,
                  4367.3359375,
                  4367.3359375,
                  4422.48046875,
                  4396.57421875,
                  4396.57421875,
                  4385.56640625,
                  4374.609375,
                  4374.609375],
           'x4': [1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1]}],
 'mac_addy': 'a9:cf:21:3c:b6:53',
 'q2': {'ambientlight': 0,
        'cycle_start': 1575450446.464978,
        'cycle_start_': datetime.datetime(2019, 12, 4, 1, 7, 26, 464978),
        'raw': b'*\x08\t\x00\x02\x00\x88A\x01\x00\x00\x00\x00\x00@K'
               b'\x14\x00\xb64\x1b\x00\xc5\x00\n\x0b\t\x00',
        'time': 1576042360.464978,
        'time_': datetime.datetime(2019, 12, 10, 21, 32, 40, 464978),
        'time_elapsed': 591914},
 'q3': {'num_records': 4131,
        'raw': b'T\xb1\x8f\r]\x00\x03)\xdf#\x10\xff\xff',
        'series_start': 1561169841,
        'series_start_': datetime.datetime(2019, 6, 21, 19, 17, 21),
        'time': 1576042360.6598046,
        'time_': datetime.datetime(2019, 12, 10, 21, 32, 40, 659805)},
 'serial_number': 2930012345,
 'time': 1576042352.4996765,
 'time_': datetime.datetime(2019, 12, 10, 21, 32, 32, 499676)}



[<< | Prev | Index | Next | >>]


Simon Funk / simonfunk@gmail.com