homebridge-aiseg-awning-window-command 0.2.3 → 0.2.4
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.
- package/package.json +3 -2
- package/scripts/aiseg_device.py +332 -0
- package/scripts/aiseg_window.py +4 -266
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-aiseg-awning-window-command",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Homebridge plugin for AiSEG2 awning windows",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"config.schema.json",
|
|
23
23
|
"README.md",
|
|
24
24
|
"LICENSE",
|
|
25
|
-
"scripts/aiseg_window.py"
|
|
25
|
+
"scripts/aiseg_window.py",
|
|
26
|
+
"scripts/aiseg_device.py"
|
|
26
27
|
]
|
|
27
28
|
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import urllib.request
|
|
8
|
+
import urllib.error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PROFILES = {
|
|
12
|
+
"shutter": {
|
|
13
|
+
"status_root": "shutter",
|
|
14
|
+
"default_device_id": "3252",
|
|
15
|
+
"page_extra_name": "page325",
|
|
16
|
+
"page_extra_env": "AISEG_PAGE325",
|
|
17
|
+
"default_page_extra": "1",
|
|
18
|
+
"default_track": "325",
|
|
19
|
+
"default_eoj": "0x026301",
|
|
20
|
+
"default_type": "0x0e",
|
|
21
|
+
"usage": "aiseg_shutter.py nodeId open|close|stop|halfopen|angle1|angle2|angle3|angle4|angle5|angle6|status",
|
|
22
|
+
"actions": {
|
|
23
|
+
"open": "0",
|
|
24
|
+
"close": "1",
|
|
25
|
+
"stop": "2",
|
|
26
|
+
"halfopen": "3",
|
|
27
|
+
"angle1": "4",
|
|
28
|
+
"angle2": "5",
|
|
29
|
+
"angle3": "6",
|
|
30
|
+
"angle4": "7",
|
|
31
|
+
"angle5": "8",
|
|
32
|
+
"angle6": "9",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
"window": {
|
|
36
|
+
"status_root": "sash",
|
|
37
|
+
"default_device_id": "3261",
|
|
38
|
+
"page_extra_name": "page326",
|
|
39
|
+
"page_extra_env": "AISEG_PAGE326",
|
|
40
|
+
"default_page_extra": "1",
|
|
41
|
+
"default_track": "326",
|
|
42
|
+
"default_eoj": "0x026501",
|
|
43
|
+
"default_type": "0x0f",
|
|
44
|
+
"usage": "aiseg_window.py nodeId open|close|stop|angle1|angle2|angle3|status",
|
|
45
|
+
"actions": {
|
|
46
|
+
"open": "0",
|
|
47
|
+
"close": "1",
|
|
48
|
+
"stop": "2",
|
|
49
|
+
"angle1": "3",
|
|
50
|
+
"angle2": "4",
|
|
51
|
+
"angle3": "5",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def getenv_required(name):
|
|
58
|
+
value = os.environ.get(name, "").strip()
|
|
59
|
+
if not value:
|
|
60
|
+
raise RuntimeError(f"{name} is not set")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def getenv(name, default=""):
|
|
65
|
+
return os.environ.get(name, default)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_aiseg_host():
|
|
69
|
+
host = (os.environ.get("AISEG_HOST") or os.environ.get("AISEG_IP") or "").strip()
|
|
70
|
+
if not host:
|
|
71
|
+
raise RuntimeError("AISEG_HOST or AISEG_IP is not set")
|
|
72
|
+
if not host.startswith("http://") and not host.startswith("https://"):
|
|
73
|
+
host = "http://" + host
|
|
74
|
+
return host.rstrip("/")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AiSegContext:
|
|
78
|
+
def __init__(self, kind):
|
|
79
|
+
if kind not in PROFILES:
|
|
80
|
+
raise RuntimeError(f"unknown device kind: {kind}")
|
|
81
|
+
|
|
82
|
+
self.kind = kind
|
|
83
|
+
self.profile = PROFILES[kind]
|
|
84
|
+
|
|
85
|
+
self.host = build_aiseg_host()
|
|
86
|
+
self.user = getenv("AISEG_USER", "aiseg")
|
|
87
|
+
self.password = getenv_required("AISEG_PASSWORD")
|
|
88
|
+
|
|
89
|
+
self.device_id = getenv("AISEG_DEVICE_ID", self.profile["default_device_id"])
|
|
90
|
+
self.page = getenv("AISEG_PAGE", "1")
|
|
91
|
+
self.page_extra_name = self.profile["page_extra_name"]
|
|
92
|
+
self.page_extra = getenv(
|
|
93
|
+
self.profile["page_extra_env"],
|
|
94
|
+
self.profile["default_page_extra"],
|
|
95
|
+
)
|
|
96
|
+
self.track = getenv("AISEG_TRACK", self.profile["default_track"])
|
|
97
|
+
self.request_by_form = getenv("AISEG_REQUEST_BY_FORM", "1")
|
|
98
|
+
self.accept_id = getenv("AISEG_ACCEPT_ID", "65550")
|
|
99
|
+
self.default_eoj = getenv("AISEG_EOJ", self.profile["default_eoj"])
|
|
100
|
+
self.default_type = getenv("AISEG_TYPE", self.profile["default_type"])
|
|
101
|
+
|
|
102
|
+
self.actions = self.profile["actions"]
|
|
103
|
+
self.status_root = self.profile["status_root"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AiSegHttpClient:
|
|
107
|
+
def __init__(self, host, user, password):
|
|
108
|
+
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
|
109
|
+
password_mgr.add_password(None, host, user, password)
|
|
110
|
+
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
|
111
|
+
self.opener = urllib.request.build_opener(auth_handler)
|
|
112
|
+
|
|
113
|
+
def request(self, method, url, data=None, timeout=10):
|
|
114
|
+
headers = {
|
|
115
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
116
|
+
"User-Agent": "Node.js",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
body = None
|
|
120
|
+
if data is not None:
|
|
121
|
+
body = data.encode("utf-8") if isinstance(data, str) else data
|
|
122
|
+
|
|
123
|
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
with self.opener.open(req, timeout=timeout) as response:
|
|
127
|
+
text = response.read().decode("utf-8", errors="replace")
|
|
128
|
+
return response.status, text
|
|
129
|
+
except urllib.error.HTTPError as e:
|
|
130
|
+
text = e.read().decode("utf-8", errors="replace")
|
|
131
|
+
raise RuntimeError(f"HTTP_STATUS={e.code} {text}") from e
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_device_info(ctx, target):
|
|
135
|
+
env_node_id = (
|
|
136
|
+
os.environ.get("AISEG_NODE_ID")
|
|
137
|
+
or os.environ.get("AISEG_ID")
|
|
138
|
+
or os.environ.get("AISEG_UNIQUE_WINDOW_ID")
|
|
139
|
+
or ""
|
|
140
|
+
).strip()
|
|
141
|
+
|
|
142
|
+
node_id = env_node_id or str(target).strip()
|
|
143
|
+
|
|
144
|
+
if not re.fullmatch(r"\d+", node_id):
|
|
145
|
+
raise RuntimeError("AISEG_NODE_ID is not set and argument is not a numeric nodeId")
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"nodeId": node_id,
|
|
149
|
+
"eoj": ctx.default_eoj,
|
|
150
|
+
"type": ctx.default_type,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_device_page_url(ctx, info):
|
|
155
|
+
return (
|
|
156
|
+
f"{ctx.host}/page/devices/device/{ctx.device_id}"
|
|
157
|
+
f"?page={ctx.page}"
|
|
158
|
+
f"&{ctx.page_extra_name}={ctx.page_extra}"
|
|
159
|
+
f"&nodeId={info['nodeId']}"
|
|
160
|
+
f"&eoj={info['eoj']}"
|
|
161
|
+
f"&type={info['type']}"
|
|
162
|
+
f"&track={ctx.track}"
|
|
163
|
+
f"&request_by_form={ctx.request_by_form}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_operation_pu_url(ctx, info):
|
|
168
|
+
return (
|
|
169
|
+
f"{ctx.host}/page/devices/device/{ctx.device_id}/operation_pu"
|
|
170
|
+
f"?page={ctx.page}"
|
|
171
|
+
f"&{ctx.page_extra_name}={ctx.page_extra}"
|
|
172
|
+
f"&nodeId={info['nodeId']}"
|
|
173
|
+
f"&eoj={info['eoj']}"
|
|
174
|
+
f"&type={info['type']}"
|
|
175
|
+
f"&track={ctx.track}"
|
|
176
|
+
f"&request_by_form={ctx.request_by_form}"
|
|
177
|
+
f"&acceptId={ctx.accept_id}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def extract_hidden_values(html):
|
|
182
|
+
return re.findall(
|
|
183
|
+
r'<span class="setting_value" style="display:none;">(.*?)</span>',
|
|
184
|
+
html,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_operation_token(ctx, client, info):
|
|
189
|
+
url = get_operation_pu_url(ctx, info)
|
|
190
|
+
status, text = client.request("GET", url, timeout=10)
|
|
191
|
+
|
|
192
|
+
if status != 200:
|
|
193
|
+
raise RuntimeError(f"operation token取得失敗 HTTP_STATUS={status}")
|
|
194
|
+
|
|
195
|
+
values = extract_hidden_values(text)
|
|
196
|
+
|
|
197
|
+
if len(values) < 2:
|
|
198
|
+
raise RuntimeError(f"operation token取得失敗 values={values}")
|
|
199
|
+
|
|
200
|
+
return values[1]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def convert_open_state(open_state):
|
|
204
|
+
open_state = str(open_state)
|
|
205
|
+
|
|
206
|
+
if open_state == "0x41":
|
|
207
|
+
return "open"
|
|
208
|
+
if open_state == "0x42":
|
|
209
|
+
return "closed"
|
|
210
|
+
if open_state == "0x43":
|
|
211
|
+
return "opening"
|
|
212
|
+
if open_state == "0x44":
|
|
213
|
+
return "closing"
|
|
214
|
+
if open_state == "0x45":
|
|
215
|
+
return "vent"
|
|
216
|
+
return "unknown"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def extract_init_json(html):
|
|
220
|
+
m = re.search(
|
|
221
|
+
r'window\.onload\s*=\s*init\((.*?)\);',
|
|
222
|
+
html,
|
|
223
|
+
re.S,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if not m:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
return m.group(1)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_status(ctx, client, target):
|
|
233
|
+
info = get_device_info(ctx, target)
|
|
234
|
+
url = get_device_page_url(ctx, info)
|
|
235
|
+
|
|
236
|
+
status, text = client.request("GET", url, timeout=10)
|
|
237
|
+
|
|
238
|
+
if status != 200:
|
|
239
|
+
print("unknown")
|
|
240
|
+
print(f"HTTP_STATUS={status}", file=sys.stderr)
|
|
241
|
+
print(text, file=sys.stderr)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
init_json = extract_init_json(text)
|
|
245
|
+
|
|
246
|
+
if not init_json:
|
|
247
|
+
print("unknown")
|
|
248
|
+
print("init JSON not found", file=sys.stderr)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
data = json.loads(init_json)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print("unknown")
|
|
255
|
+
print(f"init JSON parse error: {e}", file=sys.stderr)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
open_state = data.get(ctx.status_root, {}).get("openState", "unknown")
|
|
259
|
+
print(convert_open_state(open_state))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def send_command(ctx, client, target, action):
|
|
263
|
+
if action not in ctx.actions:
|
|
264
|
+
print("unknown action", file=sys.stderr)
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
|
|
267
|
+
info = get_device_info(ctx, target)
|
|
268
|
+
open_value = ctx.actions[action]
|
|
269
|
+
token = get_operation_token(ctx, client, info)
|
|
270
|
+
|
|
271
|
+
payload = (
|
|
272
|
+
"data="
|
|
273
|
+
+ json.dumps(
|
|
274
|
+
{
|
|
275
|
+
"objSendData": json.dumps(
|
|
276
|
+
{
|
|
277
|
+
"nodeId": info["nodeId"],
|
|
278
|
+
"eoj": info["eoj"],
|
|
279
|
+
"type": info["type"],
|
|
280
|
+
"device": {
|
|
281
|
+
"open": open_value,
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
),
|
|
285
|
+
"token": token,
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
url = f"{ctx.host}/action/devices/device/{ctx.device_id}/operation"
|
|
291
|
+
status, text = client.request("POST", url, data=payload, timeout=10)
|
|
292
|
+
|
|
293
|
+
if status < 200 or status >= 300:
|
|
294
|
+
raise RuntimeError(f"command failed HTTP_STATUS={status}")
|
|
295
|
+
|
|
296
|
+
print(text)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def main(default_kind=None):
|
|
300
|
+
args = list(sys.argv[1:])
|
|
301
|
+
|
|
302
|
+
if default_kind is None:
|
|
303
|
+
if len(args) != 3:
|
|
304
|
+
print("usage: aiseg_device.py shutter|window nodeId action", file=sys.stderr)
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
kind = args.pop(0).strip().lower()
|
|
307
|
+
else:
|
|
308
|
+
kind = str(default_kind).strip().lower()
|
|
309
|
+
|
|
310
|
+
if kind not in PROFILES:
|
|
311
|
+
print(f"unknown device kind: {kind}", file=sys.stderr)
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
|
|
314
|
+
if len(args) != 2:
|
|
315
|
+
print(f"usage: {PROFILES[kind]['usage']}", file=sys.stderr)
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
|
|
318
|
+
target = args[0].strip()
|
|
319
|
+
action = args[1].strip().lower()
|
|
320
|
+
|
|
321
|
+
ctx = AiSegContext(kind)
|
|
322
|
+
client = AiSegHttpClient(ctx.host, ctx.user, ctx.password)
|
|
323
|
+
|
|
324
|
+
if action == "status":
|
|
325
|
+
get_status(ctx, client, target)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
send_command(ctx, client, target, action)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|
package/scripts/aiseg_window.py
CHANGED
|
@@ -1,273 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
import sys
|
|
4
|
-
import os
|
|
5
|
-
import json
|
|
6
|
-
import re
|
|
7
|
-
import urllib.request
|
|
8
|
-
import urllib.error
|
|
9
5
|
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def getenv_required(name):
|
|
15
|
-
value = os.environ.get(name, "").strip()
|
|
16
|
-
if not value:
|
|
17
|
-
raise RuntimeError(f"{name} is not set")
|
|
18
|
-
return value
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def getenv(name, default=""):
|
|
22
|
-
return os.environ.get(name, default)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def build_aiseg_host():
|
|
26
|
-
host = (os.environ.get("AISEG_HOST") or os.environ.get("AISEG_IP") or "").strip()
|
|
27
|
-
if not host:
|
|
28
|
-
raise RuntimeError("AISEG_HOST or AISEG_IP is not set")
|
|
29
|
-
if not host.startswith("http://") and not host.startswith("https://"):
|
|
30
|
-
host = "http://" + host
|
|
31
|
-
return host.rstrip("/")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
AISEG_HOST = build_aiseg_host()
|
|
35
|
-
PASSWORD = getenv_required("AISEG_PASSWORD")
|
|
36
|
-
|
|
37
|
-
DEVICE_ID = getenv("AISEG_DEVICE_ID", "3261")
|
|
38
|
-
|
|
39
|
-
PAGE = getenv("AISEG_PAGE", "1")
|
|
40
|
-
PAGE326 = getenv("AISEG_PAGE326", "1")
|
|
41
|
-
TRACK = getenv("AISEG_TRACK", "326")
|
|
42
|
-
REQUEST_BY_FORM = getenv("AISEG_REQUEST_BY_FORM", "1")
|
|
43
|
-
ACCEPT_ID = getenv("AISEG_ACCEPT_ID", "65550")
|
|
44
|
-
|
|
45
|
-
DEFAULT_EOJ = getenv("AISEG_EOJ", "0x026501")
|
|
46
|
-
DEFAULT_TYPE = getenv("AISEG_TYPE", "0x0f")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
ACTIONS = {
|
|
50
|
-
"open": "0",
|
|
51
|
-
"close": "1",
|
|
52
|
-
"stop": "2",
|
|
53
|
-
"angle1": "3",
|
|
54
|
-
"angle2": "4",
|
|
55
|
-
"angle3": "5",
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class AiSegHttpClient:
|
|
60
|
-
def __init__(self, host, user, password):
|
|
61
|
-
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
|
62
|
-
password_mgr.add_password(None, host, user, password)
|
|
63
|
-
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
|
|
64
|
-
self.opener = urllib.request.build_opener(auth_handler)
|
|
65
|
-
|
|
66
|
-
def request(self, method, url, data=None, timeout=10):
|
|
67
|
-
headers = {
|
|
68
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
69
|
-
"User-Agent": "Node.js",
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
body = None
|
|
73
|
-
if data is not None:
|
|
74
|
-
body = data.encode("utf-8") if isinstance(data, str) else data
|
|
75
|
-
|
|
76
|
-
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
with self.opener.open(req, timeout=timeout) as response:
|
|
80
|
-
text = response.read().decode("utf-8", errors="replace")
|
|
81
|
-
return response.status, text
|
|
82
|
-
except urllib.error.HTTPError as e:
|
|
83
|
-
text = e.read().decode("utf-8", errors="replace")
|
|
84
|
-
raise RuntimeError(f"HTTP_STATUS={e.code} {text}") from e
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def get_window_info(window):
|
|
88
|
-
env_node_id = (
|
|
89
|
-
os.environ.get("AISEG_NODE_ID")
|
|
90
|
-
or os.environ.get("AISEG_ID")
|
|
91
|
-
or os.environ.get("AISEG_UNIQUE_WINDOW_ID")
|
|
92
|
-
or ""
|
|
93
|
-
).strip()
|
|
94
|
-
|
|
95
|
-
node_id = env_node_id or str(window).strip()
|
|
96
|
-
|
|
97
|
-
if not re.fullmatch(r"\d+", node_id):
|
|
98
|
-
raise RuntimeError("AISEG_NODE_ID is not set and argument is not a numeric nodeId")
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
"nodeId": node_id,
|
|
102
|
-
"eoj": DEFAULT_EOJ,
|
|
103
|
-
"type": DEFAULT_TYPE,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def get_device_page_url(info):
|
|
108
|
-
return (
|
|
109
|
-
f"{AISEG_HOST}/page/devices/device/{DEVICE_ID}"
|
|
110
|
-
f"?page={PAGE}"
|
|
111
|
-
f"&page326={PAGE326}"
|
|
112
|
-
f"&nodeId={info['nodeId']}"
|
|
113
|
-
f"&eoj={info['eoj']}"
|
|
114
|
-
f"&type={info['type']}"
|
|
115
|
-
f"&track={TRACK}"
|
|
116
|
-
f"&request_by_form={REQUEST_BY_FORM}"
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def get_operation_pu_url(info):
|
|
121
|
-
return (
|
|
122
|
-
f"{AISEG_HOST}/page/devices/device/{DEVICE_ID}/operation_pu"
|
|
123
|
-
f"?page={PAGE}"
|
|
124
|
-
f"&page326={PAGE326}"
|
|
125
|
-
f"&nodeId={info['nodeId']}"
|
|
126
|
-
f"&eoj={info['eoj']}"
|
|
127
|
-
f"&type={info['type']}"
|
|
128
|
-
f"&track={TRACK}"
|
|
129
|
-
f"&request_by_form={REQUEST_BY_FORM}"
|
|
130
|
-
f"&acceptId={ACCEPT_ID}"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def extract_hidden_values(html):
|
|
135
|
-
return re.findall(
|
|
136
|
-
r'<span class="setting_value" style="display:none;">(.*?)</span>',
|
|
137
|
-
html,
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def get_operation_token(client, info):
|
|
142
|
-
url = get_operation_pu_url(info)
|
|
143
|
-
status, text = client.request("GET", url, timeout=10)
|
|
144
|
-
|
|
145
|
-
if status != 200:
|
|
146
|
-
raise RuntimeError(f"operation token取得失敗 HTTP_STATUS={status}")
|
|
147
|
-
|
|
148
|
-
values = extract_hidden_values(text)
|
|
149
|
-
|
|
150
|
-
if len(values) < 2:
|
|
151
|
-
raise RuntimeError(f"operation token取得失敗 values={values}")
|
|
152
|
-
|
|
153
|
-
return values[1]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def convert_open_state(open_state):
|
|
157
|
-
open_state = str(open_state)
|
|
158
|
-
|
|
159
|
-
if open_state == "0x41":
|
|
160
|
-
return "open"
|
|
161
|
-
if open_state == "0x42":
|
|
162
|
-
return "closed"
|
|
163
|
-
if open_state == "0x43":
|
|
164
|
-
return "opening"
|
|
165
|
-
if open_state == "0x44":
|
|
166
|
-
return "closing"
|
|
167
|
-
if open_state == "0x45":
|
|
168
|
-
return "vent"
|
|
169
|
-
return "unknown"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def extract_init_json(html):
|
|
173
|
-
m = re.search(
|
|
174
|
-
r'window\.onload\s*=\s*init\((.*?)\);',
|
|
175
|
-
html,
|
|
176
|
-
re.S,
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
if not m:
|
|
180
|
-
return None
|
|
181
|
-
|
|
182
|
-
return m.group(1)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def get_window_status(client, window):
|
|
186
|
-
info = get_window_info(window)
|
|
187
|
-
url = get_device_page_url(info)
|
|
188
|
-
|
|
189
|
-
status, text = client.request("GET", url, timeout=10)
|
|
190
|
-
|
|
191
|
-
if status != 200:
|
|
192
|
-
print("unknown")
|
|
193
|
-
print(f"HTTP_STATUS={status}", file=sys.stderr)
|
|
194
|
-
print(text, file=sys.stderr)
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
init_json = extract_init_json(text)
|
|
198
|
-
|
|
199
|
-
if not init_json:
|
|
200
|
-
print("unknown")
|
|
201
|
-
print("init JSON not found", file=sys.stderr)
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
data = json.loads(init_json)
|
|
206
|
-
except Exception as e:
|
|
207
|
-
print("unknown")
|
|
208
|
-
print(f"init JSON parse error: {e}", file=sys.stderr)
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
open_state = data.get("sash", {}).get("openState", "unknown")
|
|
212
|
-
print(convert_open_state(open_state))
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def send_window_command(client, window, action):
|
|
216
|
-
if action not in ACTIONS:
|
|
217
|
-
print("unknown action", file=sys.stderr)
|
|
218
|
-
sys.exit(1)
|
|
219
|
-
|
|
220
|
-
info = get_window_info(window)
|
|
221
|
-
open_value = ACTIONS[action]
|
|
222
|
-
token = get_operation_token(client, info)
|
|
223
|
-
|
|
224
|
-
payload = (
|
|
225
|
-
"data="
|
|
226
|
-
+ json.dumps(
|
|
227
|
-
{
|
|
228
|
-
"objSendData": json.dumps(
|
|
229
|
-
{
|
|
230
|
-
"nodeId": info["nodeId"],
|
|
231
|
-
"eoj": info["eoj"],
|
|
232
|
-
"type": info["type"],
|
|
233
|
-
"device": {
|
|
234
|
-
"open": open_value,
|
|
235
|
-
},
|
|
236
|
-
}
|
|
237
|
-
),
|
|
238
|
-
"token": token,
|
|
239
|
-
}
|
|
240
|
-
)
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
url = f"{AISEG_HOST}/action/devices/device/{DEVICE_ID}/operation"
|
|
244
|
-
status, text = client.request("POST", url, data=payload, timeout=10)
|
|
245
|
-
|
|
246
|
-
if status < 200 or status >= 300:
|
|
247
|
-
raise RuntimeError(f"command failed HTTP_STATUS={status}")
|
|
248
|
-
|
|
249
|
-
print(text)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def main():
|
|
253
|
-
if len(sys.argv) != 3:
|
|
254
|
-
print(
|
|
255
|
-
"usage: aiseg_window.py nodeId open|close|stop|angle1|angle2|angle3|status",
|
|
256
|
-
file=sys.stderr,
|
|
257
|
-
)
|
|
258
|
-
sys.exit(1)
|
|
259
|
-
|
|
260
|
-
window = sys.argv[1].strip()
|
|
261
|
-
action = sys.argv[2].strip().lower()
|
|
262
|
-
|
|
263
|
-
client = AiSegHttpClient(AISEG_HOST, USER, PASSWORD)
|
|
264
|
-
|
|
265
|
-
if action == "status":
|
|
266
|
-
get_window_status(client, window)
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
send_window_command(client, window, action)
|
|
270
|
-
|
|
8
|
+
from aiseg_device import main
|
|
271
9
|
|
|
272
10
|
if __name__ == "__main__":
|
|
273
|
-
main()
|
|
11
|
+
main("window")
|