11import aiohttp
22import logging
33import json
4+ from time import time
45from datetime import datetime , timedelta
56from homeassistant .helpers .entity import DeviceInfo
67from .const import DOMAIN
78
89_LOGGER = logging .getLogger (__name__ )
910
11+ GOOGLE_API_KEY = "AIzaSyC8ZeZngm33tpOXLpbXeKfwtyZ1WrkbdBY"
12+
1013
1114class OhmeApiClient :
1215 """API client for Ohme EV chargers."""
@@ -20,136 +23,185 @@ def __init__(self, email, password):
2023
2124 self ._device_info = None
2225 self ._capabilities = {}
26+ self ._token_birth = 0
2327 self ._token = None
28+ self ._refresh_token = None
2429 self ._user_id = ""
2530 self ._serial = ""
26- self ._session = aiohttp .ClientSession ()
31+ self ._session = aiohttp .ClientSession (
32+ base_url = "https://api.ohme.io" )
33+ self ._auth_session = aiohttp .ClientSession ()
2734
28- async def async_refresh_session (self ):
35+
36+ # Auth methods
37+ async def async_create_session (self ):
2938 """Refresh the user auth token from the stored credentials."""
30- async with self ._session .post (
31- ' https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyC8ZeZngm33tpOXLpbXeKfwtyZ1WrkbdBY' ,
39+ async with self ._auth_session .post (
40+ f" https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={ GOOGLE_API_KEY } " ,
3241 data = {"email" : self ._email , "password" : self ._password ,
3342 "returnSecureToken" : True }
3443 ) as resp :
35-
3644 if resp .status != 200 :
3745 return None
3846
3947 resp_json = await resp .json ()
48+ self ._token_birth = time ()
4049 self ._token = resp_json ['idToken' ]
50+ self ._refresh_token = resp_json ['refreshToken' ]
4151 return True
4252
43- async def _post_request (self , url , skip_json = False , data = None , is_retry = False ):
44- """Try to make a POST request
45- If we get a non 200 response, refresh auth token and try again"""
53+ async def async_refresh_session (self ):
54+ """Refresh auth token if needed."""
55+ if self ._token is None :
56+ return await self .async_create_session ()
57+
58+ # Don't refresh token unless its over 45 mins old
59+ if time () - self ._token_birth < 2700 :
60+ return
61+
62+ async with self ._auth_session .post (
63+ f"https://securetoken.googleapis.com/v1/token?key={ GOOGLE_API_KEY } " ,
64+ data = {"grantType" : "refresh_token" ,
65+ "refreshToken" : self ._refresh_token }
66+ ) as resp :
67+ if resp .status != 200 :
68+ text = await resp .text ()
69+ msg = f"Ohme auth refresh error: { text } "
70+ _LOGGER .error (msg )
71+ raise AuthException (msg )
72+
73+ resp_json = await resp .json ()
74+ self ._token_birth = time ()
75+ self ._token = resp_json ['id_token' ]
76+ self ._refresh_token = resp_json ['refresh_token' ]
77+ return True
78+
79+
80+ # Internal methods
81+ def _last_second_of_month_timestamp (self ):
82+ """Get the last second of this month."""
83+ dt = datetime .today ()
84+ dt = dt .replace (day = 1 ) + timedelta (days = 32 )
85+ dt = dt .replace (day = 1 , hour = 0 , minute = 0 , second = 0 ,
86+ microsecond = 0 ) - timedelta (seconds = 1 )
87+ return int (dt .timestamp ()* 1e3 )
88+
89+ async def _handle_api_error (self , url , resp ):
90+ """Raise an exception if API response failed."""
91+ if resp .status != 200 :
92+ text = await resp .text ()
93+ msg = f"Ohme API response error: { url } , { resp .status } ; { text } "
94+ _LOGGER .error (msg )
95+ raise ApiException (msg )
96+
97+ def _get_headers (self ):
98+ """Get auth and content-type headers"""
99+ return {
100+ "Authorization" : "Firebase %s" % self ._token ,
101+ "Content-Type" : "application/json"
102+ }
103+
104+ async def _post_request (self , url , skip_json = False , data = None ):
105+ """Make a POST request."""
106+ await self .async_refresh_session ()
46107 async with self ._session .post (
47108 url ,
48109 data = data ,
49- headers = { "Authorization" : "Firebase %s" % self ._token }
110+ headers = self ._get_headers ()
50111 ) as resp :
51- if resp .status != 200 and not is_retry :
52- await self .async_refresh_session ()
53- return await self ._post_request (url , skip_json = skip_json , data = data , is_retry = True )
54- elif resp .status != 200 :
55- return False
112+ await self ._handle_api_error (url , resp )
56113
57114 if skip_json :
58115 return await resp .text ()
59116
60- resp_json = await resp .json ()
61- return resp_json
117+ return await resp .json ()
62118
63- async def _put_request (self , url , data = None , is_retry = False ):
64- """Try to make a PUT request
65- If we get a non 200 response, refresh auth token and try again"""
119+ async def _put_request (self , url , data = None ):
120+ """Make a PUT request."""
121+ await self . async_refresh_session ()
66122 async with self ._session .put (
67123 url ,
68124 data = json .dumps (data ),
69- headers = {
70- "Authorization" : "Firebase %s" % self ._token ,
71- "Content-Type" : "application/json"
72- }
125+ headers = self ._get_headers ()
73126 ) as resp :
74- if resp .status != 200 and not is_retry :
75- await self .async_refresh_session ()
76- return await self ._put_request (url , data = data , is_retry = True )
77- elif resp .status != 200 :
78- return False
127+ await self ._handle_api_error (url , resp )
79128
80129 return True
81130
82- async def _get_request (self , url , is_retry = False ):
83- """Try to make a GET request
84- If we get a non 200 response, refresh auth token and try again"""
131+ async def _get_request (self , url ):
132+ """Make a GET request."""
133+ await self . async_refresh_session ()
85134 async with self ._session .get (
86135 url ,
87- headers = { "Authorization" : "Firebase %s" % self ._token }
136+ headers = self ._get_headers ()
88137 ) as resp :
89- if resp .status != 200 and not is_retry :
90- await self .async_refresh_session ()
91- return await self ._get_request (url , is_retry = True )
92- elif resp .status != 200 :
93- return False
138+ await self ._handle_api_error (url , resp )
94139
95140 return await resp .json ()
96141
142+
143+ # Simple getters
144+ def is_capable (self , capability ):
145+ """Return whether or not this model has a given capability."""
146+ return bool (self ._capabilities [capability ])
147+
148+ def get_device_info (self ):
149+ return self ._device_info
150+
151+ def get_unique_id (self , name ):
152+ return f"ohme_{ self ._serial } _{ name } "
153+
154+
155+ # Push methods
97156 async def async_pause_charge (self ):
98157 """Pause an ongoing charge"""
99- result = await self ._post_request (f"https://api.ohme.io /v1/chargeSessions/{ self ._serial } /stop" , skip_json = True )
158+ result = await self ._post_request (f"/v1/chargeSessions/{ self ._serial } /stop" , skip_json = True )
100159 return bool (result )
101160
102161 async def async_resume_charge (self ):
103162 """Resume a paused charge"""
104- result = await self ._post_request (f"https://api.ohme.io /v1/chargeSessions/{ self ._serial } /resume" , skip_json = True )
163+ result = await self ._post_request (f"/v1/chargeSessions/{ self ._serial } /resume" , skip_json = True )
105164 return bool (result )
106165
107166 async def async_approve_charge (self ):
108167 """Approve a charge"""
109- result = await self ._put_request (f"https://api.ohme.io /v1/chargeSessions/{ self ._serial } /approve?approve=true" )
168+ result = await self ._put_request (f"/v1/chargeSessions/{ self ._serial } /approve?approve=true" )
110169 return bool (result )
111170
112171 async def async_max_charge (self ):
113172 """Enable max charge"""
114- result = await self ._put_request (f"https://api.ohme.io /v1/chargeSessions/{ self ._serial } /rule?maxCharge=true" )
173+ result = await self ._put_request (f"/v1/chargeSessions/{ self ._serial } /rule?maxCharge=true" )
115174 return bool (result )
116175
117176 async def async_stop_max_charge (self ):
118177 """Stop max charge.
119178 This is more complicated than starting one as we need to give more parameters."""
120- result = await self ._put_request (f"https://api.ohme.io /v1/chargeSessions/{ self ._serial } /rule?enableMaxPrice=false&toPercent=80.0&inSeconds=43200" )
179+ result = await self ._put_request (f"/v1/chargeSessions/{ self ._serial } /rule?enableMaxPrice=false&toPercent=80.0&inSeconds=43200" )
121180 return bool (result )
122181
123182 async def async_set_configuration_value (self , values ):
124183 """Set a configuration value or values."""
125- result = await self ._put_request (f"https://api.ohme.io /v1/chargeDevices/{ self ._serial } /appSettings" , data = values )
184+ result = await self ._put_request (f"/v1/chargeDevices/{ self ._serial } /appSettings" , data = values )
126185 return bool (result )
127186
187+
188+ # Pull methods
128189 async def async_get_charge_sessions (self , is_retry = False ):
129190 """Try to fetch charge sessions endpoint.
130191 If we get a non 200 response, refresh auth token and try again"""
131- resp = await self ._get_request ('https://api.ohme.io/v1/chargeSessions' )
132-
133- if not resp :
134- return False
192+ resp = await self ._get_request ('/v1/chargeSessions' )
135193
136194 return resp [0 ]
137195
138196 async def async_get_account_info (self ):
139- resp = await self ._get_request ('https://api.ohme.io/v1/users/me/account' )
140-
141- if not resp :
142- return False
197+ resp = await self ._get_request ('/v1/users/me/account' )
143198
144199 return resp
145200
146201 async def async_update_device_info (self , is_retry = False ):
147202 """Update _device_info with our charger model."""
148203 resp = await self .async_get_account_info ()
149204
150- if not resp :
151- return False
152-
153205 device = resp ['chargeDevices' ][0 ]
154206
155207 info = DeviceInfo (
@@ -168,30 +220,24 @@ async def async_update_device_info(self, is_retry=False):
168220
169221 return True
170222
171- def is_capable (self , capability ):
172- """Return whether or not this model has a given capability."""
173- return bool (self ._capabilities [capability ])
174-
175- def _last_second_of_month_timestamp (self ):
176- """Get the last second of this month."""
177- dt = datetime .today ()
178- dt = dt .replace (day = 1 ) + timedelta (days = 32 )
179- dt = dt .replace (day = 1 , hour = 0 , minute = 0 , second = 0 ,
180- microsecond = 0 ) - timedelta (seconds = 1 )
181- return int (dt .timestamp ()* 1e3 )
182-
183223 async def async_get_charge_statistics (self ):
184224 """Get charge statistics. Currently this is just for all time (well, Jan 2019)."""
185225 end_ts = self ._last_second_of_month_timestamp ()
186- resp = await self ._get_request (f"https://api.ohme.io/v1/chargeSessions/summary/users/{ self ._user_id } ?&startTs=1546300800000&endTs={ end_ts } &granularity=MONTH" )
187-
188- if not resp :
189- return False
226+ resp = await self ._get_request (f"/v1/chargeSessions/summary/users/{ self ._user_id } ?&startTs=1546300800000&endTs={ end_ts } &granularity=MONTH" )
190227
191228 return resp ['totalStats' ]
192229
193- def get_device_info (self ):
194- return self ._device_info
230+ async def async_get_ct_reading (self ):
231+ """Get CT clamp reading."""
232+ resp = await self ._get_request (f"/v1/chargeDevices/{ self ._serial } /advancedSettings" )
195233
196- def get_unique_id (self , name ):
197- return f"ohme_{ self ._serial } _{ name } "
234+ return resp ['clampAmps' ]
235+
236+
237+
238+ # Exceptions
239+ class ApiException (Exception ):
240+ ...
241+
242+ class AuthException (ApiException ):
243+ ...
0 commit comments