pypetkitapi 1.8.1__py3-none-any.whl → 1.9.0__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 +195 -9
- 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 +1 -1
- pypetkitapi/water_fountain_container.py +3 -0
- {pypetkitapi-1.8.1.dist-info → pypetkitapi-1.9.0.dist-info}/METADATA +9 -2
- pypetkitapi-1.9.0.dist-info/RECORD +16 -0
- {pypetkitapi-1.8.1.dist-info → pypetkitapi-1.9.0.dist-info}/WHEEL +1 -1
- pypetkitapi-1.8.1.dist-info/RECORD +0 -16
- {pypetkitapi-1.8.1.dist-info → pypetkitapi-1.9.0.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,
|
@@ -29,7 +34,14 @@ from pypetkitapi.const import (
|
|
29
34
|
PetkitDomain,
|
30
35
|
PetkitEndpoint,
|
31
36
|
)
|
32
|
-
from pypetkitapi.containers import
|
37
|
+
from pypetkitapi.containers import (
|
38
|
+
AccountData,
|
39
|
+
BleRelay,
|
40
|
+
Device,
|
41
|
+
Pet,
|
42
|
+
RegionInfo,
|
43
|
+
SessionInfo,
|
44
|
+
)
|
33
45
|
from pypetkitapi.exceptions import (
|
34
46
|
PetkitAuthenticationError,
|
35
47
|
PetkitAuthenticationUnregisteredEmailError,
|
@@ -201,7 +213,16 @@ class PetKitClient:
|
|
201
213
|
if account.pet_list:
|
202
214
|
for pet in account.pet_list:
|
203
215
|
self.petkit_entities[pet.pet_id] = pet
|
204
|
-
pet.device_nfo = Device(
|
216
|
+
pet.device_nfo = Device(
|
217
|
+
deviceType=PET,
|
218
|
+
deviceId=pet.pet_id,
|
219
|
+
createdAt=pet.created_at,
|
220
|
+
deviceName=pet.pet_name,
|
221
|
+
groupId=0,
|
222
|
+
type=0,
|
223
|
+
typeCode=0,
|
224
|
+
uniqueId=pet.sn,
|
225
|
+
)
|
205
226
|
|
206
227
|
async def get_devices_data(self) -> None:
|
207
228
|
"""Get the devices data from the PetKit servers."""
|
@@ -223,7 +244,7 @@ class PetKitClient:
|
|
223
244
|
_LOGGER.debug("Found %s devices", len(account.device_list))
|
224
245
|
|
225
246
|
for device in device_list:
|
226
|
-
device_type = device.device_type
|
247
|
+
device_type = device.device_type
|
227
248
|
|
228
249
|
if device_type in DEVICES_FEEDER:
|
229
250
|
main_tasks.append(self._fetch_device_data(device, Feeder))
|
@@ -259,7 +280,7 @@ class PetKitClient:
|
|
259
280
|
stats_tasks = [
|
260
281
|
self.populate_pet_stats(self.petkit_entities[device.device_id])
|
261
282
|
for device in device_list
|
262
|
-
if device.device_type
|
283
|
+
if device.device_type in DEVICES_LITTER_BOX
|
263
284
|
]
|
264
285
|
|
265
286
|
# Execute stats tasks
|
@@ -285,7 +306,7 @@ class PetKitClient:
|
|
285
306
|
],
|
286
307
|
) -> None:
|
287
308
|
"""Fetch the device data from the PetKit servers."""
|
288
|
-
device_type = device.device_type
|
309
|
+
device_type = device.device_type
|
289
310
|
|
290
311
|
_LOGGER.debug("Reading device type : %s (id=%s)", device_type, device.device_id)
|
291
312
|
|
@@ -404,6 +425,171 @@ class PetKitClient:
|
|
404
425
|
pet.last_duration_usage = self.get_safe_value(graph.toilet_time)
|
405
426
|
pet.last_device_used = "Purobot Ultra"
|
406
427
|
|
428
|
+
async def _get_fountain_instance(self, fountain_id: int) -> WaterFountain:
|
429
|
+
# Retrieve the water fountain object
|
430
|
+
water_fountain = self.petkit_entities.get(fountain_id)
|
431
|
+
if not water_fountain:
|
432
|
+
_LOGGER.error("Water fountain with ID %s not found.", fountain_id)
|
433
|
+
raise ValueError(f"Water fountain with ID {fountain_id} not found.")
|
434
|
+
return water_fountain
|
435
|
+
|
436
|
+
async def check_relay_availability(self, fountain_id: int) -> bool:
|
437
|
+
"""Check if BLE relay is available for the account."""
|
438
|
+
fountain = None
|
439
|
+
for account in self.account_data:
|
440
|
+
fountain = next(
|
441
|
+
(
|
442
|
+
device
|
443
|
+
for device in account.device_list
|
444
|
+
if device.device_id == fountain_id
|
445
|
+
),
|
446
|
+
None,
|
447
|
+
)
|
448
|
+
if fountain:
|
449
|
+
break
|
450
|
+
|
451
|
+
if not fountain:
|
452
|
+
raise ValueError(
|
453
|
+
f"Fountain with device_id {fountain_id} not found for the current account"
|
454
|
+
)
|
455
|
+
|
456
|
+
group_id = fountain.group_id
|
457
|
+
|
458
|
+
response = await self.req.request(
|
459
|
+
method=HTTPMethod.POST,
|
460
|
+
url=f"{PetkitEndpoint.BLE_AS_RELAY}",
|
461
|
+
params={"groupId": group_id},
|
462
|
+
headers=await self.get_session_id(),
|
463
|
+
)
|
464
|
+
ble_relays = [BleRelay(**relay) for relay in response]
|
465
|
+
|
466
|
+
if len(ble_relays) == 0:
|
467
|
+
_LOGGER.warning("No BLE relay devices found.")
|
468
|
+
return False
|
469
|
+
return True
|
470
|
+
|
471
|
+
async def open_ble_connection(self, fountain_id: int) -> bool:
|
472
|
+
"""Open a BLE connection to a PetKit device."""
|
473
|
+
_LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
|
474
|
+
water_fountain = await self._get_fountain_instance(fountain_id)
|
475
|
+
|
476
|
+
if await self.check_relay_availability(fountain_id) is False:
|
477
|
+
_LOGGER.error("BLE relay not available.")
|
478
|
+
return False
|
479
|
+
|
480
|
+
if water_fountain.is_connected is True:
|
481
|
+
_LOGGER.error("BLE connection already established.")
|
482
|
+
return True
|
483
|
+
|
484
|
+
response = await self.req.request(
|
485
|
+
method=HTTPMethod.POST,
|
486
|
+
url=PetkitEndpoint.BLE_CONNECT,
|
487
|
+
data={
|
488
|
+
"bleId": fountain_id,
|
489
|
+
"type": 24,
|
490
|
+
"mac": water_fountain.mac,
|
491
|
+
},
|
492
|
+
headers=await self.get_session_id(),
|
493
|
+
)
|
494
|
+
if response != {"state": 1}:
|
495
|
+
_LOGGER.error("Failed to establish BLE connection.")
|
496
|
+
water_fountain.is_connected = False
|
497
|
+
return False
|
498
|
+
|
499
|
+
for attempt in range(BLE_CONNECT_ATTEMPT):
|
500
|
+
_LOGGER.warning("BLE connection attempt n%s", attempt)
|
501
|
+
response = await self.req.request(
|
502
|
+
method=HTTPMethod.POST,
|
503
|
+
url=PetkitEndpoint.BLE_POLL,
|
504
|
+
data={
|
505
|
+
"bleId": fountain_id,
|
506
|
+
"type": 24,
|
507
|
+
"mac": water_fountain.mac,
|
508
|
+
},
|
509
|
+
headers=await self.get_session_id(),
|
510
|
+
)
|
511
|
+
if response == 1:
|
512
|
+
_LOGGER.info("BLE connection established successfully.")
|
513
|
+
water_fountain.is_connected = True
|
514
|
+
water_fountain.last_ble_poll = datetime.now().strftime(
|
515
|
+
"%Y-%m-%dT%H:%M:%S.%f"
|
516
|
+
)
|
517
|
+
return True
|
518
|
+
await asyncio.sleep(4)
|
519
|
+
_LOGGER.error("Failed to establish BLE connection after multiple attempts.")
|
520
|
+
water_fountain.is_connected = False
|
521
|
+
return False
|
522
|
+
|
523
|
+
async def close_ble_connection(self, fountain_id: int) -> None:
|
524
|
+
"""Close the BLE connection to a PetKit device."""
|
525
|
+
_LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
|
526
|
+
water_fountain = await self._get_fountain_instance(fountain_id)
|
527
|
+
|
528
|
+
await self.req.request(
|
529
|
+
method=HTTPMethod.POST,
|
530
|
+
url=PetkitEndpoint.BLE_CANCEL,
|
531
|
+
data={
|
532
|
+
"bleId": fountain_id,
|
533
|
+
"type": 24,
|
534
|
+
"mac": water_fountain.mac,
|
535
|
+
},
|
536
|
+
headers=await self.get_session_id(),
|
537
|
+
)
|
538
|
+
_LOGGER.info("BLE connection closed successfully.")
|
539
|
+
|
540
|
+
async def get_ble_cmd_data(
|
541
|
+
self, fountain_command: list, counter: int
|
542
|
+
) -> tuple[int, str]:
|
543
|
+
"""Prepare BLE data by adding start and end trame to the command and extracting the first number."""
|
544
|
+
cmd_code = fountain_command[0]
|
545
|
+
modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
|
546
|
+
ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
|
547
|
+
encoded_data = await self._encode_ble_data(ble_data)
|
548
|
+
return cmd_code, encoded_data
|
549
|
+
|
550
|
+
@staticmethod
|
551
|
+
async def _encode_ble_data(byte_list: list) -> str:
|
552
|
+
"""Encode a list of bytes to a URL encoded base64 string."""
|
553
|
+
byte_array = bytearray(byte_list)
|
554
|
+
b64_encoded = base64.b64encode(byte_array)
|
555
|
+
return urllib.parse.quote(b64_encoded)
|
556
|
+
|
557
|
+
async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
|
558
|
+
"""BLE command to a PetKit device."""
|
559
|
+
_LOGGER.info("Sending BLE command to fountain %s", fountain_id)
|
560
|
+
water_fountain = await self._get_fountain_instance(fountain_id)
|
561
|
+
|
562
|
+
if water_fountain.is_connected is False:
|
563
|
+
_LOGGER.error("BLE connection not established.")
|
564
|
+
return False
|
565
|
+
|
566
|
+
command = FOUNTAIN_COMMAND.get[command, None]
|
567
|
+
if command is None:
|
568
|
+
_LOGGER.error("Command not found.")
|
569
|
+
return False
|
570
|
+
|
571
|
+
cmd_code, cmd_data = await self.get_ble_cmd_data(
|
572
|
+
command, water_fountain.ble_counter
|
573
|
+
)
|
574
|
+
|
575
|
+
response = await self.req.request(
|
576
|
+
method=HTTPMethod.POST,
|
577
|
+
url=PetkitEndpoint.BLE_CONTROL_DEVICE,
|
578
|
+
data={
|
579
|
+
"bleId": water_fountain.id,
|
580
|
+
"cmd": cmd_code,
|
581
|
+
"data": cmd_data,
|
582
|
+
"mac": water_fountain.mac,
|
583
|
+
"type": 24,
|
584
|
+
},
|
585
|
+
headers=await self.get_session_id(),
|
586
|
+
)
|
587
|
+
if response != 1:
|
588
|
+
_LOGGER.error("Failed to send BLE command.")
|
589
|
+
return False
|
590
|
+
_LOGGER.info("BLE command sent successfully.")
|
591
|
+
return True
|
592
|
+
|
407
593
|
async def send_api_request(
|
408
594
|
self,
|
409
595
|
device_id: int,
|
@@ -413,7 +599,7 @@ class PetKitClient:
|
|
413
599
|
"""Control the device using the PetKit API."""
|
414
600
|
await self.validate_session()
|
415
601
|
|
416
|
-
device = self.petkit_entities.get(device_id)
|
602
|
+
device = self.petkit_entities.get(device_id, None)
|
417
603
|
if not device:
|
418
604
|
raise PypetkitError(f"Device with ID {device_id} not found.")
|
419
605
|
|
@@ -427,7 +613,7 @@ class PetKitClient:
|
|
427
613
|
|
428
614
|
# Check if the device type is supported
|
429
615
|
if device.device_nfo.device_type:
|
430
|
-
device_type = device.device_nfo.device_type
|
616
|
+
device_type = device.device_nfo.device_type
|
431
617
|
else:
|
432
618
|
raise PypetkitError(
|
433
619
|
"Device type is not available, and is mandatory for sending commands."
|
@@ -452,7 +638,7 @@ class PetKitClient:
|
|
452
638
|
else:
|
453
639
|
endpoint = action_info.endpoint
|
454
640
|
_LOGGER.debug("Endpoint field")
|
455
|
-
url = f"{device.device_nfo.device_type
|
641
|
+
url = f"{device.device_nfo.device_type}/{endpoint}"
|
456
642
|
|
457
643
|
# Get the parameters
|
458
644
|
if setting is not None:
|
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
@@ -253,7 +253,7 @@ class LitterRecord(BaseModel):
|
|
253
253
|
request_date: str | None = None,
|
254
254
|
) -> dict:
|
255
255
|
"""Generate query parameters including request_date."""
|
256
|
-
device_type = device.device_type
|
256
|
+
device_type = device.device_type
|
257
257
|
if device_type == T4:
|
258
258
|
if request_date is None:
|
259
259
|
request_date = datetime.now().strftime("%Y%m%d")
|
@@ -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.0
|
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=FSbNIvn4PuEEWbVnJ2rQEQkyOZgCYiXVAgrWqokbnVg,1562
|
2
|
+
pypetkitapi/client.py,sha256=J7d8lgrCyR1fJPEB9zUTu_z7HVDp_ALgD_qihu9KAYo,27630
|
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=kVbPddHV7wjaeBbRIsigHn_-G42qLZyFUXJg5U5PcQ0,19061
|
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.0.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
14
|
+
pypetkitapi-1.9.0.dist-info/METADATA,sha256=ga5bwlaFeRelb8zt8nUMGpSt6X8Uh3mvnbbJwjYteEU,5167
|
15
|
+
pypetkitapi-1.9.0.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
16
|
+
pypetkitapi-1.9.0.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
|