Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Aqara TVOC Air Quality Monitor (VOCKQJK11LM) #5213

Merged
merged 2 commits into from
Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ bool DeRestPluginPrivate::sendConfigureReportingRequest(BindingTask &bt)
modelId == QLatin1String("TS0202") || // Tuya sensor
modelId == QLatin1String("3AFE14010402000D") || // Konke presence sensor
modelId == QLatin1String("3AFE28010402000D") || // Konke presence sensor
modelId == QLatin1String("lumi.airmonitor.acn01") || // Xiaomi Aqara TVOC Air Quality Monitor
modelId.startsWith(QLatin1String("GZ-PIR02")) || // Sercomm motion sensor
modelId.startsWith(QLatin1String("SZ-WTD02N_CAR")) || // Sercomm water sensor
modelId.startsWith(QLatin1String("3300")) || // Centralite contatc sensor
Expand Down Expand Up @@ -2958,6 +2959,7 @@ bool DeRestPluginPrivate::checkSensorBindingsForAttributeReporting(Sensor *senso
sensor->modelId().startsWith(QLatin1String("lumi.plug.maeu01")) ||
sensor->modelId().startsWith(QLatin1String("lumi.sen_ill.mgl01")) ||
sensor->modelId().startsWith(QLatin1String("lumi.switch.b1naus01")) ||
sensor->modelId() == QLatin1String("lumi.airmonitor.acn01") ||
sensor->modelId() == QLatin1String("lumi.sensor_magnet.agl02") ||
sensor->modelId() == QLatin1String("lumi.motion.agl04") ||
sensor->modelId() == QLatin1String("lumi.flood.agl02") ||
Expand Down
45 changes: 41 additions & 4 deletions de_web_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ static const SupportedDevice supportedDevices[] = {
{ VENDOR_XIAOMI, "lumi.remote.b686opcn01", xiaomiMacPrefix }, // Xiaomi Aqara Opple WXCJKG13LM
{ VENDOR_XIAOMI, "lumi.sen_ill.mgl01", xiaomiMacPrefix }, // Xiaomi ZB3.0 light sensor
{ VENDOR_XIAOMI2, "lumi.sen_ill.mgl01", lumiMacPrefix }, // Mi light detection sensor GZCGQ01LM
{ VENDOR_XIAOMI, "lumi.airmonitor.acn01", lumiMacPrefix}, // Xiaomi Aqara TVOC Air Quality Monitor VOCKQJK11LM
{ VENDOR_XIAOMI, "lumi.plug", xiaomiMacPrefix }, // Xiaomi smart plugs (router)
{ VENDOR_XIAOMI, "lumi.switch.b1naus01", xiaomiMacPrefix }, // Xiaomi Aqara ZB3.0 Smart Wall Switch Single Rocker WS-USC03
// { VENDOR_XIAOMI, "lumi.curtain", jennicMacPrefix}, // Xiaomi curtain controller (router) - exposed only as light
Expand Down Expand Up @@ -5409,6 +5410,7 @@ void DeRestPluginPrivate::addSensorNode(const deCONZ::Node *node, const deCONZ::
fpWaterSensor.inClusters.push_back(ci->id());
fpDoorLockSensor.inClusters.push_back(ci->id());
fpAncillaryControlSensor.inClusters.push_back(ci->id());
fpAirQualitySensor.inClusters.push_back(ci->id());
}
break;

Expand Down Expand Up @@ -5786,6 +5788,10 @@ void DeRestPluginPrivate::addSensorNode(const deCONZ::Node *node, const deCONZ::
{
fpConsumptionSensor.inClusters.push_back(ci->id());
}
else if (modelId == QLatin1String("lumi.airmonitor.acn01"))
{
fpAirQualitySensor.inClusters.push_back(ci->id());
}
}
break;

Expand Down Expand Up @@ -6585,8 +6591,9 @@ void DeRestPluginPrivate::addSensorNode(const deCONZ::Node *node, const deCONZ::
}

// ZHAAirQuality
if (fpAirQualitySensor.hasInCluster(DEVELCO_AIR_QUALITY_CLUSTER_ID) // Develco specific -> VOC Management
|| fpAirQualitySensor.hasInCluster(BOSCH_AIR_QUALITY_CLUSTER_ID)) // Bosch Air quality sensor
if (fpAirQualitySensor.hasInCluster(DEVELCO_AIR_QUALITY_CLUSTER_ID) || // Develco specific -> VOC Management
fpAirQualitySensor.hasInCluster(ANALOG_INPUT_CLUSTER_ID) || // Xiaomi Aqara TVOC Air Quality Monitor
fpAirQualitySensor.hasInCluster(BOSCH_AIR_QUALITY_CLUSTER_ID)) // Bosch Air quality sensor
{
fpAirQualitySensor.endpoint = i->endpoint();
fpAirQualitySensor.deviceId = i->deviceId();
Expand Down Expand Up @@ -7296,9 +7303,14 @@ void DeRestPluginPrivate::addSensorNode(const deCONZ::Node *node, const SensorFi
item = sensorNode.addItem(DataTypeUInt16, RConfigPending);
item->setValue(item->toNumber() | R_PENDING_WRITE_POLL_CHECKIN_INTERVAL | R_PENDING_SET_LONG_POLL_INTERVAL);
}
else if (sensorNode.fingerPrint().hasInCluster(ANALOG_INPUT_CLUSTER_ID))
{
clusterId = ANALOG_INPUT_CLUSTER_ID;
}

if (modelId == QLatin1String("AQSZB-110") // Develco air quality sensor
|| (node->nodeDescriptor().manufacturerCode() == VENDOR_BOSCH2 && modelId == QLatin1String("AIR"))) // Bosch air quality sensor
if (modelId == QLatin1String("AQSZB-110") || // Develco air quality sensor
modelId == QLatin1String("lumi.airmonitor.acn01") || // Xiaomi Aqara TVOC Air Quality Monitor
(node->nodeDescriptor().manufacturerCode() == VENDOR_BOSCH2 && modelId == QLatin1String("AIR"))) // Bosch air quality sensor
{
item = sensorNode.addItem(DataTypeString, RStateAirQuality);
item = sensorNode.addItem(DataTypeUInt16, RStateAirQualityPpb);
Expand Down Expand Up @@ -9020,6 +9032,31 @@ void DeRestPluginPrivate::updateSensorNode(const deCONZ::NodeEvent &event)
updateSensorEtag(&*i);
}
}
else if (i->modelId() == QLatin1String("lumi.airmonitor.acn01"))
{
quint32 levelPpb = static_cast<quint32>(ia->numericValue().real);
ResourceItem *item = i->item(RStateAirQualityPpb);

if (item && item->toNumber() != levelPpb)
{
item->setValue(levelPpb);
QString airQuality = getAirQualityString(levelPpb);
ResourceItem *item = i->item(RStateAirQuality);

if (item && item->toString() != airQuality)
{
item->setValue(airQuality);
enqueueEvent(Event(RSensors, RStateAirQuality, i->id(), item));
}

i->updateStateTimestamp();
i->setNeedSaveDatabase(true);
enqueueEvent(Event(RSensors, RStateAirQualityPpb, i->id(), item));
enqueueEvent(Event(RSensors, RStateLastUpdated, i->id()));
}

updateSensorEtag(&*i);
}
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions utils/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,20 @@ bool copyString(char *dst, size_t dstSize, const char *src, ssize_t srcSize)
return true;
}

QString getAirQualityString(quint32 levelPpb)
{
QString airquality;

if (levelPpb <= 65) { airquality = QLatin1String("excellent"); }
if (levelPpb > 65 && levelPpb <= 220) { airquality = QLatin1String("good"); }
if (levelPpb > 220 && levelPpb <= 660) { airquality = QLatin1String("moderate"); }
if (levelPpb > 660 && levelPpb <= 2200) { airquality = QLatin1String("poor"); }
if (levelPpb > 2200 && levelPpb <= 5500) { airquality = QLatin1String("unhealthy"); }
if (levelPpb > 5500 ) { airquality = QLatin1String("out of scale"); }

return airquality;
}

quint8 calculateBatteryPercentageRemaining(const quint8 batteryVoltage, const float vmin, const float vmax)
{
float batteryPercentage = batteryVoltage;
Expand Down
1 change: 1 addition & 0 deletions utils/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ int indexOf(QLatin1String haystack, QLatin1String needle);
bool contains(QLatin1String haystack, QLatin1String needle);
RestData verifyRestData(const ResourceItemDescriptor &rid, const QVariant &val);
bool isSameAddress(const deCONZ::Address &a, const deCONZ::Address &b);
QString getAirQualityString(quint32 levelPpb);
quint8 calculateBatteryPercentageRemaining(const quint8 batteryVoltage, const float vmin, const float vmax);

inline bool isValid(const KeyMap &entry) { return entry.key.size() != 0; }
Expand Down