Abstract AirQualityTelemetry module

Previously the module was designed around a single sensor.
This patch brings it into line with the same design as
EnvironmentTelemetry.
This commit is contained in:
Tom Fifield 2024-10-18 18:16:45 +11:00
parent a28e83c35a
commit 318da220dc
7 changed files with 186 additions and 80 deletions

View File

@ -10,12 +10,32 @@
#include "PowerFSM.h" #include "PowerFSM.h"
#include "RTC.h" #include "RTC.h"
#include "Router.h" #include "Router.h"
#include "UnitConversions.h"
#include "detect/ScanI2CTwoWire.h" #include "detect/ScanI2CTwoWire.h"
#include "graphics/ScreenFonts.h"
#include "main.h" #include "main.h"
#include "sleep.h"
#include <Throttle.h> #include <Throttle.h>
// Sensors
#include "Sensor/PMSA0031Sensor.h"
#include "Sensor/SCD4XSensor.h"
SCD4XSensor scd4xSensor;
PMSA0031Sensor pmsa0031Sensor;
int32_t AirQualityTelemetryModule::runOnce() int32_t AirQualityTelemetryModule::runOnce()
{ {
if (sleepOnNextExecution == true) {
sleepOnNextExecution = false;
uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval,
default_telemetry_broadcast_interval_secs);
LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs);
doDeepSleep(nightyNightMs, true);
}
uint32_t result = UINT32_MAX;
/* /*
Uncomment the preferences below if you want to use the module Uncomment the preferences below if you want to use the module
without having to configure it from the PythonAPI or WebUI. without having to configure it from the PythonAPI or WebUI.
@ -34,28 +54,13 @@ int32_t AirQualityTelemetryModule::runOnce()
if (moduleConfig.telemetry.air_quality_enabled) { if (moduleConfig.telemetry.air_quality_enabled) {
LOG_INFO("Air quality Telemetry: Initializing"); LOG_INFO("Air quality Telemetry: Initializing");
if (!aqi.begin_I2C()) {
LOG_WARN("Could not establish i2c connection to AQI sensor. Rescanning..."); if (scd4xSensor.hasSensor())
// rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. result = scd4xSensor.runOnce();
uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; if (pmsa0031Sensor.hasSensor())
uint8_t i2caddr_asize = 1; result = pmsa0031Sensor.runOnce();
auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire());
#if defined(I2C_SDA1)
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize);
#endif
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize);
auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031);
if (found.type != ScanI2C::DeviceType::NONE) {
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address;
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
i2cScanner->fetchI2CBus(found.address);
return 1000;
} }
return disable(); return result;
}
return 1000;
}
return disable();
} else { } else {
// if we somehow got to a second run of this module with measurement disabled, then just wait forever // if we somehow got to a second run of this module with measurement disabled, then just wait forever
if (!moduleConfig.telemetry.air_quality_enabled) if (!moduleConfig.telemetry.air_quality_enabled)
@ -69,13 +74,15 @@ int32_t AirQualityTelemetryModule::runOnce()
airTime->isTxAllowedAirUtil()) { airTime->isTxAllowedAirUtil()) {
sendTelemetry(); sendTelemetry();
lastSentToMesh = millis(); lastSentToMesh = millis();
} else if (service->isToPhoneQueueEmpty()) { } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) &&
(service->isToPhoneQueueEmpty())) {
// Just send to phone when it's not our time to send to mesh yet // Just send to phone when it's not our time to send to mesh yet
// Only send while queue is empty (phone assumed connected) // Only send while queue is empty (phone assumed connected)
sendTelemetry(NODENUM_BROADCAST, true); sendTelemetry(NODENUM_BROADCAST, true);
lastSentToPhone = millis();
} }
} }
return sendToPhoneIntervalMs; return min(sendToPhoneIntervalMs, result);
} }
bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
@ -84,9 +91,9 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
#ifdef DEBUG_PORT #ifdef DEBUG_PORT
const char *sender = getSenderShortName(mp); const char *sender = getSenderShortName(mp);
LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i, co2=%i ppm", sender,
t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard,
t->variant.air_quality_metrics.pm100_standard); t->variant.air_quality_metrics.pm100_standard, t->variant.air_quality_metrics.co2);
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental,
@ -102,32 +109,68 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack
return false; // Let others look at this message also if they want return false; // Let others look at this message also if they want
} }
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) bool AirQualityTelemetryModule::wantUIFrame()
{ {
if (!aqi.read(&data)) { return moduleConfig.telemetry.environment_screen_enabled;
LOG_WARN("Skipping send measurements. Could not read AQIn"); }
return false;
void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
if (lastMeasurementPacket == nullptr) {
// If there's no valid packet, display "Environment"
display->drawString(x, y, "Air Quality");
display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
return;
} }
// Decode the last measurement packet
meshtastic_Telemetry lastMeasurement;
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
const char *lastSender = getSenderShortName(*lastMeasurementPacket);
const meshtastic_Data &p = lastMeasurementPacket->decoded;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
display->drawString(x, y, "Measurement Error");
LOG_ERROR("Unable to decode last packet");
return;
}
// Display "Env. From: ..." on its own
display->drawString(x, y, "AQ. From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
// Continue with the remaining details
/*display->drawString(x, y += _fontHeight(FONT_SMALL),
"Temp/Hum: " + last_temp + " / " +
String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%");*/
/*if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
"Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA");
}*/
}
bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m)
{
// XXX
bool valid = true;
bool hasSensor = false;
m->time = getTime(); m->time = getTime();
m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag;
m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero;
m->variant.air_quality_metrics.pm25_standard = data.pm25_standard;
m->variant.air_quality_metrics.pm100_standard = data.pm100_standard;
m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; if (scd4xSensor.hasSensor()) {
m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; valid = valid && scd4xSensor.getMetrics(m);
m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; hasSensor = true;
}
if (pmsa0031Sensor.hasSensor()) {
valid = valid && pmsa0031Sensor.getMetrics(m);
hasSensor = true;
}
LOG_INFO("(Sending): PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", return valid && hasSensor;
m->variant.air_quality_metrics.pm10_standard, m->variant.air_quality_metrics.pm25_standard,
m->variant.air_quality_metrics.pm100_standard);
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental,
m->variant.air_quality_metrics.pm100_environmental);
return true;
} }
meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply()
@ -162,6 +205,14 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
{ {
meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; meshtastic_Telemetry m = meshtastic_Telemetry_init_zero;
if (getAirQualityTelemetry(&m)) { if (getAirQualityTelemetry(&m)) {
LOG_INFO("(Sending): PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i, cO2=%i ppm",
m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard,
m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.co2);
LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i",
m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental,
m.variant.air_quality_metrics.pm100_environmental);
meshtastic_MeshPacket *p = allocDataProtobuf(m); meshtastic_MeshPacket *p = allocDataProtobuf(m);
p->to = dest; p->to = dest;
p->decoded.want_response = false; p->decoded.want_response = false;

View File

@ -4,9 +4,10 @@
#pragma once #pragma once
#include "../mesh/generated/meshtastic/telemetry.pb.h" #include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "Adafruit_PM25AQI.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "ProtobufModule.h" #include "ProtobufModule.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry> class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule<meshtastic_Telemetry>
{ {
@ -20,10 +21,15 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf
ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg)
{ {
lastMeasurementPacket = nullptr; lastMeasurementPacket = nullptr;
setIntervalFromNow(10 * 1000);
aqi = Adafruit_PM25AQI();
nodeStatusObserver.observe(&nodeStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus);
setIntervalFromNow(10 * 1000);
} }
virtual bool wantUIFrame() override;
#if !HAS_SCREEN
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
#else
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
#endif
protected: protected:
/** Called to handle a particular incoming message /** Called to handle a particular incoming message
@ -42,12 +48,12 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf
bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false);
private: private:
Adafruit_PM25AQI aqi;
PM25_AQI_Data data = {0};
bool firstTime = true; bool firstTime = true;
meshtastic_MeshPacket *lastMeasurementPacket; meshtastic_MeshPacket *lastMeasurementPacket;
uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute
uint32_t lastSentToMesh = 0; uint32_t lastSentToMesh = 0;
uint32_t lastSentToPhone = 0;
uint32_t sensor_read_error_count = 0;
}; };
#endif #endif

View File

@ -32,7 +32,6 @@
#include "Sensor/NAU7802Sensor.h" #include "Sensor/NAU7802Sensor.h"
#include "Sensor/OPT3001Sensor.h" #include "Sensor/OPT3001Sensor.h"
#include "Sensor/RCWL9620Sensor.h" #include "Sensor/RCWL9620Sensor.h"
#include "Sensor/SCD4XSensor.h"
#include "Sensor/SHT31Sensor.h" #include "Sensor/SHT31Sensor.h"
#include "Sensor/SHT4XSensor.h" #include "Sensor/SHT4XSensor.h"
#include "Sensor/SHTC3Sensor.h" #include "Sensor/SHTC3Sensor.h"
@ -57,7 +56,6 @@ AHT10Sensor aht10Sensor;
MLX90632Sensor mlx90632Sensor; MLX90632Sensor mlx90632Sensor;
DFRobotLarkSensor dfRobotLarkSensor; DFRobotLarkSensor dfRobotLarkSensor;
NAU7802Sensor nau7802Sensor; NAU7802Sensor nau7802Sensor;
SCD4XSensor scd4xSensor;
BMP3XXSensor bmp3xxSensor; BMP3XXSensor bmp3xxSensor;
#ifdef T1000X_SENSOR_EN #ifdef T1000X_SENSOR_EN
T1000xSensor t1000xSensor; T1000xSensor t1000xSensor;
@ -149,8 +147,6 @@ int32_t EnvironmentTelemetryModule::runOnce()
result = nau7802Sensor.runOnce(); result = nau7802Sensor.runOnce();
if (max17048Sensor.hasSensor()) if (max17048Sensor.hasSensor())
result = max17048Sensor.runOnce(); result = max17048Sensor.runOnce();
if (scd4xSensor.hasSensor())
result = scd4xSensor.runOnce();
#endif #endif
} }
return result; return result;
@ -247,10 +243,6 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
if (lastMeasurement.variant.environment_metrics.weight != 0) if (lastMeasurement.variant.environment_metrics.weight != 0)
display->drawString(x, y += _fontHeight(FONT_SMALL), display->drawString(x, y += _fontHeight(FONT_SMALL),
"Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg"); "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg");
if (lastMeasurement.variant.environment_metrics.co2 != 0)
display->drawString(x, y += _fontHeight(FONT_SMALL),
"CO2: " + String(lastMeasurement.variant.environment_metrics.co2) + " ppm");
} }
bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t)
@ -266,9 +258,9 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac
t->variant.environment_metrics.temperature); t->variant.environment_metrics.temperature);
LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f", sender, t->variant.environment_metrics.voltage, LOG_INFO("(Received from %s): voltage=%f, IAQ=%d, distance=%f, lux=%f", sender, t->variant.environment_metrics.voltage,
t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.lux); t->variant.environment_metrics.iaq, t->variant.environment_metrics.distance, t->variant.environment_metrics.lux);
LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg, co2=%d ppm", sender, LOG_INFO("(Received from %s): wind speed=%fm/s, direction=%d degrees, weight=%fkg", sender,
t->variant.environment_metrics.wind_speed, t->variant.environment_metrics.wind_direction, t->variant.environment_metrics.wind_speed, t->variant.environment_metrics.wind_direction,
t->variant.environment_metrics.weight, t->variant.environment_metrics.co2); t->variant.environment_metrics.weight);
#endif #endif
// release previous packet before occupying a new spot // release previous packet before occupying a new spot
@ -395,10 +387,6 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
valid = valid && max17048Sensor.getMetrics(m); valid = valid && max17048Sensor.getMetrics(m);
hasSensor = true; hasSensor = true;
} }
if (scd4xSensor.hasSensor()) {
valid = valid && scd4xSensor.getMetrics(m);
hasSensor = true;
}
#endif #endif
return valid && hasSensor; return valid && hasSensor;
@ -449,8 +437,8 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
LOG_INFO("(Sending): voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage, LOG_INFO("(Sending): voltage=%f, IAQ=%d, distance=%f, lux=%f", m.variant.environment_metrics.voltage,
m.variant.environment_metrics.iaq, m.variant.environment_metrics.distance, m.variant.environment_metrics.lux); m.variant.environment_metrics.iaq, m.variant.environment_metrics.distance, m.variant.environment_metrics.lux);
LOG_INFO("(Sending): wind speed=%fm/s, direction=%d degrees, weight=%fkg, co2=%d ppm", m.variant.environment_metrics.wind_speed, LOG_INFO("(Sending): wind speed=%fm/s, direction=%d degrees, weight=%fkg", m.variant.environment_metrics.wind_speed,
m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight, m.variant.environment_metrics.co2); m.variant.environment_metrics.wind_direction, m.variant.environment_metrics.weight);
sensor_read_error_count = 0; sensor_read_error_count = 0;
@ -594,11 +582,6 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule
if (result != AdminMessageHandleResult::NOT_HANDLED) if (result != AdminMessageHandleResult::NOT_HANDLED)
return result; return result;
} }
if (scd4xSensor.hasSensor()) {
result = scd4xSensor.handleAdminMessage(mp, request, response);
if (result != AdminMessageHandleResult::NOT_HANDLED)
return result;
}
return result; return result;
} }

View File

@ -0,0 +1,47 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "PMSA0031Sensor.h"
#include "TelemetrySensor.h"
#include <Adafruit_PM25AQI.h>
PMSA0031Sensor::PMSA0031Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA0031") {}
int32_t PMSA0031Sensor::runOnce()
{
LOG_INFO("Init sensor: %s\n", sensorName);
if (!hasSensor()) {
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
}
aqi = Adafruit_PM25AQI();
delay(10000);
aqi.begin_I2C();
/* nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address;
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second =
i2cScanner->fetchI2CBus(found.address););*/
return initI2CSensor();
}
void PMSA0031Sensor::setup() {}
bool PMSA0031Sensor::getMetrics(meshtastic_Telemetry *measurement)
{
uint16_t co2, error;
float temperature, humidity;
if (!aqi.read(&data)) {
LOG_WARN("Skipping send measurements. Could not read AQIn");
return false;
}
measurement->variant.air_quality_metrics.pm10_standard = data.pm10_standard;
measurement->variant.air_quality_metrics.pm25_standard = data.pm25_standard;
measurement->variant.air_quality_metrics.pm100_standard = data.pm100_standard;
measurement->variant.air_quality_metrics.pm10_environmental = data.pm10_env;
measurement->variant.air_quality_metrics.pm25_environmental = data.pm25_env;
measurement->variant.air_quality_metrics.pm100_environmental = data.pm100_env;
return true;
}
#endif

View File

@ -0,0 +1,24 @@
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#include "../mesh/generated/meshtastic/telemetry.pb.h"
#include "TelemetrySensor.h"
#include <Adafruit_PM25AQI.h>
class PMSA0031Sensor : public TelemetrySensor
{
private:
Adafruit_PM25AQI aqi;
PM25_AQI_Data data = {0};
protected:
virtual void setup() override;
public:
PMSA0031Sensor();
virtual int32_t runOnce() override;
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
};
#endif

View File

@ -28,28 +28,23 @@ int32_t SCD4XSensor::runOnce()
return initI2CSensor(); return initI2CSensor();
} }
void SCD4XSensor::setup() void SCD4XSensor::setup() {}
{
}
bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement)
{ {
uint16_t co2, error; uint16_t co2, error;
float temperature, humidity; float temperature, humidity;
error = scd4x.readMeasurement( error = scd4x.readMeasurement(co2, temperature, humidity);
co2, temperature, humidity
);
if (error || co2 == 0) { if (error || co2 == 0) {
LOG_DEBUG("Skipping invalid SCD4X measurement.\n"); LOG_DEBUG("Skipping invalid SCD4X measurement.\n");
return false; return false;
} else { } else {
measurement->variant.environment_metrics.has_temperature = true; measurement->variant.environment_metrics.has_temperature = true;
measurement->variant.environment_metrics.has_relative_humidity = true; measurement->variant.environment_metrics.has_relative_humidity = true;
measurement->variant.environment_metrics.has_co2 = true; measurement->variant.air_quality_metrics.has_co2 = true;
measurement->variant.environment_metrics.temperature = temperature; measurement->variant.environment_metrics.temperature = temperature;
measurement->variant.environment_metrics.relative_humidity = humidity; measurement->variant.environment_metrics.relative_humidity = humidity;
measurement->variant.environment_metrics.co2 = co2; measurement->variant.air_quality_metrics.co2 = co2;
return true; return true;
} }
} }