forked from debsahu/AirthingsMQTT
-
Notifications
You must be signed in to change notification settings - Fork 1
/
AirthingsMQTTBridge.ino
399 lines (350 loc) · 14.4 KB
/
AirthingsMQTTBridge.ino
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
/**
A sketch that searches for a compatible Airthings device and
publishes the radon level, temperature, and humidity to an MQTT
server.
The sketch was created with the intention to allow Airthings devices
to cheaply integrate with Home Assistant.
To use:
(1) Set up your Airthings following the manufacter's instructions.
(2) Install the PunSubClient library (https://pubsubclient.knolleary.net/).
(3) Set your WiFi credentials below.
(4) Set your MQTT server/credentials below.
(5) Update the published topics below (if desired).
(6) Flash to any ESP32 board.
(7) Watch the Serial output to make sure it works.
* The library runs once an hour to take a reading and deep sleeps in
between, so feasibly this could run on a battery for a very long time.
* The library will attempt to find any airthings device to read from,
picking the first it finds. The Airthings BLE API is unauthenticated
so no device configuration or pairing is necessary on the Airthings.
* The library will not interfere with your Airthings' normal upload to a
phone/cloud.
* If it fails to read, it will attempt again after 30 seconds instead.
* I only have an Airthings Wave to test this with, though presumably it
would also work with the Wave Plus.
* The ESP32's bluetooth stack is a little unstable IMHO so expect this to
hang for a few minutes, restart prematurely, and report errors often.
*/
#include <FS.h>
#include <BLEDevice.h>
#include <WiFi.h>
#include <SPIFFS.h>
#include <PubSubClient.h> //https://github.com/knolleary/pubsubclient
#include <WiFiManager.h> //https://github.com/tzapu/WiFiManager/archive/development.zip
#include <ArduinoJson.h> //https://github.com/bblanchon/ArduinoJson
#define HOSTNAME "radon_esp32_airthings_wave_client"
// WiFi credentials.
// #define WIFI_SSID "YOUR SSID"
// #define WIFI_PASS "YOUR PASSWORD"
// MQTT Settings.
char mqtt_server[40] = "192.168.0.xxx";
char mqtt_username[40] = "";
char mqtt_password[40] = "";
char mqtt_port[6] = "1883";
char mqtt_client_name[100] = HOSTNAME;
// The MQTT topic to publish a 24 hour average of radon levels to.
#define TOPIC_RADON_24HR "stat/airthings/radon24hour"
// The MQTT topic to publish the lifetime radon average to. Documentation
// says this will be the average ever since the batteries were removed.
#define TOPIC_RADON_LIFETIME "stat/airthings/radonLifetime"
// Topics for temperature and humidity.
#define TOPIC_TEMPERATURE "stat/airthings/temperature"
#define TOPIC_HUMIDITY "stat/airthings/humidity"
// Unlikely you'll need to change any of the settings below.
// The time to take between readings. One hour has worked pretty well for me.
// Since the device only gives us the 24hr average, more frequent readings
// probably wouldn't be useful, run the airthings battery down, and risk
// interfering with the "normal" mechanism Airthings uses to publish info
// to your phone.
#define READ_WAIT_SECONDS 60*60 // One hour
// If taking a reading fails for any reason (BLE is pretty flaky...) then
// the ESP will sleep for this long before retrying.
#define READ_WAIT_RETRY_SECONDS 30
// How long the ESP will wait to connect to WiFi, scan for
// Airthings devices, etc.
#define CONNECT_WAIT_SECONDS 30
// Some useful constants.
#define uS_TO_S_FACTOR 1000000
#define SECONDS_TO_MILLIS 1000
// #define BECQUERELS_M3_TO_PICOCURIES_L 37.0
#define DOT_PRINT_INTERVAL 50
// The hard-coded uuid's airthings uses to advertise itself and its data.
static BLEUUID serviceUUID("b42e1f6e-ade7-11e4-89d3-123b93f75cba");
static BLEUUID charUUID("b42e01aa-ade7-11e4-89d3-123b93f75cba");
static BLEUUID radon24UUID("b42e01aa-ade7-11e4-89d3-123b93f75cba");
static BLEUUID radonLongTermUUID("b42e0a4c-ade7-11e4-89d3-123b93f75cba");
static BLEUUID datetimeUUID((uint32_t)0x2A08);
static BLEUUID temperatureUUID((uint32_t)0x2A6E);
static BLEUUID humidityUUID((uint32_t)0x2A6F);
bool getAndRecordReadings(BLEAddress pAddress) {
Serial.println();
Serial.println("Connecting...");
BLEClient* client = BLEDevice::createClient();
// Connect to the remove BLE Server.
if (!client->connect(pAddress)) {
Serial.println("Failed to connect.");
return false;
}
Serial.println("Connected!");
// Obtain a reference to the service we are after in the remote BLE server.
Serial.println("Retrieving service reference...");
BLERemoteService* pRemoteService = client->getService(serviceUUID);
if (pRemoteService == nullptr) {
Serial.print("Airthings refused its service UUID.");
client->disconnect();
return false;
}
// Get references to our characteristics
Serial.println("Reading radon/temperature/humidity...");
BLERemoteCharacteristic* temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureUUID);
BLERemoteCharacteristic* humidityCharacteristic = pRemoteService->getCharacteristic(humidityUUID);
BLERemoteCharacteristic* radon24Characteristic = pRemoteService->getCharacteristic(radon24UUID);
BLERemoteCharacteristic* radonLongTermCharacteristic = pRemoteService->getCharacteristic(radonLongTermUUID);
if (temperatureCharacteristic == nullptr ||
humidityCharacteristic == nullptr ||
radon24Characteristic == nullptr ||
radonLongTermCharacteristic == nullptr) {
Serial.print("Failed to read from the device!");
return false;
}
float temperature = ((short)temperatureCharacteristic->readUInt16()) / 100.0;
float humidity = humidityCharacteristic->readUInt16() / 100.0;
// The radon values are reported in terms of - previous version pCi/l radon values
// float radon = radon24Characteristic->readUInt16() / BECQUERELS_M3_TO_PICOCURIES_L;
// float radonLongterm = radonLongTermCharacteristic->readUInt16() / BECQUERELS_M3_TO_PICOCURIES_L;
// The radon values are reported in terms of - newer version Bq/m^3 radon values
float radon = radon24Characteristic->readUInt16();
float radonLongterm = radonLongTermCharacteristic->readUInt16();
client->disconnect();
Serial.printf("Temperature: %f\n", temperature);
Serial.printf("Relative Humidity: %f\n", humidity);
Serial.printf("Radon 24hr average: %f\n", radon);
Serial.printf("Radon Lifetime average: %f\n", radonLongterm);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() < start + CONNECT_WAIT_SECONDS * SECONDS_TO_MILLIS) {
delay(500);
Serial.print(".");
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Failed to connect to wifi");
return false;
}
// Connect and publish to MQTT.
WiFiClient espClient;
PubSubClient mqtt(espClient);
mqtt.setServer(mqtt_server, atoi(mqtt_port));
if (!mqtt.connect(mqtt_client_name, mqtt_username, mqtt_password) ||
!mqtt.publish(TOPIC_RADON_24HR, String(radon).c_str()) ||
!mqtt.publish(TOPIC_RADON_LIFETIME, String(radonLongterm).c_str()) ||
!mqtt.publish(TOPIC_TEMPERATURE, String(temperature).c_str()) ||
!mqtt.publish(TOPIC_HUMIDITY, String(humidity).c_str())) {
Serial.println("Unable to connect/publish to mqtt server.");
return false;
}
return true;
}
// The bluetooth stack takes a callback when scannign for devices. The first Airthings device it finds it will record in pServerAddress.
class FoundDeviceCallback: public BLEAdvertisedDeviceCallbacks {
public:
BLEAddress* address;
bool found = false;
bool foundAirthings() {
return found;
}
BLEAddress getAddress() {
return *address;
}
void onResult(BLEAdvertisedDevice device) {
// We have found a device, see if it has the Airthings service UUID
if (device.haveServiceUUID() && device.getServiceUUID().equals(serviceUUID)) {
Serial.print("Found our device: ");
Serial.println(device.toString().c_str());
device.getScan()->stop();
address = new BLEAddress(device.getAddress());
found = true;
}
}
};
// /**************************** Read/Write MQTT Settings from SPIFFs ****************************************/
bool readConfigFS()
{
//if (resetsettings) { SPIFFS.begin(); SPIFFS.remove("/config.json"); SPIFFS.format(); delay(1000);}
if (SPIFFS.exists("/config.json"))
{
Serial.print(F("Read cfg: "));
File configFile = SPIFFS.open("/config.json", "r");
if (configFile)
{
size_t size = configFile.size(); // Allocate a buffer to store contents of the file.
std::unique_ptr<char[]> buf(new char[size]);
configFile.readBytes(buf.get(), size);
StaticJsonDocument<200> jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, buf.get());
if (!error)
{
JsonObject json = jsonBuffer.as<JsonObject>();
serializeJson(json, Serial);
strcpy(mqtt_server, json["mqtt_server"]);
strcpy(mqtt_port, json["mqtt_port"]);
strcpy(mqtt_username, json["mqtt_username"]);
strcpy(mqtt_password, json["mqtt_password"]);
return true;
}
else
Serial.println(F("Failed to parse JSON!"));
}
else
Serial.println(F("Failed to open \"/config.json\""));
}
else
Serial.println(F("Couldn't find \"/config.json\""));
return false;
}
bool writeConfigFS()
{
Serial.print(F("Saving /config.json: "));
StaticJsonDocument<200> jsonBuffer;
JsonObject json = jsonBuffer.to<JsonObject>();
json["mqtt_server"] = mqtt_server;
json["mqtt_port"] = mqtt_port;
json["mqtt_username"] = mqtt_username;
json["mqtt_password"] = mqtt_password;
File configFile = SPIFFS.open("/config.json", "w");
if (!configFile)
{
Serial.println(F("failed to open config file for writing"));
return false;
}
serializeJson(json, Serial);
serializeJson(json, configFile);
configFile.close();
Serial.println(F("ok!"));
return true;
}
// /***************** Read SPIFFs values *****************************/
void listDir(fs::FS &fs, const char *dirname, uint8_t levels)
{
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if (!root)
{
Serial.println("Failed to open directory");
return;
}
if (!root.isDirectory())
{
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while (file)
{
if (file.isDirectory())
{
Serial.print(" DIR : ");
Serial.print(file.name());
time_t t = file.getLastWrite();
struct tm *tmstruct = localtime(&t);
Serial.printf(" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec);
if (levels)
{
listDir(fs, file.name(), levels - 1);
}
}
else
{
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.print(file.size());
time_t t = file.getLastWrite();
struct tm *tmstruct = localtime(&t);
Serial.printf(" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec);
}
file = root.openNextFile();
}
Serial.println(F("SPIFFs started"));
Serial.println(F("---------------------------"));
}
// /***************** WiFiManager *****************************/
bool shouldSaveConfig = false;
void saveConfigCallback()
{
Serial.println("Should save config");
shouldSaveConfig = true;
}
// /***************** Setup *****************************/
void setup() {
Serial.begin(115200);
Serial.println(F("---------------------------"));
Serial.println(F("Starting SPIFFs"));
if (SPIFFS.begin(true)) //format SPIFFS if needed
{
listDir(SPIFFS, "/", 0);
}
if (readConfigFS())
Serial.println(F(" yay!"));
char NameChipId[64] = {0}, chipId[9] = {0};
WiFi.mode(WIFI_STA); // Make sure you're in station mode
WiFi.setHostname(HOSTNAME);
snprintf(chipId, sizeof(chipId), "%08x", (uint32_t)ESP.getEfuseMac());
snprintf(NameChipId, sizeof(NameChipId), "%s_%08x", HOSTNAME, (uint32_t)ESP.getEfuseMac());
WiFi.setHostname(const_cast<char *>(NameChipId));
WiFiManager wifiManager;
WiFiManagerParameter custom_mqtt_server("mqtt_server", "mqtt server", mqtt_server, 40);
WiFiManagerParameter custom_mqtt_username("mqtt_username", "mqtt username", mqtt_username, 40, " maxlength=31");
WiFiManagerParameter custom_mqtt_password("mqtt_password", "mqtt password", mqtt_password, 40, " maxlength=31 type='password'");
WiFiManagerParameter custom_mqtt_port("mqtt_port", "mqtt port", mqtt_port, 6);
wifiManager.setSaveConfigCallback(saveConfigCallback);
wifiManager.addParameter(&custom_mqtt_server);
wifiManager.addParameter(&custom_mqtt_port);
wifiManager.addParameter(&custom_mqtt_username);
wifiManager.addParameter(&custom_mqtt_password);
if (!wifiManager.autoConnect(HOSTNAME))
{
Serial.println(F("failed to connect and hit timeout"));
delay(3000);
ESP.restart();
delay(5000);
}
Serial.println(F("connected!"));
strcpy(mqtt_server, custom_mqtt_server.getValue());
strcpy(mqtt_port, custom_mqtt_port.getValue());
strcpy(mqtt_username, custom_mqtt_username.getValue());
strcpy(mqtt_password, custom_mqtt_password.getValue());
if (shouldSaveConfig)
{
writeConfigFS();
shouldSaveConfig = false;
}
byte mac[6];
WiFi.macAddress(mac);
sprintf(mqtt_client_name, "%02X%02X%02X%02X%02X%02X%s", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], HOSTNAME);
// Scan for an Airthings device.
Serial.println("Scanning for airthings devices");
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
FoundDeviceCallback* callback = new FoundDeviceCallback();
pBLEScan->setAdvertisedDeviceCallbacks(callback);
pBLEScan->setActiveScan(true);
pBLEScan->start(30);
unsigned long timeToSleep = 0;
if (!callback->foundAirthings()) {
// We timed out looking for an Airthings.
Serial.printf("\nFAILED to find any Airthings devices. Sleeping for %i seconds before retrying.\n", READ_WAIT_RETRY_SECONDS);
timeToSleep = READ_WAIT_RETRY_SECONDS;
} else if (getAndRecordReadings(callback->getAddress())) {
Serial.printf("\nReading complete. Sleeping for %i seconds before taking another reading.\n", READ_WAIT_SECONDS);
timeToSleep = READ_WAIT_SECONDS;
} else {
Serial.printf("\nReading FAILED. Sleeping for %i seconds before retrying.\n", READ_WAIT_RETRY_SECONDS);
timeToSleep = READ_WAIT_RETRY_SECONDS;
}
Serial.flush();
esp_sleep_enable_timer_wakeup(timeToSleep * uS_TO_S_FACTOR);
esp_deep_sleep_start();
}
void loop() {
// We should never reach here.
delay(1);
}