Zigbee co2 sensor (#24337)

* Add CO2 sensor support

* Add CO2 sensor support merge with actual development

* Merge bug fix
This commit is contained in:
Alexey Pavlov
2026-01-13 10:21:03 +01:00
committed by GitHub
parent d6174aaa7e
commit 7fac724ec2
7 changed files with 708 additions and 677 deletions
@@ -462,24 +462,28 @@ public:
pressure(-0x8000),
humidity(0xFFFF),
th_setpoint(0xFF),
temperature_target(-0x8000)
temperature_target(-0x8000),
CO2(-1)
{}
inline bool validTemperature(void) const { return -0x8000 != temperature; }
inline bool validPressure(void) const { return -0x8000 != pressure; }
inline bool validHumidity(void) const { return 0xFFFF != humidity; }
inline bool validCO2(void) const { return -1 != CO2; }
inline bool validThSetpoint(void) const { return 0xFF != th_setpoint; }
inline bool validTempTarget(void) const { return -0x8000 != temperature_target; }
inline int16_t getTemperature(void) const { return temperature; }
inline int16_t getPressure(void) const { return pressure; }
inline int16_t getPressure(void) const { return pressure; }
inline uint16_t getHumidity(void) const { return humidity; }
inline float getCO2(void) const { return CO2; }
inline uint8_t getThSetpoint(void) const { return th_setpoint; }
inline int16_t getTempTarget(void) const { return temperature_target; }
inline void setTemperature(int16_t _temperature) { temperature = _temperature; }
inline void setPressure(int16_t _pressure) { pressure = _pressure; }
inline void setHumidity(uint16_t _humidity) { humidity = _humidity; }
inline void setCO2(float _CO2) { CO2 = _CO2; }
inline void setThSetpoint(uint8_t _th_setpoint) { th_setpoint = _th_setpoint; }
inline void setTempTarget(int16_t _temperature_target){ temperature_target = _temperature_target; }
@@ -492,6 +496,7 @@ public:
// thermostat
uint8_t th_setpoint; // percentage of heat/cool in percent
int16_t temperature_target; // settings for the temparature
float CO2; // CO2 in ppm, 0..10000, -1 if unknown
};
/*********************************************************************************************\
File diff suppressed because it is too large Load Diff
@@ -190,8 +190,8 @@ enum Cx_cluster_short {
Cx0010, Cx0011, Cx0012, Cx0013, Cx0014, Cx001A, Cx0020, Cx0021,
Cx0100, Cx0101, Cx0102, Cx0201, Cx0202, Cx0203, Cx0204,
Cx0300, Cx0301, Cx0400, Cx0401, Cx0402, Cx0403,
Cx0404, Cx0405, Cx0406, Cx0500, Cx0702, Cx0B01, Cx0B04, Cx0B05,
CxEF00, CxFC01, CxFC40, CxFCC0, CxFCCC,
Cx0404, Cx0405, Cx0406, Cx040D, Cx0500, Cx0702, Cx0B01, Cx0B04,
Cx0B05, CxEF00, CxFC01, CxFC40, CxFCC0, CxFCCC,
};
const uint16_t Cx_cluster[] PROGMEM = {
@@ -200,8 +200,8 @@ const uint16_t Cx_cluster[] PROGMEM = {
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x001A, 0x0020, 0x0021,
0x0100, 0x0101, 0x0102, 0x0201, 0x0202, 0x0203, 0x0204,
0x0300, 0x0301, 0x0400, 0x0401, 0x0402, 0x0403,
0x0404, 0x0405, 0x0406, 0x0500, 0x0702, 0x0B01, 0x0B04, 0x0B05,
0xEF00, 0xFC01, 0xFC40, 0xFCC0, 0xFCCC,
0x0404, 0x0405, 0x0406, 0x040D, 0x0500, 0x0702, 0x0B01, 0x0B04,
0x0B05, 0xEF00, 0xFC01, 0xFC40, 0xFCC0, 0xFCCC,
};
uint16_t CxToCluster(uint8_t cx) {
@@ -223,20 +223,20 @@ uint8_t ClusterToCx(uint16_t cluster) {
// Multiplier contains only a limited set of values, so instead of storing the value
// we store an index in a table, and reduce it to 4 bits
enum Cm_multiplier_nibble {
Cm0 = 0, Cm1 = 1, Cm2, Cm5, Cm10, Cm100,
Cm0 = 0, Cm1 = 1, Cm2, Cm5, Cm10, Cm100, Cm1000000,
// negative numbers
Cm_2, Cm_5, Cm_10, Cm_100
};
const int8_t Cm_multiplier[] PROGMEM = {
0, 1, 2, 5, 10, 100,
-2, -5, -10, -100,
const int32_t Cm_multiplier[] PROGMEM = {
0, 1, 2, 5, 10, 100, 1000000,
-2, -5, -10, -100
};
int8_t CmToMultiplier(uint8_t cm) {
int32_t CmToMultiplier(uint8_t cm) {
cm = cm & 0x0F; // get only low nibble
if (cm < nitems(Cm_multiplier)) {
return pgm_read_byte(&Cm_multiplier[cm]);
return pgm_read_dword(&Cm_multiplier[cm]);
}
return 1;
}
@@ -1109,6 +1109,13 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = {
{ Zuint8, Cx0406, 0x0012, Z_(PIRUnoccupiedToOccupiedThreshold), Cm1, 0 },
// { Zunk, Cx0406, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values
// CO2 Concentration Measurement cluster
{ Zsingle, Cx040D, 0x0000, Z_(CO2), Cm1000000 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Thermo, CO2) }, // CO2 (ppm)
{ Zsingle, Cx040D, 0x0001, Z_(CO2MinMeasuredValue), Cm1000000, 0 }, //
{ Zsingle, Cx040D, 0x0002, Z_(CO2MaxMeasuredValue), Cm1000000, 0 }, //
{ Zsingle, Cx040D, 0x0003, Z_(CO2Tolerance), Cm1000000, 0 }, //
// { Zunk, Cx040D, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values
// IAS Cluster (Intruder Alarm System)
{ Zenum8, Cx0500, 0x0000, Z_(ZoneState), Cm1, 0 }, // Occupancy (map8)
{ Zenum16, Cx0500, 0x0001, Z_(ZoneType), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Alarm, zone_type) }, // Zone type for sensor
@@ -46,9 +46,9 @@ class Z_attribute_match Z_findAttributeMatcherByName(uint16_t shortaddr, const c
matched_attr.cluster = CxToCluster(pgm_read_byte(&converter->cluster_short));
matched_attr.attribute = pgm_read_word(&converter->attribute);
matched_attr.name = (Z_strings + pgm_read_word(&converter->name_offset));
int8_t multiplier8 = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
if (multiplier8 > 1) { matched_attr.multiplier = multiplier8; }
if (multiplier8 < 0) { matched_attr.divider = -multiplier8; }
int32_t multiplier32 = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
if (multiplier32 > 1) { matched_attr.multiplier = multiplier32; }
if (multiplier32 < 0) { matched_attr.divider = -multiplier32; }
matched_attr.zigbee_type = pgm_read_byte(&converter->type);
uint8_t conv_mapping = pgm_read_byte(&converter->mapping);
matched_attr.map_type = (Z_Data_Type) ((conv_mapping & 0xF0)>>4);
@@ -79,9 +79,9 @@ class Z_attribute_match Z_findAttributeMatcherById(uint16_t shortaddr, uint16_t
matched_attr.cluster = cluster;
matched_attr.attribute = attr_id;
matched_attr.name = (Z_strings + pgm_read_word(&converter->name_offset));
int8_t multiplier8 = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
if (multiplier8 > 1) { matched_attr.multiplier = multiplier8; }
if (multiplier8 < 0) { matched_attr.divider = -multiplier8; }
int32_t multiplier32 = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
if (multiplier32 > 1) { matched_attr.multiplier = multiplier32; }
if (multiplier32 < 0) { matched_attr.divider = -multiplier32; }
matched_attr.zigbee_type = pgm_read_byte(&converter->type);
uint8_t conv_mapping = pgm_read_byte(&converter->mapping);
matched_attr.map_type = (Z_Data_Type) ((conv_mapping & 0xF0)>>4);
@@ -1531,6 +1531,8 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib
uint8_t * attr_address = ((uint8_t*)&data) + sizeof(Z_Data) + matched_attr.map_offset;
uint32_t uval32 = attr.getUInt(); // call converter to uint only once
int32_t ival32 = attr.getInt(); // call converter to int only once
float fval = attr.getFloat(); // call converter to int only once
// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_ZIGBEE "Mapping type=%d offset=%d zigbee_type=%02X value=%d\n"), (uint8_t) matched_attr.map_type, matched_attr.map_offset, matched_attr.zigbee_type, ival32);
switch (ccccaaaa) {
case 0xEF000202:
@@ -1551,6 +1553,7 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib
case Zint8: *(int8_t*)attr_address = ival32; break;
case Zint16: *(int16_t*)attr_address = ival32; break;
case Zint32: *(int32_t*)attr_address = ival32; break;
case Zsingle: *(float*)attr_address = fval; break;
}
if (Z_Data_Set::updateData(data)) {
zigbee_devices.dirty();
@@ -1723,11 +1726,11 @@ void Z_Data::toAttributes(Z_attribute_list & attr_list) const {
const Z_AttributeConverter *converter = &Z_PostProcess[i];
uint8_t conv_export = pgm_read_byte(&converter->multiplier_idx) & Z_EXPORT_DATA;
uint8_t conv_mapping = pgm_read_byte(&converter->mapping);
uint16_t multiplier = 1;
uint16_t divider = 1;
int8_t multiplier8 = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
if (multiplier8 > 1) { multiplier = multiplier8; }
if (multiplier8 < 0) { divider = -multiplier8; }
uint32_t multiplier = 1;
uint32_t divider = 1;
int32_t multiplier32 = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
if (multiplier32 > 1) { multiplier = multiplier32; }
if (multiplier32 < 0) { divider = -multiplier32; }
Z_Data_Type map_type = (Z_Data_Type) ((conv_mapping & 0xF0)>>4);
uint8_t map_offset = (conv_mapping & 0x0F);
@@ -335,11 +335,11 @@ ZBM(ZBS_AF_REGISTERF2, Z_SREQ | Z_AF, AF_REGISTER, 0xF2 /* endpoint */, Z_B0(Z_P
// Z_AF:register profile:104, ep:01 - main clusters for router or device
ZBM(ZBS_AF_REGISTER_ALL, Z_SREQ | Z_AF, AF_REGISTER, 0x01 /* endpoint */, Z_B0(Z_PROF_HA), Z_B1(Z_PROF_HA), // 24000401050000000000
0x05, 0x00 /* AppDeviceId */, 0x00 /* AppDevVer */, 0x00 /* LatencyReq */,
0x0E /* AppNumInClusters */, // actually all clusters will be received
0x0F /* AppNumInClusters */, // actually all clusters will be received
0x00,0x00, 0x04,0x00, 0x05,0x00, 0x06,0x00, // 0x0000, 0x0004, 0x0005, 0x0006
0x07,0x00, 0x08,0x00, 0x0A,0x00, 0x02,0x01, // 0x0007, 0x0008, 0x000A, 0X0102
0x00,0x03, 0x00,0x04, 0x02,0x04, 0x03,0x04, // 0x0300, 0x0400, 0x0402, 0x0403
0x05,0x04, 0x06,0x04, // 0x0405, 0x0406
0x05,0x04, 0x06,0x04, 0x0D,0x04, // 0x0405, 0x0406, 0x040D
0x00 /* AppNumOutClusters */)
// Z_ZDO:mgmtPermitJoinReq
@@ -724,16 +724,16 @@ ZBR(ZBR_SET_OK2, EZSP_setConfigurationValue, 0x00 /*high*/, 0x00 /*ok*/)
// Add Endpoints
ZBM(ZBS_ADD_ENDPOINT1, EZSP_addEndpoint, 0x00 /*high*/, 0x01 /*ep*/, Z_B0(Z_PROF_HA), Z_B1(Z_PROF_HA),
0x05, 0x00 /* AppDeviceId */, 0x00 /* AppDevVer */,
0x0F /* inputClusterCount */, // actually all clusters will be received
0X0F /* outputClusterCount */, // 02000104010500000F0F0000040005000600070008000A00020100030004020403040504060400050000040005000600070008000A0002010003000402040304050406040005
0x10 /* inputClusterCount */, // actually all clusters will be received
0X10 /* outputClusterCount */, // 02000104010500000F0F0000040005000600070008000A00020100030004020403040504060400050000040005000600070008000A0002010003000402040304050406040005
0x00,0x00, 0x04,0x00, 0x05,0x00, 0x06,0x00, // 0x0000, 0x0004, 0x0005, 0x0006
0x07,0x00, 0x08,0x00, 0x0A,0x00, 0x02,0x01, // 0x0007, 0x0008, 0x000A, 0X0102
0x00,0x03, 0x00,0x04, 0x02,0x04, 0x03,0x04, // 0x0300, 0x0400, 0x0402, 0x0403
0x05,0x04, 0x06,0x04, 0x00,0x05, // 0x0405, 0x0406, 0x0500
0x05,0x04, 0x06,0x04, 0x0D,0x04, 0x00,0x05, // 0x0405, 0x0406, 0x040D, 0x0500
0x00,0x00, 0x04,0x00, 0x05,0x00, 0x06,0x00, // 0x0000, 0x0004, 0x0005, 0x0006
0x07,0x00, 0x08,0x00, 0x0A,0x00, 0x02,0x01, // 0x0007, 0x0008, 0x000A, 0X0102
0x00,0x03, 0x00,0x04, 0x02,0x04, 0x03,0x04, // 0x0300, 0x0400, 0x0402, 0x0403
0x05,0x04, 0x06,0x04, 0x00,0x05, // 0x0405, 0x0406, 0x0500
0x05,0x04, 0x06,0x04, 0x0D,0x04, 0x00,0x05, // 0x0405, 0x0406, 0x040D, 0x0500
)
ZBM(ZBS_ADD_ENDPOINTB, EZSP_addEndpoint, 0x00 /*high*/, 0x0B /*ep*/, Z_B0(Z_PROF_HA), Z_B1(Z_PROF_HA),
0x05, 0x00 /* AppDeviceId */, 0x00 /* AppDevVer */,
@@ -665,7 +665,7 @@ int32_t Z_ReceiveActiveEp(int32_t res, const SBuffer &buf) {
// list of clusters that need bindings
const uint8_t Z_bindings[] PROGMEM = {
Cx0001, Cx0006, Cx0008, Cx0102, Cx0201, Cx0300,
Cx0400, Cx0402, Cx0403, Cx0405, Cx0406,
Cx0400, Cx0402, Cx0403, Cx0405, Cx0406, Cx040D,
Cx0500, Cx0B04,
};
@@ -1564,6 +1564,7 @@ const Z_autoAttributeReporting_t Z_autoAttributeReporting[] PROGMEM = {
{ 0x0403, 0x0000, 30, USE_ZIGBEE_MAXTIME_SENSOR, USE_ZIGBEE_AUTOBIND_PRESSURE }, // Pressure (1 hPa)
{ 0x0405, 0x0000, 30, USE_ZIGBEE_MAXTIME_SENSOR, USE_ZIGBEE_AUTOBIND_HUMIDITY }, // Humidity (1 %)
{ 0x0406, 0x0000, 10, USE_ZIGBEE_MAXTIME_SENSOR, 0 }, // Occupancy
{ 0x040D, 0x0000, 60, USE_ZIGBEE_MAXTIME_SENSOR, 0.00005 }, // CO2
{ 0x0500, 0x0002, 1, USE_ZIGBEE_MAXTIME_SENSOR, 0 }, // ZoneStatus
};
@@ -2285,42 +2285,49 @@ void ZigbeeShow(void)
WSContentSend_PD(msg[ZB_WEB_END_STATUS], dhm);
// Sensors
const Z_Data_Thermo & thermo = device.data.find<Z_Data_Thermo>();
for (uint8_t i = 0; i < endpoints_max; i++) {
if (0 == device.endpoints[i]) continue;
uint8_t endpoint = device.endpoints[i];
// Sensors
const Z_Data_Thermo & thermo = device.data.find<Z_Data_Thermo>(endpoint);
if (&thermo != &z_data_unk) {
bool validTemp = thermo.validTemperature();
bool validTempTarget = thermo.validTempTarget();
bool validThSetpoint = thermo.validThSetpoint();
bool validHumidity = thermo.validHumidity();
bool validPressure = thermo.validPressure();
if (&thermo != &z_data_unk) {
bool validTemp = thermo.validTemperature();
bool validTempTarget = thermo.validTempTarget();
bool validThSetpoint = thermo.validThSetpoint();
bool validHumidity = thermo.validHumidity();
bool validPressure = thermo.validPressure();
bool validCO2 = thermo.validCO2();
if (validTemp || validTempTarget || validThSetpoint || validHumidity || validPressure) {
WSContentSend_P(msg[ZB_WEB_LINE_START]);
if (validTemp) {
char buf[12];
dtostrf(thermo.getTemperature() / 100.0f, 3, 1, buf);
WSContentSend_PD(PSTR(" &#x2600;&#xFE0F; %s°C"), buf);
}
if (validTempTarget) {
char buf[12];
dtostrf(thermo.getTempTarget() / 100.0f, 3, 1, buf);
WSContentSend_PD(PSTR(" &#127919; %s°C"), buf);
}
if (validThSetpoint) {
WSContentSend_PD(PSTR(" &#9881;&#65039; %d%%"), thermo.getThSetpoint());
}
if (validHumidity) {
WSContentSend_P(PSTR(" &#x1F4A7; %d%%"), (uint16_t)(thermo.getHumidity() / 100.0f + 0.5f));
}
if (validPressure) {
WSContentSend_P(PSTR(" &#x26C5; %d hPa"), thermo.getPressure());
}
if (validTemp || validTempTarget || validThSetpoint || validHumidity || validPressure || validCO2) {
WSContentSend_P(msg[ZB_WEB_LINE_START]);
if (validTemp) {
char buf[12];
dtostrf(thermo.getTemperature() / 100.0f, 3, 1, buf);
WSContentSend_PD(PSTR(" &#x2600;&#xFE0F; %s°C"), buf);
}
if (validTempTarget) {
char buf[12];
dtostrf(thermo.getTempTarget() / 100.0f, 3, 1, buf);
WSContentSend_PD(PSTR(" &#127919; %s°C"), buf);
}
if (validThSetpoint) {
WSContentSend_PD(PSTR(" &#9881;&#65039; %d%%"), thermo.getThSetpoint());
}
if (validHumidity) {
WSContentSend_P(PSTR(" &#x1F4A7; %d%%"), (uint16_t)(thermo.getHumidity() / 100.0f + 0.5f));
}
if (validPressure) {
WSContentSend_P(PSTR(" &#x26C5; %d hPa"), thermo.getPressure());
}
if (validCO2) {
WSContentSend_P(PSTR(" &#x1FAE7; %.0f ppm"), 1000000*thermo.getCO2());
}
WSContentSend_P(PSTR("{e}"));
WSContentSend_P(PSTR("{e}"));
}
}
}
// Light, switches and plugs
const Z_Data_OnOff & onoff = device.data.find<Z_Data_OnOff>();
bool onoff_display = (&onoff != &z_data_unk) ? onoff.validPower() : false;