pypetkitapi 1.7.10__tar.gz → 1.9.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-1.7.10 → pypetkitapi-1.9.0}/PKG-INFO +11 -2
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/README.md +9 -0
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/__init__.py +47 -17
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/client.py +218 -22
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/command.py +57 -60
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/const.py +15 -5
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/containers.py +13 -7
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/feeder_container.py +3 -2
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/litter_container.py +28 -5
- pypetkitapi-1.9.0/pypetkitapi/purifier_container.py +77 -0
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/water_fountain_container.py +4 -2
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pyproject.toml +2 -2
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/LICENSE +0 -0
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/exceptions.py +0 -0
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/medias.py +0 -0
- {pypetkitapi-1.7.10 → pypetkitapi-1.9.0}/pypetkitapi/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: pypetkitapi
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.9.0
|
4
4
|
Summary: Python client for PetKit API
|
5
5
|
Home-page: https://github.com/Jezza34000/pypetkit
|
6
6
|
License: MIT
|
@@ -66,6 +66,8 @@ pip install pypetkitapi
|
|
66
66
|
## Usage Example:
|
67
67
|
|
68
68
|
```python
|
69
|
+
import asyncio
|
70
|
+
import logging
|
69
71
|
import aiohttp
|
70
72
|
from pypetkitapi.client import PetKitClient
|
71
73
|
from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
|
@@ -99,7 +101,10 @@ async def main():
|
|
99
101
|
|
100
102
|
### Example 2 : Feed the pet
|
101
103
|
### Device_ID, Command, Payload
|
104
|
+
# simple hopper :
|
102
105
|
await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
|
106
|
+
# dual hopper :
|
107
|
+
await client.send_api_request(123456789, FeederCommand.MANUAL_FEED_DUAL, {"amount1": 2})
|
103
108
|
|
104
109
|
### Example 3 : Start the cleaning process
|
105
110
|
### Device_ID, Command, Payload
|
@@ -110,6 +115,10 @@ if __name__ == "__main__":
|
|
110
115
|
asyncio.run(main())
|
111
116
|
```
|
112
117
|
|
118
|
+
## More example usage
|
119
|
+
|
120
|
+
Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
|
121
|
+
|
113
122
|
## Contributing
|
114
123
|
|
115
124
|
Contributions are welcome! Please open an issue or submit a pull request.
|
@@ -46,6 +46,8 @@ pip install pypetkitapi
|
|
46
46
|
## Usage Example:
|
47
47
|
|
48
48
|
```python
|
49
|
+
import asyncio
|
50
|
+
import logging
|
49
51
|
import aiohttp
|
50
52
|
from pypetkitapi.client import PetKitClient
|
51
53
|
from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
|
@@ -79,7 +81,10 @@ async def main():
|
|
79
81
|
|
80
82
|
### Example 2 : Feed the pet
|
81
83
|
### Device_ID, Command, Payload
|
84
|
+
# simple hopper :
|
82
85
|
await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
|
86
|
+
# dual hopper :
|
87
|
+
await client.send_api_request(123456789, FeederCommand.MANUAL_FEED_DUAL, {"amount1": 2})
|
83
88
|
|
84
89
|
### Example 3 : Start the cleaning process
|
85
90
|
### Device_ID, Command, Payload
|
@@ -90,6 +95,10 @@ if __name__ == "__main__":
|
|
90
95
|
asyncio.run(main())
|
91
96
|
```
|
92
97
|
|
98
|
+
## More example usage
|
99
|
+
|
100
|
+
Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
|
101
|
+
|
93
102
|
## Contributing
|
94
103
|
|
95
104
|
Contributions are welcome! Please open an issue or submit a pull request.
|
@@ -1,15 +1,25 @@
|
|
1
1
|
"""Pypetkit: A Python library for interfacing with PetKit"""
|
2
2
|
|
3
3
|
from .client import PetKitClient
|
4
|
-
from .command import
|
4
|
+
from .command import (
|
5
|
+
DeviceAction,
|
6
|
+
DeviceCommand,
|
7
|
+
FeederCommand,
|
8
|
+
LBCommand,
|
9
|
+
LitterCommand,
|
10
|
+
PetCommand,
|
11
|
+
PurMode,
|
12
|
+
)
|
5
13
|
from .const import (
|
6
14
|
CTW3,
|
7
15
|
D3,
|
8
16
|
D4,
|
9
17
|
D4H,
|
10
18
|
D4S,
|
19
|
+
D4SH,
|
11
20
|
DEVICES_FEEDER,
|
12
21
|
DEVICES_LITTER_BOX,
|
22
|
+
DEVICES_PURIFIER,
|
13
23
|
DEVICES_WATER_FOUNTAIN,
|
14
24
|
FEEDER,
|
15
25
|
FEEDER_MINI,
|
@@ -22,35 +32,55 @@ from .const import (
|
|
22
32
|
W5,
|
23
33
|
RecordType,
|
24
34
|
)
|
35
|
+
from .containers import Pet
|
36
|
+
from .exceptions import PetkitAuthenticationError, PypetkitError
|
37
|
+
from .feeder_container import Feeder, RecordsItems
|
38
|
+
from .litter_container import Litter, LitterRecord, WorkState
|
25
39
|
from .medias import MediaHandler, MediasFiles
|
40
|
+
from .purifier_container import Purifier
|
41
|
+
from .water_fountain_container import WaterFountain
|
26
42
|
|
27
|
-
__version__ = "1.
|
43
|
+
__version__ = "1.9.0"
|
28
44
|
|
29
45
|
__all__ = [
|
30
|
-
"
|
31
|
-
"MediaHandler",
|
46
|
+
"CTW3",
|
32
47
|
"D3",
|
33
48
|
"D4",
|
34
49
|
"D4H",
|
35
50
|
"D4S",
|
51
|
+
"D4SH",
|
52
|
+
"DEVICES_FEEDER",
|
53
|
+
"DEVICES_LITTER_BOX",
|
54
|
+
"DEVICES_PURIFIER",
|
55
|
+
"DEVICES_WATER_FOUNTAIN",
|
56
|
+
"DeviceAction",
|
57
|
+
"DeviceCommand",
|
36
58
|
"FEEDER",
|
37
59
|
"FEEDER_MINI",
|
60
|
+
"Feeder",
|
61
|
+
"FeederCommand",
|
62
|
+
"K2",
|
63
|
+
"K3",
|
64
|
+
"LBCommand",
|
65
|
+
"Litter",
|
66
|
+
"LitterCommand",
|
67
|
+
"LitterRecord",
|
68
|
+
"MediaHandler",
|
69
|
+
"MediasFiles",
|
70
|
+
"Pet",
|
71
|
+
"PetCommand",
|
72
|
+
"PetKitClient",
|
73
|
+
"PetkitAuthenticationError",
|
74
|
+
"PurMode",
|
75
|
+
"Purifier",
|
76
|
+
"PypetkitError",
|
77
|
+
"RecordType",
|
78
|
+
"RecordsItems",
|
38
79
|
"T3",
|
39
80
|
"T4",
|
40
81
|
"T5",
|
41
82
|
"T6",
|
42
83
|
"W5",
|
43
|
-
"
|
44
|
-
"
|
45
|
-
"K3",
|
46
|
-
"DEVICES_FEEDER",
|
47
|
-
"DEVICES_LITTER_BOX",
|
48
|
-
"DEVICES_WATER_FOUNTAIN",
|
49
|
-
"RecordType",
|
50
|
-
"PetKitClient",
|
51
|
-
"FeederCommand",
|
52
|
-
"LitterCommand",
|
53
|
-
"PetCommand",
|
54
|
-
"LBCommand",
|
55
|
-
"LBAction",
|
84
|
+
"WaterFountain",
|
85
|
+
"WorkState",
|
56
86
|
]
|
@@ -1,25 +1,32 @@
|
|
1
1
|
"""Pypetkit Client: A Python library for interfacing with PetKit"""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
import base64
|
4
5
|
from datetime import datetime, timedelta
|
5
6
|
from enum import StrEnum
|
6
7
|
import hashlib
|
7
8
|
from http import HTTPMethod
|
8
9
|
import logging
|
10
|
+
import urllib.parse
|
9
11
|
|
10
12
|
import aiohttp
|
11
13
|
from aiohttp import ContentTypeError
|
12
14
|
|
13
|
-
from pypetkitapi.command import ACTIONS_MAP
|
15
|
+
from pypetkitapi.command import ACTIONS_MAP, FOUNTAIN_COMMAND, FountainAction
|
14
16
|
from pypetkitapi.const import (
|
17
|
+
BLE_CONNECT_ATTEMPT,
|
18
|
+
BLE_END_TRAME,
|
19
|
+
BLE_START_TRAME,
|
15
20
|
DEVICE_DATA,
|
16
21
|
DEVICE_RECORDS,
|
17
22
|
DEVICE_STATS,
|
18
23
|
DEVICES_FEEDER,
|
19
24
|
DEVICES_LITTER_BOX,
|
25
|
+
DEVICES_PURIFIER,
|
20
26
|
DEVICES_WATER_FOUNTAIN,
|
21
27
|
ERR_KEY,
|
22
28
|
LOGIN_DATA,
|
29
|
+
PET,
|
23
30
|
RES_KEY,
|
24
31
|
T4,
|
25
32
|
T6,
|
@@ -27,7 +34,14 @@ from pypetkitapi.const import (
|
|
27
34
|
PetkitDomain,
|
28
35
|
PetkitEndpoint,
|
29
36
|
)
|
30
|
-
from pypetkitapi.containers import
|
37
|
+
from pypetkitapi.containers import (
|
38
|
+
AccountData,
|
39
|
+
BleRelay,
|
40
|
+
Device,
|
41
|
+
Pet,
|
42
|
+
RegionInfo,
|
43
|
+
SessionInfo,
|
44
|
+
)
|
31
45
|
from pypetkitapi.exceptions import (
|
32
46
|
PetkitAuthenticationError,
|
33
47
|
PetkitAuthenticationUnregisteredEmailError,
|
@@ -40,6 +54,7 @@ from pypetkitapi.exceptions import (
|
|
40
54
|
)
|
41
55
|
from pypetkitapi.feeder_container import Feeder, FeederRecord
|
42
56
|
from pypetkitapi.litter_container import Litter, LitterRecord, LitterStats, PetOutGraph
|
57
|
+
from pypetkitapi.purifier_container import Purifier
|
43
58
|
from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
|
44
59
|
|
45
60
|
_LOGGER = logging.getLogger(__name__)
|
@@ -50,7 +65,7 @@ class PetKitClient:
|
|
50
65
|
|
51
66
|
_session: SessionInfo | None = None
|
52
67
|
account_data: list[AccountData] = []
|
53
|
-
petkit_entities: dict[int, Feeder | Litter | WaterFountain | Pet] = {}
|
68
|
+
petkit_entities: dict[int, Feeder | Litter | WaterFountain | Purifier | Pet] = {}
|
54
69
|
|
55
70
|
def __init__(
|
56
71
|
self,
|
@@ -75,6 +90,7 @@ class PetKitClient:
|
|
75
90
|
async def _get_base_url(self) -> None:
|
76
91
|
"""Get the list of API servers, filter by region, and return the matching server."""
|
77
92
|
_LOGGER.debug("Getting API server list")
|
93
|
+
self.req.base_url = PetkitDomain.PASSPORT_PETKIT
|
78
94
|
|
79
95
|
if self.region.lower() == "china":
|
80
96
|
self.req.base_url = PetkitDomain.CHINA_SRV
|
@@ -126,8 +142,9 @@ class PetKitClient:
|
|
126
142
|
data["validCode"] = valid_code
|
127
143
|
else:
|
128
144
|
_LOGGER.debug("Login method: using password")
|
129
|
-
|
130
|
-
|
145
|
+
data["password"] = hashlib.md5(
|
146
|
+
self.password.encode()
|
147
|
+
).hexdigest() # noqa: S324
|
131
148
|
|
132
149
|
# Send the login request
|
133
150
|
response = await self.req.request(
|
@@ -149,6 +166,7 @@ class PetKitClient:
|
|
149
166
|
)
|
150
167
|
session_data = response["session"]
|
151
168
|
self._session = SessionInfo(**session_data)
|
169
|
+
self._session.refreshed_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")
|
152
170
|
|
153
171
|
async def validate_session(self) -> None:
|
154
172
|
"""Check if the session is still valid and refresh or re-login if necessary."""
|
@@ -195,6 +213,16 @@ class PetKitClient:
|
|
195
213
|
if account.pet_list:
|
196
214
|
for pet in account.pet_list:
|
197
215
|
self.petkit_entities[pet.pet_id] = pet
|
216
|
+
pet.device_nfo = Device(
|
217
|
+
deviceType=PET,
|
218
|
+
deviceId=pet.pet_id,
|
219
|
+
createdAt=pet.created_at,
|
220
|
+
deviceName=pet.pet_name,
|
221
|
+
groupId=0,
|
222
|
+
type=0,
|
223
|
+
typeCode=0,
|
224
|
+
uniqueId=pet.sn,
|
225
|
+
)
|
198
226
|
|
199
227
|
async def get_devices_data(self) -> None:
|
200
228
|
"""Get the devices data from the PetKit servers."""
|
@@ -216,10 +244,12 @@ class PetKitClient:
|
|
216
244
|
_LOGGER.debug("Found %s devices", len(account.device_list))
|
217
245
|
|
218
246
|
for device in device_list:
|
219
|
-
device_type = device.device_type
|
247
|
+
device_type = device.device_type
|
248
|
+
|
220
249
|
if device_type in DEVICES_FEEDER:
|
221
250
|
main_tasks.append(self._fetch_device_data(device, Feeder))
|
222
251
|
record_tasks.append(self._fetch_device_data(device, FeederRecord))
|
252
|
+
|
223
253
|
elif device_type in DEVICES_LITTER_BOX:
|
224
254
|
main_tasks.append(
|
225
255
|
self._fetch_device_data(device, Litter),
|
@@ -237,6 +267,9 @@ class PetKitClient:
|
|
237
267
|
self._fetch_device_data(device, WaterFountainRecord)
|
238
268
|
)
|
239
269
|
|
270
|
+
elif device_type in DEVICES_PURIFIER:
|
271
|
+
main_tasks.append(self._fetch_device_data(device, Purifier))
|
272
|
+
|
240
273
|
# Execute main device tasks first
|
241
274
|
await asyncio.gather(*main_tasks)
|
242
275
|
|
@@ -247,7 +280,7 @@ class PetKitClient:
|
|
247
280
|
stats_tasks = [
|
248
281
|
self.populate_pet_stats(self.petkit_entities[device.device_id])
|
249
282
|
for device in device_list
|
250
|
-
if device.device_type
|
283
|
+
if device.device_type in DEVICES_LITTER_BOX
|
251
284
|
]
|
252
285
|
|
253
286
|
# Execute stats tasks
|
@@ -264,6 +297,7 @@ class PetKitClient:
|
|
264
297
|
Feeder
|
265
298
|
| Litter
|
266
299
|
| WaterFountain
|
300
|
+
| Purifier
|
267
301
|
| FeederRecord
|
268
302
|
| LitterRecord
|
269
303
|
| WaterFountainRecord
|
@@ -272,7 +306,7 @@ class PetKitClient:
|
|
272
306
|
],
|
273
307
|
) -> None:
|
274
308
|
"""Fetch the device data from the PetKit servers."""
|
275
|
-
device_type = device.device_type
|
309
|
+
device_type = device.device_type
|
276
310
|
|
277
311
|
_LOGGER.debug("Reading device type : %s (id=%s)", device_type, device.device_id)
|
278
312
|
|
@@ -309,15 +343,9 @@ class PetKitClient:
|
|
309
343
|
_LOGGER.error("Unexpected response type: %s", type(response))
|
310
344
|
return
|
311
345
|
|
312
|
-
# Add the device type into dataclass
|
313
|
-
if isinstance(device_data, list):
|
314
|
-
for item in device_data:
|
315
|
-
item.device_type = device_type
|
316
|
-
else:
|
317
|
-
device_data.device_type = device_type
|
318
|
-
|
319
346
|
if data_class.data_type == DEVICE_DATA:
|
320
347
|
self.petkit_entities[device.device_id] = device_data
|
348
|
+
self.petkit_entities[device.device_id].device_nfo = device
|
321
349
|
_LOGGER.debug("Device data fetched OK for %s", device_type)
|
322
350
|
elif data_class.data_type == DEVICE_RECORDS:
|
323
351
|
self.petkit_entities[device.device_id].device_records = device_data
|
@@ -358,9 +386,12 @@ class PetKitClient:
|
|
358
386
|
|
359
387
|
pets_list = await self.get_pets_list()
|
360
388
|
for pet in pets_list:
|
361
|
-
if stats_data.device_type == T4 and stats_data.device_records:
|
389
|
+
if stats_data.device_nfo.device_type == T4 and stats_data.device_records:
|
362
390
|
await self._process_t4(pet, stats_data.device_records)
|
363
|
-
elif
|
391
|
+
elif (
|
392
|
+
stats_data.device_nfo.device_type == T6
|
393
|
+
and stats_data.device_pet_graph_out
|
394
|
+
):
|
364
395
|
await self._process_t6(pet, stats_data.device_pet_graph_out)
|
365
396
|
|
366
397
|
async def _process_t4(self, pet, device_records):
|
@@ -394,6 +425,171 @@ class PetKitClient:
|
|
394
425
|
pet.last_duration_usage = self.get_safe_value(graph.toilet_time)
|
395
426
|
pet.last_device_used = "Purobot Ultra"
|
396
427
|
|
428
|
+
async def _get_fountain_instance(self, fountain_id: int) -> WaterFountain:
|
429
|
+
# Retrieve the water fountain object
|
430
|
+
water_fountain = self.petkit_entities.get(fountain_id)
|
431
|
+
if not water_fountain:
|
432
|
+
_LOGGER.error("Water fountain with ID %s not found.", fountain_id)
|
433
|
+
raise ValueError(f"Water fountain with ID {fountain_id} not found.")
|
434
|
+
return water_fountain
|
435
|
+
|
436
|
+
async def check_relay_availability(self, fountain_id: int) -> bool:
|
437
|
+
"""Check if BLE relay is available for the account."""
|
438
|
+
fountain = None
|
439
|
+
for account in self.account_data:
|
440
|
+
fountain = next(
|
441
|
+
(
|
442
|
+
device
|
443
|
+
for device in account.device_list
|
444
|
+
if device.device_id == fountain_id
|
445
|
+
),
|
446
|
+
None,
|
447
|
+
)
|
448
|
+
if fountain:
|
449
|
+
break
|
450
|
+
|
451
|
+
if not fountain:
|
452
|
+
raise ValueError(
|
453
|
+
f"Fountain with device_id {fountain_id} not found for the current account"
|
454
|
+
)
|
455
|
+
|
456
|
+
group_id = fountain.group_id
|
457
|
+
|
458
|
+
response = await self.req.request(
|
459
|
+
method=HTTPMethod.POST,
|
460
|
+
url=f"{PetkitEndpoint.BLE_AS_RELAY}",
|
461
|
+
params={"groupId": group_id},
|
462
|
+
headers=await self.get_session_id(),
|
463
|
+
)
|
464
|
+
ble_relays = [BleRelay(**relay) for relay in response]
|
465
|
+
|
466
|
+
if len(ble_relays) == 0:
|
467
|
+
_LOGGER.warning("No BLE relay devices found.")
|
468
|
+
return False
|
469
|
+
return True
|
470
|
+
|
471
|
+
async def open_ble_connection(self, fountain_id: int) -> bool:
|
472
|
+
"""Open a BLE connection to a PetKit device."""
|
473
|
+
_LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
|
474
|
+
water_fountain = await self._get_fountain_instance(fountain_id)
|
475
|
+
|
476
|
+
if await self.check_relay_availability(fountain_id) is False:
|
477
|
+
_LOGGER.error("BLE relay not available.")
|
478
|
+
return False
|
479
|
+
|
480
|
+
if water_fountain.is_connected is True:
|
481
|
+
_LOGGER.error("BLE connection already established.")
|
482
|
+
return True
|
483
|
+
|
484
|
+
response = await self.req.request(
|
485
|
+
method=HTTPMethod.POST,
|
486
|
+
url=PetkitEndpoint.BLE_CONNECT,
|
487
|
+
data={
|
488
|
+
"bleId": fountain_id,
|
489
|
+
"type": 24,
|
490
|
+
"mac": water_fountain.mac,
|
491
|
+
},
|
492
|
+
headers=await self.get_session_id(),
|
493
|
+
)
|
494
|
+
if response != {"state": 1}:
|
495
|
+
_LOGGER.error("Failed to establish BLE connection.")
|
496
|
+
water_fountain.is_connected = False
|
497
|
+
return False
|
498
|
+
|
499
|
+
for attempt in range(BLE_CONNECT_ATTEMPT):
|
500
|
+
_LOGGER.warning("BLE connection attempt n%s", attempt)
|
501
|
+
response = await self.req.request(
|
502
|
+
method=HTTPMethod.POST,
|
503
|
+
url=PetkitEndpoint.BLE_POLL,
|
504
|
+
data={
|
505
|
+
"bleId": fountain_id,
|
506
|
+
"type": 24,
|
507
|
+
"mac": water_fountain.mac,
|
508
|
+
},
|
509
|
+
headers=await self.get_session_id(),
|
510
|
+
)
|
511
|
+
if response == 1:
|
512
|
+
_LOGGER.info("BLE connection established successfully.")
|
513
|
+
water_fountain.is_connected = True
|
514
|
+
water_fountain.last_ble_poll = datetime.now().strftime(
|
515
|
+
"%Y-%m-%dT%H:%M:%S.%f"
|
516
|
+
)
|
517
|
+
return True
|
518
|
+
await asyncio.sleep(4)
|
519
|
+
_LOGGER.error("Failed to establish BLE connection after multiple attempts.")
|
520
|
+
water_fountain.is_connected = False
|
521
|
+
return False
|
522
|
+
|
523
|
+
async def close_ble_connection(self, fountain_id: int) -> None:
|
524
|
+
"""Close the BLE connection to a PetKit device."""
|
525
|
+
_LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
|
526
|
+
water_fountain = await self._get_fountain_instance(fountain_id)
|
527
|
+
|
528
|
+
await self.req.request(
|
529
|
+
method=HTTPMethod.POST,
|
530
|
+
url=PetkitEndpoint.BLE_CANCEL,
|
531
|
+
data={
|
532
|
+
"bleId": fountain_id,
|
533
|
+
"type": 24,
|
534
|
+
"mac": water_fountain.mac,
|
535
|
+
},
|
536
|
+
headers=await self.get_session_id(),
|
537
|
+
)
|
538
|
+
_LOGGER.info("BLE connection closed successfully.")
|
539
|
+
|
540
|
+
async def get_ble_cmd_data(
|
541
|
+
self, fountain_command: list, counter: int
|
542
|
+
) -> tuple[int, str]:
|
543
|
+
"""Prepare BLE data by adding start and end trame to the command and extracting the first number."""
|
544
|
+
cmd_code = fountain_command[0]
|
545
|
+
modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
|
546
|
+
ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
|
547
|
+
encoded_data = await self._encode_ble_data(ble_data)
|
548
|
+
return cmd_code, encoded_data
|
549
|
+
|
550
|
+
@staticmethod
|
551
|
+
async def _encode_ble_data(byte_list: list) -> str:
|
552
|
+
"""Encode a list of bytes to a URL encoded base64 string."""
|
553
|
+
byte_array = bytearray(byte_list)
|
554
|
+
b64_encoded = base64.b64encode(byte_array)
|
555
|
+
return urllib.parse.quote(b64_encoded)
|
556
|
+
|
557
|
+
async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
|
558
|
+
"""BLE command to a PetKit device."""
|
559
|
+
_LOGGER.info("Sending BLE command to fountain %s", fountain_id)
|
560
|
+
water_fountain = await self._get_fountain_instance(fountain_id)
|
561
|
+
|
562
|
+
if water_fountain.is_connected is False:
|
563
|
+
_LOGGER.error("BLE connection not established.")
|
564
|
+
return False
|
565
|
+
|
566
|
+
command = FOUNTAIN_COMMAND.get[command, None]
|
567
|
+
if command is None:
|
568
|
+
_LOGGER.error("Command not found.")
|
569
|
+
return False
|
570
|
+
|
571
|
+
cmd_code, cmd_data = await self.get_ble_cmd_data(
|
572
|
+
command, water_fountain.ble_counter
|
573
|
+
)
|
574
|
+
|
575
|
+
response = await self.req.request(
|
576
|
+
method=HTTPMethod.POST,
|
577
|
+
url=PetkitEndpoint.BLE_CONTROL_DEVICE,
|
578
|
+
data={
|
579
|
+
"bleId": water_fountain.id,
|
580
|
+
"cmd": cmd_code,
|
581
|
+
"data": cmd_data,
|
582
|
+
"mac": water_fountain.mac,
|
583
|
+
"type": 24,
|
584
|
+
},
|
585
|
+
headers=await self.get_session_id(),
|
586
|
+
)
|
587
|
+
if response != 1:
|
588
|
+
_LOGGER.error("Failed to send BLE command.")
|
589
|
+
return False
|
590
|
+
_LOGGER.info("BLE command sent successfully.")
|
591
|
+
return True
|
592
|
+
|
397
593
|
async def send_api_request(
|
398
594
|
self,
|
399
595
|
device_id: int,
|
@@ -403,21 +599,21 @@ class PetKitClient:
|
|
403
599
|
"""Control the device using the PetKit API."""
|
404
600
|
await self.validate_session()
|
405
601
|
|
406
|
-
device = self.petkit_entities.get(device_id)
|
602
|
+
device = self.petkit_entities.get(device_id, None)
|
407
603
|
if not device:
|
408
604
|
raise PypetkitError(f"Device with ID {device_id} not found.")
|
409
605
|
|
410
606
|
_LOGGER.debug(
|
411
607
|
"Control API device=%s id=%s action=%s param=%s",
|
412
|
-
device.device_type,
|
608
|
+
device.device_nfo.device_type,
|
413
609
|
device_id,
|
414
610
|
action,
|
415
611
|
setting,
|
416
612
|
)
|
417
613
|
|
418
614
|
# Check if the device type is supported
|
419
|
-
if device.device_type:
|
420
|
-
device_type = device.device_type
|
615
|
+
if device.device_nfo.device_type:
|
616
|
+
device_type = device.device_nfo.device_type
|
421
617
|
else:
|
422
618
|
raise PypetkitError(
|
423
619
|
"Device type is not available, and is mandatory for sending commands."
|
@@ -442,7 +638,7 @@ class PetKitClient:
|
|
442
638
|
else:
|
443
639
|
endpoint = action_info.endpoint
|
444
640
|
_LOGGER.debug("Endpoint field")
|
445
|
-
url = f"{device.device_type
|
641
|
+
url = f"{device.device_nfo.device_type}/{endpoint}"
|
446
642
|
|
447
643
|
# Get the parameters
|
448
644
|
if setting is not None:
|
@@ -16,6 +16,8 @@ from pypetkitapi.const import (
|
|
16
16
|
DEVICES_FEEDER,
|
17
17
|
FEEDER,
|
18
18
|
FEEDER_MINI,
|
19
|
+
K2,
|
20
|
+
K3,
|
19
21
|
T3,
|
20
22
|
T4,
|
21
23
|
T5,
|
@@ -27,11 +29,19 @@ from pypetkitapi.const import (
|
|
27
29
|
class DeviceCommand(StrEnum):
|
28
30
|
"""Device Command"""
|
29
31
|
|
32
|
+
POWER = "power_device"
|
33
|
+
CONTROL_DEVICE = "control_device"
|
30
34
|
UPDATE_SETTING = "update_setting"
|
31
35
|
|
32
36
|
|
37
|
+
class FountainCommand(StrEnum):
|
38
|
+
"""Device Command"""
|
39
|
+
|
40
|
+
CONTROL_DEVICE = "control_device"
|
41
|
+
|
42
|
+
|
33
43
|
class FeederCommand(StrEnum):
|
34
|
-
"""Feeder Command"""
|
44
|
+
"""Specific Feeder Command"""
|
35
45
|
|
36
46
|
CALL_PET = "call_pet"
|
37
47
|
CALIBRATION = "food_reset"
|
@@ -45,25 +55,17 @@ class FeederCommand(StrEnum):
|
|
45
55
|
|
46
56
|
|
47
57
|
class LitterCommand(StrEnum):
|
48
|
-
"""LitterCommand"""
|
58
|
+
"""Specific LitterCommand"""
|
49
59
|
|
50
|
-
POWER = "power"
|
51
|
-
CONTROL_DEVICE = "control_device"
|
52
60
|
RESET_DEODORIZER = "reset_deodorizer"
|
53
61
|
|
54
62
|
|
55
63
|
class PetCommand(StrEnum):
|
56
|
-
"""PetCommand"""
|
64
|
+
"""Specific PetCommand"""
|
57
65
|
|
58
66
|
PET_UPDATE_SETTING = "pet_update_setting"
|
59
67
|
|
60
68
|
|
61
|
-
class FountainCommand(StrEnum):
|
62
|
-
"""Fountain Command"""
|
63
|
-
|
64
|
-
CONTROL_DEVICE = "control_device"
|
65
|
-
|
66
|
-
|
67
69
|
class LBCommand(IntEnum):
|
68
70
|
"""LitterBoxCommand"""
|
69
71
|
|
@@ -79,29 +81,43 @@ class LBCommand(IntEnum):
|
|
79
81
|
MAINTENANCE = 9
|
80
82
|
|
81
83
|
|
82
|
-
class
|
83
|
-
"""
|
84
|
+
class PurMode(IntEnum):
|
85
|
+
"""Purifier working mode"""
|
86
|
+
|
87
|
+
AUTO_MODE = 0
|
88
|
+
SILENT_MODE = 1
|
89
|
+
STANDARD_MODE = 2
|
90
|
+
STRONG_MODE = 3
|
91
|
+
|
92
|
+
|
93
|
+
class DeviceAction(StrEnum):
|
94
|
+
"""Device action for LitterBox and Purifier"""
|
84
95
|
|
96
|
+
# LitterBox only
|
85
97
|
CONTINUE = "continue_action"
|
86
98
|
END = "end_action"
|
87
|
-
POWER = "power_action"
|
88
99
|
START = "start_action"
|
89
100
|
STOP = "stop_action"
|
101
|
+
# Purifier only
|
102
|
+
MODE = "mode_action"
|
103
|
+
# All devices
|
104
|
+
POWER = "power_action"
|
90
105
|
|
91
106
|
|
92
107
|
class FountainAction(StrEnum):
|
93
108
|
"""Fountain Action"""
|
94
109
|
|
110
|
+
MODE_NORMAL = "Normal"
|
111
|
+
MODE_SMART = "Smart"
|
112
|
+
MODE_STANDARD = "Standard"
|
113
|
+
MODE_INTERMITTENT = "Intermittent"
|
95
114
|
PAUSE = "Pause"
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
SMART = "Smart"
|
115
|
+
CONTINUE = "Continue"
|
116
|
+
POWER_OFF = "Power Off"
|
117
|
+
POWER_ON = "Power On"
|
100
118
|
RESET_FILTER = "Reset Filter"
|
101
119
|
DO_NOT_DISTURB = "Do Not Disturb"
|
102
120
|
DO_NOT_DISTURB_OFF = "Do Not Disturb Off"
|
103
|
-
FIRST_BLE_CMND = "First BLE Command"
|
104
|
-
SECOND_BLE_CMND = "Second BLE Command"
|
105
121
|
LIGHT_LOW = "Light Low"
|
106
122
|
LIGHT_MEDIUM = "Light Medium"
|
107
123
|
LIGHT_HIGH = "Light High"
|
@@ -109,20 +125,12 @@ class FountainAction(StrEnum):
|
|
109
125
|
LIGHT_OFF = "Light Off"
|
110
126
|
|
111
127
|
|
112
|
-
|
113
|
-
FountainAction.
|
114
|
-
FountainAction.
|
115
|
-
FountainAction.
|
116
|
-
FountainAction.
|
117
|
-
FountainAction.
|
118
|
-
FountainAction.LIGHT_MEDIUM: "221",
|
119
|
-
FountainAction.LIGHT_HIGH: "221",
|
120
|
-
FountainAction.PAUSE: "220",
|
121
|
-
FountainAction.RESET_FILTER: "222",
|
122
|
-
FountainAction.NORMAL: "220",
|
123
|
-
FountainAction.NORMAL_TO_PAUSE: "220",
|
124
|
-
FountainAction.SMART: "220",
|
125
|
-
FountainAction.SMART_TO_PAUSE: "220",
|
128
|
+
FOUNTAIN_COMMAND = {
|
129
|
+
FountainAction.PAUSE: [220, 1, 3, 0, 1, 0, 2],
|
130
|
+
FountainAction.CONTINUE: [220, 1, 3, 0, 1, 1, 2],
|
131
|
+
FountainAction.RESET_FILTER: [222, 1, 0, 0],
|
132
|
+
FountainAction.POWER_OFF: [220, 1, 3, 0, 0, 1, 1],
|
133
|
+
FountainAction.POWER_ON: [220, 1, 3, 0, 1, 1, 1],
|
126
134
|
}
|
127
135
|
|
128
136
|
|
@@ -137,18 +145,18 @@ class CmdData:
|
|
137
145
|
|
138
146
|
def get_endpoint_manual_feed(device):
|
139
147
|
"""Get the endpoint for the device"""
|
140
|
-
if device.device_type == FEEDER_MINI:
|
148
|
+
if device.device_nfo.device_type == FEEDER_MINI:
|
141
149
|
return PetkitEndpoint.MANUAL_FEED_MINI
|
142
|
-
if device.device_type == FEEDER:
|
150
|
+
if device.device_nfo.device_type == FEEDER:
|
143
151
|
return PetkitEndpoint.MANUAL_FEED_FRESH_ELEMENT
|
144
152
|
return PetkitEndpoint.MANUAL_FEED_DUAL
|
145
153
|
|
146
154
|
|
147
155
|
def get_endpoint_reset_desiccant(device):
|
148
156
|
"""Get the endpoint for the device"""
|
149
|
-
if device.device_type == FEEDER_MINI:
|
157
|
+
if device.device_nfo.device_type == FEEDER_MINI:
|
150
158
|
return PetkitEndpoint.MINI_DESICCANT_RESET
|
151
|
-
if device.device_type == FEEDER:
|
159
|
+
if device.device_nfo.device_type == FEEDER:
|
152
160
|
return PetkitEndpoint.FRESH_ELEMENT_DESICCANT_RESET
|
153
161
|
return PetkitEndpoint.DESICCANT_RESET
|
154
162
|
|
@@ -162,6 +170,15 @@ ACTIONS_MAP = {
|
|
162
170
|
},
|
163
171
|
supported_device=ALL_DEVICES,
|
164
172
|
),
|
173
|
+
DeviceCommand.CONTROL_DEVICE: CmdData(
|
174
|
+
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
175
|
+
params=lambda device, command: {
|
176
|
+
"id": device.id,
|
177
|
+
"kv": json.dumps(command),
|
178
|
+
"type": list(command.keys())[0].split("_")[0],
|
179
|
+
},
|
180
|
+
supported_device=[K2, K3, T3, T4, T5, T6],
|
181
|
+
),
|
165
182
|
FeederCommand.REMOVE_DAILY_FEED: CmdData(
|
166
183
|
endpoint=PetkitEndpoint.REMOVE_DAILY_FEED,
|
167
184
|
params=lambda device, setting: {
|
@@ -204,7 +221,7 @@ ACTIONS_MAP = {
|
|
204
221
|
FeederCommand.CANCEL_MANUAL_FEED: CmdData(
|
205
222
|
endpoint=lambda device: (
|
206
223
|
PetkitEndpoint.FRESH_ELEMENT_CANCEL_FEED
|
207
|
-
if device.device_type == FEEDER
|
224
|
+
if device.device_nfo.device_type == FEEDER
|
208
225
|
else PetkitEndpoint.CANCEL_FEED
|
209
226
|
),
|
210
227
|
params=lambda device: {
|
@@ -212,7 +229,7 @@ ACTIONS_MAP = {
|
|
212
229
|
"deviceId": device.id,
|
213
230
|
**(
|
214
231
|
{"id": device.manual_feed_id}
|
215
|
-
if device.device_type
|
232
|
+
if device.device_nfo.device_type in [D4H, D4S, D4SH]
|
216
233
|
else {}
|
217
234
|
),
|
218
235
|
},
|
@@ -255,15 +272,6 @@ ACTIONS_MAP = {
|
|
255
272
|
},
|
256
273
|
supported_device=[D3],
|
257
274
|
),
|
258
|
-
LitterCommand.CONTROL_DEVICE: CmdData(
|
259
|
-
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
260
|
-
params=lambda device, command: {
|
261
|
-
"id": device.id,
|
262
|
-
"kv": json.dumps(command),
|
263
|
-
"type": list(command.keys())[0].split("_")[0],
|
264
|
-
},
|
265
|
-
supported_device=[T3, T4, T5, T6],
|
266
|
-
),
|
267
275
|
PetCommand.PET_UPDATE_SETTING: CmdData(
|
268
276
|
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
269
277
|
params=lambda pet, setting: {
|
@@ -272,15 +280,4 @@ ACTIONS_MAP = {
|
|
272
280
|
},
|
273
281
|
supported_device=ALL_DEVICES,
|
274
282
|
),
|
275
|
-
# FountainCommand.CONTROL_DEVICE: CmdData(
|
276
|
-
# endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
277
|
-
# params=lambda device, setting: {
|
278
|
-
# "bleId": device.id,
|
279
|
-
# "cmd": cmnd_code,
|
280
|
-
# "data": ble_data,
|
281
|
-
# "mac": device.mac,
|
282
|
-
# "type": water_fountain.ble_relay,
|
283
|
-
# },
|
284
|
-
# supported_device=[CTW3],
|
285
|
-
# ),
|
286
283
|
}
|
@@ -2,9 +2,6 @@
|
|
2
2
|
|
3
3
|
from enum import StrEnum
|
4
4
|
|
5
|
-
MIN_FEED_AMOUNT = 0
|
6
|
-
MAX_FEED_AMOUNT = 10
|
7
|
-
|
8
5
|
RES_KEY = "result"
|
9
6
|
ERR_KEY = "error"
|
10
7
|
SUCCESS_KEY = "success"
|
@@ -14,6 +11,11 @@ DEVICE_DATA = "deviceData"
|
|
14
11
|
DEVICE_STATS = "deviceStats"
|
15
12
|
PET_DATA = "petData"
|
16
13
|
|
14
|
+
# Bluetooth
|
15
|
+
BLE_CONNECT_ATTEMPT = 4
|
16
|
+
BLE_START_TRAME = [250, 252, 253]
|
17
|
+
BLE_END_TRAME = [251]
|
18
|
+
|
17
19
|
# PetKit Models
|
18
20
|
FEEDER = "feeder"
|
19
21
|
FEEDER_MINI = "feedermini"
|
@@ -30,11 +32,18 @@ W5 = "w5"
|
|
30
32
|
CTW3 = "ctw3"
|
31
33
|
K2 = "k2"
|
32
34
|
K3 = "k3"
|
35
|
+
PET = "pet"
|
33
36
|
|
34
37
|
DEVICES_LITTER_BOX = [T3, T4, T5, T6]
|
35
38
|
DEVICES_FEEDER = [FEEDER, FEEDER_MINI, D3, D4, D4S, D4H, D4SH]
|
36
39
|
DEVICES_WATER_FOUNTAIN = [W5, CTW3]
|
37
|
-
|
40
|
+
DEVICES_PURIFIER = [K2]
|
41
|
+
ALL_DEVICES = [
|
42
|
+
*DEVICES_LITTER_BOX,
|
43
|
+
*DEVICES_FEEDER,
|
44
|
+
*DEVICES_WATER_FOUNTAIN,
|
45
|
+
*DEVICES_PURIFIER,
|
46
|
+
]
|
38
47
|
|
39
48
|
|
40
49
|
class PetkitDomain(StrEnum):
|
@@ -116,11 +125,12 @@ class PetkitEndpoint(StrEnum):
|
|
116
125
|
GET_DEVICE_RECORD_RELEASE = "getDeviceRecordRelease"
|
117
126
|
UPDATE_SETTING = "updateSettings"
|
118
127
|
|
119
|
-
# Bluetooth
|
128
|
+
# Bluetooth
|
120
129
|
BLE_AS_RELAY = "ble/ownSupportBleDevices"
|
121
130
|
BLE_CONNECT = "ble/connect"
|
122
131
|
BLE_POLL = "ble/poll"
|
123
132
|
BLE_CANCEL = "ble/cancel"
|
133
|
+
BLE_CONTROL_DEVICE = "ble/controlDevice"
|
124
134
|
|
125
135
|
# Fountain & Litter Box
|
126
136
|
CONTROL_DEVICE = "controlDevice"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Dataclasses container for petkit API."""
|
2
2
|
|
3
|
-
from pydantic import BaseModel, Field
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
4
4
|
|
5
5
|
|
6
6
|
class RegionInfo(BaseModel):
|
@@ -43,6 +43,7 @@ class SessionInfo(BaseModel):
|
|
43
43
|
expires_in: int = Field(alias="expiresIn")
|
44
44
|
region: str | None = None
|
45
45
|
created_at: str = Field(alias="createdAt")
|
46
|
+
refreshed_at: str | None = None
|
46
47
|
|
47
48
|
|
48
49
|
class Device(BaseModel):
|
@@ -56,24 +57,31 @@ class Device(BaseModel):
|
|
56
57
|
device_type: str = Field(alias="deviceType")
|
57
58
|
group_id: int = Field(alias="groupId")
|
58
59
|
type: int
|
59
|
-
type_code: int = Field(alias="typeCode")
|
60
|
+
type_code: int = Field(0, alias="typeCode")
|
60
61
|
unique_id: str = Field(alias="uniqueId")
|
61
62
|
|
63
|
+
@field_validator("device_name", "device_type", "unique_id", mode="before")
|
64
|
+
def convert_to_lower(cls, value): # noqa: N805
|
65
|
+
"""Convert device_name, device_type and unique_id to lowercase."""
|
66
|
+
if value is not None and isinstance(value, str):
|
67
|
+
return value.lower()
|
68
|
+
return value
|
69
|
+
|
62
70
|
|
63
71
|
class Pet(BaseModel):
|
64
72
|
"""Dataclass for pet data.
|
65
73
|
Subclass of AccountData.
|
66
74
|
"""
|
67
75
|
|
68
|
-
avatar: str
|
76
|
+
avatar: str
|
69
77
|
created_at: int = Field(alias="createdAt")
|
70
78
|
pet_id: int = Field(alias="petId")
|
71
|
-
pet_name: str
|
79
|
+
pet_name: str = Field(alias="petName")
|
72
80
|
id: int | None = None # Fictive field copied from id (for HA compatibility)
|
73
81
|
sn: str | None = None # Fictive field copied from id (for HA compatibility)
|
74
82
|
name: str | None = None # Fictive field copied from pet_name (for HA compatibility)
|
75
|
-
device_type: str = "pet" # Fictive fixed field (for HA compatibility)
|
76
83
|
firmware: str | None = None # Fictive fixed field (for HA compatibility)
|
84
|
+
device_nfo: Device | None = None # Device is now optional
|
77
85
|
|
78
86
|
# Litter stats
|
79
87
|
last_litter_usage: int = 0
|
@@ -85,9 +93,7 @@ class Pet(BaseModel):
|
|
85
93
|
"""Initialize the Pet dataclass.
|
86
94
|
This method is used to fill the fictive fields after the standard initialization.
|
87
95
|
"""
|
88
|
-
# Appeler l'initialisation standard de Pydantic
|
89
96
|
super().__init__(**data)
|
90
|
-
# Remplir les champs fictifs après l'initialisation
|
91
97
|
self.id = self.id or self.pet_id
|
92
98
|
self.sn = self.sn or str(self.id)
|
93
99
|
self.name = self.name or self.pet_name
|
@@ -267,6 +267,7 @@ class FeederRecord(BaseModel):
|
|
267
267
|
move: list[RecordsType] | None = None
|
268
268
|
pet: list[RecordsType] | None = None
|
269
269
|
device_type: str | None = Field(None, alias="deviceType")
|
270
|
+
type_code: int = Field(0, alias="typeCode")
|
270
271
|
|
271
272
|
@classmethod
|
272
273
|
def get_endpoint(cls, device_type: str) -> str | None:
|
@@ -290,7 +291,7 @@ class FeederRecord(BaseModel):
|
|
290
291
|
if request_date is None:
|
291
292
|
request_date = datetime.now().strftime("%Y%m%d")
|
292
293
|
|
293
|
-
if device.device_type
|
294
|
+
if device.device_type == D4:
|
294
295
|
return {"date": request_date, "type": 0, "deviceId": device.device_id}
|
295
296
|
return {"days": request_date, "deviceId": device.device_id}
|
296
297
|
|
@@ -305,7 +306,6 @@ class Feeder(BaseModel):
|
|
305
306
|
cloud_product: CloudProduct | None = Field(None, alias="cloudProduct")
|
306
307
|
created_at: str | None = Field(None, alias="createdAt")
|
307
308
|
device_records: FeederRecord | None = None
|
308
|
-
device_type: str | None = Field(None, alias="deviceType")
|
309
309
|
firmware: float
|
310
310
|
firmware_details: list[FirmwareDetail] | None = Field(None, alias="firmwareDetails")
|
311
311
|
hardware: int
|
@@ -326,6 +326,7 @@ class Feeder(BaseModel):
|
|
326
326
|
sn: str
|
327
327
|
state: StateFeeder | None = None
|
328
328
|
timezone: float | None = None
|
329
|
+
device_nfo: Device | None = None
|
329
330
|
|
330
331
|
@classmethod
|
331
332
|
def get_endpoint(cls, device_type: str) -> str:
|
@@ -235,7 +235,6 @@ class LitterRecord(BaseModel):
|
|
235
235
|
toilet_detection: int | None = Field(None, alias="toiletDetection")
|
236
236
|
upload: int | None = None
|
237
237
|
user_id: str | None = Field(None, alias="userId")
|
238
|
-
device_type: str | None = Field(None, alias="deviceType")
|
239
238
|
|
240
239
|
@classmethod
|
241
240
|
def get_endpoint(cls, device_type: str) -> str:
|
@@ -254,7 +253,7 @@ class LitterRecord(BaseModel):
|
|
254
253
|
request_date: str | None = None,
|
255
254
|
) -> dict:
|
256
255
|
"""Generate query parameters including request_date."""
|
257
|
-
device_type = device.device_type
|
256
|
+
device_type = device.device_type
|
258
257
|
if device_type == T4:
|
259
258
|
if request_date is None:
|
260
259
|
request_date = datetime.now().strftime("%Y%m%d")
|
@@ -295,7 +294,6 @@ class LitterStats(BaseModel):
|
|
295
294
|
statistic_time: str | None = Field(None, alias="statisticTime")
|
296
295
|
times: int | None = None
|
297
296
|
total_time: int | None = Field(None, alias="totalTime")
|
298
|
-
device_type: str | None = None
|
299
297
|
|
300
298
|
@classmethod
|
301
299
|
def get_endpoint(cls, device_type: str) -> str:
|
@@ -359,7 +357,6 @@ class PetOutGraph(BaseModel):
|
|
359
357
|
storage_space: int | None = Field(None, alias="storageSpace")
|
360
358
|
time: int | None = None
|
361
359
|
toilet_time: int | None = Field(None, alias="toiletTime")
|
362
|
-
device_type: str | None = None
|
363
360
|
|
364
361
|
@classmethod
|
365
362
|
def get_endpoint(cls, device_type: str) -> str:
|
@@ -382,6 +379,31 @@ class PetOutGraph(BaseModel):
|
|
382
379
|
}
|
383
380
|
|
384
381
|
|
382
|
+
class K3Device(BaseModel):
|
383
|
+
"""Dataclass for K3 device data."""
|
384
|
+
|
385
|
+
battery: int | None = None
|
386
|
+
created_at: datetime | None = Field(None, alias="createdAt")
|
387
|
+
firmware: int | None = None
|
388
|
+
hardware: int | None = None
|
389
|
+
id: int | None = None
|
390
|
+
lighting: int | None = None
|
391
|
+
liquid: int | None = None
|
392
|
+
liquid_lack: int | None = Field(None, alias="liquidLack")
|
393
|
+
mac: str | None = None
|
394
|
+
name: str | None = None
|
395
|
+
refreshing: int | None = None
|
396
|
+
relate_t4: int | None = Field(None, alias="relateT4")
|
397
|
+
relation: dict | None = None
|
398
|
+
secret: str | None = None
|
399
|
+
settings: dict | None = None
|
400
|
+
sn: str | None = None
|
401
|
+
timezone: float | None = None
|
402
|
+
update_at: datetime | None = Field(None, alias="updateAt")
|
403
|
+
user_id: str | None = Field(None, alias="userId")
|
404
|
+
voltage: int | None = None
|
405
|
+
|
406
|
+
|
385
407
|
class Litter(BaseModel):
|
386
408
|
"""Dataclass for Litter Data.
|
387
409
|
Supported devices = T3, T4, T6
|
@@ -396,6 +418,7 @@ class Litter(BaseModel):
|
|
396
418
|
firmware_details: list[FirmwareDetail] = Field(alias="firmwareDetails")
|
397
419
|
hardware: int
|
398
420
|
id: int
|
421
|
+
k3_device: K3Device | None = Field(None, alias="k3Device")
|
399
422
|
is_pet_out_tips: int | None = Field(None, alias="isPetOutTips")
|
400
423
|
locale: str | None = None
|
401
424
|
mac: str | None = None
|
@@ -422,10 +445,10 @@ class Litter(BaseModel):
|
|
422
445
|
service_status: int | None = Field(None, alias="serviceStatus")
|
423
446
|
total_time: int | None = Field(None, alias="totalTime")
|
424
447
|
with_k3: int | None = Field(None, alias="withK3")
|
425
|
-
device_type: str | None = Field(None, alias="deviceType")
|
426
448
|
device_records: list[LitterRecord] | None = None
|
427
449
|
device_stats: LitterStats | None = None
|
428
450
|
device_pet_graph_out: list[PetOutGraph] | None = None
|
451
|
+
device_nfo: Device | None = None
|
429
452
|
|
430
453
|
@classmethod
|
431
454
|
def get_endpoint(cls, device_type: str) -> str:
|
@@ -0,0 +1,77 @@
|
|
1
|
+
"""Dataclasses for feeder data."""
|
2
|
+
|
3
|
+
from typing import Any, ClassVar
|
4
|
+
|
5
|
+
from pydantic import BaseModel, Field
|
6
|
+
|
7
|
+
from pypetkitapi.const import DEVICE_DATA, PetkitEndpoint
|
8
|
+
from pypetkitapi.containers import Device, FirmwareDetail, Wifi
|
9
|
+
|
10
|
+
|
11
|
+
class Settings(BaseModel):
|
12
|
+
"""Dataclass for settings data."""
|
13
|
+
|
14
|
+
auto_work: int | None = Field(None, alias="autoWork")
|
15
|
+
lack_notify: int | None = Field(None, alias="lackNotify")
|
16
|
+
light_mode: int | None = Field(None, alias="lightMode")
|
17
|
+
light_range: list[int] | None = Field(None, alias="lightRange")
|
18
|
+
log_notify: int | None = Field(None, alias="logNotify")
|
19
|
+
manual_lock: int | None = Field(None, alias="manualLock")
|
20
|
+
sound: int | None = None
|
21
|
+
temp_unit: int | None = Field(None, alias="tempUnit")
|
22
|
+
|
23
|
+
|
24
|
+
class State(BaseModel):
|
25
|
+
"""Dataclass for state data."""
|
26
|
+
|
27
|
+
humidity: int | None = None
|
28
|
+
left_day: int | None = Field(None, alias="leftDay")
|
29
|
+
liquid: int | None = None
|
30
|
+
mode: int | None = None
|
31
|
+
ota: int | None = None
|
32
|
+
overall: int | None = None
|
33
|
+
pim: int | None = None
|
34
|
+
power: int | None = None
|
35
|
+
refresh: float | None = None
|
36
|
+
temp: int | None = None
|
37
|
+
wifi: Wifi | None = None
|
38
|
+
|
39
|
+
|
40
|
+
class Purifier(BaseModel):
|
41
|
+
"""Dataclass for feeder data."""
|
42
|
+
|
43
|
+
data_type: ClassVar[str] = DEVICE_DATA
|
44
|
+
|
45
|
+
bt_mac: str | None = Field(None, alias="btMac")
|
46
|
+
created_at: str | None = Field(None, alias="createdAt")
|
47
|
+
firmware: str | None = None
|
48
|
+
firmware_details: list[FirmwareDetail] | None = Field(None, alias="firmwareDetails")
|
49
|
+
hardware: int | None = None
|
50
|
+
id: int | None = None
|
51
|
+
locale: str | None = None
|
52
|
+
mac: str | None = None
|
53
|
+
name: str | None = None
|
54
|
+
relation: dict[str, str]
|
55
|
+
secret: str | None = None
|
56
|
+
settings: Settings | None = None
|
57
|
+
share_open: int | None = Field(None, alias="shareOpen")
|
58
|
+
signup_at: str | None = Field(None, alias="signupAt")
|
59
|
+
sn: str | None = None
|
60
|
+
state: State | None = None
|
61
|
+
timezone: float | None = None
|
62
|
+
work_time: list[tuple[int, int]] | None = Field(None, alias="workTime")
|
63
|
+
device_nfo: Device | None = None
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
def get_endpoint(cls, device_type: str) -> str:
|
67
|
+
"""Get the endpoint URL for the given device type."""
|
68
|
+
return PetkitEndpoint.DEVICE_DETAIL
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
def query_param(
|
72
|
+
cls,
|
73
|
+
device: Device,
|
74
|
+
device_data: Any | None = None,
|
75
|
+
) -> dict:
|
76
|
+
"""Generate query parameters including request_date."""
|
77
|
+
return {"id": int(device.device_id)}
|
@@ -93,7 +93,6 @@ class WaterFountainRecord(BaseModel):
|
|
93
93
|
day_time: int | None = Field(None, alias="dayTime")
|
94
94
|
stay_time: int | None = Field(None, alias="stayTime")
|
95
95
|
work_time: int | None = Field(None, alias="workTime")
|
96
|
-
device_type: str | None = Field(None, alias="deviceType")
|
97
96
|
|
98
97
|
@classmethod
|
99
98
|
def get_endpoint(cls, device_type: str) -> str | None:
|
@@ -163,8 +162,11 @@ class WaterFountain(BaseModel):
|
|
163
162
|
update_at: str | None = Field(None, alias="updateAt")
|
164
163
|
user_id: str | None = Field(None, alias="userId")
|
165
164
|
water_pump_run_time: int | None = Field(None, alias="waterPumpRunTime")
|
166
|
-
device_type: str | None = Field(None, alias="deviceType")
|
167
165
|
device_records: list[WaterFountainRecord] | None = None
|
166
|
+
device_nfo: Device | None = None
|
167
|
+
is_connected: bool = False
|
168
|
+
ble_counter: int = 0
|
169
|
+
last_ble_poll: str | None = None
|
168
170
|
|
169
171
|
@classmethod
|
170
172
|
def get_endpoint(cls, device_type: str) -> str:
|
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
|
|
187
187
|
|
188
188
|
[tool.poetry]
|
189
189
|
name = "pypetkitapi"
|
190
|
-
version = "1.
|
190
|
+
version = "1.9.0"
|
191
191
|
description = "Python client for PetKit API"
|
192
192
|
authors = ["Jezza34000 <info@mail.com>"]
|
193
193
|
readme = "README.md"
|
@@ -208,7 +208,7 @@ ruff = "^0.8.1"
|
|
208
208
|
types-aiofiles = "^24.1.0.20240626"
|
209
209
|
|
210
210
|
[tool.bumpver]
|
211
|
-
current_version = "1.
|
211
|
+
current_version = "1.9.0"
|
212
212
|
version_pattern = "MAJOR.MINOR.PATCH"
|
213
213
|
commit_message = "bump version {old_version} -> {new_version}"
|
214
214
|
tag_message = "{new_version}"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|