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.
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/PKG-INFO +1 -1
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/client.py +71 -18
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/const.py +3 -3
- pypetkitapi-0.2.0/pypetkitapi/exceptions.py +40 -0
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/feeder_container.py +6 -5
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/litter_container.py +6 -5
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/water_fountain_container.py +5 -5
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pyproject.toml +4 -3
- pypetkitapi-0.1.0/pypetkitapi/exceptions.py +0 -15
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/LICENSE +0 -0
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/README.md +0 -0
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/__init__.py +0 -0
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/command.py +0 -0
- {pypetkitapi-0.1.0 → pypetkitapi-0.2.0}/pypetkitapi/containers.py +0 -0
@@ -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
|
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
|
-
(
|
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
|
-
|
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.
|
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
|
-
|
233
|
+
tasks.append(self._fetch_device_data(device, Feeder))
|
217
234
|
elif device_type in DEVICES_LITTER_BOX:
|
218
|
-
|
235
|
+
tasks.append(self._fetch_device_data(device, Litter))
|
219
236
|
elif device_type in DEVICES_WATER_FOUNTAIN:
|
220
|
-
|
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.
|
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
|
-
|
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:
|
167
|
-
firmware_details: list[FirmwareDetail]
|
168
|
-
hardware: int
|
169
|
-
id: int
|
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
|
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:
|
144
|
-
firmware_details: list[FirmwareDetail]
|
145
|
-
hardware: int
|
146
|
-
id: int
|
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
|
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:
|
104
|
-
hardware: int
|
105
|
-
id: int
|
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
|
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
|
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.
|
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.
|
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
|
-
'
|
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
|
File without changes
|
File without changes
|
File without changes
|