Skip to content

Commit 70c7843

Browse files
committed
Move metering formatting out of cluster handler and into entity
1 parent 4747e1c commit 70c7843

File tree

3 files changed

+96
-69
lines changed

3 files changed

+96
-69
lines changed

zha/application/platforms/sensor/__init__.py

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import logging
1212
import numbers
1313
import typing
14-
from typing import TYPE_CHECKING, Any
14+
from typing import TYPE_CHECKING, Any, cast
1515

1616
from zhaquirks.danfoss import thermostat as danfoss_thermostat
1717
from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
@@ -21,7 +21,11 @@
2121
from zigpy.zcl import foundation
2222
from zigpy.zcl.clusters.closures import WindowCovering
2323
from zigpy.zcl.clusters.general import Basic
24-
from zigpy.zcl.clusters.smartenergy import Metering
24+
from zigpy.zcl.clusters.smartenergy import (
25+
Metering,
26+
MeteringUnitofMeasure,
27+
NumberFormatting,
28+
)
2529

2630
from zha.application import Platform
2731
from zha.application.platforms import (
@@ -41,7 +45,10 @@
4145
SensorDeviceClass,
4246
SensorStateClass,
4347
)
44-
from zha.application.platforms.sensor.helpers import resolution_to_decimal_precision
48+
from zha.application.platforms.sensor.helpers import (
49+
create_number_formatter,
50+
resolution_to_decimal_precision,
51+
)
4552
from zha.application.registries import PLATFORM_ENTITIES
4653
from zha.decorators import periodic
4754
from zha.units import (
@@ -96,6 +103,12 @@
96103
from zha.zigbee.device import Device
97104
from zha.zigbee.endpoint import Endpoint
98105

106+
DEFAULT_FORMATTING = NumberFormatting(
107+
num_digits_right_of_decimal=1,
108+
num_digits_left_of_decimal=15,
109+
suppress_leading_zeros=1,
110+
)
111+
99112
BATTERY_SIZES = {
100113
0: "No battery",
101114
1: "Built in",
@@ -1100,9 +1113,43 @@ def state(self) -> dict[str, Any]:
11001113
response["zcl_unit_of_measurement"] = self._cluster_handler.unit_of_measurement
11011114
return response
11021115

1116+
@property
1117+
def _multiplier(self) -> int | float | None:
1118+
return self._cluster_handler.multiplier
1119+
1120+
@_multiplier.setter
1121+
def _multiplier(self, value: int | float | None) -> None:
1122+
raise AttributeError("Cannot set multiplier directly")
1123+
1124+
@property
1125+
def _divisor(self) -> int | float | None:
1126+
return self._cluster_handler.divisor
1127+
1128+
@_divisor.setter
1129+
def _divisor(self, value: int | float | None) -> None:
1130+
raise AttributeError("Cannot set divisor directly")
1131+
11031132
def formatter(self, value: int) -> int | float:
1104-
"""Pass through cluster handler formatter."""
1105-
return self._cluster_handler.demand_formatter(value)
1133+
"""Metering formatter."""
1134+
# TODO: improve typing for base class
1135+
scaled_value = cast(float, super().formatter(value))
1136+
1137+
if (
1138+
self._cluster_handler.unit_of_measurement
1139+
== MeteringUnitofMeasure.Kwh_and_Kwh_binary
1140+
):
1141+
# Zigbee spec power unit is kW, but we show the value in W
1142+
value_watt = scaled_value * 1000
1143+
if value_watt < 100:
1144+
return round(value_watt, 1)
1145+
return round(value_watt)
1146+
1147+
demand_formater = create_number_formatter(
1148+
self._cluster_handler.demand_formatting
1149+
if self._cluster_handler.demand_formatting is not None
1150+
else DEFAULT_FORMATTING
1151+
)
1152+
return float(demand_formater.format(scaled_value))
11061153

11071154

11081155
@dataclass(frozen=True, kw_only=True)
@@ -1184,14 +1231,22 @@ class SmartEnergySummation(SmartEnergyMetering):
11841231
}
11851232

11861233
def formatter(self, value: int) -> int | float:
1187-
"""Numeric pass-through formatter."""
1188-
if self._cluster_handler.unit_of_measurement != 0:
1189-
return self._cluster_handler.summa_formatter(value)
1234+
"""Metering summation formatter."""
1235+
# TODO: improve typing for base class
1236+
scaled_value = cast(float, Sensor.formatter(self, value))
1237+
1238+
if (
1239+
self._cluster_handler.unit_of_measurement
1240+
== MeteringUnitofMeasure.Kwh_and_Kwh_binary
1241+
):
1242+
return scaled_value
11901243

1191-
return (
1192-
float(self._cluster_handler.multiplier * value)
1193-
/ self._cluster_handler.divisor
1244+
summation_formater = create_number_formatter(
1245+
self._cluster_handler.summation_formatting
1246+
if self._cluster_handler.summation_formatting is not None
1247+
else DEFAULT_FORMATTING
11941248
)
1249+
return float(summation_formater.format(scaled_value))
11951250

11961251

11971252
@MULTI_MATCH(

zha/application/platforms/sensor/helpers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Helpers for sensor platform."""
22

3+
import functools
34
from math import ceil, log10
45

6+
from zigpy.zcl.clusters.smartenergy import NumberFormatting
7+
58

69
def resolution_to_decimal_precision(
710
resolution: float, *, epsilon: float = 2**-23, max_digits: int = 16
@@ -27,3 +30,22 @@ def resolution_to_decimal_precision(
2730

2831
# If nothing was found, fall back to the number of decimal places in epsilon
2932
return ceil(-log10(epsilon))
33+
34+
35+
@functools.lru_cache(maxsize=32)
36+
def create_number_formatter(formatting: int) -> str:
37+
"""Return a formatting string, given the formatting value."""
38+
formatting_obj = NumberFormatting(formatting)
39+
r_digits = formatting_obj.num_digits_right_of_decimal
40+
l_digits = formatting_obj.num_digits_left_of_decimal
41+
42+
if l_digits == 0:
43+
l_digits = 15
44+
45+
width = r_digits + l_digits + (1 if r_digits > 0 else 0)
46+
47+
if formatting_obj.suppress_leading_zeros:
48+
# suppress leading 0
49+
return f"{{:{width}.{r_digits}f}}"
50+
51+
return f"{{:0{width}.{r_digits}f}}"

zha/zigbee/cluster_handlers/smartenergy.py

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import enum
6-
from functools import partialmethod
76
from typing import TYPE_CHECKING
87

98
import zigpy.zcl
@@ -17,7 +16,6 @@
1716
MduPairing,
1817
Messaging,
1918
Metering,
20-
NumberFormatting,
2119
Prepayment,
2220
Price,
2321
Tunneling,
@@ -34,13 +32,6 @@
3432
from zha.zigbee.endpoint import Endpoint
3533

3634

37-
DEFAULT_FORMATTING = NumberFormatting(
38-
num_digits_right_of_decimal=1,
39-
num_digits_left_of_decimal=15,
40-
suppress_leading_zeros=1,
41-
)
42-
43-
4435
@registries.CLUSTER_HANDLER_REGISTRY.register(Calendar.cluster_id)
4536
class CalendarClusterHandler(ClusterHandler):
4637
"""Calendar cluster handler."""
@@ -306,18 +297,15 @@ def unit_of_measurement(self) -> int:
306297
"""Return unit of measurement."""
307298
return self.cluster.get(Metering.AttributeDefs.unit_of_measure.name)
308299

309-
async def async_initialize_cluster_handler_specific(self, from_cache: bool) -> None: # pylint: disable=unused-argument
310-
"""Fetch config from device and updates format specifier."""
311-
312-
fmting = self.cluster.get(
313-
Metering.AttributeDefs.demand_formatting.name, DEFAULT_FORMATTING
314-
)
315-
self._format_spec = self.get_formatting(fmting)
300+
@property
301+
def demand_formatting(self) -> int | None:
302+
"""Return demand formatting."""
303+
return self.cluster.get(Metering.AttributeDefs.demand_formatting.name)
316304

317-
fmting = self.cluster.get(
318-
Metering.AttributeDefs.summation_formatting.name, DEFAULT_FORMATTING
319-
)
320-
self._summa_format = self.get_formatting(fmting)
305+
@property
306+
def summation_formatting(self) -> int | None:
307+
"""Return summation formatting."""
308+
return self.cluster.get(Metering.AttributeDefs.summation_formatting.name)
321309

322310
async def async_update(self) -> None:
323311
"""Retrieve latest state."""
@@ -330,44 +318,6 @@ async def async_update(self) -> None:
330318
]
331319
await self.get_attributes(attrs, from_cache=False, only_cache=False)
332320

333-
@staticmethod
334-
def get_formatting(formatting: int) -> str:
335-
"""Return a formatting string, given the formatting value."""
336-
formatting_obj = NumberFormatting(formatting)
337-
r_digits = formatting_obj.num_digits_right_of_decimal
338-
l_digits = formatting_obj.num_digits_left_of_decimal
339-
340-
if l_digits == 0:
341-
l_digits = 15
342-
343-
width = r_digits + l_digits + (1 if r_digits > 0 else 0)
344-
345-
if formatting_obj.suppress_leading_zeros:
346-
# suppress leading 0
347-
return f"{{:{width}.{r_digits}f}}"
348-
349-
return f"{{:0{width}.{r_digits}f}}"
350-
351-
def _formatter_function(
352-
self, selector: FormatSelector, value: int
353-
) -> int | float | str:
354-
"""Return formatted value for display."""
355-
value_float = value * self.multiplier / self.divisor
356-
if self.unit_of_measurement == 0:
357-
# Zigbee spec power unit is kW, but we show the value in W
358-
value_watt = value_float * 1000
359-
if value_watt < 100:
360-
return round(value_watt, 1)
361-
return round(value_watt)
362-
if selector == self.FormatSelector.SUMMATION:
363-
assert self._summa_format
364-
return float(self._summa_format.format(value_float).lstrip())
365-
assert self._format_spec
366-
return float(self._format_spec.format(value_float).lstrip())
367-
368-
demand_formatter = partialmethod(_formatter_function, FormatSelector.DEMAND)
369-
summa_formatter = partialmethod(_formatter_function, FormatSelector.SUMMATION)
370-
371321

372322
@registries.CLUSTER_HANDLER_REGISTRY.register(Prepayment.cluster_id)
373323
class PrepaymentClusterHandler(ClusterHandler):

0 commit comments

Comments
 (0)