diff --git a/README.md b/README.md index 219094b..fcf29c0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/custom_components/ohme/__init__.py b/custom_components/ohme/__init__.py index 4891161..fe82310 100644 --- a/custom_components/ohme/__init__.py +++ b/custom_components/ohme/__init__.py @@ -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: @@ -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: diff --git a/custom_components/ohme/api_client.py b/custom_components/ohme/api_client.py index 7f4298c..9ddd6df 100644 --- a/custom_components/ohme/api_client.py +++ b/custom_components/ohme/api_client.py @@ -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( @@ -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.""" diff --git a/custom_components/ohme/binary_sensor.py b/custom_components/ohme/binary_sensor.py index 500e810..a7d123e 100644 --- a/custom_components/ohme/binary_sensor.py +++ b/custom_components/ohme/binary_sensor.py @@ -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 diff --git a/custom_components/ohme/const.py b/custom_components/ohme/const.py index 6f5e008..8f83c0a 100644 --- a/custom_components/ohme/const.py +++ b/custom_components/ohme/const.py @@ -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 \ No newline at end of file +COORDINATOR_ADVANCED = 3 +COORDINATOR_SCHEDULES = 4 \ No newline at end of file diff --git a/custom_components/ohme/coordinator.py b/custom_components/ohme/coordinator.py index 95a8129..9451859 100644 --- a/custom_components/ohme/coordinator.py +++ b/custom_components/ohme/coordinator.py @@ -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") + diff --git a/custom_components/ohme/number.py b/custom_components/ohme/number.py index e740856..ef8ceb7 100644 --- a/custom_components/ohme/number.py +++ b/custom_components/ohme/number.py @@ -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( @@ -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) @@ -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 = {} @@ -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): @@ -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 diff --git a/custom_components/ohme/time.py b/custom_components/ohme/time.py index 19f406b..c8814f4 100644 --- a/custom_components/ohme/time.py +++ b/custom_components/ohme/time.py @@ -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__) @@ -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) @@ -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 @@ -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): @@ -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,