1) Buy your sensor, connect it to your wifi and configure it to get it active in the Airthings dashboard https://dashboard.airthings.com
2) Configure your account in the Airthings dashboard
- Click 'Account
- Change your 'User settings' to :
- Language --> English
- Radon --> Bq/m³
- Temperature --> Celsius
- Pressure --> hPa
- VOC --> ppb
- Date format --> dd.mm.yyyy
- Change your 'User settings' to :
- Click 'Devices'
- Click on your device
- Note the number on 10-digits near the 'View Plus' icon (information to be used to update your script later)
- Click 'Integrations'
- Click 'Request API Client'
- Enter 'Name' --> 'Airthings-view-plus'
- Enter 'Description' --> ''Airthings-view-plus @home'
- Check 'read:device:current_values'
- Select 'Confidential'
- Select 'Flow type' --> Client credentials (machine-to-machine)
- Note the 'Id' and the 'Secret' (information to be used to update your script later)
- Click 'Request API Client'
- Create an new 'Hardware' with type 'Dummy'
- Create 9 virtual Sensors with type and Axis Label' such as :
- CO2 --> Custom sensor --> ppm
- Battery --> Percentage
- PM2.5 --> Custom sensor --> µg/m³
- PM1 --> Custom sensor --> µg/m³
- Pression --> Barometer
- VOC --> Custom sensor --> ppb
- Radon --> Bq/m³
- Humidity --> Humidity
- Temperature --> Temp
- Create file named '/home/pi/domoticz/scripts/python/airthings_viewplus.py' with tis content:
Code: Select all
#!/usr/bin/env python3
"""
Airthings Business API integration for Domoticz.
Auto-refreshes token and updates Domoticz virtual sensors with latest device values.
Also saves retrieved data to a local JSON flat file for debugging or backup.
"""
import sys
import os
import time
import json
import requests
# === CONFIGURATION ===
CLIENT_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # Replace this with your personnel CLIEND_ID
CLIENT_SECRET = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # Replace this with your personnel CLIEND_SECRET
DEVICE_ID = 'xxxxxxxxxx' # Replace this with your personnel DEVICE_ID
TOKEN_FILE = '/home/pi/domoticz/scripts/python/airthings_token.json' # Adapt this if needed
DATA_FILE = '/home/pi/domoticz/scripts/python/airthings_data.json' # Adapt this if needed
DOMOTICZ_URL = 'http://127.0.0.1:8080'
DOMOTICZ_IDX = {
'co2': 228, # Replace this with your idx device reference
'humidity': 220,# Replace this with your idx device reference
'radon': 222,# Replace this with your idx device reference
'temperature': 219,# Replace this with your idx device reference
'voc': 223,# Replace this with your idx device reference
'pressure': 224,# Replace this with your idx device reference
'pm1': 225,# Replace this with your idx device reference
'pm25': 226,# Replace this with your idx device reference
'battery': 227 # Replace this with your idx device reference
}
# === TOKEN MANAGEMENT ===
def save_token(token_data):
token_data['timestamp'] = int(time.time())
with open(TOKEN_FILE, 'w') as f:
json.dump(token_data, f)
def load_token():
if not os.path.exists(TOKEN_FILE):
return None
with open(TOKEN_FILE, 'r') as f:
token_data = json.load(f)
expires_in = token_data.get('expires_in', 3600)
age = int(time.time()) - token_data.get('timestamp', 0)
if age > expires_in - 60: # refresh 1 min early
return None
return token_data.get('access_token')
def request_new_token():
url = 'https://accounts-api.airthings.com/v1/token'
payload = {
'grant_type': 'client_credentials',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'scope': 'read:device:current_values'
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(url, data=payload, headers=headers)
response.raise_for_status()
token_data = response.json()
save_token(token_data)
return token_data['access_token']
def get_token():
token = load_token()
if token:
return token
return request_new_token()
def refresh_token_if_needed(response):
if response.status_code == 401:
return request_new_token()
return None
# === AIRTHINGS BUSINESS API ===
def get_latest_samples(token):
url = f'https://ext-api.airthings.com/v1/devices/{DEVICE_ID}/latest-samples'
headers = {'Authorization': f'Bearer {token}'}
response = requests.get(url, headers=headers)
if response.status_code == 401:
token = refresh_token_if_needed(response)
headers = {'Authorization': f'Bearer {token}'}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()['data']
def get_device_info(token):
url = f'https://ext-api.airthings.com/v1/devices/{DEVICE_ID}'
headers = {'Authorization': f'Bearer {token}'}
response = requests.get(url, headers=headers)
if response.status_code == 401:
token = refresh_token_if_needed(response)
headers = {'Authorization': f'Bearer {token}'}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
# === SAVE RAW DATA ===
def save_airthings_data(samples, device_info, file_path=DATA_FILE):
try:
data = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"samples": samples,
"device_info": device_info
}
with open(file_path, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error saving data to {file_path}: {e}")
# === DOMOTICZ UPDATE ===
def update_domoticz(sensor, value):
idx = DOMOTICZ_IDX[sensor]
if sensor == 'temperature':
url = f'{DOMOTICZ_URL}/json.htm?type=command¶m=udevice&idx={idx}&nvalue=0&svalue={float(value):.1f}'
elif sensor == 'humidity':
url = f'{DOMOTICZ_URL}/json.htm?type=command¶m=udevice&idx={idx}&nvalue={int(value)}&svalue=0'
elif sensor == 'battery':
url = f'{DOMOTICZ_URL}/json.htm?type=command¶m=udevice&idx={idx}&nvalue=0&svalue={int(value)}'
else:
url = f'{DOMOTICZ_URL}/json.htm?type=command¶m=udevice&idx={idx}&nvalue=0&svalue={value}'
try:
requests.get(url, timeout=5)
except Exception as e:
print(f"Error updating Domoticz idx {idx}: {e}")
# === MAIN ===
def main():
token = get_token()
try:
samples = get_latest_samples(token)
device_info = get_device_info(token)
except requests.exceptions.HTTPError as e:
print("Error fetching data from Airthings API:", e)
return
#save_airthings_data(samples, device_info)
# Update Domoticz
update_domoticz('co2', samples.get('co2', 0))
update_domoticz('humidity', samples.get('humidity', 0))
update_domoticz('radon', samples.get('radonShortTermAvg', 0))
update_domoticz('temperature', samples.get('temp', 0))
update_domoticz('voc', samples.get('voc', 0))
update_domoticz('pressure', samples.get('pressure', 0))
update_domoticz('pm1', samples.get('pm1', 0))
update_domoticz('pm25', samples.get('pm25', 0))
update_domoticz('battery', samples.get('battery', 0))
if __name__ == '__main__':
main()
- Adapt the '=== CONFIGURATION ===' section with your values
Create file named '/home/pi/domoticz/scripts/dzVents/scripts/airthings_trigger.lua' with this content:
Code: Select all
return {
active = true,
on = {
timer = {'every 5 minutes'}
},
logging = {
level = domoticz.LOG_ERROR,
marker = "Airthings"
},
execute = function(domoticz)
local pythonScript = "/home/pi/domoticz/scripts/python/airthings_viewplus.py"
local handle = io.popen("python3 " .. pythonScript .. " 2>&1")
local result = handle:read("*a")
handle:close()
if result ~= "" then
domoticz.log("Error or output from script: " .. result, domoticz.LOG_ERROR)
else
domoticz.log("Airthings script executed successfully", domoticz.LOG_INFO)
end
end
}
{
"timestamp": "2025-10-16 05:00:01",
"samples": {
"time": 1760583439,
"battery": 100,
"co2": 727.0,
"humidity": 62.0,
"pm1": 8.0,
"pm25": 8.0,
"pressure": 990.0,
"radonShortTermAvg": 98.0,
"relayDeviceType": "hub",
"rssi": 0,
"temp": 20.7,
"voc": 167.0
},
"device_info": {
"id": "xxxxxxxxxx", # This has been replaced but it corresponds to your device serial number
"deviceType": "VIEW_PLUS",
"sensors": [
"radonShortTermAvg",
"temp",
"humidity",
"pressure",
"co2",
"voc",
"pm1",
"pm25"
],
"segment": {
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # This has been replaced for anonymisation
"name": "View plus",
"started": "2025-10-13T08:57:31",
"active": true
},
"location": {
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # This has been replaced for anonymisation
"name": "Mon domicile"
},
"productName": "View Plus"
}
}