Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit ea0cbfd

Browse files
authored
Added battery SOC sensor, added session energy usage sensor, fixes for charge slots sensor, changes to accumulative energy sensor (#54)
* Fix dodgy next end time * Fix bug with start time sensor * Fix regressions * Fix bug with charge slot list when charge in progress * Bump version * Fix end of last slot missing bug * Over zealous rounding * Don't show slots if charge finished * Slot evaluation skip * Add battery SOC sensor * Initial implementation of live EnergyUsageSensor * Fixes to energy usage and SOC * Added session energy usage * Revert accumulative usage sensor * Allow state to drop to 0 * Use provisioning date for accumulative sensor start * Don't let SOC go backwards * Version bump
1 parent 071e972 commit ea0cbfd

File tree

4 files changed

+150
-8
lines changed

4 files changed

+150
-8
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ This integration exposes the following entities:
5757
* Sensors (Other)
5858
* CT Reading (Amps) - Reading from attached CT clamp
5959
* Accumulative Energy Usage (kWh) - Total energy used by the charger
60+
* Session Energy Usage (kWh) - Energy used in the current session
61+
* Battery State of Charge (%) - If your car is API connected this is read from the car, if not it is how much charge Ohme thinks it has added
6062
* Switches (Settings) - **Only options available to your charger model will show**
6163
* Lock Buttons - Locks buttons on charger
6264
* Require Approval - Require approval to start a charge
@@ -88,7 +90,7 @@ The coordinators are listed with their refresh intervals below. Relevant coordin
8890
* OhmeChargeSessionsCoordinator (30s refresh)
8991
* Binary Sensors: Car connected, car charging, pending approval and charge slot active
9092
* Buttons: Approve Charge
91-
* Sensors: Power, current, voltage, charge slots and next slot (start & end)
93+
* Sensors: Power, current, voltage, session energy usage, charge slots, and next slot (start & end)
9294
* Switches: Max charge, pause charge
9395
* Inputs: Target time, target percentage and preconditioning (If car connected)
9496
* OhmeAccountInfoCoordinator (1m refresh)

custom_components/ohme/api_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def __init__(self, email, password):
2727
self._device_info = None
2828
self._capabilities = {}
2929
self._ct_connected = False
30+
self._provision_date = None
3031

3132
# Authentication
3233
self._token_birth = 0
@@ -312,13 +313,14 @@ async def async_update_device_info(self, is_retry=False):
312313
self._user_id = resp['user']['id']
313314
self._serial = device['id']
314315
self._device_info = info
316+
self._provision_date = device['provisioningTs']
315317

316318
return True
317319

318320
async def async_get_charge_statistics(self):
319321
"""Get charge statistics. Currently this is just for all time (well, Jan 2019)."""
320322
end_ts = self._last_second_of_month_timestamp()
321-
resp = await self._get_request(f"/v1/chargeSessions/summary/users/{self._user_id}?&startTs=1546300800000&endTs={end_ts}&granularity=MONTH")
323+
resp = await self._get_request(f"/v1/chargeSessions/summary/users/{self._user_id}?&startTs={self._provision_date}&endTs={end_ts}&granularity=MONTH")
322324

323325
return resp['totalStats']
324326

custom_components/ohme/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Component constants"""
22
DOMAIN = "ohme"
33
USER_AGENT = "dan-r-homeassistant-ohme"
4-
INTEGRATION_VERSION = "0.5.1"
4+
INTEGRATION_VERSION = "0.6.0"
55
CONFIG_VERSION = 1
66
ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"]
77

custom_components/ohme/sensor.py

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@
66
SensorStateClass,
77
SensorEntity
88
)
9+
import json
10+
import hashlib
11+
import math
12+
import logging
913
from 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
1115
from homeassistant.core import HomeAssistant, callback
1216
from homeassistant.helpers.entity import generate_entity_id
1317
from homeassistant.util.dt import (utcnow)
1418
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_STATISTICS, COORDINATOR_ADVANCED
1519
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAdvancedSettingsCoordinator
1620
from .utils import charge_graph_next_slot, charge_graph_slot_list
1721

22+
_LOGGER = logging.getLogger(__name__)
23+
1824
async 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+
255318
class 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:
359422
class 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

Comments
 (0)