python-chargepoint 2.2.0__tar.gz → 2.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-chargepoint
3
- Version: 2.2.0
3
+ Version: 2.3.1
4
4
  Summary: A simple, Pythonic wrapper for the ChargePoint API.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -172,6 +172,29 @@ await client.set_led_brightness(charger_id, 3) # 60%
172
172
  await client.restart_home_charger(charger_id)
173
173
  ```
174
174
 
175
+ #### Charging schedule
176
+
177
+ ```python
178
+ schedule = await client.get_home_charger_schedule(charger_id)
179
+ print(schedule.schedule_enabled) # False
180
+ if schedule.default_schedule:
181
+ print(schedule.default_schedule.weekdays.start_time) # "23:00"
182
+ print(schedule.default_schedule.weekdays.end_time) # "07:00"
183
+ print(schedule.default_schedule.weekends.start_time) # "19:00"
184
+ print(schedule.default_schedule.weekends.end_time) # "15:00"
185
+
186
+ # Enable a schedule
187
+ schedule = await client.set_home_charger_schedule(
188
+ charger_id,
189
+ weekday_start="23:00", weekday_end="07:00",
190
+ weekend_start="19:00", weekend_end="15:00",
191
+ )
192
+ print(schedule.schedule_enabled) # True
193
+
194
+ # Disable the schedule
195
+ await client.disable_home_charger_schedule(charger_id)
196
+ ```
197
+
175
198
  ---
176
199
 
177
200
  ### Charging Status and Sessions
@@ -327,6 +350,9 @@ chargepoint charger config <charger_id>
327
350
  chargepoint charger set-amperage <charger_id> <amps>
328
351
  chargepoint charger set-led <charger_id> <level> # 0=off 1=20% 2=40% 3=60% 4=80% 5=100%
329
352
  chargepoint charger restart <charger_id>
353
+ chargepoint charger schedule <charger_id>
354
+ chargepoint charger set-schedule <charger_id> --weekday-start 23:00 --weekday-end 07:00 --weekend-start 19:00 --weekend-end 15:00
355
+ chargepoint charger disable-schedule <charger_id>
330
356
 
331
357
  # Sessions
332
358
  chargepoint session get <session_id>
@@ -151,6 +151,29 @@ await client.set_led_brightness(charger_id, 3) # 60%
151
151
  await client.restart_home_charger(charger_id)
152
152
  ```
153
153
 
154
+ #### Charging schedule
155
+
156
+ ```python
157
+ schedule = await client.get_home_charger_schedule(charger_id)
158
+ print(schedule.schedule_enabled) # False
159
+ if schedule.default_schedule:
160
+ print(schedule.default_schedule.weekdays.start_time) # "23:00"
161
+ print(schedule.default_schedule.weekdays.end_time) # "07:00"
162
+ print(schedule.default_schedule.weekends.start_time) # "19:00"
163
+ print(schedule.default_schedule.weekends.end_time) # "15:00"
164
+
165
+ # Enable a schedule
166
+ schedule = await client.set_home_charger_schedule(
167
+ charger_id,
168
+ weekday_start="23:00", weekday_end="07:00",
169
+ weekend_start="19:00", weekend_end="15:00",
170
+ )
171
+ print(schedule.schedule_enabled) # True
172
+
173
+ # Disable the schedule
174
+ await client.disable_home_charger_schedule(charger_id)
175
+ ```
176
+
154
177
  ---
155
178
 
156
179
  ### Charging Status and Sessions
@@ -306,6 +329,9 @@ chargepoint charger config <charger_id>
306
329
  chargepoint charger set-amperage <charger_id> <amps>
307
330
  chargepoint charger set-led <charger_id> <level> # 0=off 1=20% 2=40% 3=60% 4=80% 5=100%
308
331
  chargepoint charger restart <charger_id>
332
+ chargepoint charger schedule <charger_id>
333
+ chargepoint charger set-schedule <charger_id> --weekday-start 23:00 --weekday-end 07:00 --weekend-start 19:00 --weekend-end 15:00
334
+ chargepoint charger disable-schedule <charger_id>
309
335
 
310
336
  # Sessions
311
337
  chargepoint session get <session_id>
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-chargepoint"
3
- version = "2.2.0"
3
+ version = "2.3.1"
4
4
  description = "A simple, Pythonic wrapper for the ChargePoint API."
5
5
  authors = ["Marc Billow <mbillow@users.noreply.github.compoetry>"]
6
6
  license = "MIT"
@@ -519,6 +519,115 @@ async def charger_restart(ctx, charger_id: int) -> None:
519
519
  await client.close()
520
520
 
521
521
 
522
+ @charger.command("schedule")
523
+ @click.argument("charger_id", type=int)
524
+ @click.pass_context
525
+ @async_cmd
526
+ async def charger_schedule(ctx, charger_id: int) -> None:
527
+ """Show the charging schedule for a home charger."""
528
+ client = await _make_client(ctx.obj["debug"])
529
+ try:
530
+ schedule = await client.get_home_charger_schedule(charger_id)
531
+ if ctx.obj["as_json"]:
532
+ _dump_json(schedule)
533
+ else:
534
+ click.echo(f"Enabled: {schedule.schedule_enabled}")
535
+ for label, window in [
536
+ (
537
+ "Weekdays",
538
+ (
539
+ schedule.default_schedule.weekdays
540
+ if schedule.default_schedule
541
+ else None
542
+ ),
543
+ ),
544
+ (
545
+ "Weekends",
546
+ (
547
+ schedule.default_schedule.weekends
548
+ if schedule.default_schedule
549
+ else None
550
+ ),
551
+ ),
552
+ ]:
553
+ if window:
554
+ click.echo(f"{label}: {window.start_time} – {window.end_time}")
555
+ except CommunicationError as e:
556
+ click.echo(f"Error: {e.message}", err=True)
557
+ sys.exit(1)
558
+ finally:
559
+ await client.close()
560
+
561
+
562
+ @charger.command("set-schedule")
563
+ @click.argument("charger_id", type=int)
564
+ @click.option(
565
+ "--weekday-start", required=True, help='Weekday charge start time, e.g. "23:00"'
566
+ )
567
+ @click.option(
568
+ "--weekday-end", required=True, help='Weekday charge end time, e.g. "07:00"'
569
+ )
570
+ @click.option(
571
+ "--weekend-start", required=True, help='Weekend charge start time, e.g. "19:00"'
572
+ )
573
+ @click.option(
574
+ "--weekend-end", required=True, help='Weekend charge end time, e.g. "15:00"'
575
+ )
576
+ @click.pass_context
577
+ @async_cmd
578
+ async def charger_set_schedule(
579
+ ctx,
580
+ charger_id: int,
581
+ weekday_start: str,
582
+ weekday_end: str,
583
+ weekend_start: str,
584
+ weekend_end: str,
585
+ ) -> None:
586
+ """Set the charging schedule for a home charger."""
587
+ client = await _make_client(ctx.obj["debug"])
588
+ try:
589
+ schedule = await client.set_home_charger_schedule(
590
+ charger_id, weekday_start, weekday_end, weekend_start, weekend_end
591
+ )
592
+ if ctx.obj["as_json"]:
593
+ _dump_json(schedule)
594
+ else:
595
+ click.echo(f"Schedule enabled: {schedule.schedule_enabled}")
596
+ if schedule.user_schedule:
597
+ click.echo(
598
+ f"Weekdays: {schedule.user_schedule.weekdays.start_time} – {schedule.user_schedule.weekdays.end_time}"
599
+ )
600
+ click.echo(
601
+ f"Weekends: {schedule.user_schedule.weekends.start_time} – {schedule.user_schedule.weekends.end_time}"
602
+ )
603
+ except CommunicationError as e:
604
+ click.echo(f"Error: {e.message}", err=True)
605
+ sys.exit(1)
606
+ finally:
607
+ await client.close()
608
+
609
+
610
+ @charger.command("disable-schedule")
611
+ @click.argument("charger_id", type=int)
612
+ @click.confirmation_option(prompt="Disable the charging schedule?")
613
+ @click.pass_context
614
+ @async_cmd
615
+ async def charger_disable_schedule(ctx, charger_id: int) -> None:
616
+ """Disable the charging schedule for a home charger."""
617
+ client = await _make_client(ctx.obj["debug"])
618
+ try:
619
+ schedule = await client.disable_home_charger_schedule(charger_id)
620
+ if ctx.obj["as_json"]:
621
+ _dump_json(schedule)
622
+ else:
623
+ click.echo(f"Schedule enabled: {schedule.schedule_enabled}")
624
+ except CommunicationError as e:
625
+ click.echo(f"Error: {e.message}", err=True)
626
+ sys.exit(1)
627
+ finally:
628
+ await client.close()
629
+
630
+
522
631
  # ---------------------------------------------------------------------------
523
632
  # session subgroup
524
633
  # ---------------------------------------------------------------------------
@@ -13,6 +13,7 @@ from .types import (
13
13
  Account,
14
14
  ElectricVehicle,
15
15
  HomeChargerConfiguration,
16
+ HomeChargerSchedule,
16
17
  HomeChargerStatus,
17
18
  HomeChargerTechnicalInfo,
18
19
  MapFilter,
@@ -128,6 +129,13 @@ class ChargePoint:
128
129
  _LOGGER.debug("[%s] %s", method, url)
129
130
  headers = {**self._request_headers, **kwargs.pop("headers", {})}
130
131
  response = await self._session.request(method, url, headers=headers, **kwargs)
132
+
133
+ # ChargePoint servers return coulomb_sess with Max-Age=7200 on every response.
134
+ # Re-set it without expiry so the cookie jar never evicts it.
135
+ refreshed = response.cookies.get(COULOMB_SESSION)
136
+ if refreshed and refreshed.value:
137
+ self._set_coulomb_token(refreshed.value)
138
+
131
139
  _LOGGER.debug("Status: %d", response.status)
132
140
  _LOGGER.debug("Request Headers: %s", response.request_info.headers)
133
141
  _LOGGER.debug("Response Headers: %s", response.headers)
@@ -150,6 +158,18 @@ class ChargePoint:
150
158
 
151
159
  return response
152
160
 
161
+ async def _raise_for_status(
162
+ self, response: aiohttp.ClientResponse, message: str
163
+ ) -> None:
164
+ if response.status != 200:
165
+ text = await response.text()
166
+ _LOGGER.error(
167
+ "status_code=%s err=%s",
168
+ response.status,
169
+ text,
170
+ )
171
+ raise CommunicationError(response=response, message=message)
172
+
153
173
  async def _init_account_parameters(self):
154
174
  account: Account = await self.get_account()
155
175
  self._user_id = account.user.user_id
@@ -231,13 +251,7 @@ class ChargePoint:
231
251
  self._global_config.endpoints.sso_endpoint / "v1/user/logout",
232
252
  )
233
253
 
234
- if response.status != 200:
235
- text = await response.text()
236
- _LOGGER.error(
237
- "Failed to log out! status_code=%s err=%s", response.status, text
238
- )
239
- raise CommunicationError(response=response, message="Failed to log out!")
240
-
254
+ await self._raise_for_status(response, "Failed to log out!")
241
255
  await response.release()
242
256
  self._session.cookie_jar.clear()
243
257
  self._user_id = None
@@ -246,17 +260,9 @@ class ChargePoint:
246
260
  _LOGGER.debug("Discovering account region for username %s", username)
247
261
  request = {"username": username}
248
262
  response = await self._request("POST", DISCOVERY_API, json=request)
249
- if response.status != 200:
250
- text = await response.text()
251
- _LOGGER.error(
252
- "Failed to discover region! status_code=%s err=%s",
253
- response.status,
254
- text,
255
- )
256
- raise CommunicationError(
257
- response=response,
258
- message="Failed to discover region for provided username!",
259
- )
263
+ await self._raise_for_status(
264
+ response, "Failed to discover region for provided username!"
265
+ )
260
266
  config = GlobalConfiguration.model_validate(await response.json())
261
267
  _LOGGER.debug(
262
268
  "Discovered account region: %s / %s (%s)",
@@ -274,17 +280,7 @@ class ChargePoint:
274
280
  self._global_config.endpoints.accounts_endpoint / "v1/driver/profile/user",
275
281
  )
276
282
 
277
- if response.status != 200:
278
- text = await response.text()
279
- _LOGGER.error(
280
- "Failed to get account information! status_code=%s err=%s",
281
- response.status,
282
- text,
283
- )
284
- raise CommunicationError(
285
- response=response, message="Failed to get user information."
286
- )
287
-
283
+ await self._raise_for_status(response, "Failed to get user information.")
288
284
  return Account.model_validate(await response.json())
289
285
 
290
286
  @_require_login
@@ -295,17 +291,7 @@ class ChargePoint:
295
291
  self._global_config.endpoints.accounts_endpoint / "v1/driver/vehicle",
296
292
  )
297
293
 
298
- if response.status != 200:
299
- text = await response.text()
300
- _LOGGER.error(
301
- "Failed to list vehicles! status_code=%s err=%s",
302
- response.status,
303
- text,
304
- )
305
- raise CommunicationError(
306
- response=response, message="Failed to retrieve EVs."
307
- )
308
-
294
+ await self._raise_for_status(response, "Failed to retrieve EVs.")
309
295
  evs = await response.json()
310
296
  return [ElectricVehicle.model_validate(ev) for ev in evs]
311
297
 
@@ -318,17 +304,7 @@ class ChargePoint:
318
304
  / f"api/v1/configuration/users/{self.user_id}/chargers",
319
305
  )
320
306
 
321
- if response.status != 200:
322
- text = await response.text()
323
- _LOGGER.error(
324
- "Failed to get home chargers! status_code=%s err=%s",
325
- response.status,
326
- text,
327
- )
328
- raise CommunicationError(
329
- response=response, message="Failed to retrieve Home Flex chargers."
330
- )
331
-
307
+ await self._raise_for_status(response, "Failed to retrieve Home Flex chargers.")
332
308
  data = (await response.json())["data"]
333
309
  chargers = [int(item["id"]) for item in data]
334
310
  _LOGGER.debug(
@@ -347,17 +323,7 @@ class ChargePoint:
347
323
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/status",
348
324
  )
349
325
 
350
- if response.status != 200:
351
- text = await response.text()
352
- _LOGGER.error(
353
- "Failed to determine home charger status! status_code=%s err=%s",
354
- response.status,
355
- text,
356
- )
357
- raise CommunicationError(
358
- response=response, message="Failed to get home charger status."
359
- )
360
-
326
+ await self._raise_for_status(response, "Failed to get home charger status.")
361
327
  status = await response.json()
362
328
  _LOGGER.debug(status)
363
329
  return HomeChargerStatus.model_validate({"charger_id": charger_id, **status})
@@ -373,17 +339,7 @@ class ChargePoint:
373
339
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/technical-info",
374
340
  )
375
341
 
376
- if response.status != 200:
377
- text = await response.text()
378
- _LOGGER.error(
379
- "Failed to get home charger tech info! status_code=%s err=%s",
380
- response.status,
381
- text,
382
- )
383
- raise CommunicationError(
384
- response=response, message="Failed to get home charger tech info."
385
- )
386
-
342
+ await self._raise_for_status(response, "Failed to get home charger tech info.")
387
343
  return HomeChargerTechnicalInfo.model_validate(await response.json())
388
344
 
389
345
  @_require_login
@@ -394,17 +350,7 @@ class ChargePoint:
394
350
  "POST", self._global_config.endpoints.mapcache_endpoint / "v2", json=request
395
351
  )
396
352
 
397
- if response.status != 200:
398
- text = await response.text()
399
- _LOGGER.error(
400
- "Failed to get account charging status! status_code=%s err=%s",
401
- response.status,
402
- text,
403
- )
404
- raise CommunicationError(
405
- response=response, message="Failed to get user charging status."
406
- )
407
-
353
+ await self._raise_for_status(response, "Failed to get user charging status.")
408
354
  status = await response.json()
409
355
  if not status["user_status"]:
410
356
  _LOGGER.debug("No user status returned, assuming not charging.")
@@ -423,17 +369,7 @@ class ChargePoint:
423
369
  json={"chargeAmperageLimit": amperage_limit},
424
370
  )
425
371
 
426
- if response.status != 200:
427
- text = await response.text()
428
- _LOGGER.error(
429
- "Failed to set amperage limit! status_code=%s err=%s",
430
- response.status,
431
- text,
432
- )
433
- raise CommunicationError(
434
- response=response, message="Failed to set amperage limit."
435
- )
436
-
372
+ await self._raise_for_status(response, "Failed to set amperage limit.")
437
373
  await response.release()
438
374
 
439
375
  @_require_login
@@ -452,17 +388,7 @@ class ChargePoint:
452
388
  json={"ledBrightnessLevel": level},
453
389
  )
454
390
 
455
- if response.status != 200:
456
- text = await response.text()
457
- _LOGGER.error(
458
- "Failed to set LED brightness! status_code=%s err=%s",
459
- response.status,
460
- text,
461
- )
462
- raise CommunicationError(
463
- response=response, message="Failed to set LED brightness."
464
- )
465
-
391
+ await self._raise_for_status(response, "Failed to set LED brightness.")
466
392
  await response.release()
467
393
 
468
394
  @_require_login
@@ -474,17 +400,7 @@ class ChargePoint:
474
400
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/restart",
475
401
  )
476
402
 
477
- if response.status != 200:
478
- text = await response.text()
479
- _LOGGER.error(
480
- "Failed to restart charger! status_code=%s err=%s",
481
- response.status,
482
- text,
483
- )
484
- raise CommunicationError(
485
- response=response, message="Failed to restart charger."
486
- )
487
-
403
+ await self._raise_for_status(response, "Failed to restart charger.")
488
404
  await response.release()
489
405
 
490
406
  @_require_login
@@ -498,19 +414,61 @@ class ChargePoint:
498
414
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/configurations",
499
415
  )
500
416
 
501
- if response.status != 200:
502
- text = await response.text()
503
- _LOGGER.error(
504
- "Failed to get charger configuration! status_code=%s err=%s",
505
- response.status,
506
- text,
507
- )
508
- raise CommunicationError(
509
- response=response, message="Failed to get charger configuration."
510
- )
511
-
417
+ await self._raise_for_status(response, "Failed to get charger configuration.")
512
418
  return HomeChargerConfiguration.model_validate(await response.json())
513
419
 
420
+ @_require_login
421
+ async def get_home_charger_schedule(self, charger_id: int) -> HomeChargerSchedule:
422
+ _LOGGER.debug("Getting schedule for charger: %s", charger_id)
423
+ response = await self._request(
424
+ "GET",
425
+ self._global_config.endpoints.hcpo_hcm_endpoint
426
+ / f"api/v1/schedule/charger/{charger_id}/schedule",
427
+ )
428
+
429
+ await self._raise_for_status(response, "Failed to get charger schedule.")
430
+ return HomeChargerSchedule.model_validate(await response.json())
431
+
432
+ @_require_login
433
+ async def set_home_charger_schedule(
434
+ self,
435
+ charger_id: int,
436
+ weekday_start: str,
437
+ weekday_end: str,
438
+ weekend_start: str,
439
+ weekend_end: str,
440
+ ) -> HomeChargerSchedule:
441
+ _LOGGER.debug("Setting schedule for charger: %s", charger_id)
442
+ response = await self._request(
443
+ "PUT",
444
+ self._global_config.endpoints.hcpo_hcm_endpoint
445
+ / f"api/v1/schedule/charger/{charger_id}/schedule",
446
+ json={
447
+ "schedule": {
448
+ "weekdays": {"startTime": weekday_start, "endTime": weekday_end},
449
+ "weekends": {"startTime": weekend_start, "endTime": weekend_end},
450
+ }
451
+ },
452
+ )
453
+
454
+ await self._raise_for_status(response, "Failed to set charger schedule.")
455
+ return HomeChargerSchedule.model_validate(await response.json())
456
+
457
+ @_require_login
458
+ async def disable_home_charger_schedule(
459
+ self, charger_id: int
460
+ ) -> HomeChargerSchedule:
461
+ _LOGGER.debug("Disabling schedule for charger: %s", charger_id)
462
+ response = await self._request(
463
+ "PUT",
464
+ self._global_config.endpoints.hcpo_hcm_endpoint
465
+ / f"api/v1/schedule/charger/{charger_id}/schedule",
466
+ json={},
467
+ )
468
+
469
+ await self._raise_for_status(response, "Failed to disable charger schedule.")
470
+ return HomeChargerSchedule.model_validate(await response.json())
471
+
514
472
  @_require_login
515
473
  async def get_charging_session(self, session_id: int) -> ChargingSession:
516
474
  session = ChargingSession(session_id=session_id)
@@ -530,17 +488,7 @@ class ChargePoint:
530
488
  ).update_query({"deviceId": str(device_id), "use_cache": "false"})
531
489
  response = await self._request("GET", url)
532
490
 
533
- if response.status != 200:
534
- text = await response.text()
535
- _LOGGER.error(
536
- "Failed to get station info! status_code=%s err=%s",
537
- response.status,
538
- text,
539
- )
540
- raise CommunicationError(
541
- response=response, message="Failed to get station info."
542
- )
543
-
491
+ await self._raise_for_status(response, "Failed to get station info.")
544
492
  data = await response.json()
545
493
  return StationInfo.model_validate(data)
546
494
 
@@ -576,17 +524,7 @@ class ChargePoint:
576
524
  "POST", self._global_config.endpoints.mapcache_endpoint / "v2", json=request
577
525
  )
578
526
 
579
- if response.status != 200:
580
- text = await response.text()
581
- _LOGGER.error(
582
- "Failed to get nearby stations! status_code=%s err=%s",
583
- response.status,
584
- text,
585
- )
586
- raise CommunicationError(
587
- response=response, message="Failed to get nearby stations."
588
- )
589
-
527
+ await self._raise_for_status(response, "Failed to get nearby stations.")
590
528
  data = await response.json()
591
529
  stations = data["map_data"].get("stations", [])
592
530
  return [MapStation.model_validate(s) for s in stations]
@@ -166,6 +166,28 @@ class HomeChargerConfiguration(_CamelModel):
166
166
  return {**settings, "led_brightness": led_brightness}
167
167
 
168
168
 
169
+ class ChargeScheduleWindow(_CamelModel):
170
+ start_time: str = ""
171
+ end_time: str = ""
172
+ start_weekday: Optional[int] = None
173
+ end_weekday: Optional[int] = None
174
+
175
+
176
+ class ChargeSchedule(_CamelModel):
177
+ weekdays: ChargeScheduleWindow = Field(default_factory=ChargeScheduleWindow)
178
+ weekends: ChargeScheduleWindow = Field(default_factory=ChargeScheduleWindow)
179
+
180
+
181
+ class HomeChargerSchedule(_CamelModel):
182
+ has_tou_pricing: bool = False
183
+ schedule_enabled: bool = False
184
+ has_utility_info: bool = False
185
+ based_on_utility: Optional["PowerUtility"] = None
186
+ default_schedule: Optional[ChargeSchedule] = None
187
+ user_schedule: Optional[ChargeSchedule] = None
188
+ utility_schedule: Optional[ChargeSchedule] = None
189
+
190
+
169
191
  class Station(_BaseModel):
170
192
  id: int = Field(0, alias="deviceId")
171
193
  name: str = ""