33/**
44 * Thermostat Sinopé TH1123ZB-TH1124ZB Driver
55 *
6- * 1.0 (2022-12-31): initial release
6+ * 1.0 (2022-12-31): Initial release
7+ * 1.1 (2022-01-04): Handled short circuit and rmsVoltage/rmsCurrent
78 * Author: fblackburn
89 * Inspired by:
910 * - Sinope => https://github.com/SmartThingsCommunity/SmartThingsPublic/tree/master/devicetypes/sinope-technologies
@@ -29,6 +30,8 @@ metadata
2930 capability ' Lock'
3031 capability ' PowerMeter'
3132 capability ' EnergyMeter'
33+ capability ' CurrentMeter'
34+ capability ' VoltageMeasurement'
3235
3336 attribute ' maxPower' , ' number'
3437
@@ -159,89 +162,49 @@ void uninstalled() {
159162 unschedule()
160163}
161164
162- Map parse (String description ) {
165+ List< Map > parse (String description ) {
163166 if (! description?. startsWith(' read attr -' )) {
164167 if (! description?. startsWith(' catchall:' )) {
165168 log. warn " TH112XZB >> parse(description) ==> Unhandled event: ${ description} "
166169 }
167- return [: ]
170+ return []
168171 }
169172
170- Map event = [:]
171173 Map descMap = zigbee. parseDescriptionAsMap(description)
172- if (descMap. cluster == ' 0201' && descMap. attrId == ' 0000' ) {
173- String scale = getTemperatureScale()
174- event. name = ' temperature'
175- event. value = getTemperatureValue(descMap. value, scale)
176- event. unit = " °${ scale} "
177- } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 0008' ) {
178- Integer heatingDemand = getHeatingDemand(descMap. value)
179- event. name = ' heatingDemand'
180- event. value = heatingDemand
181- event. unit = ' %'
182-
174+ Map event = extractEvent(descMap)
175+ List<Map > events = [event]
176+ if (event. name == ' heatingDemand' ) {
183177 String operatingState = (event. value. toInteger() < 10 ) ? ' idle' : ' heating'
184178 Map opEvent = [' name' : ' thermostatOperatingState' , ' value' : operatingState]
185179 opEvent. descriptionText = generateDescription(opEvent)
186180 if (settings. trace) {
187181 log. trace " TH112XZB >> parse(description)[generated] ==> ${ opEvent.name} : ${ opEvent.value} "
188182 }
189- sendEvent (opEvent)
183+ events . add (opEvent)
190184
191185 Integer maxPower = device. currentValue(' maxPower' )
192186 if (maxPower != null ) {
193- Integer power = Math . round(maxPower * heatingDemand / 100 )
187+ Integer power = Math . round(maxPower * event . value / 100 )
194188 Map powerEvent = [name : ' power' , value : power, unit : ' W' ]
195189 powerEvent. descriptionText = generateDescription(powerEvent)
196190 if (settings. trace) {
197191 log. trace " TH112XZB >> parse(description)[generated] ==> ${ powerEvent.name} : ${ powerEvent.value} "
198192 }
199- sendEvent(powerEvent)
200- }
201- } else if (descMap. cluster == ' 0702' && descMap. attrId == ' 0000' ) {
202- BigInteger energy = getEnergy(descMap. value)
203- if (energy == 0 ) {
204- // FIXME Not able to reproduce this behavior with TH112XZB
205- log. warn ' TH112XZB >> Ignoring energy event (Caused: unknown)'
206- } else {
207- event. name = ' energy'
208- event. value = energy / 1000
209- event. unit = ' kWh'
193+ events. add(powerEvent)
210194 }
211- } else if (descMap. cluster == ' 0B04' && descMap. attrId == ' 050B' ) {
212- event. name = ' power'
213- event. value = getPower(descMap. value)
214- event. unit = ' W'
215- } else if (descMap. cluster == ' 0B04' && descMap. attrId == ' 050D' ) {
216- event. name = ' maxPower'
217- event. value = getPower(descMap. value)
218- event. unit = ' W'
219- } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 0012' ) {
220- String scale = getTemperatureScale()
221- event. name = ' heatingSetpoint'
222- event. value = getTemperatureValue(descMap. value, scale, true )
223- event. unit = " °${ scale} "
224- } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 0014' ) {
225- String scale = getTemperatureScale()
226- event. name = ' heatingSetpoint'
227- event. value = getTemperatureValue(descMap. value, scale, true )
228- event. unit = " °${ scale} "
229- } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 001C' ) {
230- event. name = ' thermostatMode'
231- event. value = getModeMap()[descMap. value]
232- } else if (descMap. cluster == ' 0204' && descMap. attrId == ' 0001' ) {
233- event. name = ' lock'
234- event. value = getLockMap()[descMap. value]
235- } else {
236- log. warn " TH112XZB >> parse(descMap) ==> Unhandled attribute: ${ descMap} "
237- return [:]
238195 }
239- event. descriptionText = generateDescription(event)
240-
241- if (settings. trace) {
242- log. trace " TH112XZB >> parse(description) ==> ${ event.name} : ${ event.value} "
196+ if (descMap. additionalAttrs) {
197+ // When many events from same cluster must be sent at the same time,
198+ // device other events in additionalAttrs instead of sending several
199+ if (settings. trace) {
200+ log. trace " TH112XZB >> Found additionalAttrs: ${ descMap} "
201+ }
202+ descMap. additionalAttrs. each { Map attribute ->
203+ attribute. cluster = descMap. cluster
204+ events. add(extractEvent(attribute))
205+ }
243206 }
244- return event
207+ return events
245208}
246209
247210void unlock () {
@@ -286,6 +249,8 @@ void refresh() {
286249 cmds + = zigbee. readAttribute(0x0201 , 0x0008 ) // PI heating demand
287250 cmds + = zigbee. readAttribute(0x0201 , 0x001C ) // System Mode
288251 cmds + = zigbee. readAttribute(0x0204 , 0x0001 ) // Keypad lock
252+ cmds + = zigbee. readAttribute(0x0B04 , 0x0505 ) // RMS Voltage
253+ cmds + = zigbee. readAttribute(0x0B04 , 0x0508 ) // RMS Current
289254 cmds + = zigbee. readAttribute(0x0B04 , 0x050B ) // Active power
290255 cmds + = zigbee. readAttribute(0x0B04 , 0x050D ) // Maximum power available
291256 cmds + = zigbee. readAttribute(0x0702 , 0x0000 ) // Total Energy
@@ -336,7 +301,6 @@ void setClockTime() {
336301 log. trace ' TH112XZB >> setClockTime()'
337302 }
338303
339-
340304 /* groovylint-disable-next-line NoJavaUtilDate */
341305 Date now = new Date ()
342306 Long currentTimeSec = now. getTime() / 1000
@@ -440,9 +404,84 @@ void handlePowerOutage() {
440404 setClockTime()
441405}
442406
407+ private Map extractEvent (Map descMap ) {
408+ Map event = [:]
409+ if (descMap. cluster == ' 0201' && descMap. attrId == ' 0000' ) {
410+ String scale = getTemperatureScale()
411+ event. name = ' temperature'
412+ event. value = getTemperatureValue(descMap. value, scale)
413+ event. unit = " °${ scale} "
414+ } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 0008' ) {
415+ event. name = ' heatingDemand'
416+ event. value = getHeatingDemand(descMap. value)
417+ event. unit = ' %'
418+ } else if (descMap. cluster == ' 0702' && descMap. attrId == ' 0000' ) {
419+ BigInteger energy = getEnergy(descMap. value)
420+ Double previousEnergy = device. currentValue(' energy' )
421+ if (energy < previousEnergy) {
422+ // When a baseboard heater is too hot, a short circuit is created for few seconds until the unit cools down.
423+ // This kind of power outage, reset to an old "random" value
424+ // Note: For some unknown reason, power outage from electrical board doesn't reset value ...
425+ // If you have this warning you should verify that nothing prevents the release of heat from your heater
426+ // (ex: curtains, bedding, reverse installation, etc)
427+ /* groovylint-disable-next-line LineLength */
428+ log. warn " TH112XZB >> Energy[${ energy} ] is lower than previous one[${ previousEnergy} ] (Caused: short circuit from heater)"
429+ }
430+ event. name = ' energy'
431+ event. value = energy / 1000
432+ event. unit = ' kWh'
433+ } else if (descMap. cluster == ' 0B04' && descMap. attrId == ' 050B' ) {
434+ event. name = ' power'
435+ event. value = getPower(descMap. value)
436+ event. unit = ' W'
437+ } else if (descMap. cluster == ' 0B04' && descMap. attrId == ' 050D' ) {
438+ event. name = ' maxPower'
439+ event. value = getPower(descMap. value)
440+ event. unit = ' W'
441+ } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 0012' ) {
442+ String scale = getTemperatureScale()
443+ event. name = ' heatingSetpoint'
444+ event. value = getTemperatureValue(descMap. value, scale, true )
445+ event. unit = " °${ scale} "
446+ } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 0014' ) {
447+ String scale = getTemperatureScale()
448+ event. name = ' heatingSetpoint'
449+ event. value = getTemperatureValue(descMap. value, scale, true )
450+ event. unit = " °${ scale} "
451+ } else if (descMap. cluster == ' 0201' && descMap. attrId == ' 001C' ) {
452+ event. name = ' thermostatMode'
453+ event. value = getModeMap()[descMap. value]
454+ } else if (descMap. cluster == ' 0204' && descMap. attrId == ' 0001' ) {
455+ event. name = ' lock'
456+ event. value = getLockMap()[descMap. value]
457+ } else if (descMap. cluster == ' 0B04' && descMap. attrId == ' 0505' ) {
458+ // This event seems to be triggered automatically after each 18 hours
459+ event. name = ' voltage'
460+ event. value = getVoltage(descMap. value)
461+ event. unit = ' V'
462+ } else if (descMap. cluster == ' 0B04' && descMap. attrId == ' 0508' ) {
463+ event. name = ' amperage'
464+ event. value = getAmperage(descMap. value)
465+ event. unit = ' A'
466+ } else if (descMap. cluster == ' 0B04' && descMap. attrId == ' 0551' ) {
467+ BigInteger energy = getEnergy(descMap. value)
468+ log. trace " TH112XZB >> Skipping duplicate event[0551] energy': ${ energy} "
469+ return [:]
470+ } else {
471+ log. warn " TH112XZB >> parse(descMap) ==> Unhandled attribute: ${ descMap} "
472+ return [:]
473+ }
474+ event. descriptionText = generateDescription(event)
475+
476+ if (settings. trace) {
477+ log. trace " TH112XZB >> parse(description) ==> ${ event.name} : ${ event.value} "
478+ }
479+ return event
480+ }
481+
443482private String generateDescription (Map event ) {
444483 String description = null
445- if (event. name && event. value) {
484+ if (event. name != null && event. value != null ) {
446485 description = " ${ device.getLabel()} ${ event.name} is ${ event.value} "
447486 if (event. unit) {
448487 description = " ${ description}${ event.unit} "
@@ -561,6 +600,20 @@ private BigInteger getEnergy(String value) {
561600 return new BigInteger (value, 16 )
562601}
563602
603+ private Double getVoltage (String value ) {
604+ if (value == null ) {
605+ return 0
606+ }
607+ return Integer . parseInt(value, 16 ) / 10
608+ }
609+
610+ private Double getAmperage (String value ) {
611+ if (value == null ) {
612+ return 0
613+ }
614+ return Integer . parseInt(value, 16 ) / 1000
615+ }
616+
564617private Map getModeMap () {
565618 return [
566619 ' 00' : ' off' ,
0 commit comments