diff --git a/README.md b/README.md index 8dd66d7..4617352 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ This integration exposes the following entities: * Next Charge Slot End - The next time your car will stop charging according to the Ohme-generated charge plan * Sensors (Other) * CT Reading (Amps) - Reading from attached CT clamp - * Accumulative Energy Usage (kWh) - Total energy used by the charger * Session Energy Usage (kWh) - Energy used in the current session + * Accumulative Energy Usage (kWh) - Total energy used by the charger (If enabled in options) * 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 * Switches (Settings) - **Only options available to your charger model will show** * Lock Buttons - Locks buttons on charger @@ -84,6 +84,7 @@ This integration exposes the following entities: ## Options Some options can be set from the 'Configure' menu in Home Assistant: * Never update an ongoing session - Override the default behaviour of the target time, percentage and preconditioning inputs and only ever update the schedule, not the current session. This was added as changing the current session can cause issues for customers on Intelligent Octopus Go. +* Enable accumulative energy usage sensor - Enable the sensor showing an all-time incrementing energy usage counter. This causes issues with some accounts. ## Coordinators diff --git a/custom_components/ohme/__init__.py b/custom_components/ohme/__init__.py index 732118d..185fcbd 100644 --- a/custom_components/ohme/__init__.py +++ b/custom_components/ohme/__init__.py @@ -1,6 +1,7 @@ import logging from homeassistant import core from .const import * +from .utils import get_option from .api_client import OhmeApiClient from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAccountInfoCoordinator, OhmeAdvancedSettingsCoordinator, OhmeChargeSchedulesCoordinator from homeassistant.exceptions import ConfigEntryNotReady @@ -25,12 +26,9 @@ async def async_setup_dependencies(hass, entry): async def async_update_listener(hass, entry): """Handle options flow credentials update.""" - # Re-instantiate the API client - await async_setup_dependencies(hass, entry) - - # Refresh all coordinators for good measure - for coordinator in hass.data[DOMAIN][DATA_COORDINATORS]: - await coordinator.async_refresh() + + # Reload this instance + await hass.config_entries.async_reload(entry.entry_id) async def async_setup_entry(hass, entry): @@ -53,7 +51,24 @@ async def async_setup_entry(hass, entry): OhmeAdvancedSettingsCoordinator ] + coordinators_skipped = [] + + # Skip statistics coordinator if we don't need it + if not get_option(hass, "enable_accumulative_energy"): + coordinators_skipped.append(OhmeStatisticsCoordinator) + for coordinator in coordinators: + # If we should skip this coordinator + skip = False + for skipped in coordinators_skipped: + if isinstance(coordinator, skipped): + skip = True + break + + if skip: + _LOGGER.debug(f"Skipping initial load of {coordinator.__class__.__name__}") + continue + # Catch failures if this is an 'optional' coordinator try: await coordinator.async_config_entry_first_refresh() diff --git a/custom_components/ohme/config_flow.py b/custom_components/ohme/config_flow.py index 3cef071..fb3ad2a 100644 --- a/custom_components/ohme/config_flow.py +++ b/custom_components/ohme/config_flow.py @@ -86,6 +86,9 @@ async def async_step_init(self, options): ): str, vol.Required( "never_session_specific", default=self._config_entry.options.get("never_session_specific", False) + ) : bool, + vol.Required( + "enable_accumulative_energy", default=self._config_entry.options.get("enable_accumulative_energy", False) ) : bool }), errors=errors ) diff --git a/custom_components/ohme/const.py b/custom_components/ohme/const.py index 31065c4..8f30ab4 100644 --- a/custom_components/ohme/const.py +++ b/custom_components/ohme/const.py @@ -1,7 +1,7 @@ """Component constants""" DOMAIN = "ohme" USER_AGENT = "dan-r-homeassistant-ohme" -INTEGRATION_VERSION = "0.6.1" +INTEGRATION_VERSION = "0.7.0" CONFIG_VERSION = 1 ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"] diff --git a/custom_components/ohme/sensor.py b/custom_components/ohme/sensor.py index a31bb54..8d0868d 100644 --- a/custom_components/ohme/sensor.py +++ b/custom_components/ohme/sensor.py @@ -7,7 +7,6 @@ SensorEntity ) import json -import hashlib import math import logging from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -17,7 +16,7 @@ from homeassistant.util.dt import (utcnow) from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, DATA_SLOTS, COORDINATOR_CHARGESESSIONS, COORDINATOR_STATISTICS, COORDINATOR_ADVANCED from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAdvancedSettingsCoordinator -from .utils import charge_graph_next_slot, charge_graph_slot_list +from .utils import charge_graph_next_slot, charge_graph_slot_list, get_option _LOGGER = logging.getLogger(__name__) @@ -39,11 +38,13 @@ async def async_setup_entry( VoltageSensor(coordinator, hass, client), CTSensor(adv_coordinator, hass, client), EnergyUsageSensor(coordinator, hass, client), - AccumulativeEnergyUsageSensor(stats_coordinator, hass, client), NextSlotEndSensor(coordinator, hass, client), NextSlotStartSensor(coordinator, hass, client), SlotListSensor(coordinator, hass, client), BatterySOCSensor(coordinator, hass, client)] + + if get_option(hass, "enable_accumulative_energy"): + sensors.append(AccumulativeEnergyUsageSensor(stats_coordinator, hass, client)) async_add_entities(sensors, update_before_add=True) @@ -293,7 +294,7 @@ def _handle_coordinator_update(self) -> None: new_state = self.coordinator.data['batterySoc']['wh'] # Let the state reset to 0, but not drop otherwise - if new_state <= 0: + if not new_state or new_state <= 0: self._state = 0 else: self._state = max(0, self._state or 0, new_state) @@ -422,7 +423,6 @@ def _handle_coordinator_update(self) -> None: class SlotListSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): """Sensor for next smart charge slot end time.""" _attr_name = "Charge Slots" - _last_hash = None def __init__( self, @@ -458,27 +458,13 @@ def native_value(self): """Return pre-calculated state.""" return self._state - def _hash_rule(self): - """Generate a hashed representation of the current charge rule.""" - serial = json.dumps(self.coordinator.data['appliedRule'], sort_keys=True) - sha1 = hashlib.sha1(serial.encode('utf-8')).hexdigest() - return sha1 - @callback def _handle_coordinator_update(self) -> None: """Get a list of charge slots.""" if self.coordinator.data is None or self.coordinator.data["mode"] == "DISCONNECTED" or self.coordinator.data["mode"] == "FINISHED_CHARGE": self._state = None - self._last_hash = None self._hass.data[DOMAIN][DATA_SLOTS] = [] else: - rule_hash = self._hash_rule() - - # Rule has not changed, no point evaluating slots again - if rule_hash == self._last_hash: - _LOGGER.debug("Slot evaluation skipped - rule has not changed") - return - slots = charge_graph_slot_list( self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points']) @@ -490,9 +476,6 @@ def _handle_coordinator_update(self) -> None: # Make sure we return None/Unknown if the list is empty self._state = None if self._state == "" else self._state - - # Store hash of the last rule - self._last_hash = self._hash_rule() self._last_updated = utcnow() self.async_write_ha_state() diff --git a/custom_components/ohme/translations/en.json b/custom_components/ohme/translations/en.json index 3d8d8f0..6c05720 100644 --- a/custom_components/ohme/translations/en.json +++ b/custom_components/ohme/translations/en.json @@ -22,7 +22,8 @@ "data": { "email": "Email address", "password": "Password", - "never_session_specific": "Never update an ongoing session" + "never_session_specific": "Never update an ongoing session", + "enable_accumulative_energy": "Enable accumulative energy sensor" }, "data_description": { "password": "If you are not changing your credentials, leave the password field empty.", diff --git a/custom_components/ohme/utils.py b/custom_components/ohme/utils.py index 1cb18d6..1648727 100644 --- a/custom_components/ohme/utils.py +++ b/custom_components/ohme/utils.py @@ -42,16 +42,6 @@ def _sanitise_points(points): return output -def _charge_finished(data): - """Is the charge finished?""" - now = int(time()) - data = [x['y'] for x in data if x["t"] > now] - - if len(data) == 0 or min(data) == max(data): - return True - return False - - def _next_slot(data, live=False, in_progress=False): """Get the next slot. live is whether or not we may start mid charge. Eg: For the next slot end sensor, we dont have the start but still want the end of the in progress session, but for the slot list sensor we only want slots that have @@ -117,7 +107,7 @@ def charge_graph_slot_list(charge_start, points, skip_format=False): data = points if skip_format else _format_charge_graph(charge_start, points) # Don't return any slots if charge is over - if _charge_finished(data): + if charge_graph_next_slot(charge_start, points)['end'] is None: return [] data = _sanitise_points(data) @@ -199,6 +189,6 @@ def session_in_progress(hass, data): return True -def get_option(hass, option): - """Return option value, default to False.""" - return hass.data[DOMAIN][DATA_OPTIONS].get(option, None) +def get_option(hass, option, default=False): + """Return option value, with settable default.""" + return hass.data[DOMAIN][DATA_OPTIONS].get(option, default)