ENOSUCHBLOG

Programming, philosophy, pedaling.


Introducing awair-local-api.rs

Mar 20, 2023     Tags: devblog, programming, rust    

This post is at least a year old.

This is another small announcement-type post, for (yet another) Rust library that I wrote a few months ago1: awair-local-api.rs.

It’s available on crates.io; read on if you’re interested in the motivation, what it can do, and how you can use it (if you have an Awair air sensor).

Background

I’ve had an Awair air monitor for a few years2; I keep it in my home office to keep track of CO2 levels while I work:

(Not my picture.)

It’s one of the few IoT/”smart” devices I like and own, in part because of their unambiguous privacy policy. As it’s a few years old some of the fancier sensors (like TVOC detection) are probably out of whack, but that doesn’t matter for my purposes (which, again, are only CO2 levels).

Another near thing about Awair’s devices is that they support a “Local API” feature: when activated (in the mobile app), they serve up a basic HTTP index and some JSON endpoints:

If you make the GET {host}/air-data/latest request as suggested, you get something very reasonable-looking back:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "timestamp": "2023-03-20T03:20:08.458Z",
  "score": 77,
  "dew_point": 3.23,
  "temp": 22.62,
  "humid": 28.1,
  "abs_humid": 5.64,
  "co2": 1304,
  "co2_est": 929,
  "co2_est_baseline": 36882,
  "voc": 352,
  "voc_baseline": 38274,
  "voc_h2_raw": 25,
  "voc_ethanol_raw": 35,
  "pm25": 0,
  "pm10_est": 1
}

The response is unitless, but the docs describe each value’s units.

So: in the interest of potentially prolonging this device’s life just a little bit (in the event Awair decides to stop supporting it), I decided to write a quick Rust library for the Local API’s endpoints.

awair-local-api.rs

The Awair Local API is very simple, so the crate is also very simple. Here’s how you can get the latest air quality data from a Local API-enabled sensor with it:

1
2
3
4
5
6
use awair_local_api::Awair;

let client = Awair::new("{your-awair-ip-here}").unwrap();
let data = client.poll().unwrap();

println!("score: {}, CO2: {}", data.score, data.co2);

You can see all of the fields available on the poll() response type in the docs.

You can also retrieve the device’s active config:

1
2
3
4
5
6
let config = client.config().unwrap();

println!(
  "ID: {}, WIFI MAC: {}, firmware version: {}",
  config.ip, config.wifi_mac, config.firmware_version
);

…and that’s really it. There’s a very small demo program in the crate’s GitHub repo that shows these endpoints off:

1
2
3
4
5
6
git clone https://github.com/woodruffw/awair-local-api.rs
cd awair-local-api.rs
cargo build --examples

# your Awair's IP goes here
./target/debug/demo http://192.168.1.255

producing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
DeviceConfig {
    device_id: "awair-r2_REDACTED",
    wifi_mac: "REDACTED",
    ssid: "REDACTED",
    ip: "192.168.1.255",
    netmask: "255.255.255.0",
    gateway: "192.168.1.1",
    firmware_version: "1.2.4",
    timezone: "America/New_York",
    display: "score",
    led: LedConfig {
        mode: "auto",
        brightness: 179,
    },
    voc_feature_set: 32,
}
AirData {
    timestamp: 2023-03-20T03:40:03.788Z,
    score: 79,
    dew_point: 3.21,
    temperature: 22.88,
    humidity: 27.64,
    absolute_humidity: 5.63,
    co2: 1174,
    estimated_co2: 803,
    estimated_co2_baseline: 36882,
    voc: 281,
    voc_baseline: 38274,
    voc_h2_raw: 26,
    voc_ethanol_raw: 35,
    pm25: 0,
    estimated_pm10: 1,
}

Hopefully this is of some use to other Awair owners. I may eventually build a dashboard around it.


  1. I usually mean to write these types of post when I release these things, but I consistently forget to. 

  2. It’s “old” at this point, at least in IoT-years. 


Discussions: Reddit