pypetkitapi 1.9.2__py3-none-any.whl → 1.9.4__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 +5 -4
- pypetkitapi/client.py +201 -91
- pypetkitapi/command.py +14 -22
- pypetkitapi/const.py +39 -15
- pypetkitapi/exceptions.py +9 -0
- pypetkitapi/feeder_container.py +5 -6
- pypetkitapi/litter_container.py +7 -8
- pypetkitapi/media.py +423 -0
- pypetkitapi/schedule_container.py +67 -0
- pypetkitapi/utils.py +22 -0
- {pypetkitapi-1.9.2.dist-info → pypetkitapi-1.9.4.dist-info}/LICENSE +1 -1
- {pypetkitapi-1.9.2.dist-info → pypetkitapi-1.9.4.dist-info}/METADATA +12 -3
- pypetkitapi-1.9.4.dist-info/RECORD +18 -0
- {pypetkitapi-1.9.2.dist-info → pypetkitapi-1.9.4.dist-info}/WHEEL +1 -1
- pypetkitapi/medias.py +0 -199
- pypetkitapi-1.9.2.dist-info/RECORD +0 -16
pypetkitapi/__init__.py
CHANGED
@@ -36,11 +36,11 @@ from .containers import Pet
|
|
36
36
|
from .exceptions import PetkitAuthenticationError, PypetkitError
|
37
37
|
from .feeder_container import Feeder, RecordsItems
|
38
38
|
from .litter_container import Litter, LitterRecord, WorkState
|
39
|
-
from .
|
39
|
+
from .media import DownloadDecryptMedia, MediaFile, MediaManager
|
40
40
|
from .purifier_container import Purifier
|
41
41
|
from .water_fountain_container import WaterFountain
|
42
42
|
|
43
|
-
__version__ = "1.9.
|
43
|
+
__version__ = "1.9.4"
|
44
44
|
|
45
45
|
__all__ = [
|
46
46
|
"CTW3",
|
@@ -65,8 +65,9 @@ __all__ = [
|
|
65
65
|
"Litter",
|
66
66
|
"LitterCommand",
|
67
67
|
"LitterRecord",
|
68
|
-
"
|
69
|
-
"
|
68
|
+
"MediaManager",
|
69
|
+
"DownloadDecryptMedia",
|
70
|
+
"MediaFile",
|
70
71
|
"Pet",
|
71
72
|
"PetCommand",
|
72
73
|
"PetKitClient",
|
pypetkitapi/client.py
CHANGED
@@ -11,12 +11,14 @@ import urllib.parse
|
|
11
11
|
|
12
12
|
import aiohttp
|
13
13
|
from aiohttp import ContentTypeError
|
14
|
+
import m3u8
|
14
15
|
|
15
16
|
from pypetkitapi.command import ACTIONS_MAP, FOUNTAIN_COMMAND, FountainAction
|
16
17
|
from pypetkitapi.const import (
|
17
18
|
BLE_CONNECT_ATTEMPT,
|
18
19
|
BLE_END_TRAME,
|
19
20
|
BLE_START_TRAME,
|
21
|
+
CLIENT_NFO,
|
20
22
|
DEVICE_DATA,
|
21
23
|
DEVICE_RECORDS,
|
22
24
|
DEVICE_STATS,
|
@@ -25,6 +27,8 @@ from pypetkitapi.const import (
|
|
25
27
|
DEVICES_PURIFIER,
|
26
28
|
DEVICES_WATER_FOUNTAIN,
|
27
29
|
ERR_KEY,
|
30
|
+
LITTER_NO_CAMERA,
|
31
|
+
LITTER_WITH_CAMERA,
|
28
32
|
LOGIN_DATA,
|
29
33
|
PET,
|
30
34
|
RES_KEY,
|
@@ -50,6 +54,7 @@ from pypetkitapi.exceptions import (
|
|
50
54
|
PetkitInvalidHTTPResponseCodeError,
|
51
55
|
PetkitInvalidResponseFormat,
|
52
56
|
PetkitRegionalServerNotFoundError,
|
57
|
+
PetkitSessionError,
|
53
58
|
PetkitSessionExpiredError,
|
54
59
|
PetkitTimeoutError,
|
55
60
|
PypetkitError,
|
@@ -57,8 +62,22 @@ from pypetkitapi.exceptions import (
|
|
57
62
|
from pypetkitapi.feeder_container import Feeder, FeederRecord
|
58
63
|
from pypetkitapi.litter_container import Litter, LitterRecord, LitterStats, PetOutGraph
|
59
64
|
from pypetkitapi.purifier_container import Purifier
|
65
|
+
from pypetkitapi.utils import get_timezone_offset
|
60
66
|
from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
|
61
67
|
|
68
|
+
data_handlers = {}
|
69
|
+
|
70
|
+
|
71
|
+
def data_handler(data_type):
|
72
|
+
"""Register a data handler for a specific data type."""
|
73
|
+
|
74
|
+
def wrapper(func):
|
75
|
+
data_handlers[data_type] = func
|
76
|
+
return func
|
77
|
+
|
78
|
+
return wrapper
|
79
|
+
|
80
|
+
|
62
81
|
_LOGGER = logging.getLogger(__name__)
|
63
82
|
|
64
83
|
|
@@ -86,7 +105,9 @@ class PetKitClient:
|
|
86
105
|
self.petkit_entities = {}
|
87
106
|
self.aiohttp_session = session or aiohttp.ClientSession()
|
88
107
|
self.req = PrepReq(
|
89
|
-
base_url=PetkitDomain.PASSPORT_PETKIT,
|
108
|
+
base_url=PetkitDomain.PASSPORT_PETKIT,
|
109
|
+
session=self.aiohttp_session,
|
110
|
+
timezone=self.timezone,
|
90
111
|
)
|
91
112
|
|
92
113
|
async def _get_base_url(self) -> None:
|
@@ -129,12 +150,18 @@ class PetKitClient:
|
|
129
150
|
async def login(self, valid_code: str | None = None) -> None:
|
130
151
|
"""Login to the PetKit service and retrieve the appropriate server."""
|
131
152
|
# Retrieve the list of servers
|
153
|
+
self._session = None
|
132
154
|
await self._get_base_url()
|
133
155
|
|
134
156
|
_LOGGER.info("Logging in to PetKit server")
|
135
157
|
|
136
158
|
# Prepare the data to send
|
159
|
+
client_nfo = CLIENT_NFO.copy()
|
160
|
+
client_nfo["timezoneId"] = self.timezone
|
161
|
+
client_nfo["timezone"] = get_timezone_offset(self.timezone)
|
162
|
+
|
137
163
|
data = LOGIN_DATA.copy()
|
164
|
+
data["client"] = str(client_nfo)
|
138
165
|
data["encrypt"] = "1"
|
139
166
|
data["region"] = self.region
|
140
167
|
data["username"] = self.username
|
@@ -156,6 +183,8 @@ class PetKitClient:
|
|
156
183
|
)
|
157
184
|
session_data = response["session"]
|
158
185
|
self._session = SessionInfo(**session_data)
|
186
|
+
expiration_date = datetime.now() + timedelta(seconds=self._session.expires_in)
|
187
|
+
_LOGGER.debug("Login successful (token expiration %s)", expiration_date)
|
159
188
|
|
160
189
|
async def refresh_session(self) -> None:
|
161
190
|
"""Refresh the session."""
|
@@ -169,6 +198,7 @@ class PetKitClient:
|
|
169
198
|
session_data = response["session"]
|
170
199
|
self._session = SessionInfo(**session_data)
|
171
200
|
self._session.refreshed_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")
|
201
|
+
_LOGGER.debug("Session refreshed at %s", self._session.refreshed_at)
|
172
202
|
|
173
203
|
async def validate_session(self) -> None:
|
174
204
|
"""Check if the session is still valid and refresh or re-login if necessary."""
|
@@ -177,31 +207,27 @@ class PetKitClient:
|
|
177
207
|
await self.login()
|
178
208
|
return
|
179
209
|
|
180
|
-
|
181
|
-
|
182
|
-
|
210
|
+
created = datetime.strptime(self._session.created_at, "%Y-%m-%dT%H:%M:%S.%f%z")
|
211
|
+
is_expired = datetime.now(tz=created.tzinfo) - created >= timedelta(
|
212
|
+
seconds=self._session.expires_in
|
183
213
|
)
|
184
|
-
current_time = datetime.now(tz=created_at.tzinfo)
|
185
|
-
token_age = current_time - created_at
|
186
|
-
max_age = timedelta(seconds=self._session.expires_in)
|
187
|
-
half_max_age = max_age / 2
|
188
214
|
|
189
|
-
if
|
215
|
+
if is_expired:
|
190
216
|
_LOGGER.debug("Token expired, re-logging in")
|
191
217
|
await self.login()
|
192
|
-
elif
|
193
|
-
|
194
|
-
|
218
|
+
# elif (max_age / 2) < token_age < max_age:
|
219
|
+
# _LOGGER.debug("Token still OK, but refreshing session")
|
220
|
+
# await self.refresh_session()
|
195
221
|
|
196
222
|
async def get_session_id(self) -> dict:
|
197
223
|
"""Return the session ID."""
|
224
|
+
await self.validate_session()
|
198
225
|
if self._session is None:
|
199
|
-
raise
|
226
|
+
raise PetkitSessionError("No session ID available")
|
200
227
|
return {"F-Session": self._session.id, "X-Session": self._session.id}
|
201
228
|
|
202
229
|
async def _get_account_data(self) -> None:
|
203
230
|
"""Get the account data from the PetKit service."""
|
204
|
-
await self.validate_session()
|
205
231
|
_LOGGER.debug("Fetching account data")
|
206
232
|
response = await self.req.request(
|
207
233
|
method=HTTPMethod.GET,
|
@@ -223,27 +249,40 @@ class PetKitClient:
|
|
223
249
|
groupId=0,
|
224
250
|
type=0,
|
225
251
|
typeCode=0,
|
226
|
-
uniqueId=pet.sn,
|
252
|
+
uniqueId=str(pet.sn),
|
227
253
|
)
|
228
254
|
|
229
255
|
async def get_devices_data(self) -> None:
|
230
256
|
"""Get the devices data from the PetKit servers."""
|
231
|
-
await self.validate_session()
|
232
|
-
|
233
257
|
start_time = datetime.now()
|
234
258
|
if not self.account_data:
|
235
259
|
await self._get_account_data()
|
236
260
|
|
237
|
-
|
238
|
-
record_tasks =
|
239
|
-
device_list: list[Device] = []
|
261
|
+
device_list = self._collect_devices()
|
262
|
+
main_tasks, record_tasks = self._prepare_tasks(device_list)
|
240
263
|
|
264
|
+
await asyncio.gather(*main_tasks)
|
265
|
+
await asyncio.gather(*record_tasks)
|
266
|
+
await self._execute_stats_tasks()
|
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(
|
@@ -595,6 +661,46 @@ class PetKitClient:
|
|
595
661
|
_LOGGER.info("BLE command sent successfully.")
|
596
662
|
return True
|
597
663
|
|
664
|
+
async def get_cloud_video(self, video_url: str) -> dict[str, str | int]:
|
665
|
+
"""Get the video m3u8 link from the cloud."""
|
666
|
+
response = await self.req.request(
|
667
|
+
method=HTTPMethod.POST,
|
668
|
+
url=video_url,
|
669
|
+
headers=await self.get_session_id(),
|
670
|
+
)
|
671
|
+
return response[0]
|
672
|
+
|
673
|
+
async def extract_segments_m3u8(self, m3u8_url: str) -> tuple[str, str, list[str]]:
|
674
|
+
"""Extract segments from the m3u8 file.
|
675
|
+
:param: m3u8_url: URL of the m3u8 file
|
676
|
+
:return: aes_key, key_iv, segment_lst
|
677
|
+
"""
|
678
|
+
# Extract segments from m3u8 file
|
679
|
+
response = await self.req.request(
|
680
|
+
method=HTTPMethod.GET,
|
681
|
+
url=m3u8_url,
|
682
|
+
headers=await self.get_session_id(),
|
683
|
+
)
|
684
|
+
m3u8_obj = m3u8.loads(response[RES_KEY])
|
685
|
+
|
686
|
+
if not m3u8_obj.segments or not m3u8_obj.keys:
|
687
|
+
raise PetkitInvalidResponseFormat("No segments or key found in m3u8 file.")
|
688
|
+
|
689
|
+
# Extract segments from m3u8 file
|
690
|
+
segment_lst = [segment.uri for segment in m3u8_obj.segments]
|
691
|
+
# Extract key_uri and key_iv from m3u8 file
|
692
|
+
key_uri = m3u8_obj.keys[0].uri
|
693
|
+
key_iv = str(m3u8_obj.keys[0].iv)
|
694
|
+
|
695
|
+
# Extract aes_key from video segments
|
696
|
+
response = await self.req.request(
|
697
|
+
method=HTTPMethod.GET,
|
698
|
+
url=key_uri,
|
699
|
+
full_url=True,
|
700
|
+
headers=await self.get_session_id(),
|
701
|
+
)
|
702
|
+
return response[RES_KEY], key_iv, segment_lst
|
703
|
+
|
598
704
|
async def send_api_request(
|
599
705
|
self,
|
600
706
|
device_id: int,
|
@@ -602,11 +708,11 @@ class PetKitClient:
|
|
602
708
|
setting: dict | None = None,
|
603
709
|
) -> bool:
|
604
710
|
"""Control the device using the PetKit API."""
|
605
|
-
await self.validate_session()
|
606
|
-
|
607
711
|
device = self.petkit_entities.get(device_id, None)
|
608
712
|
if not device:
|
609
713
|
raise PypetkitError(f"Device with ID {device_id} not found.")
|
714
|
+
if device.device_nfo is None:
|
715
|
+
raise PypetkitError(f"Device with ID {device_id} has no device_nfo.")
|
610
716
|
|
611
717
|
_LOGGER.debug(
|
612
718
|
"Control API device=%s id=%s action=%s param=%s",
|
@@ -669,41 +775,43 @@ class PetKitClient:
|
|
669
775
|
class PrepReq:
|
670
776
|
"""Prepare the request to the PetKit API."""
|
671
777
|
|
672
|
-
def __init__(
|
778
|
+
def __init__(
|
779
|
+
self, base_url: str, session: aiohttp.ClientSession, timezone: str
|
780
|
+
) -> None:
|
673
781
|
"""Initialize the request."""
|
674
782
|
self.base_url = base_url
|
675
783
|
self.session = session
|
784
|
+
self.timezone = timezone
|
676
785
|
self.base_headers = self._generate_header()
|
677
786
|
|
678
|
-
|
679
|
-
def _generate_header() -> dict[str, str]:
|
787
|
+
def _generate_header(self) -> dict[str, str]:
|
680
788
|
"""Create header for interaction with API endpoint."""
|
681
|
-
|
682
789
|
return {
|
683
790
|
"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":
|
791
|
+
"Accept-Language": Header.ACCEPT_LANG.value,
|
792
|
+
"Accept-Encoding": Header.ENCODING.value,
|
793
|
+
"Content-Type": Header.CONTENT_TYPE.value,
|
794
|
+
"User-Agent": Header.AGENT.value,
|
795
|
+
"X-Img-Version": Header.IMG_VERSION.value,
|
796
|
+
"X-Locale": Header.LOCALE.value,
|
797
|
+
"X-Client": Header.CLIENT.value,
|
798
|
+
"X-Hour": Header.HOUR.value,
|
799
|
+
"X-TimezoneId": self.timezone,
|
800
|
+
"X-Api-Version": Header.API_VERSION.value,
|
801
|
+
"X-Timezone": get_timezone_offset(self.timezone),
|
695
802
|
}
|
696
803
|
|
697
804
|
async def request(
|
698
805
|
self,
|
699
806
|
method: str,
|
700
807
|
url: str,
|
808
|
+
full_url: bool = False,
|
701
809
|
params=None,
|
702
810
|
data=None,
|
703
811
|
headers=None,
|
704
812
|
) -> dict:
|
705
813
|
"""Make a request to the PetKit API."""
|
706
|
-
_url = "/".join(s.strip("/") for s in [self.base_url, url])
|
814
|
+
_url = url if full_url else "/".join(s.strip("/") for s in [self.base_url, url])
|
707
815
|
_headers = {**self.base_headers, **(headers or {})}
|
708
816
|
_LOGGER.debug("Request: %s %s", method, _url)
|
709
817
|
try:
|
@@ -729,12 +837,14 @@ class PrepReq:
|
|
729
837
|
) from e
|
730
838
|
|
731
839
|
try:
|
732
|
-
|
840
|
+
if response.content_type == "application/json":
|
841
|
+
response_json = await response.json()
|
842
|
+
else:
|
843
|
+
return {RES_KEY: await response.text()}
|
733
844
|
except ContentTypeError:
|
734
845
|
raise PetkitInvalidResponseFormat(
|
735
846
|
"Response is not in JSON format"
|
736
847
|
) from None
|
737
|
-
|
738
848
|
# Check for errors in the response
|
739
849
|
if ERR_KEY in response_json:
|
740
850
|
error_code = int(response_json[ERR_KEY].get("code", 0))
|
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: (
|