Skip to content

Commit

Permalink
Multiple account support (#103)
Browse files Browse the repository at this point in the history
* Change config flow unique ID

* Bump version

* Fix multi account support

* Implement translation keys, OhmeEntity base class and new unique IDs

* Bug fix with approve button

* Bump manifest version
  • Loading branch information
dan-r authored Dec 5, 2024
1 parent c07c8a5 commit aec68a6
Show file tree
Hide file tree
Showing 15 changed files with 342 additions and 762 deletions.
41 changes: 33 additions & 8 deletions custom_components/ohme/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from homeassistant import core
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from .const import *
from .utils import get_option
from .api_client import OhmeApiClient
Expand All @@ -16,9 +17,11 @@ async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
async def async_setup_dependencies(hass, entry):
"""Instantiate client and refresh session"""
client = OhmeApiClient(entry.data['email'], entry.data['password'])
hass.data[DOMAIN][DATA_CLIENT] = client
account_id = entry.data['email']

hass.data[DOMAIN][DATA_OPTIONS] = entry.options
hass.data[DOMAIN][account_id][DATA_CLIENT] = client

hass.data[DOMAIN][account_id][DATA_OPTIONS] = entry.options

await client.async_create_session()
await client.async_update_device_info()
Expand All @@ -33,15 +36,37 @@ async def async_update_listener(hass, entry):

async def async_setup_entry(hass, entry):
"""This is called from the config flow."""
hass.data.setdefault(DOMAIN, {})

def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None:
"""Update unique IDs from old format."""
if entry.unique_id.startswith("ohme_"):
parts = entry.unique_id.split('_')
legacy_id = '_'.join(parts[2:])

if legacy_id in LEGACY_MAPPING:
new_id = LEGACY_MAPPING[legacy_id]
else:
new_id = legacy_id

new_id = f"{parts[1]}_{new_id}"

return {"new_unique_id": new_id}
return None

await async_migrate_entries(hass, entry.entry_id, _update_unique_id)

account_id = entry.data['email']

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(account_id, {})

await async_setup_dependencies(hass, entry)

coordinators = [
OhmeChargeSessionsCoordinator(hass=hass), # COORDINATOR_CHARGESESSIONS
OhmeAccountInfoCoordinator(hass=hass), # COORDINATOR_ACCOUNTINFO
OhmeAdvancedSettingsCoordinator(hass=hass), # COORDINATOR_ADVANCED
OhmeChargeSchedulesCoordinator(hass=hass) # COORDINATOR_SCHEDULES
OhmeChargeSessionsCoordinator(hass=hass, account_id=account_id), # COORDINATOR_CHARGESESSIONS
OhmeAccountInfoCoordinator(hass=hass, account_id=account_id), # COORDINATOR_ACCOUNTINFO
OhmeAdvancedSettingsCoordinator(hass=hass, account_id=account_id), # COORDINATOR_ADVANCED
OhmeChargeSchedulesCoordinator(hass=hass, account_id=account_id) # COORDINATOR_SCHEDULES
]

# We can function without these so setup can continue
Expand All @@ -63,7 +88,7 @@ async def async_setup_entry(hass, entry):
else:
raise ex

hass.data[DOMAIN][DATA_COORDINATORS] = coordinators
hass.data[DOMAIN][account_id][DATA_COORDINATORS] = coordinators

# Setup entities
await hass.config_entries.async_forward_entry_setups(entry, ENTITY_TYPES)
Expand Down
39 changes: 18 additions & 21 deletions custom_components/ohme/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, email, password):
raise Exception("Credentials not provided")

# Credentials from configuration
self._email = email
self.email = email
self._password = password

# Charger and its capabilities
Expand All @@ -38,7 +38,7 @@ def __init__(self, email, password):

# User info
self._user_id = ""
self._serial = ""
self.serial = ""

# Cache the last rule to use when we disable max charge or change schedule
self._last_rule = {}
Expand All @@ -55,7 +55,7 @@ async def async_create_session(self):
"""Refresh the user auth token from the stored credentials."""
async with self._auth_session.post(
f"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={GOOGLE_API_KEY}",
data={"email": self._email, "password": self._password,
data={"email": self.email, "password": self._password,
"returnSecureToken": True}
) as resp:
if resp.status != 200:
Expand Down Expand Up @@ -171,29 +171,26 @@ def cap_available(self):
def get_device_info(self):
return self._device_info

def get_unique_id(self, name):
return f"ohme_{self._serial}_{name}"

# Push methods

async def async_pause_charge(self):
"""Pause an ongoing charge"""
result = await self._post_request(f"/v1/chargeSessions/{self._serial}/stop", skip_json=True)
result = await self._post_request(f"/v1/chargeSessions/{self.serial}/stop", skip_json=True)
return bool(result)

async def async_resume_charge(self):
"""Resume a paused charge"""
result = await self._post_request(f"/v1/chargeSessions/{self._serial}/resume", skip_json=True)
result = await self._post_request(f"/v1/chargeSessions/{self.serial}/resume", skip_json=True)
return bool(result)

async def async_approve_charge(self):
"""Approve a charge"""
result = await self._put_request(f"/v1/chargeSessions/{self._serial}/approve?approve=true")
result = await self._put_request(f"/v1/chargeSessions/{self.serial}/approve?approve=true")
return bool(result)

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

async def async_apply_session_rule(self, max_price=None, target_time=None, target_percent=None, pre_condition=None, pre_condition_length=None):
Expand Down Expand Up @@ -228,7 +225,7 @@ async def async_apply_session_rule(self, max_price=None, target_time=None, targe
max_price = 'true' if max_price else 'false'
pre_condition = 'true' if pre_condition else 'false'

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}")
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):
Expand Down Expand Up @@ -275,7 +272,7 @@ async def async_update_schedule(self, target_percent=None, target_time=None, pre

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

# Pull methods
Expand Down Expand Up @@ -303,20 +300,20 @@ async def async_update_device_info(self, is_retry=False):

device = resp['chargeDevices'][0]

info = DeviceInfo(
identifiers={(DOMAIN, "ohme_charger")},
self._capabilities = device['modelCapabilities']
self._user_id = resp['user']['id']
self.serial = device['id']
self._provision_date = device['provisioningTs']

self._device_info = DeviceInfo(
identifiers={(DOMAIN, f"ohme_charger_{self.serial}")},
name=device['modelTypeDisplayName'],
manufacturer="Ohme",
model=device['modelTypeDisplayName'].replace("Ohme ", ""),
sw_version=device['firmwareVersionLabel'],
serial_number=device['id']
serial_number=self.serial
)

self._capabilities = device['modelCapabilities']
self._user_id = resp['user']['id']
self._serial = device['id']
self._device_info = info
self._provision_date = device['provisioningTs']

if resp['tariff'] is not None and resp['tariff']['dsrTariff']:
self._disable_cap = True
Expand All @@ -329,7 +326,7 @@ async def async_update_device_info(self, is_retry=False):

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

# If we ever get a reading above 0, assume CT connected
if resp['clampAmps'] and resp['clampAmps'] > 0:
Expand Down
37 changes: 37 additions & 0 deletions custom_components/ohme/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from homeassistant.helpers.entity import Entity
from homeassistant.core import callback

class OhmeEntity(Entity):
"""Base class for all Ohme entities."""

_attr_has_entity_name = True

def __init__(self, cooordinator, hass, client):
"""Initialize the entity."""
self.coordinator = cooordinator
self._hass = hass
self._client = client

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

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
)
)

@callback
def _handle_coordinator_update(self) -> None:
self.async_write_ha_state()

@property
def unique_id(self):
"""Return unique ID of the entity."""
return f"{self._client.serial}_{self._attr_translation_key}"
Loading

0 comments on commit aec68a6

Please sign in to comment.