pypetkitapi 1.9.2__py3-none-any.whl → 1.9.3__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 +140 -71
- pypetkitapi/const.py +4 -7
- pypetkitapi/feeder_container.py +5 -6
- pypetkitapi/litter_container.py +7 -8
- pypetkitapi/medias.py +9 -9
- pypetkitapi/utils.py +22 -0
- {pypetkitapi-1.9.2.dist-info → pypetkitapi-1.9.3.dist-info}/LICENSE +1 -1
- {pypetkitapi-1.9.2.dist-info → pypetkitapi-1.9.3.dist-info}/METADATA +7 -1
- pypetkitapi-1.9.3.dist-info/RECORD +17 -0
- pypetkitapi-1.9.2.dist-info/RECORD +0 -16
- {pypetkitapi-1.9.2.dist-info → pypetkitapi-1.9.3.dist-info}/WHEEL +0 -0
pypetkitapi/__init__.py
CHANGED
pypetkitapi/client.py
CHANGED
@@ -17,6 +17,7 @@ from pypetkitapi.const import (
|
|
17
17
|
BLE_CONNECT_ATTEMPT,
|
18
18
|
BLE_END_TRAME,
|
19
19
|
BLE_START_TRAME,
|
20
|
+
CLIENT_NFO,
|
20
21
|
DEVICE_DATA,
|
21
22
|
DEVICE_RECORDS,
|
22
23
|
DEVICE_STATS,
|
@@ -25,6 +26,8 @@ from pypetkitapi.const import (
|
|
25
26
|
DEVICES_PURIFIER,
|
26
27
|
DEVICES_WATER_FOUNTAIN,
|
27
28
|
ERR_KEY,
|
29
|
+
LITTER_NO_CAMERA,
|
30
|
+
LITTER_WITH_CAMERA,
|
28
31
|
LOGIN_DATA,
|
29
32
|
PET,
|
30
33
|
RES_KEY,
|
@@ -57,8 +60,22 @@ from pypetkitapi.exceptions import (
|
|
57
60
|
from pypetkitapi.feeder_container import Feeder, FeederRecord
|
58
61
|
from pypetkitapi.litter_container import Litter, LitterRecord, LitterStats, PetOutGraph
|
59
62
|
from pypetkitapi.purifier_container import Purifier
|
63
|
+
from pypetkitapi.utils import get_timezone_offset
|
60
64
|
from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
|
61
65
|
|
66
|
+
data_handlers = {}
|
67
|
+
|
68
|
+
|
69
|
+
def data_handler(data_type):
|
70
|
+
"""Register a data handler for a specific data type."""
|
71
|
+
|
72
|
+
def wrapper(func):
|
73
|
+
data_handlers[data_type] = func
|
74
|
+
return func
|
75
|
+
|
76
|
+
return wrapper
|
77
|
+
|
78
|
+
|
62
79
|
_LOGGER = logging.getLogger(__name__)
|
63
80
|
|
64
81
|
|
@@ -86,7 +103,9 @@ class PetKitClient:
|
|
86
103
|
self.petkit_entities = {}
|
87
104
|
self.aiohttp_session = session or aiohttp.ClientSession()
|
88
105
|
self.req = PrepReq(
|
89
|
-
base_url=PetkitDomain.PASSPORT_PETKIT,
|
106
|
+
base_url=PetkitDomain.PASSPORT_PETKIT,
|
107
|
+
session=self.aiohttp_session,
|
108
|
+
timezone=self.timezone,
|
90
109
|
)
|
91
110
|
|
92
111
|
async def _get_base_url(self) -> None:
|
@@ -134,7 +153,12 @@ class PetKitClient:
|
|
134
153
|
_LOGGER.info("Logging in to PetKit server")
|
135
154
|
|
136
155
|
# Prepare the data to send
|
156
|
+
client_nfo = CLIENT_NFO.copy()
|
157
|
+
client_nfo["timezoneId"] = self.timezone
|
158
|
+
client_nfo["timezone"] = get_timezone_offset(self.timezone)
|
159
|
+
|
137
160
|
data = LOGIN_DATA.copy()
|
161
|
+
data["client"] = str(client_nfo)
|
138
162
|
data["encrypt"] = "1"
|
139
163
|
data["region"] = self.region
|
140
164
|
data["username"] = self.username
|
@@ -223,7 +247,7 @@ class PetKitClient:
|
|
223
247
|
groupId=0,
|
224
248
|
type=0,
|
225
249
|
typeCode=0,
|
226
|
-
uniqueId=pet.sn,
|
250
|
+
uniqueId=str(pet.sn),
|
227
251
|
)
|
228
252
|
|
229
253
|
async def get_devices_data(self) -> None:
|
@@ -234,16 +258,31 @@ class PetKitClient:
|
|
234
258
|
if not self.account_data:
|
235
259
|
await self._get_account_data()
|
236
260
|
|
237
|
-
|
238
|
-
record_tasks =
|
239
|
-
|
261
|
+
device_list = self._collect_devices()
|
262
|
+
main_tasks, record_tasks = self._prepare_tasks(device_list)
|
263
|
+
|
264
|
+
await asyncio.gather(*main_tasks)
|
265
|
+
await asyncio.gather(*record_tasks)
|
266
|
+
await self._execute_stats_tasks()
|
240
267
|
|
268
|
+
end_time = datetime.now()
|
269
|
+
_LOGGER.debug("Petkit data fetched successfully in: %s", end_time - start_time)
|
270
|
+
|
271
|
+
def _collect_devices(self) -> list[Device]:
|
272
|
+
"""Collect all devices from account data."""
|
273
|
+
device_list = []
|
241
274
|
for account in self.account_data:
|
242
275
|
_LOGGER.debug("List devices data for account: %s", account)
|
243
276
|
if account.device_list:
|
244
277
|
_LOGGER.debug("Devices in account: %s", account.device_list)
|
245
278
|
device_list.extend(account.device_list)
|
246
279
|
_LOGGER.debug("Found %s devices", len(account.device_list))
|
280
|
+
return device_list
|
281
|
+
|
282
|
+
def _prepare_tasks(self, device_list: list[Device]) -> tuple[list, list]:
|
283
|
+
"""Prepare main and record tasks based on device types."""
|
284
|
+
main_tasks = []
|
285
|
+
record_tasks = []
|
247
286
|
|
248
287
|
for device in device_list:
|
249
288
|
device_type = device.device_type
|
@@ -253,15 +292,9 @@ class PetKitClient:
|
|
253
292
|
record_tasks.append(self._fetch_device_data(device, FeederRecord))
|
254
293
|
|
255
294
|
elif device_type in DEVICES_LITTER_BOX:
|
256
|
-
main_tasks.append(
|
257
|
-
self._fetch_device_data(device, Litter),
|
258
|
-
)
|
295
|
+
main_tasks.append(self._fetch_device_data(device, Litter))
|
259
296
|
record_tasks.append(self._fetch_device_data(device, LitterRecord))
|
260
|
-
|
261
|
-
if device_type in [T3, T4]:
|
262
|
-
record_tasks.append(self._fetch_device_data(device, LitterStats))
|
263
|
-
if device_type in [T5, T6]:
|
264
|
-
record_tasks.append(self._fetch_device_data(device, PetOutGraph))
|
297
|
+
self._add_litter_box_tasks(record_tasks, device_type, device)
|
265
298
|
|
266
299
|
elif device_type in DEVICES_WATER_FOUNTAIN:
|
267
300
|
main_tasks.append(self._fetch_device_data(device, WaterFountain))
|
@@ -272,26 +305,26 @@ class PetKitClient:
|
|
272
305
|
elif device_type in DEVICES_PURIFIER:
|
273
306
|
main_tasks.append(self._fetch_device_data(device, Purifier))
|
274
307
|
|
275
|
-
|
276
|
-
await asyncio.gather(*main_tasks)
|
308
|
+
return main_tasks, record_tasks
|
277
309
|
|
278
|
-
|
279
|
-
|
310
|
+
def _add_litter_box_tasks(
|
311
|
+
self, record_tasks: list, device_type: str, device: Device
|
312
|
+
):
|
313
|
+
"""Add specific tasks for litter box devices."""
|
314
|
+
if device_type in [T3, T4]:
|
315
|
+
record_tasks.append(self._fetch_device_data(device, LitterStats))
|
316
|
+
if device_type in [T5, T6]:
|
317
|
+
record_tasks.append(self._fetch_device_data(device, PetOutGraph))
|
280
318
|
|
281
|
-
|
319
|
+
async def _execute_stats_tasks(self) -> None:
|
320
|
+
"""Execute tasks to populate pet stats."""
|
282
321
|
stats_tasks = [
|
283
|
-
self.populate_pet_stats(
|
284
|
-
for
|
285
|
-
if
|
322
|
+
self.populate_pet_stats(entity)
|
323
|
+
for device_id, entity in self.petkit_entities.items()
|
324
|
+
if isinstance(entity, Litter)
|
286
325
|
]
|
287
|
-
|
288
|
-
# Execute stats tasks
|
289
326
|
await asyncio.gather(*stats_tasks)
|
290
327
|
|
291
|
-
end_time = datetime.now()
|
292
|
-
total_time = end_time - start_time
|
293
|
-
_LOGGER.debug("Petkit data fetched successfully in: %s", total_time)
|
294
|
-
|
295
328
|
async def _fetch_device_data(
|
296
329
|
self,
|
297
330
|
device: Device,
|
@@ -345,23 +378,48 @@ class PetKitClient:
|
|
345
378
|
_LOGGER.error("Unexpected response type: %s", type(response))
|
346
379
|
return
|
347
380
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
381
|
+
# Dispatch to the appropriate handler
|
382
|
+
handler = data_handlers.get(data_class.data_type)
|
383
|
+
if handler:
|
384
|
+
await handler(self, device, device_data, device_type)
|
385
|
+
else:
|
386
|
+
_LOGGER.error("Unknown data type: %s", data_class.data_type)
|
387
|
+
|
388
|
+
@data_handler(DEVICE_DATA)
|
389
|
+
async def _handle_device_data(self, device, device_data, device_type):
|
390
|
+
"""Handle device data."""
|
391
|
+
self.petkit_entities[device.device_id] = device_data
|
392
|
+
self.petkit_entities[device.device_id].device_nfo = device
|
393
|
+
_LOGGER.debug("Device data fetched OK for %s", device_type)
|
394
|
+
|
395
|
+
@data_handler(DEVICE_RECORDS)
|
396
|
+
async def _handle_device_records(self, device, device_data, device_type):
|
397
|
+
"""Handle device records."""
|
398
|
+
entity = self.petkit_entities.get(device.device_id)
|
399
|
+
if entity and isinstance(entity, (Feeder, Litter, WaterFountain)):
|
400
|
+
entity.device_records = device_data
|
354
401
|
_LOGGER.debug("Device records fetched OK for %s", device_type)
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
402
|
+
else:
|
403
|
+
_LOGGER.warning(
|
404
|
+
"Cannot assign device_records to entity of type %s",
|
405
|
+
type(entity),
|
406
|
+
)
|
407
|
+
|
408
|
+
@data_handler(DEVICE_STATS)
|
409
|
+
async def _handle_device_stats(self, device, device_data, device_type):
|
410
|
+
"""Handle device stats."""
|
411
|
+
entity = self.petkit_entities.get(device.device_id)
|
412
|
+
if isinstance(entity, Litter):
|
413
|
+
if device_type in LITTER_NO_CAMERA:
|
414
|
+
entity.device_stats = device_data
|
415
|
+
if device_type in LITTER_WITH_CAMERA:
|
416
|
+
entity.device_pet_graph_out = device_data
|
362
417
|
_LOGGER.debug("Device stats fetched OK for %s", device_type)
|
363
418
|
else:
|
364
|
-
_LOGGER.
|
419
|
+
_LOGGER.warning(
|
420
|
+
"Cannot assign device_stats or device_pet_graph_out to entity of type %s",
|
421
|
+
type(entity),
|
422
|
+
)
|
365
423
|
|
366
424
|
async def get_pets_list(self) -> list[Pet]:
|
367
425
|
"""Extract and return the list of pets."""
|
@@ -386,6 +444,12 @@ class PetKitClient:
|
|
386
444
|
async def populate_pet_stats(self, stats_data: Litter) -> None:
|
387
445
|
"""Collect data from litter data to populate pet stats."""
|
388
446
|
|
447
|
+
if not stats_data.device_nfo:
|
448
|
+
_LOGGER.warning(
|
449
|
+
"No device info for %s can't populate pet infos", stats_data
|
450
|
+
)
|
451
|
+
return
|
452
|
+
|
389
453
|
pets_list = await self.get_pets_list()
|
390
454
|
for pet in pets_list:
|
391
455
|
if (
|
@@ -433,25 +497,27 @@ class PetKitClient:
|
|
433
497
|
async def _get_fountain_instance(self, fountain_id: int) -> WaterFountain:
|
434
498
|
# Retrieve the water fountain object
|
435
499
|
water_fountain = self.petkit_entities.get(fountain_id)
|
436
|
-
if not water_fountain:
|
500
|
+
if not isinstance(water_fountain, WaterFountain):
|
437
501
|
_LOGGER.error("Water fountain with ID %s not found.", fountain_id)
|
438
|
-
raise
|
502
|
+
raise TypeError(f"Water fountain with ID {fountain_id} not found.")
|
439
503
|
return water_fountain
|
440
504
|
|
441
505
|
async def check_relay_availability(self, fountain_id: int) -> bool:
|
442
506
|
"""Check if BLE relay is available for the account."""
|
443
507
|
fountain = None
|
508
|
+
|
444
509
|
for account in self.account_data:
|
445
|
-
|
446
|
-
(
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
510
|
+
if account.device_list:
|
511
|
+
fountain = next(
|
512
|
+
(
|
513
|
+
device
|
514
|
+
for device in account.device_list
|
515
|
+
if device.device_id == fountain_id
|
516
|
+
),
|
517
|
+
None,
|
518
|
+
)
|
519
|
+
if fountain:
|
520
|
+
break
|
455
521
|
|
456
522
|
if not fountain:
|
457
523
|
raise ValueError(
|
@@ -568,13 +634,13 @@ class PetKitClient:
|
|
568
634
|
_LOGGER.error("BLE connection not established.")
|
569
635
|
return False
|
570
636
|
|
571
|
-
|
572
|
-
if
|
637
|
+
command_data = FOUNTAIN_COMMAND.get(command)
|
638
|
+
if command_data is None:
|
573
639
|
_LOGGER.error("Command not found.")
|
574
640
|
return False
|
575
641
|
|
576
642
|
cmd_code, cmd_data = await self.get_ble_cmd_data(
|
577
|
-
|
643
|
+
list(command_data), water_fountain.ble_counter
|
578
644
|
)
|
579
645
|
|
580
646
|
response = await self.req.request(
|
@@ -607,6 +673,8 @@ class PetKitClient:
|
|
607
673
|
device = self.petkit_entities.get(device_id, None)
|
608
674
|
if not device:
|
609
675
|
raise PypetkitError(f"Device with ID {device_id} not found.")
|
676
|
+
if device.device_nfo is None:
|
677
|
+
raise PypetkitError(f"Device with ID {device_id} has no device_nfo.")
|
610
678
|
|
611
679
|
_LOGGER.debug(
|
612
680
|
"Control API device=%s id=%s action=%s param=%s",
|
@@ -669,29 +737,30 @@ class PetKitClient:
|
|
669
737
|
class PrepReq:
|
670
738
|
"""Prepare the request to the PetKit API."""
|
671
739
|
|
672
|
-
def __init__(
|
740
|
+
def __init__(
|
741
|
+
self, base_url: str, session: aiohttp.ClientSession, timezone: str
|
742
|
+
) -> None:
|
673
743
|
"""Initialize the request."""
|
674
744
|
self.base_url = base_url
|
675
745
|
self.session = session
|
746
|
+
self.timezone = timezone
|
676
747
|
self.base_headers = self._generate_header()
|
677
748
|
|
678
|
-
|
679
|
-
def _generate_header() -> dict[str, str]:
|
749
|
+
def _generate_header(self) -> dict[str, str]:
|
680
750
|
"""Create header for interaction with API endpoint."""
|
681
|
-
|
682
751
|
return {
|
683
752
|
"Accept": Header.ACCEPT.value,
|
684
|
-
"Accept-Language": Header.ACCEPT_LANG,
|
685
|
-
"Accept-Encoding": Header.ENCODING,
|
686
|
-
"Content-Type": Header.CONTENT_TYPE,
|
687
|
-
"User-Agent": Header.AGENT,
|
688
|
-
"X-Img-Version": Header.IMG_VERSION,
|
689
|
-
"X-Locale": Header.LOCALE,
|
690
|
-
"X-Client": Header.CLIENT,
|
691
|
-
"X-Hour": Header.HOUR,
|
692
|
-
"X-TimezoneId":
|
693
|
-
"X-Api-Version": Header.API_VERSION,
|
694
|
-
"X-Timezone":
|
753
|
+
"Accept-Language": Header.ACCEPT_LANG.value,
|
754
|
+
"Accept-Encoding": Header.ENCODING.value,
|
755
|
+
"Content-Type": Header.CONTENT_TYPE.value,
|
756
|
+
"User-Agent": Header.AGENT.value,
|
757
|
+
"X-Img-Version": Header.IMG_VERSION.value,
|
758
|
+
"X-Locale": Header.LOCALE.value,
|
759
|
+
"X-Client": Header.CLIENT.value,
|
760
|
+
"X-Hour": Header.HOUR.value,
|
761
|
+
"X-TimezoneId": self.timezone,
|
762
|
+
"X-Api-Version": Header.API_VERSION.value,
|
763
|
+
"X-Timezone": get_timezone_offset(self.timezone),
|
695
764
|
}
|
696
765
|
|
697
766
|
async def request(
|
pypetkitapi/const.py
CHANGED
@@ -35,6 +35,8 @@ K3 = "k3"
|
|
35
35
|
PET = "pet"
|
36
36
|
|
37
37
|
DEVICES_LITTER_BOX = [T3, T4, T5, T6]
|
38
|
+
LITTER_WITH_CAMERA = [T5, T6]
|
39
|
+
LITTER_NO_CAMERA = [T3, T4]
|
38
40
|
DEVICES_FEEDER = [FEEDER, FEEDER_MINI, D3, D4, D4S, D4H, D4SH]
|
39
41
|
DEVICES_WATER_FOUNTAIN = [W5, CTW3]
|
40
42
|
DEVICES_PURIFIER = [K2]
|
@@ -68,12 +70,10 @@ class Header(StrEnum):
|
|
68
70
|
ACCEPT = "*/*"
|
69
71
|
ACCEPT_LANG = "en-US;q=1, it-US;q=0.9"
|
70
72
|
ENCODING = "gzip, deflate"
|
71
|
-
API_VERSION = "11.
|
73
|
+
API_VERSION = "11.4.0"
|
72
74
|
CONTENT_TYPE = "application/x-www-form-urlencoded"
|
73
75
|
AGENT = "okhttp/3.12.11"
|
74
76
|
CLIENT = f"{Client.PLATFORM_TYPE}({Client.OS_VERSION};{Client.MODEL_NAME})"
|
75
|
-
TIMEZONE = "1.0"
|
76
|
-
TIMEZONE_ID = "Europe/Paris" # TODO: Make this dynamic
|
77
77
|
LOCALE = "en-US"
|
78
78
|
IMG_VERSION = "1.0"
|
79
79
|
HOUR = "24"
|
@@ -85,14 +85,11 @@ CLIENT_NFO = {
|
|
85
85
|
"osVersion": Client.OS_VERSION.value,
|
86
86
|
"platform": Client.PLATFORM_TYPE.value,
|
87
87
|
"source": Client.SOURCE.value,
|
88
|
-
"timezone": Header.TIMEZONE.value, # TODO: Make this dynamic
|
89
|
-
"timezoneId": Header.TIMEZONE_ID.value,
|
90
88
|
"version": Header.API_VERSION.value,
|
91
89
|
}
|
92
90
|
|
93
91
|
LOGIN_DATA = {
|
94
|
-
"
|
95
|
-
"oldVersion": Header.API_VERSION,
|
92
|
+
"oldVersion": Header.API_VERSION.value,
|
96
93
|
}
|
97
94
|
|
98
95
|
|
pypetkitapi/feeder_container.py
CHANGED
@@ -49,7 +49,7 @@ class CameraMultiNew(BaseModel):
|
|
49
49
|
|
50
50
|
enable: int | None = None
|
51
51
|
rpt: str | None = None
|
52
|
-
time: list[
|
52
|
+
time: list[list[int]] | None = None
|
53
53
|
|
54
54
|
|
55
55
|
class SettingsFeeder(BaseModel):
|
@@ -85,9 +85,7 @@ class SettingsFeeder(BaseModel):
|
|
85
85
|
highlight: int | None = None
|
86
86
|
light_config: int | None = Field(None, alias="lightConfig")
|
87
87
|
light_mode: int | None = Field(None, alias="lightMode")
|
88
|
-
light_multi_range: list[
|
89
|
-
None, alias="lightMultiRange"
|
90
|
-
)
|
88
|
+
light_multi_range: list[list[int]] | None = Field(None, alias="lightMultiRange")
|
91
89
|
live_encrypt: int | None = Field(None, alias="liveEncrypt")
|
92
90
|
low_battery_notify: int | None = Field(None, alias="lowBatteryNotify")
|
93
91
|
manual_lock: int | None = Field(None, alias="manualLock")
|
@@ -112,7 +110,7 @@ class SettingsFeeder(BaseModel):
|
|
112
110
|
time_display: int | None = Field(None, alias="timeDisplay")
|
113
111
|
tone_config: int | None = Field(None, alias="toneConfig")
|
114
112
|
tone_mode: int | None = Field(None, alias="toneMode")
|
115
|
-
tone_multi_range: list[
|
113
|
+
tone_multi_range: list[list[int]] | None = Field(None, alias="toneMultiRange")
|
116
114
|
upload: int | None = None
|
117
115
|
volume: int | None = None
|
118
116
|
|
@@ -315,8 +313,9 @@ class Feeder(BaseModel):
|
|
315
313
|
bt_mac: str | None = Field(None, alias="btMac")
|
316
314
|
cloud_product: CloudProduct | None = Field(None, alias="cloudProduct")
|
317
315
|
created_at: str | None = Field(None, alias="createdAt")
|
316
|
+
desc: str | None = None # D3
|
318
317
|
device_records: FeederRecord | None = None
|
319
|
-
firmware:
|
318
|
+
firmware: str
|
320
319
|
firmware_details: list[FirmwareDetail] | None = Field(None, alias="firmwareDetails")
|
321
320
|
hardware: int
|
322
321
|
id: int
|
pypetkitapi/litter_container.py
CHANGED
@@ -9,10 +9,9 @@ from pypetkitapi.const import (
|
|
9
9
|
DEVICE_DATA,
|
10
10
|
DEVICE_RECORDS,
|
11
11
|
DEVICE_STATS,
|
12
|
+
LITTER_NO_CAMERA,
|
13
|
+
LITTER_WITH_CAMERA,
|
12
14
|
T3,
|
13
|
-
T4,
|
14
|
-
T5,
|
15
|
-
T6,
|
16
15
|
PetkitEndpoint,
|
17
16
|
)
|
18
17
|
from pypetkitapi.containers import CloudProduct, Device, FirmwareDetail, Wifi
|
@@ -240,9 +239,9 @@ class LitterRecord(BaseModel):
|
|
240
239
|
@classmethod
|
241
240
|
def get_endpoint(cls, device_type: str) -> str:
|
242
241
|
"""Get the endpoint URL for the given device type."""
|
243
|
-
if device_type in
|
242
|
+
if device_type in LITTER_NO_CAMERA:
|
244
243
|
return PetkitEndpoint.GET_DEVICE_RECORD
|
245
|
-
if device_type in
|
244
|
+
if device_type in LITTER_WITH_CAMERA:
|
246
245
|
return PetkitEndpoint.GET_DEVICE_RECORD_RELEASE
|
247
246
|
raise ValueError(f"Invalid device type: {device_type}")
|
248
247
|
|
@@ -255,11 +254,11 @@ class LitterRecord(BaseModel):
|
|
255
254
|
) -> dict:
|
256
255
|
"""Generate query parameters including request_date."""
|
257
256
|
device_type = device.device_type
|
258
|
-
if device_type in
|
257
|
+
if device_type in LITTER_NO_CAMERA:
|
259
258
|
request_date = request_date or datetime.now().strftime("%Y%m%d")
|
260
259
|
key = "day" if device_type == T3 else "date"
|
261
260
|
return {key: int(request_date), "deviceId": device.device_id}
|
262
|
-
if device_type in
|
261
|
+
if device_type in LITTER_WITH_CAMERA:
|
263
262
|
return {
|
264
263
|
"timestamp": int(datetime.now().timestamp()),
|
265
264
|
"deviceId": device.device_id,
|
@@ -407,7 +406,7 @@ class K3Device(BaseModel):
|
|
407
406
|
|
408
407
|
class Litter(BaseModel):
|
409
408
|
"""Dataclass for Litter Data.
|
410
|
-
Supported devices = T3, T4, T6
|
409
|
+
Supported devices = T3, T4, T5, T6
|
411
410
|
"""
|
412
411
|
|
413
412
|
data_type: ClassVar[str] = DEVICE_DATA
|
pypetkitapi/medias.py
CHANGED
@@ -4,13 +4,14 @@ from dataclasses import dataclass
|
|
4
4
|
import logging
|
5
5
|
from pathlib import Path
|
6
6
|
import re
|
7
|
+
from typing import Any
|
7
8
|
|
8
9
|
from aiofiles import open as aio_open
|
9
10
|
import aiohttp
|
10
11
|
from Crypto.Cipher import AES
|
11
12
|
from Crypto.Util.Padding import unpad
|
12
13
|
|
13
|
-
from pypetkitapi.feeder_container import Feeder,
|
14
|
+
from pypetkitapi.feeder_container import Feeder, RecordsType
|
14
15
|
|
15
16
|
_LOGGER = logging.getLogger(__name__)
|
16
17
|
|
@@ -68,7 +69,7 @@ class MediaHandler:
|
|
68
69
|
return self.media_files
|
69
70
|
|
70
71
|
async def _process_records(
|
71
|
-
self, records:
|
72
|
+
self, records: RecordsType, record_type: str
|
72
73
|
) -> list[MediasFiles]:
|
73
74
|
"""Process individual records and return media info."""
|
74
75
|
media_files = []
|
@@ -104,8 +105,8 @@ class MediaHandler:
|
|
104
105
|
)
|
105
106
|
|
106
107
|
for record in records:
|
107
|
-
if record
|
108
|
-
await process_item(record.items)
|
108
|
+
if hasattr(record, "items"):
|
109
|
+
await process_item(record.items) # type: ignore[attr-defined]
|
109
110
|
|
110
111
|
return media_files
|
111
112
|
|
@@ -170,9 +171,8 @@ class MediaDownloadDecode:
|
|
170
171
|
)
|
171
172
|
return file_path
|
172
173
|
|
173
|
-
|
174
|
-
|
175
|
-
) -> bytes | None:
|
174
|
+
@staticmethod
|
175
|
+
async def _decrypt_image_from_file(file_path: Path, aes_key: str) -> bytes | None:
|
176
176
|
"""Decrypt an image from a file using AES encryption.
|
177
177
|
:param file_path: Path to the encrypted image file.
|
178
178
|
:param aes_key: AES key used for decryption.
|
@@ -183,13 +183,13 @@ class MediaDownloadDecode:
|
|
183
183
|
aes_key = aes_key[:-1]
|
184
184
|
key_bytes: bytes = aes_key.encode("utf-8")
|
185
185
|
iv: bytes = b"\x61" * 16
|
186
|
-
cipher:
|
186
|
+
cipher: Any = AES.new(key_bytes, AES.MODE_CBC, iv)
|
187
187
|
|
188
188
|
async with aio_open(file_path, "rb") as encrypted_file:
|
189
189
|
encrypted_data: bytes = await encrypted_file.read()
|
190
190
|
|
191
191
|
decrypted_data: bytes = unpad(
|
192
|
-
cipher.decrypt(encrypted_data), AES.block_size
|
192
|
+
cipher.decrypt(encrypted_data), AES.block_size # type: ignore[attr-defined]
|
193
193
|
)
|
194
194
|
except Exception as e: # noqa: BLE001
|
195
195
|
logging.error("Error decrypting image from file %s: %s", file_path, e)
|
pypetkitapi/utils.py
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
"""Utils functions for the PyPetKit API."""
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
import logging
|
5
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
6
|
+
|
7
|
+
_LOGGER = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
|
10
|
+
def get_timezone_offset(timezone_user: str) -> str:
|
11
|
+
"""Get the timezone offset from its name. Return 0.0 if an error occurs."""
|
12
|
+
try:
|
13
|
+
timezone = ZoneInfo(timezone_user)
|
14
|
+
now = datetime.now(timezone)
|
15
|
+
offset = now.utcoffset()
|
16
|
+
if offset is None:
|
17
|
+
return "0.0"
|
18
|
+
offset_in_hours = offset.total_seconds() / 3600
|
19
|
+
return str(offset_in_hours)
|
20
|
+
except (ZoneInfoNotFoundError, AttributeError) as e:
|
21
|
+
_LOGGER.warning("Cannot get timezone offset for %s: %s", timezone_user, e)
|
22
|
+
return "0.0"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: pypetkitapi
|
3
|
-
Version: 1.9.
|
3
|
+
Version: 1.9.3
|
4
4
|
Summary: Python client for PetKit API
|
5
5
|
Home-page: https://github.com/Jezza34000/pypetkit
|
6
6
|
License: MIT
|
@@ -32,6 +32,12 @@ Description-Content-Type: text/markdown
|
|
32
32
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
33
33
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
34
34
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
35
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
36
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
37
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
38
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
39
|
+
|
40
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
35
41
|
|
36
42
|
[][pre-commit]
|
37
43
|
[][black]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
pypetkitapi/__init__.py,sha256=dQiuXe1aOwEGmW27csDtKRVc4vhXrqvsW-0ElcbrTRY,1562
|
2
|
+
pypetkitapi/client.py,sha256=oi1GhGIcvWMP5J9ueN2Y1xDX-Wm91b7LfjTVSe_plk4,30357
|
3
|
+
pypetkitapi/command.py,sha256=G7AEtUcaK-lcRliNf4oUxPkvDO_GNBkJ-ZUcOo7DGHM,7697
|
4
|
+
pypetkitapi/const.py,sha256=xDsF6sdxqQPy0B0Qhpe0Nn5xrkDjfo_omL4XL_oXFDE,4050
|
5
|
+
pypetkitapi/containers.py,sha256=oJR22ZruMr-0IRgiucdnj_nutOH59MKvmaFTwLJNiJI,4635
|
6
|
+
pypetkitapi/exceptions.py,sha256=fuTLT6Iw2_kA7eOyNJPf59vQkgfByhAnTThY4lC0Rt0,1283
|
7
|
+
pypetkitapi/feeder_container.py,sha256=ZGJhgqP-gjTFB2q91XoyZQ_G1S5cAY37JoqqHbzoanU,14640
|
8
|
+
pypetkitapi/litter_container.py,sha256=-z2BtdtRg8RyLJzJYY3AIACs9GGZ0C64hVhW4do6yQo,19172
|
9
|
+
pypetkitapi/medias.py,sha256=ZFdiPj24crYYFwKBUqlxKhfKGrW2uXoXzDl2vWukZ-A,7036
|
10
|
+
pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
|
11
|
+
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
pypetkitapi/utils.py,sha256=z7325kcJQUburnF28HSXrJMvY_gY9007K73Zwxp-4DQ,743
|
13
|
+
pypetkitapi/water_fountain_container.py,sha256=5J0b-fDZYcFLNX2El7fifv8H6JMhBCt-ttxSow1ozRQ,6787
|
14
|
+
pypetkitapi-1.9.3.dist-info/LICENSE,sha256=u5jNkZEn6YMrtN4Kr5rU3TcBJ5-eAt0qMx4JDsbsnzM,1074
|
15
|
+
pypetkitapi-1.9.3.dist-info/METADATA,sha256=p0SF9NMmun43lj81I0Sbc24o7JwFhuQgHoOK4tTquC8,6115
|
16
|
+
pypetkitapi-1.9.3.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
17
|
+
pypetkitapi-1.9.3.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
pypetkitapi/__init__.py,sha256=gwZUf8yep0CbngCc6uTqzZdmmTIaNTdIX5qE0YU6CrU,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=vfgxPOwbAFd3OFDMXH8md_lk1RLVlEDCFMjbREB4eS4,14640
|
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.2.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
14
|
-
pypetkitapi-1.9.2.dist-info/METADATA,sha256=RUqDHszBUmhesyoV2WfQX-RWQrGZdGSpm-IFBwwMDnE,5167
|
15
|
-
pypetkitapi-1.9.2.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
16
|
-
pypetkitapi-1.9.2.dist-info/RECORD,,
|
File without changes
|