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

Commit a5bd807

Browse files
authored
Add energy usage sensor and tweak switch update logic (#3)
* Tweak switch update logic * Added accumulative energy usage sensor * Properly implement unique_id * Changed energy usage sensor to kWh
1 parent 0dba935 commit a5bd807

File tree

7 files changed

+181
-70
lines changed

7 files changed

+181
-70
lines changed

custom_components/ohme/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from homeassistant import core
22
from .const import *
33
from .client import OhmeApiClient
4-
from .coordinator import OhmeUpdateCoordinator
4+
from .coordinator import OhmeUpdateCoordinator, OhmeStatisticsUpdateCoordinator
55

66

77
async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
@@ -30,7 +30,14 @@ async def async_setup_entry(hass, entry):
3030
return False
3131

3232
await async_setup_dependencies(hass, config)
33+
34+
hass.data[DOMAIN][DATA_COORDINATOR] = OhmeUpdateCoordinator(hass=hass)
35+
await hass.data[DOMAIN][DATA_COORDINATOR].async_config_entry_first_refresh()
3336

37+
hass.data[DOMAIN][DATA_STATISTICS_COORDINATOR] = OhmeStatisticsUpdateCoordinator(hass=hass)
38+
await hass.data[DOMAIN][DATA_STATISTICS_COORDINATOR].async_config_entry_first_refresh()
39+
40+
# Create tasks for each entity type
3441
hass.async_create_task(
3542
hass.config_entries.async_forward_entry_setup(entry, "sensor")
3643
)
@@ -41,7 +48,9 @@ async def async_setup_entry(hass, entry):
4148
hass.config_entries.async_forward_entry_setup(entry, "switch")
4249
)
4350

44-
hass.data[DOMAIN][DATA_COORDINATOR] = OhmeUpdateCoordinator(hass=hass)
45-
await hass.data[DOMAIN][DATA_COORDINATOR].async_config_entry_first_refresh()
46-
4751
return True
52+
53+
async def async_unload_entry(hass, entry):
54+
"""Unload a config entry."""
55+
56+
return await hass.config_entries.async_unload_platforms(entry, ['binary_sensor', 'sensor', 'switch'])

custom_components/ohme/binary_sensor.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ async def async_setup_entry(
1818
async_add_entities,
1919
):
2020
"""Setup sensors and configure coordinator."""
21+
client = hass.data[DOMAIN][DATA_CLIENT]
2122
coordinator = hass.data[DOMAIN][DATA_COORDINATOR]
2223

23-
sensors = [ConnectedSensor(coordinator, hass),
24-
ChargingSensor(coordinator, hass)]
24+
sensors = [ConnectedSensor(coordinator, hass, client),
25+
ChargingSensor(coordinator, hass, client)]
2526

2627
async_add_entities(sensors, update_before_add=True)
2728

@@ -37,12 +38,14 @@ class ConnectedSensor(
3738
def __init__(
3839
self,
3940
coordinator: OhmeUpdateCoordinator,
40-
hass: HomeAssistant):
41+
hass: HomeAssistant,
42+
client):
4143
super().__init__(coordinator=coordinator)
4244

4345
self._attributes = {}
4446
self._last_updated = None
4547
self._state = False
48+
self._client = client
4649

4750
self.entity_id = generate_entity_id(
4851
"binary_sensor.{}", "ohme_car_connected", hass=hass)
@@ -58,7 +61,7 @@ def icon(self):
5861
@property
5962
def unique_id(self) -> str:
6063
"""Return the unique ID of the sensor."""
61-
return self.entity_id
64+
return self._client.get_unique_id("car_connected")
6265

6366
@property
6467
def is_on(self) -> bool:
@@ -81,12 +84,14 @@ class ChargingSensor(
8184
def __init__(
8285
self,
8386
coordinator: OhmeUpdateCoordinator,
84-
hass: HomeAssistant):
87+
hass: HomeAssistant,
88+
client):
8589
super().__init__(coordinator=coordinator)
8690

8791
self._attributes = {}
8892
self._last_updated = None
8993
self._state = False
94+
self._client = client
9095

9196
self.entity_id = generate_entity_id(
9297
"binary_sensor.{}", "ohme_car_charging", hass=hass)
@@ -102,12 +107,13 @@ def icon(self):
102107
@property
103108
def unique_id(self) -> str:
104109
"""Return the unique ID of the sensor."""
105-
return self.entity_id
110+
return self._client.get_unique_id("ohme_car_charging")
106111

107112
@property
108113
def is_on(self) -> bool:
109114
if self.coordinator.data and self.coordinator.data["power"]:
110-
self._state = self.coordinator.data["power"]["amp"] > 0
115+
# Assume the car is actively charging if drawing over 0 watts
116+
self._state = self.coordinator.data["power"]["watt"] > 0
111117
else:
112118
self._state = False
113119

custom_components/ohme/client/__init__.py

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import asyncio
33
import logging
44
import json
5+
from datetime import datetime, timedelta
56
from homeassistant.helpers.entity import DeviceInfo
67
from ..const import DOMAIN
78

@@ -20,6 +21,7 @@ def __init__(self, email, password):
2021

2122
self._device_info = None
2223
self._token = None
24+
self._user_id = ""
2325
self._serial = ""
2426
self._session = aiohttp.ClientSession()
2527

@@ -74,6 +76,21 @@ async def _put_request(self, url, data=None, is_retry=False):
7476

7577
return True
7678

79+
async def _get_request(self, url, is_retry=False):
80+
"""Try to make a GET request
81+
If we get a non 200 response, refresh auth token and try again"""
82+
async with self._session.get(
83+
url,
84+
headers={"Authorization": "Firebase %s" % self._token}
85+
) as resp:
86+
if resp.status != 200 and not is_retry:
87+
await self.async_refresh_session()
88+
return await self._get_request(url, is_retry=True)
89+
elif resp.status != 200:
90+
return False
91+
92+
return await resp.json()
93+
7794
async def async_pause_charge(self):
7895
"""Pause an ongoing charge"""
7996
result = await self._post_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/stop", skip_json=True)
@@ -98,46 +115,57 @@ async def async_stop_max_charge(self):
98115
async def async_get_charge_sessions(self, is_retry=False):
99116
"""Try to fetch charge sessions endpoint.
100117
If we get a non 200 response, refresh auth token and try again"""
101-
async with self._session.get(
102-
'https://api.ohme.io/v1/chargeSessions',
103-
headers={"Authorization": "Firebase %s" % self._token}
104-
) as resp:
105-
106-
if resp.status != 200 and not is_retry:
107-
await self.async_refresh_session()
108-
return await self.async_get_charge_sessions(True)
109-
elif resp.status != 200:
110-
return False
118+
resp = await self._get_request('https://api.ohme.io/v1/chargeSessions')
119+
120+
if not resp:
121+
return False
111122

112-
resp_json = await resp.json()
113-
return resp_json[0]
123+
return resp[0]
114124

115125
async def async_update_device_info(self, is_retry=False):
116126
"""Update _device_info with our charger model."""
117-
async with self._session.get(
118-
'https://api.ohme.io/v1/users/me/account',
119-
headers={"Authorization": "Firebase %s" % self._token}
120-
) as resp:
127+
resp = await self._get_request('https://api.ohme.io/v1/users/me/account')
121128

122-
if resp.status != 200 and not is_retry:
123-
await self.async_refresh_session()
124-
return await self.async_get_device_info(True)
125-
elif resp.status != 200:
126-
return False
129+
if not resp:
130+
return False
127131

128-
resp_json = await resp.json()
129-
device = resp_json['chargeDevices'][0]
130-
131-
info = DeviceInfo(
132-
identifiers={(DOMAIN, "ohme_charger")},
133-
name=device['modelTypeDisplayName'],
134-
manufacturer="Ohme",
135-
model=device['modelTypeDisplayName'].replace("Ohme ", ""),
136-
sw_version=device['firmwareVersionLabel'],
137-
serial_number=device['id']
138-
)
139-
self._serial = device['id']
140-
self._device_info = info
132+
device = resp['chargeDevices'][0]
133+
134+
info = DeviceInfo(
135+
identifiers={(DOMAIN, "ohme_charger")},
136+
name=device['modelTypeDisplayName'],
137+
manufacturer="Ohme",
138+
model=device['modelTypeDisplayName'].replace("Ohme ", ""),
139+
sw_version=device['firmwareVersionLabel'],
140+
serial_number=device['id']
141+
)
142+
143+
self._user_id = resp['user']['id']
144+
self._serial = device['id']
145+
self._device_info = info
146+
147+
return True
148+
149+
def _last_second_of_month_timestamp(self):
150+
"""Get the last second of this month."""
151+
dt = datetime.today()
152+
dt = dt.replace(day=1) + timedelta(days=32)
153+
dt = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - timedelta(seconds=1)
154+
return int(dt.timestamp()*1e3)
155+
156+
async def async_get_charge_statistics(self):
157+
"""Get charge statistics. Currently this is just for all time (well, Jan 2019)."""
158+
end_ts = self._last_second_of_month_timestamp()
159+
resp = await self._get_request(f"https://api.ohme.io/v1/chargeSessions/summary/users/{self._user_id}?&startTs=1546300800000&endTs={end_ts}&granularity=MONTH")
160+
161+
if not resp:
162+
return False
163+
164+
return resp['totalStats']
141165

142166
def get_device_info(self):
143167
return self._device_info
168+
169+
def get_unique_id(self, name):
170+
return f"ohme_{self._serial}_{name}"
171+

custom_components/ohme/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
DOMAIN = "ohme"
44
DATA_CLIENT = "client"
55
DATA_COORDINATOR = "coordinator"
6+
DATA_STATISTICS_COORDINATOR = "statistics_coordinator"

custom_components/ohme/coordinator.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,26 @@ async def _async_update_data(self):
3131

3232
except BaseException:
3333
raise UpdateFailed("Error communicating with API")
34+
35+
36+
class OhmeStatisticsUpdateCoordinator(DataUpdateCoordinator):
37+
"""Coordinator to update statistics from API periodically.
38+
(But less so than OhmeUpdateCoordinator)"""
39+
40+
def __init__(self, hass):
41+
"""Initialise coordinator."""
42+
super().__init__(
43+
hass,
44+
_LOGGER,
45+
name="Ohme Charger Statistics",
46+
update_interval=timedelta(minutes=30),
47+
)
48+
self._client = hass.data[DOMAIN][DATA_CLIENT]
49+
50+
async def _async_update_data(self):
51+
"""Fetch data from API endpoint."""
52+
try:
53+
return await self._client.async_get_charge_statistics()
54+
55+
except BaseException:
56+
raise UpdateFailed("Error communicating with API")

custom_components/ohme/sensor.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
SensorEntity
77
)
88
from homeassistant.helpers.update_coordinator import CoordinatorEntity
9-
from homeassistant.const import UnitOfPower
9+
from homeassistant.const import UnitOfPower, UnitOfEnergy
1010
from homeassistant.core import HomeAssistant
1111
from homeassistant.helpers.entity import generate_entity_id
12-
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATOR
13-
from .coordinator import OhmeUpdateCoordinator
12+
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATOR, DATA_STATISTICS_COORDINATOR
13+
from .coordinator import OhmeUpdateCoordinator, OhmeStatisticsUpdateCoordinator
1414

1515

1616
async def async_setup_entry(
@@ -19,9 +19,11 @@ async def async_setup_entry(
1919
async_add_entities
2020
):
2121
"""Setup sensors and configure coordinator."""
22+
client = hass.data[DOMAIN][DATA_CLIENT]
2223
coordinator = hass.data[DOMAIN][DATA_COORDINATOR]
24+
stats_coordinator = hass.data[DOMAIN][DATA_STATISTICS_COORDINATOR]
2325

24-
sensors = [PowerDrawSensor(coordinator, hass)]
26+
sensors = [PowerDrawSensor(coordinator, hass, client), EnergyUsageSensor(stats_coordinator, hass, client)]
2527

2628
async_add_entities(sensors, update_before_add=True)
2729

@@ -35,23 +37,24 @@ class PowerDrawSensor(CoordinatorEntity[OhmeUpdateCoordinator], SensorEntity):
3537
def __init__(
3638
self,
3739
coordinator: OhmeUpdateCoordinator,
38-
hass: HomeAssistant):
40+
hass: HomeAssistant,
41+
client):
3942
super().__init__(coordinator=coordinator)
4043

4144
self._state = None
4245
self._attributes = {}
4346
self._last_updated = None
47+
self._client = client
4448

4549
self.entity_id = generate_entity_id(
4650
"sensor.{}", "ohme_power_draw", hass=hass)
4751

48-
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
49-
)
52+
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info()
5053

5154
@property
5255
def unique_id(self) -> str:
5356
"""Return the unique ID of the sensor."""
54-
return self.entity_id
57+
return self._client.get_unique_id("power_draw")
5558

5659
@property
5760
def icon(self):
@@ -64,3 +67,45 @@ def native_value(self):
6467
if self.coordinator.data and self.coordinator.data['power']:
6568
return self.coordinator.data['power']['watt']
6669
return 0
70+
71+
72+
class EnergyUsageSensor(CoordinatorEntity[OhmeStatisticsUpdateCoordinator], SensorEntity):
73+
"""Sensor for total energy usage."""
74+
_attr_name = "Ohme Accumulative Energy Usage"
75+
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
76+
_attr_device_class = SensorDeviceClass.ENERGY
77+
78+
def __init__(
79+
self,
80+
coordinator: OhmeUpdateCoordinator,
81+
hass: HomeAssistant,
82+
client):
83+
super().__init__(coordinator=coordinator)
84+
85+
self._state = None
86+
self._attributes = {}
87+
self._last_updated = None
88+
self._client = client
89+
90+
self.entity_id = generate_entity_id(
91+
"sensor.{}", "ohme_accumulative_energy", hass=hass)
92+
93+
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info()
94+
95+
@property
96+
def unique_id(self) -> str:
97+
"""Return the unique ID of the sensor."""
98+
return self._client.get_unique_id("accumulative_energy")
99+
100+
@property
101+
def icon(self):
102+
"""Icon of the sensor."""
103+
return "mdi:lightning-bolt"
104+
105+
@property
106+
def native_value(self):
107+
"""Get value from data returned from API by coordinator"""
108+
if self.coordinator.data and self.coordinator.data['energyChargedTotalWh']:
109+
return self.coordinator.data['energyChargedTotalWh'] / 1000
110+
111+
return None

0 commit comments

Comments
 (0)