diff --git a/drivers/SmartThings/zigbee-air-quality-detector/config.yml b/drivers/SmartThings/zigbee-air-quality-detector/config.yml new file mode 100755 index 0000000000..30e9dceaff --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/config.yml @@ -0,0 +1,6 @@ +name: 'Zigbee Air Quality Detector' +packageKey: 'zigbee-air-quality-detector' +permissions: + zigbee: {} +description: "SmartThings driver for Zigbee air quality detector" +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml b/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml new file mode 100755 index 0000000000..b20da987e8 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml @@ -0,0 +1,6 @@ +zigbeeManufacturer: + - id: "MultiIR/PMT1006S-SGM-ZTN" + deviceLabel: MultiIR Air Quality Detector + manufacturer: MultiIR + model: PMT1006S-SGM-ZTN + deviceProfileName: air-quality-detector-MultiIR diff --git a/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml b/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml new file mode 100755 index 0000000000..7369e1158a --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml @@ -0,0 +1,38 @@ +name: air-quality-detector-MultiIR +components: + - id: main + capabilities: + - id: airQualityHealthConcern + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: carbonDioxideMeasurement + version: 1 + - id: carbonDioxideHealthConcern + version: 1 + - id: fineDustSensor + version: 1 + - id: fineDustHealthConcern + version: 1 + - id: veryFineDustSensor + version: 1 + - id: veryFineDustHealthConcern + version: 1 + - id: dustSensor + version: 1 + - id: dustHealthConcern + version: 1 + - id: formaldehydeMeasurement + version: 1 + - id: tvocMeasurement + version: 1 + - id: tvocHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirQualityDetector diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua new file mode 100755 index 0000000000..8e8f117f49 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua @@ -0,0 +1,72 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local data_types = require "st.zigbee.data_types" + +local custom_clusters = { + carbonDioxide = { + id = 0xFCC3, + mfg_specific_code = 0x1235, + attributes = { + measured_value = { + id = 0x0000, + value_type = data_types.Uint16, + } + } + }, + particulate_matter = { + id = 0xFCC1, + mfg_specific_code = 0x1235, + attributes = { + pm2_5_MeasuredValue = { + id = 0x0000, + value_type = data_types.Uint16, + }, + pm1_0_MeasuredValue = { + id = 0x0001, + value_type = data_types.Uint16, + }, + pm10_MeasuredValue = { + id = 0x0002, + value_type = data_types.Uint16, + } + } + }, + unhealthy_gas = { + id = 0xFCC2, + mfg_specific_code = 0x1235, + attributes = { + CH2O_MeasuredValue = { + id = 0x0000, + value_type = data_types.SinglePrecisionFloat, + }, + tvoc_MeasuredValue = { + id = 0x0001, + value_type = data_types.SinglePrecisionFloat, + } + } + }, + AQI = { + id = 0xFCC5, + mfg_specific_code = 0x1235, + attributes = { + AQI_value = { + id = 0x0000, + value_type = data_types.Uint16, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua new file mode 100755 index 0000000000..3574c06e59 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua @@ -0,0 +1,152 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local custom_clusters = require "MultiIR/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" + +local RelativeHumidity = clusters.RelativeHumidity +local TemperatureMeasurement = clusters.TemperatureMeasurement + +local MultiIR_SENSOR_FINGERPRINTS = { + { mfr = "MultiIR", model = "PMT1006S-SGM-ZTN" }--This is not a sleep end device +} + +local function can_handle_MultiIR_sensor(opts, driver, device) + for _, fingerprint in ipairs(MultiIR_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local function do_refresh(driver, device) + device:send(RelativeHumidity.attributes.MeasuredValue:read(device):to_endpoint(0x01)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x01)) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm2_5_MeasuredValue) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm1_0_MeasuredValue) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm10_MeasuredValue) + send_read_attr_request(device, custom_clusters.unhealthy_gas, custom_clusters.unhealthy_gas.attributes.CH2O_MeasuredValue) + send_read_attr_request(device, custom_clusters.unhealthy_gas, custom_clusters.unhealthy_gas.attributes.tvoc_MeasuredValue) + send_read_attr_request(device, custom_clusters.carbonDioxide, custom_clusters.carbonDioxide.attributes.measured_value) + send_read_attr_request(device, custom_clusters.AQI, custom_clusters.AQI.attributes.AQI_value) +end + +local function airQualityHealthConcern_attr_handler(driver, device, value, zb_rx) + local airQuality_level = "good" + if value.value >= 51 then + airQuality_level = "moderate" + end + if value.value >= 101 then + airQuality_level = "slightlyUnhealthy" + end + if value.value >= 151 then + airQuality_level = "unhealthy" + end + if value.value >= 201 then + airQuality_level = "veryUnhealthy" + end + if value.value >= 301 then + airQuality_level = "hazardous" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.airQualityHealthConcern.airQualityHealthConcern({value = airQuality_level})) +end + +local function carbonDioxide_attr_handler(driver, device, value, zb_rx) + local level = "unhealthy" + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.carbonDioxideMeasurement.carbonDioxide({value = value.value, unit = "ppm"})) + if value.value <= 1500 then + level = "good" + elseif value.value >= 1501 and value.value <= 2500 then + level = "moderate" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = level})) +end + +local function particulate_matter_attr_handler(cap,Concern,good,bad) + return function(driver, device, value, zb_rx) + local level = "unhealthy" + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, cap({value = value.value})) + if value.value <= good then + level = "good" + elseif bad > 0 and value.value > good and value.value < bad then + level = "moderate" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, Concern({value = level})) + end +end + +local function CH2O_attr_handler(driver, device, value, zb_rx) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.formaldehydeMeasurement.formaldehydeLevel({value = value.value, unit = "mg/m^3"})) +end + +local function tvoc_attr_handler(driver, device, value, zb_rx) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.tvocMeasurement.tvocLevel({value = value.value, unit = "ug/m3"})) + local level = "unhealthy" + if value.value < 600.0 then + level = "good" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.tvocHealthConcern.tvocHealthConcern({value = level})) +end + +local function added_handler(self, device) + do_refresh() +end + +local MultiIR_sensor = { + NAME = "MultiIR air quality detector", + lifecycle_handlers = { + added = added_handler + }, + zigbee_handlers = { + attr = { + [custom_clusters.carbonDioxide.id] = { + [custom_clusters.carbonDioxide.attributes.measured_value.id] = carbonDioxide_attr_handler + }, + [custom_clusters.particulate_matter.id] = { + [custom_clusters.particulate_matter.attributes.pm2_5_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.fineDustSensor.fineDustLevel,capabilities.fineDustHealthConcern.fineDustHealthConcern,75,115),--75 115 is a comparative value of good moderate unhealthy, and 0 is no comparison + [custom_clusters.particulate_matter.attributes.pm1_0_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.veryFineDustSensor.veryFineDustLevel,capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern,100,0), + [custom_clusters.particulate_matter.attributes.pm10_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.dustSensor.dustLevel,capabilities.dustHealthConcern.dustHealthConcern,150,0) + }, + [custom_clusters.unhealthy_gas.id] = { + [custom_clusters.unhealthy_gas.attributes.CH2O_MeasuredValue.id] = CH2O_attr_handler, + [custom_clusters.unhealthy_gas.attributes.tvoc_MeasuredValue.id] = tvoc_attr_handler + }, + [custom_clusters.AQI.id] = { + [custom_clusters.AQI.attributes.AQI_value.id] = airQualityHealthConcern_attr_handler + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + can_handle = can_handle_MultiIR_sensor +} + +return MultiIR_sensor diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua new file mode 100755 index 0000000000..993ba2a96d --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua @@ -0,0 +1,41 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local ZigbeeDriver = require "st.zigbee" +local capabilities = require "st.capabilities" +local defaults = require "st.zigbee.defaults" + +local zigbee_air_quality_detector_template = { + supported_capabilities = { + capabilities.airQualityHealthConcern, + capabilities.temperatureMeasurement, + capabilities.relativeHumidityMeasurement, + capabilities.carbonDioxideMeasurement, + capabilities.carbonDioxideHealthConcern, + capabilities.fineDustSensor, + capabilities.fineDustHealthConcern, + capabilities.veryFineDustSensor, + capabilities.veryFineDustHealthConcern, + capabilities.dustSensor, + capabilities.dustHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.tvocMeasurement, + capabilities.tvocHealthConcern + }, + sub_drivers = { require("MultiIR") } +} + +defaults.register_for_default_handlers(zigbee_air_quality_detector_template, zigbee_air_quality_detector_template.supported_capabilities) +local zigbee_air_quality_detector_driver = ZigbeeDriver("zigbee-air-quality-detector", zigbee_air_quality_detector_template) +zigbee_air_quality_detector_driver:run() diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua new file mode 100755 index 0000000000..883a3dc5a5 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua @@ -0,0 +1,230 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local SinglePrecisionFloat = require "st.zigbee.data_types.SinglePrecisionFloat" + +local profile_def = t_utils.get_profile_definition("air-quality-detector-MultiIR.yml") +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( +{ + label = "air quality detector", + profile = profile_def, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "MultiIR", + model = "PMT1006S-SGM-ZTN", + server_clusters = { 0x0000, 0x0402,0x0405,0xFCC1, 0xFCC2,0xFCC3,0xFCC5} + } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "capability - refresh", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } }) + local read_RelativeHumidity_messge = clusters.RelativeHumidity.attributes.MeasuredValue:read(mock_device) + local read_TemperatureMeasurement_messge = clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + local read_pm2_5_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0000, MFG_CODE) + local read_pm1_0_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0001, MFG_CODE) + local read_pm10_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0002, MFG_CODE) + local read_ch2o_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC2, 0x0000, MFG_CODE) + local read_tvoc_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC2, 0x0001, MFG_CODE) + local read_carbonDioxide_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC3, 0x0000, MFG_CODE) + local read_AQI_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC5, 0x0000, MFG_CODE) + + test.socket.zigbee:__expect_send({mock_device.id, read_RelativeHumidity_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_TemperatureMeasurement_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm2_5_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm1_0_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm10_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_ch2o_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_tvoc_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_carbonDioxide_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_AQI_messge}) + end +) + +test.register_message_test( + "Relative humidity reports should generate correct messages", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.RelativeHumidity.attributes.MeasuredValue:build_test_attr_report(mock_device, 40*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 40 })) + } + } +) + +test.register_message_test( + "Temperature reports should generate correct messages", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + } + } +) + +test.register_coroutine_test( + "Device reported carbonDioxide and driver emit carbonDioxide and carbonDioxideHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 1400 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC3, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideMeasurement.carbonDioxide({value = 1400, unit = "ppm"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "good"}))) + end +) + +test.register_coroutine_test( + "Device reported pm2.5 and driver emit pm2.5 and fineDustHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 74 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustSensor.fineDustLevel({value = 74 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustHealthConcern.fineDustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported pm1.0 and driver emit pm1.0 and veryFineDustHealthConcern", + function() + local attr_report_data = { + { 0x0001, data_types.Uint16.ID, 69 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustSensor.veryFineDustLevel({value = 69 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported pm10 and driver emit pm10 and dustHealthConcern", + function() + local attr_report_data = { + { 0x0002, data_types.Uint16.ID, 69 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustSensor.dustLevel({value = 69 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustHealthConcern.dustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported ch2o and driver emit ch2o", + function() + local attr_report_data = { + { 0x0000, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 9, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.formaldehydeMeasurement.formaldehydeLevel({value = 1000.0, unit = "mg/m^3"}))) + end +) + +test.register_coroutine_test( + "Device reported tvoc and driver emit tvoc", + function() + local attr_report_data = { + { 0x0001, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 9, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocMeasurement.tvocLevel({value = 1000.0, unit = "ug/m3"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocHealthConcern.tvocHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "Device reported AQI and driver emit airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 50 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "good"}))) + end +) + +test.run_registered_tests() diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 6165c3759f..3dba49d7b7 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -99,3 +99,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "ThirdReality Smart Watering Kit",树实智能浇灌套装 "Zemismart ZM24A Smart Curtain", Zemismart ZM24A 智能窗帘 "Greentown Lock(SG20)",绿城门锁(SG20) +"MultiIR Air Quality Detector",MultiIR空气质量检测仪