Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow modifying charge schedules #29

Merged
merged 4 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ This integration exposes the following entities:
* Switches (Charge state) - **These are only functional when a car is connected**
* Max Charge - Forces the connected car to charge regardless of set schedule
* Pause Charge - Pauses an ongoing charge
* Inputs - **Only available during a charge session**
* Number: Target Percentage - Change the target percentage of the ongoing charge
* Time: Target Time - Change the time target for the current charge
* Inputs - **If in a charge session, this will change the active charge. If disconnected, this will change your first schedule.**
* Number: Target Percentage - Change the target battery percentage
* Time: Target Time - Change the target time
* Buttons
* Approve Charge - Approves a charge when 'Pending Approval' is on

Expand Down
5 changes: 3 additions & 2 deletions custom_components/ohme/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from homeassistant import core
from .const import *
from .api_client import OhmeApiClient
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAccountInfoCoordinator, OhmeAdvancedSettingsCoordinator
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAccountInfoCoordinator, OhmeAdvancedSettingsCoordinator, OhmeChargeSchedulesCoordinator


async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
Expand Down Expand Up @@ -35,7 +35,8 @@ async def async_setup_entry(hass, entry):
OhmeChargeSessionsCoordinator(hass=hass), # COORDINATOR_CHARGESESSIONS
OhmeAccountInfoCoordinator(hass=hass), # COORDINATOR_ACCOUNTINFO
OhmeStatisticsCoordinator(hass=hass), # COORDINATOR_STATISTICS
OhmeAdvancedSettingsCoordinator(hass=hass) # COORDINATOR_ADVANCED
OhmeAdvancedSettingsCoordinator(hass=hass), # COORDINATOR_ADVANCED
OhmeChargeSchedulesCoordinator(hass=hass) # COORDINATOR_SCHEDULES
]

for coordinator in coordinators:
Expand Down
25 changes: 24 additions & 1 deletion custom_components/ohme/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ async def async_max_charge(self):
return bool(result)

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

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_get_schedule(self):
"""Get the first schedule."""
schedules = await self._get_request("/v1/chargeRules")

return schedules[0] if len(schedules) > 0 else None

async def async_update_schedule(self, target_percent=None, target_time=None):
"""Update the first listed schedule."""
rule = await self.async_get_schedule()

# Account for user having no rules
if not rule:
return None

# Update percent and time if provided
if target_percent is not None:
rule['targetPercent'] = target_percent
if target_time is not None:
rule['targetTime'] = (target_time[0] * 3600) + (target_time[1] * 60)

await self._put_request(f"/v1/chargeRules/{rule['id']}", data=rule)
return True

async def async_set_configuration_value(self, values):
"""Set a configuration value or values."""
Expand Down
4 changes: 2 additions & 2 deletions custom_components/ohme/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ def _calculate_state(self) -> bool:
@callback
def _handle_coordinator_update(self) -> None:
"""Update data."""
# Don't accept updates if 20s hasnt passed
# Don't accept updates if 5s hasnt passed
# State calculations use deltas that may be unreliable to check if requests are too often
if self._last_updated and (utcnow().timestamp() - self._last_updated.timestamp() < 20):
if self._last_updated and (utcnow().timestamp() - self._last_updated.timestamp() < 5):
_LOGGER.debug("ChargingBinarySensor: State update too soon - suppressing")
return

Expand Down
5 changes: 3 additions & 2 deletions custom_components/ohme/const.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Component constants"""
DOMAIN = "ohme"
USER_AGENT = "dan-r-homeassistant-ohme"
INTEGRATION_VERSION = "0.2.7"
INTEGRATION_VERSION = "0.2.8"

DATA_CLIENT = "client"
DATA_COORDINATORS = "coordinators"
COORDINATOR_CHARGESESSIONS = 0
COORDINATOR_ACCOUNTINFO = 1
COORDINATOR_STATISTICS = 2
COORDINATOR_ADVANCED = 3
COORDINATOR_ADVANCED = 3
COORDINATOR_SCHEDULES = 4
22 changes: 22 additions & 0 deletions custom_components/ohme/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,25 @@ async def _async_update_data(self):

except BaseException:
raise UpdateFailed("Error communicating with API")

class OhmeChargeSchedulesCoordinator(DataUpdateCoordinator):
"""Coordinator to pull charge schedules."""

def __init__(self, hass):
"""Initialise coordinator."""
super().__init__(
hass,
_LOGGER,
name="Ohme Charge Schedules",
update_interval=timedelta(minutes=10),
)
self._client = hass.data[DOMAIN][DATA_CLIENT]

async def _async_update_data(self):
"""Fetch data from API endpoint."""
try:
return await self._client.async_get_schedule()

except BaseException:
raise UpdateFailed("Error communicating with API")

36 changes: 20 additions & 16 deletions custom_components/ohme/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from homeassistant.components.number import NumberEntity, NumberDeviceClass
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES


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

coordinator = coordinators[COORDINATOR_CHARGESESSIONS]
client = hass.data[DOMAIN][DATA_CLIENT]

numbers = [TargetPercentNumber(coordinator, hass, client)]
numbers = [TargetPercentNumber(
coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client)]

async_add_entities(numbers, update_before_add=True)

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

def __init__(self, coordinator, hass: HomeAssistant, client):
def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client):
self.coordinator = coordinator
self.coordinator_schedules = coordinator_schedules

self._client = client

self._state = 0
self._state = None
self._last_updated = None
self._attributes = {}

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

async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await self._client.async_apply_charge_rule(target_percent=int(value))

await asyncio.sleep(1)
await self.coordinator.async_refresh()
# If disconnected, update top rule. If not, apply rule to current session
if self.coordinator.data and self.coordinator.data['mode'] == "DISCONNECTED":
await self._client.async_update_schedule(target_percent=int(value))
await asyncio.sleep(1)
await self.coordinator_schedules.async_refresh()
else:
await self._client.async_apply_charge_rule(target_percent=int(value))
await asyncio.sleep(1)
await self.coordinator.async_refresh()

@property
def icon(self):
Expand All @@ -62,13 +68,11 @@ def icon(self):
@property
def native_value(self):
"""Get value from data returned from API by coordinator"""
if self.coordinator.data and self.coordinator.data['appliedRule']:
if self.coordinator.data and self.coordinator.data['appliedRule'] and self.coordinator.data['mode'] != "PENDING_APPROVAL" and self.coordinator.data['mode'] != "DISCONNECTED":
target = round(
self.coordinator.data['appliedRule']['targetPercent'])
elif self.coordinator_schedules.data:
target = round(self.coordinator_schedules.data['targetPercent'])

if target == 0:
return self._state

self._state = target
return self._state
return None
self._state = target if target > 0 else None
return self._state
33 changes: 23 additions & 10 deletions custom_components/ohme/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from homeassistant.components.time import TimeEntity
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.core import callback, HomeAssistant
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES
from datetime import time as dt_time

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

coordinator = coordinators[COORDINATOR_CHARGESESSIONS]
client = hass.data[DOMAIN][DATA_CLIENT]

numbers = [TargetTime(coordinator, hass, client)]
numbers = [TargetTime(coordinators[COORDINATOR_CHARGESESSIONS],
coordinators[COORDINATOR_SCHEDULES], hass, client)]

async_add_entities(numbers, update_before_add=True)

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

def __init__(self, coordinator, hass: HomeAssistant, client):
def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client):
self.coordinator = coordinator
self.coordinator_schedules = coordinator_schedules

self._client = client

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

async def async_set_value(self, value: dt_time) -> None:
"""Update the current value."""
await self._client.async_apply_charge_rule(target_time=(int(value.hour), int(value.minute)))

await asyncio.sleep(1)
await self.coordinator.async_refresh()
# If disconnected, update top rule. If not, apply rule to current session
if self.coordinator.data and self.coordinator.data['mode'] == "DISCONNECTED":
await self._client.async_update_schedule(target_time=(int(value.hour), int(value.minute)))
await asyncio.sleep(1)
await self.coordinator_schedules.async_refresh()
else:
await self._client.async_apply_charge_rule(target_time=(int(value.hour), int(value.minute)))
await asyncio.sleep(1)
await self.coordinator.async_refresh()



@property
def icon(self):
Expand All @@ -64,9 +72,14 @@ def icon(self):
@property
def native_value(self):
"""Get value from data returned from API by coordinator"""
# Make sure we're not pending approval, as this sets the target time to now
if self.coordinator.data and self.coordinator.data['appliedRule'] and self.coordinator.data['mode'] != "PENDING_APPROVAL":
# If we are not pending approval or disconnected, return in progress charge rule
target = None
if self.coordinator.data and self.coordinator.data['appliedRule'] and self.coordinator.data['mode'] != "PENDING_APPROVAL" and self.coordinator.data['mode'] != "DISCONNECTED":
target = self.coordinator.data['appliedRule']['targetTime']
elif self.coordinator_schedules.data:
target = self.coordinator_schedules.data['targetTime']

if target:
self._state = dt_time(
hour=target // 3600,
minute=(target % 3600) // 60,
Expand Down
Loading