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
0 commit comments