Skip to content

Commit

Permalink
Add rule caching so stop max charge works properly
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-r committed Dec 29, 2023
1 parent 6e12e8c commit 27efe87
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 16 deletions.
35 changes: 23 additions & 12 deletions custom_components/ohme/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,30 @@ def __init__(self, email, password):
if email is None or password is None:
raise Exception("Credentials not provided")

# Credentials from configuration
self._email = email
self._password = password

# Charger and its capabilities
self._device_info = None
self._capabilities = {}

# Authentication
self._token_birth = 0
self._token = None
self._refresh_token = None

# User info
self._user_id = ""
self._serial = ""

# Sessions
self._session = aiohttp.ClientSession(
base_url="https://api.ohme.io")
self._auth_session = aiohttp.ClientSession()


# Auth methods

async def async_create_session(self):
"""Refresh the user auth token from the stored credentials."""
async with self._auth_session.post(
Expand All @@ -54,7 +62,7 @@ async def async_refresh_session(self):
"""Refresh auth token if needed."""
if self._token is None:
return await self.async_create_session()

# Don't refresh token unless its over 45 mins old
if time() - self._token_birth < 2700:
return
Expand All @@ -76,8 +84,8 @@ async def async_refresh_session(self):
self._refresh_token = resp_json['refresh_token']
return True


# Internal methods

def _last_second_of_month_timestamp(self):
"""Get the last second of this month."""
dt = datetime.today()
Expand Down Expand Up @@ -139,20 +147,20 @@ async def _get_request(self, url):

return await resp.json()


# Simple getters

def is_capable(self, capability):
"""Return whether or not this model has a given capability."""
return bool(self._capabilities[capability])

def get_device_info(self):
return self._device_info

def get_unique_id(self, name):
return f"ohme_{self._serial}_{name}"


# Push methods

async def async_pause_charge(self):
"""Pause an ongoing charge"""
result = await self._post_request(f"/v1/chargeSessions/{self._serial}/stop", skip_json=True)
Expand All @@ -173,19 +181,22 @@ async def async_max_charge(self):
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?maxCharge=true")
return bool(result)

async def async_stop_max_charge(self):
"""Stop max charge.
This is more complicated than starting one as we need to give more parameters."""
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?enableMaxPrice=false&toPercent=80.0&inSeconds=43200")
async def async_apply_charge_rule(self, max_price=False, target_ts=0, target_percent=100, pre_condition=False, pre_condition_length=0):
"""Apply charge rule/stop max charge."""

max_price = 'true' if max_price else 'false'
pre_condition = 'true' if pre_condition else 'false'

result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?enableMaxPrice={max_price}&targetTs={target_ts}&enablePreconditioning={pre_condition}&toPercent={target_percent}&preconditionLengthMins={pre_condition_length}")
return bool(result)

async def async_set_configuration_value(self, values):
"""Set a configuration value or values."""
result = await self._put_request(f"/v1/chargeDevices/{self._serial}/appSettings", data=values)
return bool(result)


# Pull methods

async def async_get_charge_sessions(self, is_retry=False):
"""Try to fetch charge sessions endpoint.
If we get a non 200 response, refresh auth token and try again"""
Expand Down Expand Up @@ -234,10 +245,10 @@ async def async_get_ct_reading(self):
return resp['clampAmps']



# Exceptions
class ApiException(Exception):
...


class AuthException(ApiException):
...
46 changes: 44 additions & 2 deletions custom_components/ohme/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO
from .coordinator import OhmeChargeSessionsCoordinator, OhmeAccountInfoCoordinator
from .utils import time_next_occurs

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -121,6 +122,9 @@ def __init__(self, coordinator, hass: HomeAssistant, client):
self._last_updated = None
self._attributes = {}

# Cache the last rule to use when we disable max charge
self._last_rule = {}

self.entity_id = generate_entity_id(
"switch.{}", "ohme_max_charge", hass=hass)

Expand All @@ -145,6 +149,10 @@ def _handle_coordinator_update(self) -> None:
self._attr_is_on = bool(
self.coordinator.data["mode"] == "MAX_CHARGE")

# Cache the current rule if we are given it
if self.coordinator.data["mode"] == "SMART_CHARGE" and 'appliedRule' in self.coordinator.data:
self._last_rule = self.coordinator.data["appliedRule"]

self._last_updated = utcnow()

self.async_write_ha_state()
Expand All @@ -159,8 +167,42 @@ async def async_turn_on(self):
await self.coordinator.async_refresh()

async def async_turn_off(self):
"""Turn off the switch."""
await self._client.async_stop_max_charge()
"""Stop max charging.
We have to provide a full rule to disable max charge, so we try to get as much as possible
from the cached rule, and assume sane defaults if that isn't possible."""

max_price = False
target_ts = 0
target_percent = 80
pre_condition = False,
pre_condition_length = 0

if self._last_rule and 'targetTime' in self._last_rule:
# Convert rule time (seconds from 00:00 to time) to hh:mm
# and find when it next occurs.
next_dt = time_next_occurs(
self._last_rule['targetTime'] // 3600,
(self._last_rule['targetTime'] % 3600) // 60
)
target_ts = int(next_dt.timestamp() * 1000)
else:
next_dt = time_next_occurs(9, 0)
target_ts = int(next_dt.timestamp() * 1000)

if self._last_rule:
max_price = self._last_rule['settings'][0]['enabled'] if 'settings' in self._last_rule and len(
self._last_rule['settings']) > 1 else max_price
target_percent = self._last_rule['targetPercent'] if 'targetPercent' in self._last_rule else target_percent
pre_condition = self._last_rule['preconditioningEnabled'] if 'preconditioningEnabled' in self._last_rule else pre_condition
pre_condition_length = self._last_rule['preconditionLengthMins'] if 'preconditionLengthMins' in self._last_rule else pre_condition_length

await self._client.async_apply_charge_rule(
max_price=max_price,
target_ts=target_ts,
target_percent=target_percent,
pre_condition=pre_condition,
pre_condition_length=pre_condition_length
)

await asyncio.sleep(1)
await self.coordinator.async_refresh()
Expand Down
15 changes: 13 additions & 2 deletions custom_components/ohme/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from time import time
from datetime import datetime
from datetime import datetime, timedelta
import pytz


Expand Down Expand Up @@ -29,8 +29,19 @@ def charge_graph_next_slot(charge_start, points):
# 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:
next_ts = data[idx]["t"] + 1 # 1s added here as it otherwise often rounds down to xx:59:59
# 1s added here as it otherwise often rounds down to xx:59:59
next_ts = data[idx]["t"] + 1
break

# This needs to be presented with tzinfo or Home Assistant will reject it
return None if next_ts is None else datetime.utcfromtimestamp(next_ts).replace(tzinfo=pytz.utc)


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:
target = target + timedelta(days=1)

return target

0 comments on commit 27efe87

Please sign in to comment.