Airgradient2mqtt - Cloud replacement for usage with Home Assistant

After having received my AirGradient sensors, I’ve noticed that there’s no stock way of using them locally.
There are some “solutions” like flashing ESPHome but I wasn’t very happy with that and figured that there should be another way.

And there is another way. I’ve found myself having built another cloud replacement. Again.

The linked repository contains a drop-in cloud replacement for airgradient sensors that takes the data and publishes it to MQTT providing Home Assistant autodiscovery metadata. The only to-do being to redirect the DNS.

It provides the experience I thought I would be getting when I bought these sensors that were marketed to me as being “the open solution” that you’d want to get because things just work ootb cloud-free. Well, that wasn’t the case. But now it is.

It further contains custom firmwares that implement additional functionality in combination with this mock cloud. That functionality being:

  • The ability to reset wifi credentials (at all)
  • The ability to change the brightness of the V9 LED Bar and OLED Display (for not using it as a night light)

Conceivably, additional functionality such as e.g. triggering the manual CO2 Sensor calibration could be added in the future.

It should work just fine with the stock firmware but the additional features will require the customized ones that can also be found in that repo.

1 Like

I have rewritten parts of the official source code so the esp32 will use a small mqtt client to publish to a local server. Maybe thats interessting for some1. I have used platformio for this and i also provide my platform.ini file.

Hey, this is also the kind of behaviour I was expecting from an ‘open-source’ product. Thanks for putting this together.

I’ve almost got it working, but something’s wrong, I wonder if you have any ideas…

  • I’m redirecting hw.airgradient.com to a local IP
  • I’m running the custom cloud, and it has connected to the MQTT broker:
> airgradient2mqtt@1.0.0 start
> node app.js

[2024-04-04T14:53:31.370Z] [INFO] Airgradient DummyCloud listening on port 80
[2024-04-04T14:53:31.871Z] [INFO] Connected to MQTT broker
[2024-04-04T14:53:31.879Z] [INFO] Successfully subscribed to MQTT command topics [ { topic: 'airgradient2mqtt/+/+/set', qos: 0 } ]

But there is no traffic on the log there. When I look at traffic using tcpdump, it seems like the device is trying to configure itself on the server before sending data:

$ tcpdump port 80 -A
09:45:24.813854 IP 192.168.1.70.63430 > hw.airgradient.com.http: Flags [F.], seq 207, ack 451, win 5294, length 0
E..(......7....F...f...P.h....D.P..........     ..
09:45:24.814239 IP hw.airgradient.com.http > 192.168.1.70.63430: Flags [F.], seq 451, ack 208, win 64034, length 0
E..(..@.@..#...f...F.P....D..h..P.."....
09:45:24.820162 IP 192.168.1.70.63430 > hw.airgradient.com.http: Flags [.], ack 452, win 5293, length 0
E..(......7....F...f...P.h....D.P........       ....
09:45:25.817167 IP 192.168.1.70.63431 > hw.airgradient.com.http: Flags [S], seq 3311957983, win 5744, options [mss 1436], length 0
E..,......7....F...f...P.hw.....`..p.q......ai
09:45:25.817257 IP hw.airgradient.com.http > 192.168.1.70.63431: Flags [S.], seq 2579958856, ack 3311957984, win 64240, options [mss 1460], length 0
E..,..@.@......f...F.P.....H.hw.`...=.......
09:45:25.819769 IP 192.168.1.70.63431 > hw.airgradient.com.http: Flags [.], ack 1, win 5744, length 0
E..(......7....F...f...P.hw....IP..p9.........
09:45:25.828429 IP 192.168.1.70.63431 > hw.airgradient.com.http: Flags [P.], seq 1:207, ack 1, win 5744, length 206: HTTP: GET /sensors/airgradient:84fce6099488/config?attempt=3&fw=0.4.0 HTTP/1.1
E.........6....F...f...P.hw....IP..p....GET /sensors/airgradient:84fce6099488/config?attempt=3&fw=0.4.0 HTTP/1.1
Host: hw.airgradient.com
User-Agent: ESP32HTTPClient
Connection: keep-alive
Accept-Encoding: identity;q=1,chunked;q=0.1,*;q=0


09:45:25.828462 IP hw.airgradient.com.http > 192.168.1.70.63431: Flags [.], ack 207, win 64034, length 0
E..(..@.@..F...f...F.P.....I.hx.P.."Uu..
09:45:25.829605 IP hw.airgradient.com.http > 192.168.1.70.63431: Flags [P.], seq 1:451, ack 207, win 64034, length 450: HTTP: HTTP/1.1 404 Not Found
E.....@.@......f...F.P.....I.hx.P.."....HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 178
Date: Thu, 04 Apr 2024 15:45:25 GMT
Connection: keep-alive
Keep-Alive: timeout=5

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /sensors/airgradient:84fce6099488/config</pre>
</body>
</html>

09:45:25.835695 IP 192.168.1.70.63431 > hw.airgradient.com.http: Flags [F.], seq 207, ack 451, win 5294, length 0
E..(......7....F...f...P.hx.....P...9'..HTTP..
09:45:25.835918 IP hw.airgradient.com.http > 192.168.1.70.63431: Flags [F.], seq 451, ack 208, win 64034, length 0
E..(..@.@..D...f...F.P.......hx.P.."S...
09:45:25.838453 IP 192.168.1.70.63431 > hw.airgradient.com.http: Flags [.], ack 452, win 5293, length 0
E..(......7....F...f...P.hx.....P...9'..HTTP.,

It looks like the firmware changed quite a lot since I wrote this mqtt bridge.

It might be enough to implement that endpoint as well. Though, you could also use the firmware that is part of the repo

I added the following route:

    this.app.get("/sensors/airgradient::sensorId/config", (req, res) => {
          const config = {
            // Your JSON configuration data goes here
                         // Example:
                   country: 'Canada',
                   pmStandard: 'ugm3',
                   co2CalibrationRequested: false,
                   ledBarMode: false,
                   model: 'openair',
                   mqttBrokerUrl: false,
                   abcDays: 4,
                   ledBarTestRequested: false };
           Logger.info("triggered config")
           res.json(config);
        });

and changed the existing route from

this.app.post("/sensors/airgradient::sensorId/measures", (req, res) => {

to

this.app.put("/sensors/airgradient::sensorId/openair/firmware", (req, res) => {

And it seems to have fixed it. It shows up in The CO2 values are wonky though. There might be something I need to put in for co2CalibrationRequested or abcDays.

I’m not sure which firmware I’m running, but I should probably just use the version in the repo. To be honest, flashing the firmware seems a bit intimidating which is why I tried this first.

The latest firmwares now actually has a local server running and we are currently building the ability to make local PUT calls to the monitor e.g. to trigger calibrations or the dispplay configuration (Celcius, Fahrenheit etc). So stay tuned.

We will also add a flag to completely disconnect it from the AirGradient Cloud (if people so wish :).

We will also add a flag to completely disconnect it from the AirGradient Cloud (if people so wish :).

Yes please :pray:
As said, that option was literally the reason why I bought AirGradient products in the first place because otherwise I could’ve just glued together a few sensors + an ESP myself and saved a lot of money.
Not to say that the Cases, PCBs and all aren’t well-designed of course, but they alone don’t justify the price premium over just buying the components yourself.

And, if you’re already paying a premium and you’re still using the cloud, you might as well just buy other premium cloud air sensor products like e.g. Netatmo that come with a lot more UX polish.


Anyway, what I would like to see here would be a toggle switch for the cloud that you see when you open the device provisioning page on the AP provided by the sensor. It’s fine for me if that thing is on-by-default, as long as it’s just one click and you’re offline.

It would be important to me to have that toggle there so that the sensor is set offline before it even becomes aware of any WiFi credentials and also because you can then set it up without requiring an internet connection.