Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit 4fcf404

Browse files
authored
Added voltage and next slot end sensor, and charge slot active binary sensor (#20)
* Add CT detection * Add next slot ends sensor * Added voltage sensor * Added charge slot active sensor
1 parent 46fd165 commit 4fcf404

File tree

5 files changed

+228
-33
lines changed

5 files changed

+228
-33
lines changed

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,21 @@ This integration exposes the following entities:
3737
* Car Connected - On when a car is plugged in
3838
* Car Charging - On when a car is connected and drawing power
3939
* Pending Approval - On when a car is connected and waiting for approval
40-
* Sensors
40+
* Charge Slot Active - On when a charge slot is in progress according to the Ohme-generated charge plan
41+
* Sensors (Charge power) - **These are only available during a charge session**
4142
* Power Draw (Watts) - Power draw of connected car
4243
* Current Draw (Amps) - Current draw of connected car
44+
* Voltage (Volts) - Voltage reading
45+
* Sensors (Other)
4346
* CT Reading (Amps) - Reading from attached CT clamp
4447
* Accumulative Energy Usage (kWh) - Total energy used by the charger
45-
* Next Smart Charge Slot - The next time your car will start charging according to the Ohme-generated charge plan
46-
* Switches (Settings) - Only options available to your charger model will show
48+
* Next Charge Slot Start - The next time your car will start charging according to the Ohme-generated charge plan
49+
* Next Charge Slot End - The next time your car will stop charging according to the Ohme-generated charge plan
50+
* Switches (Settings) - **Only options available to your charger model will show**
4751
* Lock Buttons - Locks buttons on charger
4852
* Require Approval - Require approval to start a charge
4953
* Sleep When Inactive - Charger screen & lights will automatically turn off
50-
* Switches (Charge state) - These are only functional when a car is connected
54+
* Switches (Charge state) - **These are only functional when a car is connected**
5155
* Max Charge - Forces the connected car to charge regardless of set schedule
5256
* Pause Charge - Pauses an ongoing charge
5357
* Buttons
@@ -61,7 +65,7 @@ The coordinators are listed with their refresh intervals below. Relevant coordin
6165
* OhmeChargeSessionsCoordinator (30s refresh)
6266
* Binary Sensors: All
6367
* Buttons: Approve Charge
64-
* Sensors: Power, current and next slot
68+
* Sensors: Power, current, voltage and next slot (start & end)
6569
* Switches: Max charge, pause charge
6670
* OhmeAccountInfoCoordinator (1m refresh)
6771
* Switches: Lock buttons, require approval and sleep when inactive

custom_components/ohme/api_client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(self, email, password):
2525
# Charger and its capabilities
2626
self._device_info = None
2727
self._capabilities = {}
28+
self._ct_connected = False
2829

2930
# Authentication
3031
self._token_birth = 0
@@ -149,6 +150,10 @@ async def _get_request(self, url):
149150

150151
# Simple getters
151152

153+
def ct_connected(self):
154+
"""Is CT clamp connected."""
155+
return self._ct_connected
156+
152157
def is_capable(self, capability):
153158
"""Return whether or not this model has a given capability."""
154159
return bool(self._capabilities[capability])
@@ -241,6 +246,10 @@ async def async_get_charge_statistics(self):
241246
async def async_get_ct_reading(self):
242247
"""Get CT clamp reading."""
243248
resp = await self._get_request(f"/v1/chargeDevices/{self._serial}/advancedSettings")
249+
250+
# If we ever get a reading above 0, assume CT connected
251+
if resp['clampAmps'] > 0:
252+
self._ct_connected = True
244253

245254
return resp['clampAmps']
246255

custom_components/ohme/binary_sensor.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
BinarySensorEntity
77
)
88
from homeassistant.helpers.update_coordinator import CoordinatorEntity
9-
from homeassistant.core import HomeAssistant
9+
from homeassistant.core import HomeAssistant, callback
1010
from homeassistant.helpers.entity import generate_entity_id
11+
from homeassistant.util.dt import (utcnow)
1112
from .const import DOMAIN, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, DATA_CLIENT
1213
from .coordinator import OhmeChargeSessionsCoordinator
14+
from .utils import charge_graph_in_slot
1315

1416

1517
async def async_setup_entry(
@@ -21,14 +23,15 @@ async def async_setup_entry(
2123
client = hass.data[DOMAIN][DATA_CLIENT]
2224
coordinator = hass.data[DOMAIN][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS]
2325

24-
sensors = [ConnectedSensor(coordinator, hass, client),
25-
ChargingSensor(coordinator, hass, client),
26-
PendingApprovalSensor(coordinator, hass, client)]
26+
sensors = [ConnectedBinarySensor(coordinator, hass, client),
27+
ChargingBinarySensor(coordinator, hass, client),
28+
PendingApprovalBinarySensor(coordinator, hass, client),
29+
CurrentSlotBinarySensor(coordinator, hass, client)]
2730

2831
async_add_entities(sensors, update_before_add=True)
2932

3033

31-
class ConnectedSensor(
34+
class ConnectedBinarySensor(
3235
CoordinatorEntity[OhmeChargeSessionsCoordinator],
3336
BinarySensorEntity):
3437
"""Binary sensor for if car is plugged in."""
@@ -74,7 +77,7 @@ def is_on(self) -> bool:
7477
return self._state
7578

7679

77-
class ChargingSensor(
80+
class ChargingBinarySensor(
7881
CoordinatorEntity[OhmeChargeSessionsCoordinator],
7982
BinarySensorEntity):
8083
"""Binary sensor for if car is charging."""
@@ -121,7 +124,7 @@ def is_on(self) -> bool:
121124
return self._state
122125

123126

124-
class PendingApprovalSensor(
127+
class PendingApprovalBinarySensor(
125128
CoordinatorEntity[OhmeChargeSessionsCoordinator],
126129
BinarySensorEntity):
127130
"""Binary sensor for if a charge is pending approval."""
@@ -165,3 +168,58 @@ def is_on(self) -> bool:
165168
self.coordinator.data["mode"] == "PENDING_APPROVAL")
166169

167170
return self._state
171+
172+
173+
class CurrentSlotBinarySensor(
174+
CoordinatorEntity[OhmeChargeSessionsCoordinator],
175+
BinarySensorEntity):
176+
"""Binary sensor for if we are currently in a smart charge slot."""
177+
178+
_attr_name = "Charge Slot Active"
179+
180+
def __init__(
181+
self,
182+
coordinator: OhmeChargeSessionsCoordinator,
183+
hass: HomeAssistant,
184+
client):
185+
super().__init__(coordinator=coordinator)
186+
187+
self._attributes = {}
188+
self._last_updated = None
189+
self._state = False
190+
self._client = client
191+
192+
self.entity_id = generate_entity_id(
193+
"binary_sensor.{}", "ohme_slot_active", hass=hass)
194+
195+
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
196+
)
197+
198+
@property
199+
def icon(self):
200+
"""Icon of the sensor."""
201+
return "mdi:calendar-check"
202+
203+
@property
204+
def unique_id(self) -> str:
205+
"""Return the unique ID of the sensor."""
206+
return self._client.get_unique_id("ohme_slot_active")
207+
208+
@property
209+
def is_on(self) -> bool:
210+
return self._state
211+
212+
@callback
213+
def _handle_coordinator_update(self) -> None:
214+
"""Are we in a charge slot? This is a bit slow so we only update on coordinator data update."""
215+
if self.coordinator.data is None:
216+
self._state = None
217+
elif self.coordinator.data["mode"] == "DISCONNECTED":
218+
self._state = False
219+
else:
220+
self._state = charge_graph_in_slot(
221+
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])
222+
223+
self._last_updated = utcnow()
224+
225+
self.async_write_ha_state()

custom_components/ohme/sensor.py

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
SensorEntity
77
)
88
from homeassistant.helpers.update_coordinator import CoordinatorEntity
9-
from homeassistant.const import UnitOfPower, UnitOfEnergy, UnitOfElectricCurrent
9+
from homeassistant.const import UnitOfPower, UnitOfEnergy, UnitOfElectricCurrent, UnitOfElectricPotential
1010
from homeassistant.core import HomeAssistant, callback
1111
from homeassistant.helpers.entity import generate_entity_id
1212
from homeassistant.util.dt import (utcnow)
@@ -30,9 +30,11 @@ async def async_setup_entry(
3030

3131
sensors = [PowerDrawSensor(coordinator, hass, client),
3232
CurrentDrawSensor(coordinator, hass, client),
33+
VoltageSensor(coordinator, hass, client),
3334
CTSensor(adv_coordinator, hass, client),
3435
EnergyUsageSensor(stats_coordinator, hass, client),
35-
NextSlotSensor(coordinator, hass, client)]
36+
NextSlotEndSensor(coordinator, hass, client),
37+
NextSlotStartSensor(coordinator, hass, client)]
3638

3739
async_add_entities(sensors, update_before_add=True)
3840

@@ -121,15 +123,57 @@ def native_value(self):
121123
return 0
122124

123125

124-
class CTSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
126+
class VoltageSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
127+
"""Sensor for EVSE voltage."""
128+
_attr_name = "Voltage"
129+
_attr_device_class = SensorDeviceClass.VOLTAGE
130+
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
131+
132+
def __init__(
133+
self,
134+
coordinator: OhmeChargeSessionsCoordinator,
135+
hass: HomeAssistant,
136+
client):
137+
super().__init__(coordinator=coordinator)
138+
139+
self._state = None
140+
self._attributes = {}
141+
self._last_updated = None
142+
self._client = client
143+
144+
self.entity_id = generate_entity_id(
145+
"sensor.{}", "ohme_voltage", hass=hass)
146+
147+
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
148+
)
149+
150+
@property
151+
def unique_id(self) -> str:
152+
"""Return the unique ID of the sensor."""
153+
return self._client.get_unique_id("voltage")
154+
155+
@property
156+
def icon(self):
157+
"""Icon of the sensor."""
158+
return "mdi:sine-wave"
159+
160+
@property
161+
def native_value(self):
162+
"""Get value from data returned from API by coordinator"""
163+
if self.coordinator.data and self.coordinator.data['power']:
164+
return self.coordinator.data['power']['volt']
165+
return None
166+
167+
168+
class CTSensor(CoordinatorEntity[OhmeAdvancedSettingsCoordinator], SensorEntity):
125169
"""Sensor for car power draw."""
126170
_attr_name = "CT Reading"
127171
_attr_device_class = SensorDeviceClass.CURRENT
128172
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
129173

130174
def __init__(
131175
self,
132-
coordinator: OhmeChargeSessionsCoordinator,
176+
coordinator: OhmeAdvancedSettingsCoordinator,
133177
hass: HomeAssistant,
134178
client):
135179
super().__init__(coordinator=coordinator)
@@ -171,7 +215,7 @@ class EnergyUsageSensor(CoordinatorEntity[OhmeStatisticsCoordinator], SensorEnti
171215

172216
def __init__(
173217
self,
174-
coordinator: OhmeChargeSessionsCoordinator,
218+
coordinator: OhmeStatisticsCoordinator,
175219
hass: HomeAssistant,
176220
client):
177221
super().__init__(coordinator=coordinator)
@@ -206,9 +250,9 @@ def native_value(self):
206250
return None
207251

208252

209-
class NextSlotSensor(CoordinatorEntity[OhmeStatisticsCoordinator], SensorEntity):
210-
"""Sensor for next smart charge slot."""
211-
_attr_name = "Next Smart Charge Slot"
253+
class NextSlotStartSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
254+
"""Sensor for next smart charge slot start time."""
255+
_attr_name = "Next Charge Slot Start"
212256
_attr_device_class = SensorDeviceClass.TIMESTAMP
213257

214258
def __init__(
@@ -234,6 +278,58 @@ def unique_id(self) -> str:
234278
"""Return the unique ID of the sensor."""
235279
return self._client.get_unique_id("next_slot")
236280

281+
@property
282+
def icon(self):
283+
"""Icon of the sensor."""
284+
return "mdi:clock-star-four-points"
285+
286+
@property
287+
def native_value(self):
288+
"""Return pre-calculated state."""
289+
return self._state
290+
291+
@callback
292+
def _handle_coordinator_update(self) -> None:
293+
"""Calculate next timeslot. This is a bit slow so we only update on coordinator data update."""
294+
if self.coordinator.data is None or self.coordinator.data["mode"] == "DISCONNECTED":
295+
self._state = None
296+
else:
297+
self._state = charge_graph_next_slot(
298+
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])['start']
299+
300+
self._last_updated = utcnow()
301+
302+
self.async_write_ha_state()
303+
304+
305+
class NextSlotEndSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity):
306+
"""Sensor for next smart charge slot end time."""
307+
_attr_name = "Next Charge Slot End"
308+
_attr_device_class = SensorDeviceClass.TIMESTAMP
309+
310+
def __init__(
311+
self,
312+
coordinator: OhmeChargeSessionsCoordinator,
313+
hass: HomeAssistant,
314+
client):
315+
super().__init__(coordinator=coordinator)
316+
317+
self._state = None
318+
self._attributes = {}
319+
self._last_updated = None
320+
self._client = client
321+
322+
self.entity_id = generate_entity_id(
323+
"sensor.{}", "ohme_next_slot_end", hass=hass)
324+
325+
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
326+
)
327+
328+
@property
329+
def unique_id(self) -> str:
330+
"""Return the unique ID of the sensor."""
331+
return self._client.get_unique_id("next_slot_end")
332+
237333
@property
238334
def icon(self):
239335
"""Icon of the sensor."""
@@ -251,7 +347,7 @@ def _handle_coordinator_update(self) -> None:
251347
self._state = None
252348
else:
253349
self._state = charge_graph_next_slot(
254-
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])
350+
self.coordinator.data['startTime'], self.coordinator.data['chargeGraph']['points'])['end']
255351

256352
self._last_updated = utcnow()
257353

0 commit comments

Comments
 (0)