pygpt-net 2.6.27__py3-none-any.whl → 2.6.28__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.
Files changed (40) hide show
  1. pygpt_net/CHANGELOG.txt +6 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/kernel/kernel.py +2 -0
  5. pygpt_net/controller/notepad/notepad.py +10 -1
  6. pygpt_net/controller/theme/markdown.py +2 -0
  7. pygpt_net/controller/ui/tabs.py +5 -0
  8. pygpt_net/core/audio/backend/native.py +1 -3
  9. pygpt_net/core/command/command.py +2 -0
  10. pygpt_net/core/render/web/helpers.py +13 -3
  11. pygpt_net/core/render/web/renderer.py +3 -3
  12. pygpt_net/data/config/config.json +3 -3
  13. pygpt_net/data/config/models.json +3 -3
  14. pygpt_net/data/css/web-blocks.darkest.css +91 -0
  15. pygpt_net/data/css/web-chatgpt.css +7 -5
  16. pygpt_net/data/css/web-chatgpt.dark.css +5 -2
  17. pygpt_net/data/css/web-chatgpt.darkest.css +91 -0
  18. pygpt_net/data/css/web-chatgpt.light.css +8 -2
  19. pygpt_net/data/css/web-chatgpt_wide.css +7 -4
  20. pygpt_net/data/css/web-chatgpt_wide.dark.css +5 -2
  21. pygpt_net/data/css/web-chatgpt_wide.darkest.css +91 -0
  22. pygpt_net/data/css/web-chatgpt_wide.light.css +9 -6
  23. pygpt_net/data/themes/dark_darkest.css +31 -0
  24. pygpt_net/data/themes/dark_darkest.xml +10 -0
  25. pygpt_net/plugin/tuya/__init__.py +12 -0
  26. pygpt_net/plugin/tuya/config.py +256 -0
  27. pygpt_net/plugin/tuya/plugin.py +117 -0
  28. pygpt_net/plugin/tuya/worker.py +588 -0
  29. pygpt_net/plugin/wikipedia/__init__.py +12 -0
  30. pygpt_net/plugin/wikipedia/config.py +228 -0
  31. pygpt_net/plugin/wikipedia/plugin.py +114 -0
  32. pygpt_net/plugin/wikipedia/worker.py +430 -0
  33. pygpt_net/provider/core/config/patch.py +11 -0
  34. pygpt_net/ui/widget/tabs/output.py +2 -0
  35. pygpt_net/ui/widget/textarea/input.py +10 -7
  36. {pygpt_net-2.6.27.dist-info → pygpt_net-2.6.28.dist-info}/METADATA +40 -2
  37. {pygpt_net-2.6.27.dist-info → pygpt_net-2.6.28.dist-info}/RECORD +40 -27
  38. {pygpt_net-2.6.27.dist-info → pygpt_net-2.6.28.dist-info}/LICENSE +0 -0
  39. {pygpt_net-2.6.27.dist-info → pygpt_net-2.6.28.dist-info}/WHEEL +0 -0
  40. {pygpt_net-2.6.27.dist-info → pygpt_net-2.6.28.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,588 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.08.27 20:00:00 #
10
+ # ================================================== #
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import time
16
+ import hmac
17
+ import hashlib
18
+ from typing import Any, Dict, List, Optional, Tuple, Union
19
+ from urllib.parse import urlencode
20
+
21
+ import requests
22
+ from PySide6.QtCore import Slot
23
+
24
+ from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
25
+
26
+
27
+ class WorkerSignals(BaseSignals):
28
+ pass
29
+
30
+
31
+ class Worker(BaseWorker):
32
+ """
33
+ Tuya Smart Home plugin worker: Auth (token), Devices, Status, Control, Sensors.
34
+ Uses Tuya Cloud API (v1.0) with proper "new signature" HMAC-SHA256 signing.
35
+ """
36
+
37
+ def __init__(self, *args, **kwargs):
38
+ super(Worker, self).__init__()
39
+ self.signals = WorkerSignals()
40
+ self.args = args
41
+ self.kwargs = kwargs
42
+ self.plugin = None
43
+ self.cmds = None
44
+ self.ctx = None
45
+ self.msg = None
46
+
47
+ # ---------------------- Core runner ----------------------
48
+
49
+ @Slot()
50
+ def run(self):
51
+ try:
52
+ responses = []
53
+ for item in self.cmds:
54
+ if self.is_stopped():
55
+ break
56
+ try:
57
+ response = None
58
+ if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
59
+
60
+ # -------- Auth --------
61
+ if item["cmd"] == "tuya_set_keys":
62
+ response = self.cmd_tuya_set_keys(item)
63
+ elif item["cmd"] == "tuya_set_uid":
64
+ response = self.cmd_tuya_set_uid(item)
65
+ elif item["cmd"] == "tuya_token_get":
66
+ response = self.cmd_tuya_token_get(item)
67
+
68
+ # -------- Devices / Info --------
69
+ elif item["cmd"] == "tuya_devices_list":
70
+ response = self.cmd_tuya_devices_list(item)
71
+ elif item["cmd"] == "tuya_device_get":
72
+ response = self.cmd_tuya_device_get(item)
73
+ elif item["cmd"] == "tuya_device_status":
74
+ response = self.cmd_tuya_device_status(item)
75
+ elif item["cmd"] == "tuya_device_functions":
76
+ response = self.cmd_tuya_device_functions(item)
77
+ elif item["cmd"] == "tuya_find_device":
78
+ response = self.cmd_tuya_find_device(item)
79
+
80
+ # -------- Control --------
81
+ elif item["cmd"] == "tuya_device_set":
82
+ response = self.cmd_tuya_device_set(item)
83
+ elif item["cmd"] == "tuya_device_send":
84
+ response = self.cmd_tuya_device_send(item)
85
+ elif item["cmd"] == "tuya_device_on":
86
+ response = self.cmd_tuya_device_on(item)
87
+ elif item["cmd"] == "tuya_device_off":
88
+ response = self.cmd_tuya_device_off(item)
89
+ elif item["cmd"] == "tuya_device_toggle":
90
+ response = self.cmd_tuya_device_toggle(item)
91
+
92
+ # -------- Sensors --------
93
+ elif item["cmd"] == "tuya_sensors_read":
94
+ response = self.cmd_tuya_sensors_read(item)
95
+
96
+ if response:
97
+ responses.append(response)
98
+
99
+ except Exception as e:
100
+ responses.append(self.make_response(item, self.throw_error(e)))
101
+
102
+ if responses:
103
+ self.reply_more(responses)
104
+ if self.msg is not None:
105
+ self.status(self.msg)
106
+ except Exception as e:
107
+ self.error(e)
108
+ finally:
109
+ self.cleanup()
110
+
111
+ # ---------------------- HTTP / Sign helpers ----------------------
112
+
113
+ def _api_base(self) -> str:
114
+ return (self.plugin.get_option_value("api_base") or "https://openapi.tuyaeu.com").rstrip("/")
115
+
116
+ def _timeout(self) -> int:
117
+ try:
118
+ return int(self.plugin.get_option_value("http_timeout") or 30)
119
+ except Exception:
120
+ return 30
121
+
122
+ def _lang(self) -> str:
123
+ return (self.plugin.get_option_value("lang") or "en").strip() or "en"
124
+
125
+ def _client(self) -> Tuple[str, str]:
126
+ cid = (self.plugin.get_option_value("tuya_client_id") or "").strip()
127
+ sec = (self.plugin.get_option_value("tuya_client_secret") or "").strip()
128
+ if not cid or not sec:
129
+ raise RuntimeError("Set Tuya Client ID and Client Secret first.")
130
+ return cid, sec
131
+
132
+ def _now_ms_str(self) -> str:
133
+ return str(int(time.time() * 1000))
134
+
135
+ def _hmac_sha256_upper(self, key: str, msg: str) -> str:
136
+ return hmac.new(key.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).hexdigest().upper()
137
+
138
+ def _sha256_hex(self, s: str) -> str:
139
+ return hashlib.sha256(s.encode("utf-8")).hexdigest()
140
+
141
+ def _json_canonical(self, body: Union[dict, list, None]) -> str:
142
+ # Canonical JSON to ensure hash matches the actual sent body
143
+ if body is None:
144
+ return ""
145
+ return json.dumps(body, separators=(",", ":"), sort_keys=True, ensure_ascii=False)
146
+
147
+ def _sorted_params_list(self, params: Optional[dict | List[Tuple[str, Any]]]) -> List[Tuple[str, str]]:
148
+ if not params:
149
+ return []
150
+ if isinstance(params, list):
151
+ # Convert values to str, preserve provided order
152
+ return [(k, str(v)) for (k, v) in params]
153
+ out: List[Tuple[str, str]] = []
154
+ for k in sorted(params.keys()):
155
+ v = params[k]
156
+ if isinstance(v, list):
157
+ for vv in v:
158
+ out.append((k, str(vv)))
159
+ else:
160
+ out.append((k, str(v)))
161
+ return out
162
+
163
+ def _canonical_qs(self, params: Optional[dict | List[Tuple[str, Any]]]) -> str:
164
+ items = self._sorted_params_list(params)
165
+ if not items:
166
+ return ""
167
+ return urlencode(items, doseq=True)
168
+
169
+ def _token_cached(self) -> Tuple[Optional[str], int]:
170
+ tok = (self.plugin.get_option_value("tuya_access_token") or "").strip()
171
+ exp_at = 0
172
+ try:
173
+ exp_at = int(self.plugin.get_option_value("tuya_token_expire_at") or 0)
174
+ except Exception:
175
+ exp_at = 0
176
+ return (tok if tok else None), exp_at
177
+
178
+ def _token_valid(self) -> bool:
179
+ tok, exp_at = self._token_cached()
180
+ if not tok:
181
+ return False
182
+ return int(time.time()) < exp_at
183
+
184
+ def _ensure_token(self):
185
+ if not self._token_valid():
186
+ self._token_get()
187
+
188
+ def _build_string_to_sign(self, method: str, path: str, params: Optional[dict | List[Tuple[str, Any]]], body_str: str) -> str:
189
+ # signature: stringToSign = method + "\n" + contentSHA256 + "\n" + headersStr + "\n" + urlPathWithQuery
190
+ method = (method or "GET").upper()
191
+ content_sha256 = self._sha256_hex(body_str or "")
192
+ headers_str = "" # We don't sign headers (optional feature)
193
+ qs = self._canonical_qs(params)
194
+ url_part = path if path.startswith("/") else "/" + path
195
+ if qs:
196
+ url_part = f"{url_part}?{qs}"
197
+ return "\n".join([method, content_sha256, headers_str, url_part])
198
+
199
+ def _headers(
200
+ self,
201
+ method: str,
202
+ path: str,
203
+ params: Optional[dict | List[Tuple[str, Any]]] = None,
204
+ body_str: str = "",
205
+ ) -> Dict[str, str]:
206
+ cid, sec = self._client()
207
+ t = self._now_ms_str()
208
+ tok, _ = self._token_cached()
209
+ # Do not include token in sign for /v1.0/token
210
+ include_token = bool(tok) and not path.startswith("/v1.0/token")
211
+
212
+ string_to_sign = self._build_string_to_sign(method, path, params, body_str)
213
+ sign_src = cid + (tok if include_token else "") + t + string_to_sign
214
+ sign = self._hmac_sha256_upper(sec, sign_src)
215
+
216
+ hdrs = {
217
+ "client_id": cid,
218
+ "sign": sign,
219
+ "t": t,
220
+ "sign_method": "HMAC-SHA256",
221
+ "Content-Type": "application/json; charset=utf-8",
222
+ "lang": self._lang(),
223
+ }
224
+ if include_token:
225
+ hdrs["access_token"] = tok
226
+ return hdrs
227
+
228
+ def _handle_tuya_response(self, r: requests.Response) -> dict:
229
+ try:
230
+ payload = r.json() if r.content else {}
231
+ except Exception:
232
+ raise RuntimeError(f"HTTP {r.status_code}: Non-JSON response: {r.text}")
233
+
234
+ if r.status_code < 200 or r.status_code >= 300:
235
+ raise RuntimeError(json.dumps({"status": r.status_code, "error": payload}, ensure_ascii=False))
236
+
237
+ if payload.get("success") is False:
238
+ code = payload.get("code")
239
+ msg = payload.get("msg") or payload.get("message")
240
+ # Common sign error codes often: 1010, "sign invalid"
241
+ raise RuntimeError(json.dumps({
242
+ "status": r.status_code,
243
+ "success": False,
244
+ "code": code,
245
+ "error": msg,
246
+ "detail": payload
247
+ }, ensure_ascii=False))
248
+
249
+ return payload
250
+
251
+ def _request(
252
+ self,
253
+ method: str,
254
+ path: str,
255
+ params: Optional[dict | List[Tuple[str, Any]]] = None,
256
+ body: Optional[dict | list] = None,
257
+ require_token: bool = True,
258
+ ) -> dict:
259
+ if not path.startswith("/"):
260
+ path = "/" + path
261
+
262
+ # Don't fetch token for token endpoint
263
+ if require_token and not path.startswith("/v1.0/token"):
264
+ self._ensure_token()
265
+
266
+ url = f"{self._api_base()}{path}"
267
+ params_items = self._sorted_params_list(params)
268
+
269
+ body_str = self._json_canonical(body) if method.upper() in ("POST", "PUT", "PATCH") else ""
270
+ headers = self._headers(method, path, params_items, body_str)
271
+
272
+ # Use data=body_str to ensure the signed body equals the sent body
273
+ if method.upper() == "GET":
274
+ r = requests.get(url, headers=headers, params=params_items, timeout=self._timeout())
275
+ elif method.upper() == "POST":
276
+ r = requests.post(url, headers=headers, params=params_items, data=body_str, timeout=self._timeout())
277
+ elif method.upper() == "DELETE":
278
+ r = requests.delete(url, headers=headers, params=params_items, data=body_str, timeout=self._timeout())
279
+ elif method.upper() == "PUT":
280
+ r = requests.put(url, headers=headers, params=params_items, data=body_str, timeout=self._timeout())
281
+ elif method.upper() == "PATCH":
282
+ r = requests.patch(url, headers=headers, params=params_items, data=body_str, timeout=self._timeout())
283
+ else:
284
+ raise RuntimeError(f"Unsupported method: {method}")
285
+
286
+ return self._handle_tuya_response(r)
287
+
288
+ def _get(self, path: str, params: Optional[dict | List[Tuple[str, Any]]] = None) -> dict:
289
+ return self._request("GET", path, params=params, body=None, require_token=not path.startswith("/v1.0/token"))
290
+
291
+ def _post_json(self, path: str, payload: dict | list | None = None, params: Optional[dict | List[Tuple[str, Any]]] = None) -> dict:
292
+ return self._request("POST", path, params=params, body=(payload or {}), require_token=not path.startswith("/v1.0/token"))
293
+
294
+ # ---------------------- Token management ----------------------
295
+
296
+ def _token_get(self) -> dict:
297
+ # signature for GET /v1.0/token?grant_type=1 (no access_token in sign)
298
+ res = self._request("GET", "/v1.0/token", params={"grant_type": "1"}, body=None, require_token=False)
299
+ result = res.get("result") or {}
300
+ access_token = (result.get("access_token") or "").strip()
301
+ expire_time = int(result.get("expire_time") or 0)
302
+ if not access_token:
303
+ raise RuntimeError("Token retrieval failed: missing access_token.")
304
+
305
+ exp_at = int(time.time()) + max(0, expire_time - 60)
306
+ self.plugin.set_option_value("tuya_access_token", access_token)
307
+ self.plugin.set_option_value("tuya_token_expires_in", str(expire_time))
308
+ self.plugin.set_option_value("tuya_token_expire_at", str(exp_at))
309
+ if result.get("refresh_token"):
310
+ self.plugin.set_option_value("tuya_refresh_token", result["refresh_token"])
311
+ return res
312
+
313
+ # ---------------------- Device helpers ----------------------
314
+
315
+ def _get_uid(self, p: dict) -> str:
316
+ uid = (p.get("uid") or self.plugin.get_option_value("tuya_uid") or "").strip()
317
+ if not uid:
318
+ raise RuntimeError("Missing UID. Set 'tuya_uid' in options or pass 'uid' param.")
319
+ return uid
320
+
321
+ def _device_status(self, device_id: str) -> List[dict]:
322
+ res = self._get(f"/v1.0/devices/{device_id}/status")
323
+ return res.get("result") or res.get("data") or []
324
+
325
+ def _device_info(self, device_id: str) -> dict:
326
+ res = self._get(f"/v1.0/devices/{device_id}")
327
+ return res.get("result") or res.get("data") or {}
328
+
329
+ def _device_functions(self, device_id: str) -> List[dict]:
330
+ res = self._get(f"/v1.0/devices/{device_id}/functions")
331
+ data = res.get("result") or res.get("data") or {}
332
+ funcs = data.get("functions") or data.get("result") or data
333
+ if isinstance(funcs, dict):
334
+ funcs = funcs.get("functions") or []
335
+ if not isinstance(funcs, list):
336
+ funcs = []
337
+ return funcs
338
+
339
+ def _guess_switch_code(self, device_id: str) -> str:
340
+ candidates = [
341
+ "switch", "switch_main", "switch_led", "power_switch",
342
+ "switch_1", "switch_2", "switch_3", "switch_usb1", "on_off",
343
+ "master_switch", "relay_switch", "socket1"
344
+ ]
345
+ try:
346
+ funcs = self._device_functions(device_id)
347
+ codes = [f.get("code") for f in funcs if isinstance(f, dict)]
348
+ for c in candidates:
349
+ if c in codes:
350
+ return c
351
+ except Exception:
352
+ pass
353
+ try:
354
+ status = self._device_status(device_id)
355
+ codes = [s.get("code") for s in status if isinstance(s, dict) and isinstance(s.get("value"), bool)]
356
+ for c in candidates:
357
+ if c in codes:
358
+ return c
359
+ if codes:
360
+ return codes[0]
361
+ except Exception:
362
+ pass
363
+ return "switch"
364
+
365
+ def _build_commands(self, mapping: Dict[str, Any]) -> List[dict]:
366
+ return [{"code": k, "value": v} for k, v in mapping.items()]
367
+
368
+ def _device_send_commands(self, device_id: str, commands: List[dict]) -> dict:
369
+ payload = {"commands": commands}
370
+ res = self._post_json(f"/v1.0/devices/{device_id}/commands", payload=payload)
371
+ return res
372
+
373
+ def _toggle_state(self, device_id: str, code: Optional[str] = None) -> Tuple[str, Optional[bool]]:
374
+ code = code or self._guess_switch_code(device_id)
375
+ status = self._device_status(device_id)
376
+ current = None
377
+ for s in status:
378
+ if s.get("code") == code and isinstance(s.get("value"), bool):
379
+ current = bool(s["value"])
380
+ break
381
+ if current is None:
382
+ for s in status:
383
+ if isinstance(s.get("value"), bool):
384
+ code = s["code"]
385
+ current = bool(s["value"])
386
+ break
387
+ return code, current
388
+
389
+ def _normalize_sensors(self, status_list: List[dict]) -> dict:
390
+ res = {"raw": status_list, "normalized": {}}
391
+ idx = {s.get("code"): s.get("value") for s in status_list if isinstance(s, dict) and "code" in s}
392
+ norm = {}
393
+
394
+ def deki(v):
395
+ try:
396
+ if isinstance(v, (int, float)):
397
+ return round(v / 10.0, 1) if abs(v) < 1000 else v
398
+ except Exception:
399
+ pass
400
+ return v
401
+
402
+ for c in ["temp_current", "temperature", "temp_value", "va_temperature", "temp_cur"]:
403
+ if c in idx:
404
+ norm["temperature_c"] = deki(idx[c])
405
+ break
406
+ for c in ["humidity_value", "humidity", "va_humidity", "hum_value"]:
407
+ if c in idx:
408
+ norm["humidity_pct"] = deki(idx[c])
409
+ break
410
+ for c in ["co2", "co2_value"]:
411
+ if c in idx:
412
+ norm["co2_ppm"] = idx[c]
413
+ break
414
+ for c in ["pm25", "pm2p5", "pm2_5"]:
415
+ if c in idx:
416
+ norm["pm25_ugm3"] = idx[c]
417
+ break
418
+ for c in ["pm10", "pm_10"]:
419
+ if c in idx:
420
+ norm["pm10_ugm3"] = idx[c]
421
+ break
422
+ for c in ["illumination", "illumination_value", "lux", "bright_value"]:
423
+ if c in idx:
424
+ norm["illuminance_lux"] = idx[c]
425
+ break
426
+ for c in ["battery", "battery_percentage", "battery_state"]:
427
+ if c in idx:
428
+ norm["battery_pct_or_state"] = idx[c]
429
+ break
430
+
431
+ res["normalized"] = norm
432
+ return res
433
+
434
+ # ---------------------- Commands: Auth ----------------------
435
+
436
+ def cmd_tuya_set_keys(self, item: dict) -> dict:
437
+ p = item.get("params", {})
438
+ cid = (p.get("client_id") or "").strip()
439
+ sec = (p.get("client_secret") or "").strip()
440
+ if not cid or not sec:
441
+ return self.make_response(item, "Params 'client_id' and 'client_secret' required")
442
+ self.plugin.set_option_value("tuya_client_id", cid)
443
+ self.plugin.set_option_value("tuya_client_secret", sec)
444
+ return self.make_response(item, {"ok": True})
445
+
446
+ def cmd_tuya_set_uid(self, item: dict) -> dict:
447
+ p = item.get("params", {})
448
+ uid = (p.get("uid") or "").strip()
449
+ if not uid:
450
+ return self.make_response(item, "Param 'uid' required")
451
+ self.plugin.set_option_value("tuya_uid", uid)
452
+ return self.make_response(item, {"ok": True})
453
+
454
+ def cmd_tuya_token_get(self, item: dict) -> dict:
455
+ res = self._token_get()
456
+ return self.make_response(item, res)
457
+
458
+ # ---------------------- Commands: Devices / Info ----------------------
459
+
460
+ def cmd_tuya_devices_list(self, item: dict) -> dict:
461
+ p = item.get("params", {})
462
+ uid = self._get_uid(p)
463
+ page_no = int(p.get("page_no") or 1)
464
+ page_size = int(p.get("page_size") or 100)
465
+ params = {"page_no": page_no, "page_size": page_size}
466
+ res = self._get(f"/v1.0/users/{uid}/devices", params=params)
467
+ try:
468
+ devices = (res.get("result") or {}).get("devices") or res.get("result") or []
469
+ self.plugin.set_option_value("tuya_cached_devices", json.dumps(devices, ensure_ascii=False))
470
+ except Exception:
471
+ pass
472
+ return self.make_response(item, res)
473
+
474
+ def cmd_tuya_device_get(self, item: dict) -> dict:
475
+ p = item.get("params", {})
476
+ device_id = p.get("device_id")
477
+ if not device_id:
478
+ return self.make_response(item, "Param 'device_id' required")
479
+ res = self._get(f"/v1.0/devices/{device_id}")
480
+ return self.make_response(item, res)
481
+
482
+ def cmd_tuya_device_status(self, item: dict) -> dict:
483
+ p = item.get("params", {})
484
+ device_id = p.get("device_id")
485
+ if not device_id:
486
+ return self.make_response(item, "Param 'device_id' required")
487
+ res = self._get(f"/v1.0/devices/{device_id}/status")
488
+ return self.make_response(item, res)
489
+
490
+ def cmd_tuya_device_functions(self, item: dict) -> dict:
491
+ p = item.get("params", {})
492
+ device_id = p.get("device_id")
493
+ if not device_id:
494
+ return self.make_response(item, "Param 'device_id' required")
495
+ res = self._get(f"/v1.0/devices/{device_id}/functions")
496
+ return self.make_response(item, res)
497
+
498
+ def cmd_tuya_find_device(self, item: dict) -> dict:
499
+ p = item.get("params", {})
500
+ name = (p.get("name") or "").strip().lower()
501
+ if not name:
502
+ return self.make_response(item, "Param 'name' required")
503
+ try:
504
+ cached = self.plugin.get_option_value("tuya_cached_devices") or "[]"
505
+ devices = json.loads(cached)
506
+ except Exception:
507
+ devices = []
508
+ found = []
509
+ for d in devices:
510
+ n = str(d.get("name") or "").lower()
511
+ if name in n:
512
+ found.append(d)
513
+ return self.make_response(item, {"matches": found})
514
+
515
+ # ---------------------- Commands: Control ----------------------
516
+
517
+ def cmd_tuya_device_set(self, item: dict) -> dict:
518
+ p = item.get("params", {})
519
+ device_id = p.get("device_id")
520
+ code = p.get("code")
521
+ value = p.get("value")
522
+ codes_map = p.get("codes")
523
+ if not device_id:
524
+ return self.make_response(item, "Param 'device_id' required")
525
+
526
+ if codes_map and isinstance(codes_map, dict):
527
+ cmds = self._build_commands(codes_map)
528
+ elif code is not None:
529
+ cmds = [{"code": code, "value": value}]
530
+ else:
531
+ return self.make_response(item, "Provide 'code'+'value' or 'codes' dict")
532
+
533
+ res = self._device_send_commands(device_id, cmds)
534
+ return self.make_response(item, res)
535
+
536
+ def cmd_tuya_device_send(self, item: dict) -> dict:
537
+ p = item.get("params", {})
538
+ device_id = p.get("device_id")
539
+ commands = p.get("commands")
540
+ if not device_id:
541
+ return self.make_response(item, "Param 'device_id' required")
542
+ if not isinstance(commands, list) or not commands:
543
+ return self.make_response(item, "Param 'commands' must be a non-empty list of {code,value}")
544
+ res = self._device_send_commands(device_id, commands)
545
+ return self.make_response(item, res)
546
+
547
+ def cmd_tuya_device_on(self, item: dict) -> dict:
548
+ p = item.get("params", {})
549
+ device_id = p.get("device_id")
550
+ code = p.get("code")
551
+ if not device_id:
552
+ return self.make_response(item, "Param 'device_id' required")
553
+ code = code or self._guess_switch_code(device_id)
554
+ res = self._device_send_commands(device_id, [{"code": code, "value": True}])
555
+ return self.make_response(item, res)
556
+
557
+ def cmd_tuya_device_off(self, item: dict) -> dict:
558
+ p = item.get("params", {})
559
+ device_id = p.get("device_id")
560
+ code = p.get("code")
561
+ if not device_id:
562
+ return self.make_response(item, "Param 'device_id' required")
563
+ code = code or self._guess_switch_code(device_id)
564
+ res = self._device_send_commands(device_id, [{"code": code, "value": False}])
565
+ return self.make_response(item, res)
566
+
567
+ def cmd_tuya_device_toggle(self, item: dict) -> dict:
568
+ p = item.get("params", {})
569
+ device_id = p.get("device_id")
570
+ code = p.get("code")
571
+ if not device_id:
572
+ return self.make_response(item, "Param 'device_id' required")
573
+ code, current = self._toggle_state(device_id, code=code)
574
+ if current is None:
575
+ return self.make_response(item, f"Unable to determine current state for device '{device_id}'. Provide 'code' or use tuya_device_on/off.")
576
+ res = self._device_send_commands(device_id, [{"code": code, "value": not current}])
577
+ return self.make_response(item, {"toggled": True, "code": code, "from": current, "to": not current, "result": res})
578
+
579
+ # ---------------------- Commands: Sensors ----------------------
580
+
581
+ def cmd_tuya_sensors_read(self, item: dict) -> dict:
582
+ p = item.get("params", {})
583
+ device_id = p.get("device_id")
584
+ if not device_id:
585
+ return self.make_response(item, "Param 'device_id' required")
586
+ status = self._device_status(device_id)
587
+ norm = self._normalize_sensors(status)
588
+ return self.make_response(item, norm)
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.08.27 20:00:00 #
10
+ # ================================================== #
11
+
12
+ from .plugin import *