pypetkitapi 1.8.1__py3-none-any.whl → 1.9.1__py3-none-any.whl
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.
- pypetkitapi/__init__.py +1 -1
- pypetkitapi/client.py +217 -26
- pypetkitapi/command.py +25 -37
- pypetkitapi/const.py +7 -1
- pypetkitapi/containers.py +18 -11
- pypetkitapi/feeder_container.py +1 -1
- pypetkitapi/litter_container.py +7 -6
- pypetkitapi/water_fountain_container.py +3 -0
- {pypetkitapi-1.8.1.dist-info → pypetkitapi-1.9.1.dist-info}/METADATA +9 -2
- pypetkitapi-1.9.1.dist-info/RECORD +16 -0
- {pypetkitapi-1.8.1.dist-info → pypetkitapi-1.9.1.dist-info}/WHEEL +1 -1
- pypetkitapi-1.8.1.dist-info/RECORD +0 -16
- {pypetkitapi-1.8.1.dist-info → pypetkitapi-1.9.1.dist-info}/LICENSE +0 -0
pypetkitapi/__init__.py
CHANGED
pypetkitapi/client.py
CHANGED
@@ -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
|
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(
|
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
|
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
|
261
|
+
if device_type in [T3, T4]:
|
239
262
|
record_tasks.append(self._fetch_device_data(device, LitterStats))
|
240
|
-
if device_type
|
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
|
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
|
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
|
356
|
+
if device_type in [T3, T4]:
|
334
357
|
self.petkit_entities[device.device_id].device_stats = device_data
|
335
|
-
if device_type
|
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
|
369
|
-
|
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
|
397
|
+
stats_data.device_nfo.device_type in [T5, T6]
|
372
398
|
and stats_data.device_pet_graph_out
|
373
399
|
):
|
374
|
-
await self.
|
400
|
+
await self._process_t5_t6(pet, stats_data)
|
375
401
|
|
376
|
-
async def
|
377
|
-
"""Process T4
|
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 =
|
417
|
+
pet.last_device_used = device_records.device_nfo.device_name
|
392
418
|
|
393
|
-
async def
|
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 =
|
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
|
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
|
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
|
pypetkitapi/command.py
CHANGED
@@ -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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
122
|
-
FountainAction.
|
123
|
-
FountainAction.
|
124
|
-
FountainAction.
|
125
|
-
FountainAction.
|
126
|
-
FountainAction.
|
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
|
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
|
}
|
pypetkitapi/const.py
CHANGED
@@ -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
|
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"
|
pypetkitapi/containers.py
CHANGED
@@ -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
|
55
|
-
device_id: int
|
56
|
-
device_name: str
|
57
|
-
device_type: str
|
58
|
-
group_id: int
|
59
|
-
type: int
|
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
|
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
|
76
|
+
avatar: str
|
70
77
|
created_at: int = Field(alias="createdAt")
|
71
78
|
pet_id: int = Field(alias="petId")
|
72
|
-
pet_name: str
|
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 =
|
84
|
+
device_nfo: Device | None = None # Device is now optional
|
78
85
|
|
79
86
|
# Litter stats
|
80
87
|
last_litter_usage: int = 0
|
pypetkitapi/feeder_container.py
CHANGED
@@ -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
|
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
|
|
pypetkitapi/litter_container.py
CHANGED
@@ -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
|
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
|
257
|
-
if device_type
|
258
|
-
|
259
|
-
|
260
|
-
return {
|
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:
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: pypetkitapi
|
3
|
-
Version: 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.
|
@@ -0,0 +1,16 @@
|
|
1
|
+
pypetkitapi/__init__.py,sha256=MLvrbBarlxdGzxkVY1NW80hTgkSy9fYe5_cuy1mnHbQ,1562
|
2
|
+
pypetkitapi/client.py,sha256=o8dBNxdupWwf7AIt6GB4Jc4SLExc0Zv1E-eX2Qjt5FY,27807
|
3
|
+
pypetkitapi/command.py,sha256=G7AEtUcaK-lcRliNf4oUxPkvDO_GNBkJ-ZUcOo7DGHM,7697
|
4
|
+
pypetkitapi/const.py,sha256=pkTJ0l-8mQix9aoJNC2UYfyUdG7ie826xnv7EkOZtPw,4208
|
5
|
+
pypetkitapi/containers.py,sha256=oJR22ZruMr-0IRgiucdnj_nutOH59MKvmaFTwLJNiJI,4635
|
6
|
+
pypetkitapi/exceptions.py,sha256=fuTLT6Iw2_kA7eOyNJPf59vQkgfByhAnTThY4lC0Rt0,1283
|
7
|
+
pypetkitapi/feeder_container.py,sha256=q9lsUvBMxVt-vs8gE3Gg7jVyiIz_eh-3Vq-vMhDIzGA,14472
|
8
|
+
pypetkitapi/litter_container.py,sha256=qKP3XFUkbzLREZPXEMDvpR1sqo6BI560O6eJYdkrX7w,19110
|
9
|
+
pypetkitapi/medias.py,sha256=IuWkC7usw0Hbx173X8TGv24jOp4nqv6bIUosZBpXMGg,6945
|
10
|
+
pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
|
11
|
+
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
pypetkitapi/water_fountain_container.py,sha256=5J0b-fDZYcFLNX2El7fifv8H6JMhBCt-ttxSow1ozRQ,6787
|
13
|
+
pypetkitapi-1.9.1.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
14
|
+
pypetkitapi-1.9.1.dist-info/METADATA,sha256=qp69EJwHbxvvY3Ve870iLQ2mmWBUwz6ePPTfn5YpALw,5167
|
15
|
+
pypetkitapi-1.9.1.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
16
|
+
pypetkitapi-1.9.1.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
pypetkitapi/__init__.py,sha256=qV8NZZeCMyY22gFOav0uic4QiORt9TsenDZkfC0aBqY,1562
|
2
|
-
pypetkitapi/client.py,sha256=_iR45tRH59FEQ_Ra1RAMAyTDvpzSLLCNtiKu1yJSW24,20886
|
3
|
-
pypetkitapi/command.py,sha256=ONP2BBdnb1nGgAK0xz50GiLO3KPzuQJD1BjqAhblkAs,8165
|
4
|
-
pypetkitapi/const.py,sha256=q0HCUoryVyNzuH8erMrTqAR8yW4vEiPE79TTbhKNIEM,4076
|
5
|
-
pypetkitapi/containers.py,sha256=9fJVXG-F0lTGcfYyz0WF9xTph5FCQJbqS8LHE5MpZhE,4411
|
6
|
-
pypetkitapi/exceptions.py,sha256=fuTLT6Iw2_kA7eOyNJPf59vQkgfByhAnTThY4lC0Rt0,1283
|
7
|
-
pypetkitapi/feeder_container.py,sha256=l8SbimdfWhmKcHYINw9_3-jXo9Rxy1K_dV3eSnn7wyk,14480
|
8
|
-
pypetkitapi/litter_container.py,sha256=UkF3eAV4ehPcCM0eBRZR1ZNc7foHJTnniLW7HZK--oU,19069
|
9
|
-
pypetkitapi/medias.py,sha256=IuWkC7usw0Hbx173X8TGv24jOp4nqv6bIUosZBpXMGg,6945
|
10
|
-
pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
|
11
|
-
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
-
pypetkitapi/water_fountain_container.py,sha256=_5K2aTpcqHG-VBu7J2vXSZuAjA_SK6DkgV14u5RkFZA,6694
|
13
|
-
pypetkitapi-1.8.1.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
14
|
-
pypetkitapi-1.8.1.dist-info/METADATA,sha256=b1aaD3dPzfW1ftitW2ApBwETN2IAJ8aWRzJnWrfy4PU,4882
|
15
|
-
pypetkitapi-1.8.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
16
|
-
pypetkitapi-1.8.1.dist-info/RECORD,,
|
File without changes
|