From 5469f6a719353e2dd1500bc1e0db9b9cf77e7747 Mon Sep 17 00:00:00 2001 From: Bram Gerritsen Date: Sun, 26 May 2024 09:55:44 +0200 Subject: [PATCH] fix issue where energy group calculation had minor errors because of rounding precision (#2259) * fix: issue where energy group calculation had minor errors because of rounding precision * fix: mypy --- custom_components/powercalc/sensors/group.py | 32 ++++++++--------- tests/sensors/test_group.py | 36 ++++++++++++++++++-- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/custom_components/powercalc/sensors/group.py b/custom_components/powercalc/sensors/group.py index a3b8b71d1..79c1c4ce3 100644 --- a/custom_components/powercalc/sensors/group.py +++ b/custom_components/powercalc/sensors/group.py @@ -516,6 +516,7 @@ def __init__( self.entity_id = entity_id self.source_device_id = device_id self._prev_state_store: PreviousStateStore = PreviousStateStore(hass) + self._native_value_exact = Decimal(0) async def async_added_to_hass(self) -> None: """Register state listeners.""" @@ -527,15 +528,9 @@ async def async_added_to_hass(self) -> None: last_sensor_state = await self.async_get_last_sensor_data() try: if last_sensor_state and last_sensor_state.native_value: - self._attr_native_value = round( - Decimal(last_sensor_state.native_value), # type: ignore - self._rounding_digits, - ) + self._set_native_value(Decimal(last_sensor_state.native_value)) #type: ignore elif last_state: - self._attr_native_value = round( - Decimal(last_state.state), - self._rounding_digits, - ) + self._set_native_value(Decimal(last_state.state)) _LOGGER.debug( "%s: Restoring state: %s", self.entity_id, @@ -600,7 +595,7 @@ def on_state_change(self, _: Any) -> None: # noqa if not available_states: if self._sensor_config.get(CONF_IGNORE_UNAVAILABLE_STATE): if isinstance(self, GroupedPowerSensor): - self._attr_native_value = 0 + self._set_native_value(Decimal(0)) self._attr_available = True else: self._attr_available = False @@ -608,13 +603,13 @@ def on_state_change(self, _: Any) -> None: # noqa return summed = self.calculate_new_state(available_states, states) - self._attr_native_value = round(summed, self._rounding_digits) + self._set_native_value(summed) self._attr_available = True self.async_write_ha_state() def _get_state_value_in_native_unit(self, state: State) -> Decimal: """Convert value of member entity state to match the unit of measurement of the group sensor.""" - value = float(state.state) + value = state.state unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if ( unit_of_measurement @@ -626,12 +621,17 @@ def _get_state_value_in_native_unit(self, state: State) -> Decimal: else PowerConverter ) value = unit_converter.convert( - value, + float(value), unit_of_measurement, self._attr_native_unit_of_measurement, ) return Decimal(value) + def _set_native_value(self, value: Decimal) -> None: + self._native_value_exact = value + self._attr_native_value = round(value, self._rounding_digits) + self.async_write_ha_state() + @abstractmethod def calculate_new_state( self, @@ -698,7 +698,7 @@ def __init__( async def async_reset(self) -> None: """Reset the group sensor and underlying member sensor when supported.""" _LOGGER.debug("%s: Reset grouped energy sensor", self.entity_id) - self._attr_native_value = 0 + self._set_native_value(Decimal(0)) self.async_write_ha_state() for entity_id in self._entities: @@ -718,7 +718,7 @@ async def async_reset(self) -> None: async def async_calibrate(self, value: str) -> None: _LOGGER.debug("%s: Calibrate group energy sensor to: %s", self.entity_id, value) - self._attr_native_value = Decimal(value) + self._set_native_value(Decimal(value)) self.async_write_ha_state() def calculate_new_state( @@ -729,8 +729,8 @@ def calculate_new_state( """Calculate the new group energy sensor state For each member sensor we calculate the delta by looking at the previous known state and compare it to the current. """ - group_sum = Decimal(self._attr_native_value) if self._attr_native_value else Decimal(0) # type: ignore - _LOGGER.debug("%s: Recalculate, current value: %d", self.entity_id, group_sum) + group_sum = Decimal(self._native_value_exact) if self._native_value_exact else Decimal(0) + _LOGGER.debug("%s: Recalculate, current value: %s", self.entity_id, group_sum) for entity_state in member_states: if entity_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: _LOGGER.debug( diff --git a/tests/sensors/test_group.py b/tests/sensors/test_group.py index dd7ab54b4..b8b420268 100644 --- a/tests/sensors/test_group.py +++ b/tests/sensors/test_group.py @@ -306,7 +306,7 @@ async def test_reset_service(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("sensor.testgroup_energy").state == "0" + assert hass.states.get("sensor.testgroup_energy").state == "0.0000" assert hass.states.get("sensor.test1_energy").state == "0" assert hass.states.get("sensor.test2_energy").state == "0" @@ -352,7 +352,7 @@ async def test_calibrate_service(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("sensor.testgroup_energy").state == "100" + assert hass.states.get("sensor.testgroup_energy").state == "100.0000" async def test_restore_state(hass: HomeAssistant) -> None: @@ -1128,6 +1128,36 @@ async def test_energy_sensor_delta_updates_new_sensor(hass: HomeAssistant) -> No assert hass.states.get("sensor.testgroup_energy").state == "5.3000" +async def test_delta_calculation_precision(hass: HomeAssistant) -> None: + """ + Make sure delta calculation is done on exact decimal value, not the rounded value. + See: https://github.com/bramstroker/homeassistant-powercalc/issues/2254 + """ + await _create_energy_group( + hass, + "TestGroup", + ["sensor.a_energy"], + ) + + test_values = [ + ("1197.865543", "1197.8655"), + ("1197.868021", "1197.8680"), + ("1197.868628", "1197.8686"), + ("1197.871022", "1197.8710"), + ("1197.871629", "1197.8716"), + ("1197.873903", "1197.8739"), + ("1197.874396", "1197.8744"), + ("1197.876372", "1197.8764"), + ("1197.876868", "1197.8769"), + ("1197.878879", "1197.8789"), + ("1197.879332", "1197.8793"), + ] + + for energy_state, expected_group_state in test_values: + hass.states.async_set("sensor.a_energy", energy_state, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}) + await hass.async_block_till_done() + assert hass.states.get("sensor.testgroup_energy").state == expected_group_state + async def test_energy_sensor_delta_updates_existing_sensor(hass: HomeAssistant) -> None: await _create_energy_group( hass, @@ -1428,7 +1458,7 @@ async def test_inital_group_sum_calculated(hass: HomeAssistant) -> None: group_state = hass.states.get("sensor.testgroup_power") assert group_state - assert group_state.state == "0" + assert group_state.state == "0.00" async def test_additional_energy_sensors(hass: HomeAssistant) -> None: