Airthings / wave-reader

MIT License
50 stars 18 forks source link

Push to online account #12

Open nakitadog opened 3 years ago

nakitadog commented 3 years ago

Would it be possible to use this script to push to the online account instead of needing to use the phone app?

Einstein2150 commented 2 years ago

It's maybe not the goal you want to reach but you can send the data to any smart-home-system of your choice. My wave arrived yesterday. This is what I made so far:

The Raspberry Pi which controls my 3D-printer runs the script with cron every half hour and gets the bluetooth-data from the device. After that it makes a POST with cURL to Node Red. I changed the script a bit. It is sending the Output now in JSON-format:

~~def str(self): msg = f"\u007B \"Humidity\" : {self.humidity}, \"Temperature\" : {self.temperature}, \"Radon STA\" : {self.radon_sta}, \"Radon LTA\" : {self.radon_lta} \u007D " return msg~~

Update: I send the data now direct out of the python-script without pipe it to cURL. --> https://github.com/Einstein2150/wave-reader

In Node Red I can log the data or visualize it in a dashboard.

LordEvron commented 2 years ago

I am also interested to this feature. Apparently their API does not support POST new data, but you can only read them.

https://developer.airthings.com/consumer-api-docs/

Did anyone found a way around this? I really would like to keep my device sync all the time!

Maybe someone from Airthings could comment on it?

nakitadog commented 2 years ago

I did the following:

1) I wrote some code that interacts with the API. It simply checks each day to see when the Wave was last synced. If it's been more than 24 hours, I alert myself via email that the Wave is out of sync.

2) I also use a spare android device whose only purpose is to sync with the Wave. I then use MacroDroid to make the Wave app sync with the cloud account on a regular basis.

All this wouldn't be necessary if there was a desktop app but it seems that they want you to purchase an Airthings Hub to accomplish this simple task of regularly syncing with the cloud account.

Einstein2150 commented 2 years ago

intercepting the traffic between the app and the server would be good 😁 I will try it in the next days.

Einstein2150 commented 2 years ago

So here is the traffic step by step:

First it refreshes the token from https://api.airthin.gs/v1/refresh The app posts the current token: { "refreshToken": "[token]" }

The server responses with a confirmation: { "accessToken": "[token]", "expiresIn": 10800, "idToken": "[token]", "now": "2022-01-11T06:36:05", "refreshToken": "[token]" }

This happens 2 times...

Now the app requests from https://api.airthin.gs/v1/me?includeHubs=true with authorization = [token] and a x-api-key = [myapikey] a cloud status report:

{ "devices": [ { "batteryPercentage": 100, "currentSensorValues": [ { "isAlert": false, "preferredUnit": "bq", "providedUnit": "bq", "thresholds": [ 100, 150 ], "type": "radonShortTermAvg", "value": 70.0 }, { "isAlert": false, "preferredUnit": "c", "providedUnit": "c", "thresholds": [ 18, 25 ], "type": "temp", "value": 20.2 }, { "isAlert": false, "preferredUnit": "pct", "providedUnit": "pct", "thresholds": [ 25, 30, 60, 70 ], "type": "humidity", "value": 42.0 } ], "lat": 47.763874, "latestSample": "2022-01-10T05:43:00", "lng": 12.457217, "locationId": "090daab0-bd3d-4c4a-8e2f-8f0074xxxxxx", "locationName": "Arbeitsplatz", "relayDevice": "APP", "roomName": "Keller", "segmentId": "7ef25646-299a-432a-ba06-033231xxxxxx", "segmentStart": "2022-01-05T10:31:12", "serialNumber": "2950xxxxxx", "signalQuality": "NO_SIGNAL", "type": "wave2" } ], "email": "xxxxxx", "enabledToggles": [ "mold_enabled", "notifications_enabled", "pollen_link_enabled", "albatross_enabled" ], "name": "Raik Schneider", "preferences": { "androidIntercomUserHash": "cfdd497173d89bb4b0deea77a71d80b4b1c2266579c665877d66d181b1xxxxxx", "dateFormat": "EUR", "hubMode": "NO_HUB", "iosIntercomUserHash": "74dcf43d8a51ddccca92105f48036328259b976da6c3ed3b70b6db41faxxxxxx", "language": "de", "measurementUnits": "METRIC", "proUser": false, "radonUnit": "bq", "tempUnit": "c", "userId": "e8949c08-7a33-4b75-b6ea-fe267fxxxxxx" } }

Look at the "latestSample": "2022-01-10T05:43:00"

Now it checks the data before starting an upload of the new values since the latest sample in the cloud.

https://api.airthin.gs/v1/me/devices/2950xxxxxx/segments/latest/samples?from=2022-01-10T05%3A43%3A00&includeIds=true&to=2022-01-11T06%3A36%3A06 with authorization = [token] and a x-api-key = [myapikey]

The app gets the latest cloud data:

{ "idsForOffsets": [ [ 13210183611279476043 ] ], "lastRecord": "2022-01-10T05:43:00", "lat": xx, "lng": xx, "location": "Arbeitsplatz", "moreDataAvailable": false, "nextPageStart": "2022-01-10T05:43:01", "offsets": [ [ 414708 ] ], "room": "Keller", "segmentId": "7ef25646-299a-432a-ba06-033231xxxxxx", "segmentName": "Keller", "segmentStart": "2022-01-05T10:31:12", "sensors": [ { "offsetType": 0, "type": "radonShortTermAvg", "values": [ 70.0 ] }, { "offsetType": 0, "type": "temp", "values": [ 20.19 ] }, { "offsetType": 0, "type": "humidity", "values": [ 41.5 ] } ] }

Now it posts the new samples to https://api.airthin.gs/v1/me/devices/2950037978/segments/latest/samples with authorization = [token] and a x-api-key = [myapikey]

{ "installationId": "61175029-3349-4BA7-953D-A5958Axxxxxx", "macAddress": "D8:71:4D:xx:xx:xx", "macAddressWrittenDate": 1641882971, "samples": [ { "accel": 0, "accelEvent": false, "appState": "active", "appVersion": "3.8.7(426)", "battCharge": null, "battVoltage": 3063, "bleConnected": null, "co2": null, "cycle": 1, "debug": null, "errorFlag": null, "handWaves": 0, "humidity": 41.5, "id": 13210183611279476044, "light": 0, "pressure": null, "radonInstant": null, "radonLongTermAvg": null, "radonShortTermAvg": null, "record": 1356, "relayDevice": "iPhone", "relayDeviceOS": "iOS,15.3", "sampleRecorded": "2022-01-10T05:48:00", "sampleTransferred": "2022-01-11T06:36:12", "submitted": "2022-01-11T06:36:12", "temp": 20.200000762939453, "voc": null, "waveCcFwVersion": "1.5.3", "waveMspFwVersion": "1.6.0", "waveSub1FwVersion": "2.0.2" }, { "accel": 0, "accelEvent": false, "appState": "active", "appVersion": "3.8.7(426)", "battCharge": null, "battVoltage": 3063, "bleConnected": null, "co2": null, "cycle": 1, "debug": null, "errorFlag": null, "handWaves": 0, "humidity": 41.5, "id": 13210183611279476045, "light": 3, "pressure": null, "radonInstant": null, "radonLongTermAvg": null, "radonShortTermAvg": null, "record": 1357, "relayDevice": "iPhone", "relayDeviceOS": "iOS,15.3", "sampleRecorded": "2022-01-10T05:53:00", "sampleTransferred": "2022-01-11T06:36:12", "submitted": "2022-01-11T06:36:12", "temp": 20.219999313354492, "voc": null, "waveCcFwVersion": "1.5.3", "waveMspFwVersion": "1.6.0", "waveSub1FwVersion": "2.0.2" } ], "submitted": "2022-01-11T06:36:12" }

id is iterating +1 for every new record. The radon level is only transmitted once in a hour.

LordEvron commented 2 years ago

@Einstein2150 Thanks.. That is very useful. What setup did u use for intercepting the traffic?

Einstein2150 commented 2 years ago

@Einstein2150 Thanks.. That is very useful. What setup did u use for intercepting the traffic?

mitmproxy running in docker

Einstein2150 commented 2 years ago

@LordEvron I wrote the first part for refreshing the token and getting the freshest cloud data. Seems like every old known token can call for a new one:

edit: removed and moved into https://github.com/Einstein2150/airthings-api-uploader

LordEvron commented 2 years ago

"Seems like every old known token can call for a new one" --> lol ..ok ... it seems that you have the Wave plus, while i have the old wave. I do not have the MITM setup yet, but i will try to spend some time to set it up (i need it for extracting a working api key, since apparently they do not use that from the browser version). i will keep you posted.

Einstein2150 commented 2 years ago

@LordEvron here is the latest result. It is nearly working but something is preventing it from an update. I get no error but there gets nothing updated. Maybe the reason is that the id and the record isn't correctly calculated.

I have the Wave v2 - no hub and no plus 👍

here ist the latest working code:

edit: removed and moved into https://github.com/Einstein2150/airthings-api-uploader

Einstein2150 commented 2 years ago

Damn. Bricked my cloud connection. Seems that playing with the post parameters got the data async. App synced but no more data gets saved in the cloud.

Unpairing and repairing fixed it but all cloud data is gone 🙃

Beware of that when playing with my code 🤓

Einstein2150 commented 2 years ago

@LordEvron a hint for your first try: close the app for a night and start the mitm-proxy before opening the app. After a long period the app first calls for a fresh token. The token which requests in the https://api.airthin.gs/v1/refresh POST never expires and is the basic token which can always call for a new one 😁. This is the token for oldToken. In the next call to https://api.airthin.gs/v1/me?includeHubs=true you can extract the token for x-api-key. Have fun.

Einstein2150 commented 2 years ago

I build a new repository for the API-stuff. Feel free to contribute: https://github.com/Einstein2150/airthings-api-uploader

LordEvron commented 2 years ago

i have extracted the x-api-key .. but I noticed the app first get all the old datapoints from the cloud, then it push the new one, with subsequent unique ids, which are offsets from a specific date. So, I guess if you do not push data properly, you end up fu*king up your dataseries/cloud as you probably already did :D . I will need some more days/time to make sure i understand the logic behind the ids.

LordEvron commented 2 years ago

So, for Wave version 1 the Post payload is different .. It post this data every hour and the IDs are subsequential ...

{ "samples": [ {"accel": 0,"accelEvent": false,"appState": "background","appVersion": "289(3.6.1)","battCharge": 145,"battVoltage": 249,"bleConnected": false,"cycle": 8,"errorFlag": false,"handWaves": 0,"humidity": 23.0,"id": "800000041","light": 0,"radonLongTermAvg": 20,"radonShortTermAvg": 23,"record": 41,"relayDevice": "samsung-SM-N5544","relayDeviceOS": "Android: 23","sampleId": "800000041","sampleRecorded": "2022-01-09T19:39:22","sampleTransferred": "2022-01-12T12:58:35","submitted": "2022-01-12T12:58:38","temp": 22.2,"waveCcFwVersion": "COR9001B.1703301","waveMspFwVersion": "2020-6-22" }, {"accel": 0,"accelEvent": false,"appState": "background","appVersion": "289(3.6.1)","battCharge": 144,"battVoltage": 249,"bleConnected": false,"cycle": 8,"errorFlag": false,"handWaves": 0,"humidity": 23.0,"id": "800000042","light": 1,"radonLongTermAvg": 20,"radonShortTermAvg": 23,"record": 42,"relayDevice": "samsung-SM-N5544","relayDeviceOS": "Android: 23","sampleId": "800000042","sampleRecorded": "2022-01-09T20:39:22","sampleTransferred": "2022-01-12T12:58:35","submitted": "2022-01-12T12:58:38","temp": 22.2,"waveCcFwVersion": "COR9001B.1703301","waveMspFwVersion": "2020-6-22" } ], "submitted": "2022-01-12T13:58:38" }

I hope this helps...