pypetkitapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pypetkitapi/__init__.py +1 -0
- pypetkitapi/client.py +355 -0
- pypetkitapi/command.py +292 -0
- pypetkitapi/const.py +112 -0
- pypetkitapi/containers.py +116 -0
- pypetkitapi/exceptions.py +15 -0
- pypetkitapi/feeder_container.py +247 -0
- pypetkitapi/litter_container.py +177 -0
- pypetkitapi/water_fountain_container.py +135 -0
- pypetkitapi-0.1.0.dist-info/LICENSE +21 -0
- pypetkitapi-0.1.0.dist-info/METADATA +111 -0
- pypetkitapi-0.1.0.dist-info/RECORD +13 -0
- pypetkitapi-0.1.0.dist-info/WHEEL +4 -0
pypetkitapi/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
"""Pypetkit: A Python library for interfacing with PetKit"""
|
pypetkitapi/client.py
ADDED
@@ -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
|
pypetkitapi/command.py
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
"""Command module for PyPetkit"""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
import datetime
|
6
|
+
from enum import StrEnum
|
7
|
+
import json
|
8
|
+
|
9
|
+
from pypetkitapi.const import (
|
10
|
+
ALL_DEVICES,
|
11
|
+
D3,
|
12
|
+
D4H,
|
13
|
+
D4S,
|
14
|
+
D4SH,
|
15
|
+
DEVICES_FEEDER,
|
16
|
+
FEEDER,
|
17
|
+
FEEDER_MINI,
|
18
|
+
T3,
|
19
|
+
T4,
|
20
|
+
T5,
|
21
|
+
T6,
|
22
|
+
PetkitEndpoint,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
class DeviceCommand(StrEnum):
|
27
|
+
"""Device Command"""
|
28
|
+
|
29
|
+
UPDATE_SETTING = "update_setting"
|
30
|
+
|
31
|
+
|
32
|
+
class FeederCommand(StrEnum):
|
33
|
+
"""Feeder Command"""
|
34
|
+
|
35
|
+
CALL_PET = "call_pet"
|
36
|
+
CALIBRATION = "food_reset"
|
37
|
+
MANUAL_FEED = "manual_feed"
|
38
|
+
MANUAL_FEED_DUAL = "manual_feed_dual"
|
39
|
+
CANCEL_MANUAL_FEED = "cancelRealtimeFeed"
|
40
|
+
FOOD_REPLENISHED = "food_replenished"
|
41
|
+
RESET_DESICCANT = "desiccantReset"
|
42
|
+
|
43
|
+
|
44
|
+
class LitterCommand(StrEnum):
|
45
|
+
"""LitterCommand"""
|
46
|
+
|
47
|
+
CONTROL_DEVICE = "control_device"
|
48
|
+
RESET_DEODORIZER = "reset_deodorizer"
|
49
|
+
|
50
|
+
|
51
|
+
class PetCommand(StrEnum):
|
52
|
+
"""PetCommand"""
|
53
|
+
|
54
|
+
UPDATE_SETTING = "update_setting"
|
55
|
+
|
56
|
+
|
57
|
+
class LitterBoxCommand(StrEnum):
|
58
|
+
"""LitterBoxCommand"""
|
59
|
+
|
60
|
+
LIGHT_ON = "light_on"
|
61
|
+
ODOR_REMOVAL = "start_odor"
|
62
|
+
PAUSE_CLEAN = "stop_clean"
|
63
|
+
POWER = "power"
|
64
|
+
RESET_DEODOR = "reset_deodorizer"
|
65
|
+
RESUME_CLEAN = "continue_clean"
|
66
|
+
START_CLEAN = "start_clean"
|
67
|
+
START_MAINTENANCE = "start_maintenance"
|
68
|
+
EXIT_MAINTENANCE = "exit_maintenance"
|
69
|
+
PAUSE_MAINTENANCE_EXIT = "pause_maintenance_exit"
|
70
|
+
RESUME_MAINTENANCE_EXIT = "resume_maintenance_exit"
|
71
|
+
DUMP_LITTER = "dump_litter"
|
72
|
+
PAUSE_LITTER_DUMP = "pause_litter_dump"
|
73
|
+
RESUME_LITTER_DUMP = "resume_litter_dump"
|
74
|
+
RESET_MAX_DEODOR = "reset_max_deodorizer"
|
75
|
+
|
76
|
+
|
77
|
+
class LitterBoxCommandKey(StrEnum):
|
78
|
+
"""LitterBoxCommandKey"""
|
79
|
+
|
80
|
+
CONTINUE = "continue_action"
|
81
|
+
END = "end_action"
|
82
|
+
POWER = "power_action"
|
83
|
+
START = "start_action"
|
84
|
+
STOP = "stop_action"
|
85
|
+
|
86
|
+
|
87
|
+
class LitterBoxCommandType(StrEnum):
|
88
|
+
"""LitterBoxCommandType"""
|
89
|
+
|
90
|
+
CONTINUE = "continue"
|
91
|
+
END = "end"
|
92
|
+
POWER = "power"
|
93
|
+
START = "start"
|
94
|
+
STOP = "stop"
|
95
|
+
|
96
|
+
|
97
|
+
LB_CMD_TO_KEY = {
|
98
|
+
LitterBoxCommand.LIGHT_ON: LitterBoxCommandKey.START,
|
99
|
+
LitterBoxCommand.POWER: LitterBoxCommandKey.POWER,
|
100
|
+
LitterBoxCommand.START_CLEAN: LitterBoxCommandKey.START,
|
101
|
+
LitterBoxCommand.PAUSE_CLEAN: LitterBoxCommandKey.STOP,
|
102
|
+
LitterBoxCommand.RESUME_CLEAN: LitterBoxCommandKey.CONTINUE,
|
103
|
+
LitterBoxCommand.ODOR_REMOVAL: LitterBoxCommandKey.START,
|
104
|
+
LitterBoxCommand.RESET_DEODOR: LitterBoxCommandKey.START,
|
105
|
+
LitterBoxCommand.START_MAINTENANCE: LitterBoxCommandKey.START,
|
106
|
+
LitterBoxCommand.EXIT_MAINTENANCE: LitterBoxCommandKey.END,
|
107
|
+
LitterBoxCommand.PAUSE_MAINTENANCE_EXIT: LitterBoxCommandKey.STOP,
|
108
|
+
LitterBoxCommand.RESUME_MAINTENANCE_EXIT: LitterBoxCommandKey.CONTINUE,
|
109
|
+
LitterBoxCommand.DUMP_LITTER: LitterBoxCommandKey.START,
|
110
|
+
LitterBoxCommand.PAUSE_LITTER_DUMP: LitterBoxCommandKey.STOP,
|
111
|
+
LitterBoxCommand.RESUME_LITTER_DUMP: LitterBoxCommandKey.CONTINUE,
|
112
|
+
LitterBoxCommand.RESET_MAX_DEODOR: LitterBoxCommandKey.START,
|
113
|
+
}
|
114
|
+
|
115
|
+
LB_CMD_TO_TYPE = {
|
116
|
+
LitterBoxCommand.LIGHT_ON: LitterBoxCommandType.START,
|
117
|
+
LitterBoxCommand.POWER: LitterBoxCommandType.POWER,
|
118
|
+
LitterBoxCommand.START_CLEAN: LitterBoxCommandType.START,
|
119
|
+
LitterBoxCommand.PAUSE_CLEAN: LitterBoxCommandType.STOP,
|
120
|
+
LitterBoxCommand.RESUME_CLEAN: LitterBoxCommandType.CONTINUE,
|
121
|
+
LitterBoxCommand.ODOR_REMOVAL: LitterBoxCommandType.START,
|
122
|
+
LitterBoxCommand.RESET_DEODOR: LitterBoxCommandType.START,
|
123
|
+
LitterBoxCommand.START_MAINTENANCE: LitterBoxCommandType.START,
|
124
|
+
LitterBoxCommand.EXIT_MAINTENANCE: LitterBoxCommandType.END,
|
125
|
+
LitterBoxCommand.PAUSE_MAINTENANCE_EXIT: LitterBoxCommandType.STOP,
|
126
|
+
LitterBoxCommand.RESUME_MAINTENANCE_EXIT: LitterBoxCommandType.CONTINUE,
|
127
|
+
LitterBoxCommand.DUMP_LITTER: LitterBoxCommandType.START,
|
128
|
+
LitterBoxCommand.PAUSE_LITTER_DUMP: LitterBoxCommandType.STOP,
|
129
|
+
LitterBoxCommand.RESUME_LITTER_DUMP: LitterBoxCommandType.CONTINUE,
|
130
|
+
LitterBoxCommand.RESET_MAX_DEODOR: LitterBoxCommandType.START,
|
131
|
+
}
|
132
|
+
|
133
|
+
LB_CMD_TO_VALUE = {
|
134
|
+
LitterBoxCommand.LIGHT_ON: 7,
|
135
|
+
LitterBoxCommand.START_CLEAN: 0,
|
136
|
+
LitterBoxCommand.PAUSE_CLEAN: 0,
|
137
|
+
LitterBoxCommand.RESUME_CLEAN: 0,
|
138
|
+
LitterBoxCommand.ODOR_REMOVAL: 2,
|
139
|
+
LitterBoxCommand.RESET_DEODOR: 6,
|
140
|
+
LitterBoxCommand.START_MAINTENANCE: 9,
|
141
|
+
LitterBoxCommand.EXIT_MAINTENANCE: 9,
|
142
|
+
LitterBoxCommand.PAUSE_MAINTENANCE_EXIT: 9,
|
143
|
+
LitterBoxCommand.RESUME_MAINTENANCE_EXIT: 9,
|
144
|
+
LitterBoxCommand.DUMP_LITTER: 1,
|
145
|
+
LitterBoxCommand.PAUSE_LITTER_DUMP: 1,
|
146
|
+
LitterBoxCommand.RESUME_LITTER_DUMP: 1,
|
147
|
+
LitterBoxCommand.RESET_MAX_DEODOR: 8,
|
148
|
+
}
|
149
|
+
|
150
|
+
|
151
|
+
@dataclass
|
152
|
+
class CmdData:
|
153
|
+
"""Command Info"""
|
154
|
+
|
155
|
+
endpoint: str | Callable
|
156
|
+
params: Callable
|
157
|
+
supported_device: list[str] = field(default_factory=list)
|
158
|
+
|
159
|
+
|
160
|
+
def get_endpoint_manual_feed(device):
|
161
|
+
"""Get the endpoint for the device"""
|
162
|
+
if device.device_type == FEEDER_MINI:
|
163
|
+
return PetkitEndpoint.MINI_MANUAL_FEED
|
164
|
+
if device.device_type == FEEDER:
|
165
|
+
return PetkitEndpoint.FRESH_ELEMENT_MANUAL_FEED
|
166
|
+
return PetkitEndpoint.MANUAL_FEED
|
167
|
+
|
168
|
+
|
169
|
+
def get_endpoint_reset_desiccant(device):
|
170
|
+
"""Get the endpoint for the device"""
|
171
|
+
if device.device_type == FEEDER_MINI:
|
172
|
+
return PetkitEndpoint.MINI_DESICCANT_RESET
|
173
|
+
if device.device_type == FEEDER:
|
174
|
+
return PetkitEndpoint.FRESH_ELEMENT_DESICCANT_RESET
|
175
|
+
return PetkitEndpoint.DESICCANT_RESET
|
176
|
+
|
177
|
+
|
178
|
+
ACTIONS_MAP = {
|
179
|
+
DeviceCommand.UPDATE_SETTING: CmdData(
|
180
|
+
endpoint=PetkitEndpoint.UPDATE_SETTING,
|
181
|
+
params=lambda device, setting: {
|
182
|
+
"id": device.id,
|
183
|
+
"kv": json.dumps(setting),
|
184
|
+
},
|
185
|
+
supported_device=ALL_DEVICES,
|
186
|
+
),
|
187
|
+
FeederCommand.MANUAL_FEED: CmdData(
|
188
|
+
endpoint=lambda device: get_endpoint_manual_feed(device),
|
189
|
+
params=lambda device, setting: {
|
190
|
+
"day": datetime.datetime.now().strftime("%Y%m%d"),
|
191
|
+
"deviceId": device.id,
|
192
|
+
"time": "-1",
|
193
|
+
**setting,
|
194
|
+
},
|
195
|
+
supported_device=DEVICES_FEEDER, # TODO: Check if this is correct
|
196
|
+
),
|
197
|
+
FeederCommand.MANUAL_FEED_DUAL: CmdData(
|
198
|
+
endpoint=PetkitEndpoint.UPDATE_SETTING,
|
199
|
+
params=lambda device, setting: {
|
200
|
+
"day": datetime.datetime.now().strftime("%Y%m%d"),
|
201
|
+
"deviceId": device.id,
|
202
|
+
"name": "",
|
203
|
+
"time": "-1",
|
204
|
+
**setting,
|
205
|
+
},
|
206
|
+
supported_device=ALL_DEVICES,
|
207
|
+
),
|
208
|
+
FeederCommand.CANCEL_MANUAL_FEED: CmdData(
|
209
|
+
endpoint=lambda device: (
|
210
|
+
PetkitEndpoint.FRESH_ELEMENT_CANCEL_FEED
|
211
|
+
if device.device_type == FEEDER
|
212
|
+
else PetkitEndpoint.CANCEL_FEED
|
213
|
+
),
|
214
|
+
params=lambda device: {
|
215
|
+
"day": datetime.datetime.now().strftime("%Y%m%d"),
|
216
|
+
"deviceId": device.id,
|
217
|
+
**(
|
218
|
+
{"id": device.manual_feed_id}
|
219
|
+
if device.device_type.lower() in [D4H, D4S, D4SH]
|
220
|
+
else {}
|
221
|
+
),
|
222
|
+
},
|
223
|
+
supported_device=DEVICES_FEEDER,
|
224
|
+
),
|
225
|
+
FeederCommand.FOOD_REPLENISHED: CmdData(
|
226
|
+
endpoint=PetkitEndpoint.REPLENISHED_FOOD,
|
227
|
+
params=lambda device: {
|
228
|
+
"deviceId": device.id,
|
229
|
+
"noRemind": "3",
|
230
|
+
},
|
231
|
+
supported_device=[D4H, D4S, D4SH],
|
232
|
+
),
|
233
|
+
FeederCommand.CALIBRATION: CmdData(
|
234
|
+
endpoint=PetkitEndpoint.FRESH_ELEMENT_CALIBRATION,
|
235
|
+
params=lambda device, value: {
|
236
|
+
"deviceId": device.id,
|
237
|
+
"action": value,
|
238
|
+
},
|
239
|
+
supported_device=[FEEDER],
|
240
|
+
),
|
241
|
+
FeederCommand.RESET_DESICCANT: CmdData(
|
242
|
+
endpoint=lambda device: get_endpoint_reset_desiccant(device),
|
243
|
+
params=lambda device: {
|
244
|
+
"deviceId": device.id,
|
245
|
+
},
|
246
|
+
supported_device=DEVICES_FEEDER,
|
247
|
+
),
|
248
|
+
LitterCommand.RESET_DEODORIZER: CmdData(
|
249
|
+
endpoint=PetkitEndpoint.DEODORANT_RESET,
|
250
|
+
params=lambda device: {
|
251
|
+
"deviceId": device.id,
|
252
|
+
},
|
253
|
+
supported_device=[T4, T5, T6],
|
254
|
+
),
|
255
|
+
FeederCommand.CALL_PET: CmdData(
|
256
|
+
endpoint=PetkitEndpoint.CALL_PET,
|
257
|
+
params=lambda device: {
|
258
|
+
"deviceId": device.id,
|
259
|
+
},
|
260
|
+
supported_device=[D3],
|
261
|
+
),
|
262
|
+
# TODO : Find how to support power ON/OFF
|
263
|
+
LitterCommand.CONTROL_DEVICE: CmdData(
|
264
|
+
endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
265
|
+
params=lambda device, command: {
|
266
|
+
"id": device.id,
|
267
|
+
"kv": json.dumps({LB_CMD_TO_KEY[command]: LB_CMD_TO_VALUE[command]}),
|
268
|
+
"type": LB_CMD_TO_TYPE[command],
|
269
|
+
},
|
270
|
+
supported_device=[T3, T4, T5, T6],
|
271
|
+
),
|
272
|
+
# TODO : Find how to support Pet Setting with send_api_request
|
273
|
+
# PetCommand.UPDATE_SETTING: CmdData(
|
274
|
+
# endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
275
|
+
# params=lambda pet, setting: {
|
276
|
+
# "petId": pet,
|
277
|
+
# "kv": json.dumps(setting),
|
278
|
+
# },
|
279
|
+
# supported_device=ALL_DEVICES,
|
280
|
+
# ),
|
281
|
+
# FountainCommand.CONTROL_DEVICE: CmdData(
|
282
|
+
# endpoint=PetkitEndpoint.CONTROL_DEVICE,
|
283
|
+
# params=lambda device, setting: {
|
284
|
+
# "bleId": water_fountain.data["id"],
|
285
|
+
# "cmd": cmnd_code,
|
286
|
+
# "data": ble_data,
|
287
|
+
# "mac": water_fountain.data["mac"],
|
288
|
+
# "type": water_fountain.ble_relay,
|
289
|
+
# },
|
290
|
+
# supported_device=[CTW3],
|
291
|
+
# ),
|
292
|
+
}
|