Skip to content

Commit 38fc2f4

Browse files
committed
#162528: Use SensorStateClass.TOTAL with last_reset for Tesla energy sensors
1 parent 519e915 commit 38fc2f4

File tree

12 files changed

+635
-151
lines changed

12 files changed

+635
-151
lines changed

homeassistant/components/tesla_fleet/coordinator.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
if TYPE_CHECKING:
2626
from . import TeslaFleetConfigEntry
2727

28+
from homeassistant.util import dt as dt_util
29+
2830
from .const import ENERGY_HISTORY_FIELDS, LOGGER, TeslaFleetState
2931

3032
VEHICLE_INTERVAL_SECONDS = 600
@@ -261,16 +263,20 @@ async def _async_update_data(self) -> dict[str, Any]:
261263
if not data or not isinstance(data.get("time_series"), list):
262264
raise UpdateFailed("Received invalid data")
263265

266+
time_series = data["time_series"]
267+
264268
# Add all time periods together
265-
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
266-
for period in data.get("time_series", []):
269+
output: dict[str, Any] = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
270+
for period in time_series:
267271
for key in ENERGY_HISTORY_FIELDS:
268272
if key in period:
269273
if output[key] is None:
270274
output[key] = period[key]
271275
else:
272276
output[key] += period[key]
273277

278+
output["_period_start"] = dt_util.parse_datetime(time_series[0]["timestamp"])
279+
274280
return output
275281

276282

homeassistant/components/tesla_fleet/sensor.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747

4848
PARALLEL_UPDATES = 0
4949

50+
CHARGE_ENERGY_RESET_KEYS = frozenset({"charge_state_charge_energy_added"})
51+
CHARGE_ENERGY_RESET_THRESHOLD = 1.0 # kWh
52+
5053
CHARGE_STATES = {
5154
"Starting": "starting",
5255
"Charging": "charging",
@@ -88,7 +91,7 @@ class TeslaFleetSensorEntityDescription(SensorEntityDescription):
8891
),
8992
TeslaFleetSensorEntityDescription(
9093
key="charge_state_charge_energy_added",
91-
state_class=SensorStateClass.TOTAL_INCREASING,
94+
state_class=SensorStateClass.TOTAL,
9295
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
9396
device_class=SensorDeviceClass.ENERGY,
9497
suggested_display_precision=1,
@@ -424,7 +427,7 @@ class TeslaFleetTimeEntityDescription(SensorEntityDescription):
424427
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
425428
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
426429
suggested_display_precision=2,
427-
state_class=SensorStateClass.TOTAL_INCREASING,
430+
state_class=SensorStateClass.TOTAL,
428431
entity_registry_enabled_default=(
429432
key.startswith("total") or key == "grid_energy_imported"
430433
),
@@ -497,6 +500,7 @@ class TeslaFleetVehicleSensorEntity(TeslaFleetVehicleEntity, RestoreSensor):
497500
"""Base class for Tesla Fleet vehicle metric sensors."""
498501

499502
entity_description: TeslaFleetSensorEntityDescription
503+
_previous_value: float | None = None
500504

501505
def __init__(
502506
self,
@@ -513,11 +517,30 @@ async def async_added_to_hass(self) -> None:
513517
if self.coordinator.data.get("state") != TeslaFleetState.ONLINE:
514518
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
515519
self._attr_native_value = sensor_data.native_value
520+
if isinstance(sensor_data.native_value, float | int):
521+
self._previous_value = float(sensor_data.native_value)
522+
523+
if (
524+
self.entity_description.key in CHARGE_ENERGY_RESET_KEYS
525+
and (last_state := await self.async_get_last_state()) is not None
526+
and (last_reset := last_state.attributes.get("last_reset")) is not None
527+
):
528+
self._attr_last_reset = dt_util.parse_datetime(str(last_reset))
516529

517530
def _async_update_attrs(self) -> None:
518531
"""Update the attributes of the sensor."""
519532
if self.has:
520-
self._attr_native_value = self.entity_description.value_fn(self._value)
533+
new_value = self.entity_description.value_fn(self._value)
534+
if self.entity_description.key in CHARGE_ENERGY_RESET_KEYS and isinstance(
535+
new_value, float | int
536+
):
537+
if self._previous_value is not None and (
538+
new_value == 0
539+
or new_value < self._previous_value - CHARGE_ENERGY_RESET_THRESHOLD
540+
):
541+
self._attr_last_reset = dt_util.utcnow()
542+
self._previous_value = float(new_value)
543+
self._attr_native_value = new_value
521544
else:
522545
self._attr_native_value = None
523546

@@ -584,6 +607,7 @@ def __init__(
584607
def _async_update_attrs(self) -> None:
585608
"""Update the attributes of the sensor."""
586609
self._attr_native_value = self._value
610+
self._attr_last_reset = self.coordinator.data.get("_period_start")
587611

588612

589613
class TeslaFleetWallConnectorSensorEntity(TeslaFleetWallConnectorEntity, SensorEntity):

homeassistant/components/teslemetry/coordinator.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
if TYPE_CHECKING:
2525
from . import TeslemetryConfigEntry
2626

27+
from homeassistant.util import dt as dt_util
28+
2729
from .const import DOMAIN, ENERGY_HISTORY_FIELDS, LOGGER
2830
from .helpers import flatten
2931

@@ -300,14 +302,18 @@ async def _async_update_data(self) -> dict[str, Any]:
300302
translation_key="update_failed_invalid_data",
301303
)
302304

305+
time_series = data["time_series"]
306+
303307
# Add all time periods together
304-
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
305-
for period in data.get("time_series", []):
308+
output: dict[str, Any] = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
309+
for period in time_series:
306310
for key in ENERGY_HISTORY_FIELDS:
307311
if key in period:
308312
if output[key] is None:
309313
output[key] = period[key]
310314
else:
311315
output[key] += period[key]
312316

317+
output["_period_start"] = dt_util.parse_datetime(time_series[0]["timestamp"])
318+
313319
return output

homeassistant/components/teslemetry/sensor.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949

5050
PARALLEL_UPDATES = 0
5151

52+
CHARGE_ENERGY_RESET_KEYS = frozenset(
53+
{"charge_state_charge_energy_added", "dc_charging_energy_in"}
54+
)
55+
CHARGE_ENERGY_RESET_THRESHOLD = 1.0 # kWh
56+
5257
BMS_STATES = {
5358
"Standby": "standby",
5459
"Drive": "drive",
@@ -238,7 +243,7 @@ class TeslemetryVehicleSensorEntityDescription(SensorEntityDescription):
238243
streaming_listener=lambda vehicle, callback: vehicle.listen_ACChargingEnergyIn(
239244
callback
240245
),
241-
state_class=SensorStateClass.TOTAL_INCREASING,
246+
state_class=SensorStateClass.TOTAL,
242247
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
243248
device_class=SensorDeviceClass.ENERGY,
244249
suggested_display_precision=1,
@@ -612,7 +617,7 @@ class TeslemetryVehicleSensorEntityDescription(SensorEntityDescription):
612617
streaming_listener=lambda vehicle, callback: vehicle.listen_DCChargingEnergyIn(
613618
callback
614619
),
615-
state_class=SensorStateClass.TOTAL_INCREASING,
620+
state_class=SensorStateClass.TOTAL,
616621
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
617622
device_class=SensorDeviceClass.ENERGY,
618623
entity_category=EntityCategory.DIAGNOSTIC,
@@ -1564,7 +1569,7 @@ class TeslemetrySensorEntityDescription(SensorEntityDescription):
15641569
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
15651570
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
15661571
suggested_display_precision=2,
1567-
state_class=SensorStateClass.TOTAL_INCREASING,
1572+
state_class=SensorStateClass.TOTAL,
15681573
entity_registry_enabled_default=(
15691574
key.startswith("total") or key == "grid_energy_imported"
15701575
),
@@ -1650,6 +1655,7 @@ class TeslemetryStreamSensorEntity(TeslemetryVehicleStreamEntity, RestoreSensor)
16501655
"""Base class for Teslemetry vehicle streaming sensors."""
16511656

16521657
entity_description: TeslemetryVehicleSensorEntityDescription
1658+
_previous_value: float | None = None
16531659

16541660
def __init__(
16551661
self,
@@ -1666,6 +1672,15 @@ async def async_added_to_hass(self) -> None:
16661672

16671673
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
16681674
self._attr_native_value = sensor_data.native_value
1675+
if isinstance(sensor_data.native_value, float | int):
1676+
self._previous_value = float(sensor_data.native_value)
1677+
1678+
if (
1679+
self.entity_description.key in CHARGE_ENERGY_RESET_KEYS
1680+
and (last_state := await self.async_get_last_state()) is not None
1681+
and (last_reset := last_state.attributes.get("last_reset")) is not None
1682+
):
1683+
self._attr_last_reset = dt_util.parse_datetime(str(last_reset))
16691684

16701685
if self.entity_description.streaming_listener is not None:
16711686
self.async_on_remove(
@@ -1676,6 +1691,15 @@ async def async_added_to_hass(self) -> None:
16761691

16771692
def _async_value_from_stream(self, value: StateType) -> None:
16781693
"""Update the value of the entity."""
1694+
if self.entity_description.key in CHARGE_ENERGY_RESET_KEYS and isinstance(
1695+
value, float | int
1696+
):
1697+
if self._previous_value is not None and (
1698+
value == 0
1699+
or value < self._previous_value - CHARGE_ENERGY_RESET_THRESHOLD
1700+
):
1701+
self._attr_last_reset = dt_util.utcnow()
1702+
self._previous_value = float(value)
16791703
self._attr_native_value = value
16801704
self.async_write_ha_state()
16811705

@@ -1684,6 +1708,7 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehiclePollingEntity, SensorEntity
16841708
"""Base class for Teslemetry vehicle metric sensors."""
16851709

16861710
entity_description: TeslemetryVehicleSensorEntityDescription
1711+
_previous_value: float | None = None
16871712

16881713
def __init__(
16891714
self,
@@ -1698,9 +1723,17 @@ def _async_update_attrs(self) -> None:
16981723
"""Update the attributes of the sensor."""
16991724
if self.entity_description.nullable or self._value is not None:
17001725
self._attr_available = True
1701-
self._attr_native_value = self.entity_description.polling_value_fn(
1702-
self._value
1703-
)
1726+
new_value = self.entity_description.polling_value_fn(self._value)
1727+
if self.entity_description.key in CHARGE_ENERGY_RESET_KEYS and isinstance(
1728+
new_value, float | int
1729+
):
1730+
if self._previous_value is not None and (
1731+
new_value == 0
1732+
or new_value < self._previous_value - CHARGE_ENERGY_RESET_THRESHOLD
1733+
):
1734+
self._attr_last_reset = dt_util.utcnow()
1735+
self._previous_value = float(new_value)
1736+
self._attr_native_value = new_value
17041737
else:
17051738
self._attr_available = False
17061739
self._attr_native_value = None
@@ -1852,6 +1885,7 @@ def __init__(
18521885
def _async_update_attrs(self) -> None:
18531886
"""Update the attributes of the sensor."""
18541887
self._attr_native_value = self._value
1888+
self._attr_last_reset = self.coordinator.data.get("_period_start")
18551889

18561890

18571891
class TeslemetryCreditBalanceSensor(RestoreSensor):

homeassistant/components/tessie/sensor.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class TessieSensorEntityDescription(SensorEntityDescription):
7070
),
7171
TessieSensorEntityDescription(
7272
key="charge_state_charge_energy_added",
73-
state_class=SensorStateClass.TOTAL_INCREASING,
73+
state_class=SensorStateClass.TOTAL,
7474
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
7575
device_class=SensorDeviceClass.ENERGY,
7676
suggested_display_precision=1,
@@ -378,6 +378,9 @@ class TessieSensorEntityDescription(SensorEntityDescription):
378378

379379
PARALLEL_UPDATES = 0
380380

381+
CHARGE_ENERGY_RESET_KEYS = frozenset({"charge_state_charge_energy_added"})
382+
CHARGE_ENERGY_RESET_THRESHOLD = 1.0 # kWh
383+
381384

382385
async def async_setup_entry(
383386
hass: HomeAssistant,
@@ -424,6 +427,7 @@ class TessieVehicleSensorEntity(TessieEntity, SensorEntity):
424427
"""Base class for Tessie sensor entities."""
425428

426429
entity_description: TessieSensorEntityDescription
430+
_previous_native_value: float | None = None
427431

428432
def __init__(
429433
self,
@@ -434,6 +438,20 @@ def __init__(
434438
self.entity_description = description
435439
super().__init__(vehicle, description.key)
436440

441+
def _async_update_attrs(self) -> None:
442+
"""Update the attributes of the sensor."""
443+
if self.entity_description.key in CHARGE_ENERGY_RESET_KEYS:
444+
raw_value = self.get()
445+
if isinstance(raw_value, float | int):
446+
new_value = float(raw_value)
447+
if self._previous_native_value is not None and (
448+
new_value == 0
449+
or new_value
450+
< self._previous_native_value - CHARGE_ENERGY_RESET_THRESHOLD
451+
):
452+
self._attr_last_reset = dt_util.utcnow()
453+
self._previous_native_value = new_value
454+
437455
@property
438456
def native_value(self) -> StateType | datetime:
439457
"""Return the state of the sensor."""

0 commit comments

Comments
 (0)