Skip to content

Commit 2994e5e

Browse files
WWSTCERT-8756 Add support to frient air quality sensor (#2439)
* added support to frient air quality sensor * delete device:add_monitored_attribute from the driver * remove unused variable * change profile * removed unused health concern value * removed log statements
1 parent c178696 commit 2994e5e

File tree

6 files changed

+525
-2
lines changed

6 files changed

+525
-2
lines changed

drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ zigbeeManufacturer:
5858
manufacturer: HEIMAN
5959
model: HT-EF-3.0
6060
deviceProfileName: humidity-temp-battery
61+
- id: frient/AQSZB-110
62+
deviceLabel: Air Quality Sensor
63+
manufacturer: frient A/S
64+
model: AQSZB-110
65+
deviceProfileName: frient-airquality-humidity-temperature-battery
6166
- id: frient/HMSZB-110
6267
deviceLabel: frient Humidity Sensor
6368
manufacturer: frient A/S
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: frient-airquality-humidity-temperature-battery
2+
components:
3+
- id: main
4+
capabilities:
5+
- id: airQualitySensor
6+
version: 1
7+
- id: tvocMeasurement
8+
version: 1
9+
- id: tvocHealthConcern
10+
version: 1
11+
config:
12+
values:
13+
- key: "tvocHealthConcern.value"
14+
enabledValues:
15+
- good
16+
- moderate
17+
- slightlyUnhealthy
18+
- unhealthy
19+
- veryUnhealthy
20+
- id: relativeHumidityMeasurement
21+
version: 1
22+
- id: temperatureMeasurement
23+
version: 1
24+
- id: battery
25+
version: 1
26+
- id: firmwareUpdate
27+
version: 1
28+
- id: refresh
29+
version: 1
30+
preferences:
31+
- preferenceId: humidityOffset
32+
explicit: true
33+
- title: "Humidity Sensitivity (%)"
34+
name: humiditySensitivity
35+
description: "Minimum change in humidity level to report"
36+
required: false
37+
preferenceType: number
38+
definition:
39+
minimum: 1
40+
maximum: 50
41+
default: 3
42+
- preferenceId: tempOffset
43+
explicit: true
44+
- title: "Temperature Sensitivity (°)"
45+
name: temperatureSensitivity
46+
description: "Minimum change in temperature to report"
47+
required: false
48+
preferenceType: number
49+
definition:
50+
minimum: 0.1
51+
maximum: 2.0
52+
default: 1.0

drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ local devices = {
2323
FRIENT_HUMIDITY_TEMP_SENSOR = {
2424
FINGERPRINTS = {
2525
{ mfr = "frient A/S", model = "HMSZB-110" },
26-
{ mfr = "frient A/S", model = "HMSZB-120" }
26+
{ mfr = "frient A/S", model = "HMSZB-120" },
27+
{ mfr = "frient A/S", model = "AQSZB-110" }
2728
},
2829
CONFIGURATION = {
2930
{
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
local capabilities = require "st.capabilities"
2+
local util = require "st.utils"
3+
local data_types = require "st.zigbee.data_types"
4+
local zcl_clusters = require "st.zigbee.zcl.clusters"
5+
local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement
6+
local HumidityMeasurement = zcl_clusters.RelativeHumidity
7+
local PowerConfiguration = zcl_clusters.PowerConfiguration
8+
local device_management = require "st.zigbee.device_management"
9+
local cluster_base = require "st.zigbee.cluster_base"
10+
local battery_defaults = require "st.zigbee.defaults.battery_defaults"
11+
local configurationMap = require "configurations"
12+
13+
local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = {
14+
{ mfr = "frient A/S", model = "AQSZB-110", subdriver = "airquality" }
15+
}
16+
17+
local function can_handle_frient(opts, driver, device, ...)
18+
for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do
19+
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "airquality" then
20+
return true
21+
end
22+
end
23+
return false
24+
end
25+
26+
local Frient_VOCMeasurement = {
27+
ID = 0xFC03,
28+
ManufacturerSpecificCode = 0x1015,
29+
attributes = {
30+
MeasuredValue = { ID = 0x0000, base_type = data_types.Uint16 },
31+
MinMeasuredValue = { ID = 0x0001, base_type = data_types.Uint16 },
32+
MaxMeasuredValue = { ID = 0x0002, base_type = data_types.Uint16 },
33+
Resolution = { ID = 0x0003, base_type = data_types.Uint16 },
34+
},
35+
}
36+
37+
Frient_VOCMeasurement.attributes.MeasuredValue._cluster = Frient_VOCMeasurement
38+
Frient_VOCMeasurement.attributes.MinMeasuredValue._cluster = Frient_VOCMeasurement
39+
Frient_VOCMeasurement.attributes.MaxMeasuredValue._cluster = Frient_VOCMeasurement
40+
Frient_VOCMeasurement.attributes.Resolution._cluster = Frient_VOCMeasurement
41+
42+
local MAX_VOC_REPORTABLE_VALUE = 5500 -- Max VOC reportable value
43+
44+
--- Table to map VOC (ppb) to HealthConcern
45+
local VOC_TO_HEALTHCONCERN_MAPPING = {
46+
[2201] = "veryUnhealthy",
47+
[661] = "unhealthy",
48+
[221] = "slightlyUnhealthy",
49+
[66] = "moderate",
50+
[0] = "good",
51+
}
52+
53+
--- Map VOC (ppb) to HealthConcern
54+
local function voc_to_healthconcern(raw_voc)
55+
for voc, perc in util.rkeys(VOC_TO_HEALTHCONCERN_MAPPING) do
56+
if raw_voc >= voc then
57+
return perc
58+
end
59+
end
60+
end
61+
--- Map VOC (ppb) to CAQI
62+
local function voc_to_caqi(raw_voc)
63+
if (raw_voc > 5500) then
64+
return 100
65+
else
66+
return math.floor(raw_voc*99/5500)
67+
end
68+
end
69+
70+
-- May take around 8 minutes for the first valid VOC measurement to be reported after the device is powered on
71+
local function voc_measure_value_attr_handler(driver, device, attr_val, zb_rx)
72+
local voc_value = attr_val.value
73+
if (voc_value < 65535) then -- ignore it if it's outside the limits
74+
voc_value = util.clamp_value(voc_value, 0, MAX_VOC_REPORTABLE_VALUE)
75+
device:emit_event(capabilities.airQualitySensor.airQuality({ value = voc_to_caqi(voc_value)}))
76+
device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(voc_value)))
77+
device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = voc_value, unit = "ppb" }))
78+
end
79+
end
80+
81+
-- The device sends the value of MeasuredValue to be 0x8000, which corresponds to -327.68C, until it gets the first valid measurement. Therefore we don't emit event before the value is correct. It may take up to 4 minutes
82+
local function temperatureHandler(driver, device, attr_val, zb_rx)
83+
local temp_value = attr_val.value
84+
if (temp_value > -32768) then
85+
device:emit_event(capabilities.temperatureMeasurement.temperature({ value = temp_value / 100, unit = "C" }))
86+
end
87+
end
88+
89+
local function device_init(driver, device)
90+
battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device)
91+
local configuration = configurationMap.get_device_configuration(device)
92+
if configuration ~= nil then
93+
for _, attribute in ipairs(configuration) do
94+
device:add_configured_attribute(attribute)
95+
end
96+
end
97+
end
98+
99+
local function device_added(driver, device)
100+
device:emit_event(capabilities.airQualitySensor.airQuality(voc_to_caqi(0)))
101+
device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(0)))
102+
device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = 0, unit = "ppb" }))
103+
end
104+
105+
local function do_refresh(driver, device)
106+
for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do
107+
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
108+
device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_VOCMeasurement.ID, Frient_VOCMeasurement.attributes.MeasuredValue.ID, Frient_VOCMeasurement.ManufacturerSpecificCode):to_endpoint(0x26))
109+
device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26))
110+
device:send(HumidityMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26))
111+
device:send(PowerConfiguration.attributes.BatteryVoltage:read(device))
112+
end
113+
end
114+
end
115+
116+
local function do_configure(driver, device)
117+
device:configure()
118+
device:send(device_management.build_bind_request(device, Frient_VOCMeasurement.ID, driver.environment_info.hub_zigbee_eui, 0x26))
119+
120+
device:send(
121+
cluster_base.configure_reporting(
122+
device,
123+
data_types.ClusterId(Frient_VOCMeasurement.ID),
124+
Frient_VOCMeasurement.attributes.MeasuredValue.ID,
125+
Frient_VOCMeasurement.attributes.MeasuredValue.base_type.ID,
126+
60, 600, 10
127+
):to_endpoint(0x26)
128+
)
129+
130+
device.thread:call_with_delay(5, function()
131+
do_refresh(driver, device)
132+
end)
133+
end
134+
135+
local frient_airquality_sensor = {
136+
NAME = "frient Air Quality Sensor",
137+
lifecycle_handlers = {
138+
init = device_init,
139+
added = device_added,
140+
doConfigure = do_configure,
141+
},
142+
zigbee_handlers = {
143+
cluster = {},
144+
attr = {
145+
[Frient_VOCMeasurement.ID] = {
146+
[Frient_VOCMeasurement.attributes.MeasuredValue.ID] = voc_measure_value_attr_handler,
147+
},
148+
[TemperatureMeasurement.ID] = {
149+
[TemperatureMeasurement.attributes.MeasuredValue.ID] = temperatureHandler,
150+
},
151+
}
152+
},
153+
can_handle = can_handle_frient
154+
}
155+
156+
return frient_airquality_sensor

drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement
2020

2121
local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = {
2222
{ mfr = "frient A/S", model = "HMSZB-110" },
23-
{ mfr = "frient A/S", model = "HMSZB-120" }
23+
{ mfr = "frient A/S", model = "HMSZB-120" },
24+
{ mfr = "frient A/S", model = "AQSZB-110" }
2425
}
2526

2627
local function can_handle_frient_sensor(opts, driver, device)
@@ -73,6 +74,9 @@ local frient_sensor = {
7374
doConfigure = do_configure,
7475
infoChanged = info_changed
7576
},
77+
sub_drivers = {
78+
require("frient-sensor/air-quality")
79+
},
7680
can_handle = can_handle_frient_sensor
7781
}
7882

0 commit comments

Comments
 (0)