python-chargepoint 2.2.0__tar.gz → 2.3.0__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.0
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.0"
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,
@@ -150,6 +151,18 @@ class ChargePoint:
150
151
 
151
152
  return response
152
153
 
154
+ async def _raise_for_status(
155
+ self, response: aiohttp.ClientResponse, message: str
156
+ ) -> None:
157
+ if response.status != 200:
158
+ text = await response.text()
159
+ _LOGGER.error(
160
+ "status_code=%s err=%s",
161
+ response.status,
162
+ text,
163
+ )
164
+ raise CommunicationError(response=response, message=message)
165
+
153
166
  async def _init_account_parameters(self):
154
167
  account: Account = await self.get_account()
155
168
  self._user_id = account.user.user_id
@@ -231,13 +244,7 @@ class ChargePoint:
231
244
  self._global_config.endpoints.sso_endpoint / "v1/user/logout",
232
245
  )
233
246
 
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
-
247
+ await self._raise_for_status(response, "Failed to log out!")
241
248
  await response.release()
242
249
  self._session.cookie_jar.clear()
243
250
  self._user_id = None
@@ -246,17 +253,9 @@ class ChargePoint:
246
253
  _LOGGER.debug("Discovering account region for username %s", username)
247
254
  request = {"username": username}
248
255
  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
- )
256
+ await self._raise_for_status(
257
+ response, "Failed to discover region for provided username!"
258
+ )
260
259
  config = GlobalConfiguration.model_validate(await response.json())
261
260
  _LOGGER.debug(
262
261
  "Discovered account region: %s / %s (%s)",
@@ -274,17 +273,7 @@ class ChargePoint:
274
273
  self._global_config.endpoints.accounts_endpoint / "v1/driver/profile/user",
275
274
  )
276
275
 
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
-
276
+ await self._raise_for_status(response, "Failed to get user information.")
288
277
  return Account.model_validate(await response.json())
289
278
 
290
279
  @_require_login
@@ -295,17 +284,7 @@ class ChargePoint:
295
284
  self._global_config.endpoints.accounts_endpoint / "v1/driver/vehicle",
296
285
  )
297
286
 
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
-
287
+ await self._raise_for_status(response, "Failed to retrieve EVs.")
309
288
  evs = await response.json()
310
289
  return [ElectricVehicle.model_validate(ev) for ev in evs]
311
290
 
@@ -318,17 +297,7 @@ class ChargePoint:
318
297
  / f"api/v1/configuration/users/{self.user_id}/chargers",
319
298
  )
320
299
 
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
-
300
+ await self._raise_for_status(response, "Failed to retrieve Home Flex chargers.")
332
301
  data = (await response.json())["data"]
333
302
  chargers = [int(item["id"]) for item in data]
334
303
  _LOGGER.debug(
@@ -347,17 +316,7 @@ class ChargePoint:
347
316
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/status",
348
317
  )
349
318
 
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
-
319
+ await self._raise_for_status(response, "Failed to get home charger status.")
361
320
  status = await response.json()
362
321
  _LOGGER.debug(status)
363
322
  return HomeChargerStatus.model_validate({"charger_id": charger_id, **status})
@@ -373,17 +332,7 @@ class ChargePoint:
373
332
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/technical-info",
374
333
  )
375
334
 
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
-
335
+ await self._raise_for_status(response, "Failed to get home charger tech info.")
387
336
  return HomeChargerTechnicalInfo.model_validate(await response.json())
388
337
 
389
338
  @_require_login
@@ -394,17 +343,7 @@ class ChargePoint:
394
343
  "POST", self._global_config.endpoints.mapcache_endpoint / "v2", json=request
395
344
  )
396
345
 
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
-
346
+ await self._raise_for_status(response, "Failed to get user charging status.")
408
347
  status = await response.json()
409
348
  if not status["user_status"]:
410
349
  _LOGGER.debug("No user status returned, assuming not charging.")
@@ -423,17 +362,7 @@ class ChargePoint:
423
362
  json={"chargeAmperageLimit": amperage_limit},
424
363
  )
425
364
 
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
-
365
+ await self._raise_for_status(response, "Failed to set amperage limit.")
437
366
  await response.release()
438
367
 
439
368
  @_require_login
@@ -452,17 +381,7 @@ class ChargePoint:
452
381
  json={"ledBrightnessLevel": level},
453
382
  )
454
383
 
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
-
384
+ await self._raise_for_status(response, "Failed to set LED brightness.")
466
385
  await response.release()
467
386
 
468
387
  @_require_login
@@ -474,17 +393,7 @@ class ChargePoint:
474
393
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/restart",
475
394
  )
476
395
 
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
-
396
+ await self._raise_for_status(response, "Failed to restart charger.")
488
397
  await response.release()
489
398
 
490
399
  @_require_login
@@ -498,19 +407,61 @@ class ChargePoint:
498
407
  / f"api/v1/configuration/users/{self.user_id}/chargers/{charger_id}/configurations",
499
408
  )
500
409
 
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
-
410
+ await self._raise_for_status(response, "Failed to get charger configuration.")
512
411
  return HomeChargerConfiguration.model_validate(await response.json())
513
412
 
413
+ @_require_login
414
+ async def get_home_charger_schedule(self, charger_id: int) -> HomeChargerSchedule:
415
+ _LOGGER.debug("Getting schedule for charger: %s", charger_id)
416
+ response = await self._request(
417
+ "GET",
418
+ self._global_config.endpoints.hcpo_hcm_endpoint
419
+ / f"api/v1/schedule/charger/{charger_id}/schedule",
420
+ )
421
+
422
+ await self._raise_for_status(response, "Failed to get charger schedule.")
423
+ return HomeChargerSchedule.model_validate(await response.json())
424
+
425
+ @_require_login
426
+ async def set_home_charger_schedule(
427
+ self,
428
+ charger_id: int,
429
+ weekday_start: str,
430
+ weekday_end: str,
431
+ weekend_start: str,
432
+ weekend_end: str,
433
+ ) -> HomeChargerSchedule:
434
+ _LOGGER.debug("Setting schedule for charger: %s", charger_id)
435
+ response = await self._request(
436
+ "PUT",
437
+ self._global_config.endpoints.hcpo_hcm_endpoint
438
+ / f"api/v1/schedule/charger/{charger_id}/schedule",
439
+ json={
440
+ "schedule": {
441
+ "weekdays": {"startTime": weekday_start, "endTime": weekday_end},
442
+ "weekends": {"startTime": weekend_start, "endTime": weekend_end},
443
+ }
444
+ },
445
+ )
446
+
447
+ await self._raise_for_status(response, "Failed to set charger schedule.")
448
+ return HomeChargerSchedule.model_validate(await response.json())
449
+
450
+ @_require_login
451
+ async def disable_home_charger_schedule(
452
+ self, charger_id: int
453
+ ) -> HomeChargerSchedule:
454
+ _LOGGER.debug("Disabling schedule for charger: %s", charger_id)
455
+ response = await self._request(
456
+ "PUT",
457
+ self._global_config.endpoints.hcpo_hcm_endpoint
458
+ / f"api/v1/schedule/charger/{charger_id}/schedule",
459
+ json={},
460
+ )
461
+
462
+ await self._raise_for_status(response, "Failed to disable charger schedule.")
463
+ return HomeChargerSchedule.model_validate(await response.json())
464
+
514
465
  @_require_login
515
466
  async def get_charging_session(self, session_id: int) -> ChargingSession:
516
467
  session = ChargingSession(session_id=session_id)
@@ -530,17 +481,7 @@ class ChargePoint:
530
481
  ).update_query({"deviceId": str(device_id), "use_cache": "false"})
531
482
  response = await self._request("GET", url)
532
483
 
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
-
484
+ await self._raise_for_status(response, "Failed to get station info.")
544
485
  data = await response.json()
545
486
  return StationInfo.model_validate(data)
546
487
 
@@ -576,17 +517,7 @@ class ChargePoint:
576
517
  "POST", self._global_config.endpoints.mapcache_endpoint / "v2", json=request
577
518
  )
578
519
 
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
-
520
+ await self._raise_for_status(response, "Failed to get nearby stations.")
590
521
  data = await response.json()
591
522
  stations = data["map_data"].get("stations", [])
592
523
  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 = ""