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

Commit 799c9f7

Browse files
authored
Allow modifying charge schedules (#29)
* Add ChargeSchedulesCoordinator and schedule update * Add schedule change function * Shorten ChargingBinarySensor cooldown * Updated version
1 parent 8956d41 commit 799c9f7

File tree

8 files changed

+100
-36
lines changed

8 files changed

+100
-36
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ This integration exposes the following entities:
5454
* Switches (Charge state) - **These are only functional when a car is connected**
5555
* Max Charge - Forces the connected car to charge regardless of set schedule
5656
* Pause Charge - Pauses an ongoing charge
57-
* Inputs - **Only available during a charge session**
58-
* Number: Target Percentage - Change the target percentage of the ongoing charge
59-
* Time: Target Time - Change the time target for the current charge
57+
* Inputs - **If in a charge session, this will change the active charge. If disconnected, this will change your first schedule.**
58+
* Number: Target Percentage - Change the target battery percentage
59+
* Time: Target Time - Change the target time
6060
* Buttons
6161
* Approve Charge - Approves a charge when 'Pending Approval' is on
6262

custom_components/ohme/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from homeassistant import core
22
from .const import *
33
from .api_client import OhmeApiClient
4-
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAccountInfoCoordinator, OhmeAdvancedSettingsCoordinator
4+
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAccountInfoCoordinator, OhmeAdvancedSettingsCoordinator, OhmeChargeSchedulesCoordinator
55

66

77
async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
@@ -35,7 +35,8 @@ async def async_setup_entry(hass, entry):
3535
OhmeChargeSessionsCoordinator(hass=hass), # COORDINATOR_CHARGESESSIONS
3636
OhmeAccountInfoCoordinator(hass=hass), # COORDINATOR_ACCOUNTINFO
3737
OhmeStatisticsCoordinator(hass=hass), # COORDINATOR_STATISTICS
38-
OhmeAdvancedSettingsCoordinator(hass=hass) # COORDINATOR_ADVANCED
38+
OhmeAdvancedSettingsCoordinator(hass=hass), # COORDINATOR_ADVANCED
39+
OhmeChargeSchedulesCoordinator(hass=hass) # COORDINATOR_SCHEDULES
3940
]
4041

4142
for coordinator in coordinators:

custom_components/ohme/api_client.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ async def async_max_charge(self):
195195
return bool(result)
196196

197197
async def async_apply_charge_rule(self, max_price=None, target_time=None, target_percent=None, pre_condition=None, pre_condition_length=None):
198-
"""Apply charge rule/stop max charge."""
198+
"""Apply rule to ongoing charge/stop max charge."""
199199
# Check every property. If we've provided it, use that. If not, use the existing.
200200
if max_price is None:
201201
max_price = self._last_rule['settings'][0]['enabled'] if 'settings' in self._last_rule and len(
@@ -226,6 +226,29 @@ async def async_apply_charge_rule(self, max_price=None, target_time=None, target
226226

227227
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}")
228228
return bool(result)
229+
230+
async def async_get_schedule(self):
231+
"""Get the first schedule."""
232+
schedules = await self._get_request("/v1/chargeRules")
233+
234+
return schedules[0] if len(schedules) > 0 else None
235+
236+
async def async_update_schedule(self, target_percent=None, target_time=None):
237+
"""Update the first listed schedule."""
238+
rule = await self.async_get_schedule()
239+
240+
# Account for user having no rules
241+
if not rule:
242+
return None
243+
244+
# Update percent and time if provided
245+
if target_percent is not None:
246+
rule['targetPercent'] = target_percent
247+
if target_time is not None:
248+
rule['targetTime'] = (target_time[0] * 3600) + (target_time[1] * 60)
249+
250+
await self._put_request(f"/v1/chargeRules/{rule['id']}", data=rule)
251+
return True
229252

230253
async def async_set_configuration_value(self, values):
231254
"""Set a configuration value or values."""

custom_components/ohme/binary_sensor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ def _calculate_state(self) -> bool:
188188
@callback
189189
def _handle_coordinator_update(self) -> None:
190190
"""Update data."""
191-
# Don't accept updates if 20s hasnt passed
191+
# Don't accept updates if 5s hasnt passed
192192
# State calculations use deltas that may be unreliable to check if requests are too often
193-
if self._last_updated and (utcnow().timestamp() - self._last_updated.timestamp() < 20):
193+
if self._last_updated and (utcnow().timestamp() - self._last_updated.timestamp() < 5):
194194
_LOGGER.debug("ChargingBinarySensor: State update too soon - suppressing")
195195
return
196196

custom_components/ohme/const.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Component constants"""
22
DOMAIN = "ohme"
33
USER_AGENT = "dan-r-homeassistant-ohme"
4-
INTEGRATION_VERSION = "0.2.7"
4+
INTEGRATION_VERSION = "0.2.8"
55

66
DATA_CLIENT = "client"
77
DATA_COORDINATORS = "coordinators"
88
COORDINATOR_CHARGESESSIONS = 0
99
COORDINATOR_ACCOUNTINFO = 1
1010
COORDINATOR_STATISTICS = 2
11-
COORDINATOR_ADVANCED = 3
11+
COORDINATOR_ADVANCED = 3
12+
COORDINATOR_SCHEDULES = 4

custom_components/ohme/coordinator.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,25 @@ async def _async_update_data(self):
9797

9898
except BaseException:
9999
raise UpdateFailed("Error communicating with API")
100+
101+
class OhmeChargeSchedulesCoordinator(DataUpdateCoordinator):
102+
"""Coordinator to pull charge schedules."""
103+
104+
def __init__(self, hass):
105+
"""Initialise coordinator."""
106+
super().__init__(
107+
hass,
108+
_LOGGER,
109+
name="Ohme Charge Schedules",
110+
update_interval=timedelta(minutes=10),
111+
)
112+
self._client = hass.data[DOMAIN][DATA_CLIENT]
113+
114+
async def _async_update_data(self):
115+
"""Fetch data from API endpoint."""
116+
try:
117+
return await self._client.async_get_schedule()
118+
119+
except BaseException:
120+
raise UpdateFailed("Error communicating with API")
121+

custom_components/ohme/number.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from homeassistant.components.number import NumberEntity, NumberDeviceClass
44
from homeassistant.helpers.entity import generate_entity_id
55
from homeassistant.core import callback, HomeAssistant
6-
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO
6+
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES
77

88

99
async def async_setup_entry(
@@ -14,10 +14,10 @@ async def async_setup_entry(
1414
"""Setup switches and configure coordinator."""
1515
coordinators = hass.data[DOMAIN][DATA_COORDINATORS]
1616

17-
coordinator = coordinators[COORDINATOR_CHARGESESSIONS]
1817
client = hass.data[DOMAIN][DATA_CLIENT]
1918

20-
numbers = [TargetPercentNumber(coordinator, hass, client)]
19+
numbers = [TargetPercentNumber(
20+
coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client)]
2121

2222
async_add_entities(numbers, update_before_add=True)
2323

@@ -28,12 +28,13 @@ class TargetPercentNumber(NumberEntity):
2828
_attr_device_class = NumberDeviceClass.BATTERY
2929
_attr_suggested_display_precision = 0
3030

31-
def __init__(self, coordinator, hass: HomeAssistant, client):
31+
def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client):
3232
self.coordinator = coordinator
33+
self.coordinator_schedules = coordinator_schedules
3334

3435
self._client = client
3536

36-
self._state = 0
37+
self._state = None
3738
self._last_updated = None
3839
self._attributes = {}
3940

@@ -49,10 +50,15 @@ def unique_id(self):
4950

5051
async def async_set_native_value(self, value: float) -> None:
5152
"""Update the current value."""
52-
await self._client.async_apply_charge_rule(target_percent=int(value))
53-
54-
await asyncio.sleep(1)
55-
await self.coordinator.async_refresh()
53+
# If disconnected, update top rule. If not, apply rule to current session
54+
if self.coordinator.data and self.coordinator.data['mode'] == "DISCONNECTED":
55+
await self._client.async_update_schedule(target_percent=int(value))
56+
await asyncio.sleep(1)
57+
await self.coordinator_schedules.async_refresh()
58+
else:
59+
await self._client.async_apply_charge_rule(target_percent=int(value))
60+
await asyncio.sleep(1)
61+
await self.coordinator.async_refresh()
5662

5763
@property
5864
def icon(self):
@@ -62,13 +68,11 @@ def icon(self):
6268
@property
6369
def native_value(self):
6470
"""Get value from data returned from API by coordinator"""
65-
if self.coordinator.data and self.coordinator.data['appliedRule']:
71+
if self.coordinator.data and self.coordinator.data['appliedRule'] and self.coordinator.data['mode'] != "PENDING_APPROVAL" and self.coordinator.data['mode'] != "DISCONNECTED":
6672
target = round(
6773
self.coordinator.data['appliedRule']['targetPercent'])
74+
elif self.coordinator_schedules.data:
75+
target = round(self.coordinator_schedules.data['targetPercent'])
6876

69-
if target == 0:
70-
return self._state
71-
72-
self._state = target
73-
return self._state
74-
return None
77+
self._state = target if target > 0 else None
78+
return self._state

custom_components/ohme/time.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from homeassistant.components.time import TimeEntity
55
from homeassistant.helpers.entity import generate_entity_id
66
from homeassistant.core import callback, HomeAssistant
7-
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO
7+
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES
88
from datetime import time as dt_time
99

1010
_LOGGER = logging.getLogger(__name__)
@@ -18,10 +18,10 @@ async def async_setup_entry(
1818
"""Setup switches and configure coordinator."""
1919
coordinators = hass.data[DOMAIN][DATA_COORDINATORS]
2020

21-
coordinator = coordinators[COORDINATOR_CHARGESESSIONS]
2221
client = hass.data[DOMAIN][DATA_CLIENT]
2322

24-
numbers = [TargetTime(coordinator, hass, client)]
23+
numbers = [TargetTime(coordinators[COORDINATOR_CHARGESESSIONS],
24+
coordinators[COORDINATOR_SCHEDULES], hass, client)]
2525

2626
async_add_entities(numbers, update_before_add=True)
2727

@@ -30,8 +30,9 @@ class TargetTime(TimeEntity):
3030
"""Target time sensor."""
3131
_attr_name = "Target Time"
3232

33-
def __init__(self, coordinator, hass: HomeAssistant, client):
33+
def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client):
3434
self.coordinator = coordinator
35+
self.coordinator_schedules = coordinator_schedules
3536

3637
self._client = client
3738

@@ -51,10 +52,17 @@ def unique_id(self):
5152

5253
async def async_set_value(self, value: dt_time) -> None:
5354
"""Update the current value."""
54-
await self._client.async_apply_charge_rule(target_time=(int(value.hour), int(value.minute)))
55-
56-
await asyncio.sleep(1)
57-
await self.coordinator.async_refresh()
55+
# If disconnected, update top rule. If not, apply rule to current session
56+
if self.coordinator.data and self.coordinator.data['mode'] == "DISCONNECTED":
57+
await self._client.async_update_schedule(target_time=(int(value.hour), int(value.minute)))
58+
await asyncio.sleep(1)
59+
await self.coordinator_schedules.async_refresh()
60+
else:
61+
await self._client.async_apply_charge_rule(target_time=(int(value.hour), int(value.minute)))
62+
await asyncio.sleep(1)
63+
await self.coordinator.async_refresh()
64+
65+
5866

5967
@property
6068
def icon(self):
@@ -64,9 +72,14 @@ def icon(self):
6472
@property
6573
def native_value(self):
6674
"""Get value from data returned from API by coordinator"""
67-
# Make sure we're not pending approval, as this sets the target time to now
68-
if self.coordinator.data and self.coordinator.data['appliedRule'] and self.coordinator.data['mode'] != "PENDING_APPROVAL":
75+
# If we are not pending approval or disconnected, return in progress charge rule
76+
target = None
77+
if self.coordinator.data and self.coordinator.data['appliedRule'] and self.coordinator.data['mode'] != "PENDING_APPROVAL" and self.coordinator.data['mode'] != "DISCONNECTED":
6978
target = self.coordinator.data['appliedRule']['targetTime']
79+
elif self.coordinator_schedules.data:
80+
target = self.coordinator_schedules.data['targetTime']
81+
82+
if target:
7083
self._state = dt_time(
7184
hour=target // 3600,
7285
minute=(target % 3600) // 60,

0 commit comments

Comments
 (0)