Skip to content

Commit

Permalink
Pull dev into main
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-r committed Dec 26, 2023
1 parent 4165550 commit cf726c4
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 160 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@

A basic integration for interacting with Ohme EV Chargers.

This is an unofficial integration. I have no affiliation with Ohme besides owning one of their EV chargers.

This has only be tested with an Ohme Home Pro and does not currently support social login or accounts with multiple chargers.

It's still very early in development but I plan to add more sensors and support for pausing/resuming charge.

## Installation

1. Install with HACS or manually
2. Enter your Ohme credentials
### HACS
This is the recommended installation method.
1. Add this repository to HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories)
2. Search for and install the Ohme addon from HACS
3. Restart Home Assistant

### Manual
1. Download the [latest release](https://github.com/dan-r/HomeAssistant-Ohme/releases)
2. Copy the contents of `custom_components` into the `<config directory>/custom_components` directory of your Home Assistant installation
3. Restart Home Assistant

## Setup
From the Home Assistant Integrations page, search for an add the Ohme integration. If you created your Ohme account through a social login, you will need to 'reset your password' to use this integration.
2 changes: 1 addition & 1 deletion custom_components/ohme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
"""Set up the Ohme EV Charger component."""
# @TODO: Add setup code.
return True


async def async_setup_dependencies(hass, config):
"""Instantiate client and refresh session"""
client = OhmeApiClient(config['email'], config['password'])
hass.data[DOMAIN][DATA_CLIENT] = client

Expand Down
19 changes: 12 additions & 7 deletions custom_components/ohme/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ async def async_setup_entry(
config_entry: config_entries.ConfigEntry,
async_add_entities,
):
"""Setup sensors and configure coordinator."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR]

sensors = [ConnectedSensor(coordinator, hass), ChargingSensor(coordinator, hass)]
sensors = [ConnectedSensor(coordinator, hass),
ChargingSensor(coordinator, hass)]

async_add_entities(sensors, update_before_add=True)


class ConnectedSensor(
CoordinatorEntity[OhmeUpdateCoordinator],
BinarySensorEntity):
"""Representation of a Sensor."""
"""Binary sensor for if car is plugged in."""

_attr_name = "Ohme Car Connected"
_attr_device_class = BinarySensorDeviceClass.PLUG
Expand All @@ -45,7 +47,8 @@ def __init__(
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()
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
)

@property
def icon(self):
Expand All @@ -60,16 +63,17 @@ def unique_id(self) -> str:
@property
def is_on(self) -> bool:
if self.coordinator.data is None:
self._state = False
self._state = False
else:
self._state = bool(self.coordinator.data["mode"] != "DISCONNECTED")
self._state = bool(self.coordinator.data["mode"] != "DISCONNECTED")

return self._state


class ChargingSensor(
CoordinatorEntity[OhmeUpdateCoordinator],
BinarySensorEntity):
"""Representation of a Sensor."""
"""Binary sensor for if car is charging."""

_attr_name = "Ohme Car Charging"
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
Expand All @@ -87,7 +91,8 @@ def __init__(
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()
self._attr_device_info = hass.data[DOMAIN][DATA_CLIENT].get_device_info(
)

@property
def icon(self):
Expand Down
125 changes: 64 additions & 61 deletions custom_components/ohme/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,76 +7,79 @@

_LOGGER = logging.getLogger(__name__)


class OhmeApiClient:
def __init__(self, email, password):
if email is None or password is None:
raise Exception("Credentials not provided")

self._email = email
self._password = password

self._device_info = None
self._token = None

async def async_refresh_session(self):
async with aiohttp.ClientSession() as session:
async with session.post(
"""API client for Ohme EV chargers."""

def __init__(self, email, password):
if email is None or password is None:
raise Exception("Credentials not provided")

self._email = email
self._password = password

self._device_info = None
self._token = None
self._session = aiohttp.ClientSession()

async def async_refresh_session(self):
"""Refresh the user auth token from the stored credentials."""
async with self._session.post(
'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyC8ZeZngm33tpOXLpbXeKfwtyZ1WrkbdBY',
data={"email": self._email, "password": self._password, "returnSecureToken": True}
) as resp:
data={"email": self._email, "password": self._password,
"returnSecureToken": True}
) as resp:

if resp.status != 200:
return None
if resp.status != 200:
return None

resp_json = await resp.json()
self._token = resp_json['idToken']
return True
resp_json = await resp.json()
self._token = resp_json['idToken']
return True

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 aiohttp.ClientSession() as session:
async with session.get(
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:
) as resp:

if resp.status != 200 and not is_retry:
await self.async_refresh_session()
return self.async_get_charge_sessions(True)
elif resp.status != 200:
return False
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_json = await resp.json()
return resp_json[0]
resp_json = await resp.json()
return resp_json[0]

async def async_update_device_info(self, is_retry=False):
async with aiohttp.ClientSession() as session:
async with session.get(
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 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'],
connections=set(),
manufacturer="Ohme",
model=device['modelTypeDisplayName'].replace("Ohme ", "")
)

self._device_info = info

def get_device_info(self):
return self._device_info


) 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._device_info = info

def get_device_info(self):
return self._device_info
2 changes: 1 addition & 1 deletion custom_components/ohme/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import (ConfigFlow, OptionsFlow)
from .const import DOMAIN
from .client import OhmeApiClient


class OhmeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow."""

Expand Down
16 changes: 4 additions & 12 deletions custom_components/ohme/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"""Example integration using DataUpdateCoordinator."""

from datetime import timedelta
import logging

from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
UpdateFailed
)

from .const import DOMAIN, DATA_CLIENT
Expand All @@ -14,26 +12,20 @@


class OhmeUpdateCoordinator(DataUpdateCoordinator):
"""My custom coordinator."""
"""Coordinator to pull from API periodically."""

def __init__(self, hass):
"""Initialize my coordinator."""
"""Initialise coordinator."""
super().__init__(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="Ohme Charger",
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=60),
)
self._client = hass.data[DOMAIN][DATA_CLIENT]

async def _async_update_data(self):
"""Fetch data from API endpoint.
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
"""Fetch data from API endpoint."""
try:
return await self._client.async_get_charge_sessions()

Expand Down
6 changes: 4 additions & 2 deletions custom_components/ohme/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"codeowners": ["@dan-r"],
"codeowners": [
"@dan-r"
],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/dan-r/HomeAssistant-Ohme",
Expand All @@ -8,4 +10,4 @@
"name": "Ohme",
"requirements": [],
"version": "1.0.0"
}
}
11 changes: 7 additions & 4 deletions custom_components/ohme/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ async def async_setup_entry(
config_entry: config_entries.ConfigEntry,
async_add_entities
):
"""Setup sensors and configure coordinator."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR]

sensors = [ExampleSensor(coordinator, hass)]
sensors = [PowerDrawSensor(coordinator, hass)]

async_add_entities(sensors, update_before_add=True)


class ExampleSensor(CoordinatorEntity[OhmeUpdateCoordinator], SensorEntity):
class PowerDrawSensor(CoordinatorEntity[OhmeUpdateCoordinator], SensorEntity):
"""Sensor for car power draw."""
_attr_name = "Ohme Power Draw"
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_device_class = SensorDeviceClass.POWER
Expand All @@ -43,7 +45,8 @@ 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:
Expand All @@ -55,9 +58,9 @@ 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"""
if self.coordinator.data and self.coordinator.data['power']:
return self.coordinator.data['power']['watt']
return 0
11 changes: 4 additions & 7 deletions custom_components/ohme/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
"error": {
"auth_error": "Invalid credentials provided."
},
"abort": {
}
"abort": {}
},
"options": {
"step": {
Expand All @@ -30,9 +29,7 @@
"error": {
"auth_error": "Invalid credentials provided."
},
"abort": {
}
"abort": {}
},
"issues": {
}
}
"issues": {}
}
2 changes: 1 addition & 1 deletion hacs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"name": "Ohme EV Charger",
"render_readme": true,
"iot_class": "cloud_polling"
}
}
Loading

0 comments on commit cf726c4

Please sign in to comment.