Skip to content

Commit 32f2c35

Browse files
committed
Start migration to new API endpoints. EVSE mostly supported at this time.
1 parent 9aa85c5 commit 32f2c35

4 files changed

Lines changed: 343 additions & 89 deletions

File tree

pyemvue/auth.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ def __init__(
2525
max_retry_attempts: int = 5,
2626
initial_retry_delay: float = 0.5,
2727
max_retry_delay: float = 30.0,
28+
as_bearer: bool = False,
2829
):
2930
self.host = host
3031
self.connect_timeout = connect_timeout
3132
self.read_timeout = read_timeout
3233
self.token_updater = token_updater
34+
self.as_bearer = as_bearer
3335
self.max_retry_attempts = max(max_retry_attempts, 1)
3436
self.initial_retry_delay = max(initial_retry_delay, 0.5)
3537
self.max_retry_delay = max(max_retry_delay, 0)
@@ -132,7 +134,12 @@ def _do_request(self, method: str, path: str, **kwargs) -> requests.Response:
132134
headers = {}
133135
else:
134136
headers = dict(headers)
135-
headers["authtoken"] = self.tokens["id_token"]
137+
138+
if self.as_bearer:
139+
# The newer API uses standard Bearer token authorization
140+
headers["Authorization"] = f"Bearer {self.tokens['id_token']}"
141+
else:
142+
headers["authtoken"] = self.tokens["id_token"]
136143

137144
return requests.request(
138145
method,

pyemvue/device.py

Lines changed: 180 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
from typing import Any, Optional
33
from typing_extensions import Self
44
from dateutil.parser import parse
5+
from dateutil import tz
56

67

78
class VueDevice(object):
89
def __init__(self, gid=0, manId="", modelNum="", firmwareVersion=""):
10+
self.device_id = ""
911
self.device_gid: int = gid
1012
self.manufacturer_id = manId
1113
self.model = modelNum
@@ -42,6 +44,7 @@ def __init__(self, gid=0, manId="", modelNum="", firmwareVersion=""):
4244

4345
def from_json_dictionary(self, js: "dict[str, Any]") -> Self:
4446
"""Populate device data from a dictionary extracted from the response json."""
47+
self.device_id = js.get("device_id", "")
4548
if "deviceGid" in js:
4649
self.device_gid = js["deviceGid"]
4750
if "manufacturerDeviceId" in js:
@@ -247,92 +250,208 @@ def from_json_dictionary(self, js: "dict[str, Any]") -> Self:
247250

248251
class OutletDevice(object):
249252
def __init__(self, gid: int = 0, on: bool = False):
253+
self.device_id = ""
250254
self.device_gid = gid
251255
self.outlet_on = on
252256
self.load_gid: int = 0
253-
self.schedules = []
254257

255258
def from_json_dictionary(self, js: "dict[str, Any]") -> Self:
256-
if "deviceGid" in js:
257-
self.device_gid = js["deviceGid"]
258-
if "outletOn" in js:
259-
self.outlet_on = js["outletOn"]
260-
if "loadGid" in js:
261-
self.load_gid = js["loadGid"]
262-
# don't have support for schedules yet
259+
self.device_id = js.get("device_id", "")
260+
self.device_gid = js.get("device_gid", 0)
261+
self.load_gid = js.get("load_gid", 0)
262+
self.outlet_on = js.get("outlet_on", False)
263263
return self
264264

265265
def as_dictionary(self) -> "dict[str, Any]":
266266
return {
267-
"deviceGid": self.device_gid,
268-
"outletOn": self.outlet_on,
269-
"loadGid": self.load_gid,
267+
"device_id": self.device_id,
268+
"device_gid": self.device_gid,
269+
"load_gid": self.load_gid,
270+
"outlet_on": self.outlet_on,
270271
}
271272

273+
class EvseStatus(object):
274+
"""{
275+
"device_id": "D2129A0700AC67B2FBBD9C",
276+
"device_gid": 62626,
277+
"load_gid": 42275,
278+
"charger_status": "DISCONNECTED_ON"
279+
}"""
280+
281+
def __init__(self, id: str = "", gid: int = 0, load_gid: int = 0, status: str = ""):
282+
self.device_id = id
283+
self.device_gid = gid
284+
self.load_gid = load_gid
285+
self.charger_status = status
286+
287+
def from_json_dictionary(self, js: "dict[str, Any]") -> Self:
288+
self.device_id = js.get("device_id", "")
289+
self.device_gid = js.get("device_gid", 0)
290+
self.load_gid = js.get("load_gid", 0)
291+
self.charger_status = js.get("charger_status", "")
292+
return self
272293

273294
class ChargerDevice(object):
274-
def __init__(self, gid: int = 0, on: bool = False):
295+
"""{
296+
"device_id": "D2129A0700AC67B2FBBD9C",
297+
"category": "EVSE",
298+
"name": "EV",
299+
"device_description": null,
300+
"connected": true,
301+
"connected_changed_time": "2025-12-25T03:52:27.629Z",
302+
"time_zone": "America/New_York",
303+
"billing_cycle_start_day": 16,
304+
"firmware": "EVCharger-599",
305+
"latitude": 43.1910367,
306+
"longitude": -77.8089975,
307+
"excess_solar_configured": false,
308+
"peak_demand_configured": false,
309+
"energy_management_active": "NONE",
310+
"energy_managements_configured": [],
311+
"energy_managements_active": [],
312+
"evse_product": "C1",
313+
"evse_model": "Classic",
314+
"evse_branding": "EMPORIA",
315+
"evse_power_source": "NEMA",
316+
"evse_color": "WHITE",
317+
"evse_gun_type": "J1772",
318+
"breaker_pin": "1313",
319+
"charger_on": true,
320+
"charge_rate_amps": 24,
321+
"max_charge_rate_amps": 24,
322+
"power_smart_configuration": null,
323+
"vehicle_connected": false,
324+
"vehicle_charging": false,
325+
"partner_schedule_status": "INELIGIBLE"
326+
}"""
327+
328+
def __init__(self, id: str = "", gid: int = 0, on: bool = False):
329+
self.device_id = id
275330
self.device_gid = gid
276331
self.charger_on = on
277-
self.message = ""
278-
self.status = ""
279-
self.icon = ""
280-
self.icon_label = ""
281-
self.icon_detail_text = ""
282-
self.fault_text = ""
332+
self.category = ""
333+
self.name = ""
334+
self.device_description = ""
335+
self.connected = False
336+
self.connected_changed_time: Optional[datetime.datetime] = None
337+
self.time_zone = datetime.timezone.utc
338+
self.time_zone_string = datetime.timezone.utc.tzname(None)
339+
self.billing_cycle_start_day = 0
340+
self.firmware = ""
341+
self.latitude = 0.0
342+
self.longitude = 0.0
343+
self.excess_solar_configured = False
344+
self.peak_demand_configured = False
345+
self.energy_management_active = "NONE"
346+
self.energy_managements_configured: list[str] = []
347+
self.energy_managements_active: list[str] = []
348+
self.evse_product = ""
349+
self.evse_model = ""
350+
self.evse_branding = ""
351+
self.evse_power_source = ""
352+
self.evse_color = ""
353+
self.evse_gun_type = ""
354+
self.breaker_pin = ""
283355
self.charging_rate = 0
284356
self.max_charging_rate = 0
285-
self.off_peak_schedules_enabled = False
286-
self.custom_schedules = []
287-
self.load_gid: int = 0
288-
self.debug_code = ""
289-
self.pro_control_code = ""
290-
self.breaker_pin = ""
357+
self.power_smart_configuration: Optional[dict[str, Any]] = None
358+
self.vehicle_connected = False
359+
self.vehicle_charging = False
360+
self.partner_schedule_status = ""
291361

292362
def from_json_dictionary(self, js: "dict[str, Any]") -> Self:
293-
if "deviceGid" in js:
294-
self.device_gid = js["deviceGid"]
295-
if "loadGid" in js:
296-
self.load_gid = js["loadGid"]
297-
if "chargerOn" in js:
298-
self.charger_on = js["chargerOn"]
299-
if "message" in js:
300-
self.message = js["message"]
301-
if "status" in js:
302-
self.status = js["status"]
303-
if "icon" in js:
304-
self.icon = js["icon"]
305-
if "iconLabel" in js:
306-
self.icon_label = js["iconLabel"]
307-
if "iconDetailText" in js:
308-
self.icon_detail_text = js["iconDetailText"]
309-
if "faultText" in js:
310-
self.fault_text = js["faultText"]
311-
if "chargingRate" in js:
312-
self.charging_rate = js["chargingRate"]
313-
if "maxChargingRate" in js:
314-
self.max_charging_rate = js["maxChargingRate"]
315-
if "offPeakSchedulesEnabled" in js:
316-
self.off_peak_schedules_enabled = js["offPeakSchedulesEnabled"]
317-
if "debugCode" in js:
318-
self.debug_code = js["debugCode"]
319-
if "proControlCode" in js:
320-
self.pro_control_code = js["proControlCode"]
321-
if "breakerPIN" in js:
322-
self.breaker_pin = js["breakerPIN"]
323-
# don't have support for schedules yet
363+
self.device_id = js.get("device_id", self.device_id)
364+
self.device_gid = js.get("device_gid", self.device_gid)
365+
self.category = js.get("category", self.category)
366+
self.name = js.get("name", self.name)
367+
self.device_description = js.get("device_description", self.device_description)
368+
self.connected = js.get("connected", self.connected)
369+
if "connected_changed_time" in js and js["connected_changed_time"]:
370+
try:
371+
self.connected_changed_time = parse(js["connected_changed_time"])
372+
except:
373+
self.connected_changed_time = None
374+
timezone_string = js.get("time_zone", None)
375+
if timezone_string: # convert timezone string to a tzinfo object
376+
self.time_zone_string = timezone_string
377+
self.time_zone = tz.gettz(timezone_string)
378+
379+
self.billing_cycle_start_day = js.get(
380+
"billing_cycle_start_day", self.billing_cycle_start_day
381+
)
382+
self.firmware = js.get("firmware", self.firmware)
383+
self.latitude = js.get("latitude", self.latitude)
384+
self.longitude = js.get("longitude", self.longitude)
385+
self.excess_solar_configured = js.get(
386+
"excess_solar_configured", self.excess_solar_configured
387+
)
388+
self.peak_demand_configured = js.get(
389+
"peak_demand_configured", self.peak_demand_configured
390+
)
391+
self.energy_management_active = js.get(
392+
"energy_management_active", self.energy_management_active
393+
)
394+
self.energy_managements_configured = js.get(
395+
"energy_managements_configured", self.energy_managements_configured
396+
)
397+
self.energy_managements_active = js.get(
398+
"energy_managements_active", self.energy_managements_active
399+
)
400+
self.evse_product = js.get("evse_product", self.evse_product)
401+
self.evse_model = js.get("evse_model", self.evse_model)
402+
self.evse_branding = js.get("evse_branding", self.evse_branding)
403+
self.evse_power_source = js.get("evse_power_source", self.evse_power_source)
404+
self.evse_color = js.get("evse_color", self.evse_color)
405+
self.evse_gun_type = js.get("evse_gun_type", self.evse_gun_type)
406+
self.breaker_pin = js.get("breaker_pin", self.breaker_pin)
407+
self.charger_on = js.get("charger_on", self.charger_on)
408+
self.charging_rate = js.get("charge_rate_amps", self.charging_rate)
409+
self.max_charging_rate = js.get("max_charge_rate_amps", self.max_charging_rate)
410+
self.power_smart_configuration = js.get(
411+
"power_smart_configuration", self.power_smart_configuration
412+
)
413+
self.vehicle_connected = js.get("vehicle_connected", self.vehicle_connected)
414+
self.vehicle_charging = js.get("vehicle_charging", self.vehicle_charging)
415+
self.partner_schedule_status = js.get(
416+
"partner_schedule_status", self.partner_schedule_status
417+
)
324418
return self
325419

326420
def as_dictionary(self) -> "dict[str, Any]":
327421
d = {
328-
"deviceGid": self.device_gid,
329-
"loadGid": self.load_gid,
330-
"chargerOn": self.charger_on,
331-
"chargingRate": self.charging_rate,
332-
"maxChargingRate": self.max_charging_rate,
422+
"device_id": self.device_id,
423+
"device_gid": self.device_gid,
424+
"charger_on": self.charger_on,
425+
"category": self.category,
426+
"name": self.name,
427+
"device_description": self.device_description,
428+
"connected": self.connected,
429+
"connected_changed_time": self.connected_changed_time.isoformat() if self.connected_changed_time else None,
430+
"time_zone": self.time_zone_string,
431+
"billing_cycle_start_day": self.billing_cycle_start_day,
432+
"firmware": self.firmware,
433+
"latitude": self.latitude,
434+
"longitude": self.longitude,
435+
"excess_solar_configured": self.excess_solar_configured,
436+
"peak_demand_configured": self.peak_demand_configured,
437+
"energy_management_active": self.energy_management_active,
438+
"energy_managements_configured": self.energy_managements_configured,
439+
"energy_managements_active": self.energy_managements_active,
440+
"evse_product": self.evse_product,
441+
"evse_model": self.evse_model,
442+
"evse_branding": self.evse_branding,
443+
"evse_power_source": self.evse_power_source,
444+
"evse_color": self.evse_color,
445+
"evse_gun_type": self.evse_gun_type,
446+
"charging_rate_amps": self.charging_rate,
447+
"max_charge_rate_amps": self.max_charging_rate,
448+
"power_smart_configuration": self.power_smart_configuration,
449+
"vehicle_connected": self.vehicle_connected,
450+
"vehicle_charging": self.vehicle_charging,
451+
"partner_schedule_status": self.partner_schedule_status,
333452
}
334453
if self.breaker_pin:
335-
d["breakerPIN"] = self.breaker_pin
454+
d["breaker_pin"] = self.breaker_pin
336455
return d
337456

338457

0 commit comments

Comments
 (0)