pypetkitapi 1.8.1__tar.gz → 1.9.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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pypetkitapi
3
- Version: 1.8.1
3
+ Version: 1.9.1
4
4
  Summary: Python client for PetKit API
5
5
  Home-page: https://github.com/Jezza34000/pypetkit
6
6
  License: MIT
@@ -101,7 +101,10 @@ async def main():
101
101
 
102
102
  ### Example 2 : Feed the pet
103
103
  ### Device_ID, Command, Payload
104
+ # simple hopper :
104
105
  await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
106
+ # dual hopper :
107
+ await client.send_api_request(123456789, FeederCommand.MANUAL_FEED_DUAL, {"amount1": 2})
105
108
 
106
109
  ### Example 3 : Start the cleaning process
107
110
  ### Device_ID, Command, Payload
@@ -112,6 +115,10 @@ if __name__ == "__main__":
112
115
  asyncio.run(main())
113
116
  ```
114
117
 
118
+ ## More example usage
119
+
120
+ Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
121
+
115
122
  ## Contributing
116
123
 
117
124
  Contributions are welcome! Please open an issue or submit a pull request.
@@ -81,7 +81,10 @@ async def main():
81
81
 
82
82
  ### Example 2 : Feed the pet
83
83
  ### Device_ID, Command, Payload
84
+ # simple hopper :
84
85
  await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
86
+ # dual hopper :
87
+ await client.send_api_request(123456789, FeederCommand.MANUAL_FEED_DUAL, {"amount1": 2})
85
88
 
86
89
  ### Example 3 : Start the cleaning process
87
90
  ### Device_ID, Command, Payload
@@ -92,6 +95,10 @@ if __name__ == "__main__":
92
95
  asyncio.run(main())
93
96
  ```
94
97
 
98
+ ## More example usage
99
+
100
+ Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
101
+
95
102
  ## Contributing
96
103
 
97
104
  Contributions are welcome! Please open an issue or submit a pull request.
@@ -40,7 +40,7 @@ from .medias import MediaHandler, MediasFiles
40
40
  from .purifier_container import Purifier
41
41
  from .water_fountain_container import WaterFountain
42
42
 
43
- __version__ = "1.8.1"
43
+ __version__ = "1.9.1"
44
44
 
45
45
  __all__ = [
46
46
  "CTW3",
@@ -1,17 +1,22 @@
1
1
  """Pypetkit Client: A Python library for interfacing with PetKit"""
2
2
 
3
3
  import asyncio
4
+ import base64
4
5
  from datetime import datetime, timedelta
5
6
  from enum import StrEnum
6
7
  import hashlib
7
8
  from http import HTTPMethod
8
9
  import logging
10
+ import urllib.parse
9
11
 
10
12
  import aiohttp
11
13
  from aiohttp import ContentTypeError
12
14
 
13
- from pypetkitapi.command import ACTIONS_MAP
15
+ from pypetkitapi.command import ACTIONS_MAP, FOUNTAIN_COMMAND, FountainAction
14
16
  from pypetkitapi.const import (
17
+ BLE_CONNECT_ATTEMPT,
18
+ BLE_END_TRAME,
19
+ BLE_START_TRAME,
15
20
  DEVICE_DATA,
16
21
  DEVICE_RECORDS,
17
22
  DEVICE_STATS,
@@ -23,13 +28,22 @@ from pypetkitapi.const import (
23
28
  LOGIN_DATA,
24
29
  PET,
25
30
  RES_KEY,
31
+ T3,
26
32
  T4,
33
+ T5,
27
34
  T6,
28
35
  Header,
29
36
  PetkitDomain,
30
37
  PetkitEndpoint,
31
38
  )
32
- from pypetkitapi.containers import AccountData, Device, Pet, RegionInfo, SessionInfo
39
+ from pypetkitapi.containers import (
40
+ AccountData,
41
+ BleRelay,
42
+ Device,
43
+ Pet,
44
+ RegionInfo,
45
+ SessionInfo,
46
+ )
33
47
  from pypetkitapi.exceptions import (
34
48
  PetkitAuthenticationError,
35
49
  PetkitAuthenticationUnregisteredEmailError,
@@ -201,7 +215,16 @@ class PetKitClient:
201
215
  if account.pet_list:
202
216
  for pet in account.pet_list:
203
217
  self.petkit_entities[pet.pet_id] = pet
204
- pet.device_nfo = Device(deviceType=PET, deviceId=pet.pet_id)
218
+ pet.device_nfo = Device(
219
+ deviceType=PET,
220
+ deviceId=pet.pet_id,
221
+ createdAt=pet.created_at,
222
+ deviceName=pet.pet_name,
223
+ groupId=0,
224
+ type=0,
225
+ typeCode=0,
226
+ uniqueId=pet.sn,
227
+ )
205
228
 
206
229
  async def get_devices_data(self) -> None:
207
230
  """Get the devices data from the PetKit servers."""
@@ -223,7 +246,7 @@ class PetKitClient:
223
246
  _LOGGER.debug("Found %s devices", len(account.device_list))
224
247
 
225
248
  for device in device_list:
226
- device_type = device.device_type.lower()
249
+ device_type = device.device_type
227
250
 
228
251
  if device_type in DEVICES_FEEDER:
229
252
  main_tasks.append(self._fetch_device_data(device, Feeder))
@@ -235,9 +258,9 @@ class PetKitClient:
235
258
  )
236
259
  record_tasks.append(self._fetch_device_data(device, LitterRecord))
237
260
 
238
- if device_type == T4:
261
+ if device_type in [T3, T4]:
239
262
  record_tasks.append(self._fetch_device_data(device, LitterStats))
240
- if device_type == T6:
263
+ if device_type in [T5, T6]:
241
264
  record_tasks.append(self._fetch_device_data(device, PetOutGraph))
242
265
 
243
266
  elif device_type in DEVICES_WATER_FOUNTAIN:
@@ -259,7 +282,7 @@ class PetKitClient:
259
282
  stats_tasks = [
260
283
  self.populate_pet_stats(self.petkit_entities[device.device_id])
261
284
  for device in device_list
262
- if device.device_type.lower() in DEVICES_LITTER_BOX
285
+ if device.device_type in DEVICES_LITTER_BOX
263
286
  ]
264
287
 
265
288
  # Execute stats tasks
@@ -285,7 +308,7 @@ class PetKitClient:
285
308
  ],
286
309
  ) -> None:
287
310
  """Fetch the device data from the PetKit servers."""
288
- device_type = device.device_type.lower()
311
+ device_type = device.device_type
289
312
 
290
313
  _LOGGER.debug("Reading device type : %s (id=%s)", device_type, device.device_id)
291
314
 
@@ -330,9 +353,9 @@ class PetKitClient:
330
353
  self.petkit_entities[device.device_id].device_records = device_data
331
354
  _LOGGER.debug("Device records fetched OK for %s", device_type)
332
355
  elif data_class.data_type == DEVICE_STATS:
333
- if device_type == T4:
356
+ if device_type in [T3, T4]:
334
357
  self.petkit_entities[device.device_id].device_stats = device_data
335
- if device_type == T6:
358
+ if device_type in [T5, T6]:
336
359
  self.petkit_entities[device.device_id].device_pet_graph_out = (
337
360
  device_data
338
361
  )
@@ -365,17 +388,20 @@ class PetKitClient:
365
388
 
366
389
  pets_list = await self.get_pets_list()
367
390
  for pet in pets_list:
368
- if stats_data.device_nfo.device_type == T4 and stats_data.device_records:
369
- await self._process_t4(pet, stats_data.device_records)
391
+ if (
392
+ stats_data.device_nfo.device_type in [T3, T4]
393
+ and stats_data.device_records
394
+ ):
395
+ await self._process_t3_t4(pet, stats_data)
370
396
  elif (
371
- stats_data.device_nfo.device_type == T6
397
+ stats_data.device_nfo.device_type in [T5, T6]
372
398
  and stats_data.device_pet_graph_out
373
399
  ):
374
- await self._process_t6(pet, stats_data.device_pet_graph_out)
400
+ await self._process_t5_t6(pet, stats_data)
375
401
 
376
- async def _process_t4(self, pet, device_records):
377
- """Process T4 device records."""
378
- for stat in device_records:
402
+ async def _process_t3_t4(self, pet, device_records):
403
+ """Process T3/T4 devices records."""
404
+ for stat in device_records.device_records:
379
405
  if stat.pet_id == pet.pet_id and (
380
406
  pet.last_litter_usage is None
381
407
  or self.get_safe_value(stat.timestamp) > pet.last_litter_usage
@@ -388,11 +414,11 @@ class PetKitClient:
388
414
  stat.content.time_in if stat.content else None,
389
415
  stat.content.time_out if stat.content else None,
390
416
  )
391
- pet.last_device_used = "Pura Max"
417
+ pet.last_device_used = device_records.device_nfo.device_name
392
418
 
393
- async def _process_t6(self, pet, pet_graphs):
394
- """Process T6 pet graphs."""
395
- for graph in pet_graphs:
419
+ async def _process_t5_t6(self, pet, pet_graphs):
420
+ """Process T5/T6 pet graphs."""
421
+ for graph in pet_graphs.device_pet_graph_out:
396
422
  if graph.pet_id == pet.pet_id and (
397
423
  pet.last_litter_usage is None
398
424
  or self.get_safe_value(graph.time) > pet.last_litter_usage
@@ -402,7 +428,172 @@ class PetKitClient:
402
428
  graph.content.pet_weight if graph.content else None
403
429
  )
404
430
  pet.last_duration_usage = self.get_safe_value(graph.toilet_time)
405
- pet.last_device_used = "Purobot Ultra"
431
+ pet.last_device_used = pet_graphs.device_nfo.device_name
432
+
433
+ async def _get_fountain_instance(self, fountain_id: int) -> WaterFountain:
434
+ # Retrieve the water fountain object
435
+ water_fountain = self.petkit_entities.get(fountain_id)
436
+ if not water_fountain:
437
+ _LOGGER.error("Water fountain with ID %s not found.", fountain_id)
438
+ raise ValueError(f"Water fountain with ID {fountain_id} not found.")
439
+ return water_fountain
440
+
441
+ async def check_relay_availability(self, fountain_id: int) -> bool:
442
+ """Check if BLE relay is available for the account."""
443
+ fountain = None
444
+ for account in self.account_data:
445
+ fountain = next(
446
+ (
447
+ device
448
+ for device in account.device_list
449
+ if device.device_id == fountain_id
450
+ ),
451
+ None,
452
+ )
453
+ if fountain:
454
+ break
455
+
456
+ if not fountain:
457
+ raise ValueError(
458
+ f"Fountain with device_id {fountain_id} not found for the current account"
459
+ )
460
+
461
+ group_id = fountain.group_id
462
+
463
+ response = await self.req.request(
464
+ method=HTTPMethod.POST,
465
+ url=f"{PetkitEndpoint.BLE_AS_RELAY}",
466
+ params={"groupId": group_id},
467
+ headers=await self.get_session_id(),
468
+ )
469
+ ble_relays = [BleRelay(**relay) for relay in response]
470
+
471
+ if len(ble_relays) == 0:
472
+ _LOGGER.warning("No BLE relay devices found.")
473
+ return False
474
+ return True
475
+
476
+ async def open_ble_connection(self, fountain_id: int) -> bool:
477
+ """Open a BLE connection to a PetKit device."""
478
+ _LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
479
+ water_fountain = await self._get_fountain_instance(fountain_id)
480
+
481
+ if await self.check_relay_availability(fountain_id) is False:
482
+ _LOGGER.error("BLE relay not available.")
483
+ return False
484
+
485
+ if water_fountain.is_connected is True:
486
+ _LOGGER.error("BLE connection already established.")
487
+ return True
488
+
489
+ response = await self.req.request(
490
+ method=HTTPMethod.POST,
491
+ url=PetkitEndpoint.BLE_CONNECT,
492
+ data={
493
+ "bleId": fountain_id,
494
+ "type": 24,
495
+ "mac": water_fountain.mac,
496
+ },
497
+ headers=await self.get_session_id(),
498
+ )
499
+ if response != {"state": 1}:
500
+ _LOGGER.error("Failed to establish BLE connection.")
501
+ water_fountain.is_connected = False
502
+ return False
503
+
504
+ for attempt in range(BLE_CONNECT_ATTEMPT):
505
+ _LOGGER.warning("BLE connection attempt n%s", attempt)
506
+ response = await self.req.request(
507
+ method=HTTPMethod.POST,
508
+ url=PetkitEndpoint.BLE_POLL,
509
+ data={
510
+ "bleId": fountain_id,
511
+ "type": 24,
512
+ "mac": water_fountain.mac,
513
+ },
514
+ headers=await self.get_session_id(),
515
+ )
516
+ if response == 1:
517
+ _LOGGER.info("BLE connection established successfully.")
518
+ water_fountain.is_connected = True
519
+ water_fountain.last_ble_poll = datetime.now().strftime(
520
+ "%Y-%m-%dT%H:%M:%S.%f"
521
+ )
522
+ return True
523
+ await asyncio.sleep(4)
524
+ _LOGGER.error("Failed to establish BLE connection after multiple attempts.")
525
+ water_fountain.is_connected = False
526
+ return False
527
+
528
+ async def close_ble_connection(self, fountain_id: int) -> None:
529
+ """Close the BLE connection to a PetKit device."""
530
+ _LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
531
+ water_fountain = await self._get_fountain_instance(fountain_id)
532
+
533
+ await self.req.request(
534
+ method=HTTPMethod.POST,
535
+ url=PetkitEndpoint.BLE_CANCEL,
536
+ data={
537
+ "bleId": fountain_id,
538
+ "type": 24,
539
+ "mac": water_fountain.mac,
540
+ },
541
+ headers=await self.get_session_id(),
542
+ )
543
+ _LOGGER.info("BLE connection closed successfully.")
544
+
545
+ async def get_ble_cmd_data(
546
+ self, fountain_command: list, counter: int
547
+ ) -> tuple[int, str]:
548
+ """Prepare BLE data by adding start and end trame to the command and extracting the first number."""
549
+ cmd_code = fountain_command[0]
550
+ modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
551
+ ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
552
+ encoded_data = await self._encode_ble_data(ble_data)
553
+ return cmd_code, encoded_data
554
+
555
+ @staticmethod
556
+ async def _encode_ble_data(byte_list: list) -> str:
557
+ """Encode a list of bytes to a URL encoded base64 string."""
558
+ byte_array = bytearray(byte_list)
559
+ b64_encoded = base64.b64encode(byte_array)
560
+ return urllib.parse.quote(b64_encoded)
561
+
562
+ async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
563
+ """BLE command to a PetKit device."""
564
+ _LOGGER.info("Sending BLE command to fountain %s", fountain_id)
565
+ water_fountain = await self._get_fountain_instance(fountain_id)
566
+
567
+ if water_fountain.is_connected is False:
568
+ _LOGGER.error("BLE connection not established.")
569
+ return False
570
+
571
+ command = FOUNTAIN_COMMAND.get[command, None]
572
+ if command is None:
573
+ _LOGGER.error("Command not found.")
574
+ return False
575
+
576
+ cmd_code, cmd_data = await self.get_ble_cmd_data(
577
+ command, water_fountain.ble_counter
578
+ )
579
+
580
+ response = await self.req.request(
581
+ method=HTTPMethod.POST,
582
+ url=PetkitEndpoint.BLE_CONTROL_DEVICE,
583
+ data={
584
+ "bleId": water_fountain.id,
585
+ "cmd": cmd_code,
586
+ "data": cmd_data,
587
+ "mac": water_fountain.mac,
588
+ "type": 24,
589
+ },
590
+ headers=await self.get_session_id(),
591
+ )
592
+ if response != 1:
593
+ _LOGGER.error("Failed to send BLE command.")
594
+ return False
595
+ _LOGGER.info("BLE command sent successfully.")
596
+ return True
406
597
 
407
598
  async def send_api_request(
408
599
  self,
@@ -413,7 +604,7 @@ class PetKitClient:
413
604
  """Control the device using the PetKit API."""
414
605
  await self.validate_session()
415
606
 
416
- device = self.petkit_entities.get(device_id)
607
+ device = self.petkit_entities.get(device_id, None)
417
608
  if not device:
418
609
  raise PypetkitError(f"Device with ID {device_id} not found.")
419
610
 
@@ -427,7 +618,7 @@ class PetKitClient:
427
618
 
428
619
  # Check if the device type is supported
429
620
  if device.device_nfo.device_type:
430
- device_type = device.device_nfo.device_type.lower()
621
+ device_type = device.device_nfo.device_type
431
622
  else:
432
623
  raise PypetkitError(
433
624
  "Device type is not available, and is mandatory for sending commands."
@@ -452,7 +643,7 @@ class PetKitClient:
452
643
  else:
453
644
  endpoint = action_info.endpoint
454
645
  _LOGGER.debug("Endpoint field")
455
- url = f"{device.device_nfo.device_type.lower()}/{endpoint}"
646
+ url = f"{device.device_nfo.device_type}/{endpoint}"
456
647
 
457
648
  # Get the parameters
458
649
  if setting is not None:
@@ -562,7 +753,7 @@ class PrepReq:
562
753
  )
563
754
  case _:
564
755
  raise PypetkitError(
565
- f"Request failed code : {error_code} details : {error_msg}"
756
+ f"Request failed code : {error_code}, details : {error_msg} url : {url}"
566
757
  )
567
758
 
568
759
  # Check for success in the response
@@ -34,6 +34,12 @@ class DeviceCommand(StrEnum):
34
34
  UPDATE_SETTING = "update_setting"
35
35
 
36
36
 
37
+ class FountainCommand(StrEnum):
38
+ """Device Command"""
39
+
40
+ CONTROL_DEVICE = "control_device"
41
+
42
+
37
43
  class FeederCommand(StrEnum):
38
44
  """Specific Feeder Command"""
39
45
 
@@ -101,16 +107,17 @@ class DeviceAction(StrEnum):
101
107
  class FountainAction(StrEnum):
102
108
  """Fountain Action"""
103
109
 
110
+ MODE_NORMAL = "Normal"
111
+ MODE_SMART = "Smart"
112
+ MODE_STANDARD = "Standard"
113
+ MODE_INTERMITTENT = "Intermittent"
104
114
  PAUSE = "Pause"
105
- NORMAL_TO_PAUSE = "Normal To Pause"
106
- SMART_TO_PAUSE = "Smart To Pause"
107
- NORMAL = "Normal"
108
- SMART = "Smart"
115
+ CONTINUE = "Continue"
116
+ POWER_OFF = "Power Off"
117
+ POWER_ON = "Power On"
109
118
  RESET_FILTER = "Reset Filter"
110
119
  DO_NOT_DISTURB = "Do Not Disturb"
111
120
  DO_NOT_DISTURB_OFF = "Do Not Disturb Off"
112
- FIRST_BLE_CMND = "First BLE Command"
113
- SECOND_BLE_CMND = "Second BLE Command"
114
121
  LIGHT_LOW = "Light Low"
115
122
  LIGHT_MEDIUM = "Light Medium"
116
123
  LIGHT_HIGH = "Light High"
@@ -118,20 +125,12 @@ class FountainAction(StrEnum):
118
125
  LIGHT_OFF = "Light Off"
119
126
 
120
127
 
121
- FOUNTAIN_COMMAND_TO_CODE = {
122
- FountainAction.DO_NOT_DISTURB: "221",
123
- FountainAction.DO_NOT_DISTURB_OFF: "221",
124
- FountainAction.LIGHT_ON: "221",
125
- FountainAction.LIGHT_OFF: "221",
126
- FountainAction.LIGHT_LOW: "221",
127
- FountainAction.LIGHT_MEDIUM: "221",
128
- FountainAction.LIGHT_HIGH: "221",
129
- FountainAction.PAUSE: "220",
130
- FountainAction.RESET_FILTER: "222",
131
- FountainAction.NORMAL: "220",
132
- FountainAction.NORMAL_TO_PAUSE: "220",
133
- FountainAction.SMART: "220",
134
- FountainAction.SMART_TO_PAUSE: "220",
128
+ FOUNTAIN_COMMAND = {
129
+ FountainAction.PAUSE: [220, 1, 3, 0, 1, 0, 2],
130
+ FountainAction.CONTINUE: [220, 1, 3, 0, 1, 1, 2],
131
+ FountainAction.RESET_FILTER: [222, 1, 0, 0],
132
+ FountainAction.POWER_OFF: [220, 1, 3, 0, 0, 1, 1],
133
+ FountainAction.POWER_ON: [220, 1, 3, 0, 1, 1, 1],
135
134
  }
136
135
 
137
136
 
@@ -146,18 +145,18 @@ class CmdData:
146
145
 
147
146
  def get_endpoint_manual_feed(device):
148
147
  """Get the endpoint for the device"""
149
- if device.device_type == FEEDER_MINI:
148
+ if device.device_nfo.device_type == FEEDER_MINI:
150
149
  return PetkitEndpoint.MANUAL_FEED_MINI
151
- if device.device_type == FEEDER:
150
+ if device.device_nfo.device_type == FEEDER:
152
151
  return PetkitEndpoint.MANUAL_FEED_FRESH_ELEMENT
153
152
  return PetkitEndpoint.MANUAL_FEED_DUAL
154
153
 
155
154
 
156
155
  def get_endpoint_reset_desiccant(device):
157
156
  """Get the endpoint for the device"""
158
- if device.device_type == FEEDER_MINI:
157
+ if device.device_nfo.device_type == FEEDER_MINI:
159
158
  return PetkitEndpoint.MINI_DESICCANT_RESET
160
- if device.device_type == FEEDER:
159
+ if device.device_nfo.device_type == FEEDER:
161
160
  return PetkitEndpoint.FRESH_ELEMENT_DESICCANT_RESET
162
161
  return PetkitEndpoint.DESICCANT_RESET
163
162
 
@@ -222,7 +221,7 @@ ACTIONS_MAP = {
222
221
  FeederCommand.CANCEL_MANUAL_FEED: CmdData(
223
222
  endpoint=lambda device: (
224
223
  PetkitEndpoint.FRESH_ELEMENT_CANCEL_FEED
225
- if device.device_type == FEEDER
224
+ if device.device_nfo.device_type == FEEDER
226
225
  else PetkitEndpoint.CANCEL_FEED
227
226
  ),
228
227
  params=lambda device: {
@@ -230,7 +229,7 @@ ACTIONS_MAP = {
230
229
  "deviceId": device.id,
231
230
  **(
232
231
  {"id": device.manual_feed_id}
233
- if device.device_type.lower() in [D4H, D4S, D4SH]
232
+ if device.device_nfo.device_type in [D4H, D4S, D4SH]
234
233
  else {}
235
234
  ),
236
235
  },
@@ -281,15 +280,4 @@ ACTIONS_MAP = {
281
280
  },
282
281
  supported_device=ALL_DEVICES,
283
282
  ),
284
- # FountainCommand.CONTROL_DEVICE: CmdData(
285
- # endpoint=PetkitEndpoint.CONTROL_DEVICE,
286
- # params=lambda device, setting: {
287
- # "bleId": device.id,
288
- # "cmd": cmnd_code,
289
- # "data": ble_data,
290
- # "mac": device.mac,
291
- # "type": water_fountain.ble_relay,
292
- # },
293
- # supported_device=[CTW3],
294
- # ),
295
283
  }
@@ -11,6 +11,11 @@ DEVICE_DATA = "deviceData"
11
11
  DEVICE_STATS = "deviceStats"
12
12
  PET_DATA = "petData"
13
13
 
14
+ # Bluetooth
15
+ BLE_CONNECT_ATTEMPT = 4
16
+ BLE_START_TRAME = [250, 252, 253]
17
+ BLE_END_TRAME = [251]
18
+
14
19
  # PetKit Models
15
20
  FEEDER = "feeder"
16
21
  FEEDER_MINI = "feedermini"
@@ -120,11 +125,12 @@ class PetkitEndpoint(StrEnum):
120
125
  GET_DEVICE_RECORD_RELEASE = "getDeviceRecordRelease"
121
126
  UPDATE_SETTING = "updateSettings"
122
127
 
123
- # Bluetooth relay
128
+ # Bluetooth
124
129
  BLE_AS_RELAY = "ble/ownSupportBleDevices"
125
130
  BLE_CONNECT = "ble/connect"
126
131
  BLE_POLL = "ble/poll"
127
132
  BLE_CANCEL = "ble/cancel"
133
+ BLE_CONTROL_DEVICE = "ble/controlDevice"
128
134
 
129
135
  # Fountain & Litter Box
130
136
  CONTROL_DEVICE = "controlDevice"
@@ -1,6 +1,6 @@
1
1
  """Dataclasses container for petkit API."""
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel, Field, field_validator
4
4
 
5
5
 
6
6
  class RegionInfo(BaseModel):
@@ -51,14 +51,21 @@ class Device(BaseModel):
51
51
  Subclass of AccountData.
52
52
  """
53
53
 
54
- created_at: int | None = Field(None, alias="createdAt")
55
- device_id: int | None = Field(None, alias="deviceId")
56
- device_name: str | None = Field(None, alias="deviceName")
57
- device_type: str | None = Field(None, alias="deviceType")
58
- group_id: int | None = Field(None, alias="groupId")
59
- type: int | None = None
54
+ created_at: int = Field(alias="createdAt")
55
+ device_id: int = Field(alias="deviceId")
56
+ device_name: str = Field(alias="deviceName")
57
+ device_type: str = Field(alias="deviceType")
58
+ group_id: int = Field(alias="groupId")
59
+ type: int
60
60
  type_code: int = Field(0, alias="typeCode")
61
- unique_id: str | None = Field(None, alias="uniqueId")
61
+ unique_id: str = Field(alias="uniqueId")
62
+
63
+ @field_validator("device_name", "device_type", "unique_id", mode="before")
64
+ def convert_to_lower(cls, value): # noqa: N805
65
+ """Convert device_name, device_type and unique_id to lowercase."""
66
+ if value is not None and isinstance(value, str):
67
+ return value.lower()
68
+ return value
62
69
 
63
70
 
64
71
  class Pet(BaseModel):
@@ -66,15 +73,15 @@ class Pet(BaseModel):
66
73
  Subclass of AccountData.
67
74
  """
68
75
 
69
- avatar: str | None = None
76
+ avatar: str
70
77
  created_at: int = Field(alias="createdAt")
71
78
  pet_id: int = Field(alias="petId")
72
- pet_name: str | None = Field(None, alias="petName")
79
+ pet_name: str = Field(alias="petName")
73
80
  id: int | None = None # Fictive field copied from id (for HA compatibility)
74
81
  sn: str | None = None # Fictive field copied from id (for HA compatibility)
75
82
  name: str | None = None # Fictive field copied from pet_name (for HA compatibility)
76
83
  firmware: str | None = None # Fictive fixed field (for HA compatibility)
77
- device_nfo: Device = Field(default_factory=Device)
84
+ device_nfo: Device | None = None # Device is now optional
78
85
 
79
86
  # Litter stats
80
87
  last_litter_usage: int = 0
@@ -291,7 +291,7 @@ class FeederRecord(BaseModel):
291
291
  if request_date is None:
292
292
  request_date = datetime.now().strftime("%Y%m%d")
293
293
 
294
- if device.device_type.lower() == D4:
294
+ if device.device_type == D4:
295
295
  return {"date": request_date, "type": 0, "deviceId": device.device_id}
296
296
  return {"days": request_date, "deviceId": device.device_id}
297
297
 
@@ -9,6 +9,7 @@ from pypetkitapi.const import (
9
9
  DEVICE_DATA,
10
10
  DEVICE_RECORDS,
11
11
  DEVICE_STATS,
12
+ T3,
12
13
  T4,
13
14
  T5,
14
15
  T6,
@@ -239,7 +240,7 @@ class LitterRecord(BaseModel):
239
240
  @classmethod
240
241
  def get_endpoint(cls, device_type: str) -> str:
241
242
  """Get the endpoint URL for the given device type."""
242
- if device_type == T4:
243
+ if device_type in [T3, T4]:
243
244
  return PetkitEndpoint.GET_DEVICE_RECORD
244
245
  if device_type in [T5, T6]:
245
246
  return PetkitEndpoint.GET_DEVICE_RECORD_RELEASE
@@ -253,11 +254,11 @@ class LitterRecord(BaseModel):
253
254
  request_date: str | None = None,
254
255
  ) -> dict:
255
256
  """Generate query parameters including request_date."""
256
- device_type = device.device_type.lower()
257
- if device_type == T4:
258
- if request_date is None:
259
- request_date = datetime.now().strftime("%Y%m%d")
260
- return {"date": int(request_date), "deviceId": device.device_id}
257
+ device_type = device.device_type
258
+ if device_type in [T3, T4]:
259
+ request_date = request_date or datetime.now().strftime("%Y%m%d")
260
+ key = "day" if device_type == T3 else "date"
261
+ return {key: int(request_date), "deviceId": device.device_id}
261
262
  if device_type in [T5, T6]:
262
263
  return {
263
264
  "timestamp": int(datetime.now().timestamp()),
@@ -164,6 +164,9 @@ class WaterFountain(BaseModel):
164
164
  water_pump_run_time: int | None = Field(None, alias="waterPumpRunTime")
165
165
  device_records: list[WaterFountainRecord] | None = None
166
166
  device_nfo: Device | None = None
167
+ is_connected: bool = False
168
+ ble_counter: int = 0
169
+ last_ble_poll: str | None = None
167
170
 
168
171
  @classmethod
169
172
  def get_endpoint(cls, device_type: str) -> str:
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
187
187
 
188
188
  [tool.poetry]
189
189
  name = "pypetkitapi"
190
- version = "1.8.1"
190
+ version = "1.9.1"
191
191
  description = "Python client for PetKit API"
192
192
  authors = ["Jezza34000 <info@mail.com>"]
193
193
  readme = "README.md"
@@ -208,7 +208,7 @@ ruff = "^0.8.1"
208
208
  types-aiofiles = "^24.1.0.20240626"
209
209
 
210
210
  [tool.bumpver]
211
- current_version = "1.8.1"
211
+ current_version = "1.9.1"
212
212
  version_pattern = "MAJOR.MINOR.PATCH"
213
213
  commit_message = "bump version {old_version} -> {new_version}"
214
214
  tag_message = "{new_version}"
File without changes