pypetkitapi 0.1.0__tar.gz → 0.2.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: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python client for PetKit API
5
5
  Home-page: https://github.com/Jezza34000/pypetkit
6
6
  License: MIT
@@ -1,5 +1,6 @@
1
1
  """Pypetkit Client: A Python library for interfacing with PetKit"""
2
2
 
3
+ import asyncio
3
4
  from datetime import datetime, timedelta
4
5
  from enum import StrEnum
5
6
  import hashlib
@@ -22,7 +23,14 @@ from pypetkitapi.const import (
22
23
  PetkitURL,
23
24
  )
24
25
  from pypetkitapi.containers import AccountData, Device, RegionInfo, SessionInfo
25
- from pypetkitapi.exceptions import PypetkitError
26
+ from pypetkitapi.exceptions import (
27
+ PetkitAuthenticationError,
28
+ PetkitInvalidHTTPResponseCodeError,
29
+ PetkitInvalidResponseFormat,
30
+ PetkitRegionalServerNotFoundError,
31
+ PetkitTimeoutError,
32
+ PypetkitError,
33
+ )
26
34
  from pypetkitapi.feeder_container import Feeder
27
35
  from pypetkitapi.litter_container import Litter
28
36
  from pypetkitapi.water_fountain_container import WaterFountain
@@ -49,12 +57,13 @@ class PetKitClient:
49
57
  """Initialize the PetKit Client."""
50
58
  self.username = username
51
59
  self.password = password
52
- self.region = region
60
+ self.region = region.lower()
53
61
  self.timezone = timezone
54
62
 
55
63
  async def _generate_header(self) -> dict[str, str]:
56
64
  """Create header for interaction with devices."""
57
65
  session_id = self._session.id if self._session is not None else ""
66
+
58
67
  return {
59
68
  "Accept": Header.ACCEPT.value,
60
69
  "Accept-Language": Header.ACCEPT_LANG,
@@ -92,7 +101,11 @@ class PetKitClient:
92
101
  _LOGGER.debug("Finding region server for region: %s", self.region)
93
102
 
94
103
  regional_server = next(
95
- (server for server in self._servers_list if server.name == self.region),
104
+ (
105
+ server
106
+ for server in self._servers_list
107
+ if server.name.lower() == self.region
108
+ ),
96
109
  None,
97
110
  )
98
111
 
@@ -102,7 +115,7 @@ class PetKitClient:
102
115
  )
103
116
  self._base_url = regional_server.gateway
104
117
  return
105
- _LOGGER.debug("Region %s not found in server list", self.region)
118
+ raise PetkitRegionalServerNotFoundError(self.region)
106
119
 
107
120
  async def request_login_code(self) -> bool:
108
121
  """Request a login code to be sent to the user's email."""
@@ -124,6 +137,8 @@ class PetKitClient:
124
137
  # Retrieve the list of servers
125
138
  await self._get_base_url()
126
139
 
140
+ _LOGGER.debug("Logging in to PetKit server")
141
+
127
142
  # Prepare the data to send
128
143
  data = LOGIN_DATA.copy()
129
144
  data["encrypt"] = "1"
@@ -198,6 +213,7 @@ class PetKitClient:
198
213
 
199
214
  async def get_devices_data(self) -> None:
200
215
  """Get the devices data from the PetKit servers."""
216
+ start_time = datetime.now()
201
217
  if not self.account_data:
202
218
  await self._get_account_data()
203
219
 
@@ -207,19 +223,25 @@ class PetKitClient:
207
223
  if account.device_list:
208
224
  device_list.extend(account.device_list)
209
225
 
210
- _LOGGER.info("%s devices found for this account", len(device_list))
226
+ _LOGGER.debug("Fetch %s devices for this account", len(device_list))
227
+
228
+ tasks = []
211
229
  for device in device_list:
212
230
  _LOGGER.debug("Fetching devices data: %s", device)
213
231
  device_type = device.device_type.lower()
214
- # TODO: Fetch device records
215
232
  if device_type in DEVICES_FEEDER:
216
- await self._fetch_device_data(device, Feeder)
233
+ tasks.append(self._fetch_device_data(device, Feeder))
217
234
  elif device_type in DEVICES_LITTER_BOX:
218
- await self._fetch_device_data(device, Litter)
235
+ tasks.append(self._fetch_device_data(device, Litter))
219
236
  elif device_type in DEVICES_WATER_FOUNTAIN:
220
- await self._fetch_device_data(device, WaterFountain)
237
+ tasks.append(self._fetch_device_data(device, WaterFountain))
221
238
  else:
222
239
  _LOGGER.warning("Unknown device type: %s", device_type)
240
+ await asyncio.gather(*tasks)
241
+
242
+ end_time = datetime.now()
243
+ total_time = end_time - start_time
244
+ _LOGGER.debug("Petkit fetch took : %s", total_time)
223
245
 
224
246
  async def _fetch_device_data(
225
247
  self,
@@ -241,7 +263,7 @@ class PetKitClient:
241
263
  )
242
264
  device_data = data_class(**response)
243
265
  device_data.device_type = device.device_type # Add the device_type attribute
244
- _LOGGER.info("Adding device type: %s", device.device_type)
266
+ _LOGGER.debug("Reading device type : %s", device.device_type)
245
267
  self.device_list.append(device_data)
246
268
 
247
269
  async def send_api_request(
@@ -334,14 +356,7 @@ class PrepReq:
334
356
  data=data,
335
357
  headers=_headers,
336
358
  ) as resp:
337
- response = await resp.json()
338
- if ERR_KEY in response:
339
- error_msg = response[ERR_KEY].get("msg", "Unknown error")
340
- raise PypetkitError(f"Request failed: {error_msg}")
341
- if RES_KEY in response:
342
- _LOGGER.debug("Request response: %s", response)
343
- return response[RES_KEY]
344
- raise PypetkitError("Unexpected response format")
359
+ return await self._handle_response(resp, _url)
345
360
  except ContentTypeError:
346
361
  """If we get an error, lets log everything for debugging."""
347
362
  try:
@@ -353,3 +368,41 @@ class PrepReq:
353
368
  _LOGGER.info("Resp raw: %s", resp_raw)
354
369
  # Still raise the err so that it's clear it failed.
355
370
  raise
371
+ except TimeoutError:
372
+ raise PetkitTimeoutError("The request timed out") from None
373
+
374
+ @staticmethod
375
+ async def _handle_response(response: aiohttp.ClientResponse, url: str) -> dict:
376
+ """Handle the response from the PetKit API."""
377
+
378
+ try:
379
+ response.raise_for_status()
380
+ except aiohttp.ClientResponseError as e:
381
+ raise PetkitInvalidHTTPResponseCodeError(
382
+ f"Request failed with status code {e.status}"
383
+ ) from e
384
+
385
+ try:
386
+ response_json = await response.json()
387
+ except ContentTypeError:
388
+ raise PetkitInvalidResponseFormat(
389
+ "Response is not in JSON format"
390
+ ) from None
391
+
392
+ if ERR_KEY in response_json:
393
+ error_msg = response_json[ERR_KEY].get("msg", "Unknown error")
394
+ if any(
395
+ endpoint in url
396
+ for endpoint in [
397
+ PetkitEndpoint.LOGIN,
398
+ PetkitEndpoint.GET_LOGIN_CODE,
399
+ PetkitEndpoint.REFRESH_SESSION,
400
+ ]
401
+ ):
402
+ raise PetkitAuthenticationError(f"Login failed: {error_msg}")
403
+ raise PypetkitError(f"Request failed: {error_msg}")
404
+
405
+ if RES_KEY in response_json:
406
+ return response_json[RES_KEY]
407
+
408
+ raise PypetkitError("Unexpected response format")
@@ -57,7 +57,7 @@ class Header(StrEnum):
57
57
  AGENT = "okhttp/3.12.11"
58
58
  CLIENT = f"{Client.PLATFORM_TYPE}({Client.OS_VERSION};{Client.MODEL_NAME})"
59
59
  TIMEZONE = "1.0"
60
- TIMEZONE_ID = "Europe/Paris"
60
+ TIMEZONE_ID = "Europe/Paris" # TODO: Make this dynamic, check if this really matters (record hours?)
61
61
  LOCALE = "en-US"
62
62
  IMG_VERSION = "1.0"
63
63
  HOUR = "24"
@@ -69,8 +69,8 @@ CLIENT_NFO = {
69
69
  "osVersion": Client.OS_VERSION.value,
70
70
  "platform": Client.PLATFORM_TYPE.value,
71
71
  "source": Client.SOURCE.value,
72
- "timezone": Header.TIMEZONE.value,
73
- "timezoneId": Header.TIMEZONE_ID.value,
72
+ "timezone": Header.TIMEZONE.value, # TODO: Make this dynamic
73
+ "timezoneId": Header.TIMEZONE_ID.value, # TODO: Make this dynamic
74
74
  "version": Header.API_VERSION.value,
75
75
  }
76
76
 
@@ -0,0 +1,40 @@
1
+ """PyPetkit exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class PypetkitError(Exception):
7
+ """Class for PyPetkit exceptions."""
8
+
9
+
10
+ class PetkitTimeoutError(PypetkitError):
11
+ """Class for PyPetkit timeout exceptions."""
12
+
13
+
14
+ class PetkitConnectionError(PypetkitError):
15
+ """Class for PyPetkit connection exceptions."""
16
+
17
+
18
+ class PetkitRegionalServerNotFoundError(PypetkitError):
19
+ """Exception raised when the specified region server is not found."""
20
+
21
+ def __init__(self, region: str):
22
+ """Initialize the exception."""
23
+ self.region = region
24
+ self.message = (
25
+ f"Region you provided: '{region}' was not found in the server list. "
26
+ f"Are you sure you provided the correct region?"
27
+ )
28
+ super().__init__(self.message)
29
+
30
+
31
+ class PetkitInvalidHTTPResponseCodeError(PypetkitError):
32
+ """Class for PyPetkit invalid HTTP Response exceptions."""
33
+
34
+
35
+ class PetkitInvalidResponseFormat(PypetkitError):
36
+ """Class for PyPetkit invalid Response Format exceptions."""
37
+
38
+
39
+ class PetkitAuthenticationError(PypetkitError):
40
+ """Class for PyPetkit authentication exceptions."""
@@ -141,6 +141,7 @@ class StateFeeder(BaseModel):
141
141
  door: int | None = None
142
142
  feed_state: FeedState | None = Field(None, alias="feedState")
143
143
  feeding: int | None = None
144
+ error_msg: str | None = Field(None, alias="errorMsg")
144
145
  ota: int | None = None
145
146
  overall: int | None = None
146
147
  pim: int | None = None
@@ -163,10 +164,10 @@ class Feeder(BaseModel):
163
164
  bt_mac: str | None = Field(None, alias="btMac")
164
165
  cloud_product: CloudProduct | None = Field(None, alias="cloudProduct")
165
166
  created_at: str | None = Field(None, alias="createdAt")
166
- firmware: str | None = None
167
- firmware_details: list[FirmwareDetail] | None = Field(None, alias="firmwareDetails")
168
- hardware: int | None = None
169
- id: int | None = None
167
+ firmware: float
168
+ firmware_details: list[FirmwareDetail] = Field(alias="firmwareDetails")
169
+ hardware: int
170
+ id: int
170
171
  locale: str | None = None
171
172
  mac: str | None = None
172
173
  model_code: int | None = Field(None, alias="modelCode")
@@ -177,7 +178,7 @@ class Feeder(BaseModel):
177
178
  settings: SettingsFeeder | None = None
178
179
  share_open: int | None = Field(None, alias="shareOpen")
179
180
  signup_at: str | None = Field(None, alias="signupAt")
180
- sn: str | None = None
181
+ sn: str
181
182
  state: StateFeeder | None = None
182
183
  timezone: float | None = None
183
184
  p2p_type: int | None = Field(None, alias="p2pType")
@@ -91,6 +91,7 @@ class StateLitter(BaseModel):
91
91
  box_full: bool | None = Field(None, alias="boxFull")
92
92
  box_state: int | None = Field(None, alias="boxState")
93
93
  deodorant_left_days: int | None = Field(None, alias="deodorantLeftDays")
94
+ error_msg: str | None = Field(None, alias="errorMsg")
94
95
  frequent_restroom: int | None = Field(None, alias="frequentRestroom")
95
96
  liquid_empty: bool | None = Field(None, alias="liquidEmpty")
96
97
  liquid_lack: bool | None = Field(None, alias="liquidLack")
@@ -140,10 +141,10 @@ class Litter(BaseModel):
140
141
  auto_upgrade: int | None = Field(None, alias="autoUpgrade")
141
142
  bt_mac: str | None = Field(None, alias="btMac")
142
143
  created_at: str | None = Field(None, alias="createdAt")
143
- firmware: str | None = None
144
- firmware_details: list[FirmwareDetail] | None = Field(None, alias="firmwareDetails")
145
- hardware: int | None = None
146
- id: int | None = None
144
+ firmware: float
145
+ firmware_details: list[FirmwareDetail] = Field(alias="firmwareDetails")
146
+ hardware: int
147
+ id: int
147
148
  is_pet_out_tips: int | None = Field(None, alias="isPetOutTips")
148
149
  locale: str | None = None
149
150
  mac: str | None = None
@@ -156,7 +157,7 @@ class Litter(BaseModel):
156
157
  settings: SettingsLitter | None = None
157
158
  share_open: int | None = Field(None, alias="shareOpen")
158
159
  signup_at: str | None = Field(None, alias="signupAt")
159
- sn: str | None = None
160
+ sn: str
160
161
  state: StateLitter | None = None
161
162
  timezone: float | None = None
162
163
  cloud_product: CloudProduct | None = Field(None, alias="cloudProduct") # For T5/T6
@@ -100,9 +100,9 @@ class WaterFountain(BaseModel):
100
100
  filter_expected_days: int | None = Field(None, alias="filterExpectedDays")
101
101
  filter_percent: int | None = Field(None, alias="filterPercent")
102
102
  filter_warning: int | None = Field(None, alias="filterWarning")
103
- firmware: int | None = None
104
- hardware: int | None = None
105
- id: int | None = None
103
+ firmware: float
104
+ hardware: int
105
+ id: int
106
106
  is_night_no_disturbing: int | None = Field(None, alias="isNightNoDisturbing")
107
107
  lack_warning: int | None = Field(None, alias="lackWarning")
108
108
  locale: str | None = None
@@ -110,14 +110,14 @@ class WaterFountain(BaseModel):
110
110
  mac: str | None = None
111
111
  mode: int | None = None
112
112
  module_status: int | None = Field(None, alias="moduleStatus")
113
- name: str | None = None
113
+ name: str
114
114
  record_automatic_add_water: int | None = Field(
115
115
  None, alias="recordAutomaticAddWater"
116
116
  )
117
117
  schedule: Schedule | None = None
118
118
  secret: str | None = None
119
119
  settings: SettingsFountain | None = None
120
- sn: str | None = None
120
+ sn: str
121
121
  status: Status | None = None
122
122
  sync_time: str | None = Field(None, alias="syncTime")
123
123
  timezone: float | None = None
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
187
187
 
188
188
  [tool.poetry]
189
189
  name = "pypetkitapi"
190
- version = "0.1.0"
190
+ version = "0.2.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 = "0.1.0"
207
+ current_version = "0.2.0"
208
208
  version_pattern = "MAJOR.MINOR.PATCH"
209
209
  commit_message = "bump version {old_version} -> {new_version}"
210
210
  tag_message = "{new_version}"
@@ -217,7 +217,8 @@ push = true
217
217
 
218
218
  [tool.bumpver.file_patterns]
219
219
  "pyproject.toml" = [
220
- 'current_version = "{version}"',
220
+ '^version = "{version}"',
221
+ '^current_version = "{version}"',
221
222
  ]
222
223
 
223
224
  [tool.tox]
@@ -1,15 +0,0 @@
1
- """PyPetkit exceptions."""
2
-
3
- from __future__ import annotations
4
-
5
-
6
- class PypetkitError(Exception):
7
- """Class for PyPetkit exceptions."""
8
-
9
-
10
- class PetkitTimeoutError(PypetkitError):
11
- """Class for PyPetkit timeout exceptions."""
12
-
13
-
14
- class PetkitConnectionError(PypetkitError):
15
- """Class for PyPetkit connection exceptions."""
File without changes
File without changes