node-red-contrib-tapo-p125m 0.1.0
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/node-red-contrib-tapo-p125m-0.1.0.tgz +0 -0
- package/package.json +17 -0
- package/package.json.old +17 -0
- package/tapo.html +196 -0
- package/tapo.html-1-16-25-8-50 +228 -0
- package/tapo.html.old +73 -0
- package/tapo.js +122 -0
- package/tapo.js-1-16-25-8-51a +113 -0
- package/tapo.js.old +96 -0
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-tapo-p125m",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node-RED nodes for TP-Link Tapo (P125M) using tp-link-tapo-connect",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "tapo.js",
|
|
7
|
+
"keywords": ["node-red", "tapo", "tplink", "p125m", "matter"],
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"tp-link-tapo-connect": "^2.0.0"
|
|
10
|
+
},
|
|
11
|
+
"node-red": {
|
|
12
|
+
"nodes": {
|
|
13
|
+
"tapo-nodes": "tapo.js"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
package/package.json.old
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-tapo-p125m",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node-RED nodes for TP-Link Tapo (P125M) using tp-link-tapo-connect",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "tapo.js",
|
|
7
|
+
"keywords": ["node-red", "tapo", "tplink", "p125m", "matter"],
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"tp-link-tapo-connect": "^2.0.0"
|
|
10
|
+
},
|
|
11
|
+
"node-red": {
|
|
12
|
+
"nodes": {
|
|
13
|
+
"tapo-control": "tapo.js"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
package/tapo.html
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
// ---------- Config node: tapo-device ----------
|
|
4
|
+
RED.nodes.registerType("tapo-device", {
|
|
5
|
+
category: "config",
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: "" },
|
|
8
|
+
ip: { value: "", required: true },
|
|
9
|
+
useEnv: { value: true }
|
|
10
|
+
},
|
|
11
|
+
credentials: {
|
|
12
|
+
email: { type: "text" },
|
|
13
|
+
password: { type: "password" }
|
|
14
|
+
},
|
|
15
|
+
label: function() {
|
|
16
|
+
return this.name || this.ip || "tapo-device";
|
|
17
|
+
},
|
|
18
|
+
oneditprepare: function() {
|
|
19
|
+
const $useEnv = $("#node-config-input-useEnv");
|
|
20
|
+
const $email = $("#node-config-input-email");
|
|
21
|
+
const $password = $("#node-config-input-password");
|
|
22
|
+
|
|
23
|
+
function applyCredMode() {
|
|
24
|
+
const usingEnv = $useEnv.is(":checked");
|
|
25
|
+
$email.prop("disabled", usingEnv).css("opacity", usingEnv ? 0.5 : 1.0);
|
|
26
|
+
$password.prop("disabled", usingEnv).css("opacity", usingEnv ? 0.5 : 1.0);
|
|
27
|
+
}
|
|
28
|
+
$useEnv.on("change", applyCredMode);
|
|
29
|
+
applyCredMode();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ---------- Shared defaults for action nodes ----------
|
|
34
|
+
function actionDefaults() {
|
|
35
|
+
return {
|
|
36
|
+
name: { value: "" },
|
|
37
|
+
device: { value: "", type: "tapo-device", required: true }, // <-- config node reference
|
|
38
|
+
retries: { value: 1, validate: RED.validators.number() },
|
|
39
|
+
postDelayMs: { value: 250, validate: RED.validators.number() },
|
|
40
|
+
emitRaw: { value: false }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function registerAction(typeName, paletteLabel) {
|
|
45
|
+
RED.nodes.registerType(typeName, {
|
|
46
|
+
category: "Tapo P125M",
|
|
47
|
+
color: "#C7E9C0",
|
|
48
|
+
defaults: actionDefaults(),
|
|
49
|
+
inputs: 1,
|
|
50
|
+
outputs: 1,
|
|
51
|
+
icon: "font-awesome/fa-plug",
|
|
52
|
+
paletteLabel: paletteLabel,
|
|
53
|
+
label: function() {
|
|
54
|
+
return this.name || paletteLabel;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
registerAction("tapo-turn-on", "P125M on");
|
|
60
|
+
registerAction("tapo-turn-off", "P125M off");
|
|
61
|
+
registerAction("tapo-status", "P125M status");
|
|
62
|
+
})();
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<!-- ===================== -->
|
|
66
|
+
<!-- Config node edit pane -->
|
|
67
|
+
<!-- ===================== -->
|
|
68
|
+
<script type="text/html" data-template-name="tapo-device">
|
|
69
|
+
<div class="form-row">
|
|
70
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
71
|
+
<input type="text" id="node-config-input-name" placeholder="Desert Shack Plug">
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="form-row">
|
|
75
|
+
<label for="node-config-input-ip"><i class="fa fa-globe"></i> IP</label>
|
|
76
|
+
<input type="text" id="node-config-input-ip" placeholder="192.168.1.11">
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="form-row">
|
|
80
|
+
<label for="node-config-input-useEnv"><i class="fa fa-lock"></i> Credentials</label>
|
|
81
|
+
<input type="checkbox" id="node-config-input-useEnv" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
82
|
+
<span style="margin-left:8px;">Use env vars TAPO_EMAIL / TAPO_PASSWORD</span>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="form-row">
|
|
86
|
+
<label for="node-config-input-email"><i class="fa fa-user"></i> Email</label>
|
|
87
|
+
<input type="text" id="node-config-input-email" autocomplete="off" placeholder="(stored securely; not exported)">
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="form-row">
|
|
91
|
+
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
|
|
92
|
+
<input type="password" id="node-config-input-password" autocomplete="new-password" placeholder="(stored securely; not exported)">
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="form-tips">
|
|
96
|
+
Tip: Leave <b>Use env vars</b> checked to keep creds out of Node-RED entirely.
|
|
97
|
+
Otherwise, Email/Password are saved in <code>flows_cred.json</code> and won’t export.
|
|
98
|
+
</div>
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<script type="text/html" data-help-name="tapo-device">
|
|
102
|
+
<p>Configuration for a Tapo device (IP + credentials). Credentials are stored in Node-RED credential storage.</p>
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<!-- ================= -->
|
|
106
|
+
<!-- Action node panes -->
|
|
107
|
+
<!-- ================= -->
|
|
108
|
+
<script type="text/html" data-template-name="tapo-turn-on">
|
|
109
|
+
<div class="form-row">
|
|
110
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
111
|
+
<input type="text" id="node-input-name" placeholder="Turn plug ON">
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="form-row">
|
|
115
|
+
<label for="node-input-device"><i class="fa fa-cog"></i> Device</label>
|
|
116
|
+
<input type="text" id="node-input-device">
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="form-row">
|
|
120
|
+
<label for="node-input-retries"><i class="fa fa-refresh"></i> Retries</label>
|
|
121
|
+
<input type="number" id="node-input-retries" min="0" step="1">
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="form-row">
|
|
125
|
+
<label for="node-input-postDelayMs"><i class="fa fa-clock-o"></i> Post-action delay (ms)</label>
|
|
126
|
+
<input type="number" id="node-input-postDelayMs" min="0" step="50">
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="form-row">
|
|
130
|
+
<label for="node-input-emitRaw"><i class="fa fa-code"></i> Output</label>
|
|
131
|
+
<input type="checkbox" id="node-input-emitRaw" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
132
|
+
<span style="margin-left:8px;">Include raw device info</span>
|
|
133
|
+
</div>
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<script type="text/html" data-template-name="tapo-turn-off">
|
|
137
|
+
<div class="form-row">
|
|
138
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
139
|
+
<input type="text" id="node-input-name" placeholder="Turn plug OFF">
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="form-row">
|
|
143
|
+
<label for="node-input-device"><i class="fa fa-cog"></i> Device</label>
|
|
144
|
+
<input type="text" id="node-input-device">
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="form-row">
|
|
148
|
+
<label for="node-input-retries"><i class="fa fa-refresh"></i> Retries</label>
|
|
149
|
+
<input type="number" id="node-input-retries" min="0" step="1">
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="form-row">
|
|
153
|
+
<label for="node-input-postDelayMs"><i class="fa fa-clock-o"></i> Post-action delay (ms)</label>
|
|
154
|
+
<input type="number" id="node-input-postDelayMs" min="0" step="50">
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="form-row">
|
|
158
|
+
<label for="node-input-emitRaw"><i class="fa fa-code"></i> Output</label>
|
|
159
|
+
<input type="checkbox" id="node-input-emitRaw" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
160
|
+
<span style="margin-left:8px;">Include raw device info</span>
|
|
161
|
+
</div>
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<script type="text/html" data-template-name="tapo-status">
|
|
165
|
+
<div class="form-row">
|
|
166
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
167
|
+
<input type="text" id="node-input-name" placeholder="Get plug status">
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="form-row">
|
|
171
|
+
<label for="node-input-device"><i class="fa fa-cog"></i> Device</label>
|
|
172
|
+
<input type="text" id="node-input-device">
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="form-row">
|
|
176
|
+
<label for="node-input-retries"><i class="fa fa-refresh"></i> Retries</label>
|
|
177
|
+
<input type="number" id="node-input-retries" min="0" step="1">
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="form-row">
|
|
181
|
+
<label for="node-input-emitRaw"><i class="fa fa-code"></i> Output</label>
|
|
182
|
+
<input type="checkbox" id="node-input-emitRaw" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
183
|
+
<span style="margin-left:8px;">Include raw device info</span>
|
|
184
|
+
</div>
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<script type="text/html" data-help-name="tapo-turn-on">
|
|
188
|
+
<p>Turns a configured Tapo device <b>ON</b>.</p>
|
|
189
|
+
</script>
|
|
190
|
+
<script type="text/html" data-help-name="tapo-turn-off">
|
|
191
|
+
<p>Turns a configured Tapo device <b>OFF</b>.</p>
|
|
192
|
+
</script>
|
|
193
|
+
<script type="text/html" data-help-name="tapo-status">
|
|
194
|
+
<p>Reads status from a configured Tapo device.</p>
|
|
195
|
+
</script>
|
|
196
|
+
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<!-- tapo.html
|
|
2
|
+
Three separate nodes:
|
|
3
|
+
- tapo-turn-on
|
|
4
|
+
- tapo-turn-off
|
|
5
|
+
- tapo-status
|
|
6
|
+
|
|
7
|
+
Email/Password are Node-RED credentials (stored in flows_cred.json, not exported)
|
|
8
|
+
-->
|
|
9
|
+
<script type="text/javascript">
|
|
10
|
+
(function () {
|
|
11
|
+
function tapoDefaults() {
|
|
12
|
+
return {
|
|
13
|
+
name: { value: "" },
|
|
14
|
+
ip: { value: "", required: true },
|
|
15
|
+
useEnv: { value: true },
|
|
16
|
+
retries: { value: 1, validate: RED.validators.number() },
|
|
17
|
+
postDelayMs: { value: 250, validate: RED.validators.number() },
|
|
18
|
+
emitRaw: { value: false }
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function registerTapoType(typeName, paletteLabel) {
|
|
23
|
+
RED.nodes.registerType(typeName, {
|
|
24
|
+
category: "home",
|
|
25
|
+
color: "#C7E9C0",
|
|
26
|
+
defaults: tapoDefaults(),
|
|
27
|
+
credentials: {
|
|
28
|
+
email: { type: "text" },
|
|
29
|
+
password: { type: "password" }
|
|
30
|
+
},
|
|
31
|
+
inputs: 1,
|
|
32
|
+
outputs: 1,
|
|
33
|
+
icon: "font-awesome/fa-plug",
|
|
34
|
+
paletteLabel: paletteLabel,
|
|
35
|
+
label: function () {
|
|
36
|
+
return this.name || paletteLabel;
|
|
37
|
+
},
|
|
38
|
+
oneditprepare: function () {
|
|
39
|
+
const $useEnv = $("#node-input-useEnv");
|
|
40
|
+
const $email = $("#node-input-email");
|
|
41
|
+
const $password = $("#node-input-password");
|
|
42
|
+
|
|
43
|
+
function applyCredMode() {
|
|
44
|
+
const usingEnv = $useEnv.is(":checked");
|
|
45
|
+
// Disable credential fields if using env vars, to avoid confusion
|
|
46
|
+
$email.prop("disabled", usingEnv);
|
|
47
|
+
$password.prop("disabled", usingEnv);
|
|
48
|
+
|
|
49
|
+
// Optional: add subtle opacity
|
|
50
|
+
const opacity = usingEnv ? 0.5 : 1.0;
|
|
51
|
+
$email.css("opacity", opacity);
|
|
52
|
+
$password.css("opacity", opacity);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$useEnv.on("change", applyCredMode);
|
|
56
|
+
applyCredMode();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
registerTapoType("tapo-turn-on", "tapo on");
|
|
62
|
+
registerTapoType("tapo-turn-off", "tapo off");
|
|
63
|
+
registerTapoType("tapo-status", "tapo status");
|
|
64
|
+
})();
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<!-- ========================= -->
|
|
68
|
+
<!-- Shared UI template blocks -->
|
|
69
|
+
<!-- ========================= -->
|
|
70
|
+
|
|
71
|
+
<script type="text/html" data-template-name="tapo-turn-on">
|
|
72
|
+
<div class="form-row">
|
|
73
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
74
|
+
<input type="text" id="node-input-name" placeholder="Tapo ON">
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="form-row">
|
|
78
|
+
<label for="node-input-ip"><i class="fa fa-globe"></i> IP</label>
|
|
79
|
+
<input type="text" id="node-input-ip" placeholder="192.168.196.101">
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="form-row">
|
|
83
|
+
<label for="node-input-useEnv"><i class="fa fa-lock"></i> Credentials</label>
|
|
84
|
+
<input type="checkbox" id="node-input-useEnv" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
85
|
+
<span style="margin-left:8px;">Use env vars TAPO_EMAIL / TAPO_PASSWORD</span>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="form-row">
|
|
89
|
+
<label for="node-input-email"><i class="fa fa-user"></i> Email</label>
|
|
90
|
+
<input type="text" id="node-input-email" autocomplete="off" placeholder="(stored securely; not exported)">
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="form-row">
|
|
94
|
+
<label for="node-input-password"><i class="fa fa-key"></i> Password</label>
|
|
95
|
+
<input type="password" id="node-input-password" autocomplete="new-password" placeholder="(stored securely; not exported)">
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="form-row">
|
|
99
|
+
<label for="node-input-retries"><i class="fa fa-refresh"></i> Retries</label>
|
|
100
|
+
<input type="number" id="node-input-retries" min="0" step="1">
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="form-row">
|
|
104
|
+
<label for="node-input-postDelayMs"><i class="fa fa-clock-o"></i> Post-action delay (ms)</label>
|
|
105
|
+
<input type="number" id="node-input-postDelayMs" min="0" step="50">
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="form-row">
|
|
109
|
+
<label for="node-input-emitRaw"><i class="fa fa-code"></i> Output</label>
|
|
110
|
+
<input type="checkbox" id="node-input-emitRaw" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
111
|
+
<span style="margin-left:8px;">Include raw device info</span>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="form-tips">
|
|
115
|
+
Input: any message triggers <b>ON</b>.<br/>
|
|
116
|
+
Output: <code>msg.payload</code> = { ok, cmd, ip, device_on, model, ... }.
|
|
117
|
+
</div>
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<script type="text/html" data-template-name="tapo-turn-off">
|
|
121
|
+
<div class="form-row">
|
|
122
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
123
|
+
<input type="text" id="node-input-name" placeholder="Tapo OFF">
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="form-row">
|
|
127
|
+
<label for="node-input-ip"><i class="fa fa-globe"></i> IP</label>
|
|
128
|
+
<input type="text" id="node-input-ip" placeholder="192.168.196.101">
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="form-row">
|
|
132
|
+
<label for="node-input-useEnv"><i class="fa fa-lock"></i> Credentials</label>
|
|
133
|
+
<input type="checkbox" id="node-input-useEnv" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
134
|
+
<span style="margin-left:8px;">Use env vars TAPO_EMAIL / TAPO_PASSWORD</span>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="form-row">
|
|
138
|
+
<label for="node-input-email"><i class="fa fa-user"></i> Email</label>
|
|
139
|
+
<input type="text" id="node-input-email" autocomplete="off" placeholder="(stored securely; not exported)">
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="form-row">
|
|
143
|
+
<label for="node-input-password"><i class="fa fa-key"></i> Password</label>
|
|
144
|
+
<input type="password" id="node-input-password" autocomplete="new-password" placeholder="(stored securely; not exported)">
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="form-row">
|
|
148
|
+
<label for="node-input-retries"><i class="fa fa-refresh"></i> Retries</label>
|
|
149
|
+
<input type="number" id="node-input-retries" min="0" step="1">
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="form-row">
|
|
153
|
+
<label for="node-input-postDelayMs"><i class="fa fa-clock-o"></i> Post-action delay (ms)</label>
|
|
154
|
+
<input type="number" id="node-input-postDelayMs" min="0" step="50">
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="form-row">
|
|
158
|
+
<label for="node-input-emitRaw"><i class="fa fa-code"></i> Output</label>
|
|
159
|
+
<input type="checkbox" id="node-input-emitRaw" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
160
|
+
<span style="margin-left:8px;">Include raw device info</span>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="form-tips">
|
|
164
|
+
Input: any message triggers <b>OFF</b>.<br/>
|
|
165
|
+
Output: <code>msg.payload</code> = { ok, cmd, ip, device_on, model, ... }.
|
|
166
|
+
</div>
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<script type="text/html" data-template-name="tapo-status">
|
|
170
|
+
<div class="form-row">
|
|
171
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
172
|
+
<input type="text" id="node-input-name" placeholder="Tapo STATUS">
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="form-row">
|
|
176
|
+
<label for="node-input-ip"><i class="fa fa-globe"></i> IP</label>
|
|
177
|
+
<input type="text" id="node-input-ip" placeholder="192.168.196.101">
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="form-row">
|
|
181
|
+
<label for="node-input-useEnv"><i class="fa fa-lock"></i> Credentials</label>
|
|
182
|
+
<input type="checkbox" id="node-input-useEnv" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
183
|
+
<span style="margin-left:8px;">Use env vars TAPO_EMAIL / TAPO_PASSWORD</span>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="form-row">
|
|
187
|
+
<label for="node-input-email"><i class="fa fa-user"></i> Email</label>
|
|
188
|
+
<input type="text" id="node-input-email" autocomplete="off" placeholder="(stored securely; not exported)">
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="form-row">
|
|
192
|
+
<label for="node-input-password"><i class="fa fa-key"></i> Password</label>
|
|
193
|
+
<input type="password" id="node-input-password" autocomplete="new-password" placeholder="(stored securely; not exported)">
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div class="form-row">
|
|
197
|
+
<label for="node-input-retries"><i class="fa fa-refresh"></i> Retries</label>
|
|
198
|
+
<input type="number" id="node-input-retries" min="0" step="1">
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div class="form-row">
|
|
202
|
+
<label for="node-input-emitRaw"><i class="fa fa-code"></i> Output</label>
|
|
203
|
+
<input type="checkbox" id="node-input-emitRaw" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
204
|
+
<span style="margin-left:8px;">Include raw device info</span>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="form-tips">
|
|
208
|
+
Input: any message triggers <b>STATUS</b>.<br/>
|
|
209
|
+
Output: <code>msg.payload</code> = { ok, cmd, ip, device_on, model, ... }.
|
|
210
|
+
</div>
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<!-- Help sections -->
|
|
214
|
+
<script type="text/html" data-help-name="tapo-turn-on">
|
|
215
|
+
<p>Turns a Tapo plug <b>ON</b> using <code>tp-link-tapo-connect</code>.</p>
|
|
216
|
+
<p>Credentials are stored in Node-RED credential storage (not exported).</p>
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<script type="text/html" data-help-name="tapo-turn-off">
|
|
220
|
+
<p>Turns a Tapo plug <b>OFF</b> using <code>tp-link-tapo-connect</code>.</p>
|
|
221
|
+
<p>Credentials are stored in Node-RED credential storage (not exported).</p>
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<script type="text/html" data-help-name="tapo-status">
|
|
225
|
+
<p>Reads Tapo plug status using <code>tp-link-tapo-connect</code>.</p>
|
|
226
|
+
<p>Credentials are stored in Node-RED credential storage (not exported).</p>
|
|
227
|
+
</script>
|
|
228
|
+
|
package/tapo.html.old
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('tapo-control', {
|
|
3
|
+
category: 'home',
|
|
4
|
+
color: '#C7E9C0',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
ip: { value: "", required: true },
|
|
8
|
+
email: { value: "" }, // optional if using env
|
|
9
|
+
password: { value: "" }, // optional if using env
|
|
10
|
+
useEnv: { value: true },
|
|
11
|
+
emitRaw: { value: false }
|
|
12
|
+
},
|
|
13
|
+
inputs: 1,
|
|
14
|
+
outputs: 1,
|
|
15
|
+
icon: "font-awesome/fa-plug",
|
|
16
|
+
label: function () {
|
|
17
|
+
return this.name || "tapo-control";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script type="text/html" data-template-name="tapo-control">
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
25
|
+
<input type="text" id="node-input-name" placeholder="Tapo P125M">
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="form-row">
|
|
29
|
+
<label for="node-input-ip"><i class="fa fa-globe"></i> IP</label>
|
|
30
|
+
<input type="text" id="node-input-ip" placeholder="192.168.1.50">
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-input-useEnv"><i class="fa fa-leaf"></i> Credentials</label>
|
|
35
|
+
<input type="checkbox" id="node-input-useEnv" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
36
|
+
<span style="margin-left:8px;">Use env vars (TAPO_EMAIL / TAPO_PASSWORD)</span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-email"><i class="fa fa-user"></i> Email</label>
|
|
41
|
+
<input type="text" id="node-input-email" placeholder="(optional if using env vars)">
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="form-row">
|
|
45
|
+
<label for="node-input-password"><i class="fa fa-key"></i> Password</label>
|
|
46
|
+
<input type="password" id="node-input-password" placeholder="(optional if using env vars)">
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label for="node-input-emitRaw"><i class="fa fa-code"></i> Output</label>
|
|
51
|
+
<input type="checkbox" id="node-input-emitRaw" style="display:inline-block; width:auto; vertical-align:middle;">
|
|
52
|
+
<span style="margin-left:8px;">Include raw device info</span>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="form-tips">
|
|
56
|
+
Input: msg.payload = <code>on</code> | <code>off</code> | <code>toggle</code> | <code>status</code><br/>
|
|
57
|
+
Output: msg.payload = { ok, cmd, ip, device_on, model, ... }
|
|
58
|
+
</div>
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<script type="text/html" data-help-name="tapo-control">
|
|
62
|
+
<p>Control a TP-Link Tapo plug (e.g., P125M) via <code>tp-link-tapo-connect</code>.</p>
|
|
63
|
+
<h3>Inputs</h3>
|
|
64
|
+
<ul>
|
|
65
|
+
<li><code>msg.payload</code>: <code>on</code>, <code>off</code>, <code>toggle</code>, or <code>status</code></li>
|
|
66
|
+
</ul>
|
|
67
|
+
<h3>Configuration</h3>
|
|
68
|
+
<ul>
|
|
69
|
+
<li>IP: required</li>
|
|
70
|
+
<li>Credentials: either env vars <code>TAPO_EMAIL</code> / <code>TAPO_PASSWORD</code> or set in node</li>
|
|
71
|
+
</ul>
|
|
72
|
+
</script>
|
|
73
|
+
|
package/tapo.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
|
|
5
|
+
module.exports = function (RED) {
|
|
6
|
+
const { loginDeviceByIp } = require("tp-link-tapo-connect");
|
|
7
|
+
|
|
8
|
+
// ----- Config Node -----
|
|
9
|
+
function TapoDeviceNode(n) {
|
|
10
|
+
RED.nodes.createNode(this, n);
|
|
11
|
+
this.name = n.name;
|
|
12
|
+
this.ip = (n.ip || "").trim();
|
|
13
|
+
this.useEnv = !!n.useEnv;
|
|
14
|
+
}
|
|
15
|
+
RED.nodes.registerType("tapo-device", TapoDeviceNode, {
|
|
16
|
+
credentials: {
|
|
17
|
+
email: { type: "text" },
|
|
18
|
+
password: { type: "password" }
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function getCredsFromDevice(deviceNode) {
|
|
23
|
+
const emailCred = (deviceNode.credentials?.email || "").trim();
|
|
24
|
+
const passCred = (deviceNode.credentials?.password || "").trim();
|
|
25
|
+
if (emailCred && passCred) return { email: emailCred, password: passCred };
|
|
26
|
+
|
|
27
|
+
if (deviceNode.useEnv) {
|
|
28
|
+
const emailEnv = (process.env.TAPO_EMAIL || "").trim();
|
|
29
|
+
const passEnv = (process.env.TAPO_PASSWORD || "").trim();
|
|
30
|
+
return { email: emailEnv, password: passEnv };
|
|
31
|
+
}
|
|
32
|
+
return { email: "", password: "" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getInfo(device) {
|
|
36
|
+
const info = await device.getDeviceInfo();
|
|
37
|
+
const deviceOn =
|
|
38
|
+
(info?.result && typeof info.result.device_on !== "undefined") ? info.result.device_on :
|
|
39
|
+
(typeof info?.device_on !== "undefined") ? info.device_on :
|
|
40
|
+
null;
|
|
41
|
+
|
|
42
|
+
const model = info?.result?.model ?? info?.model ?? null;
|
|
43
|
+
return { info, deviceOn, model };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeActionNode(fixedCmd) {
|
|
47
|
+
return function ActionNode(config) {
|
|
48
|
+
RED.nodes.createNode(this, config);
|
|
49
|
+
const node = this;
|
|
50
|
+
|
|
51
|
+
node.device = RED.nodes.getNode(config.device);
|
|
52
|
+
|
|
53
|
+
node.on("input", async (msg, send, done) => {
|
|
54
|
+
send = send || ((...args) => node.send(...args));
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (!node.device) throw new Error("Missing Device config. Select a tapo-device.");
|
|
58
|
+
const ip = (node.device.ip || "").trim();
|
|
59
|
+
if (!ip) throw new Error("Device config missing IP.");
|
|
60
|
+
|
|
61
|
+
const { email, password } = getCredsFromDevice(node.device);
|
|
62
|
+
if (!email || !password) throw new Error("Missing credentials in tapo-device (or env vars).");
|
|
63
|
+
|
|
64
|
+
const retries = Math.max(0, parseInt(config.retries ?? 1, 10) || 0);
|
|
65
|
+
const postDelayMs = Math.max(0, parseInt(config.postDelayMs ?? 0, 10) || 0);
|
|
66
|
+
|
|
67
|
+
let lastErr = null;
|
|
68
|
+
for (let i = 0; i <= retries; i++) {
|
|
69
|
+
try {
|
|
70
|
+
node.status({ fill: "blue", shape: "dot", text: `connecting ${ip}` });
|
|
71
|
+
|
|
72
|
+
const dev = await loginDeviceByIp(email, password, ip);
|
|
73
|
+
|
|
74
|
+
if (fixedCmd === "on") await dev.turnOn();
|
|
75
|
+
if (fixedCmd === "off") await dev.turnOff();
|
|
76
|
+
if ((fixedCmd === "on" || fixedCmd === "off") && postDelayMs > 0) {
|
|
77
|
+
await sleep(postDelayMs);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const post = await getInfo(dev);
|
|
81
|
+
|
|
82
|
+
node.status({
|
|
83
|
+
fill: post.deviceOn ? "green" : "red",
|
|
84
|
+
shape: "dot",
|
|
85
|
+
text: post.deviceOn ? "on" : "off"
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
msg.payload = {
|
|
89
|
+
ok: true,
|
|
90
|
+
cmd: fixedCmd,
|
|
91
|
+
ip,
|
|
92
|
+
device_on: post.deviceOn,
|
|
93
|
+
model: post.model
|
|
94
|
+
};
|
|
95
|
+
if (config.emitRaw) msg.payload.raw = post.info;
|
|
96
|
+
|
|
97
|
+
send(msg);
|
|
98
|
+
done();
|
|
99
|
+
return;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
lastErr = e;
|
|
102
|
+
if (i < retries) await sleep(200 + i * 200);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw lastErr || new Error("Unknown error");
|
|
106
|
+
} catch (err) {
|
|
107
|
+
node.status({ fill: "grey", shape: "ring", text: "error" });
|
|
108
|
+
msg.payload = { ok: false, cmd: fixedCmd, error: String(err?.message || err) };
|
|
109
|
+
send(msg);
|
|
110
|
+
done();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
node.on("close", () => node.status({}));
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
RED.nodes.registerType("tapo-turn-on", makeActionNode("on"));
|
|
119
|
+
RED.nodes.registerType("tapo-turn-off", makeActionNode("off"));
|
|
120
|
+
RED.nodes.registerType("tapo-status", makeActionNode("status"));
|
|
121
|
+
};
|
|
122
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
|
|
5
|
+
module.exports = function (RED) {
|
|
6
|
+
const { loginDeviceByIp } = require("tp-link-tapo-connect");
|
|
7
|
+
|
|
8
|
+
function getCreds(node, config) {
|
|
9
|
+
// Prefer per-node credentials (stored in flows_cred.json, not exported)
|
|
10
|
+
const email = (node.credentials && node.credentials.email ? String(node.credentials.email) : "").trim();
|
|
11
|
+
const password = (node.credentials && node.credentials.password ? String(node.credentials.password) : "").trim();
|
|
12
|
+
if (email && password) return { email, password };
|
|
13
|
+
|
|
14
|
+
// Optional fallback to env vars
|
|
15
|
+
if (config.useEnv) {
|
|
16
|
+
const envEmail = (process.env.TAPO_EMAIL || "").trim();
|
|
17
|
+
const envPass = (process.env.TAPO_PASSWORD || "").trim();
|
|
18
|
+
return { email: envEmail, password: envPass };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { email: "", password: "" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getInfo(device) {
|
|
25
|
+
const info = await device.getDeviceInfo();
|
|
26
|
+
const deviceOn = (info && info.result && typeof info.result.device_on !== "undefined")
|
|
27
|
+
? info.result.device_on
|
|
28
|
+
: (typeof info.device_on !== "undefined" ? info.device_on : null);
|
|
29
|
+
|
|
30
|
+
const model = (info && info.result && info.result.model) ? info.result.model : (info && info.model ? info.model : null);
|
|
31
|
+
return { info, deviceOn, model };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeNodeType(fixedCmd) {
|
|
35
|
+
return function TapoFixedCmdNode(config) {
|
|
36
|
+
RED.nodes.createNode(this, config);
|
|
37
|
+
const node = this;
|
|
38
|
+
|
|
39
|
+
node.on("input", async (msg, send, done) => {
|
|
40
|
+
send = send || ((...args) => node.send(...args));
|
|
41
|
+
|
|
42
|
+
const ip = String(config.ip || "").trim();
|
|
43
|
+
const retries = Math.max(0, parseInt(config.retries ?? 1, 10) || 0);
|
|
44
|
+
const postDelayMs = Math.max(0, parseInt(config.postDelayMs ?? 0, 10) || 0);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const creds = getCreds(node, config);
|
|
48
|
+
if (!creds.email || !creds.password) {
|
|
49
|
+
throw new Error("Missing credentials. Enter Email/Password in the node (credentials) or enable env vars.");
|
|
50
|
+
}
|
|
51
|
+
if (!ip) throw new Error("Missing IP in node config");
|
|
52
|
+
|
|
53
|
+
let lastErr = null;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i <= retries; i++) {
|
|
56
|
+
try {
|
|
57
|
+
node.status({ fill: "blue", shape: "dot", text: `connecting ${ip}` });
|
|
58
|
+
|
|
59
|
+
const device = await loginDeviceByIp(creds.email, creds.password, ip);
|
|
60
|
+
|
|
61
|
+
if (fixedCmd === "on") await device.turnOn();
|
|
62
|
+
if (fixedCmd === "off") await device.turnOff();
|
|
63
|
+
if ((fixedCmd === "on" || fixedCmd === "off") && postDelayMs > 0) {
|
|
64
|
+
await sleep(postDelayMs);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const post = await getInfo(device);
|
|
68
|
+
|
|
69
|
+
node.status({
|
|
70
|
+
fill: post.deviceOn ? "green" : "red",
|
|
71
|
+
shape: "dot",
|
|
72
|
+
text: post.deviceOn ? "on" : "off"
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
msg.payload = {
|
|
76
|
+
ok: true,
|
|
77
|
+
cmd: fixedCmd,
|
|
78
|
+
ip,
|
|
79
|
+
device_on: post.deviceOn,
|
|
80
|
+
model: post.model
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (config.emitRaw) msg.payload.raw = post.info;
|
|
84
|
+
|
|
85
|
+
send(msg);
|
|
86
|
+
done();
|
|
87
|
+
return;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
lastErr = e;
|
|
90
|
+
if (i < retries) await sleep(200 + i * 200);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw lastErr || new Error("Unknown error");
|
|
95
|
+
} catch (err) {
|
|
96
|
+
node.status({ fill: "grey", shape: "ring", text: "error" });
|
|
97
|
+
msg.payload = { ok: false, cmd: fixedCmd, ip, error: String(err && err.message ? err.message : err) };
|
|
98
|
+
send(msg);
|
|
99
|
+
done();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
node.on("close", () => node.status({}));
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const credsDef = { email: { type: "text" }, password: { type: "password" } };
|
|
108
|
+
|
|
109
|
+
RED.nodes.registerType("tapo-turn-on", makeNodeType("on"), { credentials: credsDef });
|
|
110
|
+
RED.nodes.registerType("tapo-turn-off", makeNodeType("off"), { credentials: credsDef });
|
|
111
|
+
RED.nodes.registerType("tapo-status", makeNodeType("status"), { credentials: credsDef });
|
|
112
|
+
};
|
|
113
|
+
|
package/tapo.js.old
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
function TapoControlNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
const { loginDeviceByIp } = require("tp-link-tapo-connect");
|
|
9
|
+
|
|
10
|
+
function getCreds() {
|
|
11
|
+
if (config.useEnv) {
|
|
12
|
+
return {
|
|
13
|
+
email: process.env.TAPO_EMAIL,
|
|
14
|
+
password: process.env.TAPO_PASSWORD
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
email: config.email,
|
|
19
|
+
password: config.password
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function run(cmd, msg) {
|
|
24
|
+
const ip = (config.ip || "").trim();
|
|
25
|
+
if (!ip) throw new Error("Missing IP in node config");
|
|
26
|
+
|
|
27
|
+
const { email, password } = getCreds();
|
|
28
|
+
if (!email || !password) {
|
|
29
|
+
throw new Error("Missing credentials. Use env vars TAPO_EMAIL/TAPO_PASSWORD or set Email/Password in node.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
node.status({ fill: "blue", shape: "dot", text: `connecting ${ip}` });
|
|
33
|
+
|
|
34
|
+
const device = await loginDeviceByIp(email, password, ip);
|
|
35
|
+
|
|
36
|
+
// Always read info first for toggle/status
|
|
37
|
+
let info = await device.getDeviceInfo();
|
|
38
|
+
let deviceOn = info?.result?.device_on ?? info?.device_on;
|
|
39
|
+
|
|
40
|
+
if (cmd === "toggle") {
|
|
41
|
+
cmd = deviceOn ? "off" : "on";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (cmd === "on") await device.turnOn();
|
|
45
|
+
if (cmd === "off") await device.turnOff();
|
|
46
|
+
|
|
47
|
+
// Refresh state after any action (and for status)
|
|
48
|
+
info = await device.getDeviceInfo();
|
|
49
|
+
deviceOn = info?.result?.device_on ?? info?.device_on;
|
|
50
|
+
|
|
51
|
+
const out = {
|
|
52
|
+
ok: true,
|
|
53
|
+
cmd,
|
|
54
|
+
ip,
|
|
55
|
+
device_on: deviceOn ?? null,
|
|
56
|
+
model: info?.result?.model ?? info?.model ?? null
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (config.emitRaw) out.raw = info;
|
|
60
|
+
|
|
61
|
+
node.status({ fill: deviceOn ? "green" : "red", shape: "dot", text: deviceOn ? "on" : "off" });
|
|
62
|
+
|
|
63
|
+
msg.payload = out;
|
|
64
|
+
return msg;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
node.on("input", async (msg, send, done) => {
|
|
68
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const rawCmd = msg?.payload;
|
|
72
|
+
const cmd = String(rawCmd || "").toLowerCase().trim();
|
|
73
|
+
|
|
74
|
+
if (!["on", "off", "toggle", "status"].includes(cmd)) {
|
|
75
|
+
throw new Error('msg.payload must be one of: "on", "off", "toggle", "status"');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const outMsg = await run(cmd, msg);
|
|
79
|
+
send(outMsg);
|
|
80
|
+
done();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
node.status({ fill: "grey", shape: "ring", text: "error" });
|
|
83
|
+
msg.payload = { ok: false, error: err.message || String(err) };
|
|
84
|
+
send(msg);
|
|
85
|
+
done();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
node.on("close", () => {
|
|
90
|
+
node.status({});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
RED.nodes.registerType("tapo-control", TapoControlNode);
|
|
95
|
+
};
|
|
96
|
+
|