The problem
The DeviceTemperature sensor hardcodes _divisor = 100:
https://github.com/zigpy/zha/blob/dev/zha/application/platforms/sensor/__init__.py — class DeviceTemperature(Sensor) sets _divisor = 100, and the base Sensor applies value /= self._divisor.
But per ZCL R8 §3.4.2.2.1.1 (CurrentTemperature attribute):
The Current Temperature attribute is 16 bits in length and specifies the current internal temperature, in degrees Celsius, of the device. This attribute SHALL be specified in the range −200 to +200.
So CurrentTemperature (0x0000 on the Device Temperature Configuration cluster, 0x0002) is a signed int16 in whole degrees Celsius, with no scaling (range −200…+200 °C). The /100 divisor is therefore wrong for a spec-compliant device: one reporting 41 (= 41 °C) is displayed as 0.41 °C.
This has been masked by two things: a number of devices (mostly Xiaomi/Aqara) report centidegrees instead of the spec's whole degrees, and a body of existing zha-device-handlers quirks multiply current_temperature by 100 specifically to cancel this divisor. So the current default is "correct" only for devices that are themselves non-compliant or that have a compensating quirk — and it is silently wrong for every compliant device without one.
This is not a one-line fix
Flipping _divisor to 1 (spec-correct) would invert the breakage: every device currently relying on the divisor (native-centidegree devices, and the quirks below that pre-scale ×100) would then read 100× too high. Any change here has to be coordinated with the quirks side. The three buckets below are the full regression surface.
Bucket A — spec-compliant, no compensation → currently displayed 100× too low (the bug)
Raw current_temperature from tests/data/devices/ snapshots with quirk_applied: false (so these are unmodified device reports):
| Device |
raw value |
ZHA displays |
| AEOTEC ZGA004 |
41 |
0.41 °C |
| LUMI lumi.plug.maeu01 |
29 |
0.29 °C |
| LUMI lumi.switch.b1laus01 |
20 |
0.20 °C |
| LUMI lumi.switch.b1naus01 |
23 |
0.23 °C |
| LUMI lumi.switch.b2nc01 |
24 |
0.24 °C |
| LUMI lumi.switch.l1aeu1 |
30 |
0.30 °C |
| LUMI lumi.switch.l2aeu1 |
30 |
0.30 °C |
| LUMI lumi.switch.n1aeu1 |
28 |
0.28 °C |
| frient A/S SPLZB-141 (ep 2) |
32 |
0.32 °C |
Bucket B — existing quirks that already multiply current_temperature ×100 to cancel the divisor
These would need the ×100 removed if the core divisor is fixed (otherwise they'd read 100× too high):
| Quirk file |
Mechanism |
Devices covered |
zhaquirks/sinope/__init__.py (CustomDeviceTemperatureCluster, used by sinope/switch.py + sinope/light.py) |
_update_attribute: value * 100 |
Sinope load controllers / switches / dimmers (e.g. RM3250ZB, RM3500ZB, SW2500ZB, SW2500ZB-G2, …) |
zhaquirks/xiaomi/__init__.py (XiaomiCluster, ~line 272) |
aggregate-report handler writes attributes[TEMPERATURE] * 100 into device_temperature |
broad Xiaomi/Aqara set that routes the 0xFF01 manufacturer report through XiaomiCluster |
zhaquirks/xiaomi/aqara/airm_fhac01.py (CustomDeviceTemperature) |
_update_attribute: value * 100 |
LUMI lumi.airm.fhac01 (air-quality monitor) |
zhaquirks/tuya/ts0601_rcbo.py (TuyaRCBODeviceTemperature) |
DP converter lambda x: x * 100 |
Tuya TS0601 RCBO breaker |
(Note the airm comment literally reads "The device reports temperature divided by 100, so multiply by 100" — i.e. the author was compensating for this divisor.)
Bucket C — native-centidegree devices with no quirk → currently correct by accident
Raw values (quirk_applied: false) that happen to match the /100 divisor; these would need a new compensating quirk if the divisor were removed:
| Device |
raw value |
ZHA displays |
| LUMI lumi.ctrl_neutral2 |
2300 |
23 °C |
| LUMI lumi.switch.b1lacn02 |
1700 |
17 °C |
| LUMI lumi.switch.b2naus01 |
1400 |
14 °C |
That the same vendor (Xiaomi) appears in both Bucket A (whole degrees) and Bucket C (centidegrees) is the core difficulty — there is no single divisor correct for the whole fleet.
Anomalies: Namron 4512785 reports 352 (→ 3.52 °C with the divisor; fits neither whole nor centidegrees cleanly). Develco ZHEMI and aqara lumi.sensor_occupy.agl1 (FP1E) report None.
Why this surfaced now
Quirk PR zigpy/zha-device-handlers#5023 (Heiman HS1RM-E) adds yet another DeviceTemperature cluster that multiplies current_temperature by 100 purely to cancel this /100. That's the de-facto pattern for spec-compliant devices today (see Bucket B), but it means every compliant device needs a quirk to display a spec-defined attribute correctly.
Possible directions (for discussion, not prescriptive)
- Make the divisor device-overridable (e.g. settable from a quirk declaratively) so neither population needs a custom cluster +
_update_attribute, and the core default can stay where it is during migration.
- Flip the core default to
_divisor = 1 (spec-correct), remove the ×100 from the Bucket B quirks, and add quirks for the Bucket C devices. Spec-aligned but the largest blast radius.
- Status quo: keep compensating compliant devices in quirks (means every correct device needs one).
Caveat on the evidence: values are read from ZHA's tests/data/devices/ snapshots. For quirk_applied: true devices a cached value can be post-quirk, so Buckets A and C are drawn only from quirk_applied: false snapshots to keep them unambiguous; Bucket B is sourced from the quirk code itself.
cc @TheJulianJES
The problem
The
DeviceTemperaturesensor hardcodes_divisor = 100:https://github.com/zigpy/zha/blob/dev/zha/application/platforms/sensor/__init__.py —
class DeviceTemperature(Sensor)sets_divisor = 100, and the baseSensorappliesvalue /= self._divisor.But per ZCL R8 §3.4.2.2.1.1 (CurrentTemperature attribute):
So
CurrentTemperature(0x0000 on the Device Temperature Configuration cluster, 0x0002) is a signedint16in whole degrees Celsius, with no scaling (range −200…+200 °C). The/100divisor is therefore wrong for a spec-compliant device: one reporting41(= 41 °C) is displayed as 0.41 °C.This has been masked by two things: a number of devices (mostly Xiaomi/Aqara) report centidegrees instead of the spec's whole degrees, and a body of existing
zha-device-handlersquirks multiplycurrent_temperatureby 100 specifically to cancel this divisor. So the current default is "correct" only for devices that are themselves non-compliant or that have a compensating quirk — and it is silently wrong for every compliant device without one.This is not a one-line fix
Flipping
_divisorto1(spec-correct) would invert the breakage: every device currently relying on the divisor (native-centidegree devices, and the quirks below that pre-scale ×100) would then read 100× too high. Any change here has to be coordinated with the quirks side. The three buckets below are the full regression surface.Bucket A — spec-compliant, no compensation → currently displayed 100× too low (the bug)
Raw
current_temperaturefromtests/data/devices/snapshots withquirk_applied: false(so these are unmodified device reports):Bucket B — existing quirks that already multiply
current_temperature×100 to cancel the divisorThese would need the ×100 removed if the core divisor is fixed (otherwise they'd read 100× too high):
zhaquirks/sinope/__init__.py(CustomDeviceTemperatureCluster, used bysinope/switch.py+sinope/light.py)_update_attribute:value * 100zhaquirks/xiaomi/__init__.py(XiaomiCluster, ~line 272)attributes[TEMPERATURE] * 100intodevice_temperatureXiaomiClusterzhaquirks/xiaomi/aqara/airm_fhac01.py(CustomDeviceTemperature)_update_attribute:value * 100zhaquirks/tuya/ts0601_rcbo.py(TuyaRCBODeviceTemperature)lambda x: x * 100(Note the airm comment literally reads "The device reports temperature divided by 100, so multiply by 100" — i.e. the author was compensating for this divisor.)
Bucket C — native-centidegree devices with no quirk → currently correct by accident
Raw values (
quirk_applied: false) that happen to match the/100divisor; these would need a new compensating quirk if the divisor were removed:That the same vendor (Xiaomi) appears in both Bucket A (whole degrees) and Bucket C (centidegrees) is the core difficulty — there is no single divisor correct for the whole fleet.
Anomalies: Namron 4512785 reports
352(→ 3.52 °C with the divisor; fits neither whole nor centidegrees cleanly). Develco ZHEMI and aqara lumi.sensor_occupy.agl1 (FP1E) reportNone.Why this surfaced now
Quirk PR zigpy/zha-device-handlers#5023 (Heiman HS1RM-E) adds yet another
DeviceTemperaturecluster that multipliescurrent_temperatureby 100 purely to cancel this/100. That's the de-facto pattern for spec-compliant devices today (see Bucket B), but it means every compliant device needs a quirk to display a spec-defined attribute correctly.Possible directions (for discussion, not prescriptive)
_update_attribute, and the core default can stay where it is during migration._divisor = 1(spec-correct), remove the ×100 from the Bucket B quirks, and add quirks for the Bucket C devices. Spec-aligned but the largest blast radius.Caveat on the evidence: values are read from ZHA's
tests/data/devices/snapshots. Forquirk_applied: truedevices a cached value can be post-quirk, so Buckets A and C are drawn only fromquirk_applied: falsesnapshots to keep them unambiguous; Bucket B is sourced from the quirk code itself.cc @TheJulianJES