diff --git a/drivers/SmartThings/zwave-thermostat/fingerprints.yml b/drivers/SmartThings/zwave-thermostat/fingerprints.yml index c8f8a4ef9c..7cff40b340 100644 --- a/drivers/SmartThings/zwave-thermostat/fingerprints.yml +++ b/drivers/SmartThings/zwave-thermostat/fingerprints.yml @@ -86,6 +86,12 @@ zwaveManufacturer: manufacturerId: 0x0002 productType: 0x0005 deviceProfileName: thermostat-heating-battery + - id: "heatit/ztrm6" + deviceLabel: Heatit Z-TRM6 + manufacturerId: 0x019B + productType: 0x0030 + productId: 0x3001 + deviceProfileName: heatit-ztrm6-thermostat zwaveGeneric: - id: "GenericBatteryThermostat" deviceLabel: Z-Wave Battery Thermostat diff --git a/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-extra-temp.yml b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-extra-temp.yml new file mode 100644 index 0000000000..1362d34bdd --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-extra-temp.yml @@ -0,0 +1,384 @@ +name: heatit-ztrm6-thermostat-extra-temp +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + config: + values: + - key: "heatingSetpoint.value" + range: [5, 40] + - id: thermostatOperatingState + version: 1 + - id: thermostatMode + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat +- id: floorTemperature + capabilities: + - id: temperatureMeasurement + version: 1 + categories: + - name: Thermostat +preferences: +# Profile selection + - name: "showFloorTemp" + title: "Show floor temperature" + description: "Use if AF or A2F sensormode is selected" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hide floor temperature" + 1 : "Show floor temperature" + default: 0 +# 1 Disable Buttons + - name: "disableButtons" + title: "Enable or disable buttons" + description: "Set button status from program" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Enable" + 1 : "Disable" + default: 0 +# 2 + - name: "sensorMode" + title: "Sensor Mode" + description: "Chooses which sensor type to use" + required: false + preferenceType: enumeration + definition: + options: + 0 : "F Floor sensor" + 1 : "A Air sensor internal" + 2 : "AF Air with Floor limitation" + 3 : "A2 external sensor" + 4 : "A2F external with Floor limit" + 5 : "PWER Regulator" + default: 0 +# 3 + - name: "floorSensorType" + title: "Floor Sensor Type" + description: "NTC sensor type" + required: false + preferenceType: enumeration + definition: + options: + 0 : "10k NTC" + 1 : "12k NTC" + 2 : "15k NTC" + 3 : "22k NTC" + 4 : "33k NTC" + 5 : "47k NTC" + 6 : "6.8k NTC" + 7 : "100k NTC" + default: 0 +# 4 + - name: "aLo" + title: "Internal Min Temperature" + description: "Minimum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 5 + - name: "fLo" + title: "Floor Min Temperature" + description: "Minimum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 6 + - name: "eLo" + title: "External Min Temperature" + description: "Minimum limit for external sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 7 + - name: "aHi" + title: "Internal Max Temperature" + description: "Maximum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 8 + - name: "fHi" + title: "Floor Max Temperature" + description: "Maximum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 9 + - name: "eHi" + title: "External Max Temperature" + description: "Maximum limit for external sensorin tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 10 + - name: "roomSensorCalibration" + title: "Internal sensor calibration" + description: "Calibrates internal sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 11 + - name: "floorSensorCalibration" + title: "Floor sensor calibration" + description: "Calibrates floor sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 12 + - name: "externSensorCalibration" + title: "External sensor calibration" + description: "Calibrates external sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 13 + - name: "regulationMode" + title: "Regulation Mode" + description: "Choose between regulation modes PWM and Hysteresis." + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hysteresis" + 1 : "PWM regulation" + default: 0 +# 14 + - name: "tempHysteresis" + title: "Temp control Hysteresis (diF)" + description: "0.3 to 3.0 Celsius in tenths" + required: false + preferenceType: integer + definition: + minimum: 3 + maximum: 30 + default: 5 +# 15 + - name: "tempDisplay" + title: "Temperature display" + description: "Selects which temperature shown on display" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Setpoint temperature" + 1 : "Measured temperature" + default: 0 +# 16 + - name: "displayBrightnessActive" + title: "Display brightness in active state" + description: "Configures the brightness of the display in active state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 10 +# 17 + - name: "displayBrightnessDimmed" + title: "Display brightness in dimmed state" + description: "Configures the brightness of the display in dimmed state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 5 +# 18 + - name: "tempReportInterval" + title: "Temperature report interval" + description: "How often temperature reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 19 + - name: "tempReportHysteresis" + title: "Temperature report hysteresis" + description: "Difference in temperature before a new report is sent 0.1 to 10 C" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 100 + default: 10 +# 20 + - name: "meterReportInterval" + title: "Meter report interval" + description: "How often meter reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 21 + - name: "actionAfterError" + title: "Action after error" + description: "Reaction when error has turned OFF relay." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 0 +# 22 + - name: "comfortSetpoint" + title: "Comfort mode setpoint" + description: "Normal value for heating in tenths of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 210 +# 23 + - name: "coolingSetpoint" + title: "Cooling mode setpoint" + description: "Setpoint when termostat in Cooling mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 24 + - name: "ecoSetpoint" + title: "ECO mode setpoint" + description: "Setpoint when termostat in ECO mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 25 + - name: "regulatorLevel" + title: "Power level for Regulator" + description: "Setting for regulator in percent 1 = 10% (Default 2 = 20%)" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 2 +# 26 + - name: "updateInterval" + title: "Thermostat update interval" + description: "How often setpoint and on/off is sent to associated devices" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 43200 +# 27 Operation mode (Off, Heating, Energy Save Heating) + - name: "operationMode" + title: "Thermostat operation mode" + description: "Current thermostat operation mode" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Off" + 1 : "Heat" + 2 : "Cool" + 3 : "Energy Save Heat" + default: 1 +# 28 + - name: "openWindowDetection" + title: "Open window detection" + description: "Enable or disable open window detection" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Disable (default)" + 1 : "Enable" + default: 0 +# 29 + - name: "loadSize" + title: "Size of load" + description: "Load using a contactor 1-99 (* 100 W). 0=measured value" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 99 + default: 0 +# Associations 302 + - name: "assocGroup2OnOff" + title: "Association group 2, On Off Control" + description: "Binary Switch Set association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 303 + - name: "assocGroup3Setpoint" + title: "Association group 3,Setpoint)" + description: "Thermostat Setpoint association group. Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 304 + - name: "assocGroup4Mode" + title: "Association group 4, Thermostat Mode" + description: "Thermostat Mode association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" diff --git a/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-regulator-extra-temp.yml b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-regulator-extra-temp.yml new file mode 100644 index 0000000000..683e7bdf85 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-regulator-extra-temp.yml @@ -0,0 +1,378 @@ +name: heatit-ztrm6-thermostat-regulator-extra-temp +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatOperatingState + version: 1 + - id: thermostatMode + version: 1 + - id: level + version: 1 + config: + values: + - key: "level.value" + range: [10, 100] + step: 10 + - key: "setLevel" + range: [10, 100] + step: 10 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat +- id: floorTemperature + capabilities: + - id: temperatureMeasurement + version: 1 + categories: + - name: Thermostat +preferences: +# Profile selection + - name: "showFloorTemp" + title: "Show floor temperature" + description: "Use if AF or A2F sensormode is selected" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hide floor temperature" + 1 : "Show floor temperature" + default: 0 +# 1 Disable Buttons + - name: "disableButtons" + title: "Enable or disable buttons" + description: "Set button status from program" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Enable" + 1 : "Disable" + default: 0 +# 2 + - name: "sensorMode" + title: "Sensor Mode" + description: "Chooses which sensor type to use" + required: false + preferenceType: enumeration + definition: + options: + 0 : "F Floor sensor" + 1 : "A Air sensor internal" + 2 : "AF Air with Floor limitation" + 3 : "A2 external sensor" + 4 : "A2F external with Floor limit" + 5 : "PWER Regulator" + default: 1 +# 3 + - name: "floorSensorType" + title: "Floor Sensor Type" + description: "NTC sensor type" + required: false + preferenceType: enumeration + definition: + options: + 0 : "10k NTC" + 1 : "12k NTC" + 2 : "15k NTC" + 3 : "22k NTC" + 4 : "33k NTC" + 5 : "47k NTC" + 6 : "6.8k NTC" + 7 : "100k NTC" + default: 0 +# 4 + - name: "aLo" + title: "Internal Min Temperature" + description: "Minimum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 5 + - name: "fLo" + title: "Floor Min Temperature" + description: "Minimum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 6 + - name: "eLo" + title: "External Min Temperature" + description: "Minimum limit for external sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 7 + - name: "aHi" + title: "Internal Max Temperature" + description: "Maximum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 8 + - name: "fHi" + title: "Floor Max Temperature" + description: "Maximum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 9 + - name: "eHi" + title: "External Max Temperature" + description: "Maximum limit for external sensorin tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 10 + - name: "roomSensorCalibration" + title: "Internal sensor calibration" + description: "Calibrates internal sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 11 + - name: "floorSensorCalibration" + title: "Floor sensor calibration" + description: "Calibrates floor sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 12 + - name: "externSensorCalibration" + title: "External sensor calibration" + description: "Calibrates external sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 13 + - name: "regulationMode" + title: "Regulation Mode" + description: "Choose between regulation modes PWM and Hysteresis." + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hysteresis" + 1 : "PWM regulation" + default: 0 +# 14 + - name: "tempHysteresis" + title: "Temp control Hysteresis (diF)" + description: "0.3 to 3.0 Celsius in tenths" + required: false + preferenceType: integer + definition: + minimum: 3 + maximum: 30 + default: 5 +# 15 + - name: "tempDisplay" + title: "Temperature display" + description: "Selects which temperature shown on display" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Setpoint temperature" + 1 : "Measured temperature" + default: 0 +# 16 + - name: "displayBrightnessActive" + title: "Display brightness in active state" + description: "Configures the brightness of the display in active state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 10 +# 17 + - name: "displayBrightnessDimmed" + title: "Display brightness in dimmed state" + description: "Configures the brightness of the display in dimmed state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 5 +# 18 + - name: "tempReportInterval" + title: "Temperature report interval" + description: "How often temperature reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 19 + - name: "tempReportHysteresis" + title: "Temperature report hysteresis" + description: "Difference in temperature before a new report is sent 0.1 to 10 C" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 100 + default: 10 +# 20 + - name: "meterReportInterval" + title: "Meter report interval" + description: "How often meter reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 21 + - name: "actionAfterError" + title: "Action after error" + description: "Reaction when error has turned OFF relay." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 0 +# 22 + - name: "comfortSetpoint" + title: "Comfort mode setpoint" + description: "Normal value for heating in tenths of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 210 +# 23 + - name: "coolingSetpoint" + title: "Cooling mode setpoint" + description: "Setpoint when termostat in Cooling mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 24 + - name: "ecoSetpoint" + title: "ECO mode setpoint" + description: "Setpoint when termostat in ECO mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 26 + - name: "updateInterval" + title: "Thermostat update interval" + description: "How often setpoint and on/off is sent to associated devices" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 43200 +# 27 Operation mode (Off, Heating, Energy Save Heating) + - name: "operationMode" + title: "Thermostat operation mode" + description: "Current thermostat operation mode" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Off" + 1 : "Heat" + 2 : "Cool" + 3 : "Energy Save Heat" + default: 1 +# 28 + - name: "openWindowDetection" + title: "Open window detection" + description: "Enable or disable open window detection" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Disable (default)" + 1 : "Enable" + default: 0 +# 29 + - name: "loadSize" + title: "Size of load" + description: "Load using a contactor 1-99 (* 100 W). 0=measured value" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 99 + default: 0 +# Associations 302 + - name: "assocGroup2OnOff" + title: "Association group 2, On Off Control" + description: "Binary Switch Set association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 303 + - name: "assocGroup3Setpoint" + title: "Association group 3,Setpoint)" + description: "Thermostat Setpoint association group. Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 304 + - name: "assocGroup4Mode" + title: "Association group 4, Thermostat Mode" + description: "Thermostat Mode association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" diff --git a/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-regulator.yml b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-regulator.yml new file mode 100644 index 0000000000..497c0be2dd --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat-regulator.yml @@ -0,0 +1,372 @@ +name: heatit-ztrm6-thermostat-regulator +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatOperatingState + version: 1 + - id: thermostatMode + version: 1 + - id: level + version: 1 + config: + values: + - key: "level.value" + range: [10, 100] + step: 10 + - key: "setLevel" + range: [10, 100] + step: 10 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat +preferences: +# Profile selection + - name: "showFloorTemp" + title: "Show floor temperature" + description: "Use if AF or A2F sensormode is selected" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hide floor temperature" + 1 : "Show floor temperature" + default: 0 +# 1 Disable Buttons + - name: "disableButtons" + title: "Enable or disable buttons" + description: "Set button status from program" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Enable" + 1 : "Disable" + default: 0 +# 2 + - name: "sensorMode" + title: "Sensor Mode" + description: "Chooses which sensor type to use" + required: false + preferenceType: enumeration + definition: + options: + 0 : "F Floor sensor" + 1 : "A Air sensor internal" + 2 : "AF Air with Floor limitation" + 3 : "A2 external sensor" + 4 : "A2F external with Floor limit" + 5 : "PWER Regulator" + default: 1 +# 3 + - name: "floorSensorType" + title: "Floor Sensor Type" + description: "NTC sensor type" + required: false + preferenceType: enumeration + definition: + options: + 0 : "10k NTC" + 1 : "12k NTC" + 2 : "15k NTC" + 3 : "22k NTC" + 4 : "33k NTC" + 5 : "47k NTC" + 6 : "6.8k NTC" + 7 : "100k NTC" + default: 0 +# 4 + - name: "aLo" + title: "Internal Min Temperature" + description: "Minimum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 5 + - name: "fLo" + title: "Floor Min Temperature" + description: "Minimum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 6 + - name: "eLo" + title: "External Min Temperature" + description: "Minimum limit for external sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 7 + - name: "aHi" + title: "Internal Max Temperature" + description: "Maximum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 8 + - name: "fHi" + title: "Floor Max Temperature" + description: "Maximum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 9 + - name: "eHi" + title: "External Max Temperature" + description: "Maximum limit for external sensorin tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 10 + - name: "roomSensorCalibration" + title: "Internal sensor calibration" + description: "Calibrates internal sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 11 + - name: "floorSensorCalibration" + title: "Floor sensor calibration" + description: "Calibrates floor sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 12 + - name: "externSensorCalibration" + title: "External sensor calibration" + description: "Calibrates external sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 13 + - name: "regulationMode" + title: "Regulation Mode" + description: "Choose between regulation modes PWM and Hysteresis." + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hysteresis" + 1 : "PWM regulation" + default: 0 +# 14 + - name: "tempHysteresis" + title: "Temp control Hysteresis (diF)" + description: "0.3 to 3.0 Celsius in tenths" + required: false + preferenceType: integer + definition: + minimum: 3 + maximum: 30 + default: 5 +# 15 + - name: "tempDisplay" + title: "Temperature display" + description: "Selects which temperature shown on display" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Setpoint temperature" + 1 : "Measured temperature" + default: 0 +# 16 + - name: "displayBrightnessActive" + title: "Display brightness in active state" + description: "Configures the brightness of the display in active state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 10 +# 17 + - name: "displayBrightnessDimmed" + title: "Display brightness in dimmed state" + description: "Configures the brightness of the display in dimmed state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 5 +# 18 + - name: "tempReportInterval" + title: "Temperature report interval" + description: "How often temperature reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 19 + - name: "tempReportHysteresis" + title: "Temperature report hysteresis" + description: "Difference in temperature before a new report is sent 0.1 to 10 C" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 100 + default: 10 +# 20 + - name: "meterReportInterval" + title: "Meter report interval" + description: "How often meter reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 21 + - name: "actionAfterError" + title: "Action after error" + description: "Reaction when error has turned OFF relay." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 0 +# 22 + - name: "comfortSetpoint" + title: "Comfort mode setpoint" + description: "Normal value for heating in tenths of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 210 +# 23 + - name: "coolingSetpoint" + title: "Cooling mode setpoint" + description: "Setpoint when termostat in Cooling mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 24 + - name: "ecoSetpoint" + title: "ECO mode setpoint" + description: "Setpoint when termostat in ECO mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 26 + - name: "updateInterval" + title: "Thermostat update interval" + description: "How often setpoint and on/off is sent to associated devices" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 43200 +# 27 Operation mode (Off, Heating, Energy Save Heating) + - name: "operationMode" + title: "Thermostat operation mode" + description: "Current thermostat operation mode" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Off" + 1 : "Heat" + 2 : "Cool" + 3 : "Energy Save Heat" + default: 1 +# 28 + - name: "openWindowDetection" + title: "Open window detection" + description: "Enable or disable open window detection" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Disable (default)" + 1 : "Enable" + default: 0 +# 29 + - name: "loadSize" + title: "Size of load" + description: "Load using a contactor 1-99 (* 100 W). 0=measured value" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 99 + default: 0 +# Associations 302 + - name: "assocGroup2OnOff" + title: "Association group 2, On Off Control" + description: "Binary Switch Set association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 303 + - name: "assocGroup3Setpoint" + title: "Association group 3,Setpoint)" + description: "Thermostat Setpoint association group. Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 304 + - name: "assocGroup4Mode" + title: "Association group 4, Thermostat Mode" + description: "Thermostat Mode association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" diff --git a/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat.yml b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat.yml new file mode 100644 index 0000000000..489613f7ee --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/profiles/heatit-ztrm6-thermostat.yml @@ -0,0 +1,378 @@ +name: heatit-ztrm6-thermostat +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + config: + values: + - key: "heatingSetpoint.value" + range: [5, 40] + - id: thermostatOperatingState + version: 1 + - id: thermostatMode + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat +preferences: +# Profile selection + - name: "showFloorTemp" + title: "Show floor temperature" + description: "Use if AF or A2F sensormode is selected" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hide floor temperature" + 1 : "Show floor temperature" + default: 0 +# 1 Disable Buttons + - name: "disableButtons" + title: "Enable or disable buttons" + description: "Set button status from program" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Enable" + 1 : "Disable" + default: 0 +# 2 + - name: "sensorMode" + title: "Sensor Mode" + description: "Chooses which sensor type to use" + required: false + preferenceType: enumeration + definition: + options: + 0 : "F Floor sensor" + 1 : "A Air sensor internal" + 2 : "AF Air with Floor limitation" + 3 : "A2 external sensor" + 4 : "A2F external with Floor limit" + 5 : "PWER Regulator" + default: 1 +# 3 + - name: "floorSensorType" + title: "Floor Sensor Type" + description: "NTC sensor type" + required: false + preferenceType: enumeration + definition: + options: + 0 : "10k NTC" + 1 : "12k NTC" + 2 : "15k NTC" + 3 : "22k NTC" + 4 : "33k NTC" + 5 : "47k NTC" + 6 : "6.8k NTC" + 7 : "100k NTC" + default: 0 +# 4 + - name: "aLo" + title: "Internal Min Temperature" + description: "Minimum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 5 + - name: "fLo" + title: "Floor Min Temperature" + description: "Minimum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 6 + - name: "eLo" + title: "External Min Temperature" + description: "Minimum limit for external sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 50 +# 7 + - name: "aHi" + title: "Internal Max Temperature" + description: "Maximum limit for internal sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 8 + - name: "fHi" + title: "Floor Max Temperature" + description: "Maximum limit for floor sensor in tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 9 + - name: "eHi" + title: "External Max Temperature" + description: "Maximum limit for external sensorin tenths" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 400 +# 10 + - name: "roomSensorCalibration" + title: "Internal sensor calibration" + description: "Calibrates internal sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 11 + - name: "floorSensorCalibration" + title: "Floor sensor calibration" + description: "Calibrates floor sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 12 + - name: "externSensorCalibration" + title: "External sensor calibration" + description: "Calibrates external sensor to increase accuracy in tenths" + required: false + preferenceType: integer + definition: + minimum: -60 + maximum: 60 + default: 0 +# 13 + - name: "regulationMode" + title: "Regulation Mode" + description: "Choose between regulation modes PWM and Hysteresis." + required: false + preferenceType: enumeration + definition: + options: + 0 : "Hysteresis" + 1 : "PWM regulation" + default: 0 +# 14 + - name: "tempHysteresis" + title: "Temp control Hysteresis (diF)" + description: "0.3 to 3.0 Celsius in tenths" + required: false + preferenceType: integer + definition: + minimum: 3 + maximum: 30 + default: 5 +# 15 + - name: "tempDisplay" + title: "Temperature display" + description: "Selects which temperature shown on display" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Setpoint temperature" + 1 : "Measured temperature" + default: 0 +# 16 + - name: "displayBrightnessActive" + title: "Display brightness in active state" + description: "Configures the brightness of the display in active state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 10 +# 17 + - name: "displayBrightnessDimmed" + title: "Display brightness in dimmed state" + description: "Configures the brightness of the display in dimmed state in %" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 5 +# 18 + - name: "tempReportInterval" + title: "Temperature report interval" + description: "How often temperature reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 19 + - name: "tempReportHysteresis" + title: "Temperature report hysteresis" + description: "Difference in temperature before a new report is sent 0.1 to 10 C" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 100 + default: 10 +# 20 + - name: "meterReportInterval" + title: "Meter report interval" + description: "How often meter reports are sent in seconds" + required: false + preferenceType: integer + definition: + minimum: 30 + maximum: 65535 + default: 840 +# 21 + - name: "actionAfterError" + title: "Action after error" + description: "Reaction when error has turned OFF relay." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 0 +# 22 + - name: "comfortSetpoint" + title: "Comfort mode setpoint" + description: "Normal value for heating in tenths of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 210 +# 23 + - name: "coolingSetpoint" + title: "Cooling mode setpoint" + description: "Setpoint when termostat in Cooling mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 24 + - name: "ecoSetpoint" + title: "ECO mode setpoint" + description: "Setpoint when termostat in ECO mode in tenth of degrees" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 400 + default: 180 +# 25 + - name: "regulatorLevel" + title: "Power level for Regulator" + description: "Setting for regulator in percent 1 = 10% (Default 2 = 20%)" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 10 + default: 2 +# 26 + - name: "updateInterval" + title: "Thermostat update interval" + description: "How often setpoint and on/off is sent to associated devices" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65535 + default: 43200 +# 27 Operation mode (Off, Heating, Energy Save Heating) + - name: "operationMode" + title: "Thermostat operation mode" + description: "Current thermostat operation mode" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Off" + 1 : "Heat" + 2 : "Cool" + 3 : "Energy Save Heat" + default: 1 +# 28 + - name: "openWindowDetection" + title: "Open window detection" + description: "Enable or disable open window detection" + required: false + preferenceType: enumeration + definition: + options: + 0 : "Disable (default)" + 1 : "Enable" + default: 0 +# 29 + - name: "loadSize" + title: "Size of load" + description: "Load using a contactor 1-99 (* 100 W). 0=measured value" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 99 + default: 0 +# Associations 302 + - name: "assocGroup2OnOff" + title: "Association group 2, On Off Control" + description: "Binary Switch Set association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 303 + - name: "assocGroup3Setpoint" + title: "Association group 3,Setpoint)" + description: "Thermostat Setpoint association group. Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" +# Associations 304 + - name: "assocGroup4Mode" + title: "Association group 4, Thermostat Mode" + description: "Thermostat Mode association group (On Off control). Use dots to define End Points and separate devices with semicolons. Example: 4.2;5;6.4" + required: false + preferenceType: string + definition: + stringType: text + minLength: 0 + maxLength: 35 + default: "" diff --git a/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/can_handle.lua new file mode 100644 index 0000000000..3c65ea138e --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_heatit_ztrm6(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("heatit-ztrm6.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("heatit-ztrm6") + end + end + + return false +end + +return can_handle_heatit_ztrm6 diff --git a/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/fingerprints.lua new file mode 100644 index 0000000000..162bd064d0 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local HEATIT_ZTRM6_FINGERPRINTS = { + { manufacturerId = 0x019B, productType = 0x0030, productId = 0x3001 } -- Heatit Z-TRM6 +} + +return HEATIT_ZTRM6_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/init.lua b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/init.lua new file mode 100644 index 0000000000..af31cce973 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/init.lua @@ -0,0 +1,515 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.ThermostatSetpoint +local ThermostatSetpoint = (require "st.zwave.CommandClass.ThermostatSetpoint")({version=3}) +--- @type st.zwave.CommandClass.ThermostatOperatingState +local ThermostatOperatingState = (require "st.zwave.CommandClass.ThermostatOperatingState")({version=1}) +--- @type st.zwave.CommandClass.ThermostatMode +local ThermostatMode = (require "st.zwave.CommandClass.ThermostatMode")({version=3}) +--- @type st.zwave.CommandClass.SensorMultilevel +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version=11}) +--- @type st.zwave.CommandClass.Configuration +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=1}) +--- @type st.zwave.CommandClass.MultiChannelAssociation +local MultiChannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({version=3}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({version=2}) +local utils = require "st.utils" +local constants = require "st.zwave.constants" + +local PARAMETERS = require("heatit-ztrm6.preferences") + +local CURRENT_THERMOSTAT_HEATING_MODE = "current_mode" +local CURRENT_SENSOR_CHANNEL = "current_channel" +local CURRENT_SENSOR_MODE = "current_sensor_mode" + +local function setpoint_type_for_mode(mode) + if mode == ThermostatMode.mode.ENERGY_SAVE_HEAT then + return ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING + elseif mode == ThermostatMode.mode.COOL then + return ThermostatSetpoint.setpoint_type.COOLING_1 + end + return ThermostatSetpoint.setpoint_type.HEATING_1 +end + +--- Determine the correct profile name based on sensor mode and floor-temp visibility. +--- @param sensor_mode number Configuration parameter 2 value (0-5) +--- @param show_floor_temp string|number The showFloorTemp preference value +--- @return string profile_name +local function get_profile_name(sensor_mode, show_floor_temp) + local is_regulator = sensor_mode == 5 + local show_floor = show_floor_temp == "1" or show_floor_temp == 1 + if is_regulator and show_floor then + return "heatit-ztrm6-thermostat-regulator-extra-temp" + elseif is_regulator then + return "heatit-ztrm6-thermostat-regulator" + elseif show_floor then + return "heatit-ztrm6-thermostat-extra-temp" + else + return "heatit-ztrm6-thermostat" + end +end + +--- Switch the device profile if it does not match the requested one. +--- @param device st.zwave.Device +--- @param profile_name string +local function switch_profile(device, profile_name) + if device.profile and device.profile.id == profile_name then + return + end + device:try_update_metadata({ profile = profile_name }) + device.thread:call_with_delay(2, function() + device:refresh() + end) +end + +--- Map sensor mode parameter value to the Z-Wave endpoint used for the +--- selected sensor and the component name that represents it. +--- @param sensor_value number Configuration parameter 2 value +--- @return table {channel_sensor_value, component, extra_capability} +local function get_sensor_channel(sensor_value) + -- 0 = F, 1 = A, 2 = AF, 3 = A2, 4 = A2F, 5 = Regulator + local channel_and_component + if sensor_value == 5 then + channel_and_component = { channel_sensor_value = 2, component = "roomTemperature", extra_capability = "" } + elseif sensor_value >= 3 then + channel_and_component = { channel_sensor_value = 3, component = "roomTemperatureExternal", extra_capability = "" } + elseif sensor_value >= 1 then + channel_and_component = { channel_sensor_value = 2, component = "roomTemperature", extra_capability = "" } + elseif sensor_value == 0 then + channel_and_component = { channel_sensor_value = 4, component = "floorTemperature", extra_capability = "" } + else + channel_and_component = { channel_sensor_value = 2, component = "roomTemperature", extra_capability = "" } + end + if sensor_value == 2 or sensor_value == 4 then + channel_and_component.extra_capability = "floorTemperature" + end + return channel_and_component +end + +--- Persist the sensor channel derived from configuration parameter 2 and +--- request a fresh temperature reading from the corresponding endpoint. +local function save_current_sensor_channel(device, sensor_value) + local i_sensor_value = tonumber(sensor_value) + local channel_and_component = get_sensor_channel(i_sensor_value) + device:set_field(CURRENT_SENSOR_CHANNEL, channel_and_component.channel_sensor_value, { persist = true }) + device:set_field(CURRENT_SENSOR_MODE, i_sensor_value, { persist = true }) + + -- Switch to regulator profile when PWER mode (5) is active, otherwise back to normal + local profile_name = get_profile_name(i_sensor_value, device.preferences.showFloorTemp) + switch_profile(device, profile_name) + + device:send_to_component(SensorMultilevel:Get({}), channel_and_component.component) + if i_sensor_value == 5 then + device:send(Configuration:Get({parameter_number = 25})) + end +end + +--- Return the persisted sensor channel. If unknown, request parameter 2 from +--- the device so it will be available on the next report. +local function get_current_sensor_channel(device) + local current_channel = device:get_field(CURRENT_SENSOR_CHANNEL) + if current_channel == nil then + current_channel = -1 + device:send(Configuration:Get({parameter_number = 2})) + end + return current_channel +end + +local function temperature_report_handler(self, device, cmd) + if (cmd.args.sensor_type == SensorMultilevel.sensor_type.TEMPERATURE) then + local scale = 'C' + if (cmd.args.scale == SensorMultilevel.scale.temperature.FAHRENHEIT) then scale = 'F' end + + if cmd.src_channel == get_current_sensor_channel(device) then + device:emit_event(capabilities.temperatureMeasurement.temperature({value = cmd.args.sensor_value, unit = scale})) + end + + -- Floor temperature report channel = 4. Update floorTemperature capability if it exists + if (cmd.src_channel == 4 and (device.preferences.showFloorTemp == "1" or device.preferences.showFloorTemp == 1) + and device.profile.components["floorTemperature"] ~= nil) then + device:emit_event_for_endpoint(4, capabilities.temperatureMeasurement.temperature({value = cmd.args.sensor_value, unit = scale})) + end + end +end + +local function setpoint_report_handler(self, device, cmd) + local current_thermostat_heating_mode = device:get_field(CURRENT_THERMOSTAT_HEATING_MODE) + if (current_thermostat_heating_mode == nil) then + device:send(ThermostatMode:Get({})) + elseif cmd.args.setpoint_type == setpoint_type_for_mode(current_thermostat_heating_mode) then + device:emit_event(capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = cmd.args.value, unit = "C"})) + end +end + +local function thermostat_mode_report_handler(self, device, cmd) + local mode = cmd.args.mode + local event = nil + local current_thermostat_heating_mode = device:get_field(CURRENT_THERMOSTAT_HEATING_MODE) + + if (mode == ThermostatMode.mode.OFF) then + event = capabilities.thermostatMode.thermostatMode.off() + else + if (mode == ThermostatMode.mode.HEAT) then + event = capabilities.thermostatMode.thermostatMode.heat() + current_thermostat_heating_mode = ThermostatMode.mode.HEAT + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1})) + elseif (mode == ThermostatMode.mode.ENERGY_SAVE_HEAT) then + event = capabilities.thermostatMode.thermostatMode.eco() + current_thermostat_heating_mode = ThermostatMode.mode.ENERGY_SAVE_HEAT + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING})) + elseif (mode == ThermostatMode.mode.COOL) then + event = capabilities.thermostatMode.thermostatMode.cool() + current_thermostat_heating_mode = ThermostatMode.mode.COOL + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.COOLING_1})) + end + device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, current_thermostat_heating_mode) + end + if (event ~= nil) then + device:emit_event(event) + end +end + +local function configuration_report_handler(self, device, cmd) + -- Update OperatingState + if (cmd.args.parameter_number == 2) then + -- Sensor setting + save_current_sensor_channel(device, cmd.args.configuration_value) + elseif cmd.args.parameter_number == 25 then + local value = cmd.args.configuration_value + if device:supports_capability(capabilities.level, "main") and value >= 1 and value <= 10 then + device:emit_event(capabilities.level.level(value * 10)) + end + elseif (cmd.args.parameter_number == 27) then + local modes = { + [0] = ThermostatMode.mode.OFF, + [1] = ThermostatMode.mode.HEAT, + [2] = ThermostatMode.mode.COOL, + [3] = ThermostatMode.mode.ENERGY_SAVE_HEAT + } + local mode = modes[cmd.args.configuration_value] + if mode ~= nil then + thermostat_mode_report_handler(self, device, {args = {mode = mode}}) + device:send(ThermostatOperatingState:Get({})) + end + end +end + +local function set_regulator_level(driver, device, command) + local level = command.args.level + if not device:supports_capability(capabilities.level, "main") + or type(level) ~= "number" or level < 10 or level > 100 then + return + end + + local regulator_value = math.floor((level + 5) / 10) + device:send(Configuration:Set({parameter_number = 25, size = 1, configuration_value = regulator_value})) + device:send(Configuration:Get({parameter_number = 25})) +end + +local function set_thermostat_mode(driver, device, command) + local modes = capabilities.thermostatMode.thermostatMode + local mode = command.args.mode + local modeValue = nil + if (mode == modes.off.NAME) then + modeValue = ThermostatMode.mode.OFF + elseif (mode == modes.heat.NAME) then + modeValue = ThermostatMode.mode.HEAT + elseif (mode == modes.eco.NAME) then + modeValue = ThermostatMode.mode.ENERGY_SAVE_HEAT + elseif (mode == modes.cool.NAME) then + modeValue = ThermostatMode.mode.COOL + elseif (mode == "energysaveheat" or mode == "energy save heat") then + modeValue = ThermostatMode.mode.ENERGY_SAVE_HEAT + end + + if (modeValue ~= nil) then + if modeValue ~= ThermostatMode.mode.OFF then + device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, modeValue) + end + device:send(ThermostatMode:Set({mode = modeValue})) + + local follow_up_poll = function() + device:refresh() + end + device.thread:call_with_delay(1, follow_up_poll) + end +end + +--- Z-TRM6 is a Celsius device; only convert if device explicitly reports Fahrenheit +local function convert_to_device_temp(command_temp, device_scale) + if device_scale == ThermostatSetpoint.scale.FAHRENHEIT then + command_temp = utils.c_to_f(command_temp) + end + -- Round to 1 decimal place to avoid integer overflow in Z-Wave serialization + command_temp = math.floor(command_temp * 10 + 0.5) / 10 + return command_temp +end + +local function set_setpoint_factory(setpoint_type) + return function(driver, device, cmd) + local scale = device:get_field(constants.TEMPERATURE_SCALE) + local value = convert_to_device_temp(cmd.args.setpoint, scale) + local current_mode = device:get_field(CURRENT_THERMOSTAT_HEATING_MODE) + local current_setpoint_type = current_mode and setpoint_type_for_mode(current_mode) or setpoint_type + + if current_mode == nil then + -- With no prior mode (for example after startup while OFF), use HEAT until the device reports its mode. + device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, ThermostatMode.mode.HEAT) + device:send(ThermostatMode:Get({})) + end + device:send(ThermostatSetpoint:Set({value = value, setpoint_type = current_setpoint_type, size = 2})) + + local follow_up_poll = function() + device:send(ThermostatSetpoint:Get({setpoint_type = current_setpoint_type})) + device:send(SensorMultilevel:Get({})) + end + device.thread:call_with_delay(1, follow_up_poll) + end +end + +local function do_refresh(self, device) + device:send(ThermostatMode:Get({})) + device:send(ThermostatOperatingState:Get({})) + if device:supports_capability(capabilities.level, "main") then + device:send(Configuration:Get({parameter_number = 25})) + end + + device:send_to_component(SensorMultilevel:Get({}), "roomTemperature") + device:send_to_component(SensorMultilevel:Get({}), "roomTemperatureExternal") + device:send_to_component(SensorMultilevel:Get({}), "floorTemperature") + + -- fetch meter values + device:send(Meter:Get({scale = Meter.scale.electric_meter.WATTS})) + device:send(Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS})) + + -- fetch correct setpoint + local current_thermostat_heating_mode = device:get_field(CURRENT_THERMOSTAT_HEATING_MODE) + + if current_thermostat_heating_mode == nil or current_thermostat_heating_mode == ThermostatMode.mode.HEAT then + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1})) + elseif current_thermostat_heating_mode == ThermostatMode.mode.ENERGY_SAVE_HEAT then + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING})) + elseif current_thermostat_heating_mode == ThermostatMode.mode.COOL then + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.COOLING_1})) + end +end + +local function reset_energy_meter(driver, device, command) + local component = command and command.component and command.component or "main" + -- Send Meter:Reset to the device, then re-fetch the energy value after a short delay + device:send_to_component(Meter:Reset({}), component) + device.thread:call_with_delay(1.5, function() + device:send_to_component(Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}), component) + end) + -- Optimistically emit 0 kWh so the UI updates immediately + device:emit_event(capabilities.energyMeter.energy({value = 0, unit = "kWh"})) +end + +--- Meter report handler. +--- When loadSize preference is > 0 (contactor mode), the device cannot measure +--- the external load and reports 0 W. Override the power reading with the +--- simulated load (loadSize * 100 W) when the thermostat is actively heating. +local PowerMeterDefaults = require "st.zwave.defaults.powerMeter" +local EnergyMeterDefaults = require "st.zwave.defaults.energyMeter" + +local function meter_report_handler(driver, device, cmd) + local load_size = tonumber(device.preferences.loadSize) or 0 + if load_size > 0 and cmd.args.scale == Meter.scale.electric_meter.WATTS then + local op_state = device:get_latest_state( + "main", capabilities.thermostatOperatingState.ID, + capabilities.thermostatOperatingState.thermostatOperatingState.NAME + ) + if op_state == "heating" then + device:emit_event(capabilities.powerMeter.power({value = load_size * 100, unit = "W"})) + return + elseif op_state == "idle" then + device:emit_event(capabilities.powerMeter.power({value = 0, unit = "W"})) + return + end + end + -- Fall through to default handlers for both power (W) and energy (kWh) + if cmd.args.scale == Meter.scale.electric_meter.KILOWATT_HOURS then + EnergyMeterDefaults.zwave_handlers[cc.METER][Meter.REPORT](driver, device, cmd) + else + PowerMeterDefaults.zwave_handlers[cc.METER][Meter.REPORT](driver, device, cmd) + end +end + +local function endpoint_to_component(device, endpoint) + if endpoint == 2 then + return "roomTemperature" + elseif endpoint == 3 then + return "roomTemperatureExternal" + elseif endpoint == 4 then + return "floorTemperature" + end + return "main" +end + +local function component_to_endpoint(device, component_id) + if component_id == "roomTemperature" then + return {2} + elseif component_id == "roomTemperatureExternal" then + return {3} + elseif component_id == "floorTemperature" then + return {4} + else + return {} + end +end + +local function map_components(self, device) + device:set_endpoint_to_component_fn(endpoint_to_component) + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function do_added(self, device) + -- Explicitly advertise supported modes so the platform sees "eco" instead of + -- the default "energy save heat" that the Z-Wave SupportedReport handler produces. + local supported_modes = { + capabilities.thermostatMode.thermostatMode.off.NAME, + capabilities.thermostatMode.thermostatMode.heat.NAME, + capabilities.thermostatMode.thermostatMode.cool.NAME, + capabilities.thermostatMode.thermostatMode.eco.NAME + } + device:emit_event(capabilities.thermostatMode.supportedThermostatModes(supported_modes, { visibility = { displayed = false } })) + device:send(Meter:Get({scale = Meter.scale.electric_meter.WATTS})) + device:send(Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS})) + device:refresh() +end + +local function do_init(self, device) + map_components(self, device) +end + +--- Read and use associations from string. +--- Expected format: decimal NODEID or NODEID.ENDPOINT delimited by one or more spaces +local function get_node_ids_from_string(node_id_string) + local assoc_nodes = {} + local assoc_nodes_endpoints = {} + if string.len(node_id_string) >= 1 then + for node in string.gmatch(node_id_string, "%d+%.?%d*") do + local dot_start = string.find(node, "%.") + if dot_start == nil then + table.insert(assoc_nodes, tonumber(node)) + else + table.insert(assoc_nodes_endpoints, { + multi_channel_node_id = tonumber(string.sub(node, 1, dot_start - 1)), + end_point = tonumber(string.sub(node, dot_start + 1, dot_start + 3)), + bit_address = false + }) + end + end + end + return assoc_nodes, assoc_nodes_endpoints +end + +--- Handle changes to the association group preferences (parameter numbers >= 202). +--- Group id is the last two digits of the parameter number, e.g. 302 -> group 2. +local function info_changed_associations(driver, device, parameter_number, value) + local assoc_nodes, assoc_nodes_endpoints = get_node_ids_from_string(value or "") + local group_id = parameter_number - 300 + device:send(MultiChannelAssociation:Remove({grouping_identifier = group_id, node_ids = {}})) + device:send(MultiChannelAssociation:Set({ grouping_identifier = group_id, node_ids = assoc_nodes, multi_channel_nodes = assoc_nodes_endpoints})) + device:send(MultiChannelAssociation:Get({ grouping_identifier = group_id})) +end + +--- Handle preference (Settings) changes from the SmartThings app. +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param event table +--- @param args table +local function info_changed(driver, device, event, args) + for id, value in pairs(device.preferences) do + local param = PARAMETERS[id] + if param and args.old_st_store.preferences[id] ~= value then + local new_parameter_value = tonumber(value) + if new_parameter_value == nil then -- in case the value is boolean + new_parameter_value = value and 1 or 0 + end + local parameter_number = param.parameter_number + + if parameter_number > 400 then + if parameter_number == 999 then + device:send(ThermostatSetpoint:CapabilitiesGet({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1})) + elseif parameter_number == 901 then + -- Determine profile based on both floor-temp toggle and current sensor mode + local sensor_mode = tonumber(device:get_field(CURRENT_SENSOR_MODE) or device.preferences.sensorMode or 1) + switch_profile(device, get_profile_name(sensor_mode, new_parameter_value)) + end + elseif parameter_number >= 202 then + info_changed_associations(driver, device, parameter_number, value) + else + if param.size == 2 and new_parameter_value >= 0x8000 then + -- Configuration v1 writes signed integers; the same 16 bits represent the unsigned device value. + new_parameter_value = new_parameter_value - 0x10000 + end + device:send(Configuration:Set({parameter_number = parameter_number, size = param.size, configuration_value = new_parameter_value})) + device:send(Configuration:Get({parameter_number = parameter_number})) + + if (id == "comfortSetpoint") then + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1})) + elseif id == "ecoSetpoint" then + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING})) + elseif id == "coolingSetpoint" then + device:send(ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.COOLING_1})) + elseif id == "loadSize" then + device:send(Meter:Get({scale = Meter.scale.electric_meter.WATTS})) + end + end + end + end +end + +local heatit_ztrm6_thermostat = { + NAME = "Heatit Z-TRM6 thermostat", + zwave_handlers = { + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report_handler + }, + [cc.THERMOSTAT_MODE] = { + [ThermostatMode.REPORT] = thermostat_mode_report_handler, + [ThermostatMode.SUPPORTED_REPORT] = function(driver, device, cmd) end + }, + [cc.THERMOSTAT_SETPOINT] = { + [ThermostatSetpoint.REPORT] = setpoint_report_handler, + [ThermostatSetpoint.SET] = setpoint_report_handler + }, + [cc.SENSOR_MULTILEVEL] = { + [SensorMultilevel.REPORT] = temperature_report_handler + }, + [cc.METER] = { + [Meter.REPORT] = meter_report_handler + }, + }, + capability_handlers = { + [capabilities.level.ID] = { + [capabilities.level.commands.setLevel.NAME] = set_regulator_level + }, + [capabilities.thermostatMode.ID] = { + [capabilities.thermostatMode.commands.setThermostatMode.NAME] = set_thermostat_mode, + }, + [capabilities.thermostatHeatingSetpoint.ID] = { + [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = set_setpoint_factory(ThermostatSetpoint.setpoint_type.HEATING_1) + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = reset_energy_meter + } + }, + lifecycle_handlers = { + init = do_init, + added = do_added, + infoChanged = info_changed, + }, + can_handle = require("heatit-ztrm6.can_handle") +} + +return heatit_ztrm6_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/preferences.lua b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/preferences.lua new file mode 100644 index 0000000000..547d587e20 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/heatit-ztrm6/preferences.lua @@ -0,0 +1,52 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Device preference (Z-Wave Configuration parameter) definitions for the +--- Heatit Z-TRM6 thermostat. Parameter numbers above 400 are handled as +--- special actions rather than plain Configuration:Set/Get (see init.lua), +--- and parameter numbers >= 202 are Association / MultiChannelAssociation +--- groups (last digits of the parameter number encode the group id). +local HEATIT_ZTRM6_PARAMETERS = { + disableButtons = {parameter_number = 1, size = 1}, + sensorMode = {parameter_number = 2, size = 1}, + floorSensorType = {parameter_number = 3, size = 1}, + aLo = {parameter_number = 4, size = 2}, + fLo = {parameter_number = 5, size = 2}, + eLo = {parameter_number = 6, size = 2}, + aHi = {parameter_number = 7, size = 2}, + fHi = {parameter_number = 8, size = 2}, + eHi = {parameter_number = 9, size = 2}, + + roomSensorCalibration = {parameter_number = 10, size = 1}, + floorSensorCalibration = {parameter_number = 11, size = 1}, + externSensorCalibration = {parameter_number = 12, size = 1}, + + regulationMode = {parameter_number = 13, size = 1}, + tempHysteresis = {parameter_number = 14, size = 1}, + tempDisplay = {parameter_number = 15, size = 1}, + displayBrightnessActive = {parameter_number = 16, size = 1}, + displayBrightnessDimmed = {parameter_number = 17, size = 1}, + tempReportInterval = {parameter_number = 18, size = 2}, + tempReportHysteresis = {parameter_number = 19, size = 1}, + meterReportInterval = {parameter_number = 20, size = 2}, + actionAfterError = {parameter_number = 21, size = 2}, + + comfortSetpoint = {parameter_number = 22, size = 2}, + coolingSetpoint = {parameter_number = 23, size = 2}, + ecoSetpoint = {parameter_number = 24, size = 2}, + regulatorLevel = {parameter_number = 25, size = 1}, + + updateInterval = {parameter_number = 26, size = 2}, + operationMode = {parameter_number = 27, size = 1}, + openWindowDetection = {parameter_number = 28, size = 1}, + loadSize = {parameter_number = 29, size = 1}, + + assocGroup2OnOff = {parameter_number = 302, size = 35}, + assocGroup3Setpoint = {parameter_number = 303, size = 35}, + assocGroup4Mode = {parameter_number = 304, size = 35}, + + showFloorTemp = {parameter_number = 901, size = 1}, + testFunction = {parameter_number = 999, size = 1}, +} + +return HEATIT_ZTRM6_PARAMETERS diff --git a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua index 38b6d5de87..4b323f2239 100644 --- a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua @@ -10,5 +10,6 @@ local sub_drivers = { lazy_load_if_possible("stelpro-ki-thermostat"), lazy_load_if_possible("qubino-flush-thermostat"), lazy_load_if_possible("thermostat-heating-battery"), + lazy_load_if_possible("heatit-ztrm6"), } return sub_drivers diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_heatit_ztrm6.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_heatit_ztrm6.lua new file mode 100644 index 0000000000..ad69a9cd13 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_heatit_ztrm6.lua @@ -0,0 +1,954 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw_test_utilities = require "integration_test.zwave_test_utils" +local zw = require "st.zwave" +local t_utils = require "integration_test.utils" + +--- @type st.zwave.CommandClass.ThermostatSetpoint +local ThermostatSetpoint = (require "st.zwave.CommandClass.ThermostatSetpoint")({version=3}) +--- @type st.zwave.CommandClass.ThermostatOperatingState +local ThermostatOperatingState = (require "st.zwave.CommandClass.ThermostatOperatingState")({version=1}) +--- @type st.zwave.CommandClass.ThermostatMode +local ThermostatMode = (require "st.zwave.CommandClass.ThermostatMode")({version=3}) +--- @type st.zwave.CommandClass.SensorMultilevel +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version=11}) +--- @type st.zwave.CommandClass.Configuration +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=1}) +--- @type st.zwave.CommandClass.MultiChannelAssociation +local MultiChannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({version=3}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({version=2}) + +-- supported command classes +local thermostat_endpoints = { + { + command_classes = { + {value = zw.THERMOSTAT_MODE}, + {value = zw.THERMOSTAT_SETPOINT}, + {value = zw.THERMOSTAT_OPERATING_STATE}, + {value = zw.SENSOR_MULTILEVEL}, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("heatit-ztrm6-thermostat.yml"), + zwave_endpoints = thermostat_endpoints, + zwave_manufacturer_id = 0x019B, + zwave_product_type = 0x0030, + zwave_product_id = 0x3001, + } +) + +local mock_device_extra_temp = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("heatit-ztrm6-thermostat-extra-temp.yml"), + zwave_endpoints = thermostat_endpoints, + zwave_manufacturer_id = 0x019B, + zwave_product_type = 0x0030, + zwave_product_id = 0x3001, + } +) + +local mock_device_regulator = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("heatit-ztrm6-thermostat-regulator.yml"), + zwave_endpoints = thermostat_endpoints, + zwave_manufacturer_id = 0x019B, + zwave_product_type = 0x0030, + zwave_product_id = 0x3001, + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_device_extra_temp) + test.mock_device.add_test_device(mock_device_regulator) +end +test.set_test_init_function(test_init) + +local CURRENT_THERMOSTAT_HEATING_MODE = "current_mode" +local CURRENT_SENSOR_CHANNEL = "current_channel" +local CURRENT_SENSOR_MODE = "current_sensor_mode" + +-- ----- L I F E C Y C L E T e s t s ----- + +test.register_coroutine_test( + "1.1 Lifecycle: added should be handled", + function() + mock_device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, ThermostatMode.mode.HEAT, { persist = true }) + + test.socket.device_lifecycle():__queue_receive({mock_device.id, "added"}) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes( + {"off", "heat", "cool", "eco"}, { visibility = { displayed = false } })) + ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + -- Do Refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) -- roomTemperature + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) -- roomTemperatureExternal + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) -- floorTemperature + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) ) ) + end +) + +test.register_coroutine_test( + "1.3.1 Lifecycle: info_changed", + function() + local paramNumber = 14 + local paramSize = 1 + local paramValue = 6 + local _preferences = {} + _preferences.tempHysteresis = paramValue + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = _preferences })) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, Configuration:Set({ parameter_number = paramNumber, configuration_value = paramValue, size = paramSize }) )) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, Configuration:Get({ parameter_number = paramNumber }) )) + end +) + +test.register_coroutine_test( + "Temperature report hysteresis preference writes configuration parameter 19", + function() + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ + preferences = {tempReportHysteresis = 11} + })) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, Configuration:Set({parameter_number = 19, size = 1, configuration_value = 11}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, Configuration:Get({parameter_number = 19}) + )) + end +) + +test.register_coroutine_test( + "1.3.1a Lifecycle: two-byte update interval accepts its default above 32767", + function() + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ + preferences = { updateInterval = 43200 } + })) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, Configuration:Set({parameter_number = 26, size = 2, configuration_value = -22336}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, Configuration:Get({parameter_number = 26}) + )) + end +) + +test.register_coroutine_test( + "1.3.1b Lifecycle: two-byte update interval accepts 65535", + function() + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ + preferences = { updateInterval = 65535 } + })) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, Configuration:Set({parameter_number = 26, size = 2, configuration_value = -1}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, Configuration:Get({parameter_number = 26}) + )) + end +) + +test.register_coroutine_test( + "1.3.2 Lifecycle: info_changed - Show floor temperature and back to default", + function() + local paramValue = 1 + local _preferences = {} + _preferences.showFloorTemp = paramValue + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = _preferences })) + + mock_device:expect_metadata_update({profile = "heatit-ztrm6-thermostat-extra-temp"}) + + test.wait_for_events() + test.mock_time.advance_time(2) + + -- Refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) -- roomTemperature + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) -- roomTemperatureExternal + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) -- floorTemperature + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) ) ) + end +) + +test.register_coroutine_test( + "1.3.3 Lifecycle: info_changed - Hide floor temperature", + function() + test.socket.zwave:__set_channel_ordering("relaxed") + + -- Set showFloorTemp = 1 + local paramValue = "1" + local _preferences = {} + _preferences.showFloorTemp = paramValue + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.device_lifecycle():__queue_receive(mock_device_extra_temp:generate_info_changed({ preferences = _preferences })) + + mock_device_extra_temp:expect_metadata_update({profile = "heatit-ztrm6-thermostat-extra-temp"}) + + test.wait_for_events() + test.mock_time.advance_time(2) + + -- Refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) -- roomTemperature + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) -- roomTemperatureExternal + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) -- floorTemperature + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) ) ) + + test.wait_for_events() + test.mock_time.advance_time(2) + + -- Set showFloorTemp = 0 + _preferences.showFloorTemp = 0 + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.device_lifecycle():__queue_receive(mock_device_extra_temp:generate_info_changed({ preferences = _preferences })) + + mock_device_extra_temp:expect_metadata_update({profile = "heatit-ztrm6-thermostat"}) + + test.wait_for_events() + test.mock_time.advance_time(2) + + -- Refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) -- roomTemperature + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) -- roomTemperatureExternal + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) -- floorTemperature + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device_extra_temp, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) ) ) + end +) + +-- ----- C A P A B I L I T Y T e s t s ----- + +test.register_coroutine_test( + "2.1.1 Capability: set_thermostat_mode HEAT Should send zwave ThermostatMode:Set", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "thermostatMode", command = "setThermostatMode", args = { "heat" } } }) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, ThermostatMode:Set({mode = ThermostatMode.mode.HEAT}) ) ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + -- Do refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) ) ) + end +) + +test.register_coroutine_test( + "2.1.3 Capability: set_thermostat_mode ENERGY_SAVE_HEAT Should send zwave ThermostatMode:Set", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "thermostatMode", command = "setThermostatMode", args = { "energysaveheat" } } }) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, ThermostatMode:Set({mode = ThermostatMode.mode.ENERGY_SAVE_HEAT}) ) ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + -- Do refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING}) ) ) + end +) + +test.register_coroutine_test( + "2.1.4 Capability: set_thermostat_mode COOL Should send zwave ThermostatMode:Set", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "thermostatMode", command = "setThermostatMode", args = { "cool" } } }) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, ThermostatMode:Set({mode = ThermostatMode.mode.COOL}) ) ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + -- Do refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.COOLING_1}) ) ) + end +) + +test.register_coroutine_test( + "2.2.1 Capability: set_setpoint_factory mode = HEAT Should send zwave ThermostatSetpoint:Set", + function() + mock_device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, ThermostatMode.mode.HEAT, { persist = true }) + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "thermostatHeatingSetpoint", command = "setHeatingSetpoint", args = { 21.5 } } }) + + test.socket.zwave:__expect_send( + zw_test_utilities.zwave_test_build_send_command( + mock_device, + ThermostatSetpoint:Set({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1, value = 21.5, size = 2}) + ) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utilities.zwave_test_build_send_command(mock_device, + ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) + ) + ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}) ) ) + end +) + +test.register_coroutine_test( + "2.2.2 Capability: setting a setpoint while OFF uses the last active COOL setpoint", + function() + mock_device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, ThermostatMode.mode.COOL) + test.socket.capability:__queue_receive({ + mock_device.id, {capability = "thermostatMode", command = "setThermostatMode", args = {"off"}} + }) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatMode:Set({mode = ThermostatMode.mode.OFF}) + )) + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_device.id, {capability = "thermostatHeatingSetpoint", command = "setHeatingSetpoint", args = {21.5}} + }) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatSetpoint:Set({setpoint_type = ThermostatSetpoint.setpoint_type.COOLING_1, value = 21.5, size = 2}) + )) + end +) + +test.register_coroutine_test( + "2.2.3 Capability: setting a setpoint after startup in OFF mode defaults to HEAT", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, zw_test_utilities.zwave_test_build_receive_command( + ThermostatMode:Report({mode = ThermostatMode.mode.OFF}) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.off()) + ) + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_device.id, {capability = "thermostatHeatingSetpoint", command = "setHeatingSetpoint", args = {21.5}} + }) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatMode:Get({}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatSetpoint:Set({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1, value = 21.5, size = 2}) + )) + test.wait_for_events() + + test.socket.zwave:__queue_receive({ + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(ThermostatSetpoint:Report({ + setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1, value = 21.5 + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.5, unit = "C"})) + ) + end +) + +test.register_coroutine_test( + "2.3 Capability: do_refresh", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + mock_device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, ThermostatMode.mode.HEAT, { persist = true }) + log.debug("#### Lifecycle: added: current_mode: " .. mock_device:get_field(CURRENT_THERMOSTAT_HEATING_MODE)) + + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } }) + + -- Do refresh + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatMode:Get({}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatOperatingState:Get({}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {2}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {3}}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, SensorMultilevel:Get({}, {dst_channels = {4}}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command(mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) ) ) + end +) + +-- ----- Z - W A V E R e c e i v e T e s t s ----- + +test.register_message_test( + "3.1.1 Thermostat mode HEAT reports should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, + zw_test_utilities.zwave_test_build_receive_command(ThermostatMode:Report({ mode = ThermostatMode.mode.HEAT })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode({ value = "heat" })) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utilities.zwave_test_build_send_command(mock_device, + ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) + ) + }, + } +) + +test.register_message_test( + "3.1.2 Thermostat mode OFF reports should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, + zw_test_utilities.zwave_test_build_receive_command(ThermostatMode:Report({ mode = ThermostatMode.mode.OFF })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode({ value = "off" })) + } + } +) + +test.register_message_test( + "3.1.3 Thermostat mode ENERGY_SAVE_HEAT reports should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, + zw_test_utilities.zwave_test_build_receive_command(ThermostatMode:Report({ mode = ThermostatMode.mode.ENERGY_SAVE_HEAT })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode({ value = "eco" })) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utilities.zwave_test_build_send_command(mock_device, + ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING}) + ) + } + } +) + +test.register_message_test( + "3.1.4 Thermostat mode COOL reports should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, + zw_test_utilities.zwave_test_build_receive_command(ThermostatMode:Report({ mode = ThermostatMode.mode.COOL })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode({ value = "cool" })) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utilities.zwave_test_build_send_command(mock_device, + ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.COOLING_1}) + ) + } + } +) + +test.register_coroutine_test( + "3.2.1 Z-wave receive: Heating setpoint report should be handled for mode HEAT.", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + mock_device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, ThermostatMode.mode.HEAT, { persist = true }) + + test.socket.zwave:__queue_receive({mock_device.id, ThermostatSetpoint:Report({ + setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1, value = 235 + }) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 235, unit = "C"})) + ) + end +) + +test.register_coroutine_test( + "3.2.2 Z-wave receive: inactive ECO setpoint must not replace the displayed HEAT setpoint", + function() + mock_device:set_field(CURRENT_THERMOSTAT_HEATING_MODE, ThermostatMode.mode.HEAT) + test.socket.zwave:__queue_receive({ + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(ThermostatSetpoint:Report({ + setpoint_type = ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING, value = 18 + })) + }) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(ThermostatSetpoint:Report({ + setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1, value = 21 + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21, unit = "C"})) + ) + end +) + +test.register_message_test( + "3.3.1 Configuration 27 selects HEAT mode without assuming the relay is heating", + { + {channel = "zwave", direction = "receive", message = { + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 27, size = 1, configuration_value = 1 + })) + }}, + {channel = "zwave", direction = "send", message = zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) + )}, + {channel = "capability", direction = "send", message = + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.heat()) + }, + {channel = "zwave", direction = "send", message = zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatOperatingState:Get({}) + )} + }, + {inner_block_ordering = "relaxed"} +) + +test.register_message_test( + "3.3.2 Configuration 27 selects ECO mode without assuming the relay is heating", + { + {channel = "zwave", direction = "receive", message = { + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 27, size = 1, configuration_value = 3 + })) + }}, + {channel = "zwave", direction = "send", message = zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.ENERGY_SAVE_HEATING}) + )}, + {channel = "capability", direction = "send", message = + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco()) + }, + {channel = "zwave", direction = "send", message = zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatOperatingState:Get({}) + )} + }, + {inner_block_ordering = "relaxed"} +) + +test.register_message_test( + "3.3.3 Operating state reports actual idle instead of assuming the selected HEAT mode is heating", + { + {channel = "zwave", direction = "receive", message = { + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(ThermostatOperatingState:Report({ + operating_state = ThermostatOperatingState.operating_state.IDLE + })) + }}, + {channel = "capability", direction = "send", message = + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.thermostatOperatingState.idle()) + } + } +) + +test.register_message_test( + "3.3.4 Configuration 27 selects OFF without emitting an operating state", + { + {channel = "zwave", direction = "receive", message = { + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 27, size = 1, configuration_value = 0 + })) + }}, + {channel = "capability", direction = "send", message = + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.off()) + }, + {channel = "zwave", direction = "send", message = zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatOperatingState:Get({}) + )} + }, + {inner_block_ordering = "relaxed"} +) + +test.register_message_test( + "3.3.5 Configuration 27 selects COOL without emitting an operating state", + { + {channel = "zwave", direction = "receive", message = { + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 27, size = 1, configuration_value = 2 + })) + }}, + {channel = "zwave", direction = "send", message = zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.COOLING_1}) + )}, + {channel = "capability", direction = "send", message = + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.cool()) + }, + {channel = "zwave", direction = "send", message = zw_test_utilities.zwave_test_build_send_command( + mock_device, ThermostatOperatingState:Get({}) + )} + }, + {inner_block_ordering = "relaxed"} +) + +-- ----- A d d i t i o n a l Z - w a v e r e c e i v e t e s t s ----- + +test.register_coroutine_test( + "4.2.1 Air Temperature received with Sensor setting A", + function() + mock_device:set_field(CURRENT_SENSOR_CHANNEL, 2, { persist = true }) + test.socket.zwave:__queue_receive({mock_device.id, SensorMultilevel:Report({sensor_type = SensorMultilevel.sensor_type.TEMPERATURE, scale = 0, sensor_value = 21.5} + , { encap = zw.ENCAP.AUTO, src_channel = 2, dst_channels = {} }) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = "C" }))) + end +) + +test.register_coroutine_test( + "4.2.12 Floor Temperature received with Sensor setting F", + function() + mock_device:set_field(CURRENT_SENSOR_CHANNEL, 4) + test.socket.zwave:__queue_receive({mock_device.id, SensorMultilevel:Report({sensor_type = SensorMultilevel.sensor_type.TEMPERATURE, scale = 0, sensor_value = 22.5} + , { encap = zw.ENCAP.AUTO, src_channel = 4, dst_channels = {} }) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 22.5, unit = "C" }))) + end +) + +test.register_message_test( + "Energy meter reports should be handled", + { + { channel = "zwave", direction = "receive", + message = { mock_device.id, zw_test_utilities.zwave_test_build_receive_command( Meter:Report({ scale = Meter.scale.electric_meter.KILOWATT_HOURS, meter_value = 5}) )} + }, + { channel = "capability", direction = "send", + message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) } + } +) + +test.register_message_test( + "Power meter reports should be handled", + { + { channel = "zwave", direction = "receive", + message = { mock_device.id, zw_test_utilities.zwave_test_build_receive_command(Meter:Report({ scale = Meter.scale.electric_meter.WATTS, meter_value = 5 }))} + }, + { channel = "capability", direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + } + } +) + +-- Update to handle settings update for MultiChannelAssociation +test.register_coroutine_test( + "4.3.1 MultiChannelAssociation update", + function() + local _preferences = {} + _preferences.assocGroup2OnOff = "41.3 1" + + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = _preferences })) + + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( mock_device, + MultiChannelAssociation:Remove({ grouping_identifier = 2, node_ids = {} }) + )) + + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( mock_device, + MultiChannelAssociation:Set({ grouping_identifier = 2, multi_channel_nodes = {{ multi_channel_node_id = 41, end_point=3, bit_address = false }}, node_ids = {1} }) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( mock_device, + MultiChannelAssociation:Get({ grouping_identifier = 2 }) + )) + end +) + +test.register_coroutine_test( + "4.4 Change Floor sensor type. First 12k then 10k", + function() + local _preferences = {} + _preferences.floorSensorType = 1 + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = _preferences })) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, Configuration:Set({ parameter_number = 3, configuration_value = 1, size = 1 }) ) ) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, Configuration:Get({ parameter_number = 3 }) ) ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + _preferences.floorSensorType = 0 + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = _preferences })) + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, Configuration:Set({ parameter_number = 3, configuration_value = 0, size = 1 }) ) ) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command (mock_device, Configuration:Get({ parameter_number = 3 }) ) ) + end +) + +test.register_coroutine_test( + "2.5.1 Capability: resetEnergyMeter Should send zwave Meter:Reset and emit 0 kWh", + function() + test.timer.__create_and_queue_test_time_advance_timer(1.5, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "energyMeter", command = "resetEnergyMeter", args = {}, component = "main" } }) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, Meter:Reset({}) ) ) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0, unit = "kWh" })) ) + + test.wait_for_events() + test.mock_time.advance_time(1.5) + + test.socket.zwave:__expect_send( zw_test_utilities.zwave_test_build_send_command( mock_device, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) ) + end +) + +test.register_message_test( + "1.2 Lifecycle: do_configure", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_device.id, "do_configure" } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_coroutine_test( + "PWER level command maps 30 percent to parameter 25 even before a mode report", + function() + test.socket.capability:__queue_receive({ + mock_device_regulator.id, {capability = "level", command = "setLevel", component = "main", args = {30}} + }) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Set({parameter_number = 25, size = 1, configuration_value = 3}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Get({parameter_number = 25}) + )) + end +) + +test.register_coroutine_test( + "PWER level command maps 100 percent to configuration parameter 25 value 10", + function() + mock_device_regulator:set_field(CURRENT_SENSOR_MODE, 5) + test.socket.capability:__queue_receive({ + mock_device_regulator.id, {capability = "level", command = "setLevel", component = "main", args = {100}} + }) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Set({parameter_number = 25, size = 1, configuration_value = 10}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Get({parameter_number = 25}) + )) + end +) + +test.register_coroutine_test( + "Legacy PWER setting still writes parameter 25", + function() + test.socket.device_lifecycle():__queue_receive(mock_device_regulator:generate_info_changed({ + preferences = {regulatorLevel = 3} + })) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Set({parameter_number = 25, size = 1, configuration_value = 3}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Get({parameter_number = 25}) + )) + end +) + +test.register_coroutine_test( + "Entering PWER switches profile and requests current regulator level", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 2, size = 1, configuration_value = 5 + })) + }) + mock_device:expect_metadata_update({profile = "heatit-ztrm6-thermostat-regulator"}) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, SensorMultilevel:Get({}, {dst_channels = {2}}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device, Configuration:Get({parameter_number = 25}) + )) + end +) + +test.register_coroutine_test( + "PWER refresh requests current regulator level", + function() + test.socket.capability:__queue_receive({ + mock_device_regulator.id, {capability = "refresh", command = "refresh", component = "main", args = {}} + }) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, ThermostatMode:Get({}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, ThermostatOperatingState:Get({}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Get({parameter_number = 25}) + )) + for _, endpoint in ipairs({2, 3, 4}) do + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, SensorMultilevel:Get({}, {dst_channels = {endpoint}}) + )) + end + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Meter:Get({scale = Meter.scale.electric_meter.WATTS}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, ThermostatSetpoint:Get({setpoint_type = ThermostatSetpoint.setpoint_type.HEATING_1}) + )) + end +) + +test.register_coroutine_test( + "PWER level command rounds to supported ten-percent increments", + function() + mock_device_regulator:set_field(CURRENT_SENSOR_MODE, 5) + test.socket.capability:__queue_receive({ + mock_device_regulator.id, {capability = "level", command = "setLevel", component = "main", args = {36}} + }) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Set({parameter_number = 25, size = 1, configuration_value = 4}) + )) + test.socket.zwave:__expect_send(zw_test_utilities.zwave_test_build_send_command( + mock_device_regulator, Configuration:Get({parameter_number = 25}) + )) + end +) + +test.register_coroutine_test( + "PWER level command rejects unsupported values and non-PWER modes", + function() + mock_device_regulator:set_field(CURRENT_SENSOR_MODE, 5) + test.socket.capability:__queue_receive({ + mock_device_regulator.id, {capability = "level", command = "setLevel", component = "main", args = {0}} + }) + test.socket.capability:__queue_receive({ + mock_device_regulator.id, {capability = "level", command = "setLevel", component = "main", args = {9}} + }) + mock_device:set_field(CURRENT_SENSOR_MODE, 1) + test.socket.capability:__queue_receive({ + mock_device.id, {capability = "level", command = "setLevel", component = "main", args = {30}} + }) + test.wait_for_events() + end +) + +test.register_message_test( + "PWER parameter 25 report tracks local power level changes", + { + {channel = "zwave", direction = "receive", message = { + mock_device_regulator.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 25, size = 1, configuration_value = 3 + })) + }}, + {channel = "capability", direction = "send", message = + mock_device_regulator:generate_test_message("main", capabilities.level.level(30)) + } + } +) + +test.register_message_test( + "PWER parameter 25 report ignores invalid device values", + { + {channel = "zwave", direction = "receive", message = { + mock_device_regulator.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 25, size = 1, configuration_value = 0 + })) + }} + } +) + +test.register_message_test( + "Parameter 25 report does not emit level for normal thermostat profile", + { + {channel = "zwave", direction = "receive", message = { + mock_device.id, zw_test_utilities.zwave_test_build_receive_command(Configuration:Report({ + parameter_number = 25, size = 1, configuration_value = 3 + })) + }} + } +) + +test.run_registered_tests()