From a1301d5916f969c9e980ef006a3fc1424b1260ea Mon Sep 17 00:00:00 2001 From: Dan Raper Date: Thu, 28 Dec 2023 14:55:13 +0000 Subject: [PATCH] Added approve charge functionality (#7) * Add settings switches * Add capability check * Doc updates * Doc updates * Added pending approval sensor and approve button * Doc changes --- README.md | 3 ++ custom_components/ohme/__init__.py | 6 ++- custom_components/ohme/api_client.py | 11 +++-- custom_components/ohme/binary_sensor.py | 49 +++++++++++++++++- custom_components/ohme/button.py | 66 +++++++++++++++++++++++++ custom_components/ohme/coordinator.py | 1 + custom_components/ohme/switch.py | 17 ++++--- 7 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 custom_components/ohme/button.py diff --git a/README.md b/README.md index a835711..48c6ff0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This integration exposes the following entities: * Binary Sensors * Car Connected - On when a car is plugged in * Car Charging - On when a car is connected and drawing power + * Pending Approval - On when a car is connected and waiting for approval * Sensors * Power Draw (Watts) - Power draw of connected car * Accumulative Energy Usage (kWh) - Total energy used by the charger @@ -23,6 +24,8 @@ 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 +* Buttons + * Approve Charge - Approves a charge when 'Pending Approval' is on ## Installation diff --git a/custom_components/ohme/__init__.py b/custom_components/ohme/__init__.py index 23625e5..4c2a9e0 100644 --- a/custom_components/ohme/__init__.py +++ b/custom_components/ohme/__init__.py @@ -31,7 +31,8 @@ async def async_setup_entry(hass, entry): await async_setup_dependencies(hass, config) - hass.data[DOMAIN][DATA_CHARGESESSIONS_COORDINATOR] = OhmeChargeSessionsCoordinator(hass=hass) + hass.data[DOMAIN][DATA_CHARGESESSIONS_COORDINATOR] = OhmeChargeSessionsCoordinator( + hass=hass) await hass.data[DOMAIN][DATA_CHARGESESSIONS_COORDINATOR].async_config_entry_first_refresh() hass.data[DOMAIN][DATA_STATISTICS_COORDINATOR] = OhmeStatisticsCoordinator( @@ -52,6 +53,9 @@ async def async_setup_entry(hass, entry): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "switch") ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "button") + ) return True diff --git a/custom_components/ohme/api_client.py b/custom_components/ohme/api_client.py index e0a98b6..242f75f 100644 --- a/custom_components/ohme/api_client.py +++ b/custom_components/ohme/api_client.py @@ -69,7 +69,7 @@ async def _put_request(self, url, data=None, is_retry=False): headers={ "Authorization": "Firebase %s" % self._token, "Content-Type": "application/json" - } + } ) as resp: if resp.status != 200 and not is_retry: await self.async_refresh_session() @@ -104,6 +104,11 @@ async def async_resume_charge(self): result = await self._post_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/resume", skip_json=True) return bool(result) + async def async_approve_charge(self): + """Approve a charge""" + result = await self._put_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/approve?approve=true") + return bool(result) + async def async_max_charge(self): """Enable max charge""" result = await self._put_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/rule?maxCharge=true") @@ -135,7 +140,7 @@ async def async_get_account_info(self): if not resp: return False - + return resp async def async_update_device_info(self, is_retry=False): @@ -166,7 +171,7 @@ async def async_update_device_info(self, is_retry=False): def is_capable(self, capability): """Return whether or not this model has a given capability.""" return bool(self._capabilities[capability]) - + def _last_second_of_month_timestamp(self): """Get the last second of this month.""" dt = datetime.today() diff --git a/custom_components/ohme/binary_sensor.py b/custom_components/ohme/binary_sensor.py index 1c213f8..85a1d33 100644 --- a/custom_components/ohme/binary_sensor.py +++ b/custom_components/ohme/binary_sensor.py @@ -22,7 +22,8 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][DATA_CHARGESESSIONS_COORDINATOR] sensors = [ConnectedSensor(coordinator, hass, client), - ChargingSensor(coordinator, hass, client)] + ChargingSensor(coordinator, hass, client), + PendingApprovalSensor(coordinator, hass, client)] async_add_entities(sensors, update_before_add=True) @@ -118,3 +119,49 @@ def is_on(self) -> bool: self._state = False return self._state + + +class PendingApprovalSensor( + CoordinatorEntity[OhmeChargeSessionsCoordinator], + BinarySensorEntity): + """Binary sensor for if a charge is pending approval.""" + + _attr_name = "Pending Approval" + + def __init__( + self, + coordinator: OhmeChargeSessionsCoordinator, + hass: HomeAssistant, + client): + super().__init__(coordinator=coordinator) + + self._attributes = {} + self._last_updated = None + self._state = False + self._client = client + + self.entity_id = generate_entity_id( + "binary_sensor.{}", "ohme_pending_approval", hass=hass) + + self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( + ) + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:alert-decagram" + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor.""" + return self._client.get_unique_id("pending_approval") + + @property + def is_on(self) -> bool: + if self.coordinator.data is None: + self._state = False + else: + self._state = bool( + self.coordinator.data["mode"] == "PENDING_APPROVAL") + + return self._state diff --git a/custom_components/ohme/button.py b/custom_components/ohme/button.py new file mode 100644 index 0000000..60f7302 --- /dev/null +++ b/custom_components/ohme/button.py @@ -0,0 +1,66 @@ +from __future__ import annotations +import logging +import asyncio + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.components.button import ButtonEntity + +from .const import DOMAIN, DATA_CLIENT, DATA_CHARGESESSIONS_COORDINATOR +from .coordinator import OhmeChargeSessionsCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities +): + """Setup switches.""" + client = hass.data[DOMAIN][DATA_CLIENT] + coordinator = hass.data[DOMAIN][DATA_CHARGESESSIONS_COORDINATOR] + + buttons = [] + + if client.is_capable("pluginsRequireApprovalMode"): + buttons.append( + OhmeApproveChargeButton(coordinator, hass, client) + ) + + async_add_entities(buttons, update_before_add=True) + + +class OhmeApproveChargeButton(ButtonEntity): + """Button for approving a charge.""" + _attr_name = "Approve Charge" + + def __init__(self, coordinator: OhmeChargeSessionsCoordinator, hass: HomeAssistant, client): + self._client = client + self._coordinator = coordinator + + self._state = False + self._last_updated = None + self._attributes = {} + + self.entity_id = generate_entity_id( + "switch.{}", "ohme_approve_charge", hass=hass) + + self._attr_device_info = client.get_device_info() + + @property + def unique_id(self): + """The unique ID of the switch.""" + return self._client.get_unique_id("approve_charge") + + @property + def icon(self): + """Icon of the switch.""" + return "mdi:check-decagram-outline" + + async def async_press(self): + """Approve the charge.""" + await self._client.async_approve_charge() + + await asyncio.sleep(1) + await self._coordinator.async_refresh() diff --git a/custom_components/ohme/coordinator.py b/custom_components/ohme/coordinator.py index e54435a..2f280d5 100644 --- a/custom_components/ohme/coordinator.py +++ b/custom_components/ohme/coordinator.py @@ -32,6 +32,7 @@ async def _async_update_data(self): except BaseException: raise UpdateFailed("Error communicating with API") + class OhmeAccountInfoCoordinator(DataUpdateCoordinator): """Coordinator to pull from API periodically.""" diff --git a/custom_components/ohme/switch.py b/custom_components/ohme/switch.py index 3ecce3c..67b9373 100644 --- a/custom_components/ohme/switch.py +++ b/custom_components/ohme/switch.py @@ -28,19 +28,22 @@ async def async_setup_entry( client = hass.data[DOMAIN][DATA_CLIENT] switches = [OhmePauseChargeSwitch(coordinator, hass, client), - OhmeMaxChargeSwitch(coordinator, hass, client)] - + OhmeMaxChargeSwitch(coordinator, hass, client)] + if client.is_capable("buttonsLockable"): switches.append( - OhmeConfigurationSwitch(accountinfo_coordinator, hass, client, "Lock Buttons", "lock", "buttonsLocked") + OhmeConfigurationSwitch( + accountinfo_coordinator, hass, client, "Lock Buttons", "lock", "buttonsLocked") ) if client.is_capable("pluginsRequireApprovalMode"): switches.append( - OhmeConfigurationSwitch(accountinfo_coordinator, hass, client, "Require Approval", "check-decagram", "pluginsRequireApproval") + OhmeConfigurationSwitch(accountinfo_coordinator, hass, client, + "Require Approval", "check-decagram", "pluginsRequireApproval") ) if client.is_capable("stealth"): switches.append( - OhmeConfigurationSwitch(accountinfo_coordinator, hass, client, "Sleep When Inactive", "power-sleep", "stealthEnabled") + OhmeConfigurationSwitch(accountinfo_coordinator, hass, client, + "Sleep When Inactive", "power-sleep", "stealthEnabled") ) async_add_entities(switches, update_before_add=True) @@ -206,14 +209,14 @@ def _handle_coordinator_update(self) -> None: async def async_turn_on(self): """Turn on the switch.""" - await self._client.async_set_configuration_value({ self._config_key: True }) + await self._client.async_set_configuration_value({self._config_key: True}) await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_turn_off(self): """Turn off the switch.""" - await self._client.async_set_configuration_value({ self._config_key: False}) + await self._client.async_set_configuration_value({self._config_key: False}) await asyncio.sleep(1) await self.coordinator.async_refresh()