Skip to content

Commit

Permalink
Add functionality from new app version (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-r committed Jul 10, 2024
1 parent 35968b7 commit 7bd0d37
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 257 deletions.
7 changes: 2 additions & 5 deletions custom_components/ohme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,8 @@ async def async_setup_entry(hass, entry):

hass.data[DOMAIN][DATA_COORDINATORS] = coordinators

# Create tasks for each entity type
for entity_type in ENTITY_TYPES:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, entity_type)
)
# Setup entities
await hass.config_entries.async_forward_entry_setups(entry, ENTITY_TYPES)

entry.async_on_unload(entry.add_update_listener(async_update_listener))

Expand Down
4 changes: 2 additions & 2 deletions custom_components/ohme/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ async def async_approve_charge(self):
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/approve?approve=true")
return bool(result)

async def async_max_charge(self):
async def async_max_charge(self, state=True):
"""Enable max charge"""
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?maxCharge=true")
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?maxCharge=" + str(state).lower())
return bool(result)

async def async_apply_session_rule(self, max_price=None, target_time=None, target_percent=None, pre_condition=None, pre_condition_length=None):
Expand Down
8 changes: 3 additions & 5 deletions custom_components/ohme/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from homeassistant.util.dt import (utcnow)
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
from .utils import in_slot

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -137,8 +137,7 @@ def _calculate_state(self) -> bool:
return power > 0

# See if we are in a charge slot now and if we were for the last reading
in_charge_slot = charge_graph_in_slot(
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])
in_charge_slot = in_slot(self.coordinator.data)
lr_in_charge_slot = self._last_reading_in_slot
# Store this for next time
self._last_reading_in_slot = in_charge_slot
Expand Down Expand Up @@ -313,8 +312,7 @@ def _handle_coordinator_update(self) -> None:
elif self.coordinator.data["mode"] == "DISCONNECTED":
self._state = False
else:
self._state = charge_graph_in_slot(
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])
self._state = in_slot(self.coordinator.data)

self._last_updated = utcnow()

Expand Down
2 changes: 1 addition & 1 deletion 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 = "0.8.3"
INTEGRATION_VERSION = "0.9.0"
CONFIG_VERSION = 1
ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"]

Expand Down
12 changes: 4 additions & 8 deletions custom_components/ohme/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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, get_option
from .utils import next_slot, get_option, slot_list

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -360,8 +360,7 @@ def _handle_coordinator_update(self) -> None:
if self.coordinator.data is None or self.coordinator.data["mode"] == "DISCONNECTED":
self._state = None
else:
self._state = charge_graph_next_slot(
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])['start']
self._state = next_slot(self.coordinator.data)['start']

self._last_updated = utcnow()

Expand Down Expand Up @@ -412,8 +411,7 @@ def _handle_coordinator_update(self) -> None:
if self.coordinator.data is None or self.coordinator.data["mode"] == "DISCONNECTED":
self._state = None
else:
self._state = charge_graph_next_slot(
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])['end']
self._state = next_slot(self.coordinator.data)['end']

self._last_updated = utcnow()

Expand Down Expand Up @@ -463,10 +461,8 @@ 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._hass.data[DOMAIN][DATA_SLOTS] = []
else:
slots = charge_graph_slot_list(
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])
slots = slot_list(self.coordinator.data)

# Store slots for external use
self._hass.data[DOMAIN][DATA_SLOTS] = slots
Expand Down
4 changes: 2 additions & 2 deletions custom_components/ohme/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def _handle_coordinator_update(self) -> None:

async def async_turn_on(self):
"""Turn on the switch."""
await self._client.async_max_charge()
await self._client.async_max_charge(True)

# Not very graceful but wait here to avoid the mode coming back as 'CALCULATING'
# It would be nice to simply ignore this state in future and try again after x seconds.
Expand All @@ -162,7 +162,7 @@ async def async_turn_on(self):
async def async_turn_off(self):
"""Stop max charging.
We are not changing anything, just applying the last rule. No need to supply anything."""
await self._client.async_apply_session_rule()
await self._client.async_max_charge(False)

await asyncio.sleep(1)
await self.coordinator.async_refresh()
Expand Down
171 changes: 38 additions & 133 deletions custom_components/ohme/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,160 +5,65 @@
# import logging
# _LOGGER = logging.getLogger(__name__)

def _format_charge_graph(charge_start, points):
"""Convert relative time in points array to real timestamp (s)."""

charge_start = round(charge_start / 1000)

# _LOGGER.debug("Charge slot graph points: " + str([{"t": datetime.fromtimestamp(x["x"] + charge_start).strftime('%H:%M:%S'), "y": x["y"]} for x in points]))

return [{"t": x["x"] + charge_start, "y": x["y"]} for x in points]


def _sanitise_points(points):
"""Discard any points that aren't on a quarter-hour boundary."""
output = []
seen = []
high = max([x['y'] for x in points])

points.reverse()

for point in points:
# Round up the timestamp and get the minute
ts = point['t'] + 30
dt = datetime.fromtimestamp(ts)
hm = dt.strftime('%H:%M')
m = int(dt.strftime('%M'))

# If this point is on a 15m boundary and we haven't seen this time before
# OR y == yMax - so we don't miss the end of the last slot
if (m % 15 == 0 and hm not in seen) or point['y'] == high:
output.append(point)
seen.append(hm)

output.reverse()
# _LOGGER.warning("Charge slot graph points: " + str([{"t": datetime.fromtimestamp(x["t"] + 30).strftime('%H:%M:%S'), "y": x["y"]} for x in output]))

return output


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
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):
# Calculate the delta between this element and the next
delta = data[idx + 1]["y"] - data[idx]["y"]
delta = 0 if delta < 0 else delta # Zero floor deltas

# If the next point has a Y delta of 10+, consider this the start of a slot
# This should be 0+ but I had some strange results in testing... revisit
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.
if start_ts and live and in_progress and not end_ts:
start_ts = None

# 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
def next_slot(data):
"""Get the next charge slot start/end times."""
slots = slot_list(data)
start = None
end = None

# Loop through slots
for slot in slots:
# Only take the first slot start/end that matches. These are in order.
if start is None and slot['start'] > datetime.now().astimezone():
start = slot['start']
if end is None and slot['end'] > datetime.now().astimezone():
end = slot['end']

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


def charge_graph_next_slot(charge_start, points, skip_format=False):
"""Get the next charge slot start/end times from a list of graph points."""
now = int(time())
data = points if skip_format else _format_charge_graph(charge_start, points)
in_progress = charge_graph_in_slot(charge_start, data, skip_format=True)

# Filter to points from now onwards
data = [x for x in data if x["t"] > now]

# Give up if we have less than 2 points
if len(data) < 2:
return {"start": None, "end": None}

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 {
"start": datetime.utcfromtimestamp(start_ts).replace(tzinfo=pytz.utc) if start_ts else None,
"end": datetime.utcfromtimestamp(end_ts).replace(tzinfo=pytz.utc) if end_ts else None,
"start": start,
"end": end
}


def charge_graph_slot_list(charge_start, points, skip_format=False):
"""Get list of charge slots from graph points."""
data = points if skip_format else _format_charge_graph(charge_start, points)

# Don't return any slots if charge is over
if charge_graph_next_slot(charge_start, points)['end'] is None:
return []

data = _sanitise_points(data)

# Give up if we have less than 2 points
if len(data) < 2:
def slot_list(data):
"""Get list of charge slots."""
session_slots = data['allSessionSlots']
if session_slots is None or len(session_slots) == 0:
return []

slots = []
wh_tally = 0

if 'batterySocBefore' in data and data['batterySocBefore'] is not None and data['batterySocBefore']['wh'] is not None:
wh_tally = data['batterySocBefore']['wh'] # Get the wh value we start from

# While we still have data, keep looping
while len(data) > 1:
# Get the next slot
result = _next_slot(data)

# Break if we fail
if result[0] is None or result[1] is None:
break

# Append a dict to the slots list with the start and end time
for slot in session_slots:
slots.append(
{
"start": datetime.utcfromtimestamp(result[0]).replace(tzinfo=pytz.utc).astimezone(),
"end": datetime.utcfromtimestamp(result[1]).replace(tzinfo=pytz.utc).astimezone(),
"charge_in_kwh": -(result[3] / 1000),
"start": datetime.utcfromtimestamp(slot['startTimeMs'] / 1000).replace(tzinfo=pytz.utc, microsecond=0).astimezone(),
"end": datetime.utcfromtimestamp(slot['endTimeMs'] / 1000).replace(tzinfo=pytz.utc, microsecond=0).astimezone(),
"charge_in_kwh": -((slot['estimatedSoc']['wh'] - wh_tally) / 1000), # Work out how much we add in just this slot
"source": "smart-charge",
"location": None
}
)

# Cut off where we got to in this iteration for next time
data = data[result[2]:]

wh_tally = slot['estimatedSoc']['wh']

return slots


def charge_graph_in_slot(charge_start, points, skip_format=False):
def in_slot(data):
"""Are we currently in a charge slot?"""
now = int(time())
data = points if skip_format else _format_charge_graph(charge_start, points)

# Loop through every value, skipping the last
for idx in range(0, len(data) - 1):
# This is our current point
if data[idx]["t"] < now and data[idx + 1]["t"] > now:
# If the delta line we are on is steeper than 10,
# we are in a charge slot.
if data[idx + 1]["y"] - data[idx]["y"] > 10:
return True
break
slots = slot_list(data)

# Loop through slots
for slot in slots:
# If we are in one
if slot['start'] < datetime.now().astimezone() and slot['end'] > datetime.now().astimezone():
return True

return False


Expand Down
Loading

0 comments on commit 7bd0d37

Please sign in to comment.