pypetkitapi 1.11.3__tar.gz → 1.12.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pypetkitapi
3
- Version: 1.11.3
3
+ Version: 1.12.0
4
4
  Summary: Python client for PetKit API
5
5
  License: MIT
6
6
  Author: Jezza34000
@@ -23,12 +23,14 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  ---
25
25
 
26
- [![PyPI](https://img.shields.io/pypi/v/pypetkitapi.svg)][pypi_]
26
+ [![Lifecycle:Maturing](https://img.shields.io/badge/Lifecycle-Maturing-007EC6)](https://github.com/Jezza34000/py-petkit-api/)
27
27
  [![Python Version](https://img.shields.io/pypi/pyversions/pypetkitapi)][python version] [![Actions status](https://github.com/Jezza34000/py-petkit-api/workflows/CI/badge.svg)](https://github.com/Jezza34000/py-petkit-api/actions)
28
28
 
29
+ [![PyPI](https://img.shields.io/pypi/v/pypetkitapi.svg)][pypi_] [![PyPI Downloads](https://static.pepy.tech/badge/pypetkitapi)](https://pepy.tech/projects/pypetkitapi)
30
+
29
31
  ---
30
32
 
31
- [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
33
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
32
34
 
33
35
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
34
36
  [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
@@ -38,8 +40,6 @@ Description-Content-Type: text/markdown
38
40
  [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
39
41
  [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
40
42
 
41
- [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
42
-
43
43
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit]
44
44
  [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black]
45
45
  [![mypy](https://img.shields.io/badge/mypy-checked-blue)](https://mypy.readthedocs.io/en/stable/)
@@ -58,9 +58,10 @@ PetKit Client is a Python library for interacting with the PetKit API. It allows
58
58
 
59
59
  ## Features
60
60
 
61
- Login and session management
62
- Fetch account and device data
63
- Control PetKit devices (Feeder, Litter Box, Water Fountain)
61
+ - Login and session management
62
+ - Fetch account and device data
63
+ - Control PetKit devices (Feeder, Litter Box, Water Fountain, Purifiers)
64
+ - Fetch images & videos produced by devices
64
65
 
65
66
  ## Installation
66
67
 
@@ -72,6 +73,9 @@ pip install pypetkitapi
72
73
 
73
74
  ## Usage Example:
74
75
 
76
+ Here is a simple example of how to use the library to interact with the PetKit API \
77
+ This example is not an exhaustive list of all the features available in the library.
78
+
75
79
  ```python
76
80
  import asyncio
77
81
  import logging
@@ -130,9 +134,14 @@ if __name__ == "__main__":
130
134
 
131
135
  Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
132
136
 
137
+ ## Help and Support
138
+
139
+ A discord server is available for support and help, check here: [here](https://github.com/Jezza34000/homeassistant_petkit)
140
+
133
141
  ## Contributing
134
142
 
135
- Contributions are welcome! Please open an issue or submit a pull request.
143
+ Contributions are welcome!\
144
+ Please open an issue or submit a pull request.
136
145
 
137
146
  ## License
138
147
 
@@ -2,12 +2,14 @@
2
2
 
3
3
  ---
4
4
 
5
- [![PyPI](https://img.shields.io/pypi/v/pypetkitapi.svg)][pypi_]
5
+ [![Lifecycle:Maturing](https://img.shields.io/badge/Lifecycle-Maturing-007EC6)](https://github.com/Jezza34000/py-petkit-api/)
6
6
  [![Python Version](https://img.shields.io/pypi/pyversions/pypetkitapi)][python version] [![Actions status](https://github.com/Jezza34000/py-petkit-api/workflows/CI/badge.svg)](https://github.com/Jezza34000/py-petkit-api/actions)
7
7
 
8
+ [![PyPI](https://img.shields.io/pypi/v/pypetkitapi.svg)][pypi_] [![PyPI Downloads](https://static.pepy.tech/badge/pypetkitapi)](https://pepy.tech/projects/pypetkitapi)
9
+
8
10
  ---
9
11
 
10
- [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
12
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
11
13
 
12
14
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
13
15
  [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
@@ -17,8 +19,6 @@
17
19
  [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
18
20
  [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
19
21
 
20
- [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
21
-
22
22
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit]
23
23
  [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black]
24
24
  [![mypy](https://img.shields.io/badge/mypy-checked-blue)](https://mypy.readthedocs.io/en/stable/)
@@ -37,9 +37,10 @@ PetKit Client is a Python library for interacting with the PetKit API. It allows
37
37
 
38
38
  ## Features
39
39
 
40
- Login and session management
41
- Fetch account and device data
42
- Control PetKit devices (Feeder, Litter Box, Water Fountain)
40
+ - Login and session management
41
+ - Fetch account and device data
42
+ - Control PetKit devices (Feeder, Litter Box, Water Fountain, Purifiers)
43
+ - Fetch images & videos produced by devices
43
44
 
44
45
  ## Installation
45
46
 
@@ -51,6 +52,9 @@ pip install pypetkitapi
51
52
 
52
53
  ## Usage Example:
53
54
 
55
+ Here is a simple example of how to use the library to interact with the PetKit API \
56
+ This example is not an exhaustive list of all the features available in the library.
57
+
54
58
  ```python
55
59
  import asyncio
56
60
  import logging
@@ -109,9 +113,14 @@ if __name__ == "__main__":
109
113
 
110
114
  Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
111
115
 
116
+ ## Help and Support
117
+
118
+ A discord server is available for support and help, check here: [here](https://github.com/Jezza34000/homeassistant_petkit)
119
+
112
120
  ## Contributing
113
121
 
114
- Contributions are welcome! Please open an issue or submit a pull request.
122
+ Contributions are welcome!\
123
+ Please open an issue or submit a pull request.
115
124
 
116
125
  ## License
117
126
 
@@ -51,7 +51,7 @@ from .media import DownloadDecryptMedia, MediaCloud, MediaFile, MediaManager
51
51
  from .purifier_container import Purifier
52
52
  from .water_fountain_container import WaterFountain
53
53
 
54
- __version__ = "1.11.3"
54
+ __version__ = "1.12.0"
55
55
 
56
56
  __all__ = [
57
57
  "CTW3",
@@ -31,7 +31,10 @@ class BluetoothManager:
31
31
  self.client = client
32
32
 
33
33
  async def _get_fountain_instance(self, fountain_id: int) -> "WaterFountain":
34
- """Get the WaterFountain instance for the given fountain_id."""
34
+ """Get the WaterFountain instance for the given fountain_id.
35
+ :param fountain_id: The ID of the fountain to get the instance for.
36
+ :return: The WaterFountain instance for the given fountain_id.
37
+ """
35
38
  from pypetkitapi.water_fountain_container import WaterFountain
36
39
 
37
40
  water_fountain = self.client.petkit_entities.get(fountain_id)
@@ -41,7 +44,10 @@ class BluetoothManager:
41
44
  return water_fountain
42
45
 
43
46
  async def check_relay_availability(self, fountain_id: int) -> bool:
44
- """Check if BLE relay is available for the given fountain_id."""
47
+ """Check if BLE relay is available for the given fountain_id.
48
+ :param fountain_id: The ID of the fountain to check the relay for.
49
+ :return: True if the relay is available, False otherwise.
50
+ """
45
51
  fountain = None
46
52
  for account in self.client.account_data:
47
53
  if account.device_list:
@@ -73,14 +79,17 @@ class BluetoothManager:
73
79
  return True
74
80
 
75
81
  async def open_ble_connection(self, fountain_id: int) -> bool:
76
- """Open a BLE connection to the given fountain_id."""
77
- _LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
82
+ """Open a BLE connection to the given fountain_id.
83
+ :param fountain_id: The ID of the fountain to open the BLE connection for.
84
+ :return: True if the BLE connection was established, False otherwise.
85
+ """
86
+ _LOGGER.debug("Opening BLE connection to fountain %s", fountain_id)
78
87
  water_fountain = await self._get_fountain_instance(fountain_id)
79
88
  if await self.check_relay_availability(fountain_id) is False:
80
- _LOGGER.error("BLE relay not available (id: %s).", fountain_id)
89
+ _LOGGER.debug("BLE relay not available (id: %s).", fountain_id)
81
90
  return False
82
91
  if water_fountain.is_connected is True:
83
- _LOGGER.error("BLE connection already established (id %s)", fountain_id)
92
+ _LOGGER.debug("BLE connection already established (id %s)", fountain_id)
84
93
  return True
85
94
  response = await self.client.req.request(
86
95
  method=HTTPMethod.POST,
@@ -89,7 +98,7 @@ class BluetoothManager:
89
98
  headers=await self.client.get_session_id(),
90
99
  )
91
100
  if response != {"state": 1}:
92
- _LOGGER.error("Unable to open a BLE connection (id %s)", fountain_id)
101
+ _LOGGER.debug("Unable to open a BLE connection (id %s)", fountain_id)
93
102
  water_fountain.is_connected = False
94
103
  return False
95
104
  for attempt in range(BLE_CONNECT_ATTEMPT):
@@ -106,7 +115,7 @@ class BluetoothManager:
106
115
  headers=await self.client.get_session_id(),
107
116
  )
108
117
  if response == 1:
109
- _LOGGER.info(
118
+ _LOGGER.debug(
110
119
  "BLE connection established successfully (id %s)", fountain_id
111
120
  )
112
121
  water_fountain.is_connected = True
@@ -115,7 +124,7 @@ class BluetoothManager:
115
124
  )
116
125
  return True
117
126
  await asyncio.sleep(4)
118
- _LOGGER.error(
127
+ _LOGGER.debug(
119
128
  "Failed to establish BLE connection after %s attempts (id %s)",
120
129
  BLE_CONNECT_ATTEMPT,
121
130
  fountain_id,
@@ -124,12 +133,15 @@ class BluetoothManager:
124
133
  return False
125
134
 
126
135
  async def close_ble_connection(self, fountain_id: int) -> None:
127
- """Close the BLE connection to the given fountain_id."""
128
- _LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
136
+ """Close the BLE connection to the given fountain_id.
137
+ :param fountain_id: The ID of the fountain to close the BLE connection for.
138
+ :return: None
139
+ """
140
+ _LOGGER.debug("Closing BLE connection to fountain %s", fountain_id)
129
141
  water_fountain = await self._get_fountain_instance(fountain_id)
130
142
 
131
143
  if water_fountain.is_connected is False:
132
- _LOGGER.error(
144
+ _LOGGER.debug(
133
145
  "BLE connection not established. Cannot close (id %s)", fountain_id
134
146
  )
135
147
  return
@@ -140,12 +152,16 @@ class BluetoothManager:
140
152
  data={"bleId": fountain_id, "type": 24, "mac": water_fountain.mac},
141
153
  headers=await self.client.get_session_id(),
142
154
  )
143
- _LOGGER.info("BLE connection closed successfully (id %s)", fountain_id)
155
+ _LOGGER.debug("BLE connection closed successfully (id %s)", fountain_id)
144
156
 
145
157
  async def get_ble_cmd_data(
146
158
  self, fountain_command: list, counter: int
147
159
  ) -> tuple[int, str]:
148
- """Get the BLE command data for the given fountain_command."""
160
+ """Get the BLE command data for the given fountain_command.
161
+ :param fountain_command: The fountain command to get the BLE data for.
162
+ :param counter: The BLE counter for the fountain.
163
+ :return: The BLE command code and the encoded BLE data.
164
+ """
149
165
  cmd_code = fountain_command[0]
150
166
  modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
151
167
  ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
@@ -154,17 +170,24 @@ class BluetoothManager:
154
170
 
155
171
  @staticmethod
156
172
  async def _encode_ble_data(byte_list: list) -> str:
157
- """Encode the given byte_list to a base64 encoded string."""
173
+ """Encode the given byte_list to a base64 encoded string.
174
+ :param byte_list: The byte list to encode.
175
+ :return: The base64 encoded string.
176
+ """
158
177
  byte_array = bytearray(byte_list)
159
178
  b64_encoded = base64.b64encode(byte_array)
160
179
  return urllib.parse.quote(b64_encoded)
161
180
 
162
181
  async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
163
- """Send the given BLE command to the fountain_id."""
164
- _LOGGER.info("Sending BLE command to fountain %s", fountain_id)
182
+ """Send the given BLE command to the fountain_id.
183
+ :param fountain_id: The ID of the fountain to send the command to.
184
+ :param command: The command to send to the fountain.
185
+ :return: True if the command was sent successfully, False otherwise.
186
+ """
187
+ _LOGGER.debug("Sending BLE command to fountain %s", fountain_id)
165
188
  water_fountain = await self._get_fountain_instance(fountain_id)
166
189
  if water_fountain.is_connected is False:
167
- _LOGGER.error("BLE connection not established (id %s)", fountain_id)
190
+ _LOGGER.debug("BLE connection not established (id %s)", fountain_id)
168
191
  return False
169
192
  command_data = FOUNTAIN_COMMAND.get(command)
170
193
  if command_data is None:
@@ -190,5 +213,5 @@ class BluetoothManager:
190
213
  if response != 1:
191
214
  _LOGGER.error("Failed to send BLE command (id %s)", fountain_id)
192
215
  return False
193
- _LOGGER.info("BLE command sent successfully (id %s)", fountain_id)
216
+ _LOGGER.debug("BLE command sent successfully (id %s)", fountain_id)
194
217
  return True
@@ -6,6 +6,7 @@ from enum import StrEnum
6
6
  import hashlib
7
7
  from http import HTTPMethod
8
8
  import logging
9
+ from typing import Any
9
10
 
10
11
  import aiohttp
11
12
  from aiohttp import ContentTypeError
@@ -146,7 +147,9 @@ class PetKitClient:
146
147
  return False
147
148
 
148
149
  async def login(self, valid_code: str | None = None) -> None:
149
- """Login to the PetKit service and retrieve the appropriate server."""
150
+ """Login to the PetKit service and retrieve the appropriate server.
151
+ :param valid_code: The valid code sent to the user's email.
152
+ """
150
153
  # Retrieve the list of servers
151
154
  self._session = None
152
155
  await self._get_base_url()
@@ -268,7 +271,9 @@ class PetKitClient:
268
271
  _LOGGER.debug("Petkit data fetched successfully in: %s", end_time - start_time)
269
272
 
270
273
  def _collect_devices(self) -> list[Device]:
271
- """Collect all devices from account data."""
274
+ """Collect all devices from account data.
275
+ :return: List of devices.
276
+ """
272
277
  device_list = []
273
278
  for account in self.account_data:
274
279
  _LOGGER.debug("List devices data for account: %s", account)
@@ -279,7 +284,10 @@ class PetKitClient:
279
284
  return device_list
280
285
 
281
286
  def _prepare_tasks(self, device_list: list[Device]) -> tuple[list, list, list]:
282
- """Prepare main and record tasks based on device types."""
287
+ """Prepare main and record tasks based on device types.
288
+ :param device_list: List of devices.
289
+ :return: Tuple of main tasks, record tasks and media tasks.
290
+ """
283
291
  main_tasks: list = []
284
292
  record_tasks: list = []
285
293
  media_tasks: list = []
@@ -312,8 +320,13 @@ class PetKitClient:
312
320
 
313
321
  def _add_lb_task_by_type(
314
322
  self, record_tasks: list, media_tasks: list, device_type: str, device: Device
315
- ):
316
- """Add specific tasks for litter box devices."""
323
+ ) -> None:
324
+ """Add specific tasks for litter box devices.
325
+ :param record_tasks: List of record tasks.
326
+ :param media_tasks: List of media tasks.
327
+ :param device_type: Device type.
328
+ :param device: Device data.
329
+ """
317
330
  if device_type in LITTER_NO_CAMERA:
318
331
  record_tasks.append(self._fetch_device_data(device, LitterStats))
319
332
  if device_type in LITTER_WITH_CAMERA:
@@ -322,8 +335,12 @@ class PetKitClient:
322
335
 
323
336
  def _add_feeder_task_by_type(
324
337
  self, media_tasks: list, device_type: str, device: Device
325
- ):
326
- """Add specific tasks for feeder box devices."""
338
+ ) -> None:
339
+ """Add specific tasks for feeder box devices.
340
+ :param media_tasks: List of media tasks.
341
+ :param device_type: Device type.
342
+ :param device: Device data.
343
+ """
327
344
  if device_type in FEEDER_WITH_CAMERA:
328
345
  media_tasks.append(self._fetch_media(device))
329
346
 
@@ -337,7 +354,9 @@ class PetKitClient:
337
354
  await asyncio.gather(*stats_tasks)
338
355
 
339
356
  async def _fetch_media(self, device: Device) -> None:
340
- """Fetch media data from the PetKit servers."""
357
+ """Fetch media data from the PetKit servers.
358
+ :param device: Device data.
359
+ """
341
360
  _LOGGER.debug("Fetching media data for device: %s", device.device_id)
342
361
 
343
362
  device_entity = self.petkit_entities[device.device_id]
@@ -360,7 +379,10 @@ class PetKitClient:
360
379
  | LitterStats
361
380
  ],
362
381
  ) -> None:
363
- """Fetch the device data from the PetKit servers."""
382
+ """Fetch the device data from the PetKit servers.
383
+ :param device: Device data.
384
+ :param data_class: Data class
385
+ """
364
386
  device_type = device.device_type
365
387
 
366
388
  _LOGGER.debug("Reading device type : %s (id=%s)", device_type, device.device_id)
@@ -398,7 +420,7 @@ class PetKitClient:
398
420
  _LOGGER.error("Unexpected response type: %s", type(response))
399
421
  return
400
422
 
401
- # Dispatch to the appropriate handler
423
+ # Dispatch to the appropriate data handler
402
424
  handler = data_handlers.get(data_class.data_type)
403
425
  if handler:
404
426
  await handler(self, device, device_data, device_type)
@@ -406,14 +428,21 @@ class PetKitClient:
406
428
  _LOGGER.error("Unknown data type: %s", data_class.data_type)
407
429
 
408
430
  @data_handler(DEVICE_DATA)
409
- async def _handle_device_data(self, device, device_data, device_type):
431
+ async def _handle_device_data(
432
+ self,
433
+ device: Device,
434
+ device_data: Feeder | Litter | WaterFountain | Purifier,
435
+ device_type: str,
436
+ ):
410
437
  """Handle device data."""
411
438
  self.petkit_entities[device.device_id] = device_data
412
439
  self.petkit_entities[device.device_id].device_nfo = device
413
440
  _LOGGER.debug("Device data fetched OK for %s", device_type)
414
441
 
415
442
  @data_handler(DEVICE_RECORDS)
416
- async def _handle_device_records(self, device, device_data, device_type):
443
+ async def _handle_device_records(
444
+ self, device: Device, device_data, device_type: str
445
+ ):
417
446
  """Handle device records."""
418
447
  entity = self.petkit_entities.get(device.device_id)
419
448
  if entity and isinstance(entity, (Feeder, Litter, WaterFountain)):
@@ -426,7 +455,7 @@ class PetKitClient:
426
455
  )
427
456
 
428
457
  @data_handler(DEVICE_STATS)
429
- async def _handle_device_stats(self, device, device_data, device_type):
458
+ async def _handle_device_stats(self, device: Device, device_data, device_type: str):
430
459
  """Handle device stats."""
431
460
  entity = self.petkit_entities.get(device.device_id)
432
461
  if isinstance(entity, Litter):
@@ -442,7 +471,9 @@ class PetKitClient:
442
471
  )
443
472
 
444
473
  async def get_pets_list(self) -> list[Pet]:
445
- """Extract and return the list of pets."""
474
+ """Extract and return the list of pets.
475
+ :return: List of pets.
476
+ """
446
477
  return [
447
478
  entity
448
479
  for entity in self.petkit_entities.values()
@@ -451,18 +482,28 @@ class PetKitClient:
451
482
 
452
483
  @staticmethod
453
484
  def get_safe_value(value: int | None, default: int = 0) -> int:
454
- """Return the value if not None, otherwise return the default."""
485
+ """Return the value if not None, otherwise return the default.
486
+ :param value: Value to check.
487
+ :param default: Default value.
488
+ :return: Value or default.
489
+ """
455
490
  return value if value is not None else default
456
491
 
457
492
  @staticmethod
458
493
  def calculate_duration(start: int | None, end: int | None) -> int:
459
- """Calculate the duration, ensuring both start and end are not None."""
494
+ """Calculate the duration, ensuring both start and end are not None.
495
+ :param start: Start time.
496
+ :param end: End time.
497
+ :return: Duration.
498
+ """
460
499
  if start is None or end is None:
461
500
  return 0
462
501
  return end - start
463
502
 
464
503
  async def populate_pet_stats(self, stats_data: Litter) -> None:
465
- """Collect data from litter data to populate pet stats."""
504
+ """Collect data from litter data to populate pet stats.
505
+ :param stats_data: Litter data.
506
+ """
466
507
 
467
508
  if not stats_data.device_nfo:
468
509
  _LOGGER.warning(
@@ -476,16 +517,21 @@ class PetKitClient:
476
517
  stats_data.device_nfo.device_type in [T3, T4]
477
518
  and stats_data.device_records
478
519
  ):
479
- await self._process_t3_t4(pet, stats_data)
520
+ await self._process_litter_no_camera(pet, stats_data)
480
521
  elif (
481
522
  stats_data.device_nfo.device_type in [T5, T6]
482
523
  and stats_data.device_pet_graph_out
483
524
  ):
484
- await self._process_t5_t6(pet, stats_data)
525
+ await self._process_litter_camera(pet, stats_data)
485
526
 
486
- async def _process_t3_t4(self, pet, device_records):
487
- """Process T3/T4 devices records."""
488
- for stat in device_records.device_records:
527
+ async def _process_litter_no_camera(self, pet: Pet, device_records: Litter) -> None:
528
+ """Process litter T3/T4 records (litter without camera).
529
+ :param pet: Pet data.
530
+ :param device_records: Litter data.
531
+ """
532
+ for stat in (
533
+ s for s in device_records.device_records or [] if isinstance(s, LitterStats)
534
+ ):
489
535
  if stat.pet_id == pet.pet_id and (
490
536
  pet.last_litter_usage is None
491
537
  or self.get_safe_value(stat.timestamp) > pet.last_litter_usage
@@ -498,24 +544,34 @@ class PetKitClient:
498
544
  stat.content.time_in if stat.content else None,
499
545
  stat.content.time_out if stat.content else None,
500
546
  )
501
- pet.last_device_used = device_records.device_nfo.device_name
502
-
503
- async def _process_t5_t6(self, pet, pet_graphs):
504
- """Process T5/T6 pet graphs."""
505
- for graph in pet_graphs.device_pet_graph_out:
547
+ pet.last_device_used = getattr(
548
+ device_records.device_nfo, "device_name", "Unknown"
549
+ ).capitalize()
550
+
551
+ async def _process_litter_camera(self, pet: Pet, pet_graphs: Litter) -> None:
552
+ """Process litter T5/T6 records (litter WITH camera).
553
+ :param pet: Pet data.
554
+ :param pet_graphs: Litter data.
555
+ """
556
+ for graph in pet_graphs.device_pet_graph_out or []:
506
557
  if graph.pet_id == pet.pet_id and (
507
558
  pet.last_litter_usage is None
508
559
  or self.get_safe_value(graph.time) > pet.last_litter_usage
509
560
  ):
510
- pet.last_litter_usage = graph.time
561
+ pet.last_litter_usage = graph.time or 0
511
562
  pet.last_measured_weight = self.get_safe_value(
512
563
  graph.content.pet_weight if graph.content else None
513
564
  )
514
- pet.last_duration_usage = self.get_safe_value(graph.toilet_time)
515
- pet.last_device_used = pet_graphs.device_nfo.device_name
565
+ pet.last_duration_usage = self.get_safe_value(graph.toilet_time) or 0
566
+ pet.last_device_used = getattr(
567
+ pet_graphs.device_nfo, "device_name", "Unknown"
568
+ ).capitalize()
516
569
 
517
570
  async def get_cloud_video(self, video_url: str) -> dict[str, str | int] | None:
518
- """Get the video m3u8 link from the cloud."""
571
+ """Get the video m3u8 link from the cloud.
572
+ :param video_url: URL of the video.
573
+ :return: Video data.
574
+ """
519
575
  response = await self.req.request(
520
576
  method=HTTPMethod.POST,
521
577
  url=video_url,
@@ -528,7 +584,9 @@ class PetKitClient:
528
584
  return None
529
585
  return response[0]
530
586
 
531
- async def extract_segments_m3u8(self, m3u8_url: str) -> tuple[str, str, list[str]]:
587
+ async def extract_segments_m3u8(
588
+ self, m3u8_url: str
589
+ ) -> tuple[Any, str | None, list[str | None]]:
532
590
  """Extract segments from the m3u8 file.
533
591
  :param: m3u8_url: URL of the m3u8 file
534
592
  :return: aes_key, key_iv, segment_lst
@@ -542,7 +600,7 @@ class PetKitClient:
542
600
  m3u8_obj = m3u8.loads(response[RES_KEY])
543
601
 
544
602
  if not m3u8_obj.segments or not m3u8_obj.keys:
545
- raise PetkitInvalidResponseFormat("No segments or key found in m3u8 file.")
603
+ return None, None, []
546
604
 
547
605
  # Extract segments from m3u8 file
548
606
  segment_lst = [segment.uri for segment in m3u8_obj.segments]
@@ -550,6 +608,9 @@ class PetKitClient:
550
608
  key_uri = m3u8_obj.keys[0].uri
551
609
  key_iv = str(m3u8_obj.keys[0].iv)
552
610
 
611
+ if not key_uri or not key_iv:
612
+ return None, None, []
613
+
553
614
  # Extract aes_key from video segments
554
615
  response = await self.req.request(
555
616
  method=HTTPMethod.GET,
@@ -565,7 +626,12 @@ class PetKitClient:
565
626
  action: StrEnum,
566
627
  setting: dict | None = None,
567
628
  ) -> bool:
568
- """Control the device using the PetKit API."""
629
+ """Control the device using the PetKit API.
630
+ :param device_id: ID of the device.
631
+ :param action: Action to perform.
632
+ :param setting: Setting to apply.
633
+ :return: True if the command was successful, False otherwise.
634
+ """
569
635
  device = self.petkit_entities.get(device_id, None)
570
636
  if not device:
571
637
  raise PypetkitError(f"Device with ID {device_id} not found.")
@@ -668,7 +734,15 @@ class PrepReq:
668
734
  data=None,
669
735
  headers=None,
670
736
  ) -> dict:
671
- """Make a request to the PetKit API."""
737
+ """Make a request to the PetKit API.
738
+ :param method: HTTP method.
739
+ :param url: URL of the API endpoint.
740
+ :param full_url: Use full URL.
741
+ :param params: Parameters to send.
742
+ :param data: Data to send.
743
+ :param headers: Headers to send.
744
+ :return: Response from the API.
745
+ """
672
746
  _url = url if full_url else "/".join(s.strip("/") for s in [self.base_url, url])
673
747
  _headers = {**self.base_headers, **(headers or {})}
674
748
  _LOGGER.debug("Request: %s %s", method, _url)
@@ -686,7 +760,11 @@ class PrepReq:
686
760
 
687
761
  @staticmethod
688
762
  async def _handle_response(response: aiohttp.ClientResponse, url: str) -> dict:
689
- """Handle the response from the PetKit API."""
763
+ """Handle the response from the PetKit API.
764
+ :param response: Response from the API.
765
+ :param url: URL of the API endpoint.
766
+ :return: Data from the API.
767
+ """
690
768
  try:
691
769
  response.raise_for_status()
692
770
  except aiohttp.ClientResponseError as e:
@@ -147,6 +147,7 @@ class StateLitter(BaseModel):
147
147
  camera_status: int | None = Field(None, alias="cameraStatus")
148
148
  dump_state: int | None = Field(None, alias="dumpState")
149
149
  liquid: int | None = None
150
+ light_state: dict | None = Field(None, alias="lightState")
150
151
  pack_state: int | None = Field(None, alias="packState")
151
152
  package_install: int | None = Field(None, alias="packageInstall")
152
153
  package_secret: str | None = Field(None, alias="packageSecret")
@@ -6,6 +6,7 @@ import asyncio
6
6
  from dataclasses import dataclass
7
7
  from datetime import datetime
8
8
  import logging
9
+ import os
9
10
  from pathlib import Path
10
11
  import re
11
12
  from typing import Any
@@ -105,10 +106,7 @@ class MediaManager:
105
106
  async def gather_all_media_from_disk(
106
107
  self, storage_path: Path, device_id: int
107
108
  ) -> list[MediaFile]:
108
- """Construct the media file table for disk storage.
109
- :param storage_path: Path to the storage directory
110
- :param device_id: Device ID
111
- """
109
+ """Construct the media file table for disk storage."""
112
110
  self.media_table.clear()
113
111
 
114
112
  today_str = datetime.now().strftime("%Y%m%d")
@@ -116,121 +114,171 @@ class MediaManager:
116
114
 
117
115
  _LOGGER.debug("Populating files from directory %s", base_path)
118
116
 
117
+ valid_pattern = re.compile(
118
+ rf"^{device_id}_\d+\.({MediaType.IMAGE}|{MediaType.VIDEO})$"
119
+ )
120
+
119
121
  for record_type in RecordType:
120
122
  record_path = base_path / record_type
121
- snapshot_path = record_path / "snapshot"
122
- video_path = record_path / "video"
123
-
124
- # Regex pattern to match valid filenames
125
- valid_pattern = re.compile(
126
- rf"^{device_id}_\d+\.({MediaType.IMAGE}|{MediaType.VIDEO})$"
127
- )
123
+ subdirs = [record_path / "snapshot", record_path / "video"]
124
+ for subdir in subdirs:
125
+ await self._process_subdir(
126
+ subdir, device_id, record_type, valid_pattern
127
+ )
128
128
 
129
- # Populate the media table with event_id from filenames
130
- for subdir in [snapshot_path, video_path]:
131
-
132
- # Ensure the directories exist
133
- if not await aiofiles.os.path.exists(subdir):
134
- _LOGGER.debug("Path does not exist, skip : %s", subdir)
135
- continue
136
-
137
- _LOGGER.debug("Scanning files into : %s", subdir)
138
- entries = await aiofiles.os.scandir(subdir)
139
- for entry in entries:
140
- if entry.is_file() and valid_pattern.match(entry.name):
141
- _LOGGER.debug("Media found: %s", entry.name)
142
- event_id = Path(entry.name).stem
143
- timestamp = Path(entry.name).stem.split("_")[1]
144
- media_type_str = Path(entry.name).suffix.lstrip(".")
145
- try:
146
- media_type = MediaType(media_type_str)
147
- except ValueError:
148
- _LOGGER.warning("Unknown media type: %s", media_type_str)
149
- continue
150
- self.media_table.append(
151
- MediaFile(
152
- event_id=event_id,
153
- device_id=device_id,
154
- timestamp=int(timestamp),
155
- event_type=RecordType(record_type),
156
- full_file_path=subdir / entry.name,
157
- media_type=MediaType(media_type),
158
- )
159
- )
160
129
  _LOGGER.debug("OK, Media table populated with %s files", len(self.media_table))
161
130
  return self.media_table
162
131
 
132
+ async def _process_subdir(
133
+ self,
134
+ subdir: Path,
135
+ device_id: int,
136
+ record_type: RecordType,
137
+ valid_pattern: re.Pattern,
138
+ ) -> None:
139
+ """Process a subdirectory to collect media files."""
140
+ if not await aiofiles.os.path.exists(subdir):
141
+ _LOGGER.debug("Path does not exist, skip: %s", subdir)
142
+ return
143
+
144
+ _LOGGER.debug("Scanning files into: %s", subdir)
145
+ entries = await aiofiles.os.scandir(subdir)
146
+ for entry in entries:
147
+ media_file = await self._create_media_file(
148
+ entry, device_id, record_type, subdir, valid_pattern
149
+ )
150
+ if media_file:
151
+ self.media_table.append(media_file)
152
+
153
+ @staticmethod
154
+ async def _create_media_file(
155
+ entry: os.DirEntry,
156
+ device_id: int,
157
+ record_type: RecordType,
158
+ subdir: Path,
159
+ valid_pattern: re.Pattern,
160
+ ) -> MediaFile | None:
161
+ """Create a MediaFile from a directory entry if valid."""
162
+ if not entry.is_file() or not valid_pattern.match(entry.name):
163
+ return None
164
+
165
+ try:
166
+ stem = Path(entry.name).stem
167
+ parts = stem.split("_")
168
+ timestamp = int(parts[1])
169
+ media_type_str = Path(entry.name).suffix.lstrip(".")
170
+ media_type = MediaType(media_type_str)
171
+ except (ValueError, IndexError) as e:
172
+ _LOGGER.warning("Invalid file %s: %s", entry.name, str(e))
173
+ return None
174
+
175
+ return MediaFile(
176
+ event_id=stem,
177
+ device_id=device_id,
178
+ timestamp=timestamp,
179
+ media_type=media_type,
180
+ event_type=record_type,
181
+ full_file_path=subdir / entry.name,
182
+ )
183
+
163
184
  async def list_missing_files(
164
185
  self,
165
186
  media_cloud_list: list[MediaCloud],
166
187
  dl_type: list[MediaType] | None = None,
167
188
  event_type: list[RecordType] | None = None,
168
189
  ) -> list[MediaCloud]:
169
- """Compare MediaCloud objects with MediaFile objects and return a list of missing MediaCloud objects.
170
- :param media_cloud_list: List of MediaCloud objects
171
- :param dl_type: List of media types to download
172
- :param event_type: List of event types to filter
173
- :return: List of missing MediaCloud objects
174
- """
175
- missing_media: list[MediaCloud] = []
190
+ """Compare MediaCloud objects with MediaFile objects and return missing ones."""
191
+ if not dl_type or not event_type:
192
+ _LOGGER.debug("Missing dl_type or event_type, no downloads")
193
+ return []
194
+
176
195
  existing_event_ids = {media_file.event_id for media_file in self.media_table}
196
+ return [
197
+ mc
198
+ for mc in media_cloud_list
199
+ if self._should_process_media(mc, event_type, dl_type, existing_event_ids)
200
+ ]
177
201
 
178
- if dl_type is None or event_type is None or not dl_type or not event_type:
179
- _LOGGER.debug(
180
- "Missing dl_type or event_type parameters, no media file will be downloaded"
202
+ def _should_process_media(
203
+ self,
204
+ media_cloud: MediaCloud,
205
+ event_filter: list[RecordType],
206
+ dl_types: list[MediaType],
207
+ existing_ids: set[str],
208
+ ) -> bool:
209
+ """Determine if a media should be processed as missing."""
210
+ if self._should_skip_event_type(media_cloud.event_type, event_filter):
211
+ return False
212
+
213
+ return self._is_media_missing(media_cloud, dl_types, existing_ids)
214
+
215
+ @staticmethod
216
+ def _should_skip_event_type(
217
+ event_type: RecordType, event_filter: list[RecordType]
218
+ ) -> bool:
219
+ """Check if event type should be skipped."""
220
+ if event_type not in event_filter:
221
+ _LOGGER.debug("Skipping filtered event type: %s", event_type)
222
+ return True
223
+ return False
224
+
225
+ def _is_media_missing(
226
+ self, media_cloud: MediaCloud, dl_types: list[MediaType], existing_ids: set[str]
227
+ ) -> bool:
228
+ """Check if any media type is missing."""
229
+ missing_image = self._is_image_missing(media_cloud, dl_types, existing_ids)
230
+ missing_video = self._is_video_missing(media_cloud, dl_types, existing_ids)
231
+
232
+ if missing_image or missing_video:
233
+ self._log_missing_details(media_cloud, missing_image, missing_video)
234
+ return True
235
+ return False
236
+
237
+ def _is_image_missing(
238
+ self, media_cloud: MediaCloud, dl_types: list[MediaType], existing_ids: set[str]
239
+ ) -> bool:
240
+ """Check if image is missing."""
241
+ return bool(
242
+ media_cloud.image
243
+ and MediaType.IMAGE in dl_types
244
+ and not self._media_exists(
245
+ media_cloud.event_id, MediaType.IMAGE, existing_ids
181
246
  )
182
- return missing_media
247
+ )
183
248
 
184
- for media_cloud in media_cloud_list:
185
- # Skip if event type is not in the event filter
186
- if event_type and media_cloud.event_type not in event_type:
187
- _LOGGER.debug(
188
- "Skipping event type %s, is filtered", media_cloud.event_type
189
- )
190
- continue
249
+ def _is_video_missing(
250
+ self, media_cloud: MediaCloud, dl_types: list[MediaType], existing_ids: set[str]
251
+ ) -> bool:
252
+ """Check if video is missing."""
253
+ return bool(
254
+ media_cloud.video
255
+ and MediaType.VIDEO in dl_types
256
+ and not self._media_exists(
257
+ media_cloud.event_id, MediaType.VIDEO, existing_ids
258
+ )
259
+ )
191
260
 
192
- # Check if the media file is missing
193
- is_missing = False
194
- if media_cloud.event_id not in existing_event_ids:
195
- _LOGGER.debug(
196
- "Media file IMG/VIDEO id : %s are missing", media_cloud.event_id
197
- )
198
- is_missing = True # Both image and video are missing
199
- else:
200
- # Check for missing image
201
- if (
202
- media_cloud.image
203
- and MediaType.IMAGE
204
- in (dl_type or [MediaType.IMAGE, MediaType.VIDEO])
205
- and not any(
206
- media_file.event_id == media_cloud.event_id
207
- and media_file.media_type == MediaType.IMAGE
208
- for media_file in self.media_table
209
- )
210
- ):
211
- _LOGGER.debug(
212
- "Media file IMG id : %s is missing", media_cloud.event_id
213
- )
214
- is_missing = True
215
- # Check for missing video
216
- if (
217
- media_cloud.video
218
- and MediaType.VIDEO
219
- in (dl_type or [MediaType.IMAGE, MediaType.VIDEO])
220
- and not any(
221
- media_file.event_id == media_cloud.event_id
222
- and media_file.media_type == MediaType.VIDEO
223
- for media_file in self.media_table
224
- )
225
- ):
226
- _LOGGER.debug(
227
- "Media file VIDEO id : %s is missing", media_cloud.event_id
228
- )
229
- is_missing = True
261
+ def _media_exists(
262
+ self, event_id: str, media_type: MediaType, existing_ids: set[str]
263
+ ) -> bool:
264
+ """Check if media exists in local storage."""
265
+ for mf in self.media_table:
266
+ if event_id in existing_ids and mf.media_type == media_type:
267
+ return True
268
+ return False
230
269
 
231
- if is_missing:
232
- missing_media.append(media_cloud)
233
- return missing_media
270
+ @staticmethod
271
+ def _log_missing_details(
272
+ media_cloud: MediaCloud, missing_image: bool, missing_video: bool
273
+ ) -> None:
274
+ """Log details about missing media."""
275
+ log_msg = "Media missing for event %s: "
276
+ details = []
277
+ if missing_image:
278
+ details.append("IMAGE")
279
+ if missing_video:
280
+ details.append("VIDEO")
281
+ _LOGGER.debug("%s %s %s", log_msg, media_cloud.event_id, " + ".join(details))
234
282
 
235
283
  async def _process_feeder(self, feeder: Feeder) -> list[MediaCloud]:
236
284
  """Process media files for a Feeder device.
@@ -272,11 +320,8 @@ class MediaManager:
272
320
  )
273
321
  cp_sub = self.is_subscription_active(device_obj)
274
322
 
275
- if not feeder_id:
276
- _LOGGER.warning("Missing feeder_id for record")
277
- return media_files
278
-
279
- if not record.items:
323
+ if not feeder_id or not record.items:
324
+ _LOGGER.warning("Missing feeder_id or items for record")
280
325
  return media_files
281
326
 
282
327
  for item in record.items:
@@ -287,18 +332,15 @@ class MediaManager:
287
332
  if timestamp is None:
288
333
  _LOGGER.warning("Missing timestamp for record item")
289
334
  continue
290
- if not item.event_id:
335
+ if not item.event_id or not item.aes_key:
291
336
  # Skip feed event in the future
292
337
  _LOGGER.debug(
293
- "Missing event_id for record item (probably a feed event not yet completed)"
338
+ "Missing event_id or aes_key for record item (probably a feed event not yet completed, or uploaded)"
294
339
  )
295
340
  continue
296
341
  if not user_id:
297
342
  _LOGGER.warning("Missing user_id for record item")
298
343
  continue
299
- if not item.aes_key:
300
- _LOGGER.debug("Missing aes_key for record item")
301
- continue
302
344
 
303
345
  date_str = await self.get_date_from_ts(timestamp)
304
346
  filepath = f"{feeder_id}/{date_str}/{record_type.name.lower()}"
@@ -331,30 +373,22 @@ class MediaManager:
331
373
  user_id = litter.user.id if litter.user else None
332
374
  cp_sub = self.is_subscription_active(litter)
333
375
 
334
- if not litter_id:
335
- _LOGGER.warning("Missing litter_id for record")
336
- return media_files
337
-
338
- if not device_type:
339
- _LOGGER.warning("Missing device_type for record")
340
- return media_files
341
-
342
- if not user_id:
343
- _LOGGER.warning("Missing user_id for record")
376
+ if not litter_id or not device_type or not user_id:
377
+ _LOGGER.warning(
378
+ "Missing one or more of mandatory information : litter_id/device_id/user_id for record"
379
+ )
344
380
  return media_files
345
381
 
346
382
  if not records:
383
+ _LOGGER.debug("No records found for %s", litter.name)
347
384
  return media_files
348
385
 
349
386
  for record in records:
350
387
  if not isinstance(record, LitterRecord):
351
388
  _LOGGER.debug("Record is empty")
352
389
  continue
353
- if not record.event_id:
354
- _LOGGER.debug("Missing event_id for record item")
355
- continue
356
- if not record.aes_key:
357
- _LOGGER.debug("Missing aes_key for record item")
390
+ if not record.event_id or not record.aes_key:
391
+ _LOGGER.debug("Missing event_id or aes_key for record item")
358
392
  continue
359
393
  if record.timestamp is None:
360
394
  _LOGGER.debug("Missing timestamp for record item")
@@ -537,10 +571,10 @@ class DownloadDecryptMedia:
537
571
  elif len(segment_files) == 1:
538
572
  _LOGGER.debug("Single file segment, no need to concatenate")
539
573
  elif len(segment_files) > 1:
540
- _LOGGER.debug("Concatenating segments %s", len(segment_files))
574
+ _LOGGER.debug("Concatenating video with %s segments", len(segment_files))
541
575
  await self._concat_segments(segment_files, file_name)
542
576
 
543
- async def _get_m3u8_segments(self) -> tuple[str | None, str | None, list[str]]:
577
+ async def _get_m3u8_segments(self) -> tuple[Any, str | None, list[str | None]]:
544
578
  """Extract the segments from a m3u8 file.
545
579
  :return: Tuple of AES key, IV key, and list of segment URLs
546
580
  """
@@ -557,13 +591,19 @@ class DownloadDecryptMedia:
557
591
  raise ValueError("Missing mediaApi in video data")
558
592
  return await self.client.extract_segments_m3u8(str(media_api))
559
593
 
560
- async def _get_file(self, url: str, aes_key: str, full_filename: str) -> bool:
594
+ async def _get_file(
595
+ self, url: str | None, aes_key: str | None, full_filename: str | None
596
+ ) -> bool:
561
597
  """Download a file from a URL and decrypt it.
562
598
  :param url: URL of the file to download.
563
599
  :param aes_key: AES key used for decryption.
564
600
  :param full_filename: Name of the file to save.
565
601
  :return: True if the file was downloaded successfully, False otherwise.
566
602
  """
603
+ if not url or not aes_key or not full_filename:
604
+ _LOGGER.debug("Missing URL, AES key, or filename")
605
+ return False
606
+
567
607
  # Download the file
568
608
  async with aiohttp.ClientSession() as session, session.get(url) as response:
569
609
  if response.status != 200:
@@ -623,7 +663,7 @@ class DownloadDecryptMedia:
623
663
  try:
624
664
  decrypted_data = unpad(decrypted_data, AES.block_size)
625
665
  except ValueError as e:
626
- _LOGGER.debug("Warning: Padding error occurred, ignoring error: %s", e)
666
+ _LOGGER.debug("Ignoring unpad warning : %s", e)
627
667
  return decrypted_data
628
668
 
629
669
  async def _concat_segments(self, ts_files: list[Path], output_file) -> None:
@@ -27,10 +27,10 @@ class Type(BaseModel):
27
27
  is_custom: int | None = Field(0, alias="isCustom")
28
28
  name: str | None = None
29
29
  priority: int | None = None
30
- repeat_option: str | None = Field(alias="repeatOption")
30
+ repeat_option: str | None = Field(None, alias="repeatOption")
31
31
  rpt: str | None = None
32
- schedule_appoint: str | None = Field(alias="scheduleAppoint")
33
- with_device_type: str | None = Field(alias="withDeviceType")
32
+ schedule_appoint: str | None = Field(None, alias="scheduleAppoint")
33
+ with_device_type: str | None = Field(None, alias="withDeviceType")
34
34
  with_pet: int | None = Field(0, alias="withPet")
35
35
 
36
36
 
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
187
187
 
188
188
  [tool.poetry]
189
189
  name = "pypetkitapi"
190
- version = "1.11.3"
190
+ version = "1.12.0"
191
191
  description = "Python client for PetKit API"
192
192
  authors = ["Jezza34000 <info@mail.com>"]
193
193
  readme = "README.md"
@@ -209,7 +209,7 @@ ruff = "^0.8.1"
209
209
  types-aiofiles = "^24.1.0.20240626"
210
210
 
211
211
  [tool.bumpver]
212
- current_version = "1.11.3"
212
+ current_version = "1.12.0"
213
213
  version_pattern = "MAJOR.MINOR.PATCH"
214
214
  commit_message = "bump version {old_version} -> {new_version}"
215
215
  tag_message = "{new_version}"
File without changes