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/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.command import ACTIONS_MAP, FOUNTAIN_COMMAND, FountainAction
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.petkit_entities = {}
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
- created_at = datetime.strptime(
205
- self._session.created_at,
206
- "%Y-%m-%dT%H:%M:%S.%f%z",
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 token_age > max_age:
208
+ if is_expired:
214
209
  _LOGGER.debug("Token expired, re-logging in")
215
210
  await self.login()
216
- elif half_max_age < token_age <= max_age:
217
- _LOGGER.debug("Token still OK, but refreshing session")
218
- await self.refresh_session()
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 PypetkitError("Session is not initialized.")
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._add_litter_box_tasks(record_tasks, device_type, device)
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 _add_litter_box_tasks(
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 [T3, T4]:
312
+ if device_type in LITTER_NO_CAMERA:
315
313
  record_tasks.append(self._fetch_device_data(device, LitterStats))
316
- if device_type in [T5, T6]:
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 _get_fountain_instance(self, fountain_id: int) -> WaterFountain:
498
- # Retrieve the water fountain object
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=PetkitEndpoint.BLE_CONNECT,
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 != {"state": 1}:
566
- _LOGGER.error("Failed to establish BLE connection.")
567
- water_fountain.is_connected = False
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
- if response == 1:
583
- _LOGGER.info("BLE connection established successfully.")
584
- water_fountain.is_connected = True
585
- water_fountain.last_ble_poll = datetime.now().strftime(
586
- "%Y-%m-%dT%H:%M:%S.%f"
587
- )
588
- return True
589
- await asyncio.sleep(4)
590
- _LOGGER.error("Failed to establish BLE connection after multiple attempts.")
591
- water_fountain.is_connected = False
592
- return False
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
- _LOGGER.info("BLE connection closed successfully.")
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
- command_data = FOUNTAIN_COMMAND.get(command)
638
- if command_data is None:
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
- cmd_code, cmd_data = await self.get_ble_cmd_data(
643
- list(command_data), water_fountain.ble_counter
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.POST,
648
- url=PetkitEndpoint.BLE_CONTROL_DEVICE,
649
- data={
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
- if response != 1:
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
- response_json = await response.json()
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 == FEEDER_MINI:
149
- return PetkitEndpoint.MANUAL_FEED_MINI
150
- if device.device_nfo.device_type == FEEDER:
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.MINI_DESICCANT_RESET
159
- if device.device_nfo.device_type == FEEDER:
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=PetkitEndpoint.UPDATE_SETTING,
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=[D4S, D4SH],
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 = [RecordType.EAT, RecordType.FEED, RecordType.MOVE, RecordType.PET]
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
- DESICCANT_RESET = "desiccantReset"
152
- MINI_DESICCANT_RESET = "feedermini/desiccant_reset"
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
- MANUAL_FEED_MINI = "feedermini/save_dailyfeed"
157
- MANUAL_FEED_FRESH_ELEMENT = "feeder/save_dailyfeed"
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"