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.
@@ -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
+ }