How to get data from Airthings Hub

  • Platform information:
    • Hardware: Raspberry CM4
    • OS: Linux raspberrypi 6.1.21-v8+ #1642 SMP PREEMPT Mon Apr 3 17:24:16 BST 2023 aarch64 GNU/Linux
    • Java Runtime Environment: openjdk version “17.0.9” 2023-10-17
    • openHAB version: 4.0.4

Hi,

I installed an Airthings Bundle containing an Airthings Hub a Wave Radon and a Wave Mini. Everything works fine and I can see the data in the IOS App and in the Airthings Dashboard.
What is the best way to get the data into Openhab? I saw there was a binding in OH 3.4 using Bluetooth but I found nothing for the HUB.

Many thanks

Paul

With the Hub I think your option is to pull the data from the cloud using the AirThings API.

It does not appear an OH binding has been created for this. There is likely some script somewhere on GitHub that does this though.

The BT binding supports getting data from the sensors themselves without the hub (it’s still there in OH 4) and it appears to support the Plus, Mini, and first gen.

I run OH in Docker and getting BT to work inside Docker is a pain so I use GitHub - Drolla/WavePlus_Bridge: Airthings Wave Plus Bridge to Wifi/LAN and MQTT with MQTT. I don’t know if that works with the mini though. I only have WavePlus sensors.

If you or future readers go down that route, here’s one of my Generic MQTT Things to jump start the OH configuration side of things.

UID: mqtt:topic:broker:basement_waveplus
label: Basement Airthings Wave Plus
thingTypeUID: mqtt:topic
configuration:
  transformationPattern: MAP:config:map:waveplus
  availabilityTopic: waveplus_bridge/basement/status
bridgeUID: mqtt:broker:broker
location: Basement
channels:
  - id: radon_st
    channelTypeUID: mqtt:number
    label: Short Term Radon Average
    description: ""
    configuration:
      stateTopic: waveplus_bridge/basement/radon_st
  - id: radon_lt
    channelTypeUID: mqtt:number
    label: Long Term Radon Average
    description: ""
    configuration:
      stateTopic: waveplus_bridge/basement/radon_lt
  - id: co2
    channelTypeUID: mqtt:number
    label: CO2
    description: ""
    configuration:
      stateTopic: waveplus_bridge/basement/co2
      postCommand: true
      unit: ppm
  - id: temperature
    channelTypeUID: mqtt:number
    label: Temperature
    description: ""
    configuration:
      stateTopic: waveplus_bridge/basement/temperature
      unit: °C
  - id: pressure
    channelTypeUID: mqtt:number
    label: Barometric Pressure
    description: ""
    configuration:
      stateTopic: waveplus_bridge/basement/pressure
      unit: hPa
  - id: humidity
    channelTypeUID: mqtt:number
    label: Humidity
    description: ""
    configuration:
      stateTopic: waveplus_bridge/basement/humidity
      unit: "%"
  - id: voc
    channelTypeUID: mqtt:number
    label: Volatile Organic Chemicals
    description: ""
    configuration:
      stateTopic: waveplus_bridge/basement/voc
      unit: ppb

The transform for the status (i.e. the Thing will show as OFFLINE if the bridge reports it cannot communicate with the device) is

Online=ON
Offline=OFF
Connection\ lost=OFF

Thanks - I will play with the API and post the solution if I‘m successful.

Paul

Hi,

I played around and found a working solution. I have used this example from Airthings consumer API with Python - matthewdavis111 to build a python script querying the API and extracting the data. I run the script from a DSL rule to update the Items.

The air.py script

import logging 
import requests
from requests import HTTPError
import time

client_id = "Your Client-ID"
device_id = "Serial number of your device"
client_secret = "Your Client Secret"
authorisation_url = "https://accounts-api.airthings.com/v1/token"
device_url = f"https://ext-api.airthings.com/v1/devices/{device_id}/latest-samples"
token_req_payload = {
    "grant_type": "client_credentials",
    "scope": "read:device:current_values",
}

# Request Access Token from auth server
try:
    token_response = requests.post(
        authorisation_url,
        data=token_req_payload,
        allow_redirects=False,
        auth=(client_id, client_secret),
    )
    token_response.raise_for_status()  # Check if the request was successful
    token = token_response.json()["access_token"]

except HTTPError as e:
    logging.error(e)

# Get the latest data for the device from the Airthings API.
try:
    api_headers = {"Authorization": f"Bearer {token}"}
    response = requests.get(url=device_url, headers=api_headers)
    response.raise_for_status()  # Check if the request was successful

    # Extract relevant data from the API response for the Radon Wave device
    device_data = {
        'data': {
            'battery': response.json()['data']['battery'],
            'humidity': response.json()['data']['humidity'],
            'radonShortTermAvg': response.json()['data']['radonShortTermAvg'],
            'relayDeviceType': response.json()['data']['relayDeviceType'],
            'rssi': response.json()['data']['rssi'],
            'temp': response.json()['data']['temp'],
            'time': response.json()['data']['time'],
        }
    }

except HTTPError as e:
    logging.error(e)

openhab_base_url = 'http://IP of your OpenHAB server:8080/rest'

# OpenHAB item names
item_names = {
    'battery': 'AirthingsBattery',
    'humidity': 'AirthingsHum',
    'radonShortTermAvg': 'AirthingsRadon',
    'temp': 'AirthingsTemp',
#    'rssi': 'AirthingsRSSI',
    # Add other item names as needed
}

# Construct the OpenHAB item update URL
update_url = f"{openhab_base_url}/items/{{item_name}}/state"

# Loop through the data and update corresponding OpenHAB items
for key, value in device_data['data'].items():
    item_name = item_names.get(key)
    if item_name:
        url = update_url.format(item_name=item_name)
        headers = {'Content-Type': 'text/plain'}
        response = requests.put(url, data=str(value), headers=headers)
        if response.status_code == 202: 
            print(f"Accepted update for {item_name}. Checking status again after a delay.")
            time.sleep(2)  # Adjust the delay as needed
            # Check status again
            response = requests.get(url)
            if response.status_code == 200: 
                print(f"Successfully updated {item_name} with value {value}")
            else:
                print(f"Failed to update {item_name}. Status code: {response.status_code}")
        elif response.status_code == 200: 
            print(f"Successfully updated {item_name} with value {value}")
        else:
            print(f"Failed to update {item_name}. Status code: {response.status_code}")

to run this script once per hour (the consumer API is limited to 120 requests per hour) I have this rule:

rule "Call Python Scripts for Airthings API data"
when
    Time cron "0 0 * * * ?"
then

val airthings_api_1 = executeCommandLine(Duration.ofSeconds(30), "python3", "/etc/openhab/rules/air.py")
logInfo("Airthings_Python_1", airthings_api_1)

end

Thanks for the hints to other code sources.

Best

Paul

1 Like

Hi

I wanted to do the same, getting the Airthings data from their API. I had a bit different approach, so I will share my solution here.

My approach is a generic way of getting the OAuth token, which can be used for other API’s as well.

How does it work?
I use a http-binding that gets the OAuth token. When the token refreshes, a rule is triggered and the OAuth token from that item is pushed to the other http-bindings that requires token authentication.

What do you need?
-You will need an Airthings account. And create a client_id and client_secet from their dashboard.
-You will need a openhab API Tokens (Can be created under your profile)

http-binding: OAuth token:
***** = client_id from Airthings | ****** = client_secret from Airthings

UID: http:url:AirthingsOAuth
label: Airthings OAuth
thingTypeUID: http:url
configuration:
  authMode: BASIC
  headers:
    - Content-Type=application/json
  ignoreSSLErrors: false
  baseURL: https://accounts-api.airthings.com/v1/token
  delay: 0
  stateMethod: POST
  refresh: 10700
  commandMethod: POST
  encoding: application/json
  contentType: application/json
  timeout: 10000
  bufferSize: 2048
channels:
  - id: Token
    channelTypeUID: http:string
    label: Token
    description: null
    configuration:
      mode: READONLY
      stateTransformation: JSONPATH:$.access_token
      stateContent: |
        {
          "grant_type":"client_credentials",
          "client_id":"***************",
          "client_secret":"***************",
          "scope": ["read:device:current_values"]
        }

The http item thats gets the data from Airthings (i don’t include all the channels in this example):
**** = will be updated from your rule (not needing replacement)

UID: http:url:AirthingsApiViewPlus
label: Airthings Api View Plus
thingTypeUID: http:url
configuration:
  authMode: BASIC
  headers:
    - Authorization=Bearer *********
    - ""
  ignoreSSLErrors: false
  baseURL: https://ext-api.airthings.com/v1/
  delay: 0
  stateMethod: GET
  refresh: 600
  commandMethod: GET
  timeout: 3000
  bufferSize: 2048
channels:
  - id: co2
    channelTypeUID: http:number
    label: CO2
    description: null
    configuration:
      mode: READONLY
      stateExtension: devices/<deviceid>/latest-samples
      stateTransformation: JSONPATH:$.data.co2

The rule that updates the the http items that gets the real data from Airthings:
***** = “openhab API Tokens”

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Airthings_OAuth_Token
    type: core.ItemStateUpdateTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: >-
        var selfApiToken =
        "oh.selfapi.*****************************";

        var headers = {"Authorization": "Bearer " + selfApiToken, "WWW-Authenticate": "Basic"};
        var oauthTokenItem = items.getItem("Airthings_OAuth_Token");

        var config = `

        {
            "authMode": "BASIC",
            "headers": [
                "Authorization=Bearer `+oauthTokenItem.state+`",
                ""
            ],
            "ignoreSSLErrors": false,
            "baseURL": "https://ext-api.airthings.com/v1/",
            "delay": 0,
            "stateMethod": "GET",
            "refresh": 600,
            "commandMethod": "GET",
            "timeout": 3000,
            "bufferSize": 2048
        }

        `;


        actions.HTTP.sendHttpPutRequest("http://openhab:8080/rest/things/http:url:AirthingsApiViewPlus/config", "application/json", config, headers, 1000);

Be aware that i use “http://openhab:8080” in my script, you might need to replace that url.

1 Like