difonlib 0.2.2__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.
difonlib/tuya_devs.py ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python
2
+
3
+ # 24.07.25
4
+
5
+ from typing import Any, Optional, Dict, cast, List
6
+ import xmltodict # pip install xmltodict
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import tinytuya
11
+ from tinytuya import Contrib
12
+
13
+ # from typing import Dict, Optional, cast, Any, Callable, Type, LiteralString
14
+ # import ctypes
15
+
16
+ ### export PYTHONPATH=$HOME/tips/devel/python/pymylib
17
+ from difonlib.utils import print_dicts
18
+
19
+ cur_dir = os.path.dirname(os.path.realpath(__file__))
20
+
21
+ # {'@name': 's_home_data23007434', '#text': '{"deviceRespBeen":[{"activeTime":1752406424,"devId":"bf36796bdace7a62fav1ys","displayOrder":0,"dpMaxTime":1752406939811,"dpName":{},"dps":{"1":true,"17":0,"18":0,"19":0,"20":2328,"21":1,"22":578,"23":29500,"24":15873,"25":2620,"26":0,"38":"memory","39":false,"40":"relay","41":false,"42":"","43":"","44":"","9":0},"errorCode":0,"iconUrl":"https://images.tuyaeu.com/smart/program_category_icon/cz.png","isShare":false,"key":"bf36796bdace7a62fav1ys","lat":"32.094","localKey":"lXeI5[a5-~F}MQAi","lon":"34.8863","moduleMap":{"mcu":{"cadv":"","isOnline":true,"verSw":"1.1.8"},"wifi":{"bv":"40.00","cadv":"1.0.3","isOnline":true,"pv":"2.2","verSw":"1.1.8"}},"name":"Outlet-20A-2","productId":"qm0iq4nqnrlzh4qc","resptime":0,"runtimeEnv":"prod","timezoneId":"Asia/Jerusalem","uuid":"6b789696c51d698a","virtual":false},
22
+
23
+ tuya_xml_data_file = Path(
24
+ os.path.join(
25
+ cur_dir,
26
+ "./docker-android/shared_prefs/preferences_global_keyeu1609443890901OWMrJ.xml",
27
+ )
28
+ )
29
+
30
+
31
+ class TuyaDevs:
32
+ def __init__(
33
+ self,
34
+ xml_file: Path = tuya_xml_data_file,
35
+ file_scan_result: str = "snapshot.json",
36
+ ):
37
+ self.fscan_result = file_scan_result
38
+ self.xml_file = xml_file
39
+ self.devs_cfg = self.load_cfg()
40
+
41
+ def load_cfg(self) -> List[Dict[str, Any]]:
42
+ """Return list of tuya devices as list of dictionaries"""
43
+ with open(self.xml_file) as fxml:
44
+ data_dict = xmltodict.parse(fxml.read())
45
+ txt = data_dict["map"]["string"][3]["#text"]
46
+ devices = json.loads(txt)["deviceRespBeen"]
47
+ return cast(List[Dict[str, Any]], devices)
48
+
49
+ def get_localkey(self, id: str) -> Optional[str]:
50
+ """Get localKey by device ID (key)"""
51
+ for dev in self.devs_cfg:
52
+ # print_dicts(dev)
53
+ if not dev["localKey"]:
54
+ continue
55
+ if dev["key"] == id:
56
+ return cast(str, dev["localKey"])
57
+ return None
58
+
59
+ def get_dev(self, id: str) -> Optional[Dict[str, Any]]:
60
+ """Return device config by ID, or None if not found."""
61
+ for dev in self.devs_cfg:
62
+ if dev["key"] == id:
63
+ return dev
64
+ return None
65
+
66
+ # def _scan(self, force_update=False) -> Optional[Dict[str, str]]:
67
+ def _scan(self, force_update: bool = False) -> dict:
68
+ """Get last list of connected devices
69
+ If force_update=True - Get connected devives for now
70
+ """
71
+ if not os.path.isfile(self.fscan_result) or force_update:
72
+ tinytuya.scan()
73
+ with open(self.fscan_result) as f:
74
+ scan_data: dict = json.load(f)
75
+ return cast(dict, scan_data["devices"])
76
+
77
+ def connected_devs(self, force_update: bool = False) -> list:
78
+ """
79
+ If force_update=True - get connected devives for now
80
+ if not then last scan of connected devices from self.fscan_result
81
+ """
82
+ con_devs = []
83
+ connected_devices = self._scan(force_update)
84
+ for con_dev in connected_devices:
85
+ dev_id = con_dev["id"]
86
+ dev_ip = con_dev["ip"]
87
+ dev_descript = self.get_dev(dev_id)
88
+ dev_name = None
89
+ dev_lk = None
90
+ if dev_descript:
91
+ dev_name = dev_descript["name"]
92
+ dev_lk = dev_descript["localKey"]
93
+ con_devs += [{"id": dev_id, "ip": dev_ip, "name": dev_name, "localkey": dev_lk}]
94
+ return con_devs
95
+
96
+ def all_devs(self) -> list:
97
+ """
98
+ If force_update=True - get connected devives for now
99
+ if not then last scan of connected devices from self.fscan_result
100
+ """
101
+ all_devs = []
102
+ for con_dev in self.devs_cfg:
103
+ dev_id = con_dev["devId"]
104
+ dev_descript = self.get_dev(dev_id)
105
+ dev_name = None
106
+ dev_lk = None
107
+ if dev_descript:
108
+ dev_name = dev_descript["name"]
109
+ dev_lk = dev_descript["localKey"]
110
+ all_devs += [{"id": dev_id, "name": dev_name, "localkey": dev_lk}]
111
+ return all_devs
112
+
113
+ def ir_connect_to_dev(
114
+ self, dev_id: str, local_key: str
115
+ ) -> Optional[Contrib.IRRemoteControlDevice]:
116
+ try:
117
+ ir_dev = Contrib.IRRemoteControlDevice(
118
+ dev_id=dev_id,
119
+ # address = '192.168.0.89', #- no must
120
+ local_key=local_key,
121
+ persist=True,
122
+ connection_timeout=5,
123
+ )
124
+ except Exception as e:
125
+ print(f" =!= ir_connect_to_dev(): {e}")
126
+ return None
127
+ return ir_dev
128
+
129
+ # learn a new IR button key
130
+ def ir_receive_button(
131
+ self, ir_dev: Optional[Contrib.IRRemoteControlDevice], timeout: int = 20
132
+ ) -> Optional[str]:
133
+ if not ir_dev:
134
+ print(f" =!= IR device is not connected! ir_dev:{ir_dev}")
135
+ return None
136
+ print("Press button on your remote control")
137
+ button = ir_dev.receive_button(timeout=timeout)
138
+ if isinstance(button, str):
139
+ return button
140
+ return None
141
+
142
+
143
+ dbg = print
144
+ if __name__ == "__main__":
145
+
146
+ # devs_list = tuya_xml_cfg_get_data(tuya_xml_data_file)
147
+
148
+ # from pymylib import print_dicts
149
+
150
+ # for i, dev in enumerate(devs_list, start=1):
151
+ # # print(f" {i}) {dev}")
152
+ # print(f"----------------------- {i} --------------------------")
153
+ # print_dicts(dev)
154
+ # print(f"-----------------------------------------------------")
155
+
156
+ # to = TuyaDevsData(xml_file=tuya_xml_data_file)
157
+ td = TuyaDevs(xml_file=tuya_xml_data_file)
158
+
159
+ for i, _dev in enumerate(td.devs_cfg, start=1):
160
+ # print(f" {i}) {dev}")
161
+ print(f"----------------------- {i} --------------------------")
162
+ print_dicts(_dev)
163
+ print("-----------------------------------------------------")
164
+
165
+ dev_id = "bf04409288bdad3dd5dx35"
166
+
167
+ dev = td.get_dev(dev_id)
168
+ dbg(f"dev: {dev}") # //Dima
169
+
170
+ localkey = td.get_localkey(dev_id)
171
+ dbg(f"dev_id: {dev_id}; localkey: {localkey}") # //Dima
172
+
173
+ dev_id = "bf54140ad95255549f5d2h"
174
+ localkey = td.get_localkey(dev_id)
175
+ dbg(f"dev_id: {dev_id}; localkey: {localkey}") # //Dima
176
+
177
+ all_devs = td.all_devs()
178
+ for i, dev in enumerate(all_devs, start=1):
179
+ # print(f" {i}) {dev}")
180
+ print(f"----------------------- {i} --------------------------")
181
+ print_dicts(dev)
182
+ print("-----------------------------------------------------")
183
+
184
+ con_devs = td.connected_devs()
185
+ for i, dev in enumerate(con_devs, start=1):
186
+ # print(f" {i}) {dev}")
187
+ print(f"----------------------- {i} --------------------------")
188
+ print_dicts(dev)
189
+ print("-----------------------------------------------------")
190
+
191
+ # con_devs = td.tuya_devs(force_update=True)
192
+ # for i, dev in enumerate(con_devs, start=1):
193
+ # # print(f" {i}) {dev}")
194
+ # print(f"----------------------- {i} --------------------------")
195
+ # print_dicts(dev)
196
+ # print(f"-----------------------------------------------------")
difonlib/utils.py ADDED
@@ -0,0 +1,200 @@
1
+ import logging
2
+ import re
3
+ import inspect
4
+ import struct
5
+ import os
6
+ import yaml
7
+ import shutil
8
+ import glob
9
+ from typing import Any, Callable, Optional, cast, List
10
+ from pathlib import Path
11
+
12
+ # Text print style
13
+ RESET = 0
14
+ BOLD = 1
15
+ DARKEN = 2
16
+ ITALIC = 3
17
+ UNDERLINE = 4
18
+ BLINK_SLOW = 5
19
+ BLINK_FAST = 6
20
+ REVERSE = 7
21
+ HIDE = 8
22
+ CROSS_OUT = 9
23
+
24
+ # alias color_table2='bash -c "for (( i=1; i<256; i++ )); do tput setaf \$i; echo -n [\$i]; done; tput sgr0; echo"'
25
+
26
+ COLOR_OFF = "\x1b[0m"
27
+
28
+ # ---
29
+ RED = 160
30
+ GOLD = 222
31
+
32
+
33
+ # MSG_COLOR = f"\x1b[{BOLD};38;5;{RED}m"
34
+ MSG_COLOR = f"\x1b[{RESET};38;5;{45}m"
35
+ # CRITICAL 50
36
+ # ERROR 40
37
+ # WARNING 30
38
+ # INFO 20
39
+ # DEBUG 10
40
+ # NOTSET 0
41
+
42
+ logging.basicConfig(
43
+ format=f"{MSG_COLOR}[%(filename)s:%(lineno)d]: %(message)s{COLOR_OFF}",
44
+ level=logging.DEBUG,
45
+ )
46
+ logdbg = logging.debug
47
+
48
+
49
+ class UtilsError(Exception):
50
+ def __init__(self, message: str) -> None:
51
+ super().__init__(message)
52
+
53
+
54
+ def __LINE__() -> int:
55
+ return inspect.stack()[1][2]
56
+
57
+
58
+ def is_mac_address(mac: str) -> bool:
59
+ """
60
+ Validates a MAC address string.
61
+ Acceptable formats: XX:XX:XX:XX:XX:XX (case-insensitive)
62
+ """
63
+ mac_regex = re.compile(r"^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$")
64
+ return bool(mac_regex.match(mac))
65
+
66
+
67
+ def is_mac_address2(mac: str) -> bool:
68
+ """
69
+ Validates a MAC address string.
70
+ Acceptable formats: XXXXXXXXXXXX (case-insensitive)
71
+ """
72
+ mac_regex = re.compile(r"^([0-9A-Fa-f]{2}){6}$")
73
+ return bool(mac_regex.match(mac))
74
+
75
+
76
+ def mac_format(mac: str) -> str:
77
+ """112233445566 -> 11:22:33:44:55:66"""
78
+ _mac = re.findall("[0-9A-Fa-f]{2}", mac)
79
+ if len(mac) != 12 or len(_mac) != 6:
80
+ raise UtilsError(f"Invalid MAC address: {mac}")
81
+ return ":".join(_mac)
82
+
83
+
84
+ def to_signed(number: int, bits: int = 32) -> int:
85
+ mask: int = (2**bits) - 1
86
+ if number & (1 << (bits - 1)):
87
+ return number | ~mask
88
+ else:
89
+ return number & mask
90
+
91
+
92
+ def swap32(i: int) -> int:
93
+ return cast(int, struct.unpack("<I", struct.pack(">I", i))[0])
94
+
95
+
96
+ def swap16(i: int) -> int:
97
+ return int(struct.unpack("<H", struct.pack(">H", i))[0])
98
+
99
+
100
+ def fs_remove_dir_content(dir_path: str) -> None:
101
+ shutil.rmtree(dir_path)
102
+ os.mkdir(dir_path)
103
+
104
+
105
+ def file_get_latest(path: str, fpatern: str) -> Optional[str]:
106
+ files = glob.glob(os.path.join(path, fpatern)) # * means all if need specific format then *.csv
107
+ if not files:
108
+ return None
109
+ latest_file = max(files, key=os.path.getmtime) # modify time
110
+ return latest_file
111
+
112
+
113
+ def print_lists(lst: list, shift: str = " ") -> None:
114
+ for v in lst:
115
+ if isinstance(v, list):
116
+ print_lists(v, shift=shift + " ")
117
+ else:
118
+ print(shift, v)
119
+
120
+
121
+ # import configparser
122
+ def print_dicts(dic: dict, shift: str = " ") -> None:
123
+ # Get max length of key line
124
+ klen = 0
125
+ for k in dic.keys():
126
+ kl = len(str(k))
127
+ if kl > klen:
128
+ klen = kl
129
+ for k, v in dic.items():
130
+ # print(f"**v: {type(v)}")
131
+ # if isinstance(v, (dict, configparser.SectionProxy)):
132
+ if isinstance(v, (dict)):
133
+ print(shift, k, ":")
134
+ print_dicts(v, shift=shift + " ")
135
+ else:
136
+ print(shift, f"{k:{klen}} : {v}")
137
+
138
+
139
+ def print_dicts_list(dicts_list: List[dict]) -> None:
140
+ for d in dicts_list:
141
+ print_dicts(d)
142
+ print("------------------------")
143
+
144
+
145
+ def print_ctype_fields(
146
+ ctype_struct: Any,
147
+ indent: str = " ",
148
+ show_hex: bool = False,
149
+ logger: Callable[[str], None] = print,
150
+ ) -> None:
151
+ alen = 0
152
+ for field_name, field_type in ctype_struct._fields_:
153
+ a = len(str(field_name))
154
+ if a > alen:
155
+ alen = a
156
+ for field_name, field_type in ctype_struct._fields_:
157
+ try:
158
+ if show_hex:
159
+ logger(f"{indent}{field_name:{alen}}: 0x{getattr(ctype_struct, field_name):X}")
160
+ else:
161
+ logger(f"{indent}{field_name:{alen}}: {getattr(ctype_struct, field_name):d}")
162
+ except Exception:
163
+ logger(f"{indent}{field_name}:")
164
+ print_ctype_fields(
165
+ getattr(ctype_struct, field_name),
166
+ indent=(indent + " "),
167
+ show_hex=show_hex,
168
+ logger=logger,
169
+ )
170
+
171
+
172
+ class YamlConfig:
173
+ def __init__(self, cfg_path: str):
174
+ self.config_path = Path(cfg_path)
175
+ self.config: dict = {}
176
+ self.load()
177
+
178
+ def clear(self) -> None:
179
+ self.config = {}
180
+ self.save()
181
+
182
+ def load(self) -> None:
183
+ if not self.config_path.exists():
184
+ self.save()
185
+ with self.config_path.open() as f:
186
+ self.config = yaml.safe_load(f) or {}
187
+
188
+ def save(self) -> None:
189
+ with self.config_path.open("w") as f:
190
+ yaml.safe_dump(
191
+ self.config,
192
+ f,
193
+ sort_keys=False,
194
+ allow_unicode=True,
195
+ )
196
+
197
+
198
+ if __name__ == "__main__":
199
+ logdbg("Hello 0123456789 ABCDEF")
200
+ cfg = YamlConfig("./___CONFIG.yaml")
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: difonlib
3
+ Version: 0.2.2
4
+ Summary: python libraries
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: evdev>=1.9.2
8
+ Requires-Dist: nicegui>=3.2.0
9
+ Requires-Dist: pexpect>=4.9.0
10
+ Requires-Dist: pyyaml>=6.0.3
11
+ Requires-Dist: tinytuya>=1.17.4
12
+ Requires-Dist: types-pexpect>=4.9.0.20250916
13
+ Requires-Dist: types-PyYAML>=6.0.12.20250915
14
+ Requires-Dist: types-xmltodict>=1.0.1.20250920
15
+ Requires-Dist: xmltodict>=1.0.2
16
+
17
+ # Python libraries
@@ -0,0 +1,12 @@
1
+ difonlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ difonlib/bt_utils.py,sha256=Cv5wOhkT4s5-NCzSCtCjwh4qjKo_Blrt2ED0jHcsDEs,16949
3
+ difonlib/input_devs.py,sha256=zi3hvHAMV-NZSWkbw9EGQSGz-MXULkvH3koLDNU0xo8,6424
4
+ difonlib/ng_lib.py,sha256=mEaJ86fwU1mzoEIJnh5qbsi_uITtD0YwwqvWakcDbik,6077
5
+ difonlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ difonlib/remctrl.py,sha256=KfhePrdFR9eSUlUahVAGQTh4tL2qnws0BgU4epLUA0o,5228
7
+ difonlib/tuya_devs.py,sha256=qTehh2OjJy57JI2TgfcDaRHWCb8hjZw2fDw6og3dtlU,7530
8
+ difonlib/utils.py,sha256=gTaXeuTJfFAX4WW9d13DDCdM05V1SYHfNM2w67gu0fk,4992
9
+ difonlib-0.2.2.dist-info/METADATA,sha256=yjMWhu07P-fnmjxlBPj18-ersFGi6TWwXo7yFhbmMqw,480
10
+ difonlib-0.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ difonlib-0.2.2.dist-info/top_level.txt,sha256=FDlpwQactDYUPor1ivSHo93-p1QQo-cYHNnXxCs_ECE,9
12
+ difonlib-0.2.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ difonlib