pypetkitapi 1.2.4__tar.gz → 1.3.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.1
2
2
  Name: pypetkitapi
3
- Version: 1.2.4
3
+ Version: 1.3.0
4
4
  Summary: Python client for PetKit API
5
5
  Home-page: https://github.com/Jezza34000/pypetkit
6
6
  License: MIT
@@ -14,6 +14,7 @@ from pypetkitapi.command import ACTIONS_MAP
14
14
  from pypetkitapi.const import (
15
15
  DEVICE_DATA,
16
16
  DEVICE_RECORDS,
17
+ DEVICE_STATS,
17
18
  DEVICES_FEEDER,
18
19
  DEVICES_LITTER_BOX,
19
20
  DEVICES_WATER_FOUNTAIN,
@@ -21,6 +22,8 @@ from pypetkitapi.const import (
21
22
  LOGIN_DATA,
22
23
  RES_KEY,
23
24
  SUCCESS_KEY,
25
+ T4,
26
+ T6,
24
27
  Header,
25
28
  PetkitDomain,
26
29
  PetkitEndpoint,
@@ -35,7 +38,7 @@ from pypetkitapi.exceptions import (
35
38
  PypetkitError,
36
39
  )
37
40
  from pypetkitapi.feeder_container import Feeder, FeederRecord
38
- from pypetkitapi.litter_container import Litter, LitterRecord
41
+ from pypetkitapi.litter_container import Litter, LitterRecord, LitterStats, PetOuGraph
39
42
  from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
40
43
 
41
44
  _LOGGER = logging.getLogger(__name__)
@@ -201,37 +204,30 @@ class PetKitClient:
201
204
  for account in self.account_data:
202
205
  _LOGGER.debug("List devices data for account: %s", account)
203
206
  if account.device_list:
207
+ _LOGGER.debug("Devices in account: %s", account.device_list)
204
208
  device_list.extend(account.device_list)
205
209
 
206
- _LOGGER.debug("Fetch %s devices for this account", len(device_list))
207
-
208
- for device in device_list:
209
- device_type = device.device_type.lower()
210
- if device_type in DEVICES_FEEDER:
211
- main_tasks.append(
212
- self._fetch_device_data(account, device.device_id, Feeder)
213
- )
214
- record_tasks.append(
215
- self._fetch_device_data(account, device.device_id, FeederRecord)
216
- )
217
- elif device_type in DEVICES_LITTER_BOX:
218
- main_tasks.append(
219
- self._fetch_device_data(account, device.device_id, Litter)
220
- )
221
- record_tasks.append(
222
- self._fetch_device_data(account, device.device_id, LitterRecord)
223
- )
224
- elif device_type in DEVICES_WATER_FOUNTAIN:
225
- main_tasks.append(
226
- self._fetch_device_data(
227
- account, device.device_id, WaterFountain
228
- )
229
- )
230
- record_tasks.append(
231
- self._fetch_device_data(
232
- account, device.device_id, WaterFountainRecord
233
- )
234
- )
210
+ for device in device_list:
211
+ device_type = device.device_type.lower()
212
+ if device_type in DEVICES_FEEDER:
213
+ main_tasks.append(self._fetch_device_data(device, Feeder))
214
+ record_tasks.append(self._fetch_device_data(device, FeederRecord))
215
+ elif device_type in DEVICES_LITTER_BOX:
216
+ main_tasks.append(
217
+ self._fetch_device_data(device, Litter),
218
+ )
219
+ record_tasks.append(self._fetch_device_data(device, LitterRecord))
220
+
221
+ if device_type == T4:
222
+ record_tasks.append(self._fetch_device_data(device, LitterStats))
223
+ if device_type == T6:
224
+ record_tasks.append(self._fetch_device_data(device, PetOuGraph))
225
+
226
+ elif device_type in DEVICES_WATER_FOUNTAIN:
227
+ main_tasks.append(self._fetch_device_data(device, WaterFountain))
228
+ record_tasks.append(
229
+ self._fetch_device_data(device, WaterFountainRecord)
230
+ )
235
231
 
236
232
  # Execute main device tasks first
237
233
  await asyncio.gather(*main_tasks)
@@ -241,12 +237,11 @@ class PetKitClient:
241
237
 
242
238
  end_time = datetime.now()
243
239
  total_time = end_time - start_time
244
- _LOGGER.info("Petkit data fetched successfully in: %s", total_time)
240
+ _LOGGER.debug("Petkit data fetched successfully in: %s", total_time)
245
241
 
246
242
  async def _fetch_device_data(
247
243
  self,
248
- account: AccountData,
249
- device_id: int,
244
+ device: Device,
250
245
  data_class: type[
251
246
  Feeder
252
247
  | Litter
@@ -258,26 +253,19 @@ class PetKitClient:
258
253
  ) -> None:
259
254
  """Fetch the device data from the PetKit servers."""
260
255
  await self.validate_session()
261
- device = None
262
-
263
- if account.device_list:
264
- device = next(
265
- (
266
- device
267
- for device in account.device_list
268
- if device.device_id == device_id
269
- ),
270
- None,
271
- )
272
- if device is None:
273
- _LOGGER.error("Device not found: id=%s", device_id)
274
- return
256
+
275
257
  device_type = device.device_type.lower()
276
258
 
277
- _LOGGER.debug("Reading device type : %s (id=%s)", device_type, device_id)
259
+ _LOGGER.debug("Reading device type : %s (id=%s)", device_type, device.device_id)
278
260
 
279
261
  endpoint = data_class.get_endpoint(device_type)
280
- query_param = data_class.query_param(account, device.device_id)
262
+
263
+ # Specific device ask for data from the device
264
+ device_cont = None
265
+ if self.petkit_entities.get(device.device_id, None):
266
+ device_cont = self.petkit_entities[device.device_id]
267
+
268
+ query_param = data_class.query_param(device, device_cont)
281
269
 
282
270
  response = await self.req.request(
283
271
  method=HTTPMethod.POST,
@@ -286,6 +274,10 @@ class PetKitClient:
286
274
  headers=await self.get_session_id(),
287
275
  )
288
276
 
277
+ # Workaround for the litter box T6
278
+ if isinstance(response, dict) and response.get("list", None):
279
+ response = response.get("list", [])
280
+
289
281
  # Check if the response is a list or a dict
290
282
  if isinstance(response, list):
291
283
  device_data = [data_class(**item) for item in response]
@@ -303,11 +295,19 @@ class PetKitClient:
303
295
  device_data.device_type = device_type
304
296
 
305
297
  if data_class.data_type == DEVICE_DATA:
306
- self.petkit_entities[device_id] = device_data
298
+ self.petkit_entities[device.device_id] = device_data
307
299
  _LOGGER.debug("Device data fetched OK for %s", device_type)
308
300
  elif data_class.data_type == DEVICE_RECORDS:
309
- self.petkit_entities[device_id].device_records = device_data
301
+ self.petkit_entities[device.device_id].device_records = device_data
310
302
  _LOGGER.debug("Device records fetched OK for %s", device_type)
303
+ elif data_class.data_type == DEVICE_STATS:
304
+ if device_type == T4:
305
+ self.petkit_entities[device.device_id].device_stats = device_data
306
+ if device_type == T6:
307
+ self.petkit_entities[device.device_id].device_pet_graph_out = (
308
+ device_data
309
+ )
310
+ _LOGGER.debug("Device stats fetched OK for %s", device_type)
311
311
  else:
312
312
  _LOGGER.error("Unknown data type: %s", data_class.data_type)
313
313
 
@@ -316,7 +316,7 @@ class PetKitClient:
316
316
  device_id: int,
317
317
  action: StrEnum,
318
318
  setting: dict | None = None,
319
- ) -> None:
319
+ ) -> bool:
320
320
  """Control the device using the PetKit API."""
321
321
  device = self.petkit_entities.get(device_id)
322
322
  if not device:
@@ -345,9 +345,11 @@ class PetKitClient:
345
345
  _LOGGER.debug(action)
346
346
  _LOGGER.debug(action_info)
347
347
  if device_type not in action_info.supported_device:
348
- raise PypetkitError(
349
- f"Device type {device.device_type} not supported for action {action}."
348
+ _LOGGER.error(
349
+ "Device type %s not supported for action %s.", device_type, action
350
350
  )
351
+ return False
352
+
351
353
  # Get the endpoint
352
354
  if callable(action_info.endpoint):
353
355
  endpoint = action_info.endpoint(device)
@@ -369,11 +371,12 @@ class PetKitClient:
369
371
  data=params,
370
372
  headers=await self.get_session_id(),
371
373
  )
372
- if res in (SUCCESS_KEY, RES_KEY):
373
- # TODO : Manage to get the response from manual feeding
374
+
375
+ if res in [RES_KEY, SUCCESS_KEY]:
374
376
  _LOGGER.debug("Command executed successfully")
375
- else:
376
- _LOGGER.error("Command execution failed")
377
+ return True
378
+ _LOGGER.error("Command execution failed")
379
+ return False
377
380
 
378
381
  async def close(self) -> None:
379
382
  """Close the aiohttp session if it was created by the client."""
@@ -11,6 +11,7 @@ SUCCESS_KEY = "success"
11
11
 
12
12
  DEVICE_RECORDS = "deviceRecords"
13
13
  DEVICE_DATA = "deviceData"
14
+ DEVICE_STATS = "deviceStats"
14
15
  PET_DATA = "petData"
15
16
 
16
17
  # PetKit Models
@@ -114,6 +115,9 @@ class PetkitEndpoint(StrEnum):
114
115
 
115
116
  # Litter Box
116
117
  DEODORANT_RESET = "deodorantReset"
118
+ STATISTIC = "statistic"
119
+ STATISTIC_RELEASE = "statisticRelease"
120
+ GET_PET_OUT_GRAPH = "getPetOutGraph"
117
121
 
118
122
  # Feeders
119
123
  REPLENISHED_FOOD = "added"
@@ -6,7 +6,7 @@ from typing import Any, ClassVar
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
  from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
9
- from pypetkitapi.containers import AccountData, CloudProduct, FirmwareDetail, Wifi
9
+ from pypetkitapi.containers import CloudProduct, Device, FirmwareDetail, Wifi
10
10
 
11
11
 
12
12
  class FeedItem(BaseModel):
@@ -157,6 +157,7 @@ class StateFeeder(BaseModel):
157
157
  class ManualFeed(BaseModel):
158
158
  """Dataclass for result data."""
159
159
 
160
+ amount: int | None = None
160
161
  amount1: int | None = None
161
162
  amount2: int | None = None
162
163
  id: str | None = None
@@ -259,12 +260,15 @@ class FeederRecord(BaseModel):
259
260
 
260
261
  @classmethod
261
262
  def query_param(
262
- cls, account: AccountData, device_id: int, request_date: str | None = None
263
+ cls,
264
+ device: Device,
265
+ device_data: Any | None = None,
266
+ request_date: str | None = None,
263
267
  ) -> dict:
264
268
  """Generate query parameters including request_date."""
265
269
  if request_date is None:
266
270
  request_date = datetime.now().strftime("%Y%m%d")
267
- return {"days": int(request_date), "deviceId": device_id}
271
+ return {"days": int(request_date), "deviceId": device.device_id}
268
272
 
269
273
 
270
274
  class Feeder(BaseModel):
@@ -305,6 +309,10 @@ class Feeder(BaseModel):
305
309
  return PetkitEndpoint.DEVICE_DETAIL
306
310
 
307
311
  @classmethod
308
- def query_param(cls, account: AccountData, device_id: int) -> dict:
312
+ def query_param(
313
+ cls,
314
+ device: Device,
315
+ device_data: Any | None = None,
316
+ ) -> dict:
309
317
  """Generate query parameters including request_date."""
310
- return {"id": device_id}
318
+ return {"id": int(device.device_id)}
@@ -5,8 +5,15 @@ from typing import Any, ClassVar
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
8
- from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
9
- from pypetkitapi.containers import AccountData, CloudProduct, FirmwareDetail, Wifi
8
+ from pypetkitapi.const import (
9
+ DEVICE_DATA,
10
+ DEVICE_RECORDS,
11
+ DEVICE_STATS,
12
+ T4,
13
+ T6,
14
+ PetkitEndpoint,
15
+ )
16
+ from pypetkitapi.containers import CloudProduct, Device, FirmwareDetail, Wifi
10
17
 
11
18
 
12
19
  class SettingsLitter(BaseModel):
@@ -194,7 +201,9 @@ class LRSubContent(BaseModel):
194
201
 
195
202
 
196
203
  class LitterRecord(BaseModel):
197
- """Dataclass for feeder record data."""
204
+ """Dataclass for feeder record data.
205
+ Litter records
206
+ """
198
207
 
199
208
  data_type: ClassVar[str] = DEVICE_RECORDS
200
209
 
@@ -226,21 +235,149 @@ class LitterRecord(BaseModel):
226
235
  @classmethod
227
236
  def get_endpoint(cls, device_type: str) -> str:
228
237
  """Get the endpoint URL for the given device type."""
229
- return PetkitEndpoint.GET_DEVICE_RECORD
238
+ if device_type == T4:
239
+ return PetkitEndpoint.GET_DEVICE_RECORD
240
+ if device_type == T6:
241
+ return PetkitEndpoint.GET_DEVICE_RECORD_RELEASE
242
+ raise ValueError(f"Invalid device type: {device_type}")
243
+
244
+ @classmethod
245
+ def query_param(
246
+ cls,
247
+ device: Device,
248
+ device_data: Any | None = None,
249
+ request_date: str | None = None,
250
+ ) -> dict:
251
+ """Generate query parameters including request_date."""
252
+ device_type = device.device_type.lower()
253
+ if device_type == T4:
254
+ if request_date is None:
255
+ request_date = datetime.now().strftime("%Y%m%d")
256
+ return {"date": int(request_date), "deviceId": device.device_id}
257
+ if device_type == T6:
258
+ return {
259
+ "timestamp": int(datetime.now().timestamp()),
260
+ "deviceId": device.device_id,
261
+ "type": 2,
262
+ }
263
+ raise ValueError(f"Invalid device type: {device_type}")
264
+
265
+
266
+ class StatisticInfo(BaseModel):
267
+ """Dataclass for statistic information.
268
+ Subclass of LitterStats.
269
+ """
270
+
271
+ pet_id: str | None = Field(None, alias="petId")
272
+ pet_name: str | None = Field(None, alias="petName")
273
+ pet_times: int | None = Field(None, alias="petTimes")
274
+ pet_total_time: int | None = Field(None, alias="petTotalTime")
275
+ pet_weight: int | None = Field(None, alias="petWeight")
276
+ statistic_date: str | None = Field(None, alias="statisticDate")
277
+ x_time: int | None = Field(None, alias="xTime")
278
+
279
+
280
+ class LitterStats(BaseModel):
281
+ """Dataclass for result data.
282
+ Supported devices = T4 only (T3 ?)
283
+ """
284
+
285
+ data_type: ClassVar[str] = DEVICE_STATS
286
+
287
+ avg_time: int | None = Field(None, alias="avgTime")
288
+ pet_ids: list[dict] | None = Field(None, alias="petIds")
289
+ statistic_info: list[StatisticInfo] | None = Field(None, alias="statisticInfo")
290
+ statistic_time: str | None = Field(None, alias="statisticTime")
291
+ times: int | None = None
292
+ total_time: int | None = Field(None, alias="totalTime")
293
+ device_type: str | None = None
294
+
295
+ @classmethod
296
+ def get_endpoint(cls, device_type: str) -> str:
297
+ """Get the endpoint URL for the given device type."""
298
+ return PetkitEndpoint.STATISTIC
299
+
300
+ @classmethod
301
+ def query_param(
302
+ cls,
303
+ device: Device,
304
+ device_data: Any | None = None,
305
+ start_date: str | None = None,
306
+ end_date: str | None = None,
307
+ ) -> dict:
308
+ """Generate query parameters including request_date."""
309
+
310
+ if start_date is None and end_date is None:
311
+ start_date = datetime.now().strftime("%Y%m%d")
312
+ end_date = datetime.now().strftime("%Y%m%d")
313
+
314
+ return {
315
+ "endDate": end_date,
316
+ "deviceId": device.device_id,
317
+ "type": 0,
318
+ "startDate": start_date,
319
+ }
320
+
321
+
322
+ class PetGraphContent(BaseModel):
323
+ """Dataclass for content data."""
324
+
325
+ auto_clear: int | None = Field(None, alias="autoClear")
326
+ img: str | None = None
327
+ interval: int | None = None
328
+ is_shit: int | None = Field(None, alias="isShit")
329
+ mark: int | None = None
330
+ media: int | None = None
331
+ pet_weight: int | None = Field(None, alias="petWeight")
332
+ shit_weight: int | None = Field(None, alias="shitWeight")
333
+ time_in: int | None = Field(None, alias="timeIn")
334
+ time_out: int | None = Field(None, alias="timeOut")
335
+
336
+
337
+ class PetOuGraph(BaseModel):
338
+ """Dataclass for event data."""
339
+
340
+ data_type: ClassVar[str] = DEVICE_STATS
341
+
342
+ aes_key: str | None = Field(None, alias="aesKey")
343
+ content: PetGraphContent | None = None
344
+ duration: int | None = None
345
+ event_id: str | None = Field(None, alias="eventId")
346
+ expire: int | None = None
347
+ is_need_upload_video: int | None = Field(None, alias="isNeedUploadVideo")
348
+ media_api: str | None = Field(None, alias="mediaApi")
349
+ pet_id: str | None = Field(None, alias="petId")
350
+ pet_name: str | None = Field(None, alias="petName")
351
+ preview: str | None = None
352
+ storage_space: int | None = Field(None, alias="storageSpace")
353
+ time: int | None = None
354
+ toilet_time: int | None = Field(None, alias="toiletTime")
355
+ device_type: str | None = None
356
+
357
+ @classmethod
358
+ def get_endpoint(cls, device_type: str) -> str:
359
+ """Get the endpoint URL for the given device type."""
360
+ return PetkitEndpoint.GET_PET_OUT_GRAPH
230
361
 
231
362
  @classmethod
232
363
  def query_param(
233
- cls, account: AccountData, device_id: int, request_date: str | None = None
364
+ cls,
365
+ device: Device,
366
+ device_data: Any | None = None,
367
+ start_date: str | None = None,
368
+ end_date: str | None = None,
234
369
  ) -> dict:
235
370
  """Generate query parameters including request_date."""
236
- if request_date is None:
237
- request_date = datetime.now().strftime("%Y%m%d")
238
- return {"date": int(request_date), "deviceId": device_id}
371
+
372
+ return {
373
+ "timestamp": int(datetime.now().timestamp()),
374
+ "deviceId": device.device_id,
375
+ }
239
376
 
240
377
 
241
378
  class Litter(BaseModel):
242
379
  """Dataclass for Litter Data.
243
- Supported devices = T4, T6
380
+ Supported devices = T3, T4, T6
244
381
  """
245
382
 
246
383
  data_type: ClassVar[str] = DEVICE_DATA
@@ -279,6 +416,8 @@ class Litter(BaseModel):
279
416
  total_time: int | None = Field(None, alias="totalTime")
280
417
  device_type: str | None = Field(None, alias="deviceType")
281
418
  device_records: list[LitterRecord] | None = None
419
+ device_stats: LitterStats | None = None
420
+ device_pet_graph_out: PetOuGraph | None = None
282
421
 
283
422
  @classmethod
284
423
  def get_endpoint(cls, device_type: str) -> str:
@@ -286,6 +425,10 @@ class Litter(BaseModel):
286
425
  return PetkitEndpoint.DEVICE_DETAIL
287
426
 
288
427
  @classmethod
289
- def query_param(cls, account: AccountData, device_id: int) -> dict:
428
+ def query_param(
429
+ cls,
430
+ device: Device,
431
+ device_data: Any | None = None,
432
+ ) -> dict:
290
433
  """Generate query parameters including request_date."""
291
- return {"id": device_id}
434
+ return {"id": device.device_id}
@@ -6,7 +6,7 @@ from typing import Any, ClassVar
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
  from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
9
- from pypetkitapi.containers import AccountData
9
+ from pypetkitapi.containers import Device
10
10
 
11
11
 
12
12
  class Electricity(BaseModel):
@@ -102,18 +102,20 @@ class WaterFountainRecord(BaseModel):
102
102
 
103
103
  @classmethod
104
104
  def query_param(
105
- cls, account: AccountData, device_id: int, request_date: str | None = None
105
+ cls,
106
+ device: Device,
107
+ device_data: Any | None = None,
108
+ request_date: str | None = None,
106
109
  ) -> dict:
107
110
  """Generate query parameters including request_date."""
108
- if not account.user_list or not account.user_list[0]:
109
- raise ValueError("The account does not have a valid user_list.")
110
-
111
111
  if request_date is None:
112
112
  request_date = datetime.now().strftime("%Y%m%d")
113
+ if device_data is None or not hasattr(device_data, "user_id"):
114
+ raise ValueError("The device_data does not have a valid user_id.")
113
115
  return {
114
116
  "day": int(request_date),
115
- "deviceId": device_id,
116
- "userId": account.user_list[0].user_id,
117
+ "deviceId": device.device_id,
118
+ "userId": device_data.user_id,
117
119
  }
118
120
 
119
121
 
@@ -168,6 +170,10 @@ class WaterFountain(BaseModel):
168
170
  return PetkitEndpoint.DEVICE_DATA
169
171
 
170
172
  @classmethod
171
- def query_param(cls, account: AccountData, device_id: int) -> dict:
173
+ def query_param(
174
+ cls,
175
+ device: Device,
176
+ device_data: Any | None = None,
177
+ ) -> dict:
172
178
  """Generate query parameters including request_date."""
173
- return {"id": device_id}
179
+ return {"id": device.device_id}
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
187
187
 
188
188
  [tool.poetry]
189
189
  name = "pypetkitapi"
190
- version = "1.2.4"
190
+ version = "1.3.0"
191
191
  description = "Python client for PetKit API"
192
192
  authors = ["Jezza34000 <info@mail.com>"]
193
193
  readme = "README.md"
@@ -204,7 +204,7 @@ black = "^24.10.0"
204
204
  ruff = "^0.8.1"
205
205
 
206
206
  [tool.bumpver]
207
- current_version = "1.2.4"
207
+ current_version = "1.3.0"
208
208
  version_pattern = "MAJOR.MINOR.PATCH"
209
209
  commit_message = "bump version {old_version} -> {new_version}"
210
210
  tag_message = "{new_version}"
File without changes
File without changes