11"""Aqara E1 Radiator Thermostat Quirk."""
22from __future__ import annotations
33
4- from functools import reduce
54import math
65import struct
6+ import time
77from typing import Any
88
99from zigpy .profiles import zha
4848SENSOR = 0x027E
4949BATTERY_PERCENTAGE = 0x040A
5050
51+ SENSOR_TEMP = 0x1392 # Fake address to pass external sensor temperature
52+ SENSOR_ATTR = 0xFFF2
53+ SENSOR_ATTR_NAME = "sensor_attr"
54+
5155XIAOMI_CLUSTER_ID = 0xFCC0
5256
5357DAYS_MAP = {
@@ -379,6 +383,8 @@ class AqaraThermostatSpecificCluster(XiaomiAqaraE1Cluster):
379383 SCHEDULE_SETTINGS : ("schedule_settings" , ScheduleSettings , True ),
380384 SENSOR : ("sensor" , t .uint8_t , True ),
381385 BATTERY_PERCENTAGE : ("battery_percentage" , t .uint8_t , True ),
386+ SENSOR_TEMP : ("sensor_temp" , t .uint32_t , True ),
387+ SENSOR_ATTR : (SENSOR_ATTR_NAME , t .LVBytes , True ),
382388 }
383389 )
384390
@@ -393,6 +399,172 @@ def _update_attribute(self, attrid, value):
393399 )
394400 super ()._update_attribute (attrid , value )
395401
402+ def aqaraHeader (
403+ self , counter : int , params : bytearray , action : int
404+ ) -> bytearray :
405+ """Create Aqara header for setting external sensor."""
406+ header = bytes ([0xAA , 0x71 , len (params ) + 3 , 0x44 , counter ])
407+ integrity = 512 - sum (header )
408+
409+ return header + bytes ([integrity , action , 0x41 , len (params )])
410+
411+ def _float_to_hex (self , f ):
412+ """Convert float to hex."""
413+ return hex (struct .unpack ("<I" , struct .pack ("<f" , f ))[0 ])
414+
415+ async def write_attributes (
416+ self , attributes : dict [str | int , Any ], manufacturer : int | None = None
417+ ) -> list :
418+ """Write attributes to device with internal 'attributes' validation."""
419+ sensor = bytearray .fromhex ("00158d00019d1b98" )
420+ attrs = {}
421+
422+ for attr , value in attributes .items ():
423+ # implemented with help from https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/devices/xiaomi.js
424+
425+ if attr == SENSOR_TEMP :
426+ # set external sensor temp. this function expect value to be passed multiplied by 100
427+ temperatureBuf = bytearray .fromhex (
428+ self ._float_to_hex (round (float (value )))[2 :]
429+ )
430+
431+ params = sensor
432+ params += bytes ([0x00 , 0x01 , 0x00 , 0x55 ])
433+ params += temperatureBuf
434+
435+ attrs = {}
436+ attrs [SENSOR_ATTR_NAME ] = (
437+ self .aqaraHeader (0x12 , params , 0x05 ) + params
438+ )
439+
440+ elif attr == SENSOR :
441+ # set internal/external temperature sensor
442+ device = bytearray .fromhex (
443+ ("%s" % (self .endpoint .device .ieee )).replace (":" , "" )
444+ )
445+ timestamp = bytes (
446+ reversed (t .uint32_t (int (time .time ())).serialize ())
447+ )
448+
449+ if value == 0 :
450+ # internal sensor
451+ params1 = timestamp
452+ params1 += bytes ([0x3D , 0x05 ])
453+ params1 += device
454+ params1 += bytes (
455+ [
456+ 0x00 ,
457+ 0x00 ,
458+ 0x00 ,
459+ 0x00 ,
460+ 0x00 ,
461+ 0x00 ,
462+ 0x00 ,
463+ 0x00 ,
464+ 0x00 ,
465+ 0x00 ,
466+ 0x00 ,
467+ 0x00 ,
468+ ]
469+ )
470+
471+ params2 = timestamp
472+ params2 += bytes ([0x3D , 0x04 ])
473+ params2 += device
474+ params2 += bytes (
475+ [
476+ 0x00 ,
477+ 0x00 ,
478+ 0x00 ,
479+ 0x00 ,
480+ 0x00 ,
481+ 0x00 ,
482+ 0x00 ,
483+ 0x00 ,
484+ 0x00 ,
485+ 0x00 ,
486+ 0x00 ,
487+ 0x00 ,
488+ ]
489+ )
490+
491+ attrs1 = {}
492+ attrs1 [SENSOR_ATTR_NAME ] = (
493+ self .aqaraHeader (0x12 , params1 , 0x04 ) + params1
494+ )
495+ attrs [SENSOR_ATTR_NAME ] = (
496+ self .aqaraHeader (0x13 , params2 , 0x04 ) + params2
497+ )
498+
499+ result = await super ().write_attributes (attrs1 , manufacturer )
500+ else :
501+ # external sensor
502+ params1 = timestamp
503+ params1 += bytes ([0x3D , 0x04 ])
504+ params1 += device
505+ params1 += sensor
506+ params1 += bytes ([0x00 , 0x01 , 0x00 , 0x55 ])
507+ params1 += bytes (
508+ [
509+ 0x13 ,
510+ 0x0A ,
511+ 0x02 ,
512+ 0x00 ,
513+ 0x00 ,
514+ 0x64 ,
515+ 0x04 ,
516+ 0xCE ,
517+ 0xC2 ,
518+ 0xB6 ,
519+ 0xC8 ,
520+ ]
521+ )
522+ params1 += bytes ([0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x01 , 0x3D ])
523+ params1 += bytes ([0x64 ])
524+ params1 += bytes ([0x65 ])
525+
526+ params2 = timestamp
527+ params2 += bytes ([0x3D , 0x05 ])
528+ params2 += device
529+ params2 += sensor
530+ params2 += bytes ([0x08 , 0x00 , 0x07 , 0xFD ])
531+ params2 += bytes (
532+ [
533+ 0x16 ,
534+ 0x0A ,
535+ 0x02 ,
536+ 0x0A ,
537+ 0xC9 ,
538+ 0xE8 ,
539+ 0xB1 ,
540+ 0xB8 ,
541+ 0xD4 ,
542+ 0xDA ,
543+ 0xCF ,
544+ 0xDF ,
545+ 0xC0 ,
546+ 0xEB ,
547+ ]
548+ )
549+ params2 += bytes ([0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x01 , 0x3D ])
550+ params2 += bytes ([0x04 ])
551+ params2 += bytes ([0x65 ])
552+
553+ attrs1 = {}
554+ attrs1 [SENSOR_ATTR_NAME ] = (
555+ self .aqaraHeader (0x12 , params1 , 0x02 ) + params1
556+ )
557+ attrs [SENSOR_ATTR_NAME ] = (
558+ self .aqaraHeader (0x13 , params2 , 0x02 ) + params2
559+ )
560+
561+ result = await super ().write_attributes (attrs1 , manufacturer )
562+ else :
563+ attrs [attr ] = value
564+
565+ result = await super ().write_attributes (attrs , manufacturer )
566+ return result
567+
396568
397569class AGL001 (XiaomiCustomDevice ):
398570 """Aqara E1 Radiator Thermostat (AGL001) Device."""
0 commit comments