pypetkitapi 0.5.3__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pypetkitapi/client.py +187 -140
- pypetkitapi/command.py +19 -97
- pypetkitapi/const.py +11 -4
- pypetkitapi/containers.py +3 -2
- pypetkitapi/feeder_container.py +49 -8
- pypetkitapi/litter_container.py +65 -6
- pypetkitapi/water_fountain_container.py +42 -5
- {pypetkitapi-0.5.3.dist-info → pypetkitapi-1.0.0.dist-info}/METADATA +41 -33
- pypetkitapi-1.0.0.dist-info/RECORD +14 -0
- pypetkitapi-0.5.3.dist-info/RECORD +0 -14
- {pypetkitapi-0.5.3.dist-info → pypetkitapi-1.0.0.dist-info}/LICENSE +0 -0
- {pypetkitapi-0.5.3.dist-info → pypetkitapi-1.0.0.dist-info}/WHEEL +0 -0
pypetkitapi/client.py
CHANGED
@@ -12,6 +12,8 @@ from aiohttp import ContentTypeError
|
|
12
12
|
|
13
13
|
from pypetkitapi.command import ACTIONS_MAP
|
14
14
|
from pypetkitapi.const import (
|
15
|
+
DEVICE_DATA,
|
16
|
+
DEVICE_RECORDS,
|
15
17
|
DEVICES_FEEDER,
|
16
18
|
DEVICES_LITTER_BOX,
|
17
19
|
DEVICES_WATER_FOUNTAIN,
|
@@ -20,10 +22,10 @@ from pypetkitapi.const import (
|
|
20
22
|
RES_KEY,
|
21
23
|
SUCCESS_KEY,
|
22
24
|
Header,
|
25
|
+
PetkitDomain,
|
23
26
|
PetkitEndpoint,
|
24
|
-
PetkitURL,
|
25
27
|
)
|
26
|
-
from pypetkitapi.containers import AccountData, Device, RegionInfo, SessionInfo
|
28
|
+
from pypetkitapi.containers import AccountData, Device, Pet, RegionInfo, SessionInfo
|
27
29
|
from pypetkitapi.exceptions import (
|
28
30
|
PetkitAuthenticationError,
|
29
31
|
PetkitInvalidHTTPResponseCodeError,
|
@@ -32,9 +34,9 @@ from pypetkitapi.exceptions import (
|
|
32
34
|
PetkitTimeoutError,
|
33
35
|
PypetkitError,
|
34
36
|
)
|
35
|
-
from pypetkitapi.feeder_container import Feeder
|
36
|
-
from pypetkitapi.litter_container import Litter
|
37
|
-
from pypetkitapi.water_fountain_container import WaterFountain
|
37
|
+
from pypetkitapi.feeder_container import Feeder, FeederRecord
|
38
|
+
from pypetkitapi.litter_container import Litter, LitterRecord
|
39
|
+
from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
|
38
40
|
|
39
41
|
_LOGGER = logging.getLogger(__name__)
|
40
42
|
|
@@ -46,8 +48,10 @@ class PetKitClient:
|
|
46
48
|
_session: SessionInfo | None = None
|
47
49
|
_servers_list: list[RegionInfo] = []
|
48
50
|
account_data: list[AccountData] = []
|
49
|
-
|
50
|
-
|
51
|
+
petkit_entities: dict[int, Feeder | Litter | WaterFountain | Pet] = {}
|
52
|
+
petkit_entities_records: dict[
|
53
|
+
int, FeederRecord | LitterRecord | WaterFountainRecord
|
54
|
+
] = {}
|
51
55
|
|
52
56
|
def __init__(
|
53
57
|
self,
|
@@ -55,85 +59,49 @@ class PetKitClient:
|
|
55
59
|
password: str,
|
56
60
|
region: str,
|
57
61
|
timezone: str,
|
62
|
+
session: aiohttp.ClientSession | None = None,
|
58
63
|
) -> None:
|
59
64
|
"""Initialize the PetKit Client."""
|
60
65
|
self.username = username
|
61
66
|
self.password = password
|
62
67
|
self.region = region.lower()
|
63
68
|
self.timezone = timezone
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
return {
|
70
|
-
"Accept": Header.ACCEPT.value,
|
71
|
-
"Accept-Language": Header.ACCEPT_LANG,
|
72
|
-
"Accept-Encoding": Header.ENCODING,
|
73
|
-
"Content-Type": Header.CONTENT_TYPE,
|
74
|
-
"User-Agent": Header.AGENT,
|
75
|
-
"X-Img-Version": Header.IMG_VERSION,
|
76
|
-
"X-Locale": Header.LOCALE,
|
77
|
-
"F-Session": session_id,
|
78
|
-
"X-Session": session_id,
|
79
|
-
"X-Client": Header.CLIENT,
|
80
|
-
"X-Hour": Header.HOUR,
|
81
|
-
"X-TimezoneId": Header.TIMEZONE_ID,
|
82
|
-
"X-Api-Version": Header.API_VERSION,
|
83
|
-
"X-Timezone": Header.TIMEZONE,
|
84
|
-
}
|
85
|
-
|
86
|
-
async def _get_api_server_list(self) -> None:
|
87
|
-
"""Get the list of API servers and set the base URL."""
|
88
|
-
_LOGGER.debug("Getting API server list")
|
89
|
-
prep_req = PrepReq(base_url=PetkitURL.REGION_SRV)
|
90
|
-
response = await prep_req.request(
|
91
|
-
method=HTTPMethod.GET,
|
92
|
-
url="",
|
93
|
-
headers=await self._generate_header(),
|
69
|
+
self._session = None
|
70
|
+
self.aiohttp_session = session or aiohttp.ClientSession()
|
71
|
+
self.req = PrepReq(
|
72
|
+
base_url=PetkitDomain.PASSPORT_PETKIT, session=self.aiohttp_session
|
94
73
|
)
|
95
|
-
_LOGGER.debug("API server list: %s", response)
|
96
|
-
self._servers_list = [
|
97
|
-
RegionInfo(**region) for region in response.get("list", [])
|
98
|
-
]
|
99
74
|
|
100
75
|
async def _get_base_url(self) -> None:
|
101
|
-
"""
|
102
|
-
|
103
|
-
_LOGGER.debug("Finding region server for region: %s", self.region)
|
76
|
+
"""Get the list of API servers, filter by region, and return the matching server."""
|
77
|
+
_LOGGER.debug("Getting API server list")
|
104
78
|
|
105
|
-
|
106
|
-
|
107
|
-
self._base_url = PetkitURL.CHINA_SRV
|
79
|
+
if self.region.lower() == "china":
|
80
|
+
self._base_url = PetkitDomain.CHINA_SRV
|
108
81
|
return
|
109
82
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
for server in self._servers_list
|
114
|
-
if server.name.lower() == self.region
|
115
|
-
or server.id.lower() == self.region
|
116
|
-
),
|
117
|
-
None,
|
83
|
+
response = await self.req.request(
|
84
|
+
method=HTTPMethod.GET,
|
85
|
+
url=PetkitEndpoint.REGION_SERVERS,
|
118
86
|
)
|
87
|
+
_LOGGER.debug("API server list: %s", response)
|
119
88
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
)
|
124
|
-
|
125
|
-
|
89
|
+
# Filter the servers by region
|
90
|
+
for region in response.get("list", []):
|
91
|
+
server = RegionInfo(**region)
|
92
|
+
if server.name.lower() == self.region or server.id.lower() == self.region:
|
93
|
+
self.req.base_url = server.gateway
|
94
|
+
_LOGGER.debug("Found matching server: %s", server)
|
95
|
+
return
|
126
96
|
raise PetkitRegionalServerNotFoundError(self.region)
|
127
97
|
|
128
98
|
async def request_login_code(self) -> bool:
|
129
99
|
"""Request a login code to be sent to the user's email."""
|
130
100
|
_LOGGER.debug("Requesting login code for username: %s", self.username)
|
131
|
-
|
132
|
-
response = await prep_req.request(
|
101
|
+
response = await self.req.request(
|
133
102
|
method=HTTPMethod.GET,
|
134
103
|
url=PetkitEndpoint.GET_LOGIN_CODE,
|
135
104
|
params={"username": self.username},
|
136
|
-
headers=await self._generate_header(),
|
137
105
|
)
|
138
106
|
if response:
|
139
107
|
_LOGGER.info("Login code sent to user's email")
|
@@ -145,7 +113,7 @@ class PetKitClient:
|
|
145
113
|
# Retrieve the list of servers
|
146
114
|
await self._get_base_url()
|
147
115
|
|
148
|
-
_LOGGER.
|
116
|
+
_LOGGER.info("Logging in to PetKit server")
|
149
117
|
|
150
118
|
# Prepare the data to send
|
151
119
|
data = LOGIN_DATA.copy()
|
@@ -162,12 +130,10 @@ class PetKitClient:
|
|
162
130
|
data["password"] = pwd # noqa: S324
|
163
131
|
|
164
132
|
# Send the login request
|
165
|
-
|
166
|
-
response = await prep_req.request(
|
133
|
+
response = await self.req.request(
|
167
134
|
method=HTTPMethod.POST,
|
168
135
|
url=PetkitEndpoint.LOGIN,
|
169
136
|
data=data,
|
170
|
-
headers=await self._generate_header(),
|
171
137
|
)
|
172
138
|
session_data = response["session"]
|
173
139
|
self._session = SessionInfo(**session_data)
|
@@ -175,11 +141,9 @@ class PetKitClient:
|
|
175
141
|
async def refresh_session(self) -> None:
|
176
142
|
"""Refresh the session."""
|
177
143
|
_LOGGER.debug("Refreshing session")
|
178
|
-
|
179
|
-
response = await prep_req.request(
|
144
|
+
response = await self.req.request(
|
180
145
|
method=HTTPMethod.POST,
|
181
146
|
url=PetkitEndpoint.REFRESH_SESSION,
|
182
|
-
headers=await self._generate_header(),
|
183
147
|
)
|
184
148
|
session_data = response["session"]
|
185
149
|
self._session = SessionInfo(**session_data)
|
@@ -207,83 +171,150 @@ class PetKitClient:
|
|
207
171
|
await self.refresh_session()
|
208
172
|
return
|
209
173
|
|
174
|
+
async def get_session_id(self) -> dict:
|
175
|
+
"""Return the session ID."""
|
176
|
+
if self._session is None:
|
177
|
+
raise PypetkitError("Session is not initialized.")
|
178
|
+
return {"F-Session": self._session.id, "X-Session": self._session.id}
|
179
|
+
|
210
180
|
async def _get_account_data(self) -> None:
|
211
181
|
"""Get the account data from the PetKit service."""
|
212
182
|
await self.validate_session()
|
213
183
|
_LOGGER.debug("Fetching account data")
|
214
|
-
|
215
|
-
response = await prep_req.request(
|
184
|
+
response = await self.req.request(
|
216
185
|
method=HTTPMethod.GET,
|
217
186
|
url=PetkitEndpoint.FAMILY_LIST,
|
218
|
-
headers=await self.
|
187
|
+
headers=await self.get_session_id(),
|
219
188
|
)
|
220
189
|
self.account_data = [AccountData(**account) for account in response]
|
221
190
|
|
191
|
+
# Add pets to device_list
|
192
|
+
for account in self.account_data:
|
193
|
+
if account.pet_list:
|
194
|
+
for pet in account.pet_list:
|
195
|
+
self.petkit_entities[pet.pet_id] = pet
|
196
|
+
|
222
197
|
async def get_devices_data(self) -> None:
|
223
198
|
"""Get the devices data from the PetKit servers."""
|
224
199
|
start_time = datetime.now()
|
225
200
|
if not self.account_data:
|
226
201
|
await self._get_account_data()
|
227
202
|
|
203
|
+
tasks = []
|
228
204
|
device_list: list[Device] = []
|
205
|
+
|
229
206
|
for account in self.account_data:
|
230
207
|
_LOGGER.debug("Fetching devices data for account: %s", account)
|
231
208
|
if account.device_list:
|
232
209
|
device_list.extend(account.device_list)
|
233
210
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
211
|
+
_LOGGER.debug("Fetch %s devices for this account", len(device_list))
|
212
|
+
|
213
|
+
for device in device_list:
|
214
|
+
_LOGGER.debug("Fetching devices data: %s", device)
|
215
|
+
device_type = device.device_type.lower()
|
216
|
+
device_id = device.device_id
|
217
|
+
if device_type in DEVICES_FEEDER:
|
218
|
+
# Add tasks for feeders
|
219
|
+
tasks.append(self._fetch_device_data(account, device_id, Feeder))
|
220
|
+
tasks.append(
|
221
|
+
self._fetch_device_data(account, device_id, FeederRecord)
|
222
|
+
)
|
223
|
+
elif device_type in DEVICES_LITTER_BOX:
|
224
|
+
# Add tasks for litter boxes
|
225
|
+
tasks.append(self._fetch_device_data(account, device_id, Litter))
|
226
|
+
tasks.append(
|
227
|
+
self._fetch_device_data(account, device_id, LitterRecord)
|
228
|
+
)
|
229
|
+
elif device_type in DEVICES_WATER_FOUNTAIN:
|
230
|
+
# Add tasks for water fountains
|
231
|
+
tasks.append(
|
232
|
+
self._fetch_device_data(account, device_id, WaterFountain)
|
233
|
+
)
|
234
|
+
tasks.append(
|
235
|
+
self._fetch_device_data(account, device_id, WaterFountainRecord)
|
236
|
+
)
|
237
|
+
else:
|
238
|
+
_LOGGER.warning("Unknown device type: %s", device_type)
|
248
239
|
await asyncio.gather(*tasks)
|
249
240
|
|
250
241
|
end_time = datetime.now()
|
251
242
|
total_time = end_time - start_time
|
252
|
-
_LOGGER.
|
243
|
+
_LOGGER.info("OK Petkit data fetched in : %s", total_time)
|
253
244
|
|
254
245
|
async def _fetch_device_data(
|
255
246
|
self,
|
256
|
-
|
257
|
-
|
247
|
+
account: AccountData,
|
248
|
+
device_id: int,
|
249
|
+
data_class: type[
|
250
|
+
Feeder
|
251
|
+
| Litter
|
252
|
+
| WaterFountain
|
253
|
+
| FeederRecord
|
254
|
+
| LitterRecord
|
255
|
+
| WaterFountainRecord
|
256
|
+
],
|
258
257
|
) -> None:
|
259
258
|
"""Fetch the device data from the PetKit servers."""
|
260
259
|
await self.validate_session()
|
261
|
-
|
260
|
+
device = None
|
261
|
+
|
262
|
+
if account.device_list:
|
263
|
+
device = next(
|
264
|
+
(
|
265
|
+
device
|
266
|
+
for device in account.device_list
|
267
|
+
if device.device_id == device_id
|
268
|
+
),
|
269
|
+
None,
|
270
|
+
)
|
271
|
+
if device is None:
|
272
|
+
_LOGGER.error("Device not found: id=%s", device_id)
|
273
|
+
return
|
262
274
|
device_type = device.device_type.lower()
|
263
|
-
query_param = data_class.query_param(device.device_id)
|
264
275
|
|
265
|
-
|
266
|
-
|
267
|
-
|
276
|
+
endpoint = data_class.get_endpoint(device_type)
|
277
|
+
query_param = data_class.query_param(account, device.device_id)
|
278
|
+
|
279
|
+
response = await self.req.request(
|
280
|
+
method=HTTPMethod.POST,
|
268
281
|
url=f"{device_type}/{endpoint}",
|
269
282
|
params=query_param,
|
270
|
-
headers=await self.
|
271
|
-
)
|
272
|
-
device_data = data_class(**response)
|
273
|
-
device_data.device_type = device.device_type # Add the device_type attribute
|
274
|
-
_LOGGER.debug(
|
275
|
-
"Reading device type : %s (id=%s)", device.device_type, device.device_id
|
283
|
+
headers=await self.get_session_id(),
|
276
284
|
)
|
277
|
-
|
285
|
+
|
286
|
+
# Check if the response is a list or a dict
|
287
|
+
if isinstance(response, list):
|
288
|
+
device_data = [data_class(**item) for item in response]
|
289
|
+
elif isinstance(response, dict):
|
290
|
+
device_data = data_class(**response)
|
291
|
+
else:
|
292
|
+
_LOGGER.error("Unexpected response type: %s", type(response))
|
293
|
+
return
|
294
|
+
|
295
|
+
if isinstance(device_data, list):
|
296
|
+
for item in device_data:
|
297
|
+
item.device_type = device_type
|
298
|
+
else:
|
299
|
+
device_data.device_type = device_type
|
300
|
+
|
301
|
+
_LOGGER.debug("Reading device type : %s (id=%s)", device_type, device_id)
|
302
|
+
|
303
|
+
if data_class.data_type == DEVICE_DATA:
|
304
|
+
self.petkit_entities[device_id] = device_data
|
305
|
+
elif data_class.data_type == DEVICE_RECORDS:
|
306
|
+
self.petkit_entities_records[device_id] = device_data
|
307
|
+
else:
|
308
|
+
_LOGGER.error("Unknown data type: %s", data_class.data_type)
|
278
309
|
|
279
310
|
async def send_api_request(
|
280
311
|
self,
|
281
312
|
device_id: int,
|
282
313
|
action: StrEnum,
|
283
|
-
setting: dict |
|
314
|
+
setting: dict | None = None,
|
284
315
|
) -> None:
|
285
316
|
"""Control the device using the PetKit API."""
|
286
|
-
device = self.
|
317
|
+
device = self.petkit_entities.get(device_id)
|
287
318
|
if not device:
|
288
319
|
raise PypetkitError(f"Device with ID {device_id} not found.")
|
289
320
|
|
@@ -295,57 +326,84 @@ class PetKitClient:
|
|
295
326
|
setting,
|
296
327
|
)
|
297
328
|
|
329
|
+
# Check if the device type is supported
|
298
330
|
if device.device_type:
|
299
331
|
device_type = device.device_type.lower()
|
300
332
|
else:
|
301
333
|
raise PypetkitError(
|
302
334
|
"Device type is not available, and is mandatory for sending commands."
|
303
335
|
)
|
304
|
-
|
336
|
+
# Check if the action is supported
|
305
337
|
if action not in ACTIONS_MAP:
|
306
338
|
raise PypetkitError(f"Action {action} not supported.")
|
307
339
|
|
308
340
|
action_info = ACTIONS_MAP[action]
|
341
|
+
_LOGGER.debug(action)
|
342
|
+
_LOGGER.debug(action_info)
|
309
343
|
if device_type not in action_info.supported_device:
|
310
344
|
raise PypetkitError(
|
311
345
|
f"Device type {device.device_type} not supported for action {action}."
|
312
346
|
)
|
313
|
-
|
347
|
+
# Get the endpoint
|
314
348
|
if callable(action_info.endpoint):
|
315
349
|
endpoint = action_info.endpoint(device)
|
350
|
+
_LOGGER.debug("Endpoint from callable")
|
316
351
|
else:
|
317
352
|
endpoint = action_info.endpoint
|
353
|
+
_LOGGER.debug("Endpoint field")
|
318
354
|
url = f"{device.device_type.lower()}/{endpoint}"
|
319
355
|
|
320
|
-
|
321
|
-
|
322
|
-
# Use the lambda to generate params
|
356
|
+
# Get the parameters
|
323
357
|
if setting is not None:
|
324
358
|
params = action_info.params(device, setting)
|
325
359
|
else:
|
326
360
|
params = action_info.params(device)
|
327
361
|
|
328
|
-
|
329
|
-
res = await prep_req.request(
|
362
|
+
res = await self.req.request(
|
330
363
|
method=HTTPMethod.POST,
|
331
364
|
url=url,
|
332
365
|
data=params,
|
333
|
-
headers=
|
366
|
+
headers=await self.get_session_id(),
|
334
367
|
)
|
335
368
|
if res in (SUCCESS_KEY, RES_KEY):
|
336
|
-
# TODO : Manage to get the response from
|
369
|
+
# TODO : Manage to get the response from manual feeding
|
337
370
|
_LOGGER.info("Command executed successfully")
|
338
371
|
else:
|
339
372
|
_LOGGER.error("Command execution failed")
|
340
373
|
|
374
|
+
async def close(self) -> None:
|
375
|
+
"""Close the aiohttp session if it was created by the client."""
|
376
|
+
if self.aiohttp_session:
|
377
|
+
await self.aiohttp_session.close()
|
378
|
+
|
341
379
|
|
342
380
|
class PrepReq:
|
343
381
|
"""Prepare the request to the PetKit API."""
|
344
382
|
|
345
|
-
def __init__(self, base_url: str,
|
383
|
+
def __init__(self, base_url: str, session: aiohttp.ClientSession) -> None:
|
346
384
|
"""Initialize the request."""
|
347
385
|
self.base_url = base_url
|
348
|
-
self.
|
386
|
+
self.session = session
|
387
|
+
self.base_headers = self._generate_header()
|
388
|
+
|
389
|
+
@staticmethod
|
390
|
+
def _generate_header() -> dict[str, str]:
|
391
|
+
"""Create header for interaction with API endpoint."""
|
392
|
+
|
393
|
+
return {
|
394
|
+
"Accept": Header.ACCEPT.value,
|
395
|
+
"Accept-Language": Header.ACCEPT_LANG,
|
396
|
+
"Accept-Encoding": Header.ENCODING,
|
397
|
+
"Content-Type": Header.CONTENT_TYPE,
|
398
|
+
"User-Agent": Header.AGENT,
|
399
|
+
"X-Img-Version": Header.IMG_VERSION,
|
400
|
+
"X-Locale": Header.LOCALE,
|
401
|
+
"X-Client": Header.CLIENT,
|
402
|
+
"X-Hour": Header.HOUR,
|
403
|
+
"X-TimezoneId": Header.TIMEZONE_ID,
|
404
|
+
"X-Api-Version": Header.API_VERSION,
|
405
|
+
"X-Timezone": Header.TIMEZONE,
|
406
|
+
}
|
349
407
|
|
350
408
|
async def request(
|
351
409
|
self,
|
@@ -366,34 +424,21 @@ class PrepReq:
|
|
366
424
|
data,
|
367
425
|
_headers,
|
368
426
|
)
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
"""If we get an error, lets log everything for debugging."""
|
381
|
-
try:
|
382
|
-
resp_json = await resp.json(content_type=None)
|
383
|
-
_LOGGER.info("Resp: %s", resp_json)
|
384
|
-
except ContentTypeError as err_2:
|
385
|
-
_LOGGER.info(err_2)
|
386
|
-
resp_raw = await resp.read()
|
387
|
-
_LOGGER.info("Resp raw: %s", resp_raw)
|
388
|
-
# Still raise the err so that it's clear it failed.
|
389
|
-
raise
|
390
|
-
except TimeoutError:
|
391
|
-
raise PetkitTimeoutError("The request timed out") from None
|
427
|
+
try:
|
428
|
+
async with self.session.request(
|
429
|
+
method,
|
430
|
+
_url,
|
431
|
+
params=params,
|
432
|
+
data=data,
|
433
|
+
headers=_headers,
|
434
|
+
) as resp:
|
435
|
+
return await self._handle_response(resp, _url)
|
436
|
+
except aiohttp.ClientConnectorError as e:
|
437
|
+
raise PetkitTimeoutError(f"Cannot connect to host: {e}") from e
|
392
438
|
|
393
439
|
@staticmethod
|
394
440
|
async def _handle_response(response: aiohttp.ClientResponse, url: str) -> dict:
|
395
441
|
"""Handle the response from the PetKit API."""
|
396
|
-
|
397
442
|
try:
|
398
443
|
response.raise_for_status()
|
399
444
|
except aiohttp.ClientResponseError as e:
|
@@ -408,6 +453,7 @@ class PrepReq:
|
|
408
453
|
"Response is not in JSON format"
|
409
454
|
) from None
|
410
455
|
|
456
|
+
# Check for errors in the response
|
411
457
|
if ERR_KEY in response_json:
|
412
458
|
error_msg = response_json[ERR_KEY].get("msg", "Unknown error")
|
413
459
|
if any(
|
@@ -421,6 +467,7 @@ class PrepReq:
|
|
421
467
|
raise PetkitAuthenticationError(f"Login failed: {error_msg}")
|
422
468
|
raise PypetkitError(f"Request failed: {error_msg}")
|
423
469
|
|
470
|
+
# Check for success in the response
|
424
471
|
if RES_KEY in response_json:
|
425
472
|
return response_json[RES_KEY]
|
426
473
|
|
pypetkitapi/command.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from collections.abc import Callable
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
import datetime
|
6
|
-
from enum import StrEnum
|
6
|
+
from enum import IntEnum, StrEnum
|
7
7
|
import json
|
8
8
|
|
9
9
|
from pypetkitapi.const import (
|
@@ -53,7 +53,7 @@ class LitterCommand(StrEnum):
|
|
53
53
|
class PetCommand(StrEnum):
|
54
54
|
"""PetCommand"""
|
55
55
|
|
56
|
-
|
56
|
+
PET_UPDATE_SETTING = "pet_update_setting"
|
57
57
|
|
58
58
|
|
59
59
|
class FountainCommand(StrEnum):
|
@@ -62,27 +62,22 @@ class FountainCommand(StrEnum):
|
|
62
62
|
CONTROL_DEVICE = "control_device"
|
63
63
|
|
64
64
|
|
65
|
-
class
|
65
|
+
class LBCommand(IntEnum):
|
66
66
|
"""LitterBoxCommand"""
|
67
67
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
RESUME_LITTER_DUMP = "resume_litter_dump"
|
82
|
-
RESET_MAX_DEODOR = "reset_max_deodorizer"
|
83
|
-
|
84
|
-
|
85
|
-
class LitterBoxCommandKey(StrEnum):
|
68
|
+
CLEANING = 0
|
69
|
+
DUMPING = 1
|
70
|
+
ODOR_REMOVAL = 2
|
71
|
+
RESETTING = 3
|
72
|
+
LEVELING = 4
|
73
|
+
CALIBRATING = 5
|
74
|
+
RESET_DEODOR = 6
|
75
|
+
LIGHT = 7
|
76
|
+
RESET_MAX_DEODOR = 8
|
77
|
+
MAINTENANCE = 9
|
78
|
+
|
79
|
+
|
80
|
+
class LBAction(StrEnum):
|
86
81
|
"""LitterBoxCommandKey"""
|
87
82
|
|
88
83
|
CONTINUE = "continue_action"
|
@@ -92,70 +87,6 @@ class LitterBoxCommandKey(StrEnum):
|
|
92
87
|
STOP = "stop_action"
|
93
88
|
|
94
89
|
|
95
|
-
class LitterBoxCommandType(StrEnum):
|
96
|
-
"""LitterBoxCommandType"""
|
97
|
-
|
98
|
-
CONTINUE = "continue"
|
99
|
-
END = "end"
|
100
|
-
POWER = "power"
|
101
|
-
START = "start"
|
102
|
-
STOP = "stop"
|
103
|
-
|
104
|
-
|
105
|
-
LB_CMD_TO_KEY = {
|
106
|
-
LitterBoxCommand.LIGHT_ON: LitterBoxCommandKey.START,
|
107
|
-
LitterBoxCommand.POWER: LitterBoxCommandKey.POWER,
|
108
|
-
LitterBoxCommand.START_CLEAN: LitterBoxCommandKey.START,
|
109
|
-
LitterBoxCommand.PAUSE_CLEAN: LitterBoxCommandKey.STOP,
|
110
|
-
LitterBoxCommand.RESUME_CLEAN: LitterBoxCommandKey.CONTINUE,
|
111
|
-
LitterBoxCommand.ODOR_REMOVAL: LitterBoxCommandKey.START,
|
112
|
-
LitterBoxCommand.RESET_DEODOR: LitterBoxCommandKey.START,
|
113
|
-
LitterBoxCommand.START_MAINTENANCE: LitterBoxCommandKey.START,
|
114
|
-
LitterBoxCommand.EXIT_MAINTENANCE: LitterBoxCommandKey.END,
|
115
|
-
LitterBoxCommand.PAUSE_MAINTENANCE_EXIT: LitterBoxCommandKey.STOP,
|
116
|
-
LitterBoxCommand.RESUME_MAINTENANCE_EXIT: LitterBoxCommandKey.CONTINUE,
|
117
|
-
LitterBoxCommand.DUMP_LITTER: LitterBoxCommandKey.START,
|
118
|
-
LitterBoxCommand.PAUSE_LITTER_DUMP: LitterBoxCommandKey.STOP,
|
119
|
-
LitterBoxCommand.RESUME_LITTER_DUMP: LitterBoxCommandKey.CONTINUE,
|
120
|
-
LitterBoxCommand.RESET_MAX_DEODOR: LitterBoxCommandKey.START,
|
121
|
-
}
|
122
|
-
|
123
|
-
LB_CMD_TO_TYPE = {
|
124
|
-
LitterBoxCommand.LIGHT_ON: LitterBoxCommandType.START,
|
125
|
-
LitterBoxCommand.POWER: LitterBoxCommandType.POWER,
|
126
|
-
LitterBoxCommand.START_CLEAN: LitterBoxCommandType.START,
|
127
|
-
LitterBoxCommand.PAUSE_CLEAN: LitterBoxCommandType.STOP,
|
128
|
-
LitterBoxCommand.RESUME_CLEAN: LitterBoxCommandType.CONTINUE,
|
129
|
-
LitterBoxCommand.ODOR_REMOVAL: LitterBoxCommandType.START,
|
130
|
-
LitterBoxCommand.RESET_DEODOR: LitterBoxCommandType.START,
|
131
|
-
LitterBoxCommand.START_MAINTENANCE: LitterBoxCommandType.START,
|
132
|
-
LitterBoxCommand.EXIT_MAINTENANCE: LitterBoxCommandType.END,
|
133
|
-
LitterBoxCommand.PAUSE_MAINTENANCE_EXIT: LitterBoxCommandType.STOP,
|
134
|
-
LitterBoxCommand.RESUME_MAINTENANCE_EXIT: LitterBoxCommandType.CONTINUE,
|
135
|
-
LitterBoxCommand.DUMP_LITTER: LitterBoxCommandType.START,
|
136
|
-
LitterBoxCommand.PAUSE_LITTER_DUMP: LitterBoxCommandType.STOP,
|
137
|
-
LitterBoxCommand.RESUME_LITTER_DUMP: LitterBoxCommandType.CONTINUE,
|
138
|
-
LitterBoxCommand.RESET_MAX_DEODOR: LitterBoxCommandType.START,
|
139
|
-
}
|
140
|
-
|
141
|
-
LB_CMD_TO_VALUE = {
|
142
|
-
LitterBoxCommand.LIGHT_ON: 7,
|
143
|
-
LitterBoxCommand.START_CLEAN: 0,
|
144
|
-
LitterBoxCommand.PAUSE_CLEAN: 0,
|
145
|
-
LitterBoxCommand.RESUME_CLEAN: 0,
|
146
|
-
LitterBoxCommand.ODOR_REMOVAL: 2,
|
147
|
-
LitterBoxCommand.RESET_DEODOR: 6,
|
148
|
-
LitterBoxCommand.START_MAINTENANCE: 9,
|
149
|
-
LitterBoxCommand.EXIT_MAINTENANCE: 9,
|
150
|
-
LitterBoxCommand.PAUSE_MAINTENANCE_EXIT: 9,
|
151
|
-
LitterBoxCommand.RESUME_MAINTENANCE_EXIT: 9,
|
152
|
-
LitterBoxCommand.DUMP_LITTER: 1,
|
153
|
-
LitterBoxCommand.PAUSE_LITTER_DUMP: 1,
|
154
|
-
LitterBoxCommand.RESUME_LITTER_DUMP: 1,
|
155
|
-
LitterBoxCommand.RESET_MAX_DEODOR: 8,
|
156
|
-
}
|
157
|
-
|
158
|
-
|
159
90
|
class FountainAction(StrEnum):
|
160
91
|
"""Fountain Action"""
|
161
92
|
|
@@ -304,25 +235,16 @@ ACTIONS_MAP = {
|
|
304
235
|
},
|
305
236
|
supported_device=[D3],
|
306
237
|
),
|
307
|
-
LitterCommand.POWER: CmdData(
|
308
|
-
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
309
|
-
params=lambda device, setting: {
|
310
|
-
"id": device.id,
|
311
|
-
"kv": json.dumps(setting),
|
312
|
-
"type": "power",
|
313
|
-
},
|
314
|
-
supported_device=[T3, T4, T5, T6],
|
315
|
-
),
|
316
238
|
LitterCommand.CONTROL_DEVICE: CmdData(
|
317
239
|
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
318
240
|
params=lambda device, command: {
|
319
241
|
"id": device.id,
|
320
|
-
"kv": json.dumps(
|
321
|
-
"type":
|
242
|
+
"kv": json.dumps(command),
|
243
|
+
"type": list(command.keys())[0].split("_")[0],
|
322
244
|
},
|
323
245
|
supported_device=[T3, T4, T5, T6],
|
324
246
|
),
|
325
|
-
PetCommand.
|
247
|
+
PetCommand.PET_UPDATE_SETTING: CmdData(
|
326
248
|
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
327
249
|
params=lambda pet, setting: {
|
328
250
|
"petId": pet,
|
pypetkitapi/const.py
CHANGED
@@ -9,6 +9,9 @@ RES_KEY = "result"
|
|
9
9
|
ERR_KEY = "error"
|
10
10
|
SUCCESS_KEY = "success"
|
11
11
|
|
12
|
+
DEVICE_RECORDS = "deviceRecords"
|
13
|
+
DEVICE_DATA = "deviceData"
|
14
|
+
|
12
15
|
# PetKit Models
|
13
16
|
FEEDER = "feeder"
|
14
17
|
FEEDER_MINI = "feedermini"
|
@@ -31,10 +34,10 @@ DEVICES_WATER_FOUNTAIN = [W5, CTW3]
|
|
31
34
|
ALL_DEVICES = [*DEVICES_LITTER_BOX, *DEVICES_FEEDER, *DEVICES_WATER_FOUNTAIN]
|
32
35
|
|
33
36
|
|
34
|
-
class
|
37
|
+
class PetkitDomain(StrEnum):
|
35
38
|
"""Petkit URL constants"""
|
36
39
|
|
37
|
-
|
40
|
+
PASSPORT_PETKIT = "https://passport.petkt.com/"
|
38
41
|
CHINA_SRV = "https://api.petkit.cn/6/"
|
39
42
|
|
40
43
|
|
@@ -58,7 +61,7 @@ class Header(StrEnum):
|
|
58
61
|
AGENT = "okhttp/3.12.11"
|
59
62
|
CLIENT = f"{Client.PLATFORM_TYPE}({Client.OS_VERSION};{Client.MODEL_NAME})"
|
60
63
|
TIMEZONE = "1.0"
|
61
|
-
TIMEZONE_ID = "Europe/Paris" # TODO: Make this dynamic
|
64
|
+
TIMEZONE_ID = "Europe/Paris" # TODO: Make this dynamic
|
62
65
|
LOCALE = "en-US"
|
63
66
|
IMG_VERSION = "1.0"
|
64
67
|
HOUR = "24"
|
@@ -71,7 +74,7 @@ CLIENT_NFO = {
|
|
71
74
|
"platform": Client.PLATFORM_TYPE.value,
|
72
75
|
"source": Client.SOURCE.value,
|
73
76
|
"timezone": Header.TIMEZONE.value, # TODO: Make this dynamic
|
74
|
-
"timezoneId": Header.TIMEZONE_ID.value,
|
77
|
+
"timezoneId": Header.TIMEZONE_ID.value,
|
75
78
|
"version": Header.API_VERSION.value,
|
76
79
|
}
|
77
80
|
|
@@ -84,11 +87,14 @@ LOGIN_DATA = {
|
|
84
87
|
class PetkitEndpoint(StrEnum):
|
85
88
|
"""Petkit Endpoint constants"""
|
86
89
|
|
90
|
+
REGION_SERVERS = "v1/regionservers"
|
87
91
|
LOGIN = "user/login"
|
88
92
|
GET_LOGIN_CODE = "user/sendcodeforquicklogin"
|
89
93
|
REFRESH_SESSION = "user/refreshsession"
|
90
94
|
FAMILY_LIST = "group/family/list"
|
91
95
|
REFRESH_HOME_V2 = "refreshHomeV2"
|
96
|
+
|
97
|
+
# Common to many device
|
92
98
|
DEVICE_DETAIL = "device_detail"
|
93
99
|
DEVICE_DATA = "deviceData"
|
94
100
|
GET_DEVICE_RECORD = "getDeviceRecord"
|
@@ -103,6 +109,7 @@ class PetkitEndpoint(StrEnum):
|
|
103
109
|
|
104
110
|
# Fountain & Litter Box
|
105
111
|
CONTROL_DEVICE = "controlDevice"
|
112
|
+
GET_WORK_RECORD = "getWorkRecord"
|
106
113
|
|
107
114
|
# Litter Box
|
108
115
|
DEODORANT_RESET = "deodorantReset"
|
pypetkitapi/containers.py
CHANGED
@@ -66,9 +66,10 @@ class Pet(BaseModel):
|
|
66
66
|
"""
|
67
67
|
|
68
68
|
avatar: str | None = None
|
69
|
-
created_at: int
|
70
|
-
pet_id: int
|
69
|
+
created_at: int = Field(alias="createdAt")
|
70
|
+
pet_id: int = Field(alias="petId")
|
71
71
|
pet_name: str | None = Field(None, alias="petName")
|
72
|
+
device_type: str = "pet"
|
72
73
|
|
73
74
|
|
74
75
|
class User(BaseModel):
|
pypetkitapi/feeder_container.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
"""Dataclasses for feeder data."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from datetime import datetime
|
4
4
|
from typing import Any, ClassVar
|
5
5
|
|
6
6
|
from pydantic import BaseModel, Field
|
7
7
|
|
8
|
-
from pypetkitapi.const import PetkitEndpoint
|
9
|
-
from pypetkitapi.containers import CloudProduct, FirmwareDetail, Wifi
|
8
|
+
from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
|
9
|
+
from pypetkitapi.containers import AccountData, CloudProduct, FirmwareDetail, Wifi
|
10
10
|
|
11
11
|
|
12
12
|
class FeedItem(BaseModel):
|
@@ -170,8 +170,7 @@ class ManualFeed(BaseModel):
|
|
170
170
|
class Feeder(BaseModel):
|
171
171
|
"""Dataclass for feeder data."""
|
172
172
|
|
173
|
-
|
174
|
-
query_param: ClassVar[Callable] = lambda device_id: {"id": device_id}
|
173
|
+
data_type: ClassVar[str] = DEVICE_DATA
|
175
174
|
|
176
175
|
auto_upgrade: int | None = Field(None, alias="autoUpgrade")
|
177
176
|
bt_mac: str | None = Field(None, alias="btMac")
|
@@ -202,7 +201,12 @@ class Feeder(BaseModel):
|
|
202
201
|
@classmethod
|
203
202
|
def get_endpoint(cls, device_type: str) -> str:
|
204
203
|
"""Get the endpoint URL for the given device type."""
|
205
|
-
return
|
204
|
+
return PetkitEndpoint.DEVICE_DETAIL
|
205
|
+
|
206
|
+
@classmethod
|
207
|
+
def query_param(cls, account: AccountData, device_id: int) -> dict:
|
208
|
+
"""Generate query parameters including request_date."""
|
209
|
+
return {"id": device_id}
|
206
210
|
|
207
211
|
|
208
212
|
class EventState(BaseModel):
|
@@ -217,8 +221,8 @@ class EventState(BaseModel):
|
|
217
221
|
surplus_standard: int | None = Field(None, alias="surplusStandard")
|
218
222
|
|
219
223
|
|
220
|
-
class
|
221
|
-
"""Dataclass for
|
224
|
+
class RecordsItems(BaseModel):
|
225
|
+
"""Dataclass for records items data."""
|
222
226
|
|
223
227
|
aes_key: str | None = Field(None, alias="aesKey")
|
224
228
|
duration: int | None = None
|
@@ -259,3 +263,40 @@ class FeederRecord(BaseModel):
|
|
259
263
|
src: int | None = None
|
260
264
|
state: EventState | None = None
|
261
265
|
status: int | None = None
|
266
|
+
|
267
|
+
|
268
|
+
class RecordsType(BaseModel):
|
269
|
+
"""Dataclass for records type data."""
|
270
|
+
|
271
|
+
add_amount1: int | None = Field(None, alias="addAmount1")
|
272
|
+
add_amount2: int | None = Field(None, alias="addAmount2")
|
273
|
+
day: int | None = None
|
274
|
+
device_id: int | None = Field(None, alias="deviceId")
|
275
|
+
eat_count: int | None = Field(None, alias="eatCount")
|
276
|
+
items: list[RecordsItems] | None = None
|
277
|
+
|
278
|
+
|
279
|
+
class FeederRecord(BaseModel):
|
280
|
+
"""Dataclass for feeder record data."""
|
281
|
+
|
282
|
+
data_type: ClassVar[str] = DEVICE_RECORDS
|
283
|
+
|
284
|
+
eat: list[RecordsType] | None = None
|
285
|
+
feed: list[RecordsType] | None = None
|
286
|
+
move: list[RecordsType] | None = None
|
287
|
+
pet: list[RecordsType] | None = None
|
288
|
+
device_type: str | None = Field(None, alias="deviceType")
|
289
|
+
|
290
|
+
@classmethod
|
291
|
+
def get_endpoint(cls, device_type: str) -> str:
|
292
|
+
"""Get the endpoint URL for the given device type."""
|
293
|
+
return PetkitEndpoint.GET_DEVICE_RECORD
|
294
|
+
|
295
|
+
@classmethod
|
296
|
+
def query_param(
|
297
|
+
cls, account: AccountData, device_id: int, request_date: str | None = None
|
298
|
+
) -> dict:
|
299
|
+
"""Generate query parameters including request_date."""
|
300
|
+
if request_date is None:
|
301
|
+
request_date = datetime.now().strftime("%Y%m%d")
|
302
|
+
return {"days": int(request_date), "deviceId": device_id}
|
pypetkitapi/litter_container.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
"""Dataclasses for Litter."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from datetime import datetime
|
4
4
|
from typing import Any, ClassVar
|
5
5
|
|
6
6
|
from pydantic import BaseModel, Field
|
7
7
|
|
8
|
-
from pypetkitapi.const import PetkitEndpoint
|
9
|
-
from pypetkitapi.containers import CloudProduct, FirmwareDetail, Wifi
|
8
|
+
from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
|
9
|
+
from pypetkitapi.containers import AccountData, CloudProduct, FirmwareDetail, Wifi
|
10
10
|
|
11
11
|
|
12
12
|
class SettingsLitter(BaseModel):
|
@@ -147,8 +147,7 @@ class Litter(BaseModel):
|
|
147
147
|
Supported devices = T4, T6
|
148
148
|
"""
|
149
149
|
|
150
|
-
|
151
|
-
query_param: ClassVar[Callable] = lambda device_id: {"id": device_id}
|
150
|
+
data_type: ClassVar[str] = DEVICE_DATA
|
152
151
|
|
153
152
|
auto_upgrade: int | None = Field(None, alias="autoUpgrade")
|
154
153
|
bt_mac: str | None = Field(None, alias="btMac")
|
@@ -187,4 +186,64 @@ class Litter(BaseModel):
|
|
187
186
|
@classmethod
|
188
187
|
def get_endpoint(cls, device_type: str) -> str:
|
189
188
|
"""Get the endpoint URL for the given device type."""
|
190
|
-
return
|
189
|
+
return PetkitEndpoint.DEVICE_DETAIL
|
190
|
+
|
191
|
+
@classmethod
|
192
|
+
def query_param(cls, account: AccountData, device_id: int) -> dict:
|
193
|
+
"""Generate query parameters including request_date."""
|
194
|
+
return {"id": device_id}
|
195
|
+
|
196
|
+
|
197
|
+
class Content(BaseModel):
|
198
|
+
"""Dataclass for content data."""
|
199
|
+
|
200
|
+
box: int | None = None
|
201
|
+
box_full: bool | None = Field(None, alias="boxFull")
|
202
|
+
litter_percent: int | None = Field(None, alias="litterPercent")
|
203
|
+
result: int | None = None
|
204
|
+
start_reason: int | None = Field(None, alias="startReason")
|
205
|
+
start_time: int | None = Field(None, alias="startTime")
|
206
|
+
|
207
|
+
|
208
|
+
class SubContent(BaseModel):
|
209
|
+
"""Dataclass for sub-content data."""
|
210
|
+
|
211
|
+
content: Content | None = None
|
212
|
+
device_id: int | None = Field(None, alias="deviceId")
|
213
|
+
enum_event_type: str | None = Field(None, alias="enumEventType")
|
214
|
+
event_type: int | None = Field(None, alias="eventType")
|
215
|
+
sub_content: list[Any] | None = Field(None, alias="subContent")
|
216
|
+
timestamp: int | None = None
|
217
|
+
user_id: str | None = Field(None, alias="userId")
|
218
|
+
|
219
|
+
|
220
|
+
class LitterRecord(BaseModel):
|
221
|
+
"""Dataclass for feeder record data."""
|
222
|
+
|
223
|
+
data_type: ClassVar[str] = DEVICE_RECORDS
|
224
|
+
|
225
|
+
avatar: str | None = None
|
226
|
+
content: dict[str, Any] | None = None
|
227
|
+
device_id: int | None = Field(None, alias="deviceId")
|
228
|
+
enum_event_type: str | None = Field(None, alias="enumEventType")
|
229
|
+
event_type: int | None = Field(None, alias="eventType")
|
230
|
+
pet_id: str | None = Field(None, alias="petId")
|
231
|
+
pet_name: str | None = Field(None, alias="petName")
|
232
|
+
sub_content: list[SubContent] | None = Field(None, alias="subContent")
|
233
|
+
timestamp: int | None = None
|
234
|
+
user_id: str | None = Field(None, alias="userId")
|
235
|
+
device_type: str | None = Field(None, alias="deviceType")
|
236
|
+
|
237
|
+
@classmethod
|
238
|
+
def get_endpoint(cls, device_type: str) -> str:
|
239
|
+
"""Get the endpoint URL for the given device type."""
|
240
|
+
return PetkitEndpoint.GET_DEVICE_RECORD
|
241
|
+
|
242
|
+
@classmethod
|
243
|
+
def query_param(
|
244
|
+
cls, account: AccountData, device_id: int, request_date: str | None = None
|
245
|
+
) -> dict:
|
246
|
+
"""Generate query parameters including request_date."""
|
247
|
+
if request_date is None:
|
248
|
+
request_date = datetime.now().strftime("%Y%m%d")
|
249
|
+
return {"date": int(request_date), "deviceId": device_id}
|
@@ -1,11 +1,12 @@
|
|
1
1
|
"""Dataclasses for Water Fountain."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from datetime import datetime
|
4
4
|
from typing import Any, ClassVar
|
5
5
|
|
6
6
|
from pydantic import BaseModel, Field
|
7
7
|
|
8
|
-
from pypetkitapi.const import PetkitEndpoint
|
8
|
+
from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
|
9
|
+
from pypetkitapi.containers import AccountData
|
9
10
|
|
10
11
|
|
11
12
|
class Electricity(BaseModel):
|
@@ -89,8 +90,7 @@ class WaterFountain(BaseModel):
|
|
89
90
|
Supported devices = CTW3
|
90
91
|
"""
|
91
92
|
|
92
|
-
|
93
|
-
query_param: ClassVar[Callable] = lambda device_id: {"id": device_id}
|
93
|
+
data_type: ClassVar[str] = DEVICE_DATA
|
94
94
|
|
95
95
|
breakdown_warning: int | None = Field(None, alias="breakdownWarning")
|
96
96
|
created_at: str | None = Field(None, alias="createdAt")
|
@@ -132,4 +132,41 @@ class WaterFountain(BaseModel):
|
|
132
132
|
@classmethod
|
133
133
|
def get_endpoint(cls, device_type: str) -> str:
|
134
134
|
"""Get the endpoint URL for the given device type."""
|
135
|
-
return
|
135
|
+
return PetkitEndpoint.DEVICE_DATA
|
136
|
+
|
137
|
+
@classmethod
|
138
|
+
def query_param(cls, account: AccountData, device_id: int) -> dict:
|
139
|
+
"""Generate query parameters including request_date."""
|
140
|
+
return {"id": device_id}
|
141
|
+
|
142
|
+
|
143
|
+
class WaterFountainRecord(BaseModel):
|
144
|
+
"""Dataclass for feeder record data."""
|
145
|
+
|
146
|
+
data_type: ClassVar[str] = DEVICE_RECORDS
|
147
|
+
|
148
|
+
day_time: int | None = Field(None, alias="dayTime")
|
149
|
+
stay_time: int | None = Field(None, alias="stayTime")
|
150
|
+
work_time: int | None = Field(None, alias="workTime")
|
151
|
+
device_type: str | None = Field(None, alias="deviceType")
|
152
|
+
|
153
|
+
@classmethod
|
154
|
+
def get_endpoint(cls, device_type: str) -> str:
|
155
|
+
"""Get the endpoint URL for the given device type."""
|
156
|
+
return PetkitEndpoint.GET_WORK_RECORD
|
157
|
+
|
158
|
+
@classmethod
|
159
|
+
def query_param(
|
160
|
+
cls, account: AccountData, device_id: int, request_date: str | None = None
|
161
|
+
) -> dict:
|
162
|
+
"""Generate query parameters including request_date."""
|
163
|
+
if not account.user_list or not account.user_list[0]:
|
164
|
+
raise ValueError("The account does not have a valid user_list.")
|
165
|
+
|
166
|
+
if request_date is None:
|
167
|
+
request_date = datetime.now().strftime("%Y%m%d")
|
168
|
+
return {
|
169
|
+
"day": int(request_date),
|
170
|
+
"deviceId": device_id,
|
171
|
+
"userId": account.user_list[0].user_id,
|
172
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pypetkitapi
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: Python client for PetKit API
|
5
5
|
Home-page: https://github.com/Jezza34000/pypetkit
|
6
6
|
License: MIT
|
@@ -20,19 +20,23 @@ Description-Content-Type: text/markdown
|
|
20
20
|
---
|
21
21
|
|
22
22
|
[][pypi_]
|
23
|
-
[][python version]
|
23
|
+
[][python version] [](https://github.com/Jezza34000/py-petkit-api/actions)
|
24
|
+
|
25
|
+
---
|
26
|
+
|
27
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
28
|
+
|
29
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
30
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
31
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
24
32
|
|
25
33
|
[][pre-commit]
|
26
34
|
[][black]
|
27
35
|
[](https://mypy.readthedocs.io/en/stable/)
|
28
36
|
[](https://github.com/astral-sh/ruff)
|
29
|
-
[](https://github.com/Jezza34000/py-petkit-api/actions)
|
30
37
|
|
31
38
|
---
|
32
39
|
|
33
|
-
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
34
|
-
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
35
|
-
|
36
40
|
[pypi_]: https://pypi.org/project/pypetkitapi/
|
37
41
|
[python version]: https://pypi.org/project/pypetkitapi
|
38
42
|
[pre-commit]: https://github.com/pre-commit/pre-commit
|
@@ -59,38 +63,42 @@ pip install pypetkitapi
|
|
59
63
|
## Usage Example:
|
60
64
|
|
61
65
|
```python
|
62
|
-
import
|
63
|
-
import logging
|
66
|
+
import aiohttp
|
64
67
|
from pypetkitapi.client import PetKitClient
|
68
|
+
from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
|
65
69
|
|
66
70
|
logging.basicConfig(level=logging.DEBUG)
|
67
71
|
|
68
|
-
|
69
72
|
async def main():
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
73
|
+
async with aiohttp.ClientSession() as session:
|
74
|
+
client = PetKitClient(
|
75
|
+
username="username", # Your PetKit account username or id
|
76
|
+
password="password", # Your PetKit account password
|
77
|
+
region="FR", # Your region or country code (e.g. FR, US, etc.)
|
78
|
+
timezone="Europe/Paris", # Your timezone
|
79
|
+
session=session,
|
80
|
+
)
|
81
|
+
|
82
|
+
await client.get_devices_data()
|
83
|
+
|
84
|
+
# Read the account data
|
85
|
+
print(client.account_data)
|
86
|
+
|
87
|
+
# Read the devices data
|
88
|
+
print(client.petkit_entities)
|
89
|
+
|
90
|
+
# Send command to the devices
|
91
|
+
### Example 1 : Turn on the indicator light
|
92
|
+
### Device_ID, Command, Payload
|
93
|
+
await client.send_api_request(123456789, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
|
94
|
+
|
95
|
+
### Example 2 : Feed the pet
|
96
|
+
### Device_ID, Command, Payload
|
97
|
+
await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
|
98
|
+
|
99
|
+
### Example 3 : Start the cleaning process
|
100
|
+
### Device_ID, Command, Payload
|
101
|
+
await client.send_api_request(123456789, LitterCommand.CONTROL_DEVICE, {LBAction.START: LBCommand.CLEANING})
|
94
102
|
|
95
103
|
|
96
104
|
if __name__ == "__main__":
|
@@ -0,0 +1,14 @@
|
|
1
|
+
pypetkitapi/__init__.py,sha256=eVpyGMD3tkYtiHUkdKEeNSZhQlZ4woI2Y5oVoV7CwXM,61
|
2
|
+
pypetkitapi/client.py,sha256=RCt3vxNJ14NKzwUpvL_M6xSYC4DfxEyDu7B8sJ7HFz0,16591
|
3
|
+
pypetkitapi/command.py,sha256=gw3_J_oZHuuGLk66P8uRSqSrySjYa8ArpKaPHi2ybCw,7155
|
4
|
+
pypetkitapi/const.py,sha256=EE84cCOEE6AMv3QuDJa8-FreCffhfq8nxPxqBpvB5XY,3270
|
5
|
+
pypetkitapi/containers.py,sha256=m7vzWcJG0U1EPftuBF6OB8eTVRhCoA2DFqekxI6LozI,3428
|
6
|
+
pypetkitapi/exceptions.py,sha256=NWmpsI2ewC4HaIeu_uFwCeuPIHIJxZBzjoCP7aNwvhs,1139
|
7
|
+
pypetkitapi/feeder_container.py,sha256=XpZqeQ8E4_ud3juxDBKGTMfmCfvc9QYcJInacmbWK-o,12785
|
8
|
+
pypetkitapi/litter_container.py,sha256=TK2-_yg3EmT0RQzMLurud2PHwa2P-o2KGyV9m_SQ7qw,11651
|
9
|
+
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
pypetkitapi/water_fountain_container.py,sha256=xmqYuxyppRLhqi7cF4JNRprhgmz3KKrbi4iv942wNaU,6566
|
11
|
+
pypetkitapi-1.0.0.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
12
|
+
pypetkitapi-1.0.0.dist-info/METADATA,sha256=t1zmfg9-IZqpzBWybjhVTExCgkVJ50Vu8W-Twx8njiE,4590
|
13
|
+
pypetkitapi-1.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
14
|
+
pypetkitapi-1.0.0.dist-info/RECORD,,
|
@@ -1,14 +0,0 @@
|
|
1
|
-
pypetkitapi/__init__.py,sha256=eVpyGMD3tkYtiHUkdKEeNSZhQlZ4woI2Y5oVoV7CwXM,61
|
2
|
-
pypetkitapi/client.py,sha256=wQqX5qH0NMWx0uSLo6fmn4ELhBHcegiEjEwsOrLJaOA,14945
|
3
|
-
pypetkitapi/command.py,sha256=MZbnmWpmekVzlWCg_ubcrFnOoqUCZh1Bamcf_QUos38,10605
|
4
|
-
pypetkitapi/const.py,sha256=B4uf3RGEkKmpVOo69DRP9g8hF_bodCA7vohnPjnrPFs,3183
|
5
|
-
pypetkitapi/containers.py,sha256=4O79O9HZn4kksQysXGXcqVwvkdxbvhs1pr-0WRNCANc,3425
|
6
|
-
pypetkitapi/exceptions.py,sha256=NWmpsI2ewC4HaIeu_uFwCeuPIHIJxZBzjoCP7aNwvhs,1139
|
7
|
-
pypetkitapi/feeder_container.py,sha256=6a1Y_mTSGuD8qK1YqmiCfxPbZl84bcoIIP_d5g-UctQ,11381
|
8
|
-
pypetkitapi/litter_container.py,sha256=baeIihndFfU5F3kxtyO8Dm1qx1mPuMAUIYRH60CV3GM,9447
|
9
|
-
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
pypetkitapi/water_fountain_container.py,sha256=LcCTDjk7eSbnF7e38xev3D5mCv5wwJ6go8WGGBv-CaU,5278
|
11
|
-
pypetkitapi-0.5.3.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
12
|
-
pypetkitapi-0.5.3.dist-info/METADATA,sha256=S43iNnEv3nDgaYIlF38R24rLVKlRCB-1wkqGRFK25B0,3611
|
13
|
-
pypetkitapi-0.5.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
14
|
-
pypetkitapi-0.5.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|