From 6a52f578c539e262933b5620353a4a39b671f97a Mon Sep 17 00:00:00 2001 From: Daniel Raper Date: Wed, 17 Jan 2024 17:16:55 +0000 Subject: [PATCH] Added preconditioning input --- README.md | 16 +++-- custom_components/ohme/api_client.py | 8 ++- custom_components/ohme/number.py | 98 +++++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3779ced..ac196ca 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,19 @@ This integration exposes the following entities: * Max Charge - Forces the connected car to charge regardless of set schedule * Pause Charge - Pauses an ongoing charge * Inputs - **If in a charge session, these change the active charge. If disconnected, they change your first schedule.** - * Number: Target Percentage - Change the target battery percentage - * Time: Target Time - Change the target time + * Number + * Target Percentage - Change the target battery percentage + * Preconditioning - Change pre-conditioning time. 0 is off + * Time + * Target Time - Change the target time * Buttons * Approve Charge - Approves a charge when 'Pending Approval' is on +## Options +Some options can be set from the 'Configure' menu in Home Assistant: +* Never update an ongoing session - Override the default behaviour of the target time, percentage and preconditioning inputs and only ever update the schedule, not the current session. This was added as changing the current session can cause issues for customers on Intelligent Octopus Go. + + ## Coordinators 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. @@ -74,7 +82,7 @@ The coordinators are listed with their refresh intervals below. Relevant coordin * Buttons: Approve Charge * Sensors: Power, current, voltage and next slot (start & end) * Switches: Max charge, pause charge - * Inputs: Target time and target percentage (If car connected) + * Inputs: Target time, target percentage and preconditioning (If car connected) * OhmeAccountInfoCoordinator (1m refresh) * Switches: Lock buttons, require approval and sleep when inactive * OhmeAdvancedSettingsCoordinator (1m refresh) @@ -83,4 +91,4 @@ The coordinators are listed with their refresh intervals below. Relevant coordin * OhmeStatisticsCoordinator (30m refresh) * Sensors: Accumulative energy usage * OhmeChargeSchedulesCoordinator (10m refresh) - * Inputs: Target time and target percentage (If car disconnected) + * Inputs: Target time, target percentage and preconditioning (If car disconnected) diff --git a/custom_components/ohme/api_client.py b/custom_components/ohme/api_client.py index 41ff9ab..7f124ad 100644 --- a/custom_components/ohme/api_client.py +++ b/custom_components/ohme/api_client.py @@ -233,7 +233,7 @@ async def async_get_schedule(self): return schedules[0] if len(schedules) > 0 else None - async def async_update_schedule(self, target_percent=None, target_time=None): + async def async_update_schedule(self, target_percent=None, target_time=None, pre_condition=None, pre_condition_length=None): """Update the first listed schedule.""" rule = await self.async_get_schedule() @@ -247,6 +247,12 @@ async def async_update_schedule(self, target_percent=None, target_time=None): if target_time is not None: rule['targetTime'] = (target_time[0] * 3600) + (target_time[1] * 60) + # Update pre-conditioning if provided + if pre_condition is not None: + rule['preconditioningEnabled'] = pre_condition + if pre_condition_length is not None: + rule['preconditionLengthMins'] = pre_condition_length + await self._put_request(f"/v1/chargeRules/{rule['id']}", data=rule) return True diff --git a/custom_components/ohme/number.py b/custom_components/ohme/number.py index 9530e8c..6ea6d03 100644 --- a/custom_components/ohme/number.py +++ b/custom_components/ohme/number.py @@ -1,11 +1,13 @@ from __future__ import annotations import asyncio from homeassistant.components.number import NumberEntity, NumberDeviceClass +from homeassistant.const import UnitOfTime 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_SCHEDULES from .utils import session_in_progress + async def async_setup_entry( hass: HomeAssistant, config_entry: config_entries.ConfigEntry, @@ -17,6 +19,8 @@ async def async_setup_entry( client = hass.data[DOMAIN][DATA_CLIENT] numbers = [TargetPercentNumber( + coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client), + PreconditioningNumber( coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client)] async_add_entities(numbers, update_before_add=True) @@ -56,7 +60,7 @@ async def async_added_to_hass(self) -> None: self._handle_coordinator_update, None ) ) - + @property def unique_id(self): """The unique ID of the switch.""" @@ -84,7 +88,8 @@ def _handle_coordinator_update(self) -> None: """Get value from data returned from API by coordinator""" # Set with the same logic as reading if session_in_progress(self.hass, self.coordinator.data): - target = round(self.coordinator.data['appliedRule']['targetPercent']) + target = round( + self.coordinator.data['appliedRule']['targetPercent']) elif self.coordinator_schedules.data: target = round(self.coordinator_schedules.data['targetPercent']) @@ -93,3 +98,92 @@ def _handle_coordinator_update(self) -> None: @property def native_value(self): return self._state + + +class PreconditioningNumber(NumberEntity): + """Preconditioning sensor.""" + _attr_name = "Preconditioning" + _attr_device_class = NumberDeviceClass.DURATION + _attr_native_unit_of_measurement = UnitOfTime.MINUTES + _attr_native_min_value = 0 + _attr_native_step = 5 + _attr_native_max_value = 60 + + def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client): + self.coordinator = coordinator + self.coordinator_schedules = coordinator_schedules + + self._client = client + + self._state = None + self._last_updated = None + self._attributes = {} + + self.entity_id = generate_entity_id( + "number.{}", "ohme_preconditioning", hass=hass) + + self._attr_device_info = client.get_device_info() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.coordinator.async_add_listener( + self._handle_coordinator_update, None + ) + ) + self.async_on_remove( + self.coordinator_schedules.async_add_listener( + self._handle_coordinator_update, None + ) + ) + + @property + def unique_id(self): + """The unique ID of the switch.""" + return self._client.get_unique_id("preconditioning") + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + # If session in progress, update this session, if not update the first schedule + if session_in_progress(self.hass, self.coordinator.data): + if value == 0: + await self._client.async_apply_session_rule(pre_condition=False) + else: + await self._client.async_apply_session_rule(pre_condition=True, pre_condition_length=int(value)) + await asyncio.sleep(1) + await self.coordinator.async_refresh() + else: + if value == 0: + await self._client.async_update_schedule(pre_condition=False) + else: + await self._client.async_update_schedule(pre_condition=True, pre_condition_length=int(value)) + await asyncio.sleep(1) + await self.coordinator_schedules.async_refresh() + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:air-conditioner" + + @callback + def _handle_coordinator_update(self) -> None: + """Get value from data returned from API by coordinator""" + precondition = None + # Set with the same logic as reading + if session_in_progress(self.hass, self.coordinator.data): + enabled = self.coordinator.data['appliedRule'].get( + 'preconditioningEnabled', False) + precondition = 0 if not enabled else self.coordinator.data['appliedRule'].get( + 'preconditionLengthMins', None) + elif self.coordinator_schedules.data: + enabled = self.coordinator_schedules.data.get( + 'preconditioningEnabled', False) + precondition = 0 if not enabled else self.coordinator_schedules.data.get( + 'preconditionLengthMins', None) + + self._state = precondition + + @property + def native_value(self): + return self._state