Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
local st_utils = require "st.utils"
local capabilities = require "st.capabilities"
local aqs_fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields"
local aqs_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils"

local AirQualitySensorAttributeHandlers = {}

Expand All @@ -24,19 +25,16 @@ end

function AirQualitySensorAttributeHandlers.measured_value_factory(capability_name, attribute, target_unit)
return function(driver, device, ib, response)
if ib.data.value then
local reporting_unit = device:get_field(capability_name.."_unit") or aqs_fields.unit_default[capability_name]
local conversion_function = aqs_fields.conversion_tables[reporting_unit][target_unit]
if conversion_function then
local converted_value = conversion_function(ib.data.value)
device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = converted_value, unit = aqs_fields.unit_strings[target_unit]}))
-- handle case where device profile supports both fineDustLevel and dustLevel
if not ib.data.value then return end
local reporting_unit = device:get_field(capability_name.."_unit") or aqs_fields.unit_default[capability_name]
local converted_value = aqs_utils.convert_value_to_unit(ib.data.value, reporting_unit, target_unit, capability_name)

if converted_value then
device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = converted_value, unit = aqs_fields.unit_strings[target_unit]}))
-- handle case where device profile supports both fineDustLevel and dustLevel
if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = converted_value, unit = aqs_fields.unit_strings[target_unit]}))
end
else
device.log.info_with({hub_logs=true}, string.format("Unsupported unit conversion from %s to %s", aqs_fields.unit_strings[reporting_unit], aqs_fields.unit_strings[target_unit]))
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local log = require "log"
local version = require "version"
local utils = require "st.utils"
local st_utils = require "st.utils"
local clusters = require "st.matter.clusters"
local capabilities = require "st.capabilities"

Expand Down Expand Up @@ -149,30 +150,64 @@ AirQualitySensorFields.level_strings = {
[clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous",
}

AirQualitySensorFields.conversion_tables = {
AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR = 24.45

-- measured in g/mol
AirQualitySensorFields.molecular_weights = {
[capabilities.carbonDioxideMeasurement.NAME] = 44.010,
[capabilities.nitrogenDioxideMeasurement.NAME] = 28.014,
[capabilities.ozoneMeasurement.NAME] = 48.0,
[capabilities.formaldehydeMeasurement.NAME] = 30.031,
[capabilities.veryFineDustSensor.NAME] = "N/A",
[capabilities.fineDustSensor.NAME] = "N/A",
[capabilities.dustSensor.NAME] = "N/A",
[capabilities.radonMeasurement.NAME] = 222.018,
[capabilities.tvocMeasurement.NAME] = "N/A",
}

local function is_valid_molecular_weight(molecular_weight)
if type(molecular_weight) ~= "number" or molecular_weight <= 0 then
log.warn_with({hub_logs = true}, string.format("unit conversion molecular weight (%s) is not a valid number", molecular_weight))
return false
end
return true
end

AirQualitySensorFields.unit_conversion = {
[units.PPM] = {
[units.PPM] = function(value) return utils.round(value) end,
[units.PPB] = function(value) return utils.round(value * (10^3)) end
},
[units.PPM] = function(value) return st_utils.round(value) end,
[units.PPB] = function(value) return st_utils.round(value * (10^3)) end,
[units.UGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * molecular_weight * 10^3) / AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) end
end,
[units.MGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * molecular_weight) / AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) end
end,},
[units.PPB] = {
[units.PPM] = function(value) return utils.round(value/(10^3)) end,
[units.PPB] = function(value) return utils.round(value) end
[units.PPM] = function(value) return st_utils.round(value/(10^3)) end,
[units.PPB] = function(value) return st_utils.round(value) end,
},
[units.PPT] = {
[units.PPM] = function(value) return utils.round(value/(10^6)) end
[units.PPM] = function(value) return st_utils.round(value/(10^6)) end
},
[units.MGM3] = {
[units.UGM3] = function(value) return utils.round(value * (10^3)) end
[units.UGM3] = function(value) return st_utils.round(value * (10^3)) end,
[units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end
end,
},
[units.UGM3] = {
[units.UGM3] = function(value) return utils.round(value) end
[units.UGM3] = function(value) return st_utils.round(value) end,
[units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end
end,
},
[units.NGM3] = {
[units.UGM3] = function(value) return utils.round(value/(10^3)) end
[units.UGM3] = function(value) return st_utils.round(value/(10^3)) end
},
[units.BQM3] = {
[units.PCIL] = function(value) return utils.round(value/37) end
}
[units.PCIL] = function(value) return st_utils.round(value/37) end
},
}

return AirQualitySensorFields
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-- Copyright 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local log = require "log"
local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils"
Expand Down Expand Up @@ -77,6 +78,18 @@ function AirQualitySensorUtils.set_supported_health_concern_values(device)
end
end

function AirQualitySensorUtils.convert_value_to_unit(value, from_unit, to_unit, capability_name)
local conversion_function = fields.unit_conversion[from_unit] and fields.unit_conversion[from_unit][to_unit]
if not conversion_function then
log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", fields.unit_strings[from_unit], fields.unit_strings[to_unit]))
return
elseif type(value) ~= "number" then
log.info_with( {hub_logs = true} , string.format("unit conversion value (%s) is not a number", value))
return
end
return conversion_function(value, fields.molecular_weights[capability_name])
end

--- Deeply compare two values.
--- Handles metatables. Can optionally ignore cycle checking and/or function differences.
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,23 +494,15 @@ end

function AttributeHandlers.concentration_measured_value_factory(capability_name, attribute, target_unit)
return function(driver, device, ib, response)
local reporting_unit = device:get_field(capability_name.."_unit")
if not ib.data.value then return end
local reporting_unit = device:get_field(capability_name.."_unit") or fields.unit_default[capability_name]
local converted_value = thermostat_utils.convert_value_to_unit(ib.data.value, reporting_unit, target_unit, capability_name)

if not reporting_unit then
reporting_unit = fields.unit_default[capability_name]
device:set_field(capability_name.."_unit", reporting_unit, {persist = true})
end

local value = nil
if reporting_unit then
value = thermostat_utils.unit_conversion(ib.data.value, reporting_unit, target_unit, capability_name)
end

if value then
device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = fields.unit_strings[target_unit]}))
if converted_value then
device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = converted_value, unit = fields.unit_strings[target_unit]}))
-- handle case where device profile supports both fineDustLevel and dustLevel
if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = fields.unit_strings[target_unit]}))
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = converted_value, unit = fields.unit_strings[target_unit]}))
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local log = require "log"
local version = require "version"
local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
Expand Down Expand Up @@ -205,13 +206,24 @@ ThermostatFields.molecular_weights = {
[capabilities.tvocMeasurement.NAME] = "N/A",
}

ThermostatFields.conversion_tables = {
local function is_valid_molecular_weight(molecular_weight)
if type(molecular_weight) ~= "number" or molecular_weight <= 0 then
log.warn_with({hub_logs = true}, string.format("unit conversion molecular weight (%s) is not a valid number", molecular_weight))
return false
end
return true
end

ThermostatFields.unit_conversion = {
[units.PPM] = {
[units.PPM] = function(value) return st_utils.round(value) end,
[units.PPB] = function(value) return st_utils.round(value * (10^3)) end,
[units.UGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight * 10^3) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end,
[units.MGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end,
},
[units.UGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * molecular_weight * 10^3) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end
end,
[units.MGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * molecular_weight) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end
end,},
[units.PPB] = {
[units.PPM] = function(value) return st_utils.round(value/(10^3)) end,
[units.PPB] = function(value) return st_utils.round(value) end,
Expand All @@ -221,11 +233,15 @@ ThermostatFields.conversion_tables = {
},
[units.MGM3] = {
[units.UGM3] = function(value) return st_utils.round(value * (10^3)) end,
[units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end,
[units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end
end,
},
[units.UGM3] = {
[units.UGM3] = function(value) return st_utils.round(value) end,
[units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end,
[units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then
return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end
end,
},
[units.NGM3] = {
[units.UGM3] = function(value) return st_utils.round(value/(10^3)) end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,15 @@ function ThermostatUtils.get_device_type(device)
return main_device_type
end

function ThermostatUtils.unit_conversion(value, from_unit, to_unit, capability_name)
local conversion_function = fields.conversion_tables[from_unit] and fields.conversion_tables[from_unit][to_unit] or nil
function ThermostatUtils.convert_value_to_unit(value, from_unit, to_unit, capability_name)
local conversion_function = fields.unit_conversion[from_unit] and fields.unit_conversion[from_unit][to_unit]
if not conversion_function then
log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", fields.unit_strings[from_unit], fields.unit_strings[to_unit]))
return
end

if not value then
log.info_with( {hub_logs = true} , "unit conversion value is nil")
elseif type(value) ~= "number" then
log.info_with( {hub_logs = true} , string.format("unit conversion value (%s) is not a number", value))
return
end

return conversion_function(value, fields.molecular_weights[capability_name])
end

Expand Down
Loading