diff --git a/custom_components/ohme/__init__.py b/custom_components/ohme/__init__.py index c354e94..38e2867 100644 --- a/custom_components/ohme/__init__.py +++ b/custom_components/ohme/__init__.py @@ -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 @@ -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() @@ -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 @@ -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) diff --git a/custom_components/ohme/api_client.py b/custom_components/ohme/api_client.py index 211cc52..0cea6bd 100644 --- a/custom_components/ohme/api_client.py +++ b/custom_components/ohme/api_client.py @@ -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 @@ -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 = {} @@ -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: @@ -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): @@ -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): @@ -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 @@ -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 @@ -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: diff --git a/custom_components/ohme/base.py b/custom_components/ohme/base.py new file mode 100644 index 0000000..7db6493 --- /dev/null +++ b/custom_components/ohme/base.py @@ -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}" diff --git a/custom_components/ohme/binary_sensor.py b/custom_components/ohme/binary_sensor.py index 9ac2e8d..14ad93d 100644 --- a/custom_components/ohme/binary_sensor.py +++ b/custom_components/ohme/binary_sensor.py @@ -10,8 +10,8 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.util.dt import (utcnow) from .const import DOMAIN, DATA_COORDINATORS, DATA_SLOTS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ADVANCED, DATA_CLIENT -from .coordinator import OhmeChargeSessionsCoordinator, OhmeAdvancedSettingsCoordinator from .utils import in_slot +from .base import OhmeEntity _LOGGER = logging.getLogger(__name__) @@ -21,9 +21,10 @@ async def async_setup_entry( async_add_entities, ): """Setup sensors and configure coordinator.""" - client = hass.data[DOMAIN][DATA_CLIENT] - coordinator = hass.data[DOMAIN][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS] - coordinator_advanced = hass.data[DOMAIN][DATA_COORDINATORS][COORDINATOR_ADVANCED] + account_id = config_entry.data['email'] + client = hass.data[DOMAIN][account_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][account_id][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS] + coordinator_advanced = hass.data[DOMAIN][account_id][DATA_COORDINATORS][COORDINATOR_ADVANCED] sensors = [ConnectedBinarySensor(coordinator, hass, client), ChargingBinarySensor(coordinator, hass, client), @@ -35,41 +36,14 @@ async def async_setup_entry( class ConnectedBinarySensor( - CoordinatorEntity[OhmeChargeSessionsCoordinator], + OhmeEntity, BinarySensorEntity): """Binary sensor for if car is plugged in.""" - _attr_name = "Car Connected" + _attr_translation_key = "car_connected" + _attr_icon = "mdi:ev-plug-type2" _attr_device_class = BinarySensorDeviceClass.PLUG - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._attributes = {} - self._last_updated = None - self._state = False - self._client = client - - self.entity_id = generate_entity_id( - "binary_sensor.{}", "ohme_car_connected", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:ev-plug-type2" - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("car_connected") - @property def is_on(self) -> bool: if self.coordinator.data is None: @@ -81,11 +55,12 @@ def is_on(self) -> bool: class ChargingBinarySensor( - CoordinatorEntity[OhmeChargeSessionsCoordinator], + OhmeEntity, BinarySensorEntity): """Binary sensor for if car is charging.""" - _attr_name = "Car Charging" + _attr_translation_key = "car_charging" + _attr_icon = "mdi:battery-charging-100" _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING def __init__( @@ -93,12 +68,7 @@ def __init__( coordinator: OhmeChargeSessionsCoordinator, hass: HomeAssistant, client): - super().__init__(coordinator=coordinator) - - self._attributes = {} - self._last_updated = None - self._state = False - self._client = client + super().__init__(coordinator, hass, client) # Cache the last power readings self._last_reading = None @@ -107,22 +77,6 @@ def __init__( # State variables for charge state detection self._trigger_count = 0 - self.entity_id = generate_entity_id( - "binary_sensor.{}", "ohme_car_charging", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:battery-charging-100" - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("ohme_car_charging") - @property def is_on(self) -> bool: return self._state @@ -209,39 +163,12 @@ def _handle_coordinator_update(self) -> None: class PendingApprovalBinarySensor( - CoordinatorEntity[OhmeChargeSessionsCoordinator], + OhmeEntity, BinarySensorEntity): """Binary sensor for if a charge is pending approval.""" - _attr_name = "Pending Approval" - - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._attributes = {} - self._last_updated = None - self._state = False - self._client = client - - self.entity_id = generate_entity_id( - "binary_sensor.{}", "ohme_pending_approval", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:alert-decagram" - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("pending_approval") + _attr_translation_key = "pending_approval" + _attr_icon = "mdi:alert-decagram" @property def is_on(self) -> bool: @@ -255,45 +182,18 @@ def is_on(self) -> bool: class CurrentSlotBinarySensor( - CoordinatorEntity[OhmeChargeSessionsCoordinator], + OhmeEntity, BinarySensorEntity): """Binary sensor for if we are currently in a smart charge slot.""" - _attr_name = "Charge Slot Active" - - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._last_updated = None - self._state = False - self._client = client - self._hass = hass - - self.entity_id = generate_entity_id( - "binary_sensor.{}", "ohme_slot_active", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:calendar-check" - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("ohme_slot_active") + _attr_translation_key = "slot_active" + _attr_icon = "mdi:calendar-check" @property def extra_state_attributes(self): """Attributes of the sensor.""" now = utcnow() - slots = self._hass.data[DOMAIN][DATA_SLOTS] if DATA_SLOTS in self._hass.data[DOMAIN] else [] + slots = self._hass.data[DOMAIN][self._client.email][DATA_SLOTS] if DATA_SLOTS in self._hass.data[DOMAIN][self._client.email] else [] return { "planned_dispatches": [x for x in slots if not x['end'] or x['end'] > now], @@ -319,41 +219,14 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() class ChargerOnlineBinarySensor( - CoordinatorEntity[OhmeAdvancedSettingsCoordinator], + OhmeEntity, BinarySensorEntity): """Binary sensor for if charger is online.""" - _attr_name = "Charger Online" + _attr_translation_key = "charger_online" + _attr_icon = "mdi:web" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - def __init__( - self, - coordinator: OhmeAdvancedSettingsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._attributes = {} - self._last_updated = None - self._state = None - self._client = client - - self.entity_id = generate_entity_id( - "binary_sensor.{}", "ohme_charger_online", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:web" - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("charger_online") - @property def is_on(self) -> bool: if self.coordinator.data and self.coordinator.data["online"]: diff --git a/custom_components/ohme/button.py b/custom_components/ohme/button.py index 7c2ef62..b815be4 100644 --- a/custom_components/ohme/button.py +++ b/custom_components/ohme/button.py @@ -7,7 +7,7 @@ from homeassistant.components.button import ButtonEntity from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS -from .coordinator import OhmeChargeSessionsCoordinator +from .base import OhmeEntity _LOGGER = logging.getLogger(__name__) @@ -18,8 +18,10 @@ async def async_setup_entry( async_add_entities ): """Setup switches.""" - client = hass.data[DOMAIN][DATA_CLIENT] - coordinator = hass.data[DOMAIN][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS] + account_id = config_entry.data['email'] + + client = hass.data[DOMAIN][account_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][account_id][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS] buttons = [] @@ -31,36 +33,14 @@ async def async_setup_entry( async_add_entities(buttons, update_before_add=True) -class OhmeApproveChargeButton(ButtonEntity): +class OhmeApproveChargeButton(OhmeEntity, ButtonEntity): """Button for approving a charge.""" - _attr_name = "Approve Charge" - - def __init__(self, coordinator: OhmeChargeSessionsCoordinator, hass: HomeAssistant, client): - self._client = client - self._coordinator = coordinator - - self._state = False - self._last_updated = None - self._attributes = {} - - self.entity_id = generate_entity_id( - "switch.{}", "ohme_approve_charge", 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("approve_charge") - - @property - def icon(self): - """Icon of the switch.""" - return "mdi:check-decagram-outline" + _attr_translation_key = "approve_charge" + _attr_icon = "mdi:check-decagram-outline" async def async_press(self): """Approve the charge.""" await self._client.async_approve_charge() await asyncio.sleep(1) - await self._coordinator.async_refresh() + await self.coordinator.async_refresh() diff --git a/custom_components/ohme/config_flow.py b/custom_components/ohme/config_flow.py index 3381658..18fa4ab 100644 --- a/custom_components/ohme/config_flow.py +++ b/custom_components/ohme/config_flow.py @@ -18,14 +18,14 @@ async def async_step_user(self, info): errors = {} if info is not None: - await self.async_set_unique_id("ohme") + await self.async_set_unique_id(info['email']) self._abort_if_unique_id_configured() instance = OhmeApiClient(info['email'], info['password']) if await instance.async_refresh_session() is None: errors["base"] = "auth_error" else: return self.async_create_entry( - title="Ohme Charger", + title=info['email'], data=info ) diff --git a/custom_components/ohme/const.py b/custom_components/ohme/const.py index ec6fbb5..6667ae3 100644 --- a/custom_components/ohme/const.py +++ b/custom_components/ohme/const.py @@ -1,7 +1,7 @@ """Component constants""" DOMAIN = "ohme" USER_AGENT = "dan-r-homeassistant-ohme" -INTEGRATION_VERSION = "1.0.1" +INTEGRATION_VERSION = "1.1.0" CONFIG_VERSION = 1 ENTITY_TYPES = ["sensor", "binary_sensor", "switch", "button", "number", "time"] @@ -19,3 +19,16 @@ DEFAULT_INTERVAL_ACCOUNTINFO = 1 DEFAULT_INTERVAL_ADVANCED = 1 DEFAULT_INTERVAL_SCHEDULES = 10 + +LEGACY_MAPPING = { + "ohme_car_charging": "car_charging", + "ohme_slot_active": "slot_active", + "target_percent": "target_percentage", + "session_energy": "energy", + "next_slot": "next_slot_start", + "solarMode": "solar_mode", + "buttonsLocked": "lock_buttons", + "pluginsRequireApproval": "require_approval", + "stealthEnabled": "sleep_when_inactive", + "price_cap_enabled": "enable_price_cap" +} diff --git a/custom_components/ohme/coordinator.py b/custom_components/ohme/coordinator.py index ddb8b9d..dd0f75e 100644 --- a/custom_components/ohme/coordinator.py +++ b/custom_components/ohme/coordinator.py @@ -15,17 +15,17 @@ class OhmeChargeSessionsCoordinator(DataUpdateCoordinator): """Coordinator to pull main charge state and power/current draw.""" - def __init__(self, hass): + def __init__(self, hass, account_id): """Initialise coordinator.""" super().__init__( hass, _LOGGER, name="Ohme Charge Sessions", update_interval=timedelta(minutes= - get_option(hass, "interval_chargesessions", DEFAULT_INTERVAL_CHARGESESSIONS) + get_option(hass, account_id, "interval_chargesessions", DEFAULT_INTERVAL_CHARGESESSIONS) ), ) - self._client = hass.data[DOMAIN][DATA_CLIENT] + self._client = hass.data[DOMAIN][account_id][DATA_CLIENT] async def _async_update_data(self): """Fetch data from API endpoint.""" @@ -39,17 +39,17 @@ async def _async_update_data(self): class OhmeAccountInfoCoordinator(DataUpdateCoordinator): """Coordinator to pull charger settings.""" - def __init__(self, hass): + def __init__(self, hass, account_id): """Initialise coordinator.""" super().__init__( hass, _LOGGER, name="Ohme Account Info", update_interval=timedelta(minutes= - get_option(hass, "interval_accountinfo", DEFAULT_INTERVAL_ACCOUNTINFO) + get_option(hass, account_id, "interval_accountinfo", DEFAULT_INTERVAL_ACCOUNTINFO) ), ) - self._client = hass.data[DOMAIN][DATA_CLIENT] + self._client = hass.data[DOMAIN][account_id][DATA_CLIENT] async def _async_update_data(self): """Fetch data from API endpoint.""" @@ -63,17 +63,17 @@ async def _async_update_data(self): class OhmeAdvancedSettingsCoordinator(DataUpdateCoordinator): """Coordinator to pull CT clamp reading.""" - def __init__(self, hass): + def __init__(self, hass, account_id): """Initialise coordinator.""" super().__init__( hass, _LOGGER, name="Ohme Advanced Settings", update_interval=timedelta(minutes= - get_option(hass, "interval_advanced", DEFAULT_INTERVAL_ADVANCED) + get_option(hass, account_id, "interval_advanced", DEFAULT_INTERVAL_ADVANCED) ), ) - self._client = hass.data[DOMAIN][DATA_CLIENT] + self._client = hass.data[DOMAIN][account_id][DATA_CLIENT] async def _async_update_data(self): """Fetch data from API endpoint.""" @@ -87,17 +87,17 @@ async def _async_update_data(self): class OhmeChargeSchedulesCoordinator(DataUpdateCoordinator): """Coordinator to pull charge schedules.""" - def __init__(self, hass): + def __init__(self, hass, account_id): """Initialise coordinator.""" super().__init__( hass, _LOGGER, name="Ohme Charge Schedules", update_interval=timedelta(minutes= - get_option(hass, "interval_schedules", DEFAULT_INTERVAL_SCHEDULES) + get_option(hass, account_id, "interval_schedules", DEFAULT_INTERVAL_SCHEDULES) ), ) - self._client = hass.data[DOMAIN][DATA_CLIENT] + self._client = hass.data[DOMAIN][account_id][DATA_CLIENT] async def _async_update_data(self): """Fetch data from API endpoint.""" diff --git a/custom_components/ohme/manifest.json b/custom_components/ohme/manifest.json index 7c13dda..bc4ca92 100644 --- a/custom_components/ohme/manifest.json +++ b/custom_components/ohme/manifest.json @@ -10,5 +10,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/dan-r/HomeAssistant-Ohme/issues", "requirements": [], - "version": "1.0.0" + "version": "1.1.0" } diff --git a/custom_components/ohme/number.py b/custom_components/ohme/number.py index 3be54c3..333c816 100644 --- a/custom_components/ohme/number.py +++ b/custom_components/ohme/number.py @@ -7,6 +7,7 @@ from homeassistant.core import callback, HomeAssistant from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_ACCOUNTINFO, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES from .utils import session_in_progress +from .base import OhmeEntity async def async_setup_entry( @@ -15,9 +16,10 @@ async def async_setup_entry( async_add_entities ): """Setup switches and configure coordinator.""" - coordinators = hass.data[DOMAIN][DATA_COORDINATORS] + account_id = config_entry.data['email'] - client = hass.data[DOMAIN][DATA_CLIENT] + coordinators = hass.data[DOMAIN][account_id][DATA_COORDINATORS] + client = hass.data[DOMAIN][account_id][DATA_CLIENT] numbers = [TargetPercentNumber( coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client), @@ -32,51 +34,31 @@ async def async_setup_entry( async_add_entities(numbers, update_before_add=True) -class TargetPercentNumber(NumberEntity): +class TargetPercentNumber(OhmeEntity, NumberEntity): """Target percentage sensor.""" - _attr_name = "Target Percentage" + _attr_translation_key = "target_percentage" + _attr_icon = "mdi:battery-heart" _attr_device_class = NumberDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE _attr_suggested_display_precision = 0 def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client): - self.coordinator = coordinator + super().__init__(coordinator, hass, client) self.coordinator_schedules = coordinator_schedules - self._client = client - - self._state = None - self._last_updated = None - self._attributes = {} - - self.entity_id = generate_entity_id( - "number.{}", "ohme_target_percent", hass=hass) - - 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 - ) - ) self.async_on_remove( self.coordinator_schedules.async_add_listener( self._handle_coordinator_update, None ) ) - @property - def unique_id(self): - """The unique ID of the switch.""" - return self._client.get_unique_id("target_percent") - async def async_set_native_value(self, value: float) -> None: """Update the current value.""" # If session in progress, update this session, if not update the first schedule - if session_in_progress(self.hass, self.coordinator.data): + if session_in_progress(self.hass, self._client.email, self.coordinator.data): await self._client.async_apply_session_rule(target_percent=int(value)) await asyncio.sleep(1) await self.coordinator.async_refresh() @@ -85,16 +67,11 @@ async def async_set_native_value(self, value: float) -> None: await asyncio.sleep(1) await self.coordinator_schedules.async_refresh() - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:battery-heart" - @callback def _handle_coordinator_update(self) -> None: """Get value from data returned from API by coordinator""" # Set with the same logic as reading - if session_in_progress(self.hass, self.coordinator.data): + if session_in_progress(self.hass, self._client.email, self.coordinator.data): target = round( self.coordinator.data['appliedRule']['targetPercent']) elif self.coordinator_schedules.data: @@ -107,9 +84,10 @@ def native_value(self): return self._state -class PreconditioningNumber(NumberEntity): +class PreconditioningNumber(OhmeEntity, NumberEntity): """Preconditioning sensor.""" - _attr_name = "Preconditioning" + _attr_translation_key = "preconditioning" + _attr_icon = "mdi:air-conditioner" _attr_device_class = NumberDeviceClass.DURATION _attr_native_unit_of_measurement = UnitOfTime.MINUTES _attr_native_min_value = 0 @@ -117,43 +95,22 @@ class PreconditioningNumber(NumberEntity): _attr_native_max_value = 60 def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client): - self.coordinator = coordinator + super().__init__(coordinator, hass, client) self.coordinator_schedules = coordinator_schedules - self._client = client - - self._state = None - self._last_updated = None - self._attributes = {} - - self.entity_id = generate_entity_id( - "number.{}", "ohme_preconditioning", hass=hass) - - 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 - ) - ) self.async_on_remove( self.coordinator_schedules.async_add_listener( self._handle_coordinator_update, None ) ) - @property - def unique_id(self): - """The unique ID of the switch.""" - return self._client.get_unique_id("preconditioning") - async def async_set_native_value(self, value: float) -> None: """Update the current value.""" # If session in progress, update this session, if not update the first schedule - if session_in_progress(self.hass, self.coordinator.data): + if session_in_progress(self.hass, self._client.email, self.coordinator.data): if value == 0: await self._client.async_apply_session_rule(pre_condition=False) else: @@ -168,17 +125,12 @@ async def async_set_native_value(self, value: float) -> None: await asyncio.sleep(1) await self.coordinator_schedules.async_refresh() - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:air-conditioner" - @callback def _handle_coordinator_update(self) -> None: """Get value from data returned from API by coordinator""" precondition = None # Set with the same logic as reading - if session_in_progress(self.hass, self.coordinator.data): + if session_in_progress(self.hass, self._client.email, self.coordinator.data): enabled = self.coordinator.data['appliedRule'].get( 'preconditioningEnabled', False) precondition = 0 if not enabled else self.coordinator.data['appliedRule'].get( @@ -196,37 +148,15 @@ def native_value(self): return self._state -class PriceCapNumber(NumberEntity): - _attr_name = "Price Cap" +class PriceCapNumber(OhmeEntity, NumberEntity): + _attr_translation_key = "price_cap" + _attr_icon = "mdi:cash" _attr_device_class = NumberDeviceClass.MONETARY _attr_mode = NumberMode.BOX _attr_native_step = 0.1 _attr_native_min_value = -100 _attr_native_max_value = 100 - def __init__(self, coordinator, hass: HomeAssistant, client): - self.coordinator = coordinator - self._client = client - self._state = None - self.entity_id = generate_entity_id( - "number.{}", "ohme_price_cap", hass=hass) - - 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 - ) - ) - - @property - def unique_id(self): - """The unique ID of the switch.""" - return self._client.get_unique_id("price_cap") - async def async_set_native_value(self, value: float) -> None: """Update the current value.""" await self._client.async_change_price_cap(cap=value) @@ -248,11 +178,6 @@ def native_unit_of_measurement(self): return penny_unit.get(currency, f"{currency}/100") - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:cash" - @callback def _handle_coordinator_update(self) -> None: """Get value from data returned from API by coordinator""" diff --git a/custom_components/ohme/sensor.py b/custom_components/ohme/sensor.py index 2ad81ff..7c504e6 100644 --- a/custom_components/ohme/sensor.py +++ b/custom_components/ohme/sensor.py @@ -15,6 +15,7 @@ from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, DATA_SLOTS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ADVANCED from .coordinator import OhmeChargeSessionsCoordinator, OhmeAdvancedSettingsCoordinator from .utils import next_slot, get_option, slot_list, slot_list_str +from .base import OhmeEntity _LOGGER = logging.getLogger(__name__) @@ -24,8 +25,10 @@ async def async_setup_entry( async_add_entities ): """Setup sensors and configure coordinator.""" - client = hass.data[DOMAIN][DATA_CLIENT] - coordinators = hass.data[DOMAIN][DATA_COORDINATORS] + account_id = config_entry.data['email'] + + client = hass.data[DOMAIN][account_id][DATA_CLIENT] + coordinators = hass.data[DOMAIN][account_id][DATA_COORDINATORS] coordinator = coordinators[COORDINATOR_CHARGESESSIONS] adv_coordinator = coordinators[COORDINATOR_ADVANCED] @@ -43,40 +46,13 @@ async def async_setup_entry( async_add_entities(sensors, update_before_add=True) -class PowerDrawSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class PowerDrawSensor(OhmeEntity, SensorEntity): """Sensor for car power draw.""" - _attr_name = "Power Draw" + _attr_translation_key = "power_draw" + _attr_icon = "mdi:ev-station" _attr_native_unit_of_measurement = UnitOfPower.WATT _attr_device_class = SensorDeviceClass.POWER - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - self._attributes = {} - self._last_updated = None - self._client = client - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_power_draw", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("power_draw") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:ev-station" - @property def native_value(self): """Get value from data returned from API by coordinator""" @@ -85,40 +61,13 @@ def native_value(self): return 0 -class CurrentDrawSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class CurrentDrawSensor(OhmeEntity, SensorEntity): """Sensor for car power draw.""" - _attr_name = "Current Draw" + _attr_translation_key = "current_draw" + _attr_icon = "mdi:current-ac" _attr_device_class = SensorDeviceClass.CURRENT _attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - self._attributes = {} - self._last_updated = None - self._client = client - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_current_draw", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("current_draw") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:current-ac" - @property def native_value(self): """Get value from data returned from API by coordinator""" @@ -127,40 +76,13 @@ def native_value(self): return 0 -class VoltageSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class VoltageSensor(OhmeEntity, SensorEntity): """Sensor for EVSE voltage.""" - _attr_name = "Voltage" + _attr_translation_key = "voltage" + _attr_icon = "mdi:sine-wave" _attr_device_class = SensorDeviceClass.VOLTAGE _attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - self._attributes = {} - self._last_updated = None - self._client = client - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_voltage", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("voltage") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:sine-wave" - @property def native_value(self): """Get value from data returned from API by coordinator""" @@ -169,49 +91,23 @@ def native_value(self): return None -class CTSensor(CoordinatorEntity[OhmeAdvancedSettingsCoordinator], SensorEntity): +class CTSensor(OhmeEntity, SensorEntity): """Sensor for car power draw.""" - _attr_name = "CT Reading" + _attr_translation_key = "ct_reading" + _attr_icon = "mdi:gauge" _attr_device_class = SensorDeviceClass.CURRENT _attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE - def __init__( - self, - coordinator: OhmeAdvancedSettingsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - self._attributes = {} - self._last_updated = None - self._client = client - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_ct_reading", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("ct_reading") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:gauge" - @property def native_value(self): """Get value from data returned from API by coordinator""" return self.coordinator.data['clampAmps'] -class EnergyUsageSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class EnergyUsageSensor(OhmeEntity, SensorEntity): """Sensor for total energy usage.""" - _attr_name = "Energy" + _attr_translation_key = "energy" + _attr_icon = "mdi:lightning-bolt-circle" _attr_has_entity_name = True _attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR _attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR @@ -219,23 +115,6 @@ class EnergyUsageSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], Sensor _attr_device_class = SensorDeviceClass.ENERGY _attr_state_class = SensorStateClass.TOTAL_INCREASING - def __init__( - self, - coordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - - self._attributes = {} - self._client = client - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_session_energy", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info() - @callback def _handle_coordinator_update(self) -> None: # Ensure we have data, then ensure value is going up and above 0 @@ -262,55 +141,17 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("session_energy") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:lightning-bolt-circle" - @property def native_value(self): return self._state -class NextSlotStartSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class NextSlotStartSensor(OhmeEntity, SensorEntity): """Sensor for next smart charge slot start time.""" - _attr_name = "Next Charge Slot Start" + _attr_translation_key = "next_slot_start" + _attr_icon = "mdi:clock-star-four-points" _attr_device_class = SensorDeviceClass.TIMESTAMP - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - self._attributes = {} - self._last_updated = None - self._client = client - self._hass = hass - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_next_slot", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("next_slot") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:clock-star-four-points" - @property def native_value(self): """Return pre-calculated state.""" @@ -322,47 +163,19 @@ 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._hass, self.coordinator.data)['start'] + self._state = next_slot(self._hass, self._client.email, self.coordinator.data)['start'] self._last_updated = utcnow() self.async_write_ha_state() -class NextSlotEndSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class NextSlotEndSensor(OhmeEntity, SensorEntity): """Sensor for next smart charge slot end time.""" - _attr_name = "Next Charge Slot End" + _attr_translation_key = "next_slot_end" + _attr_icon = "mdi:clock-star-four-points-outline" _attr_device_class = SensorDeviceClass.TIMESTAMP - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - 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) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("next_slot_end") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:clock-star-four-points-outline" - @property def native_value(self): """Return pre-calculated state.""" @@ -374,45 +187,17 @@ 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._hass, self.coordinator.data)['end'] + self._state = next_slot(self._hass, self._client.email, self.coordinator.data)['end'] self._last_updated = utcnow() self.async_write_ha_state() -class SlotListSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class SlotListSensor(OhmeEntity, SensorEntity): """Sensor for next smart charge slot end time.""" - _attr_name = "Charge Slots" - - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - self._slots = [] - self._last_updated = None - self._client = client - self._hass = hass - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_charge_slots", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("charge_slots") - - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:timetable" + _attr_translation_key = "charge_slots" + _attr_icon = "mdi:timetable" @property def native_value(self): @@ -428,44 +213,22 @@ def _handle_coordinator_update(self) -> None: slots = slot_list(self.coordinator.data) # Store slots for external use - self._hass.data[DOMAIN][DATA_SLOTS] = slots + self._hass.data[DOMAIN][self._client.email][DATA_SLOTS] = slots # Convert list to text - self._state = slot_list_str(self._hass, slots) + self._state = slot_list_str(self._hass, self._client.email, slots) self._last_updated = utcnow() self.async_write_ha_state() -class BatterySOCSensor(CoordinatorEntity[OhmeChargeSessionsCoordinator], SensorEntity): +class BatterySOCSensor(OhmeEntity, SensorEntity): """Sensor for car battery SOC.""" - _attr_name = "Battery SOC" + _attr_translation_key = "battery_soc" _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = SensorDeviceClass.BATTERY _attr_suggested_display_precision = 0 - def __init__( - self, - coordinator: OhmeChargeSessionsCoordinator, - hass: HomeAssistant, - client): - super().__init__(coordinator=coordinator) - - self._state = None - self._attributes = {} - self._last_updated = None - self._client = client - - self.entity_id = generate_entity_id( - "sensor.{}", "ohme_battery_soc", hass=hass) - - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info() - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._client.get_unique_id("battery_soc") - @property def icon(self): """Icon of the sensor. Round up to the nearest 10% icon.""" diff --git a/custom_components/ohme/switch.py b/custom_components/ohme/switch.py index 5c3fe7f..bb8057f 100644 --- a/custom_components/ohme/switch.py +++ b/custom_components/ohme/switch.py @@ -12,7 +12,7 @@ from homeassistant.util.dt import (utcnow) from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO -from .coordinator import OhmeChargeSessionsCoordinator, OhmeAccountInfoCoordinator +from .base import OhmeEntity _LOGGER = logging.getLogger(__name__) @@ -23,11 +23,13 @@ async def async_setup_entry( async_add_entities ): """Setup switches and configure coordinator.""" - coordinators = hass.data[DOMAIN][DATA_COORDINATORS] + account_id = config_entry.data['email'] + + coordinators = hass.data[DOMAIN][account_id][DATA_COORDINATORS] coordinator = coordinators[COORDINATOR_CHARGESESSIONS] accountinfo_coordinator = coordinators[COORDINATOR_ACCOUNTINFO] - client = hass.data[DOMAIN][DATA_CLIENT] + client = hass.data[DOMAIN][account_id][DATA_CLIENT] switches = [OhmePauseChargeSwitch(coordinator, hass, client), OhmeMaxChargeSwitch(coordinator, hass, client) @@ -45,49 +47,26 @@ async def async_setup_entry( if client.is_capable("buttonsLockable"): switches.append( OhmeConfigurationSwitch( - accountinfo_coordinator, hass, client, "Lock Buttons", "lock", "buttonsLocked") + accountinfo_coordinator, hass, client, "lock_buttons", "lock", "buttonsLocked") ) if client.is_capable("pluginsRequireApprovalMode"): switches.append( OhmeConfigurationSwitch(accountinfo_coordinator, hass, client, - "Require Approval", "check-decagram", "pluginsRequireApproval") + "require_approval", "check-decagram", "pluginsRequireApproval") ) if client.is_capable("stealth"): switches.append( OhmeConfigurationSwitch(accountinfo_coordinator, hass, client, - "Sleep When Inactive", "power-sleep", "stealthEnabled") + "sleep_when_inactive", "power-sleep", "stealthEnabled") ) async_add_entities(switches, update_before_add=True) -class OhmePauseChargeSwitch(CoordinatorEntity[OhmeChargeSessionsCoordinator], SwitchEntity): +class OhmePauseChargeSwitch(OhmeEntity, SwitchEntity): """Switch for pausing a charge.""" - _attr_name = "Pause Charge" - - def __init__(self, coordinator, hass: HomeAssistant, client): - super().__init__(coordinator=coordinator) - - self._client = client - - self._state = False - self._last_updated = None - self._attributes = {} - - self.entity_id = generate_entity_id( - "switch.{}", "ohme_pause_charge", 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("pause_charge") - - @property - def icon(self): - """Icon of the switch.""" - return "mdi:pause" + _attr_translation_key = "pause_charge" + _attr_icon = "mdi:pause" @callback def _handle_coordinator_update(self) -> None: @@ -118,33 +97,10 @@ async def async_turn_off(self): await self.coordinator.async_refresh() -class OhmeMaxChargeSwitch(CoordinatorEntity[OhmeChargeSessionsCoordinator], SwitchEntity): +class OhmeMaxChargeSwitch(OhmeEntity, SwitchEntity): """Switch for pausing a charge.""" - _attr_name = "Max Charge" - - def __init__(self, coordinator, hass: HomeAssistant, client): - super().__init__(coordinator=coordinator) - - self._client = client - - self._state = False - self._last_updated = None - self._attributes = {} - - self.entity_id = generate_entity_id( - "switch.{}", "ohme_max_charge", 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("max_charge") - - @property - def icon(self): - """Icon of the switch.""" - return "mdi:battery-arrow-up" + _attr_translation_key = "max_charge" + _attr_icon = "mdi:battery-arrow-up" @callback def _handle_coordinator_update(self) -> None: @@ -177,35 +133,16 @@ async def async_turn_off(self): await self.coordinator.async_refresh() -class OhmeConfigurationSwitch(CoordinatorEntity[OhmeAccountInfoCoordinator], SwitchEntity): +class OhmeConfigurationSwitch(OhmeEntity, SwitchEntity): """Switch for changing configuration options.""" - def __init__(self, coordinator, hass: HomeAssistant, client, name, icon, config_key): - super().__init__(coordinator=coordinator) - - self._client = client - - self._state = False - self._last_updated = None - self._attributes = {} - - self._icon = icon - self._attr_name = name + def __init__(self, coordinator, hass: HomeAssistant, client, translation_key, icon, config_key): + self._attr_icon = f"mdi:{icon}" + self._attr_translation_key = translation_key self._config_key = config_key - self.entity_id = generate_entity_id( - "switch.{}", "ohme_" + name.lower().replace(' ', '_'), 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(self._config_key) + self.legacy_id = config_key - @property - def icon(self): - """Icon of the switch.""" - return f"mdi:{self._icon}" + super().__init__(coordinator, hass, client) @callback def _handle_coordinator_update(self) -> None: @@ -235,33 +172,11 @@ async def async_turn_off(self): await self.coordinator.async_refresh() -class OhmeSolarBoostSwitch(CoordinatorEntity[OhmeAccountInfoCoordinator], SwitchEntity): +class OhmeSolarBoostSwitch(OhmeEntity, 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" + _attr_translation_key = "solar_mode" + _attr_icon = "mdi:solar-power" @callback def _handle_coordinator_update(self) -> None: @@ -291,29 +206,10 @@ async def async_turn_off(self): await self.coordinator.async_refresh() -class OhmePriceCapSwitch(CoordinatorEntity[OhmeAccountInfoCoordinator], SwitchEntity): +class OhmePriceCapSwitch(OhmeEntity, SwitchEntity): """Switch for enabling price cap.""" - _attr_name = "Enable Price Cap" - - def __init__(self, coordinator, hass: HomeAssistant, client): - super().__init__(coordinator=coordinator) - - self._client = client - - self.entity_id = generate_entity_id( - "switch.{}", "ohme_price_cap_enabled", 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("price_cap_enabled") - - @property - def icon(self): - """Icon of the switch.""" - return f"mdi:car-speed-limiter" + _attr_translation_key = "enable_price_cap" + _attr_icon = "mdi:car-speed-limiter" @callback def _handle_coordinator_update(self) -> None: diff --git a/custom_components/ohme/time.py b/custom_components/ohme/time.py index dee2dbb..7d4f306 100644 --- a/custom_components/ohme/time.py +++ b/custom_components/ohme/time.py @@ -7,6 +7,7 @@ from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES from .utils import session_in_progress from datetime import time as dt_time +from .base import OhmeEntity _LOGGER = logging.getLogger(__name__) @@ -17,9 +18,10 @@ async def async_setup_entry( async_add_entities ): """Setup switches and configure coordinator.""" - coordinators = hass.data[DOMAIN][DATA_COORDINATORS] + account_id = config_entry.data['email'] - client = hass.data[DOMAIN][DATA_CLIENT] + coordinators = hass.data[DOMAIN][account_id][DATA_COORDINATORS] + client = hass.data[DOMAIN][account_id][DATA_CLIENT] numbers = [TargetTime(coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client)] @@ -27,48 +29,30 @@ async def async_setup_entry( async_add_entities(numbers, update_before_add=True) -class TargetTime(TimeEntity): +class TargetTime(OhmeEntity, TimeEntity): """Target time sensor.""" - _attr_name = "Target Time" + _attr_translation_key = "target_time" + _attr_id = "target_time" + _attr_icon = "mdi:alarm-check" def __init__(self, coordinator, coordinator_schedules, hass: HomeAssistant, client): - self.coordinator = coordinator - self.coordinator_schedules = coordinator_schedules - - self._client = client - - self._state = None - self._last_updated = None - self._attributes = {} - - self.entity_id = generate_entity_id( - "number.{}", "ohme_target_time", hass=hass) + super().__init__(coordinator, hass, client) - self._attr_device_info = client.get_device_info() + self.coordinator_schedules = coordinator_schedules 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 - ) - ) self.async_on_remove( self.coordinator_schedules.async_add_listener( self._handle_coordinator_update, None ) ) - @property - def unique_id(self): - """The unique ID of the switch.""" - return self._client.get_unique_id("target_time") - async def async_set_value(self, value: dt_time) -> None: """Update the current value.""" # If session in progress, update this session, if not update the first schedule - if session_in_progress(self.hass, self.coordinator.data): + if session_in_progress(self.hass, self._client.email, self.coordinator.data): await self._client.async_apply_session_rule(target_time=(int(value.hour), int(value.minute))) await asyncio.sleep(1) await self.coordinator.async_refresh() @@ -77,17 +61,12 @@ async def async_set_value(self, value: dt_time) -> None: await asyncio.sleep(1) await self.coordinator_schedules.async_refresh() - @property - def icon(self): - """Icon of the sensor.""" - return "mdi:alarm-check" - @callback def _handle_coordinator_update(self) -> None: """Get value from data returned from API by coordinator""" # Read with the same logic as setting target = None - if session_in_progress(self.hass, self.coordinator.data): + if session_in_progress(self.hass, self._client.email, self.coordinator.data): target = self.coordinator.data['appliedRule']['targetTime'] elif self.coordinator_schedules.data: target = self.coordinator_schedules.data['targetTime'] diff --git a/custom_components/ohme/translations/en.json b/custom_components/ohme/translations/en.json index a3c8001..2da6fea 100644 --- a/custom_components/ohme/translations/en.json +++ b/custom_components/ohme/translations/en.json @@ -42,5 +42,97 @@ }, "abort": {} }, - "issues": {} -} \ No newline at end of file + "issues": {}, + "entity": { + "binary_sensor": { + "car_connected": { + "name": "Car Connected" + }, + "car_charging": { + "name": "Car Charging" + }, + "pending_approval": { + "name": "Pending Approval" + }, + "slot_active": { + "name": "Charge Slot Active" + }, + "charger_online": { + "name": "Charger Online" + } + }, + "button": { + "approve_charge": { + "name": "Approve Charge" + } + }, + "number": { + "target_percentage": { + "name": "Target Percentage" + }, + "preconditioning": { + "name": "Preconditioning" + }, + "price_cap": { + "name": "Price Cap" + } + }, + "sensor": { + "power_draw": { + "name": "Power Draw" + }, + "current_draw": { + "name": "Current Draw" + }, + "voltage": { + "name": "Voltage" + }, + "ct_reading": { + "name": "CT Reading" + }, + "energy": { + "name": "Energy" + }, + "next_slot_start": { + "name": "Next Charge Slot Start" + }, + "next_slot_end": { + "name": "Next Charge Slot End" + }, + "charge_slots": { + "name": "Charge Slots" + }, + "battery_soc": { + "name": "Battery SOC" + } + }, + "switch": { + "pause_charge": { + "name": "Pause Charge" + }, + "max_charge": { + "name": "Max Charge" + }, + "lock_buttons": { + "name": "Lock Buttons" + }, + "require_approval": { + "name": "Require Approval" + }, + "sleep_when_inactive": { + "name": "Sleep When Inactive" + }, + "solar_mode": { + "name": "Solar Boost" + }, + "enable_price_cap": { + "name": "Enable Price Cap" + } + }, + "time": { + "target_time": { + "name": "Target Time" + } + } + } +} diff --git a/custom_components/ohme/utils.py b/custom_components/ohme/utils.py index e5cfe41..2ac4d75 100644 --- a/custom_components/ohme/utils.py +++ b/custom_components/ohme/utils.py @@ -6,10 +6,10 @@ # _LOGGER = logging.getLogger(__name__) -def next_slot(hass, data): +def next_slot(hass, account_id, data): """Get the next charge slot start/end times.""" slots = slot_list(data) - collapse_slots = not get_option(hass, "never_collapse_slots", False) + collapse_slots = not get_option(hass, account_id, "never_collapse_slots", False) start = None end = None @@ -61,7 +61,7 @@ def slot_list(data): return slots -def slot_list_str(hass, slots): +def slot_list_str(hass, account_id, slots): """Convert slot list to string.""" # Convert list to tuples of times @@ -71,7 +71,7 @@ def slot_list_str(hass, slots): state = [] - if not get_option(hass, "never_collapse_slots", False): + if not get_option(hass, account_id, "never_collapse_slots", False): # Collapse slots so consecutive slots become one for i in range(len(t_slots)): if not state or state[-1][1] != t_slots[i][0]: @@ -111,11 +111,11 @@ def time_next_occurs(hour, minute): return target -def session_in_progress(hass, data): +def session_in_progress(hass, account_id, data): """Is there a session in progress? Used to check if we should update the current session rather than the first schedule.""" # If config option set, never update session specific schedule - if get_option(hass, "never_session_specific"): + if get_option(hass, account_id, "never_session_specific"): return False # Default to False with no data @@ -129,6 +129,6 @@ def session_in_progress(hass, data): return True -def get_option(hass, option, default=False): +def get_option(hass, account_id, option, default=False): """Return option value, with settable default.""" - return hass.data[DOMAIN][DATA_OPTIONS].get(option, default) + return hass.data[DOMAIN][account_id][DATA_OPTIONS].get(option, default)