Skip to content

Commit

Permalink
A lot of changes (#97)
Browse files Browse the repository at this point in the history
* Logic to collapse slots

* Linked to batpred docs

* Add 'Don't collapse charge slots' option

* Fixed next slot logic

* More next slot bugfixes

* Preliminary fix for #88

* Update install instructions

* Hide price cap settings for intelligent octopus

* Add solar boost switch

* Bump version
  • Loading branch information
dan-r authored Oct 7, 2024
1 parent 7bd0d37 commit 66bd621
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 32 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ This integration has been tested with the following hardware:
* Ohme ePod [v2.12]

## External Software
The 'Charge Slot Active' binary sensor mimics the `planned_dispatches` and `completed_dispatches` attributes from the [Octopus Energy](https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy) integration, so should support external software which reads this such as [predbat](https://github.com/springfall2008/batpred).
The 'Charge Slot Active' binary sensor mimics the `planned_dispatches` and `completed_dispatches` attributes from the [Octopus Energy](https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy) integration, so should support external software which reads this such as [predbat](https://springfall2008.github.io/batpred/devices/#ohme).


## Installation

### HACS
This is the recommended installation method.
1. Add this repository to HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories)
2. Search for and install the Ohme addon from HACS
3. Restart Home Assistant
1. Search for and install the Ohme addon from HACS
2. Restart Home Assistant

### Manual
1. Download the [latest release](https://github.com/dan-r/HomeAssistant-Ohme/releases)
Expand Down Expand Up @@ -64,15 +63,16 @@ This integration exposes the following entities:
* Lock Buttons - Locks buttons on charger
* Require Approval - Require approval to start a charge
* Sleep When Inactive - Charger screen & lights will automatically turn off
* Solar Boost
* 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
* Enable Price Cap - Whether price cap is applied. _Due to changes by Ohme, this will not show for Intelligent Octopus users._
* 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
* Price Cap - Maximum charge price. _Due to changes by Ohme, this will not show for Intelligent Octopus users._
* Time
* Target Time - Change the target time
* Buttons
Expand All @@ -81,6 +81,7 @@ This integration exposes the following entities:
## 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.
* Don't collapse charge slots - By default, adjacent slots are merged into one. This option shows every slot, as shown in the Ohme app.
* Enable accumulative energy usage sensor - Enable the sensor showing an all-time incrementing energy usage counter. This causes issues with some accounts.


Expand Down
24 changes: 20 additions & 4 deletions custom_components/ohme/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def __init__(self, email, password):
self._capabilities = {}
self._ct_connected = False
self._provision_date = None
self._disable_cap = False
self._solar_capable = False

# Authentication
self._token_birth = 0
Expand Down Expand Up @@ -168,6 +170,12 @@ def is_capable(self, capability):
"""Return whether or not this model has a given capability."""
return bool(self._capabilities[capability])

def solar_capable(self):
return self._solar_capable

def cap_available(self):
return not self._disable_cap

def get_device_info(self):
return self._device_info

Expand Down Expand Up @@ -212,8 +220,8 @@ async def async_apply_session_rule(self, max_price=None, target_time=None, targe
pre_condition = self._last_rule['preconditioningEnabled'] if 'preconditioningEnabled' in self._last_rule else False

if pre_condition_length is None:
pre_condition_length = self._last_rule[
'preconditionLengthMins'] if 'preconditionLengthMins' in self._last_rule else 30
pre_condition_length = self._last_rule['preconditionLengthMins'] if (
'preconditionLengthMins' in self._last_rule and self._last_rule['preconditionLengthMins'] is not None) else 30

if target_time is None:
# Default to 9am
Expand All @@ -230,7 +238,7 @@ 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")
Expand Down Expand Up @@ -261,7 +269,8 @@ async def async_update_schedule(self, target_percent=None, target_time=None, pre
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)
rule['targetTime'] = (target_time[0] * 3600) + \
(target_time[1] * 60)

# Update pre-conditioning if provided
if pre_condition is not None:
Expand Down Expand Up @@ -317,6 +326,13 @@ async def async_update_device_info(self, is_retry=False):
self._device_info = info
self._provision_date = device['provisioningTs']

if resp['tariff'] is not None and resp['tariff']['dsrTariff']:
self._disable_cap = True

solar_modes = device['modelCapabilities']['solarModes']
if isinstance(solar_modes, list) and len(solar_modes) == 1:
self._solar_capable = True

return True

async def async_get_charge_statistics(self):
Expand Down
3 changes: 3 additions & 0 deletions custom_components/ohme/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ async def async_step_init(self, options):
vol.Required(
"never_session_specific", default=self._config_entry.options.get("never_session_specific", False)
) : bool,
vol.Required(
"never_collapse_slots", default=self._config_entry.options.get("never_collapse_slots", False)
) : bool,
vol.Required(
"enable_accumulative_energy", default=self._config_entry.options.get("enable_accumulative_energy", False)
) : bool
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ohme/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Component constants"""
DOMAIN = "ohme"
USER_AGENT = "dan-r-homeassistant-ohme"
INTEGRATION_VERSION = "0.9.0"
INTEGRATION_VERSION = "1.0.0"
CONFIG_VERSION = 1
ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"]

Expand Down
15 changes: 10 additions & 5 deletions custom_components/ohme/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ async def async_setup_entry(
numbers = [TargetPercentNumber(
coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client),
PreconditioningNumber(
coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client),
PriceCapNumber(coordinators[COORDINATOR_ACCOUNTINFO], hass, client)]
coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client)]

if client.cap_available():
numbers.append(
PriceCapNumber(coordinators[COORDINATOR_ACCOUNTINFO], hass, client)
)

async_add_entities(numbers, update_before_add=True)

Expand Down Expand Up @@ -234,15 +238,16 @@ async def async_set_native_value(self, value: float) -> None:
def native_unit_of_measurement(self):
if self.coordinator.data is None:
return None

penny_unit = {
"GBP": "p",
"EUR": "c"
}
currency = self.coordinator.data["userSettings"].get("currencyCode", "XXX")
currency = self.coordinator.data["userSettings"].get(
"currencyCode", "XXX")

return penny_unit.get(currency, f"{currency}/100")

@property
def icon(self):
"""Icon of the sensor."""
Expand Down
14 changes: 6 additions & 8 deletions custom_components/ohme/sensor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Platform for sensor integration."""
from __future__ import annotations
from functools import reduce
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorStateClass,
Expand All @@ -15,7 +14,7 @@
from homeassistant.util.dt import (utcnow)
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, DATA_SLOTS, COORDINATOR_CHARGESESSIONS, COORDINATOR_STATISTICS, COORDINATOR_ADVANCED
from .coordinator import OhmeChargeSessionsCoordinator, OhmeStatisticsCoordinator, OhmeAdvancedSettingsCoordinator
from .utils import next_slot, get_option, slot_list
from .utils import next_slot, get_option, slot_list, slot_list_str

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -332,6 +331,7 @@ def __init__(
self._attributes = {}
self._last_updated = None
self._client = client
self._hass = hass

self.entity_id = generate_entity_id(
"sensor.{}", "ohme_next_slot", hass=hass)
Expand Down Expand Up @@ -360,7 +360,7 @@ def _handle_coordinator_update(self) -> None:
if self.coordinator.data is None or self.coordinator.data["mode"] == "DISCONNECTED":
self._state = None
else:
self._state = next_slot(self.coordinator.data)['start']
self._state = next_slot(self._hass, self.coordinator.data)['start']

self._last_updated = utcnow()

Expand All @@ -383,6 +383,7 @@ def __init__(
self._attributes = {}
self._last_updated = None
self._client = client
self._hass = hass

self.entity_id = generate_entity_id(
"sensor.{}", "ohme_next_slot_end", hass=hass)
Expand Down Expand Up @@ -411,7 +412,7 @@ def _handle_coordinator_update(self) -> None:
if self.coordinator.data is None or self.coordinator.data["mode"] == "DISCONNECTED":
self._state = None
else:
self._state = next_slot(self.coordinator.data)['end']
self._state = next_slot(self._hass, self.coordinator.data)['end']

self._last_updated = utcnow()

Expand Down Expand Up @@ -468,10 +469,7 @@ def _handle_coordinator_update(self) -> None:
self._hass.data[DOMAIN][DATA_SLOTS] = slots

# Convert list to text
self._state = reduce(lambda acc, slot: acc + f"{slot['start'].strftime('%H:%M')}-{slot['end'].strftime('%H:%M')}, ", slots, "")[:-2]

# Make sure we return None/Unknown if the list is empty
self._state = None if self._state == "" else self._state
self._state = slot_list_str(self._hass, slots)

self._last_updated = utcnow()
self.async_write_ha_state()
Expand Down
72 changes: 69 additions & 3 deletions custom_components/ohme/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,18 @@ async def async_setup_entry(
client = hass.data[DOMAIN][DATA_CLIENT]

switches = [OhmePauseChargeSwitch(coordinator, hass, client),
OhmeMaxChargeSwitch(coordinator, hass, client),
OhmePriceCapSwitch(accountinfo_coordinator, hass, client)]
OhmeMaxChargeSwitch(coordinator, hass, client)
]

if client.cap_available():
switches.append(
OhmePriceCapSwitch(accountinfo_coordinator, hass, client)
)

if client.solar_capable():
switches.append(
OhmeSolarBoostSwitch(accountinfo_coordinator, hass, client)
)
if client.is_capable("buttonsLockable"):
switches.append(
OhmeConfigurationSwitch(
Expand Down Expand Up @@ -226,6 +235,62 @@ async def async_turn_off(self):
await self.coordinator.async_refresh()


class OhmeSolarBoostSwitch(CoordinatorEntity[OhmeAccountInfoCoordinator], SwitchEntity):
"""Switch for changing configuration options."""

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

self._client = client

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

self._attr_name = "Solar Boost"
self.entity_id = generate_entity_id(
"switch.{}", "ohme_solar_boost", 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("solarMode")

@property
def icon(self):
"""Icon of the switch."""
return "mdi:solar-power"

@callback
def _handle_coordinator_update(self) -> None:
"""Determine configuration value."""
if self.coordinator.data is None:
self._attr_is_on = None
else:
settings = self.coordinator.data["chargeDevices"][0]["optionalSettings"]
self._attr_is_on = bool(settings["solarMode"] == "ZERO_EXPORT")

self._last_updated = utcnow()

self.async_write_ha_state()

async def async_turn_on(self):
"""Turn on the switch."""
await self._client.async_set_configuration_value({"solarMode": "ZERO_EXPORT"})

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({"solarMode": "IGNORE"})

await asyncio.sleep(1)
await self.coordinator.async_refresh()


class OhmePriceCapSwitch(CoordinatorEntity[OhmeAccountInfoCoordinator], SwitchEntity):
"""Switch for enabling price cap."""
_attr_name = "Enable Price Cap"
Expand Down Expand Up @@ -256,7 +321,8 @@ def _handle_coordinator_update(self) -> None:
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._attr_is_on = bool(
self.coordinator.data["userSettings"]["chargeSettings"][0]["enabled"])

self._last_updated = utcnow()

Expand Down
4 changes: 3 additions & 1 deletion custom_components/ohme/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
"email": "Email address",
"password": "Password",
"never_session_specific": "Never update an ongoing session",
"never_collapse_slots": "Don't collapse charge slots",
"enable_accumulative_energy": "Enable accumulative energy sensor"
},
"data_description": {
"password": "If you are not changing your credentials, leave the password field empty.",
"never_session_specific": "When adjusting charge percentage, charge target or preconditioning settings, the schedule will always be updated even if a charge session is in progress."
"never_session_specific": "When adjusting charge percentage, charge target or preconditioning settings, the schedule will always be updated even if a charge session is in progress.",
"never_collapse_slots": "By default, adjacent slots are merged into one. This option shows every slot, as shown in the Ohme app."
}
}
},
Expand Down
Loading

0 comments on commit 66bd621

Please sign in to comment.