pypetkitapi 1.9.3__py3-none-any.whl → 1.10.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 +27 -6
- pypetkitapi/bluetooth.py +174 -0
- pypetkitapi/client.py +98 -206
- pypetkitapi/command.py +14 -22
- pypetkitapi/const.py +41 -8
- pypetkitapi/containers.py +9 -0
- pypetkitapi/exceptions.py +9 -0
- pypetkitapi/feeder_container.py +9 -1
- pypetkitapi/litter_container.py +9 -1
- pypetkitapi/media.py +592 -0
- pypetkitapi/schedule_container.py +67 -0
- {pypetkitapi-1.9.3.dist-info → pypetkitapi-1.10.1.dist-info}/METADATA +6 -3
- pypetkitapi-1.10.1.dist-info/RECORD +19 -0
- {pypetkitapi-1.9.3.dist-info → pypetkitapi-1.10.1.dist-info}/WHEEL +1 -1
- pypetkitapi/medias.py +0 -199
- pypetkitapi-1.9.3.dist-info/RECORD +0 -17
- {pypetkitapi-1.9.3.dist-info → pypetkitapi-1.10.1.dist-info}/LICENSE +0 -0
pypetkitapi/client.py
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
"""Pypetkit Client: A Python library for interfacing with PetKit"""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import base64
|
5
4
|
from datetime import datetime, timedelta
|
6
5
|
from enum import StrEnum
|
7
6
|
import hashlib
|
8
7
|
from http import HTTPMethod
|
9
8
|
import logging
|
10
|
-
import urllib.parse
|
11
9
|
|
12
10
|
import aiohttp
|
13
11
|
from aiohttp import ContentTypeError
|
12
|
+
import m3u8
|
14
13
|
|
15
|
-
from pypetkitapi.
|
14
|
+
from pypetkitapi.bluetooth import BluetoothManager
|
15
|
+
from pypetkitapi.command import ACTIONS_MAP
|
16
16
|
from pypetkitapi.const import (
|
17
|
-
BLE_CONNECT_ATTEMPT,
|
18
|
-
BLE_END_TRAME,
|
19
|
-
BLE_START_TRAME,
|
20
17
|
CLIENT_NFO,
|
21
18
|
DEVICE_DATA,
|
22
19
|
DEVICE_RECORDS,
|
@@ -26,6 +23,7 @@ from pypetkitapi.const import (
|
|
26
23
|
DEVICES_PURIFIER,
|
27
24
|
DEVICES_WATER_FOUNTAIN,
|
28
25
|
ERR_KEY,
|
26
|
+
FEEDER_WITH_CAMERA,
|
29
27
|
LITTER_NO_CAMERA,
|
30
28
|
LITTER_WITH_CAMERA,
|
31
29
|
LOGIN_DATA,
|
@@ -39,20 +37,14 @@ from pypetkitapi.const import (
|
|
39
37
|
PetkitDomain,
|
40
38
|
PetkitEndpoint,
|
41
39
|
)
|
42
|
-
from pypetkitapi.containers import
|
43
|
-
AccountData,
|
44
|
-
BleRelay,
|
45
|
-
Device,
|
46
|
-
Pet,
|
47
|
-
RegionInfo,
|
48
|
-
SessionInfo,
|
49
|
-
)
|
40
|
+
from pypetkitapi.containers import AccountData, Device, Pet, RegionInfo, SessionInfo
|
50
41
|
from pypetkitapi.exceptions import (
|
51
42
|
PetkitAuthenticationError,
|
52
43
|
PetkitAuthenticationUnregisteredEmailError,
|
53
44
|
PetkitInvalidHTTPResponseCodeError,
|
54
45
|
PetkitInvalidResponseFormat,
|
55
46
|
PetkitRegionalServerNotFoundError,
|
47
|
+
PetkitSessionError,
|
56
48
|
PetkitSessionExpiredError,
|
57
49
|
PetkitTimeoutError,
|
58
50
|
PypetkitError,
|
@@ -82,10 +74,6 @@ _LOGGER = logging.getLogger(__name__)
|
|
82
74
|
class PetKitClient:
|
83
75
|
"""Petkit Client"""
|
84
76
|
|
85
|
-
_session: SessionInfo | None = None
|
86
|
-
account_data: list[AccountData] = []
|
87
|
-
petkit_entities: dict[int, Feeder | Litter | WaterFountain | Purifier | Pet] = {}
|
88
|
-
|
89
77
|
def __init__(
|
90
78
|
self,
|
91
79
|
username: str,
|
@@ -99,14 +87,21 @@ class PetKitClient:
|
|
99
87
|
self.password = password
|
100
88
|
self.region = region.lower()
|
101
89
|
self.timezone = timezone
|
102
|
-
self._session = None
|
103
|
-
self.
|
90
|
+
self._session: SessionInfo | None = None
|
91
|
+
self.account_data: list[AccountData] = []
|
92
|
+
self.petkit_entities: dict[
|
93
|
+
int, Feeder | Litter | WaterFountain | Purifier | Pet
|
94
|
+
] = {}
|
104
95
|
self.aiohttp_session = session or aiohttp.ClientSession()
|
105
96
|
self.req = PrepReq(
|
106
97
|
base_url=PetkitDomain.PASSPORT_PETKIT,
|
107
98
|
session=self.aiohttp_session,
|
108
99
|
timezone=self.timezone,
|
109
100
|
)
|
101
|
+
self.bluetooth_manager = BluetoothManager(self)
|
102
|
+
from pypetkitapi import MediaManager
|
103
|
+
|
104
|
+
self.media_manager = MediaManager()
|
110
105
|
|
111
106
|
async def _get_base_url(self) -> None:
|
112
107
|
"""Get the list of API servers, filter by region, and return the matching server."""
|
@@ -148,6 +143,7 @@ class PetKitClient:
|
|
148
143
|
async def login(self, valid_code: str | None = None) -> None:
|
149
144
|
"""Login to the PetKit service and retrieve the appropriate server."""
|
150
145
|
# Retrieve the list of servers
|
146
|
+
self._session = None
|
151
147
|
await self._get_base_url()
|
152
148
|
|
153
149
|
_LOGGER.info("Logging in to PetKit server")
|
@@ -180,6 +176,8 @@ class PetKitClient:
|
|
180
176
|
)
|
181
177
|
session_data = response["session"]
|
182
178
|
self._session = SessionInfo(**session_data)
|
179
|
+
expiration_date = datetime.now() + timedelta(seconds=self._session.expires_in)
|
180
|
+
_LOGGER.debug("Login successful (token expiration %s)", expiration_date)
|
183
181
|
|
184
182
|
async def refresh_session(self) -> None:
|
185
183
|
"""Refresh the session."""
|
@@ -193,6 +191,7 @@ class PetKitClient:
|
|
193
191
|
session_data = response["session"]
|
194
192
|
self._session = SessionInfo(**session_data)
|
195
193
|
self._session.refreshed_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")
|
194
|
+
_LOGGER.debug("Session refreshed at %s", self._session.refreshed_at)
|
196
195
|
|
197
196
|
async def validate_session(self) -> None:
|
198
197
|
"""Check if the session is still valid and refresh or re-login if necessary."""
|
@@ -201,31 +200,27 @@ class PetKitClient:
|
|
201
200
|
await self.login()
|
202
201
|
return
|
203
202
|
|
204
|
-
|
205
|
-
|
206
|
-
|
203
|
+
created = datetime.strptime(self._session.created_at, "%Y-%m-%dT%H:%M:%S.%f%z")
|
204
|
+
is_expired = datetime.now(tz=created.tzinfo) - created >= timedelta(
|
205
|
+
seconds=self._session.expires_in
|
207
206
|
)
|
208
|
-
current_time = datetime.now(tz=created_at.tzinfo)
|
209
|
-
token_age = current_time - created_at
|
210
|
-
max_age = timedelta(seconds=self._session.expires_in)
|
211
|
-
half_max_age = max_age / 2
|
212
207
|
|
213
|
-
if
|
208
|
+
if is_expired:
|
214
209
|
_LOGGER.debug("Token expired, re-logging in")
|
215
210
|
await self.login()
|
216
|
-
elif
|
217
|
-
|
218
|
-
|
211
|
+
# elif (max_age / 2) < token_age < max_age:
|
212
|
+
# _LOGGER.debug("Token still OK, but refreshing session")
|
213
|
+
# await self.refresh_session()
|
219
214
|
|
220
215
|
async def get_session_id(self) -> dict:
|
221
216
|
"""Return the session ID."""
|
217
|
+
await self.validate_session()
|
222
218
|
if self._session is None:
|
223
|
-
raise
|
219
|
+
raise PetkitSessionError("No session ID available")
|
224
220
|
return {"F-Session": self._session.id, "X-Session": self._session.id}
|
225
221
|
|
226
222
|
async def _get_account_data(self) -> None:
|
227
223
|
"""Get the account data from the PetKit service."""
|
228
|
-
await self.validate_session()
|
229
224
|
_LOGGER.debug("Fetching account data")
|
230
225
|
response = await self.req.request(
|
231
226
|
method=HTTPMethod.GET,
|
@@ -252,17 +247,16 @@ class PetKitClient:
|
|
252
247
|
|
253
248
|
async def get_devices_data(self) -> None:
|
254
249
|
"""Get the devices data from the PetKit servers."""
|
255
|
-
await self.validate_session()
|
256
|
-
|
257
250
|
start_time = datetime.now()
|
258
251
|
if not self.account_data:
|
259
252
|
await self._get_account_data()
|
260
253
|
|
261
254
|
device_list = self._collect_devices()
|
262
|
-
main_tasks, record_tasks = self._prepare_tasks(device_list)
|
255
|
+
main_tasks, record_tasks, media_tasks = self._prepare_tasks(device_list)
|
263
256
|
|
264
257
|
await asyncio.gather(*main_tasks)
|
265
258
|
await asyncio.gather(*record_tasks)
|
259
|
+
await asyncio.gather(*media_tasks)
|
266
260
|
await self._execute_stats_tasks()
|
267
261
|
|
268
262
|
end_time = datetime.now()
|
@@ -279,10 +273,11 @@ class PetKitClient:
|
|
279
273
|
_LOGGER.debug("Found %s devices", len(account.device_list))
|
280
274
|
return device_list
|
281
275
|
|
282
|
-
def _prepare_tasks(self, device_list: list[Device]) -> tuple[list, list]:
|
276
|
+
def _prepare_tasks(self, device_list: list[Device]) -> tuple[list, list, list]:
|
283
277
|
"""Prepare main and record tasks based on device types."""
|
284
|
-
main_tasks = []
|
285
|
-
record_tasks = []
|
278
|
+
main_tasks: list = []
|
279
|
+
record_tasks: list = []
|
280
|
+
media_tasks: list = []
|
286
281
|
|
287
282
|
for device in device_list:
|
288
283
|
device_type = device.device_type
|
@@ -290,11 +285,14 @@ class PetKitClient:
|
|
290
285
|
if device_type in DEVICES_FEEDER:
|
291
286
|
main_tasks.append(self._fetch_device_data(device, Feeder))
|
292
287
|
record_tasks.append(self._fetch_device_data(device, FeederRecord))
|
288
|
+
self._add_feeder_task_by_type(media_tasks, device_type, device)
|
293
289
|
|
294
290
|
elif device_type in DEVICES_LITTER_BOX:
|
295
291
|
main_tasks.append(self._fetch_device_data(device, Litter))
|
296
292
|
record_tasks.append(self._fetch_device_data(device, LitterRecord))
|
297
|
-
self.
|
293
|
+
self._add_lb_task_by_type(
|
294
|
+
record_tasks, media_tasks, device_type, device
|
295
|
+
)
|
298
296
|
|
299
297
|
elif device_type in DEVICES_WATER_FOUNTAIN:
|
300
298
|
main_tasks.append(self._fetch_device_data(device, WaterFountain))
|
@@ -305,16 +303,24 @@ class PetKitClient:
|
|
305
303
|
elif device_type in DEVICES_PURIFIER:
|
306
304
|
main_tasks.append(self._fetch_device_data(device, Purifier))
|
307
305
|
|
308
|
-
return main_tasks, record_tasks
|
306
|
+
return main_tasks, record_tasks, media_tasks
|
309
307
|
|
310
|
-
def
|
311
|
-
self, record_tasks: list, device_type: str, device: Device
|
308
|
+
def _add_lb_task_by_type(
|
309
|
+
self, record_tasks: list, media_tasks: list, device_type: str, device: Device
|
312
310
|
):
|
313
311
|
"""Add specific tasks for litter box devices."""
|
314
|
-
if device_type in
|
312
|
+
if device_type in LITTER_NO_CAMERA:
|
315
313
|
record_tasks.append(self._fetch_device_data(device, LitterStats))
|
316
|
-
if device_type in
|
314
|
+
if device_type in LITTER_WITH_CAMERA:
|
317
315
|
record_tasks.append(self._fetch_device_data(device, PetOutGraph))
|
316
|
+
media_tasks.append(self._fetch_media(device))
|
317
|
+
|
318
|
+
def _add_feeder_task_by_type(
|
319
|
+
self, media_tasks: list, device_type: str, device: Device
|
320
|
+
):
|
321
|
+
"""Add specific tasks for feeder box devices."""
|
322
|
+
if device_type in FEEDER_WITH_CAMERA:
|
323
|
+
media_tasks.append(self._fetch_media(device))
|
318
324
|
|
319
325
|
async def _execute_stats_tasks(self) -> None:
|
320
326
|
"""Execute tasks to populate pet stats."""
|
@@ -325,6 +331,15 @@ class PetKitClient:
|
|
325
331
|
]
|
326
332
|
await asyncio.gather(*stats_tasks)
|
327
333
|
|
334
|
+
async def _fetch_media(self, device: Device) -> None:
|
335
|
+
"""Fetch media data from the PetKit servers."""
|
336
|
+
_LOGGER.debug("Fetching media data for device: %s", device.device_id)
|
337
|
+
|
338
|
+
device_entity = self.petkit_entities[device.device_id]
|
339
|
+
device_entity.medias = await self.media_manager.get_all_media_files(
|
340
|
+
[device_entity]
|
341
|
+
)
|
342
|
+
|
328
343
|
async def _fetch_device_data(
|
329
344
|
self,
|
330
345
|
device: Device,
|
@@ -494,172 +509,50 @@ class PetKitClient:
|
|
494
509
|
pet.last_duration_usage = self.get_safe_value(graph.toilet_time)
|
495
510
|
pet.last_device_used = pet_graphs.device_nfo.device_name
|
496
511
|
|
497
|
-
async def
|
498
|
-
|
499
|
-
water_fountain = self.petkit_entities.get(fountain_id)
|
500
|
-
if not isinstance(water_fountain, WaterFountain):
|
501
|
-
_LOGGER.error("Water fountain with ID %s not found.", fountain_id)
|
502
|
-
raise TypeError(f"Water fountain with ID {fountain_id} not found.")
|
503
|
-
return water_fountain
|
504
|
-
|
505
|
-
async def check_relay_availability(self, fountain_id: int) -> bool:
|
506
|
-
"""Check if BLE relay is available for the account."""
|
507
|
-
fountain = None
|
508
|
-
|
509
|
-
for account in self.account_data:
|
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
|
521
|
-
|
522
|
-
if not fountain:
|
523
|
-
raise ValueError(
|
524
|
-
f"Fountain with device_id {fountain_id} not found for the current account"
|
525
|
-
)
|
526
|
-
|
527
|
-
group_id = fountain.group_id
|
528
|
-
|
529
|
-
response = await self.req.request(
|
530
|
-
method=HTTPMethod.POST,
|
531
|
-
url=f"{PetkitEndpoint.BLE_AS_RELAY}",
|
532
|
-
params={"groupId": group_id},
|
533
|
-
headers=await self.get_session_id(),
|
534
|
-
)
|
535
|
-
ble_relays = [BleRelay(**relay) for relay in response]
|
536
|
-
|
537
|
-
if len(ble_relays) == 0:
|
538
|
-
_LOGGER.warning("No BLE relay devices found.")
|
539
|
-
return False
|
540
|
-
return True
|
541
|
-
|
542
|
-
async def open_ble_connection(self, fountain_id: int) -> bool:
|
543
|
-
"""Open a BLE connection to a PetKit device."""
|
544
|
-
_LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
|
545
|
-
water_fountain = await self._get_fountain_instance(fountain_id)
|
546
|
-
|
547
|
-
if await self.check_relay_availability(fountain_id) is False:
|
548
|
-
_LOGGER.error("BLE relay not available.")
|
549
|
-
return False
|
550
|
-
|
551
|
-
if water_fountain.is_connected is True:
|
552
|
-
_LOGGER.error("BLE connection already established.")
|
553
|
-
return True
|
554
|
-
|
512
|
+
async def get_cloud_video(self, video_url: str) -> dict[str, str | int] | None:
|
513
|
+
"""Get the video m3u8 link from the cloud."""
|
555
514
|
response = await self.req.request(
|
556
515
|
method=HTTPMethod.POST,
|
557
|
-
url=
|
558
|
-
data={
|
559
|
-
"bleId": fountain_id,
|
560
|
-
"type": 24,
|
561
|
-
"mac": water_fountain.mac,
|
562
|
-
},
|
516
|
+
url=video_url,
|
563
517
|
headers=await self.get_session_id(),
|
564
518
|
)
|
565
|
-
if response
|
566
|
-
_LOGGER.
|
567
|
-
|
568
|
-
return False
|
569
|
-
|
570
|
-
for attempt in range(BLE_CONNECT_ATTEMPT):
|
571
|
-
_LOGGER.warning("BLE connection attempt n%s", attempt)
|
572
|
-
response = await self.req.request(
|
573
|
-
method=HTTPMethod.POST,
|
574
|
-
url=PetkitEndpoint.BLE_POLL,
|
575
|
-
data={
|
576
|
-
"bleId": fountain_id,
|
577
|
-
"type": 24,
|
578
|
-
"mac": water_fountain.mac,
|
579
|
-
},
|
580
|
-
headers=await self.get_session_id(),
|
519
|
+
if not isinstance(response, list) or not response:
|
520
|
+
_LOGGER.debug(
|
521
|
+
"No video data found from cloud, looks like you don't have a care+ subscription ?"
|
581
522
|
)
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
async def close_ble_connection(self, fountain_id: int) -> None:
|
595
|
-
"""Close the BLE connection to a PetKit device."""
|
596
|
-
_LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
|
597
|
-
water_fountain = await self._get_fountain_instance(fountain_id)
|
598
|
-
|
599
|
-
await self.req.request(
|
600
|
-
method=HTTPMethod.POST,
|
601
|
-
url=PetkitEndpoint.BLE_CANCEL,
|
602
|
-
data={
|
603
|
-
"bleId": fountain_id,
|
604
|
-
"type": 24,
|
605
|
-
"mac": water_fountain.mac,
|
606
|
-
},
|
523
|
+
return None
|
524
|
+
return response[0]
|
525
|
+
|
526
|
+
async def extract_segments_m3u8(self, m3u8_url: str) -> tuple[str, str, list[str]]:
|
527
|
+
"""Extract segments from the m3u8 file.
|
528
|
+
:param: m3u8_url: URL of the m3u8 file
|
529
|
+
:return: aes_key, key_iv, segment_lst
|
530
|
+
"""
|
531
|
+
# Extract segments from m3u8 file
|
532
|
+
response = await self.req.request(
|
533
|
+
method=HTTPMethod.GET,
|
534
|
+
url=m3u8_url,
|
607
535
|
headers=await self.get_session_id(),
|
608
536
|
)
|
609
|
-
|
610
|
-
|
611
|
-
async def get_ble_cmd_data(
|
612
|
-
self, fountain_command: list, counter: int
|
613
|
-
) -> tuple[int, str]:
|
614
|
-
"""Prepare BLE data by adding start and end trame to the command and extracting the first number."""
|
615
|
-
cmd_code = fountain_command[0]
|
616
|
-
modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
|
617
|
-
ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
|
618
|
-
encoded_data = await self._encode_ble_data(ble_data)
|
619
|
-
return cmd_code, encoded_data
|
620
|
-
|
621
|
-
@staticmethod
|
622
|
-
async def _encode_ble_data(byte_list: list) -> str:
|
623
|
-
"""Encode a list of bytes to a URL encoded base64 string."""
|
624
|
-
byte_array = bytearray(byte_list)
|
625
|
-
b64_encoded = base64.b64encode(byte_array)
|
626
|
-
return urllib.parse.quote(b64_encoded)
|
627
|
-
|
628
|
-
async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
|
629
|
-
"""BLE command to a PetKit device."""
|
630
|
-
_LOGGER.info("Sending BLE command to fountain %s", fountain_id)
|
631
|
-
water_fountain = await self._get_fountain_instance(fountain_id)
|
632
|
-
|
633
|
-
if water_fountain.is_connected is False:
|
634
|
-
_LOGGER.error("BLE connection not established.")
|
635
|
-
return False
|
537
|
+
m3u8_obj = m3u8.loads(response[RES_KEY])
|
636
538
|
|
637
|
-
|
638
|
-
|
639
|
-
_LOGGER.error("Command not found.")
|
640
|
-
return False
|
539
|
+
if not m3u8_obj.segments or not m3u8_obj.keys:
|
540
|
+
raise PetkitInvalidResponseFormat("No segments or key found in m3u8 file.")
|
641
541
|
|
642
|
-
|
643
|
-
|
644
|
-
|
542
|
+
# Extract segments from m3u8 file
|
543
|
+
segment_lst = [segment.uri for segment in m3u8_obj.segments]
|
544
|
+
# Extract key_uri and key_iv from m3u8 file
|
545
|
+
key_uri = m3u8_obj.keys[0].uri
|
546
|
+
key_iv = str(m3u8_obj.keys[0].iv)
|
645
547
|
|
548
|
+
# Extract aes_key from video segments
|
646
549
|
response = await self.req.request(
|
647
|
-
method=HTTPMethod.
|
648
|
-
url=
|
649
|
-
|
650
|
-
"bleId": water_fountain.id,
|
651
|
-
"cmd": cmd_code,
|
652
|
-
"data": cmd_data,
|
653
|
-
"mac": water_fountain.mac,
|
654
|
-
"type": 24,
|
655
|
-
},
|
550
|
+
method=HTTPMethod.GET,
|
551
|
+
url=key_uri,
|
552
|
+
full_url=True,
|
656
553
|
headers=await self.get_session_id(),
|
657
554
|
)
|
658
|
-
|
659
|
-
_LOGGER.error("Failed to send BLE command.")
|
660
|
-
return False
|
661
|
-
_LOGGER.info("BLE command sent successfully.")
|
662
|
-
return True
|
555
|
+
return response[RES_KEY], key_iv, segment_lst
|
663
556
|
|
664
557
|
async def send_api_request(
|
665
558
|
self,
|
@@ -668,8 +561,6 @@ class PetKitClient:
|
|
668
561
|
setting: dict | None = None,
|
669
562
|
) -> bool:
|
670
563
|
"""Control the device using the PetKit API."""
|
671
|
-
await self.validate_session()
|
672
|
-
|
673
564
|
device = self.petkit_entities.get(device_id, None)
|
674
565
|
if not device:
|
675
566
|
raise PypetkitError(f"Device with ID {device_id} not found.")
|
@@ -767,12 +658,13 @@ class PrepReq:
|
|
767
658
|
self,
|
768
659
|
method: str,
|
769
660
|
url: str,
|
661
|
+
full_url: bool = False,
|
770
662
|
params=None,
|
771
663
|
data=None,
|
772
664
|
headers=None,
|
773
665
|
) -> dict:
|
774
666
|
"""Make a request to the PetKit API."""
|
775
|
-
_url = "/".join(s.strip("/") for s in [self.base_url, url])
|
667
|
+
_url = url if full_url else "/".join(s.strip("/") for s in [self.base_url, url])
|
776
668
|
_headers = {**self.base_headers, **(headers or {})}
|
777
669
|
_LOGGER.debug("Request: %s %s", method, _url)
|
778
670
|
try:
|
@@ -798,12 +690,14 @@ class PrepReq:
|
|
798
690
|
) from e
|
799
691
|
|
800
692
|
try:
|
801
|
-
|
693
|
+
if response.content_type == "application/json":
|
694
|
+
response_json = await response.json()
|
695
|
+
else:
|
696
|
+
return {RES_KEY: await response.text()}
|
802
697
|
except ContentTypeError:
|
803
698
|
raise PetkitInvalidResponseFormat(
|
804
699
|
"Response is not in JSON format"
|
805
700
|
) from None
|
806
|
-
|
807
701
|
# Check for errors in the response
|
808
702
|
if ERR_KEY in response_json:
|
809
703
|
error_code = int(response_json[ERR_KEY].get("code", 0))
|
@@ -817,9 +711,7 @@ class PrepReq:
|
|
817
711
|
f"Authentication failed: {error_msg}"
|
818
712
|
)
|
819
713
|
case 125:
|
820
|
-
raise PetkitAuthenticationUnregisteredEmailError
|
821
|
-
f"Authentication failed: {error_msg}"
|
822
|
-
)
|
714
|
+
raise PetkitAuthenticationUnregisteredEmailError
|
823
715
|
case _:
|
824
716
|
raise PypetkitError(
|
825
717
|
f"Request failed code : {error_code}, details : {error_msg} url : {url}"
|
pypetkitapi/command.py
CHANGED
@@ -9,7 +9,6 @@ import json
|
|
9
9
|
from pypetkitapi.const import (
|
10
10
|
ALL_DEVICES,
|
11
11
|
D3,
|
12
|
-
D4,
|
13
12
|
D4H,
|
14
13
|
D4S,
|
15
14
|
D4SH,
|
@@ -145,25 +144,28 @@ class CmdData:
|
|
145
144
|
|
146
145
|
def get_endpoint_manual_feed(device):
|
147
146
|
"""Get the endpoint for the device"""
|
148
|
-
if device.device_nfo.device_type
|
149
|
-
return PetkitEndpoint.
|
150
|
-
|
151
|
-
return PetkitEndpoint.MANUAL_FEED_FRESH_ELEMENT
|
152
|
-
return PetkitEndpoint.MANUAL_FEED_DUAL
|
147
|
+
if device.device_nfo.device_type in [FEEDER_MINI, FEEDER]:
|
148
|
+
return PetkitEndpoint.MANUAL_FEED_OLD # Old endpoint snakecase
|
149
|
+
return PetkitEndpoint.MANUAL_FEED_NEW # New endpoint camelcase
|
153
150
|
|
154
151
|
|
155
152
|
def get_endpoint_reset_desiccant(device):
|
153
|
+
"""Get the endpoint for the device"""
|
154
|
+
if device.device_nfo.device_type in [FEEDER_MINI, FEEDER]:
|
155
|
+
return PetkitEndpoint.DESICCANT_RESET_OLD # Old endpoint snakecase
|
156
|
+
return PetkitEndpoint.DESICCANT_RESET_NEW # New endpoint camelcase
|
157
|
+
|
158
|
+
|
159
|
+
def get_endpoint_update_setting(device):
|
156
160
|
"""Get the endpoint for the device"""
|
157
161
|
if device.device_nfo.device_type == FEEDER_MINI:
|
158
|
-
return PetkitEndpoint.
|
159
|
-
|
160
|
-
return PetkitEndpoint.FRESH_ELEMENT_DESICCANT_RESET
|
161
|
-
return PetkitEndpoint.DESICCANT_RESET
|
162
|
+
return PetkitEndpoint.UPDATE_SETTING_FEEDER_MINI
|
163
|
+
return PetkitEndpoint.UPDATE_SETTING
|
162
164
|
|
163
165
|
|
164
166
|
ACTIONS_MAP = {
|
165
167
|
DeviceCommand.UPDATE_SETTING: CmdData(
|
166
|
-
endpoint=
|
168
|
+
endpoint=lambda device: get_endpoint_update_setting(device),
|
167
169
|
params=lambda device, setting: {
|
168
170
|
"id": device.id,
|
169
171
|
"kv": json.dumps(setting),
|
@@ -199,16 +201,6 @@ ACTIONS_MAP = {
|
|
199
201
|
),
|
200
202
|
FeederCommand.MANUAL_FEED: CmdData(
|
201
203
|
endpoint=lambda device: get_endpoint_manual_feed(device),
|
202
|
-
params=lambda device, setting: {
|
203
|
-
"day": datetime.datetime.now().strftime("%Y%m%d"),
|
204
|
-
"deviceId": device.id,
|
205
|
-
"time": "-1",
|
206
|
-
**setting,
|
207
|
-
},
|
208
|
-
supported_device=[FEEDER, FEEDER_MINI, D3, D4, D4H],
|
209
|
-
),
|
210
|
-
FeederCommand.MANUAL_FEED_DUAL: CmdData(
|
211
|
-
endpoint=PetkitEndpoint.MANUAL_FEED_DUAL,
|
212
204
|
params=lambda device, setting: {
|
213
205
|
"day": datetime.datetime.now().strftime("%Y%m%d"),
|
214
206
|
"deviceId": device.id,
|
@@ -216,7 +208,7 @@ ACTIONS_MAP = {
|
|
216
208
|
"time": "-1",
|
217
209
|
**setting,
|
218
210
|
},
|
219
|
-
supported_device=
|
211
|
+
supported_device=DEVICES_FEEDER,
|
220
212
|
),
|
221
213
|
FeederCommand.CANCEL_MANUAL_FEED: CmdData(
|
222
214
|
endpoint=lambda device: (
|
pypetkitapi/const.py
CHANGED
@@ -34,12 +34,18 @@ K2 = "k2"
|
|
34
34
|
K3 = "k3"
|
35
35
|
PET = "pet"
|
36
36
|
|
37
|
+
# Litter
|
37
38
|
DEVICES_LITTER_BOX = [T3, T4, T5, T6]
|
38
39
|
LITTER_WITH_CAMERA = [T5, T6]
|
39
40
|
LITTER_NO_CAMERA = [T3, T4]
|
41
|
+
# Feeder
|
42
|
+
FEEDER_WITH_CAMERA = [D4H, D4SH]
|
40
43
|
DEVICES_FEEDER = [FEEDER, FEEDER_MINI, D3, D4, D4S, D4H, D4SH]
|
44
|
+
# Water Fountain
|
41
45
|
DEVICES_WATER_FOUNTAIN = [W5, CTW3]
|
46
|
+
# Purifier
|
42
47
|
DEVICES_PURIFIER = [K2]
|
48
|
+
# All devices
|
43
49
|
ALL_DEVICES = [
|
44
50
|
*DEVICES_LITTER_BOX,
|
45
51
|
*DEVICES_FEEDER,
|
@@ -93,6 +99,20 @@ LOGIN_DATA = {
|
|
93
99
|
}
|
94
100
|
|
95
101
|
|
102
|
+
class MediaType(StrEnum):
|
103
|
+
"""Record Type constants"""
|
104
|
+
|
105
|
+
VIDEO = "avi"
|
106
|
+
IMAGE = "jpg"
|
107
|
+
|
108
|
+
|
109
|
+
class VideoType(StrEnum):
|
110
|
+
"""Record Type constants"""
|
111
|
+
|
112
|
+
HIGHLIGHT = "highlight"
|
113
|
+
PLAYBACK = "playback"
|
114
|
+
|
115
|
+
|
96
116
|
class RecordType(StrEnum):
|
97
117
|
"""Record Type constants"""
|
98
118
|
|
@@ -100,9 +120,16 @@ class RecordType(StrEnum):
|
|
100
120
|
FEED = "feed"
|
101
121
|
MOVE = "move"
|
102
122
|
PET = "pet"
|
123
|
+
TOILETING = "toileting"
|
103
124
|
|
104
125
|
|
105
|
-
RecordTypeLST = [
|
126
|
+
RecordTypeLST = [
|
127
|
+
RecordType.EAT,
|
128
|
+
RecordType.FEED,
|
129
|
+
RecordType.MOVE,
|
130
|
+
RecordType.PET,
|
131
|
+
RecordType.TOILETING,
|
132
|
+
]
|
106
133
|
|
107
134
|
|
108
135
|
class PetkitEndpoint(StrEnum):
|
@@ -121,6 +148,7 @@ class PetkitEndpoint(StrEnum):
|
|
121
148
|
GET_DEVICE_RECORD = "getDeviceRecord"
|
122
149
|
GET_DEVICE_RECORD_RELEASE = "getDeviceRecordRelease"
|
123
150
|
UPDATE_SETTING = "updateSettings"
|
151
|
+
UPDATE_SETTING_FEEDER_MINI = "update"
|
124
152
|
|
125
153
|
# Bluetooth
|
126
154
|
BLE_AS_RELAY = "ble/ownSupportBleDevices"
|
@@ -140,25 +168,30 @@ class PetkitEndpoint(StrEnum):
|
|
140
168
|
GET_PET_OUT_GRAPH = "getPetOutGraph"
|
141
169
|
|
142
170
|
# Video features
|
171
|
+
GET_M3U8 = "getM3u8"
|
143
172
|
CLOUD_VIDEO = "cloud/video"
|
144
173
|
GET_DOWNLOAD_M3U8 = "getDownloadM3u8"
|
145
|
-
GET_M3U8 = "getM3u8"
|
146
174
|
|
147
175
|
# Feeders
|
148
176
|
REPLENISHED_FOOD = "added"
|
149
177
|
FRESH_ELEMENT_CALIBRATION = "food_reset"
|
150
178
|
FRESH_ELEMENT_CANCEL_FEED = "cancel_realtime_feed"
|
151
|
-
|
152
|
-
|
153
|
-
FRESH_ELEMENT_DESICCANT_RESET = "feeder/desiccant_reset"
|
179
|
+
DESICCANT_RESET_OLD = "desiccant_reset"
|
180
|
+
DESICCANT_RESET_NEW = "desiccantReset"
|
154
181
|
CALL_PET = "callPet"
|
155
182
|
CANCEL_FEED = "cancelRealtimeFeed"
|
156
|
-
|
157
|
-
|
158
|
-
MANUAL_FEED_DUAL = "saveDailyFeed"
|
183
|
+
MANUAL_FEED_OLD = "save_dailyfeed" # For Feeder/FeederMini
|
184
|
+
MANUAL_FEED_NEW = "saveDailyFeed" # For all other feeders
|
159
185
|
DAILY_FEED_AND_EAT = "dailyFeedAndEat" # D3
|
160
186
|
FEED_STATISTIC = "feedStatistic" # D4
|
161
187
|
DAILY_FEED = "dailyFeeds" # D4S
|
162
188
|
REMOVE_DAILY_FEED = "removeDailyFeed"
|
163
189
|
RESTORE_DAILY_FEED = "restoreDailyFeed"
|
164
190
|
SAVE_FEED = "saveFeed" # For Feeding plan
|
191
|
+
|
192
|
+
# Schedule
|
193
|
+
SCHEDULE = "schedule/schedules"
|
194
|
+
SCHEDULE_SAVE = "schedule/save"
|
195
|
+
SCHEDULE_REMOVE = "schedule/remove"
|
196
|
+
SCHEDULE_COMPLETE = "schedule/complete"
|
197
|
+
SCHEDULE_HISTORY = "schedule/userHistorySchedules"
|