Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit 46fd165

Browse files
authored
Doc updates and fixed max charge disable functionality (#19)
* Added ePod to README * Added coordinators info to README * Shift installation and setup to the top * Add rule caching so stop max charge works properly
1 parent abbcabd commit 46fd165

File tree

4 files changed

+114
-28
lines changed

4 files changed

+114
-28
lines changed

README.md

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,29 @@ This is an unofficial integration. I have no affiliation with Ohme besides ownin
77
This integration does not currently support social login or accounts with multiple chargers. It has been tested with the following hardware:
88
* Ohme Home Pro [UK]
99
* Ohme Home/Go [UK]
10+
* Ohme ePod [UK]
1011

1112
If you find any bugs or would like to request a feature, please open an issue.
1213

14+
15+
## Installation
16+
17+
### HACS
18+
This is the recommended installation method.
19+
1. Add this repository to HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories)
20+
2. Search for and install the Ohme addon from HACS
21+
3. Restart Home Assistant
22+
23+
### Manual
24+
1. Download the [latest release](https://github.com/dan-r/HomeAssistant-Ohme/releases)
25+
2. Copy the contents of `custom_components` into the `<config directory>/custom_components` directory of your Home Assistant installation
26+
3. Restart Home Assistant
27+
28+
29+
## Setup
30+
From the Home Assistant Integrations page, search for an add the Ohme integration. If you created your Ohme account through a social login, you will need to 'reset your password' to use this integration.
31+
32+
1333
## Entities
1434
This integration exposes the following entities:
1535

@@ -33,18 +53,20 @@ This integration exposes the following entities:
3353
* Buttons
3454
* Approve Charge - Approves a charge when 'Pending Approval' is on
3555

36-
## Installation
56+
## Coordinators
57+
Updates are made to entity states by polling the Ohme API. This is handled by 'coordinators' defined to Home Assistant, which refresh at a set interval or when externally triggered.
3758

38-
### HACS
39-
This is the recommended installation method.
40-
1. Add this repository to HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories)
41-
2. Search for and install the Ohme addon from HACS
42-
3. Restart Home Assistant
59+
The coordinators are listed with their refresh intervals below. Relevant coordinators are also refreshed when using switches and buttons.
4360

44-
### Manual
45-
1. Download the [latest release](https://github.com/dan-r/HomeAssistant-Ohme/releases)
46-
2. Copy the contents of `custom_components` into the `<config directory>/custom_components` directory of your Home Assistant installation
47-
3. Restart Home Assistant
61+
* OhmeChargeSessionsCoordinator (30s refresh)
62+
* Binary Sensors: All
63+
* Buttons: Approve Charge
64+
* Sensors: Power, current and next slot
65+
* Switches: Max charge, pause charge
66+
* OhmeAccountInfoCoordinator (1m refresh)
67+
* Switches: Lock buttons, require approval and sleep when inactive
68+
* OhmeAdvancedSettingsCoordinator (1m refresh)
69+
* Sensors: CT reading sensor
70+
* OhmeStatisticsCoordinator (30m refresh)
71+
* Sensors: Accumulative energy usage
4872

49-
## Setup
50-
From the Home Assistant Integrations page, search for an add the Ohme integration. If you created your Ohme account through a social login, you will need to 'reset your password' to use this integration.

custom_components/ohme/api_client.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,30 @@ def __init__(self, email, password):
1818
if email is None or password is None:
1919
raise Exception("Credentials not provided")
2020

21+
# Credentials from configuration
2122
self._email = email
2223
self._password = password
2324

25+
# Charger and its capabilities
2426
self._device_info = None
2527
self._capabilities = {}
28+
29+
# Authentication
2630
self._token_birth = 0
2731
self._token = None
2832
self._refresh_token = None
33+
34+
# User info
2935
self._user_id = ""
3036
self._serial = ""
37+
38+
# Sessions
3139
self._session = aiohttp.ClientSession(
3240
base_url="https://api.ohme.io")
3341
self._auth_session = aiohttp.ClientSession()
3442

35-
3643
# Auth methods
44+
3745
async def async_create_session(self):
3846
"""Refresh the user auth token from the stored credentials."""
3947
async with self._auth_session.post(
@@ -54,7 +62,7 @@ async def async_refresh_session(self):
5462
"""Refresh auth token if needed."""
5563
if self._token is None:
5664
return await self.async_create_session()
57-
65+
5866
# Don't refresh token unless its over 45 mins old
5967
if time() - self._token_birth < 2700:
6068
return
@@ -76,8 +84,8 @@ async def async_refresh_session(self):
7684
self._refresh_token = resp_json['refresh_token']
7785
return True
7886

79-
8087
# Internal methods
88+
8189
def _last_second_of_month_timestamp(self):
8290
"""Get the last second of this month."""
8391
dt = datetime.today()
@@ -139,20 +147,20 @@ async def _get_request(self, url):
139147

140148
return await resp.json()
141149

142-
143150
# Simple getters
151+
144152
def is_capable(self, capability):
145153
"""Return whether or not this model has a given capability."""
146154
return bool(self._capabilities[capability])
147-
155+
148156
def get_device_info(self):
149157
return self._device_info
150158

151159
def get_unique_id(self, name):
152160
return f"ohme_{self._serial}_{name}"
153-
154161

155162
# Push methods
163+
156164
async def async_pause_charge(self):
157165
"""Pause an ongoing charge"""
158166
result = await self._post_request(f"/v1/chargeSessions/{self._serial}/stop", skip_json=True)
@@ -173,19 +181,22 @@ async def async_max_charge(self):
173181
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?maxCharge=true")
174182
return bool(result)
175183

176-
async def async_stop_max_charge(self):
177-
"""Stop max charge.
178-
This is more complicated than starting one as we need to give more parameters."""
179-
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?enableMaxPrice=false&toPercent=80.0&inSeconds=43200")
184+
async def async_apply_charge_rule(self, max_price=False, target_ts=0, target_percent=100, pre_condition=False, pre_condition_length=0):
185+
"""Apply charge rule/stop max charge."""
186+
187+
max_price = 'true' if max_price else 'false'
188+
pre_condition = 'true' if pre_condition else 'false'
189+
190+
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}")
180191
return bool(result)
181192

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

187-
188198
# Pull methods
199+
189200
async def async_get_charge_sessions(self, is_retry=False):
190201
"""Try to fetch charge sessions endpoint.
191202
If we get a non 200 response, refresh auth token and try again"""
@@ -234,10 +245,10 @@ async def async_get_ct_reading(self):
234245
return resp['clampAmps']
235246

236247

237-
238248
# Exceptions
239249
class ApiException(Exception):
240250
...
241251

252+
242253
class AuthException(ApiException):
243254
...

custom_components/ohme/switch.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO
1515
from .coordinator import OhmeChargeSessionsCoordinator, OhmeAccountInfoCoordinator
16+
from .utils import time_next_occurs
1617

1718
_LOGGER = logging.getLogger(__name__)
1819

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

125+
# Cache the last rule to use when we disable max charge
126+
self._last_rule = {}
127+
124128
self.entity_id = generate_entity_id(
125129
"switch.{}", "ohme_max_charge", hass=hass)
126130

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

152+
# Cache the current rule if we are given it
153+
if self.coordinator.data["mode"] == "SMART_CHARGE" and 'appliedRule' in self.coordinator.data:
154+
self._last_rule = self.coordinator.data["appliedRule"]
155+
148156
self._last_updated = utcnow()
149157

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

161169
async def async_turn_off(self):
162-
"""Turn off the switch."""
163-
await self._client.async_stop_max_charge()
170+
"""Stop max charging.
171+
We have to provide a full rule to disable max charge, so we try to get as much as possible
172+
from the cached rule, and assume sane defaults if that isn't possible."""
173+
174+
max_price = False
175+
target_ts = 0
176+
target_percent = 80
177+
pre_condition = False,
178+
pre_condition_length = 0
179+
180+
if self._last_rule and 'targetTime' in self._last_rule:
181+
# Convert rule time (seconds from 00:00 to time) to hh:mm
182+
# and find when it next occurs.
183+
next_dt = time_next_occurs(
184+
self._last_rule['targetTime'] // 3600,
185+
(self._last_rule['targetTime'] % 3600) // 60
186+
)
187+
target_ts = int(next_dt.timestamp() * 1000)
188+
else:
189+
next_dt = time_next_occurs(9, 0)
190+
target_ts = int(next_dt.timestamp() * 1000)
191+
192+
if self._last_rule:
193+
max_price = self._last_rule['settings'][0]['enabled'] if 'settings' in self._last_rule and len(
194+
self._last_rule['settings']) > 1 else max_price
195+
target_percent = self._last_rule['targetPercent'] if 'targetPercent' in self._last_rule else target_percent
196+
pre_condition = self._last_rule['preconditioningEnabled'] if 'preconditioningEnabled' in self._last_rule else pre_condition
197+
pre_condition_length = self._last_rule['preconditionLengthMins'] if 'preconditionLengthMins' in self._last_rule else pre_condition_length
198+
199+
await self._client.async_apply_charge_rule(
200+
max_price=max_price,
201+
target_ts=target_ts,
202+
target_percent=target_percent,
203+
pre_condition=pre_condition,
204+
pre_condition_length=pre_condition_length
205+
)
164206

165207
await asyncio.sleep(1)
166208
await self.coordinator.async_refresh()

custom_components/ohme/utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from time import time
2-
from datetime import datetime
2+
from datetime import datetime, timedelta
33
import pytz
44

55

@@ -29,8 +29,19 @@ def charge_graph_next_slot(charge_start, points):
2929
# If the next point has a Y delta of 10+, consider this the start of a slot
3030
# This should be 0+ but I had some strange results in testing... revisit
3131
if delta > 10:
32-
next_ts = data[idx]["t"] + 1 # 1s added here as it otherwise often rounds down to xx:59:59
32+
# 1s added here as it otherwise often rounds down to xx:59:59
33+
next_ts = data[idx]["t"] + 1
3334
break
3435

3536
# This needs to be presented with tzinfo or Home Assistant will reject it
3637
return None if next_ts is None else datetime.utcfromtimestamp(next_ts).replace(tzinfo=pytz.utc)
38+
39+
40+
def time_next_occurs(hour, minute):
41+
"""Find when this time next occurs."""
42+
current = datetime.now()
43+
target = current.replace(hour=hour, minute=minute, second=0, microsecond=0)
44+
while target <= current:
45+
target = target + timedelta(days=1)
46+
47+
return target

0 commit comments

Comments
 (0)