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

Commit 0e4494e

Browse files
committed
Implement translation keys, OhmeEntity base class and new unique IDs
1 parent 94028fe commit 0e4494e

File tree

11 files changed

+264
-693
lines changed

11 files changed

+264
-693
lines changed

custom_components/ohme/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from homeassistant import core
3+
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
34
from .const import *
45
from .utils import get_option
56
from .api_client import OhmeApiClient
@@ -35,6 +36,25 @@ async def async_update_listener(hass, entry):
3536

3637
async def async_setup_entry(hass, entry):
3738
"""This is called from the config flow."""
39+
40+
def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None:
41+
"""Update unique IDs from old format."""
42+
if entry.unique_id.startswith("ohme_"):
43+
parts = entry.unique_id.split('_')
44+
legacy_id = '_'.join(parts[2:])
45+
46+
if legacy_id in LEGACY_MAPPING:
47+
new_id = LEGACY_MAPPING[legacy_id]
48+
else:
49+
new_id = legacy_id
50+
51+
new_id = f"{parts[1]}_{new_id}"
52+
53+
return {"new_unique_id": new_id}
54+
return None
55+
56+
await async_migrate_entries(hass, entry.entry_id, _update_unique_id)
57+
3858
account_id = entry.data['email']
3959

4060
hass.data.setdefault(DOMAIN, {})

custom_components/ohme/api_client.py

+11-14
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def __init__(self, email, password):
3838

3939
# User info
4040
self._user_id = ""
41-
self._serial = ""
41+
self.serial = ""
4242

4343
# Cache the last rule to use when we disable max charge or change schedule
4444
self._last_rule = {}
@@ -171,29 +171,26 @@ def cap_available(self):
171171
def get_device_info(self):
172172
return self._device_info
173173

174-
def get_unique_id(self, name):
175-
return f"ohme_{self._serial}_{name}"
176-
177174
# Push methods
178175

179176
async def async_pause_charge(self):
180177
"""Pause an ongoing charge"""
181-
result = await self._post_request(f"/v1/chargeSessions/{self._serial}/stop", skip_json=True)
178+
result = await self._post_request(f"/v1/chargeSessions/{self.serial}/stop", skip_json=True)
182179
return bool(result)
183180

184181
async def async_resume_charge(self):
185182
"""Resume a paused charge"""
186-
result = await self._post_request(f"/v1/chargeSessions/{self._serial}/resume", skip_json=True)
183+
result = await self._post_request(f"/v1/chargeSessions/{self.serial}/resume", skip_json=True)
187184
return bool(result)
188185

189186
async def async_approve_charge(self):
190187
"""Approve a charge"""
191-
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/approve?approve=true")
188+
result = await self._put_request(f"/v1/chargeSessions/{self.serial}/approve?approve=true")
192189
return bool(result)
193190

194191
async def async_max_charge(self, state=True):
195192
"""Enable max charge"""
196-
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/rule?maxCharge=" + str(state).lower())
193+
result = await self._put_request(f"/v1/chargeSessions/{self.serial}/rule?maxCharge=" + str(state).lower())
197194
return bool(result)
198195

199196
async def async_apply_session_rule(self, max_price=None, target_time=None, target_percent=None, pre_condition=None, pre_condition_length=None):
@@ -228,7 +225,7 @@ async def async_apply_session_rule(self, max_price=None, target_time=None, targe
228225
max_price = 'true' if max_price else 'false'
229226
pre_condition = 'true' if pre_condition else 'false'
230227

231-
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}")
228+
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}")
232229
return bool(result)
233230

234231
async def async_change_price_cap(self, enabled=None, cap=None):
@@ -275,7 +272,7 @@ async def async_update_schedule(self, target_percent=None, target_time=None, pre
275272

276273
async def async_set_configuration_value(self, values):
277274
"""Set a configuration value or values."""
278-
result = await self._put_request(f"/v1/chargeDevices/{self._serial}/appSettings", data=values)
275+
result = await self._put_request(f"/v1/chargeDevices/{self.serial}/appSettings", data=values)
279276
return bool(result)
280277

281278
# Pull methods
@@ -305,16 +302,16 @@ async def async_update_device_info(self, is_retry=False):
305302

306303
self._capabilities = device['modelCapabilities']
307304
self._user_id = resp['user']['id']
308-
self._serial = device['id']
305+
self.serial = device['id']
309306
self._provision_date = device['provisioningTs']
310307

311308
self._device_info = DeviceInfo(
312-
identifiers={(DOMAIN, f"ohme_charger_{self._serial}")},
309+
identifiers={(DOMAIN, f"ohme_charger_{self.serial}")},
313310
name=device['modelTypeDisplayName'],
314311
manufacturer="Ohme",
315312
model=device['modelTypeDisplayName'].replace("Ohme ", ""),
316313
sw_version=device['firmwareVersionLabel'],
317-
serial_number=self._serial
314+
serial_number=self.serial
318315
)
319316

320317

@@ -329,7 +326,7 @@ async def async_update_device_info(self, is_retry=False):
329326

330327
async def async_get_advanced_settings(self):
331328
"""Get advanced settings (mainly for CT clamp reading)"""
332-
resp = await self._get_request(f"/v1/chargeDevices/{self._serial}/advancedSettings")
329+
resp = await self._get_request(f"/v1/chargeDevices/{self.serial}/advancedSettings")
333330

334331
# If we ever get a reading above 0, assume CT connected
335332
if resp['clampAmps'] and resp['clampAmps'] > 0:

custom_components/ohme/base.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from homeassistant.helpers.entity import Entity
2+
from homeassistant.core import callback
3+
4+
class OhmeEntity(Entity):
5+
"""Base class for all Ohme entities."""
6+
7+
_attr_has_entity_name = True
8+
9+
def __init__(self, cooordinator, hass, client):
10+
"""Initialize the entity."""
11+
self.coordinator = cooordinator
12+
self._hass = hass
13+
self._client = client
14+
15+
self._attributes = {}
16+
self._last_updated = None
17+
self._state = None
18+
19+
self._attr_device_info = client.get_device_info()
20+
21+
async def async_added_to_hass(self) -> None:
22+
"""When entity is added to hass."""
23+
await super().async_added_to_hass()
24+
self.async_on_remove(
25+
self.coordinator.async_add_listener(
26+
self._handle_coordinator_update, None
27+
)
28+
)
29+
30+
@callback
31+
def _handle_coordinator_update(self) -> None:
32+
self.async_write_ha_state()
33+
34+
@property
35+
def unique_id(self):
36+
"""Return unique ID of the entity."""
37+
return f"{self._client.serial}_{self._attr_translation_key}"

custom_components/ohme/binary_sensor.py

+17-140
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from homeassistant.helpers.entity import generate_entity_id
1111
from homeassistant.util.dt import (utcnow)
1212
from .const import DOMAIN, DATA_COORDINATORS, DATA_SLOTS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ADVANCED, DATA_CLIENT
13-
from .coordinator import OhmeChargeSessionsCoordinator, OhmeAdvancedSettingsCoordinator
1413
from .utils import in_slot
14+
from .base import OhmeEntity
1515

1616
_LOGGER = logging.getLogger(__name__)
1717

@@ -36,40 +36,14 @@ async def async_setup_entry(
3636

3737

3838
class ConnectedBinarySensor(
39-
CoordinatorEntity[OhmeChargeSessionsCoordinator],
39+
OhmeEntity,
4040
BinarySensorEntity):
4141
"""Binary sensor for if car is plugged in."""
4242

43-
_attr_name = "Car Connected"
43+
_attr_translation_key = "car_connected"
44+
_attr_icon = "mdi:ev-plug-type2"
4445
_attr_device_class = BinarySensorDeviceClass.PLUG
4546

46-
def __init__(
47-
self,
48-
coordinator: OhmeChargeSessionsCoordinator,
49-
hass: HomeAssistant,
50-
client):
51-
super().__init__(coordinator=coordinator)
52-
53-
self._attributes = {}
54-
self._last_updated = None
55-
self._state = False
56-
self._client = client
57-
58-
self.entity_id = generate_entity_id(
59-
"binary_sensor.{}", "ohme_car_connected", hass=hass)
60-
61-
self._attr_device_info = client.get_device_info()
62-
63-
@property
64-
def icon(self):
65-
"""Icon of the sensor."""
66-
return "mdi:ev-plug-type2"
67-
68-
@property
69-
def unique_id(self) -> str:
70-
"""Return the unique ID of the sensor."""
71-
return self._client.get_unique_id("car_connected")
72-
7347
@property
7448
def is_on(self) -> bool:
7549
if self.coordinator.data is None:
@@ -81,24 +55,20 @@ def is_on(self) -> bool:
8155

8256

8357
class ChargingBinarySensor(
84-
CoordinatorEntity[OhmeChargeSessionsCoordinator],
58+
OhmeEntity,
8559
BinarySensorEntity):
8660
"""Binary sensor for if car is charging."""
8761

88-
_attr_name = "Car Charging"
62+
_attr_translation_key = "car_charging"
63+
_attr_icon = "mdi:battery-charging-100"
8964
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
9065

9166
def __init__(
9267
self,
9368
coordinator: OhmeChargeSessionsCoordinator,
9469
hass: HomeAssistant,
9570
client):
96-
super().__init__(coordinator=coordinator)
97-
98-
self._attributes = {}
99-
self._last_updated = None
100-
self._state = False
101-
self._client = client
71+
super().__init__(coordinator, hass, client)
10272

10373
# Cache the last power readings
10474
self._last_reading = None
@@ -107,21 +77,6 @@ def __init__(
10777
# State variables for charge state detection
10878
self._trigger_count = 0
10979

110-
self.entity_id = generate_entity_id(
111-
"binary_sensor.{}", "ohme_car_charging", hass=hass)
112-
113-
self._attr_device_info = client.get_device_info()
114-
115-
@property
116-
def icon(self):
117-
"""Icon of the sensor."""
118-
return "mdi:battery-charging-100"
119-
120-
@property
121-
def unique_id(self) -> str:
122-
"""Return the unique ID of the sensor."""
123-
return self._client.get_unique_id("ohme_car_charging")
124-
12580
@property
12681
def is_on(self) -> bool:
12782
return self._state
@@ -208,38 +163,12 @@ def _handle_coordinator_update(self) -> None:
208163

209164

210165
class PendingApprovalBinarySensor(
211-
CoordinatorEntity[OhmeChargeSessionsCoordinator],
166+
OhmeEntity,
212167
BinarySensorEntity):
213168
"""Binary sensor for if a charge is pending approval."""
214169

215-
_attr_name = "Pending Approval"
216-
217-
def __init__(
218-
self,
219-
coordinator: OhmeChargeSessionsCoordinator,
220-
hass: HomeAssistant,
221-
client):
222-
super().__init__(coordinator=coordinator)
223-
224-
self._attributes = {}
225-
self._last_updated = None
226-
self._state = False
227-
self._client = client
228-
229-
self.entity_id = generate_entity_id(
230-
"binary_sensor.{}", "ohme_pending_approval", hass=hass)
231-
232-
self._attr_device_info = client.get_device_info()
233-
234-
@property
235-
def icon(self):
236-
"""Icon of the sensor."""
237-
return "mdi:alert-decagram"
238-
239-
@property
240-
def unique_id(self) -> str:
241-
"""Return the unique ID of the sensor."""
242-
return self._client.get_unique_id("pending_approval")
170+
_attr_translation_key = "pending_approval"
171+
_attr_icon = "mdi:alert-decagram"
243172

244173
@property
245174
def is_on(self) -> bool:
@@ -253,38 +182,12 @@ def is_on(self) -> bool:
253182

254183

255184
class CurrentSlotBinarySensor(
256-
CoordinatorEntity[OhmeChargeSessionsCoordinator],
185+
OhmeEntity,
257186
BinarySensorEntity):
258187
"""Binary sensor for if we are currently in a smart charge slot."""
259188

260-
_attr_name = "Charge Slot Active"
261-
262-
def __init__(
263-
self,
264-
coordinator: OhmeChargeSessionsCoordinator,
265-
hass: HomeAssistant,
266-
client):
267-
super().__init__(coordinator=coordinator)
268-
269-
self._last_updated = None
270-
self._state = False
271-
self._client = client
272-
self._hass = hass
273-
274-
self.entity_id = generate_entity_id(
275-
"binary_sensor.{}", "ohme_slot_active", hass=hass)
276-
277-
self._attr_device_info = client.get_device_info()
278-
279-
@property
280-
def icon(self):
281-
"""Icon of the sensor."""
282-
return "mdi:calendar-check"
283-
284-
@property
285-
def unique_id(self) -> str:
286-
"""Return the unique ID of the sensor."""
287-
return self._client.get_unique_id("ohme_slot_active")
189+
_attr_translation_key = "slot_active"
190+
_attr_icon = "mdi:calendar-check"
288191

289192
@property
290193
def extra_state_attributes(self):
@@ -316,40 +219,14 @@ def _handle_coordinator_update(self) -> None:
316219
self.async_write_ha_state()
317220

318221
class ChargerOnlineBinarySensor(
319-
CoordinatorEntity[OhmeAdvancedSettingsCoordinator],
222+
OhmeEntity,
320223
BinarySensorEntity):
321224
"""Binary sensor for if charger is online."""
322225

323-
_attr_name = "Charger Online"
226+
_attr_translation_key = "charger_online"
227+
_attr_icon = "mdi:web"
324228
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
325229

326-
def __init__(
327-
self,
328-
coordinator: OhmeAdvancedSettingsCoordinator,
329-
hass: HomeAssistant,
330-
client):
331-
super().__init__(coordinator=coordinator)
332-
333-
self._attributes = {}
334-
self._last_updated = None
335-
self._state = None
336-
self._client = client
337-
338-
self.entity_id = generate_entity_id(
339-
"binary_sensor.{}", "ohme_charger_online", hass=hass)
340-
341-
self._attr_device_info = client.get_device_info()
342-
343-
@property
344-
def icon(self):
345-
"""Icon of the sensor."""
346-
return "mdi:web"
347-
348-
@property
349-
def unique_id(self) -> str:
350-
"""Return the unique ID of the sensor."""
351-
return self._client.get_unique_id("charger_online")
352-
353230
@property
354231
def is_on(self) -> bool:
355232
if self.coordinator.data and self.coordinator.data["online"]:

0 commit comments

Comments
 (0)