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

Commit b2e273b

Browse files
authored
Initial implementation of switches for pause/max charge (#1)
* Add pause charge switch * Add max charge switch * Change update interval
1 parent cf726c4 commit b2e273b

File tree

4 files changed

+209
-2
lines changed

4 files changed

+209
-2
lines changed

custom_components/ohme/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ async def async_setup_entry(hass, entry):
3737
hass.async_create_task(
3838
hass.config_entries.async_forward_entry_setup(entry, "binary_sensor")
3939
)
40+
hass.async_create_task(
41+
hass.config_entries.async_forward_entry_setup(entry, "switch")
42+
)
4043

4144
hass.data[DOMAIN][DATA_COORDINATOR] = OhmeUpdateCoordinator(hass=hass)
4245
await hass.data[DOMAIN][DATA_COORDINATOR].async_config_entry_first_refresh()

custom_components/ohme/client/__init__.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def __init__(self, email, password):
2020

2121
self._device_info = None
2222
self._token = None
23+
self._serial = ""
2324
self._session = aiohttp.ClientSession()
2425

2526
async def async_refresh_session(self):
@@ -37,6 +38,63 @@ async def async_refresh_session(self):
3738
self._token = resp_json['idToken']
3839
return True
3940

41+
async def _post_request(self, url, skip_json=False, data=None, is_retry=False):
42+
"""Try to make a POST request
43+
If we get a non 200 response, refresh auth token and try again"""
44+
async with self._session.post(
45+
url,
46+
data=data,
47+
headers={"Authorization": "Firebase %s" % self._token}
48+
) as resp:
49+
if resp.status != 200 and not is_retry:
50+
await self.async_refresh_session()
51+
return await self._post_request(url, skip_json=skip_json, data=data, is_retry=True)
52+
elif resp.status != 200:
53+
return False
54+
55+
if skip_json:
56+
return await resp.text()
57+
58+
resp_json = await resp.json()
59+
return resp_json
60+
61+
async def _put_request(self, url, data=None, is_retry=False):
62+
"""Try to make a PUT request
63+
If we get a non 200 response, refresh auth token and try again"""
64+
async with self._session.put(
65+
url,
66+
data=data,
67+
headers={"Authorization": "Firebase %s" % self._token}
68+
) as resp:
69+
if resp.status != 200 and not is_retry:
70+
await self.async_refresh_session()
71+
return await self._put_request(url, data=data, is_retry=True)
72+
elif resp.status != 200:
73+
return False
74+
75+
return True
76+
77+
async def async_pause_charge(self):
78+
"""Pause an ongoing charge"""
79+
result = await self._post_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/stop", skip_json=True)
80+
return bool(result)
81+
82+
async def async_resume_charge(self):
83+
"""Resume a paused charge"""
84+
result = await self._post_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/resume", skip_json=True)
85+
return bool(result)
86+
87+
async def async_max_charge(self):
88+
"""Enable max charge"""
89+
result = await self._put_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/rule?maxCharge=true")
90+
return bool(result)
91+
92+
async def async_stop_max_charge(self):
93+
"""Stop max charge.
94+
This is more complicated than starting one as we need to give more parameters."""
95+
result = await self._put_request(f"https://api.ohme.io/v1/chargeSessions/{self._serial}/rule?enableMaxPrice=false&toPercent=80.0&inSeconds=43200")
96+
return bool(result)
97+
4098
async def async_get_charge_sessions(self, is_retry=False):
4199
"""Try to fetch charge sessions endpoint.
42100
If we get a non 200 response, refresh auth token and try again"""
@@ -78,7 +136,7 @@ async def async_update_device_info(self, is_retry=False):
78136
sw_version=device['firmwareVersionLabel'],
79137
serial_number=device['id']
80138
)
81-
139+
self._serial = device['id']
82140
self._device_info = info
83141

84142
def get_device_info(self):

custom_components/ohme/coordinator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def __init__(self, hass):
2020
hass,
2121
_LOGGER,
2222
name="Ohme Charger",
23-
update_interval=timedelta(seconds=60),
23+
update_interval=timedelta(seconds=30),
2424
)
2525
self._client = hass.data[DOMAIN][DATA_CLIENT]
2626

custom_components/ohme/switch.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from __future__ import annotations
2+
import logging
3+
4+
from homeassistant.core import callback, HomeAssistant
5+
from homeassistant.helpers.entity import generate_entity_id
6+
7+
from homeassistant.helpers.update_coordinator import (
8+
CoordinatorEntity
9+
)
10+
from homeassistant.components.switch import SwitchEntity
11+
from homeassistant.util.dt import (utcnow)
12+
13+
from .const import DOMAIN, DATA_CLIENT, DATA_COORDINATOR
14+
from .coordinator import OhmeUpdateCoordinator
15+
16+
_LOGGER = logging.getLogger(__name__)
17+
18+
19+
async def async_setup_entry(
20+
hass: HomeAssistant,
21+
config_entry: config_entries.ConfigEntry,
22+
async_add_entities
23+
):
24+
"""Setup switches and configure coordinator."""
25+
coordinator = hass.data[DOMAIN][DATA_COORDINATOR]
26+
client = hass.data[DOMAIN][DATA_CLIENT]
27+
28+
buttons = [OhmePauseCharge(coordinator, hass, client),
29+
OhmeMaxCharge(coordinator, hass, client)]
30+
31+
async_add_entities(buttons, update_before_add=True)
32+
33+
34+
class OhmePauseCharge(CoordinatorEntity[OhmeUpdateCoordinator], SwitchEntity):
35+
"""Switch for pausing a charge."""
36+
_attr_name = "Ohme Pause Charge"
37+
38+
def __init__(self, coordinator, hass: HomeAssistant, client):
39+
super().__init__(coordinator=coordinator)
40+
41+
self._client = client
42+
43+
self._state = False
44+
self._last_updated = None
45+
self._attributes = {}
46+
47+
self.entity_id = generate_entity_id(
48+
"switch.{}", "ohme_pause_charge", hass=hass)
49+
50+
self._attr_device_info = client.get_device_info()
51+
52+
@property
53+
def unique_id(self):
54+
"""The unique ID of the switch."""
55+
return self.entity_id
56+
57+
@property
58+
def icon(self):
59+
"""Icon of the switch."""
60+
return "mdi:pause"
61+
62+
@callback
63+
def _handle_coordinator_update(self) -> None:
64+
"""Determine if charge is paused.
65+
We handle this differently to the sensors as the state of this switch
66+
is changed 'optimistically' to stop the switch flicking back then forth."""
67+
if self.coordinator.data is None:
68+
self._attr_is_on = False
69+
else:
70+
self._attr_is_on = bool(self.coordinator.data["mode"] == "STOPPED")
71+
72+
self.async_write_ha_state()
73+
74+
async def async_turn_on(self):
75+
"""Turn on the switch."""
76+
await self._client.async_pause_charge()
77+
78+
self._attr_is_on = True
79+
self._last_updated = utcnow()
80+
self.async_write_ha_state()
81+
82+
await self.coordinator.async_refresh()
83+
84+
async def async_turn_off(self):
85+
"""Turn off the switch."""
86+
await self._client.async_resume_charge()
87+
88+
self._attr_is_on = False
89+
self._last_updated = utcnow()
90+
self.async_write_ha_state()
91+
92+
await self.coordinator.async_refresh()
93+
94+
class OhmeMaxCharge(CoordinatorEntity[OhmeUpdateCoordinator], SwitchEntity):
95+
"""Switch for pausing a charge."""
96+
_attr_name = "Ohme Max Charge"
97+
98+
def __init__(self, coordinator, hass: HomeAssistant, client):
99+
super().__init__(coordinator=coordinator)
100+
101+
self._client = client
102+
103+
self._state = False
104+
self._last_updated = None
105+
self._attributes = {}
106+
107+
self.entity_id = generate_entity_id(
108+
"switch.{}", "ohme_max_charge", hass=hass)
109+
110+
self._attr_device_info = client.get_device_info()
111+
112+
@property
113+
def unique_id(self):
114+
"""The unique ID of the switch."""
115+
return self.entity_id
116+
117+
@property
118+
def icon(self):
119+
"""Icon of the switch."""
120+
return "mdi:battery-arrow-up"
121+
122+
@callback
123+
def _handle_coordinator_update(self) -> None:
124+
"""Determine if we are max charging."""
125+
if self.coordinator.data is None:
126+
self._attr_is_on = False
127+
else:
128+
self._attr_is_on = bool(self.coordinator.data["mode"] == "MAX_CHARGE")
129+
130+
self.async_write_ha_state()
131+
132+
async def async_turn_on(self):
133+
"""Turn on the switch."""
134+
await self._client.async_max_charge()
135+
136+
self._attr_is_on = True
137+
self._last_updated = utcnow()
138+
self.async_write_ha_state()
139+
140+
async def async_turn_off(self):
141+
"""Turn off the switch."""
142+
await self._client.async_stop_max_charge()
143+
144+
self._attr_is_on = False
145+
self._last_updated = utcnow()
146+
self.async_write_ha_state()

0 commit comments

Comments
 (0)