diff --git a/custom_components/ohme/__init__.py b/custom_components/ohme/__init__.py index b23c57d..1bf6657 100644 --- a/custom_components/ohme/__init__.py +++ b/custom_components/ohme/__init__.py @@ -1,7 +1,7 @@ from homeassistant import core from .const import * from .client import OhmeApiClient -from .coordinator import OhmeUpdateCoordinator +from .coordinator import OhmeUpdateCoordinator, OhmeStatisticsUpdateCoordinator async def async_setup(hass: core.HomeAssistant, config: dict) -> bool: @@ -30,7 +30,8 @@ async def async_setup_entry(hass, entry): return False await async_setup_dependencies(hass, config) - + + # Create tasks for each entity type hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") ) @@ -44,4 +45,7 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][DATA_COORDINATOR] = OhmeUpdateCoordinator(hass=hass) await hass.data[DOMAIN][DATA_COORDINATOR].async_config_entry_first_refresh() + hass.data[DOMAIN][DATA_STATISTICS_COORDINATOR] = OhmeStatisticsUpdateCoordinator(hass=hass) + await hass.data[DOMAIN][DATA_STATISTICS_COORDINATOR].async_config_entry_first_refresh() + return True diff --git a/custom_components/ohme/client/__init__.py b/custom_components/ohme/client/__init__.py index 72afa97..313ec50 100644 --- a/custom_components/ohme/client/__init__.py +++ b/custom_components/ohme/client/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging import json +from datetime import datetime, timedelta from homeassistant.helpers.entity import DeviceInfo from ..const import DOMAIN @@ -20,6 +21,7 @@ def __init__(self, email, password): self._device_info = None self._token = None + self._user_id = "" self._serial = "" self._session = aiohttp.ClientSession() @@ -74,6 +76,21 @@ async def _put_request(self, url, data=None, is_retry=False): return True + async def _get_request(self, url, is_retry=False): + """Try to make a GET request + If we get a non 200 response, refresh auth token and try again""" + async with self._session.get( + url, + headers={"Authorization": "Firebase %s" % self._token} + ) as resp: + if resp.status != 200 and not is_retry: + await self.async_refresh_session() + return await self._get_request(url, is_retry=True) + elif resp.status != 200: + return False + + return await resp.json() + async def async_pause_charge(self): """Pause an ongoing charge""" result = await self._post_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/stop", skip_json=True) @@ -98,46 +115,53 @@ async def async_stop_max_charge(self): async def async_get_charge_sessions(self, is_retry=False): """Try to fetch charge sessions endpoint. If we get a non 200 response, refresh auth token and try again""" - async with self._session.get( - 'https://api.ohme.io/v1/chargeSessions', - headers={"Authorization": "Firebase %s" % self._token} - ) as resp: - - if resp.status != 200 and not is_retry: - await self.async_refresh_session() - return await self.async_get_charge_sessions(True) - elif resp.status != 200: - return False + resp = await self._get_request('https://api.ohme.io/v1/chargeSessions') + + if not resp: + return False - resp_json = await resp.json() - return resp_json[0] + return resp[0] async def async_update_device_info(self, is_retry=False): """Update _device_info with our charger model.""" - async with self._session.get( - 'https://api.ohme.io/v1/users/me/account', - headers={"Authorization": "Firebase %s" % self._token} - ) as resp: - - if resp.status != 200 and not is_retry: - await self.async_refresh_session() - return await self.async_get_device_info(True) - elif resp.status != 200: - return False - - resp_json = await resp.json() - device = resp_json['chargeDevices'][0] - - info = DeviceInfo( - identifiers={(DOMAIN, "ohme_charger")}, - name=device['modelTypeDisplayName'], - manufacturer="Ohme", - model=device['modelTypeDisplayName'].replace("Ohme ", ""), - sw_version=device['firmwareVersionLabel'], - serial_number=device['id'] - ) - self._serial = device['id'] - self._device_info = info + resp = await self._get_request('https://api.ohme.io/v1/users/me/account') + + if not resp: + return False + + device = resp['chargeDevices'][0] + + info = DeviceInfo( + identifiers={(DOMAIN, "ohme_charger")}, + name=device['modelTypeDisplayName'], + manufacturer="Ohme", + model=device['modelTypeDisplayName'].replace("Ohme ", ""), + sw_version=device['firmwareVersionLabel'], + serial_number=device['id'] + ) + + self._user_id = resp['user']['id'] + self._serial = device['id'] + self._device_info = info + + return True + + def _last_second_of_month_timestamp(self): + """Get the last second of this month.""" + dt = datetime.today() + dt = dt.replace(day=1) + timedelta(days=32) + dt = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - timedelta(seconds=1) + return int(dt.timestamp()*1e3) + + async def async_get_charge_statistics(self): + """Get charge statistics. Currently this is just for all time (well, Jan 2019).""" + end_ts = self._last_second_of_month_timestamp() + resp = await self._get_request(f"https://api.ohme.io/v1/chargeSessions/summary/users/{self._user_id}?&startTs=1546300800000&endTs={end_ts}&granularity=MONTH") + + if not resp: + return False + + return resp['totalStats'] def get_device_info(self): return self._device_info diff --git a/custom_components/ohme/const.py b/custom_components/ohme/const.py index 8ccb03b..f3bd275 100644 --- a/custom_components/ohme/const.py +++ b/custom_components/ohme/const.py @@ -3,3 +3,4 @@ DOMAIN = "ohme" DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator" +DATA_STATISTICS_COORDINATOR = "statistics_coordinator" diff --git a/custom_components/ohme/coordinator.py b/custom_components/ohme/coordinator.py index c578fc4..37a1720 100644 --- a/custom_components/ohme/coordinator.py +++ b/custom_components/ohme/coordinator.py @@ -31,3 +31,26 @@ async def _async_update_data(self): except BaseException: raise UpdateFailed("Error communicating with API") + + +class OhmeStatisticsUpdateCoordinator(DataUpdateCoordinator): + """Coordinator to update statistics from API periodically. + (But less so than OhmeUpdateCoordinator)""" + + def __init__(self, hass): + """Initialise coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Ohme Charger Statistics", + update_interval=timedelta(minutes=30), + ) + self._client = hass.data[DOMAIN][DATA_CLIENT] + + async def _async_update_data(self): + """Fetch data from API endpoint.""" + try: + return await self._client.async_get_charge_statistics() + + except BaseException: + raise UpdateFailed("Error communicating with API") diff --git a/custom_components/ohme/sensor.py b/custom_components/ohme/sensor.py index 39bc8e6..6ea78e0 100644 --- a/custom_components/ohme/sensor.py +++ b/custom_components/ohme/sensor.py @@ -6,11 +6,11 @@ SensorEntity ) from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.const import UnitOfPower +from homeassistant.const import UnitOfPower, UnitOfEnergy from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import generate_entity_id -from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATOR -from .coordinator import OhmeUpdateCoordinator +from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATOR, DATA_STATISTICS_COORDINATOR +from .coordinator import OhmeUpdateCoordinator, OhmeStatisticsUpdateCoordinator async def async_setup_entry( @@ -20,8 +20,9 @@ async def async_setup_entry( ): """Setup sensors and configure coordinator.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR] + stats_coordinator = hass.data[DOMAIN][DATA_STATISTICS_COORDINATOR] - sensors = [PowerDrawSensor(coordinator, hass)] + sensors = [PowerDrawSensor(coordinator, hass), EnergyUsageSensor(stats_coordinator, hass)] async_add_entities(sensors, update_before_add=True) @@ -45,8 +46,7 @@ def __init__( self.entity_id = generate_entity_id( "sensor.{}", "ohme_power_draw", hass=hass) - self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info( - ) + self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info() @property def unique_id(self) -> str: @@ -64,3 +64,44 @@ def native_value(self): if self.coordinator.data and self.coordinator.data['power']: return self.coordinator.data['power']['watt'] return 0 + + +class EnergyUsageSensor(CoordinatorEntity[OhmeStatisticsUpdateCoordinator], SensorEntity): + """Sensor for total energy usage.""" + _attr_name = "Ohme Accumulative Energy Usage" + _attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR + _attr_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_device_class = SensorDeviceClass.ENERGY + + def __init__( + self, + coordinator: OhmeUpdateCoordinator, + hass: HomeAssistant): + super().__init__(coordinator=coordinator) + + self._state = None + self._attributes = {} + self._last_updated = None + + self.entity_id = generate_entity_id( + "sensor.{}", "ohme_accumulative_energy", 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.entity_id + + @property + def icon(self): + """Icon of the sensor.""" + return "mdi:lightning-bolt" + + @property + def native_value(self): + """Get value from data returned from API by coordinator""" + if self.coordinator.data and self.coordinator.data['energyChargedTotalWh']: + return self.coordinator.data['energyChargedTotalWh'] + + return None