pypetkitapi 0.5.4__tar.gz → 1.1.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.5.4 → pypetkitapi-1.1.0}/PKG-INFO +41 -33
- pypetkitapi-1.1.0/README.md +97 -0
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/client.py +184 -139
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/command.py +2 -11
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/const.py +12 -4
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/containers.py +3 -2
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/feeder_container.py +49 -8
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/litter_container.py +65 -6
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/water_fountain_container.py +42 -5
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pyproject.toml +2 -2
- pypetkitapi-0.5.4/README.md +0 -89
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/LICENSE +0 -0
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/__init__.py +0 -0
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/exceptions.py +0 -0
- {pypetkitapi-0.5.4 → pypetkitapi-1.1.0}/pypetkitapi/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pypetkitapi
|
3
|
-
Version:
|
3
|
+
Version: 1.1.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,97 @@
|
|
1
|
+
# Petkit API Client
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
[][pypi_]
|
6
|
+
[][python version] [](https://github.com/Jezza34000/py-petkit-api/actions)
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api) [](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
11
|
+
|
12
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
13
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
14
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
15
|
+
|
16
|
+
[][pre-commit]
|
17
|
+
[][black]
|
18
|
+
[](https://mypy.readthedocs.io/en/stable/)
|
19
|
+
[](https://github.com/astral-sh/ruff)
|
20
|
+
|
21
|
+
---
|
22
|
+
|
23
|
+
[pypi_]: https://pypi.org/project/pypetkitapi/
|
24
|
+
[python version]: https://pypi.org/project/pypetkitapi
|
25
|
+
[pre-commit]: https://github.com/pre-commit/pre-commit
|
26
|
+
[black]: https://github.com/psf/black
|
27
|
+
|
28
|
+
## Overview
|
29
|
+
|
30
|
+
PetKit Client is a Python library for interacting with the PetKit API. It allows you to manage your PetKit devices, retrieve account data, and control devices through the API.
|
31
|
+
|
32
|
+
## Features
|
33
|
+
|
34
|
+
Login and session management
|
35
|
+
Fetch account and device data
|
36
|
+
Control PetKit devices (Feeder, Litter Box, Water Fountain)
|
37
|
+
|
38
|
+
## Installation
|
39
|
+
|
40
|
+
Install the library using pip:
|
41
|
+
|
42
|
+
```bash
|
43
|
+
pip install pypetkitapi
|
44
|
+
```
|
45
|
+
|
46
|
+
## Usage Example:
|
47
|
+
|
48
|
+
```python
|
49
|
+
import aiohttp
|
50
|
+
from pypetkitapi.client import PetKitClient
|
51
|
+
from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
|
52
|
+
|
53
|
+
logging.basicConfig(level=logging.DEBUG)
|
54
|
+
|
55
|
+
async def main():
|
56
|
+
async with aiohttp.ClientSession() as session:
|
57
|
+
client = PetKitClient(
|
58
|
+
username="username", # Your PetKit account username or id
|
59
|
+
password="password", # Your PetKit account password
|
60
|
+
region="FR", # Your region or country code (e.g. FR, US, etc.)
|
61
|
+
timezone="Europe/Paris", # Your timezone
|
62
|
+
session=session,
|
63
|
+
)
|
64
|
+
|
65
|
+
await client.get_devices_data()
|
66
|
+
|
67
|
+
# Read the account data
|
68
|
+
print(client.account_data)
|
69
|
+
|
70
|
+
# Read the devices data
|
71
|
+
print(client.petkit_entities)
|
72
|
+
|
73
|
+
# Send command to the devices
|
74
|
+
### Example 1 : Turn on the indicator light
|
75
|
+
### Device_ID, Command, Payload
|
76
|
+
await client.send_api_request(123456789, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
|
77
|
+
|
78
|
+
### Example 2 : Feed the pet
|
79
|
+
### Device_ID, Command, Payload
|
80
|
+
await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
|
81
|
+
|
82
|
+
### Example 3 : Start the cleaning process
|
83
|
+
### Device_ID, Command, Payload
|
84
|
+
await client.send_api_request(123456789, LitterCommand.CONTROL_DEVICE, {LBAction.START: LBCommand.CLEANING})
|
85
|
+
|
86
|
+
|
87
|
+
if __name__ == "__main__":
|
88
|
+
asyncio.run(main())
|
89
|
+
```
|
90
|
+
|
91
|
+
## Contributing
|
92
|
+
|
93
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
94
|
+
|
95
|
+
## License
|
96
|
+
|
97
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
@@ -12,18 +12,21 @@ 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,
|
18
20
|
ERR_KEY,
|
19
21
|
LOGIN_DATA,
|
22
|
+
PET_DATA,
|
20
23
|
RES_KEY,
|
21
24
|
SUCCESS_KEY,
|
22
25
|
Header,
|
26
|
+
PetkitDomain,
|
23
27
|
PetkitEndpoint,
|
24
|
-
PetkitURL,
|
25
28
|
)
|
26
|
-
from pypetkitapi.containers import AccountData, Device, RegionInfo, SessionInfo
|
29
|
+
from pypetkitapi.containers import AccountData, Device, Pet, RegionInfo, SessionInfo
|
27
30
|
from pypetkitapi.exceptions import (
|
28
31
|
PetkitAuthenticationError,
|
29
32
|
PetkitInvalidHTTPResponseCodeError,
|
@@ -32,9 +35,9 @@ from pypetkitapi.exceptions import (
|
|
32
35
|
PetkitTimeoutError,
|
33
36
|
PypetkitError,
|
34
37
|
)
|
35
|
-
from pypetkitapi.feeder_container import Feeder
|
36
|
-
from pypetkitapi.litter_container import Litter
|
37
|
-
from pypetkitapi.water_fountain_container import WaterFountain
|
38
|
+
from pypetkitapi.feeder_container import Feeder, FeederRecord
|
39
|
+
from pypetkitapi.litter_container import Litter, LitterRecord
|
40
|
+
from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
|
38
41
|
|
39
42
|
_LOGGER = logging.getLogger(__name__)
|
40
43
|
|
@@ -46,8 +49,11 @@ class PetKitClient:
|
|
46
49
|
_session: SessionInfo | None = None
|
47
50
|
_servers_list: list[RegionInfo] = []
|
48
51
|
account_data: list[AccountData] = []
|
49
|
-
|
50
|
-
|
52
|
+
petkit_entities: dict[
|
53
|
+
str,
|
54
|
+
dict[int, Feeder | Litter | WaterFountain | Pet]
|
55
|
+
| dict[int, FeederRecord | LitterRecord | WaterFountainRecord],
|
56
|
+
]
|
51
57
|
|
52
58
|
def __init__(
|
53
59
|
self,
|
@@ -55,85 +61,50 @@ class PetKitClient:
|
|
55
61
|
password: str,
|
56
62
|
region: str,
|
57
63
|
timezone: str,
|
64
|
+
session: aiohttp.ClientSession | None = None,
|
58
65
|
) -> None:
|
59
66
|
"""Initialize the PetKit Client."""
|
60
67
|
self.username = username
|
61
68
|
self.password = password
|
62
69
|
self.region = region.lower()
|
63
70
|
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(),
|
71
|
+
self._session = None
|
72
|
+
self.petkit_entities = {DEVICE_RECORDS: {}, DEVICE_DATA: {}, PET_DATA: {}}
|
73
|
+
self.aiohttp_session = session or aiohttp.ClientSession()
|
74
|
+
self.req = PrepReq(
|
75
|
+
base_url=PetkitDomain.PASSPORT_PETKIT, session=self.aiohttp_session
|
94
76
|
)
|
95
|
-
_LOGGER.debug("API server list: %s", response)
|
96
|
-
self._servers_list = [
|
97
|
-
RegionInfo(**region) for region in response.get("list", [])
|
98
|
-
]
|
99
77
|
|
100
78
|
async def _get_base_url(self) -> None:
|
101
|
-
"""
|
102
|
-
|
103
|
-
_LOGGER.debug("Finding region server for region: %s", self.region)
|
79
|
+
"""Get the list of API servers, filter by region, and return the matching server."""
|
80
|
+
_LOGGER.debug("Getting API server list")
|
104
81
|
|
105
|
-
|
106
|
-
|
107
|
-
self._base_url = PetkitURL.CHINA_SRV
|
82
|
+
if self.region.lower() == "china":
|
83
|
+
self._base_url = PetkitDomain.CHINA_SRV
|
108
84
|
return
|
109
85
|
|
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,
|
86
|
+
response = await self.req.request(
|
87
|
+
method=HTTPMethod.GET,
|
88
|
+
url=PetkitEndpoint.REGION_SERVERS,
|
118
89
|
)
|
90
|
+
_LOGGER.debug("API server list: %s", response)
|
119
91
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
)
|
124
|
-
|
125
|
-
|
92
|
+
# Filter the servers by region
|
93
|
+
for region in response.get("list", []):
|
94
|
+
server = RegionInfo(**region)
|
95
|
+
if server.name.lower() == self.region or server.id.lower() == self.region:
|
96
|
+
self.req.base_url = server.gateway
|
97
|
+
_LOGGER.debug("Found matching server: %s", server)
|
98
|
+
return
|
126
99
|
raise PetkitRegionalServerNotFoundError(self.region)
|
127
100
|
|
128
101
|
async def request_login_code(self) -> bool:
|
129
102
|
"""Request a login code to be sent to the user's email."""
|
130
103
|
_LOGGER.debug("Requesting login code for username: %s", self.username)
|
131
|
-
|
132
|
-
response = await prep_req.request(
|
104
|
+
response = await self.req.request(
|
133
105
|
method=HTTPMethod.GET,
|
134
106
|
url=PetkitEndpoint.GET_LOGIN_CODE,
|
135
107
|
params={"username": self.username},
|
136
|
-
headers=await self._generate_header(),
|
137
108
|
)
|
138
109
|
if response:
|
139
110
|
_LOGGER.info("Login code sent to user's email")
|
@@ -145,7 +116,7 @@ class PetKitClient:
|
|
145
116
|
# Retrieve the list of servers
|
146
117
|
await self._get_base_url()
|
147
118
|
|
148
|
-
_LOGGER.
|
119
|
+
_LOGGER.info("Logging in to PetKit server")
|
149
120
|
|
150
121
|
# Prepare the data to send
|
151
122
|
data = LOGIN_DATA.copy()
|
@@ -162,12 +133,10 @@ class PetKitClient:
|
|
162
133
|
data["password"] = pwd # noqa: S324
|
163
134
|
|
164
135
|
# Send the login request
|
165
|
-
|
166
|
-
response = await prep_req.request(
|
136
|
+
response = await self.req.request(
|
167
137
|
method=HTTPMethod.POST,
|
168
138
|
url=PetkitEndpoint.LOGIN,
|
169
139
|
data=data,
|
170
|
-
headers=await self._generate_header(),
|
171
140
|
)
|
172
141
|
session_data = response["session"]
|
173
142
|
self._session = SessionInfo(**session_data)
|
@@ -175,11 +144,9 @@ class PetKitClient:
|
|
175
144
|
async def refresh_session(self) -> None:
|
176
145
|
"""Refresh the session."""
|
177
146
|
_LOGGER.debug("Refreshing session")
|
178
|
-
|
179
|
-
response = await prep_req.request(
|
147
|
+
response = await self.req.request(
|
180
148
|
method=HTTPMethod.POST,
|
181
149
|
url=PetkitEndpoint.REFRESH_SESSION,
|
182
|
-
headers=await self._generate_header(),
|
183
150
|
)
|
184
151
|
session_data = response["session"]
|
185
152
|
self._session = SessionInfo(**session_data)
|
@@ -205,76 +172,137 @@ class PetKitClient:
|
|
205
172
|
elif half_max_age < token_age <= max_age:
|
206
173
|
_LOGGER.debug("Token still OK, but refreshing session")
|
207
174
|
await self.refresh_session()
|
208
|
-
|
175
|
+
|
176
|
+
async def get_session_id(self) -> dict:
|
177
|
+
"""Return the session ID."""
|
178
|
+
if self._session is None:
|
179
|
+
raise PypetkitError("Session is not initialized.")
|
180
|
+
return {"F-Session": self._session.id, "X-Session": self._session.id}
|
209
181
|
|
210
182
|
async def _get_account_data(self) -> None:
|
211
183
|
"""Get the account data from the PetKit service."""
|
212
184
|
await self.validate_session()
|
213
185
|
_LOGGER.debug("Fetching account data")
|
214
|
-
|
215
|
-
response = await prep_req.request(
|
186
|
+
response = await self.req.request(
|
216
187
|
method=HTTPMethod.GET,
|
217
188
|
url=PetkitEndpoint.FAMILY_LIST,
|
218
|
-
headers=await self.
|
189
|
+
headers=await self.get_session_id(),
|
219
190
|
)
|
220
191
|
self.account_data = [AccountData(**account) for account in response]
|
221
192
|
|
193
|
+
# Add pets to device_list
|
194
|
+
for account in self.account_data:
|
195
|
+
if account.pet_list:
|
196
|
+
for pet in account.pet_list:
|
197
|
+
self.petkit_entities[PET_DATA][pet.pet_id] = pet
|
198
|
+
|
222
199
|
async def get_devices_data(self) -> None:
|
223
200
|
"""Get the devices data from the PetKit servers."""
|
224
201
|
start_time = datetime.now()
|
225
202
|
if not self.account_data:
|
226
203
|
await self._get_account_data()
|
227
204
|
|
205
|
+
tasks = []
|
228
206
|
device_list: list[Device] = []
|
207
|
+
|
229
208
|
for account in self.account_data:
|
230
209
|
_LOGGER.debug("Fetching devices data for account: %s", account)
|
231
210
|
if account.device_list:
|
232
211
|
device_list.extend(account.device_list)
|
233
212
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
213
|
+
_LOGGER.debug("Fetch %s devices for this account", len(device_list))
|
214
|
+
|
215
|
+
for device in device_list:
|
216
|
+
_LOGGER.debug("Fetching devices data: %s", device)
|
217
|
+
device_type = device.device_type.lower()
|
218
|
+
device_id = device.device_id
|
219
|
+
if device_type in DEVICES_FEEDER:
|
220
|
+
# Add tasks for feeders
|
221
|
+
tasks.append(self._fetch_device_data(account, device_id, Feeder))
|
222
|
+
tasks.append(
|
223
|
+
self._fetch_device_data(account, device_id, FeederRecord)
|
224
|
+
)
|
225
|
+
elif device_type in DEVICES_LITTER_BOX:
|
226
|
+
# Add tasks for litter boxes
|
227
|
+
tasks.append(self._fetch_device_data(account, device_id, Litter))
|
228
|
+
tasks.append(
|
229
|
+
self._fetch_device_data(account, device_id, LitterRecord)
|
230
|
+
)
|
231
|
+
elif device_type in DEVICES_WATER_FOUNTAIN:
|
232
|
+
# Add tasks for water fountains
|
233
|
+
tasks.append(
|
234
|
+
self._fetch_device_data(account, device_id, WaterFountain)
|
235
|
+
)
|
236
|
+
tasks.append(
|
237
|
+
self._fetch_device_data(account, device_id, WaterFountainRecord)
|
238
|
+
)
|
239
|
+
else:
|
240
|
+
_LOGGER.warning("Unknown device type: %s", device_type)
|
248
241
|
await asyncio.gather(*tasks)
|
249
242
|
|
250
243
|
end_time = datetime.now()
|
251
244
|
total_time = end_time - start_time
|
252
|
-
_LOGGER.
|
245
|
+
_LOGGER.info("OK Petkit data fetched in : %s", total_time)
|
253
246
|
|
254
247
|
async def _fetch_device_data(
|
255
248
|
self,
|
256
|
-
|
257
|
-
|
249
|
+
account: AccountData,
|
250
|
+
device_id: int,
|
251
|
+
data_class: type[
|
252
|
+
Feeder
|
253
|
+
| Litter
|
254
|
+
| WaterFountain
|
255
|
+
| FeederRecord
|
256
|
+
| LitterRecord
|
257
|
+
| WaterFountainRecord
|
258
|
+
],
|
258
259
|
) -> None:
|
259
260
|
"""Fetch the device data from the PetKit servers."""
|
260
261
|
await self.validate_session()
|
261
|
-
|
262
|
+
device = None
|
263
|
+
|
264
|
+
if account.device_list:
|
265
|
+
device = next(
|
266
|
+
(
|
267
|
+
device
|
268
|
+
for device in account.device_list
|
269
|
+
if device.device_id == device_id
|
270
|
+
),
|
271
|
+
None,
|
272
|
+
)
|
273
|
+
if device is None:
|
274
|
+
_LOGGER.error("Device not found: id=%s", device_id)
|
275
|
+
return
|
262
276
|
device_type = device.device_type.lower()
|
263
|
-
query_param = data_class.query_param(device.device_id)
|
264
277
|
|
265
|
-
|
266
|
-
|
267
|
-
|
278
|
+
endpoint = data_class.get_endpoint(device_type)
|
279
|
+
query_param = data_class.query_param(account, device.device_id)
|
280
|
+
|
281
|
+
response = await self.req.request(
|
282
|
+
method=HTTPMethod.POST,
|
268
283
|
url=f"{device_type}/{endpoint}",
|
269
284
|
params=query_param,
|
270
|
-
headers=await self.
|
285
|
+
headers=await self.get_session_id(),
|
271
286
|
)
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
)
|
277
|
-
|
287
|
+
|
288
|
+
# Check if the response is a list or a dict
|
289
|
+
if isinstance(response, list):
|
290
|
+
device_data = [data_class(**item) for item in response]
|
291
|
+
elif isinstance(response, dict):
|
292
|
+
device_data = data_class(**response)
|
293
|
+
else:
|
294
|
+
_LOGGER.error("Unexpected response type: %s", type(response))
|
295
|
+
return
|
296
|
+
|
297
|
+
if isinstance(device_data, list):
|
298
|
+
for item in device_data:
|
299
|
+
item.device_type = device_type
|
300
|
+
else:
|
301
|
+
device_data.device_type = device_type
|
302
|
+
|
303
|
+
_LOGGER.debug("Reading device type : %s (id=%s)", device_type, device_id)
|
304
|
+
|
305
|
+
self.petkit_entities[data_class.data_type][device_id] = device_data
|
278
306
|
|
279
307
|
async def send_api_request(
|
280
308
|
self,
|
@@ -283,7 +311,8 @@ class PetKitClient:
|
|
283
311
|
setting: dict | None = None,
|
284
312
|
) -> None:
|
285
313
|
"""Control the device using the PetKit API."""
|
286
|
-
|
314
|
+
device_dict = self.petkit_entities.get(DEVICE_DATA, {})
|
315
|
+
device = device_dict.get(device_id)
|
287
316
|
if not device:
|
288
317
|
raise PypetkitError(f"Device with ID {device_id} not found.")
|
289
318
|
|
@@ -295,42 +324,44 @@ class PetKitClient:
|
|
295
324
|
setting,
|
296
325
|
)
|
297
326
|
|
327
|
+
# Check if the device type is supported
|
298
328
|
if device.device_type:
|
299
329
|
device_type = device.device_type.lower()
|
300
330
|
else:
|
301
331
|
raise PypetkitError(
|
302
332
|
"Device type is not available, and is mandatory for sending commands."
|
303
333
|
)
|
304
|
-
|
334
|
+
# Check if the action is supported
|
305
335
|
if action not in ACTIONS_MAP:
|
306
336
|
raise PypetkitError(f"Action {action} not supported.")
|
307
337
|
|
308
338
|
action_info = ACTIONS_MAP[action]
|
339
|
+
_LOGGER.debug(action)
|
340
|
+
_LOGGER.debug(action_info)
|
309
341
|
if device_type not in action_info.supported_device:
|
310
342
|
raise PypetkitError(
|
311
343
|
f"Device type {device.device_type} not supported for action {action}."
|
312
344
|
)
|
313
|
-
|
345
|
+
# Get the endpoint
|
314
346
|
if callable(action_info.endpoint):
|
315
347
|
endpoint = action_info.endpoint(device)
|
348
|
+
_LOGGER.debug("Endpoint from callable")
|
316
349
|
else:
|
317
350
|
endpoint = action_info.endpoint
|
351
|
+
_LOGGER.debug("Endpoint field")
|
318
352
|
url = f"{device.device_type.lower()}/{endpoint}"
|
319
353
|
|
320
|
-
|
321
|
-
|
322
|
-
# Use the lambda to generate params
|
354
|
+
# Get the parameters
|
323
355
|
if setting is not None:
|
324
356
|
params = action_info.params(device, setting)
|
325
357
|
else:
|
326
358
|
params = action_info.params(device)
|
327
359
|
|
328
|
-
|
329
|
-
res = await prep_req.request(
|
360
|
+
res = await self.req.request(
|
330
361
|
method=HTTPMethod.POST,
|
331
362
|
url=url,
|
332
363
|
data=params,
|
333
|
-
headers=
|
364
|
+
headers=await self.get_session_id(),
|
334
365
|
)
|
335
366
|
if res in (SUCCESS_KEY, RES_KEY):
|
336
367
|
# TODO : Manage to get the response from manual feeding
|
@@ -338,14 +369,39 @@ class PetKitClient:
|
|
338
369
|
else:
|
339
370
|
_LOGGER.error("Command execution failed")
|
340
371
|
|
372
|
+
async def close(self) -> None:
|
373
|
+
"""Close the aiohttp session if it was created by the client."""
|
374
|
+
if self.aiohttp_session:
|
375
|
+
await self.aiohttp_session.close()
|
376
|
+
|
341
377
|
|
342
378
|
class PrepReq:
|
343
379
|
"""Prepare the request to the PetKit API."""
|
344
380
|
|
345
|
-
def __init__(self, base_url: str,
|
381
|
+
def __init__(self, base_url: str, session: aiohttp.ClientSession) -> None:
|
346
382
|
"""Initialize the request."""
|
347
383
|
self.base_url = base_url
|
348
|
-
self.
|
384
|
+
self.session = session
|
385
|
+
self.base_headers = self._generate_header()
|
386
|
+
|
387
|
+
@staticmethod
|
388
|
+
def _generate_header() -> dict[str, str]:
|
389
|
+
"""Create header for interaction with API endpoint."""
|
390
|
+
|
391
|
+
return {
|
392
|
+
"Accept": Header.ACCEPT.value,
|
393
|
+
"Accept-Language": Header.ACCEPT_LANG,
|
394
|
+
"Accept-Encoding": Header.ENCODING,
|
395
|
+
"Content-Type": Header.CONTENT_TYPE,
|
396
|
+
"User-Agent": Header.AGENT,
|
397
|
+
"X-Img-Version": Header.IMG_VERSION,
|
398
|
+
"X-Locale": Header.LOCALE,
|
399
|
+
"X-Client": Header.CLIENT,
|
400
|
+
"X-Hour": Header.HOUR,
|
401
|
+
"X-TimezoneId": Header.TIMEZONE_ID,
|
402
|
+
"X-Api-Version": Header.API_VERSION,
|
403
|
+
"X-Timezone": Header.TIMEZONE,
|
404
|
+
}
|
349
405
|
|
350
406
|
async def request(
|
351
407
|
self,
|
@@ -366,34 +422,21 @@ class PrepReq:
|
|
366
422
|
data,
|
367
423
|
_headers,
|
368
424
|
)
|
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
|
425
|
+
try:
|
426
|
+
async with self.session.request(
|
427
|
+
method,
|
428
|
+
_url,
|
429
|
+
params=params,
|
430
|
+
data=data,
|
431
|
+
headers=_headers,
|
432
|
+
) as resp:
|
433
|
+
return await self._handle_response(resp, _url)
|
434
|
+
except aiohttp.ClientConnectorError as e:
|
435
|
+
raise PetkitTimeoutError(f"Cannot connect to host: {e}") from e
|
392
436
|
|
393
437
|
@staticmethod
|
394
438
|
async def _handle_response(response: aiohttp.ClientResponse, url: str) -> dict:
|
395
439
|
"""Handle the response from the PetKit API."""
|
396
|
-
|
397
440
|
try:
|
398
441
|
response.raise_for_status()
|
399
442
|
except aiohttp.ClientResponseError as e:
|
@@ -408,6 +451,7 @@ class PrepReq:
|
|
408
451
|
"Response is not in JSON format"
|
409
452
|
) from None
|
410
453
|
|
454
|
+
# Check for errors in the response
|
411
455
|
if ERR_KEY in response_json:
|
412
456
|
error_msg = response_json[ERR_KEY].get("msg", "Unknown error")
|
413
457
|
if any(
|
@@ -421,6 +465,7 @@ class PrepReq:
|
|
421
465
|
raise PetkitAuthenticationError(f"Login failed: {error_msg}")
|
422
466
|
raise PypetkitError(f"Request failed: {error_msg}")
|
423
467
|
|
468
|
+
# Check for success in the response
|
424
469
|
if RES_KEY in response_json:
|
425
470
|
return response_json[RES_KEY]
|
426
471
|
|
@@ -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):
|
@@ -235,15 +235,6 @@ ACTIONS_MAP = {
|
|
235
235
|
},
|
236
236
|
supported_device=[D3],
|
237
237
|
),
|
238
|
-
LitterCommand.POWER: CmdData(
|
239
|
-
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
240
|
-
params=lambda device, setting: {
|
241
|
-
"id": device.id,
|
242
|
-
"kv": json.dumps(setting),
|
243
|
-
"type": "power",
|
244
|
-
},
|
245
|
-
supported_device=[T3, T4, T5, T6],
|
246
|
-
),
|
247
238
|
LitterCommand.CONTROL_DEVICE: CmdData(
|
248
239
|
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
249
240
|
params=lambda device, command: {
|
@@ -253,7 +244,7 @@ ACTIONS_MAP = {
|
|
253
244
|
},
|
254
245
|
supported_device=[T3, T4, T5, T6],
|
255
246
|
),
|
256
|
-
PetCommand.
|
247
|
+
PetCommand.PET_UPDATE_SETTING: CmdData(
|
257
248
|
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
258
249
|
params=lambda pet, setting: {
|
259
250
|
"petId": pet,
|
@@ -9,6 +9,10 @@ RES_KEY = "result"
|
|
9
9
|
ERR_KEY = "error"
|
10
10
|
SUCCESS_KEY = "success"
|
11
11
|
|
12
|
+
DEVICE_RECORDS = "deviceRecords"
|
13
|
+
DEVICE_DATA = "deviceData"
|
14
|
+
PET_DATA = "petData"
|
15
|
+
|
12
16
|
# PetKit Models
|
13
17
|
FEEDER = "feeder"
|
14
18
|
FEEDER_MINI = "feedermini"
|
@@ -31,10 +35,10 @@ DEVICES_WATER_FOUNTAIN = [W5, CTW3]
|
|
31
35
|
ALL_DEVICES = [*DEVICES_LITTER_BOX, *DEVICES_FEEDER, *DEVICES_WATER_FOUNTAIN]
|
32
36
|
|
33
37
|
|
34
|
-
class
|
38
|
+
class PetkitDomain(StrEnum):
|
35
39
|
"""Petkit URL constants"""
|
36
40
|
|
37
|
-
|
41
|
+
PASSPORT_PETKIT = "https://passport.petkt.com/"
|
38
42
|
CHINA_SRV = "https://api.petkit.cn/6/"
|
39
43
|
|
40
44
|
|
@@ -58,7 +62,7 @@ class Header(StrEnum):
|
|
58
62
|
AGENT = "okhttp/3.12.11"
|
59
63
|
CLIENT = f"{Client.PLATFORM_TYPE}({Client.OS_VERSION};{Client.MODEL_NAME})"
|
60
64
|
TIMEZONE = "1.0"
|
61
|
-
TIMEZONE_ID = "Europe/Paris" # TODO: Make this dynamic
|
65
|
+
TIMEZONE_ID = "Europe/Paris" # TODO: Make this dynamic
|
62
66
|
LOCALE = "en-US"
|
63
67
|
IMG_VERSION = "1.0"
|
64
68
|
HOUR = "24"
|
@@ -71,7 +75,7 @@ CLIENT_NFO = {
|
|
71
75
|
"platform": Client.PLATFORM_TYPE.value,
|
72
76
|
"source": Client.SOURCE.value,
|
73
77
|
"timezone": Header.TIMEZONE.value, # TODO: Make this dynamic
|
74
|
-
"timezoneId": Header.TIMEZONE_ID.value,
|
78
|
+
"timezoneId": Header.TIMEZONE_ID.value,
|
75
79
|
"version": Header.API_VERSION.value,
|
76
80
|
}
|
77
81
|
|
@@ -84,11 +88,14 @@ LOGIN_DATA = {
|
|
84
88
|
class PetkitEndpoint(StrEnum):
|
85
89
|
"""Petkit Endpoint constants"""
|
86
90
|
|
91
|
+
REGION_SERVERS = "v1/regionservers"
|
87
92
|
LOGIN = "user/login"
|
88
93
|
GET_LOGIN_CODE = "user/sendcodeforquicklogin"
|
89
94
|
REFRESH_SESSION = "user/refreshsession"
|
90
95
|
FAMILY_LIST = "group/family/list"
|
91
96
|
REFRESH_HOME_V2 = "refreshHomeV2"
|
97
|
+
|
98
|
+
# Common to many device
|
92
99
|
DEVICE_DETAIL = "device_detail"
|
93
100
|
DEVICE_DATA = "deviceData"
|
94
101
|
GET_DEVICE_RECORD = "getDeviceRecord"
|
@@ -103,6 +110,7 @@ class PetkitEndpoint(StrEnum):
|
|
103
110
|
|
104
111
|
# Fountain & Litter Box
|
105
112
|
CONTROL_DEVICE = "controlDevice"
|
113
|
+
GET_WORK_RECORD = "getWorkRecord"
|
106
114
|
|
107
115
|
# Litter Box
|
108
116
|
DEODORANT_RESET = "deodorantReset"
|
@@ -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):
|
@@ -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}
|
@@ -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
|
+
}
|
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
|
|
187
187
|
|
188
188
|
[tool.poetry]
|
189
189
|
name = "pypetkitapi"
|
190
|
-
version = "
|
190
|
+
version = "1.1.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 = "
|
207
|
+
current_version = "1.1.0"
|
208
208
|
version_pattern = "MAJOR.MINOR.PATCH"
|
209
209
|
commit_message = "bump version {old_version} -> {new_version}"
|
210
210
|
tag_message = "{new_version}"
|
pypetkitapi-0.5.4/README.md
DELETED
@@ -1,89 +0,0 @@
|
|
1
|
-
# Petkit API Client
|
2
|
-
|
3
|
-
---
|
4
|
-
|
5
|
-
[][pypi_]
|
6
|
-
[][python version]
|
7
|
-
|
8
|
-
[][pre-commit]
|
9
|
-
[][black]
|
10
|
-
[](https://mypy.readthedocs.io/en/stable/)
|
11
|
-
[](https://github.com/astral-sh/ruff)
|
12
|
-
[](https://github.com/Jezza34000/py-petkit-api/actions)
|
13
|
-
|
14
|
-
---
|
15
|
-
|
16
|
-
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
17
|
-
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
18
|
-
|
19
|
-
[pypi_]: https://pypi.org/project/pypetkitapi/
|
20
|
-
[python version]: https://pypi.org/project/pypetkitapi
|
21
|
-
[pre-commit]: https://github.com/pre-commit/pre-commit
|
22
|
-
[black]: https://github.com/psf/black
|
23
|
-
|
24
|
-
## Overview
|
25
|
-
|
26
|
-
PetKit Client is a Python library for interacting with the PetKit API. It allows you to manage your PetKit devices, retrieve account data, and control devices through the API.
|
27
|
-
|
28
|
-
## Features
|
29
|
-
|
30
|
-
Login and session management
|
31
|
-
Fetch account and device data
|
32
|
-
Control PetKit devices (Feeder, Litter Box, Water Fountain)
|
33
|
-
|
34
|
-
## Installation
|
35
|
-
|
36
|
-
Install the library using pip:
|
37
|
-
|
38
|
-
```bash
|
39
|
-
pip install pypetkitapi
|
40
|
-
```
|
41
|
-
|
42
|
-
## Usage Example:
|
43
|
-
|
44
|
-
```python
|
45
|
-
import asyncio
|
46
|
-
import logging
|
47
|
-
from pypetkitapi.client import PetKitClient
|
48
|
-
|
49
|
-
logging.basicConfig(level=logging.DEBUG)
|
50
|
-
|
51
|
-
|
52
|
-
async def main():
|
53
|
-
client = PetKitClient(
|
54
|
-
username="username", # Your PetKit account username or id
|
55
|
-
password="password", # Your PetKit account password
|
56
|
-
region="France", # Your region or country code (e.g. FR, US, etc.)
|
57
|
-
timezone="Europe/Paris", # Your timezone
|
58
|
-
)
|
59
|
-
|
60
|
-
# To get the account and devices data attached to the account
|
61
|
-
await client.get_devices_data()
|
62
|
-
|
63
|
-
# Read the account data
|
64
|
-
print(client.account_data)
|
65
|
-
|
66
|
-
# Read the devices data
|
67
|
-
print(client.device_list)
|
68
|
-
|
69
|
-
# Send command to the devices
|
70
|
-
### Example 1 : Turn on the indicator light
|
71
|
-
### Device_ID, Command, Payload
|
72
|
-
await client.send_api_request(012346789, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
|
73
|
-
|
74
|
-
### Example 2 : Feed the pet
|
75
|
-
### Device_ID, Command, Payload
|
76
|
-
await client.send_api_request(0123467, FeederCommand.MANUAL_FEED, {"amount": 1})
|
77
|
-
|
78
|
-
|
79
|
-
if __name__ == "__main__":
|
80
|
-
asyncio.run(main())
|
81
|
-
```
|
82
|
-
|
83
|
-
## Contributing
|
84
|
-
|
85
|
-
Contributions are welcome! Please open an issue or submit a pull request.
|
86
|
-
|
87
|
-
## License
|
88
|
-
|
89
|
-
This project is licensed under the MIT License. See the LICENSE file for details.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|