66 SensorStateClass ,
77 SensorEntity
88)
9+ import json
10+ import hashlib
11+ import math
12+ import logging
913from homeassistant .helpers .update_coordinator import CoordinatorEntity
10- from homeassistant .const import UnitOfPower , UnitOfEnergy , UnitOfElectricCurrent , UnitOfElectricPotential
14+ from homeassistant .const import UnitOfPower , UnitOfEnergy , UnitOfElectricCurrent , UnitOfElectricPotential , PERCENTAGE
1115from homeassistant .core import HomeAssistant , callback
1216from homeassistant .helpers .entity import generate_entity_id
1317from homeassistant .util .dt import (utcnow )
1418from .const import DOMAIN , DATA_CLIENT , DATA_COORDINATORS , COORDINATOR_CHARGESESSIONS , COORDINATOR_STATISTICS , COORDINATOR_ADVANCED
1519from .coordinator import OhmeChargeSessionsCoordinator , OhmeStatisticsCoordinator , OhmeAdvancedSettingsCoordinator
1620from .utils import charge_graph_next_slot , charge_graph_slot_list
1721
22+ _LOGGER = logging .getLogger (__name__ )
23+
1824async def async_setup_entry (
1925 hass : core .HomeAssistant ,
2026 config_entry : config_entries .ConfigEntry ,
@@ -32,10 +38,12 @@ async def async_setup_entry(
3238 CurrentDrawSensor (coordinator , hass , client ),
3339 VoltageSensor (coordinator , hass , client ),
3440 CTSensor (adv_coordinator , hass , client ),
35- EnergyUsageSensor (stats_coordinator , hass , client ),
41+ EnergyUsageSensor (coordinator , hass , client ),
42+ AccumulativeEnergyUsageSensor (stats_coordinator , hass , client ),
3643 NextSlotEndSensor (coordinator , hass , client ),
3744 NextSlotStartSensor (coordinator , hass , client ),
38- SlotListSensor (coordinator , hass , client )]
45+ SlotListSensor (coordinator , hass , client ),
46+ BatterySOCSensor (coordinator , hass , client )]
3947
4048 async_add_entities (sensors , update_before_add = True )
4149
@@ -206,7 +214,7 @@ def native_value(self):
206214 return self .coordinator .data ['clampAmps' ]
207215
208216
209- class EnergyUsageSensor (CoordinatorEntity [OhmeStatisticsCoordinator ], SensorEntity ):
217+ class AccumulativeEnergyUsageSensor (CoordinatorEntity [OhmeStatisticsCoordinator ], SensorEntity ):
210218 """Sensor for total energy usage."""
211219 _attr_name = "Accumulative Energy Usage"
212220 _attr_native_unit_of_measurement = UnitOfEnergy .WATT_HOUR
@@ -252,6 +260,61 @@ def native_value(self):
252260 return None
253261
254262
263+ class EnergyUsageSensor (CoordinatorEntity [OhmeChargeSessionsCoordinator ], SensorEntity ):
264+ """Sensor for total energy usage."""
265+ _attr_name = "Session Energy Usage"
266+ _attr_native_unit_of_measurement = UnitOfEnergy .WATT_HOUR
267+ _attr_suggested_unit_of_measurement = UnitOfEnergy .KILO_WATT_HOUR
268+ _attr_suggested_display_precision = 1
269+ _attr_device_class = SensorDeviceClass .ENERGY
270+ _attr_state_class = SensorStateClass .TOTAL
271+
272+ def __init__ (
273+ self ,
274+ coordinator ,
275+ hass : HomeAssistant ,
276+ client ):
277+ super ().__init__ (coordinator = coordinator )
278+
279+ self ._state = None
280+
281+ self ._attributes = {}
282+ self ._client = client
283+
284+ self .entity_id = generate_entity_id (
285+ "sensor.{}" , "ohme_session_energy" , hass = hass )
286+
287+ self ._attr_device_info = hass .data [DOMAIN ][DATA_CLIENT ].get_device_info ()
288+
289+ @callback
290+ def _handle_coordinator_update (self ) -> None :
291+ # Ensure we have data, then ensure value is going up and above 0
292+ if self .coordinator .data and self .coordinator .data ['batterySoc' ]:
293+ new_state = self .coordinator .data ['batterySoc' ]['wh' ]
294+
295+ # Let the state reset to 0, but not drop otherwise
296+ if new_state <= 0 :
297+ self ._state = 0
298+ else :
299+ self ._state = max (0 , self ._state or 0 , new_state )
300+
301+ self .async_write_ha_state ()
302+
303+ @property
304+ def unique_id (self ) -> str :
305+ """Return the unique ID of the sensor."""
306+ return self ._client .get_unique_id ("session_energy" )
307+
308+ @property
309+ def icon (self ):
310+ """Icon of the sensor."""
311+ return "mdi:lightning-bolt-circle"
312+
313+ @property
314+ def native_value (self ):
315+ return self ._state
316+
317+
255318class NextSlotStartSensor (CoordinatorEntity [OhmeChargeSessionsCoordinator ], SensorEntity ):
256319 """Sensor for next smart charge slot start time."""
257320 _attr_name = "Next Charge Slot Start"
@@ -359,6 +422,7 @@ def _handle_coordinator_update(self) -> None:
359422class SlotListSensor (CoordinatorEntity [OhmeChargeSessionsCoordinator ], SensorEntity ):
360423 """Sensor for next smart charge slot end time."""
361424 _attr_name = "Charge Slots"
425+ _last_hash = None
362426
363427 def __init__ (
364428 self ,
@@ -393,12 +457,26 @@ def native_value(self):
393457 """Return pre-calculated state."""
394458 return self ._state
395459
460+ def _hash_rule (self ):
461+ """Generate a hashed representation of the current charge rule."""
462+ serial = json .dumps (self .coordinator .data ['appliedRule' ], sort_keys = True )
463+ sha1 = hashlib .sha1 (serial .encode ('utf-8' )).hexdigest ()
464+ return sha1
465+
396466 @callback
397467 def _handle_coordinator_update (self ) -> None :
398468 """Get a list of charge slots."""
399- if self .coordinator .data is None or self .coordinator .data ["mode" ] == "DISCONNECTED" :
469+ if self .coordinator .data is None or self .coordinator .data ["mode" ] == "DISCONNECTED" or self . coordinator . data [ "mode" ] == "FINISHED_CHARGE" :
400470 self ._state = None
471+ self ._last_hash = None
401472 else :
473+ rule_hash = self ._hash_rule ()
474+
475+ # Rule has not changed, no point evaluating slots again
476+ if rule_hash == self ._last_hash :
477+ _LOGGER .debug ("Slot evaluation skipped - rule has not changed" )
478+ return
479+
402480 slots = charge_graph_slot_list (
403481 self .coordinator .data ['startTime' ], self .coordinator .data ['chargeGraph' ]['points' ])
404482
@@ -407,6 +485,66 @@ def _handle_coordinator_update(self) -> None:
407485
408486 # Make sure we return None/Unknown if the list is empty
409487 self ._state = None if self ._state == "" else self ._state
488+
489+ # Store hash of the last rule
490+ self ._last_hash = self ._hash_rule ()
410491
411492 self ._last_updated = utcnow ()
412493 self .async_write_ha_state ()
494+
495+
496+ class BatterySOCSensor (CoordinatorEntity [OhmeChargeSessionsCoordinator ], SensorEntity ):
497+ """Sensor for car battery SOC."""
498+ _attr_name = "Battery SOC"
499+ _attr_native_unit_of_measurement = PERCENTAGE
500+ _attr_device_class = SensorDeviceClass .BATTERY
501+ _attr_suggested_display_precision = 0
502+
503+ def __init__ (
504+ self ,
505+ coordinator : OhmeChargeSessionsCoordinator ,
506+ hass : HomeAssistant ,
507+ client ):
508+ super ().__init__ (coordinator = coordinator )
509+
510+ self ._state = None
511+ self ._attributes = {}
512+ self ._last_updated = None
513+ self ._client = client
514+
515+ self .entity_id = generate_entity_id (
516+ "sensor.{}" , "ohme_battery_soc" , hass = hass )
517+
518+ self ._attr_device_info = hass .data [DOMAIN ][DATA_CLIENT ].get_device_info ()
519+
520+ @property
521+ def unique_id (self ) -> str :
522+ """Return the unique ID of the sensor."""
523+ return self ._client .get_unique_id ("battery_soc" )
524+
525+ @property
526+ def icon (self ):
527+ """Icon of the sensor. Round up to the nearest 10% icon."""
528+ nearest = math .ceil ((self ._state or 0 ) / 10.0 ) * 10
529+ if nearest == 0 :
530+ return "mdi:battery-outline"
531+ elif nearest == 100 :
532+ return "mdi:battery"
533+ else :
534+ return "mdi:battery-" + str (nearest )
535+
536+ @callback
537+ def _handle_coordinator_update (self ) -> None :
538+ """Get value from data returned from API by coordinator"""
539+ if self .coordinator .data and self .coordinator .data ['car' ] and self .coordinator .data ['car' ]['batterySoc' ]:
540+ new_state = self .coordinator .data ['car' ]['batterySoc' ]['percent' ] or self .coordinator .data ['batterySoc' ]['percent' ]
541+
542+ # Don't let it go backwards unless to 0
543+ self ._state = 0 if new_state == 0 else max (new_state , self ._state or 0 )
544+
545+ self ._last_updated = utcnow ()
546+ self .async_write_ha_state ()
547+
548+ @property
549+ def native_value (self ):
550+ return self ._state
0 commit comments