Skip to content

Commit

Permalink
Frequency configuration and data fix (#101)
Browse files Browse the repository at this point in the history
* Added debug logging to EnergyUsageSensor

* Remove deprecated acculumative energy sensor

* Remove outdated hardware version numbers

* Configurable refresh intervals

* Bump version

* Messy debug logging for EnergyUsageSensor

* Change source of energy sensor and amend logic

* Fix None comparison
  • Loading branch information
dan-r authored Nov 20, 2024
1 parent 66bd621 commit c07c8a5
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 134 deletions.
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ If you find any bugs or would like to request a feature, please open an issue.

## Tested Hardware
This integration has been tested with the following hardware:
* Ohme Home Pro [v1.32]
* Ohme Home [v1.32]
* Ohme Go [v1.32]
* Ohme ePod [v2.12]
* Ohme Home Pro
* Ohme Home
* Ohme Go
* Ohme ePod

## External Software
The 'Charge Slot Active' binary sensor mimics the `planned_dispatches` and `completed_dispatches` attributes from the [Octopus Energy](https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy) integration, so should support external software which reads this such as [predbat](https://springfall2008.github.io/batpred/devices/#ohme).
Expand Down Expand Up @@ -57,7 +57,6 @@ This integration exposes the following entities:
* Sensors (Other)
* CT Reading (Amps) - Reading from attached CT clamp
* Energy Usage (kWh) - Energy used in the current/last session. *This is supported by the energy dashboard.*
* Accumulative Energy Usage (kWh) - Deprecated - 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
Expand All @@ -82,7 +81,7 @@ This integration exposes the following entities:
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.
* Don't collapse charge slots - By default, adjacent slots are merged into one. This option shows every slot, as shown in the Ohme app.
* Enable accumulative energy usage sensor - Enable the sensor showing an all-time incrementing energy usage counter. This causes issues with some accounts.
* Refresh Intervals - The refresh interval for the four coordinators listed below can be configured manually. The default times also serve as minimums, as to be respectful to Ohme, but you can choose to fetch data less frequently.


## Coordinators
Expand All @@ -102,7 +101,5 @@ The coordinators are listed with their refresh intervals below. Relevant coordin
* OhmeAdvancedSettingsCoordinator (1m refresh)
* Sensors: CT reading sensor
* Binary Sensors: Charger online
* OhmeStatisticsCoordinator (30m refresh)
* Sensors: Accumulative energy usage
* OhmeChargeSchedulesCoordinator (10m refresh)
* Inputs: Target time, target percentage and preconditioning (If car disconnected)
21 changes: 1 addition & 20 deletions custom_components/ohme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .const import *
from .utils import get_option
from .api_client import OhmeApiClient
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAccountInfoCoordinator, OhmeAdvancedSettingsCoordinator, OhmeChargeSchedulesCoordinator
from .coordinator import OhmeChargeSessionsCoordinator, OhmeAccountInfoCoordinator, OhmeAdvancedSettingsCoordinator, OhmeChargeSchedulesCoordinator
from homeassistant.exceptions import ConfigEntryNotReady

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -40,35 +40,16 @@ async def async_setup_entry(hass, entry):
coordinators = [
OhmeChargeSessionsCoordinator(hass=hass), # COORDINATOR_CHARGESESSIONS
OhmeAccountInfoCoordinator(hass=hass), # COORDINATOR_ACCOUNTINFO
OhmeStatisticsCoordinator(hass=hass), # COORDINATOR_STATISTICS
OhmeAdvancedSettingsCoordinator(hass=hass), # COORDINATOR_ADVANCED
OhmeChargeSchedulesCoordinator(hass=hass) # COORDINATOR_SCHEDULES
]

# We can function without these so setup can continue
coordinators_optional = [
OhmeStatisticsCoordinator,
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()
Expand Down
15 changes: 0 additions & 15 deletions custom_components/ohme/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,6 @@ async def async_refresh_session(self):

# Internal methods

def _last_second_of_month_timestamp(self):
"""Get the last second of this month."""
dt = datetime.today()
dt = dt.replace(day=1) + timedelta(days=32)
dt = dt.replace(day=1, hour=0, minute=0, second=0,
microsecond=0) - timedelta(seconds=1)
return int(dt.timestamp()*1e3)

async def _handle_api_error(self, url, resp):
"""Raise an exception if API response failed."""
if resp.status != 200:
Expand Down Expand Up @@ -335,13 +327,6 @@ async def async_update_device_info(self, is_retry=False):

return True

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

return resp['totalStats']

async def async_get_advanced_settings(self):
"""Get advanced settings (mainly for CT clamp reading)"""
resp = await self._get_request(f"/v1/chargeDevices/{self._serial}/advancedSettings")
Expand Down
15 changes: 12 additions & 3 deletions custom_components/ohme/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import voluptuous as vol
from homeassistant.config_entries import (ConfigFlow, OptionsFlow)
from .const import DOMAIN, CONFIG_VERSION
from .const import DOMAIN, CONFIG_VERSION, DEFAULT_INTERVAL_CHARGESESSIONS, DEFAULT_INTERVAL_ACCOUNTINFO, DEFAULT_INTERVAL_ADVANCED, DEFAULT_INTERVAL_SCHEDULES
from .api_client import OhmeApiClient


Expand Down Expand Up @@ -91,7 +91,16 @@ async def async_step_init(self, options):
"never_collapse_slots", default=self._config_entry.options.get("never_collapse_slots", False)
) : bool,
vol.Required(
"enable_accumulative_energy", default=self._config_entry.options.get("enable_accumulative_energy", False)
) : bool
"interval_chargesessions", default=self._config_entry.options.get("interval_chargesessions", DEFAULT_INTERVAL_CHARGESESSIONS)
) : vol.All(vol.Coerce(float), vol.Clamp(min=DEFAULT_INTERVAL_CHARGESESSIONS)),
vol.Required(
"interval_accountinfo", default=self._config_entry.options.get("interval_accountinfo", DEFAULT_INTERVAL_ACCOUNTINFO)
) : vol.All(vol.Coerce(float), vol.Clamp(min=DEFAULT_INTERVAL_ACCOUNTINFO)),
vol.Required(
"interval_advanced", default=self._config_entry.options.get("interval_advanced", DEFAULT_INTERVAL_ADVANCED)
) : vol.All(vol.Coerce(float), vol.Clamp(min=DEFAULT_INTERVAL_ADVANCED)),
vol.Required(
"interval_schedules", default=self._config_entry.options.get("interval_schedules", DEFAULT_INTERVAL_SCHEDULES)
) : vol.All(vol.Coerce(float), vol.Clamp(min=DEFAULT_INTERVAL_SCHEDULES))
}), errors=errors
)
12 changes: 8 additions & 4 deletions custom_components/ohme/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Component constants"""
DOMAIN = "ohme"
USER_AGENT = "dan-r-homeassistant-ohme"
INTEGRATION_VERSION = "1.0.0"
INTEGRATION_VERSION = "1.0.1"
CONFIG_VERSION = 1
ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"]

Expand All @@ -12,6 +12,10 @@

COORDINATOR_CHARGESESSIONS = 0
COORDINATOR_ACCOUNTINFO = 1
COORDINATOR_STATISTICS = 2
COORDINATOR_ADVANCED = 3
COORDINATOR_SCHEDULES = 4
COORDINATOR_ADVANCED = 2
COORDINATOR_SCHEDULES = 3

DEFAULT_INTERVAL_CHARGESESSIONS = 0.5
DEFAULT_INTERVAL_ACCOUNTINFO = 1
DEFAULT_INTERVAL_ADVANCED = 1
DEFAULT_INTERVAL_SCHEDULES = 10
43 changes: 15 additions & 28 deletions custom_components/ohme/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
UpdateFailed
)

from .const import DOMAIN, DATA_CLIENT
from .const import DOMAIN, DATA_CLIENT, DEFAULT_INTERVAL_CHARGESESSIONS, DEFAULT_INTERVAL_ACCOUNTINFO, DEFAULT_INTERVAL_ADVANCED, DEFAULT_INTERVAL_SCHEDULES
from .utils import get_option

_LOGGER = logging.getLogger(__name__)

Expand All @@ -20,7 +21,9 @@ def __init__(self, hass):
hass,
_LOGGER,
name="Ohme Charge Sessions",
update_interval=timedelta(seconds=30),
update_interval=timedelta(minutes=
get_option(hass, "interval_chargesessions", DEFAULT_INTERVAL_CHARGESESSIONS)
),
)
self._client = hass.data[DOMAIN][DATA_CLIENT]

Expand All @@ -42,7 +45,9 @@ def __init__(self, hass):
hass,
_LOGGER,
name="Ohme Account Info",
update_interval=timedelta(minutes=1),
update_interval=timedelta(minutes=
get_option(hass, "interval_accountinfo", DEFAULT_INTERVAL_ACCOUNTINFO)
),
)
self._client = hass.data[DOMAIN][DATA_CLIENT]

Expand All @@ -55,28 +60,6 @@ async def _async_update_data(self):
raise UpdateFailed("Error communicating with API")


class OhmeStatisticsCoordinator(DataUpdateCoordinator):
"""Coordinator to update statistics from API periodically.
(But less so than the others)"""

def __init__(self, hass):
"""Initialise coordinator."""
super().__init__(
hass,
_LOGGER,
name="Ohme Charger Statistics",
update_interval=timedelta(minutes=30),
)
self._client = hass.data[DOMAIN][DATA_CLIENT]

async def _async_update_data(self):
"""Fetch data from API endpoint."""
try:
return await self._client.async_get_charge_statistics()

except BaseException:
raise UpdateFailed("Error communicating with API")

class OhmeAdvancedSettingsCoordinator(DataUpdateCoordinator):
"""Coordinator to pull CT clamp reading."""

Expand All @@ -86,7 +69,9 @@ def __init__(self, hass):
hass,
_LOGGER,
name="Ohme Advanced Settings",
update_interval=timedelta(minutes=1),
update_interval=timedelta(minutes=
get_option(hass, "interval_advanced", DEFAULT_INTERVAL_ADVANCED)
),
)
self._client = hass.data[DOMAIN][DATA_CLIENT]

Expand All @@ -98,6 +83,7 @@ async def _async_update_data(self):
except BaseException:
raise UpdateFailed("Error communicating with API")


class OhmeChargeSchedulesCoordinator(DataUpdateCoordinator):
"""Coordinator to pull charge schedules."""

Expand All @@ -107,7 +93,9 @@ def __init__(self, hass):
hass,
_LOGGER,
name="Ohme Charge Schedules",
update_interval=timedelta(minutes=10),
update_interval=timedelta(minutes=
get_option(hass, "interval_schedules", DEFAULT_INTERVAL_SCHEDULES)
),
)
self._client = hass.data[DOMAIN][DATA_CLIENT]

Expand All @@ -118,4 +106,3 @@ async def _async_update_data(self):

except BaseException:
raise UpdateFailed("Error communicating with API")

70 changes: 16 additions & 54 deletions custom_components/ohme/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import generate_entity_id
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 .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, DATA_SLOTS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ADVANCED
from .coordinator import OhmeChargeSessionsCoordinator, OhmeAdvancedSettingsCoordinator
from .utils import next_slot, get_option, slot_list, slot_list_str

_LOGGER = logging.getLogger(__name__)
Expand All @@ -28,7 +28,6 @@ async def async_setup_entry(
coordinators = hass.data[DOMAIN][DATA_COORDINATORS]

coordinator = coordinators[COORDINATOR_CHARGESESSIONS]
stats_coordinator = coordinators[COORDINATOR_STATISTICS]
adv_coordinator = coordinators[COORDINATOR_ADVANCED]

sensors = [PowerDrawSensor(coordinator, hass, client),
Expand All @@ -41,9 +40,6 @@ async def async_setup_entry(
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)


Expand Down Expand Up @@ -213,52 +209,6 @@ def native_value(self):
return self.coordinator.data['clampAmps']


class AccumulativeEnergyUsageSensor(CoordinatorEntity[OhmeStatisticsCoordinator], SensorEntity):
"""Sensor for total energy usage."""
_attr_name = "Accumulative Energy Usage"
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_suggested_display_precision = 1
_attr_device_class = SensorDeviceClass.ENERGY
_attr_state_class = SensorStateClass.TOTAL

def __init__(
self,
coordinator: OhmeStatisticsCoordinator,
hass: HomeAssistant,
client):
super().__init__(coordinator=coordinator)

self._state = None
self._attributes = {}
self._last_updated = None
self._client = client

self.entity_id = generate_entity_id(
"sensor.{}", "ohme_accumulative_energy", hass=hass)

self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
)

@property
def unique_id(self) -> str:
"""Return the unique ID of the sensor."""
return self._client.get_unique_id("accumulative_energy")

@property
def icon(self):
"""Icon of the sensor."""
return "mdi:lightning-bolt"

@property
def native_value(self):
"""Get value from data returned from API by coordinator"""
if self.coordinator.data and self.coordinator.data['energyChargedTotalWh']:
return self.coordinator.data['energyChargedTotalWh']

return None


class EnergyUsageSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
"""Sensor for total energy usage."""
_attr_name = "Energy"
Expand Down Expand Up @@ -290,13 +240,25 @@ def __init__(
def _handle_coordinator_update(self) -> None:
# Ensure we have data, then ensure value is going up and above 0
if self.coordinator.data and self.coordinator.data['batterySoc']:
new_state = self.coordinator.data['batterySoc']['wh']
new_state = 0
try:
new_state = self.coordinator.data['chargeGraph']['now']['y']
except BaseException:
_LOGGER.debug("EnergyUsageSensor: ChargeGraph reading failed, falling back to batterySoc")
new_state = self.coordinator.data['batterySoc']['wh']

# Let the state reset to 0, but not drop otherwise
if not new_state or new_state <= 0:
_LOGGER.debug("EnergyUsageSensor: Resetting Wh reading to 0")
self._state = 0
else:
self._state = max(0, self._state or 0, new_state)
# Allow a significant (90%+) drop, even if we dont hit exactly 0
if self._state and self._state > 0 and new_state > 0 and (new_state / self._state) < 0.1:
self._state = new_state
else:
self._state = max(0, self._state or 0, new_state)

_LOGGER.debug("EnergyUsageSensor: New state is %s", self._state)

self.async_write_ha_state()

Expand Down
8 changes: 6 additions & 2 deletions custom_components/ohme/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@
"password": "Password",
"never_session_specific": "Never update an ongoing session",
"never_collapse_slots": "Don't collapse charge slots",
"enable_accumulative_energy": "Enable accumulative energy sensor"
"interval_chargesessions": "Charge sessions refresh rate (minutes)",
"interval_accountinfo": "Account info refresh rate (minutes)",
"interval_advanced": "Advanced settings refresh rate (minutes)",
"interval_schedules": "Schedules refresh rate (minutes)"
},
"data_description": {
"password": "If you are not changing your credentials, leave the password field empty.",
"never_session_specific": "When adjusting charge percentage, charge target or preconditioning settings, the schedule will always be updated even if a charge session is in progress.",
"never_collapse_slots": "By default, adjacent slots are merged into one. This option shows every slot, as shown in the Ohme app."
"never_collapse_slots": "By default, adjacent slots are merged into one. This option shows every slot, as shown in the Ohme app.",
"interval_schedules": "Details on which entities are updated by each coordinator are in the README."
}
}
},
Expand Down

0 comments on commit c07c8a5

Please sign in to comment.