Skip to content

Commit

Permalink
Added predbat support, updated docs (#55)
Browse files Browse the repository at this point in the history
* Add planned/completed dispatches support

* Why did I use a while

* Move attributes to binary sensor

* Minus kwh deltas

* Fix tzinfo

* Add predbat to README
  • Loading branch information
dan-r authored Feb 7, 2024
1 parent ea0cbfd commit 13b60d0
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 20 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# Ohme EV Charger for Home Assistant

An integration for interacting with Ohme EV Chargers.
An unofficial integration for interacting with Ohme EV Chargers. I have no affiliation with Ohme besides owning one of their EV chargers.

This is an unofficial integration. I have no affiliation with Ohme besides owning one of their EV chargers.
This integration does not currently support accounts with multiple chargers.

This integration does not currently support social login or accounts with multiple chargers. It has been tested with the following hardware:
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]

If you find any bugs or would like to request a feature, please open an issue.
## 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://github.com/springfall2008/batpred).


## Installation
Expand Down
15 changes: 13 additions & 2 deletions custom_components/ohme/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ADVANCED, DATA_CLIENT
from .const import DOMAIN, DATA_COORDINATORS, DATA_SLOTS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ADVANCED, DATA_CLIENT
from .coordinator import OhmeChargeSessionsCoordinator, OhmeAdvancedSettingsCoordinator
from .utils import charge_graph_in_slot

Expand Down Expand Up @@ -269,10 +269,10 @@ def __init__(
client):
super().__init__(coordinator=coordinator)

self._attributes = {}
self._last_updated = None
self._state = False
self._client = client
self._hass = hass

self.entity_id = generate_entity_id(
"binary_sensor.{}", "ohme_slot_active", hass=hass)
Expand All @@ -290,6 +290,17 @@ def unique_id(self) -> str:
"""Return the unique ID of the sensor."""
return self._client.get_unique_id("ohme_slot_active")

@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
now = utcnow()
slots = self._hass.data[DOMAIN][DATA_SLOTS] if DATA_SLOTS in self._hass.data[DOMAIN] else []

return {
"planned_dispatches": [x for x in slots if not x['end'] or x['end'] > now],
"completed_dispatches": [x for x in slots if x['end'] < now]
}

@property
def is_on(self) -> bool:
return self._state
Expand Down
3 changes: 2 additions & 1 deletion custom_components/ohme/const.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Component constants"""
DOMAIN = "ohme"
USER_AGENT = "dan-r-homeassistant-ohme"
INTEGRATION_VERSION = "0.6.0"
INTEGRATION_VERSION = "0.6.1"
CONFIG_VERSION = 1
ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"]

DATA_CLIENT = "client"
DATA_COORDINATORS = "coordinators"
DATA_OPTIONS = "options"
DATA_SLOTS = "slots"

COORDINATOR_CHARGESESSIONS = 0
COORDINATOR_ACCOUNTINFO = 1
Expand Down
13 changes: 9 additions & 4 deletions custom_components/ohme/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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, COORDINATOR_CHARGESESSIONS, COORDINATOR_STATISTICS, COORDINATOR_ADVANCED
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

Expand Down Expand Up @@ -432,9 +432,10 @@ def __init__(
super().__init__(coordinator=coordinator)

self._state = None
self._attributes = {}
self._slots = []
self._last_updated = None
self._client = client
self._hass = hass

self.entity_id = generate_entity_id(
"sensor.{}", "ohme_charge_slots", hass=hass)
Expand Down Expand Up @@ -469,6 +470,7 @@ def _handle_coordinator_update(self) -> None:
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()

Expand All @@ -479,9 +481,12 @@ def _handle_coordinator_update(self) -> None:

slots = charge_graph_slot_list(
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])

# Store slots for external use
self._hass.data[DOMAIN][DATA_SLOTS] = slots

# Convert list of tuples to text
self._state = reduce(lambda acc, slot: acc + f"{slot[0]}-{slot[1]}, ", slots, "")[:-2]
# Convert list to text
self._state = reduce(lambda acc, slot: acc + f"{slot['start'].strftime('%H:%M')}-{slot['end'].strftime('%H:%M')}, ", slots, "")[:-2]

# Make sure we return None/Unknown if the list is empty
self._state = None if self._state == "" else self._state
Expand Down
26 changes: 17 additions & 9 deletions custom_components/ohme/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ def _next_slot(data, live=False, in_progress=False):
start but still want the end of the in progress session, but for the slot list sensor we only want slots that have
a start AND an end."""
start_ts = None
start_ts_y = 0
end_ts = None
end_ts_y = 0

# Loop through every remaining value, skipping the last
for idx in range(0, len(data) - 1):
Expand All @@ -70,6 +72,7 @@ def _next_slot(data, live=False, in_progress=False):
if delta > 10 and not start_ts:
# 1s added here as it otherwise often rounds down to xx:59:59
start_ts = data[idx]["t"] + 1
start_ts_y = data[idx]["y"]

# If we are working live, in a time slot and haven't seen an end yet,
# disregard.
Expand All @@ -79,11 +82,12 @@ def _next_slot(data, live=False, in_progress=False):
# Take the first delta of 0 as the end
if delta == 0 and data[idx]["y"] != 0 and (start_ts or live) and not end_ts:
end_ts = data[idx]["t"] + 1
end_ts_y = data[idx]["y"]

if start_ts and end_ts:
break

return [start_ts, end_ts, idx]
return [start_ts, end_ts, idx, end_ts_y - start_ts_y]


def charge_graph_next_slot(charge_start, points, skip_format=False):
Expand All @@ -99,7 +103,7 @@ def charge_graph_next_slot(charge_start, points, skip_format=False):
if len(data) < 2:
return {"start": None, "end": None}

start_ts, end_ts, _ = _next_slot(data, live=True, in_progress=in_progress)
start_ts, end_ts, _, _ = _next_slot(data, live=True, in_progress=in_progress)

# These need to be presented with tzinfo or Home Assistant will reject them
return {
Expand Down Expand Up @@ -133,11 +137,16 @@ def charge_graph_slot_list(charge_start, points, skip_format=False):
if result[0] is None or result[1] is None:
break

# Append a tuple to the slots list with the start end end time
slots.append((
datetime.fromtimestamp(result[0] + 1).strftime('%H:%M'),
datetime.fromtimestamp(result[1] + 1).strftime('%H:%M'),
))
# Append a dict to the slots list with the start and end time
slots.append(
{
"start": datetime.utcfromtimestamp(result[0]).replace(tzinfo=pytz.utc),
"end": datetime.utcfromtimestamp(result[1]).replace(tzinfo=pytz.utc),
"charge_in_kwh": -(result[3] / 1000),
"source": "smart-charge",
"location": None
}
)

# Cut off where we got to in this iteration for next time
data = data[result[2]:]
Expand Down Expand Up @@ -165,9 +174,8 @@ def charge_graph_in_slot(charge_start, points, skip_format=False):

def time_next_occurs(hour, minute):
"""Find when this time next occurs."""
current = datetime.now()
target = current.replace(hour=hour, minute=minute, second=0, microsecond=0)
while target <= current:
if target <= datetime.now():
target = target + timedelta(days=1)

return target
Expand Down

0 comments on commit 13b60d0

Please sign in to comment.