pypetkitapi 0.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.1.0/LICENSE +21 -0
- pypetkitapi-0.1.0/PKG-INFO +111 -0
- pypetkitapi-0.1.0/README.md +93 -0
- pypetkitapi-0.1.0/pypetkitapi/__init__.py +1 -0
- pypetkitapi-0.1.0/pypetkitapi/client.py +355 -0
- pypetkitapi-0.1.0/pypetkitapi/command.py +292 -0
- pypetkitapi-0.1.0/pypetkitapi/const.py +112 -0
- pypetkitapi-0.1.0/pypetkitapi/containers.py +116 -0
- pypetkitapi-0.1.0/pypetkitapi/exceptions.py +15 -0
- pypetkitapi-0.1.0/pypetkitapi/feeder_container.py +247 -0
- pypetkitapi-0.1.0/pypetkitapi/litter_container.py +177 -0
- pypetkitapi-0.1.0/pypetkitapi/water_fountain_container.py +135 -0
- pypetkitapi-0.1.0/pyproject.toml +228 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Jezza34000
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,111 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: pypetkitapi
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Python client for PetKit API
|
5
|
+
Home-page: https://github.com/Jezza34000/pypetkit
|
6
|
+
License: MIT
|
7
|
+
Author: Jezza34000
|
8
|
+
Author-email: info@mail.com
|
9
|
+
Requires-Python: >=3.11
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
15
|
+
Requires-Dist: aiohttp (>=3.11.0,<4.0.0)
|
16
|
+
Description-Content-Type: text/markdown
|
17
|
+
|
18
|
+
# Petkit API Client
|
19
|
+
|
20
|
+
---
|
21
|
+
|
22
|
+
# WIP - UNDER DEVELOPMENT
|
23
|
+
|
24
|
+
---
|
25
|
+
|
26
|
+
[][pypi_]
|
27
|
+
[][python version]
|
28
|
+
|
29
|
+
[][pre-commit]
|
30
|
+
[][black]
|
31
|
+
[](https://mypy.readthedocs.io/en/stable/)
|
32
|
+
[](https://github.com/astral-sh/ruff)
|
33
|
+
[](https://github.com/Jezza34000/py-petkit-api/actions)
|
34
|
+
|
35
|
+
---
|
36
|
+
|
37
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
38
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
39
|
+
|
40
|
+
[pypi_]: https://pypi.org/project/pypetkitapi/
|
41
|
+
[python version]: https://pypi.org/project/pypetkitapi
|
42
|
+
[pre-commit]: https://github.com/pre-commit/pre-commit
|
43
|
+
[black]: https://github.com/psf/black
|
44
|
+
|
45
|
+
## Overview
|
46
|
+
|
47
|
+
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.
|
48
|
+
|
49
|
+
## Features
|
50
|
+
|
51
|
+
Login and session management
|
52
|
+
Fetch account and device data
|
53
|
+
Control PetKit devices (Feeder, Litter Box, Water Fountain)
|
54
|
+
|
55
|
+
## Installation
|
56
|
+
|
57
|
+
Install the library using pip:
|
58
|
+
|
59
|
+
```bash
|
60
|
+
pip install pypetkitapi
|
61
|
+
```
|
62
|
+
|
63
|
+
## Usage Example:
|
64
|
+
|
65
|
+
```python
|
66
|
+
import asyncio
|
67
|
+
import logging
|
68
|
+
from pypetkitapi.client import PetKitClient
|
69
|
+
|
70
|
+
logging.basicConfig(level=logging.DEBUG)
|
71
|
+
|
72
|
+
|
73
|
+
async def main():
|
74
|
+
client = PetKitClient(
|
75
|
+
username="username", # Your PetKit account username
|
76
|
+
password="password", # Your PetKit account password
|
77
|
+
region="France", # Your region
|
78
|
+
timezone="Europe/Paris", # Your timezone
|
79
|
+
)
|
80
|
+
|
81
|
+
# To get the account and devices data attached to the account
|
82
|
+
await client.get_devices_data()
|
83
|
+
# Read the account data
|
84
|
+
print(client.account_data)
|
85
|
+
# Read the devices data
|
86
|
+
print(client.device_list)
|
87
|
+
|
88
|
+
# client.device_list[0] is the first device in the list in this example it's a Feeder
|
89
|
+
# Get the Feeder from the device list
|
90
|
+
my_feeder = client.device_list[0]
|
91
|
+
|
92
|
+
# Send command to the devices
|
93
|
+
### Example 1 : Turn on the indicator light
|
94
|
+
await client.send_api_request(my_feeder, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
|
95
|
+
|
96
|
+
### Example 2 : Feed the pet
|
97
|
+
await client.send_api_request(my_feeder, FeederCommand.MANUAL_FEED, {"amount": 1})
|
98
|
+
|
99
|
+
|
100
|
+
if __name__ == "__main__":
|
101
|
+
asyncio.run(main())
|
102
|
+
```
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
107
|
+
|
108
|
+
## License
|
109
|
+
|
110
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
111
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# Petkit API Client
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
# WIP - UNDER DEVELOPMENT
|
6
|
+
|
7
|
+
---
|
8
|
+
|
9
|
+
[][pypi_]
|
10
|
+
[][python version]
|
11
|
+
|
12
|
+
[][pre-commit]
|
13
|
+
[][black]
|
14
|
+
[](https://mypy.readthedocs.io/en/stable/)
|
15
|
+
[](https://github.com/astral-sh/ruff)
|
16
|
+
[](https://github.com/Jezza34000/py-petkit-api/actions)
|
17
|
+
|
18
|
+
---
|
19
|
+
|
20
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
21
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
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 asyncio
|
50
|
+
import logging
|
51
|
+
from pypetkitapi.client import PetKitClient
|
52
|
+
|
53
|
+
logging.basicConfig(level=logging.DEBUG)
|
54
|
+
|
55
|
+
|
56
|
+
async def main():
|
57
|
+
client = PetKitClient(
|
58
|
+
username="username", # Your PetKit account username
|
59
|
+
password="password", # Your PetKit account password
|
60
|
+
region="France", # Your region
|
61
|
+
timezone="Europe/Paris", # Your timezone
|
62
|
+
)
|
63
|
+
|
64
|
+
# To get the account and devices data attached to the account
|
65
|
+
await client.get_devices_data()
|
66
|
+
# Read the account data
|
67
|
+
print(client.account_data)
|
68
|
+
# Read the devices data
|
69
|
+
print(client.device_list)
|
70
|
+
|
71
|
+
# client.device_list[0] is the first device in the list in this example it's a Feeder
|
72
|
+
# Get the Feeder from the device list
|
73
|
+
my_feeder = client.device_list[0]
|
74
|
+
|
75
|
+
# Send command to the devices
|
76
|
+
### Example 1 : Turn on the indicator light
|
77
|
+
await client.send_api_request(my_feeder, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
|
78
|
+
|
79
|
+
### Example 2 : Feed the pet
|
80
|
+
await client.send_api_request(my_feeder, FeederCommand.MANUAL_FEED, {"amount": 1})
|
81
|
+
|
82
|
+
|
83
|
+
if __name__ == "__main__":
|
84
|
+
asyncio.run(main())
|
85
|
+
```
|
86
|
+
|
87
|
+
## Contributing
|
88
|
+
|
89
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
90
|
+
|
91
|
+
## License
|
92
|
+
|
93
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Pypetkit: A Python library for interfacing with PetKit"""
|
@@ -0,0 +1,355 @@
|
|
1
|
+
"""Pypetkit Client: A Python library for interfacing with PetKit"""
|
2
|
+
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
from enum import StrEnum
|
5
|
+
import hashlib
|
6
|
+
from http import HTTPMethod
|
7
|
+
import logging
|
8
|
+
|
9
|
+
import aiohttp
|
10
|
+
from aiohttp import ContentTypeError
|
11
|
+
|
12
|
+
from pypetkitapi.command import ACTIONS_MAP
|
13
|
+
from pypetkitapi.const import (
|
14
|
+
DEVICES_FEEDER,
|
15
|
+
DEVICES_LITTER_BOX,
|
16
|
+
DEVICES_WATER_FOUNTAIN,
|
17
|
+
ERR_KEY,
|
18
|
+
LOGIN_DATA,
|
19
|
+
RES_KEY,
|
20
|
+
Header,
|
21
|
+
PetkitEndpoint,
|
22
|
+
PetkitURL,
|
23
|
+
)
|
24
|
+
from pypetkitapi.containers import AccountData, Device, RegionInfo, SessionInfo
|
25
|
+
from pypetkitapi.exceptions import PypetkitError
|
26
|
+
from pypetkitapi.feeder_container import Feeder
|
27
|
+
from pypetkitapi.litter_container import Litter
|
28
|
+
from pypetkitapi.water_fountain_container import WaterFountain
|
29
|
+
|
30
|
+
_LOGGER = logging.getLogger(__name__)
|
31
|
+
|
32
|
+
|
33
|
+
class PetKitClient:
|
34
|
+
"""Petkit Client"""
|
35
|
+
|
36
|
+
_base_url: str
|
37
|
+
_session: SessionInfo | None = None
|
38
|
+
_servers_list: list[RegionInfo] = []
|
39
|
+
account_data: list[AccountData] = []
|
40
|
+
device_list: list[Feeder | Litter | WaterFountain] = []
|
41
|
+
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
username: str,
|
45
|
+
password: str,
|
46
|
+
region: str,
|
47
|
+
timezone: str,
|
48
|
+
) -> None:
|
49
|
+
"""Initialize the PetKit Client."""
|
50
|
+
self.username = username
|
51
|
+
self.password = password
|
52
|
+
self.region = region
|
53
|
+
self.timezone = timezone
|
54
|
+
|
55
|
+
async def _generate_header(self) -> dict[str, str]:
|
56
|
+
"""Create header for interaction with devices."""
|
57
|
+
session_id = self._session.id if self._session is not None else ""
|
58
|
+
return {
|
59
|
+
"Accept": Header.ACCEPT.value,
|
60
|
+
"Accept-Language": Header.ACCEPT_LANG,
|
61
|
+
"Accept-Encoding": Header.ENCODING,
|
62
|
+
"Content-Type": Header.CONTENT_TYPE,
|
63
|
+
"User-Agent": Header.AGENT,
|
64
|
+
"X-Img-Version": Header.IMG_VERSION,
|
65
|
+
"X-Locale": Header.LOCALE,
|
66
|
+
"F-Session": session_id,
|
67
|
+
"X-Session": session_id,
|
68
|
+
"X-Client": Header.CLIENT,
|
69
|
+
"X-Hour": Header.HOUR,
|
70
|
+
"X-TimezoneId": Header.TIMEZONE_ID,
|
71
|
+
"X-Api-Version": Header.API_VERSION,
|
72
|
+
"X-Timezone": Header.TIMEZONE,
|
73
|
+
}
|
74
|
+
|
75
|
+
async def _get_api_server_list(self) -> None:
|
76
|
+
"""Get the list of API servers and set the base URL."""
|
77
|
+
_LOGGER.debug("Getting API server list")
|
78
|
+
prep_req = PrepReq(base_url=PetkitURL.REGION_SRV)
|
79
|
+
response = await prep_req.request(
|
80
|
+
method=HTTPMethod.GET,
|
81
|
+
url="",
|
82
|
+
headers=await self._generate_header(),
|
83
|
+
)
|
84
|
+
_LOGGER.debug("API server list: %s", response)
|
85
|
+
self._servers_list = [
|
86
|
+
RegionInfo(**region) for region in response.get("list", [])
|
87
|
+
]
|
88
|
+
|
89
|
+
async def _get_base_url(self) -> None:
|
90
|
+
"""Find the region server for the specified region."""
|
91
|
+
await self._get_api_server_list()
|
92
|
+
_LOGGER.debug("Finding region server for region: %s", self.region)
|
93
|
+
|
94
|
+
regional_server = next(
|
95
|
+
(server for server in self._servers_list if server.name == self.region),
|
96
|
+
None,
|
97
|
+
)
|
98
|
+
|
99
|
+
if regional_server:
|
100
|
+
_LOGGER.debug(
|
101
|
+
"Found server %s for region : %s", regional_server, self.region
|
102
|
+
)
|
103
|
+
self._base_url = regional_server.gateway
|
104
|
+
return
|
105
|
+
_LOGGER.debug("Region %s not found in server list", self.region)
|
106
|
+
|
107
|
+
async def request_login_code(self) -> bool:
|
108
|
+
"""Request a login code to be sent to the user's email."""
|
109
|
+
_LOGGER.debug("Requesting login code for username: %s", self.username)
|
110
|
+
prep_req = PrepReq(base_url=self._base_url)
|
111
|
+
response = await prep_req.request(
|
112
|
+
method=HTTPMethod.GET,
|
113
|
+
url=PetkitEndpoint.GET_LOGIN_CODE,
|
114
|
+
params={"username": self.username},
|
115
|
+
headers=await self._generate_header(),
|
116
|
+
)
|
117
|
+
if response:
|
118
|
+
_LOGGER.info("Login code sent to user's email")
|
119
|
+
return True
|
120
|
+
return False
|
121
|
+
|
122
|
+
async def login(self, valid_code: str | None = None) -> None:
|
123
|
+
"""Login to the PetKit service and retrieve the appropriate server."""
|
124
|
+
# Retrieve the list of servers
|
125
|
+
await self._get_base_url()
|
126
|
+
|
127
|
+
# Prepare the data to send
|
128
|
+
data = LOGIN_DATA.copy()
|
129
|
+
data["encrypt"] = "1"
|
130
|
+
data["region"] = self.region
|
131
|
+
data["username"] = self.username
|
132
|
+
|
133
|
+
if valid_code:
|
134
|
+
_LOGGER.debug("Login method: using valid code")
|
135
|
+
data["validCode"] = valid_code
|
136
|
+
else:
|
137
|
+
_LOGGER.debug("Login method: using password")
|
138
|
+
pwd = hashlib.md5(self.password.encode()).hexdigest() # noqa: S324
|
139
|
+
data["password"] = pwd # noqa: S324
|
140
|
+
|
141
|
+
# Send the login request
|
142
|
+
prep_req = PrepReq(base_url=self._base_url)
|
143
|
+
response = await prep_req.request(
|
144
|
+
method=HTTPMethod.POST,
|
145
|
+
url=PetkitEndpoint.LOGIN,
|
146
|
+
data=data,
|
147
|
+
headers=await self._generate_header(),
|
148
|
+
)
|
149
|
+
session_data = response["session"]
|
150
|
+
self._session = SessionInfo(**session_data)
|
151
|
+
|
152
|
+
async def refresh_session(self) -> None:
|
153
|
+
"""Refresh the session."""
|
154
|
+
_LOGGER.debug("Refreshing session")
|
155
|
+
prep_req = PrepReq(base_url=self._base_url)
|
156
|
+
response = await prep_req.request(
|
157
|
+
method=HTTPMethod.POST,
|
158
|
+
url=PetkitEndpoint.REFRESH_SESSION,
|
159
|
+
headers=await self._generate_header(),
|
160
|
+
)
|
161
|
+
session_data = response["session"]
|
162
|
+
self._session = SessionInfo(**session_data)
|
163
|
+
|
164
|
+
async def validate_session(self) -> None:
|
165
|
+
"""Check if the session is still valid and refresh or re-login if necessary."""
|
166
|
+
if self._session is None:
|
167
|
+
await self.login()
|
168
|
+
return
|
169
|
+
|
170
|
+
created_at = datetime.strptime(
|
171
|
+
self._session.created_at,
|
172
|
+
"%Y-%m-%dT%H:%M:%S.%f%z",
|
173
|
+
)
|
174
|
+
current_time = datetime.now(tz=created_at.tzinfo)
|
175
|
+
token_age = current_time - created_at
|
176
|
+
max_age = timedelta(seconds=self._session.expires_in)
|
177
|
+
half_max_age = max_age / 2
|
178
|
+
|
179
|
+
if token_age > max_age:
|
180
|
+
_LOGGER.debug("Token expired, re-logging in")
|
181
|
+
await self.login()
|
182
|
+
elif half_max_age < token_age <= max_age:
|
183
|
+
_LOGGER.debug("Token still OK, but refreshing session")
|
184
|
+
await self.refresh_session()
|
185
|
+
return
|
186
|
+
|
187
|
+
async def _get_account_data(self) -> None:
|
188
|
+
"""Get the account data from the PetKit service."""
|
189
|
+
await self.validate_session()
|
190
|
+
_LOGGER.debug("Fetching account data")
|
191
|
+
prep_req = PrepReq(base_url=self._base_url)
|
192
|
+
response = await prep_req.request(
|
193
|
+
method=HTTPMethod.GET,
|
194
|
+
url=PetkitEndpoint.FAMILY_LIST,
|
195
|
+
headers=await self._generate_header(),
|
196
|
+
)
|
197
|
+
self.account_data = [AccountData(**account) for account in response]
|
198
|
+
|
199
|
+
async def get_devices_data(self) -> None:
|
200
|
+
"""Get the devices data from the PetKit servers."""
|
201
|
+
if not self.account_data:
|
202
|
+
await self._get_account_data()
|
203
|
+
|
204
|
+
device_list: list[Device] = []
|
205
|
+
for account in self.account_data:
|
206
|
+
_LOGGER.debug("Fetching devices data for account: %s", account)
|
207
|
+
if account.device_list:
|
208
|
+
device_list.extend(account.device_list)
|
209
|
+
|
210
|
+
_LOGGER.info("%s devices found for this account", len(device_list))
|
211
|
+
for device in device_list:
|
212
|
+
_LOGGER.debug("Fetching devices data: %s", device)
|
213
|
+
device_type = device.device_type.lower()
|
214
|
+
# TODO: Fetch device records
|
215
|
+
if device_type in DEVICES_FEEDER:
|
216
|
+
await self._fetch_device_data(device, Feeder)
|
217
|
+
elif device_type in DEVICES_LITTER_BOX:
|
218
|
+
await self._fetch_device_data(device, Litter)
|
219
|
+
elif device_type in DEVICES_WATER_FOUNTAIN:
|
220
|
+
await self._fetch_device_data(device, WaterFountain)
|
221
|
+
else:
|
222
|
+
_LOGGER.warning("Unknown device type: %s", device_type)
|
223
|
+
|
224
|
+
async def _fetch_device_data(
|
225
|
+
self,
|
226
|
+
device: Device,
|
227
|
+
data_class: type[Feeder | Litter | WaterFountain],
|
228
|
+
) -> None:
|
229
|
+
"""Fetch the device data from the PetKit servers."""
|
230
|
+
await self.validate_session()
|
231
|
+
endpoint = data_class.get_endpoint(device.device_type)
|
232
|
+
device_type = device.device_type.lower()
|
233
|
+
query_param = data_class.query_param(device.device_id)
|
234
|
+
|
235
|
+
prep_req = PrepReq(base_url=self._base_url)
|
236
|
+
response = await prep_req.request(
|
237
|
+
method=HTTPMethod.GET,
|
238
|
+
url=f"{device_type}/{endpoint}",
|
239
|
+
params=query_param,
|
240
|
+
headers=await self._generate_header(),
|
241
|
+
)
|
242
|
+
device_data = data_class(**response)
|
243
|
+
device_data.device_type = device.device_type # Add the device_type attribute
|
244
|
+
_LOGGER.info("Adding device type: %s", device.device_type)
|
245
|
+
self.device_list.append(device_data)
|
246
|
+
|
247
|
+
async def send_api_request(
|
248
|
+
self,
|
249
|
+
device: Feeder | Litter | WaterFountain,
|
250
|
+
action: StrEnum,
|
251
|
+
setting: dict | None = None,
|
252
|
+
) -> None:
|
253
|
+
"""Control the device using the PetKit API."""
|
254
|
+
|
255
|
+
_LOGGER.debug(
|
256
|
+
"Control API: %s %s %s",
|
257
|
+
action,
|
258
|
+
setting,
|
259
|
+
device,
|
260
|
+
)
|
261
|
+
|
262
|
+
if device.device_type:
|
263
|
+
device_type = device.device_type.lower()
|
264
|
+
else:
|
265
|
+
raise PypetkitError(
|
266
|
+
"Device type is not available, and is mandatory for sending commands."
|
267
|
+
)
|
268
|
+
|
269
|
+
if action not in ACTIONS_MAP:
|
270
|
+
raise PypetkitError(f"Action {action} not supported.")
|
271
|
+
|
272
|
+
action_info = ACTIONS_MAP[action]
|
273
|
+
if device_type not in action_info.supported_device:
|
274
|
+
raise PypetkitError(
|
275
|
+
f"Device type {device.device_type} not supported for action {action}."
|
276
|
+
)
|
277
|
+
|
278
|
+
if callable(action_info.endpoint):
|
279
|
+
endpoint = action_info.endpoint(device)
|
280
|
+
else:
|
281
|
+
endpoint = action_info.endpoint
|
282
|
+
url = f"{device.device_type.lower()}/{endpoint}"
|
283
|
+
|
284
|
+
headers = await self._generate_header()
|
285
|
+
|
286
|
+
# Use the lambda to generate params
|
287
|
+
if setting is not None:
|
288
|
+
params = action_info.params(device, setting)
|
289
|
+
else:
|
290
|
+
params = action_info.params(device)
|
291
|
+
|
292
|
+
prep_req = PrepReq(base_url=self._base_url)
|
293
|
+
await prep_req.request(
|
294
|
+
method=HTTPMethod.POST,
|
295
|
+
url=url,
|
296
|
+
data=params,
|
297
|
+
headers=headers,
|
298
|
+
)
|
299
|
+
|
300
|
+
|
301
|
+
class PrepReq:
|
302
|
+
"""Prepare the request to the PetKit API."""
|
303
|
+
|
304
|
+
def __init__(self, base_url: str, base_headers: dict | None = None) -> None:
|
305
|
+
"""Initialize the request."""
|
306
|
+
self.base_url = base_url
|
307
|
+
self.base_headers = base_headers or {}
|
308
|
+
|
309
|
+
async def request(
|
310
|
+
self,
|
311
|
+
method: str,
|
312
|
+
url: str,
|
313
|
+
params=None,
|
314
|
+
data=None,
|
315
|
+
headers=None,
|
316
|
+
) -> dict:
|
317
|
+
"""Make a request to the PetKit API."""
|
318
|
+
_url = "/".join(s.strip("/") for s in [self.base_url, url])
|
319
|
+
_headers = {**self.base_headers, **(headers or {})}
|
320
|
+
_LOGGER.debug(
|
321
|
+
"Request: %s %s Params: %s Data: %s Headers: %s",
|
322
|
+
method,
|
323
|
+
_url,
|
324
|
+
params,
|
325
|
+
data,
|
326
|
+
_headers,
|
327
|
+
)
|
328
|
+
async with aiohttp.ClientSession() as session:
|
329
|
+
try:
|
330
|
+
async with session.request(
|
331
|
+
method,
|
332
|
+
_url,
|
333
|
+
params=params,
|
334
|
+
data=data,
|
335
|
+
headers=_headers,
|
336
|
+
) as resp:
|
337
|
+
response = await resp.json()
|
338
|
+
if ERR_KEY in response:
|
339
|
+
error_msg = response[ERR_KEY].get("msg", "Unknown error")
|
340
|
+
raise PypetkitError(f"Request failed: {error_msg}")
|
341
|
+
if RES_KEY in response:
|
342
|
+
_LOGGER.debug("Request response: %s", response)
|
343
|
+
return response[RES_KEY]
|
344
|
+
raise PypetkitError("Unexpected response format")
|
345
|
+
except ContentTypeError:
|
346
|
+
"""If we get an error, lets log everything for debugging."""
|
347
|
+
try:
|
348
|
+
resp_json = await resp.json(content_type=None)
|
349
|
+
_LOGGER.info("Resp: %s", resp_json)
|
350
|
+
except ContentTypeError as err_2:
|
351
|
+
_LOGGER.info(err_2)
|
352
|
+
resp_raw = await resp.read()
|
353
|
+
_LOGGER.info("Resp raw: %s", resp_raw)
|
354
|
+
# Still raise the err so that it's clear it failed.
|
355
|
+
raise
|