diff --git a/README.md b/README.md index ac196ca..83496aa 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,12 @@ 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 + * Enable Price Cap - Whether price cap is applied * 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 * Preconditioning - Change pre-conditioning time. 0 is off + * Price Cap - Maximum charge price * Time * Target Time - Change the target time * Buttons @@ -84,7 +86,8 @@ The coordinators are listed with their refresh intervals below. Relevant coordin * Switches: Max charge, pause charge * Inputs: Target time, target percentage and preconditioning (If car connected) * OhmeAccountInfoCoordinator (1m refresh) - * Switches: Lock buttons, require approval and sleep when inactive + * Switches: Lock buttons, require approval, sleep when inactive and enable price cap + * Inputs: Price cap * OhmeAdvancedSettingsCoordinator (1m refresh) * Sensors: CT reading sensor * Binary Sensors: Charger online diff --git a/custom_components/ohme/api_client.py b/custom_components/ohme/api_client.py index 7f124ad..26b09b8 100644 --- a/custom_components/ohme/api_client.py +++ b/custom_components/ohme/api_client.py @@ -227,6 +227,18 @@ async def async_apply_session_rule(self, max_price=None, target_time=None, targe 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_change_price_cap(self, enabled=None, cap=None): + """Change price cap settings.""" + settings = await self._get_request("/v1/users/me/settings") + if enabled is not None: + settings['chargeSettings'][0]['enabled'] = enabled + + if cap is not None: + settings['chargeSettings'][0]['value'] = cap + + result = await self._put_request("/v1/users/me/settings", data=settings) + return bool(result) + async def async_get_schedule(self): """Get the first schedule.""" schedules = await self._get_request("/v1/chargeRules") diff --git a/custom_components/ohme/const.py b/custom_components/ohme/const.py index 31ab057..6934f19 100644 --- a/custom_components/ohme/const.py +++ b/custom_components/ohme/const.py @@ -1,7 +1,7 @@ """Component constants""" DOMAIN = "ohme" USER_AGENT = "dan-r-homeassistant-ohme" -INTEGRATION_VERSION = "0.4.0" +INTEGRATION_VERSION = "0.4.1" CONFIG_VERSION = 1 ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"] diff --git a/custom_components/ohme/number.py b/custom_components/ohme/number.py index 6ea6d03..efee03b 100644 --- a/custom_components/ohme/number.py +++ b/custom_components/ohme/number.py @@ -1,10 +1,11 @@ from __future__ import annotations import asyncio from homeassistant.components.number import NumberEntity, NumberDeviceClass +from homeassistant.components.number.const import NumberMode 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 .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_ACCOUNTINFO, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES from .utils import session_in_progress @@ -21,7 +22,8 @@ async def async_setup_entry( numbers = [TargetPercentNumber( coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client), PreconditioningNumber( - coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client)] + coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client), + PriceCapNumber(coordinators[COORDINATOR_ACCOUNTINFO], hass, client)] async_add_entities(numbers, update_before_add=True) @@ -187,3 +189,59 @@ def _handle_coordinator_update(self) -> None: @property def native_value(self): return self._state + + +class PriceCapNumber(NumberEntity): + _attr_name = "Price Cap" + _attr_native_unit_of_measurement = "p" + _attr_device_class = NumberDeviceClass.MONETARY + _attr_mode = NumberMode.BOX + _attr_native_step = 0.1 + _attr_native_min_value = 1 + _attr_native_max_value = 100 + + def __init__(self, coordinator, hass: HomeAssistant, client): + self.coordinator = coordinator + self._client = client + self._state = None + self.entity_id = generate_entity_id( + "number.{}", "ohme_price_cap", 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 + ) + ) + + @property + def unique_id(self): + """The unique ID of the switch.""" + return self._client.get_unique_id("price_cap") + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self._client.async_change_price_cap(cap=value) + + await asyncio.sleep(1) + await self.coordinator.async_refresh() + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:cash" + + @callback + def _handle_coordinator_update(self) -> None: + """Get value from data returned from API by coordinator""" + if self.coordinator.data is not None: + self._state = self.coordinator.data["userSettings"]["chargeSettings"][0]["value"] + self.async_write_ha_state() + + @property + def native_value(self): + return self._state diff --git a/custom_components/ohme/switch.py b/custom_components/ohme/switch.py index 404ff66..5d03e58 100644 --- a/custom_components/ohme/switch.py +++ b/custom_components/ohme/switch.py @@ -30,7 +30,8 @@ async def async_setup_entry( client = hass.data[DOMAIN][DATA_CLIENT] switches = [OhmePauseChargeSwitch(coordinator, hass, client), - OhmeMaxChargeSwitch(coordinator, hass, client)] + OhmeMaxChargeSwitch(coordinator, hass, client), + OhmePriceCapSwitch(accountinfo_coordinator, hass, client)] if client.is_capable("buttonsLockable"): switches.append( @@ -223,3 +224,54 @@ async def async_turn_off(self): await asyncio.sleep(1) await self.coordinator.async_refresh() + + +class OhmePriceCapSwitch(CoordinatorEntity[OhmeAccountInfoCoordinator], SwitchEntity): + """Switch for enabling price cap.""" + _attr_name = "Enable Price Cap" + + def __init__(self, coordinator, hass: HomeAssistant, client): + super().__init__(coordinator=coordinator) + + self._client = client + + self.entity_id = generate_entity_id( + "switch.{}", "ohme_price_cap_enabled", 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("price_cap_enabled") + + @property + def icon(self): + """Icon of the switch.""" + return f"mdi:car-speed-limiter" + + @callback + def _handle_coordinator_update(self) -> None: + """Determine configuration value.""" + if self.coordinator.data is None: + self._attr_is_on = None + else: + self._attr_is_on = bool(self.coordinator.data["userSettings"]["chargeSettings"][0]["enabled"]) + + self._last_updated = utcnow() + + self.async_write_ha_state() + + async def async_turn_on(self): + """Turn on the switch.""" + await self._client.async_change_price_cap(enabled=True) + + await asyncio.sleep(1) + await self.coordinator.async_refresh() + + async def async_turn_off(self): + """Turn off the switch.""" + await self._client.async_change_price_cap(enabled=False) + + await asyncio.sleep(1) + await self.coordinator.async_refresh()