smart-home-engine 0.28.2 → 0.29.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.
@@ -1,4 +1,4 @@
1
- import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-DeQBzoGc.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-BNu1W94k.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license
@@ -155,7 +155,7 @@
155
155
  }
156
156
  })();
157
157
  </script>
158
- <script type="module" crossorigin src="/assets/index-DeQBzoGc.js"></script>
158
+ <script type="module" crossorigin src="/assets/index-BNu1W94k.js"></script>
159
159
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-BW2J83t5.js">
160
160
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
161
161
  <link rel="stylesheet" crossorigin href="/assets/index-DMJ3LJnK.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "0.28.2",
3
+ "version": "0.29.0",
4
4
  "description": "Node.js based script runner for use in MQTT based Smart Home environments",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable func-name-matching, func-names, camelcase */
2
2
 
3
- module.exports = function (she) {
3
+ module.exports = function (she, ctx = {}) {
4
4
  /**
5
5
  * @method now
6
6
  * @returns {number} ms since epoch
@@ -9,106 +9,20 @@ module.exports = function (she) {
9
9
  return new Date().getTime();
10
10
  };
11
11
 
12
- /**
13
- * @method age
14
- * @param {string} topic
15
- * @returns {number} seconds since last change
16
- */
17
- she.age = function Sandbox_age(topic) {
18
- return Math.round((new Date().getTime() - she.getProp(topic, 'lc')) / 1000);
19
- };
20
-
21
- /**
22
- * Link topic(s) to other topic(s)
23
- * @method link
24
- * @param {(string|string[])} source - topic or array of topics to subscribe
25
- * @param {(string|string[])} target - topic or array of topics to publish
26
- * @param {mixed} [value] - value to publish. If omitted the sources value is published. A function can be used to transform the value.
27
- */
28
- she.link = function Sandbox_link(source, target, /* optional */ value) {
29
- she.mqttsub(source, (topic, val) => {
30
- if (typeof value === 'function') {
31
- val = value(val);
32
- } else if (typeof value !== 'undefined') {
33
- val = value;
34
- }
12
+ // Route a computed value to a topic string (setValue) or a callback (called as fn(topic, val)).
13
+ function sink(target, topic, val) {
14
+ if (typeof target === 'function') {
15
+ target(topic, val);
16
+ } else {
35
17
  she.setValue(target, val);
36
- });
37
- };
38
-
39
- /**
40
- * Combine topics through boolean or
41
- * @method combineBool
42
- * @param {string[]} srcs - array of topics to subscribe
43
- * @param {string} targets - topic to publish
44
- */
45
- she.combineBool = function Sandbox_combineBool(srcs, target) {
46
- function combine() {
47
- let result = 0;
48
- srcs.forEach((src) => {
49
- if (she.getValue(src)) {
50
- result = 1;
51
- }
52
- });
53
- she.setValue(target, result);
54
18
  }
55
- combine();
56
- she.mqttsub(srcs, { retain: true }, combine);
57
- };
19
+ }
58
20
 
59
- /**
60
- * Publish maximum of combined topics
61
- * @method combineMax
62
- * @param {string[]} srcs - array of topics to subscribe
63
- * @param {string} targets - topic to publish
64
- */
65
- she.combineMax = function (srcs, target) {
66
- function combine() {
67
- let result = 0;
68
- srcs.forEach((src) => {
69
- const srcVal = she.getValue(src);
70
- if (srcVal > result) {
71
- result = srcVal;
72
- }
73
- });
74
- she.setValue(target, result);
75
- }
76
- combine();
77
- she.mqttsub(srcs, { retain: true }, combine);
78
- };
21
+ // Timeout handles indexed by target (string key or function reference).
22
+ const timerHandles = new Map();
79
23
 
80
- const timeouts = {};
81
24
  /**
82
- * Publishes 1 on target for specific time after src changed to true
83
- * @method timer
84
- * @param {(string|string[])} src - topic or array of topics to subscribe
85
- * @param {string} target - topic to publish
86
- * @param {number} time - timeout in milliseconds
87
- */
88
- she.timer = function (src, target, time) {
89
- she.mqttsub(src, { retain: false }, (topic, val) => {
90
- if (val) {
91
- she.clearTimeout(timeouts[target]);
92
- if (!she.getValue(target)) {
93
- she.setValue(target, 1);
94
- }
95
- timeouts[target] = she.setTimeout(() => {
96
- if (she.getValue(target)) {
97
- she.setValue(target, 0);
98
- }
99
- }, time);
100
- }
101
- });
102
-
103
- timeouts[target] = she.setTimeout(() => {
104
- if (she.getValue(target)) {
105
- she.setValue(target, 0);
106
- }
107
- }, time);
108
- };
109
-
110
- /**
111
- * Namespaced MQTT API — the primary way to interact with MQTT from scripts.
25
+ * Namespaced MQTT API - the primary way to interact with MQTT from scripts.
112
26
  * @namespace she.mqtt
113
27
  */
114
28
  she.mqtt = {
@@ -123,35 +37,166 @@ module.exports = function (she) {
123
37
  /** Get a specific property from a topic's state object. */
124
38
  getProp: (...args) => she.getProp(...args),
125
39
  /** Forward value changes from source topic(s) to target topic(s). */
126
- link: (...args) => she.link(...args),
40
+ link: function Sandbox_mqtt_link(source, target, /* optional */ value) {
41
+ she.mqttsub(source, (topic, val) => {
42
+ if (typeof value === 'function') {
43
+ val = value(val);
44
+ } else if (typeof value !== 'undefined') {
45
+ val = value;
46
+ }
47
+ she.setValue(target, val);
48
+ });
49
+ },
127
50
  /** Seconds since the topic's value last changed. */
128
- age: (topic) => she.age(topic),
51
+ age: function Sandbox_mqtt_age(topic) {
52
+ return Math.round((new Date().getTime() - she.getProp(topic, 'lc')) / 1000);
53
+ },
129
54
  /** Register a callback for MQTT connection lifecycle events ('connect' or 'disconnect'). */
130
55
  on: (event, cb) => she._registerMqttEvent(event, cb),
56
+ /**
57
+ * Publish 1 to target when any source is truthy, 0 otherwise.
58
+ * target may be a topic string or a callback(topic, val).
59
+ */
60
+ or: function Sandbox_mqtt_or(srcs, target) {
61
+ function combine(topic) {
62
+ const result = srcs.some((src) => she.getValue(src)) ? 1 : 0;
63
+ sink(target, topic ?? null, result);
64
+ }
65
+ combine(null);
66
+ she.mqttsub(srcs, { retain: true }, (topic) => combine(topic));
67
+ },
68
+ /**
69
+ * Publish 1 to target when all sources are truthy, 0 otherwise.
70
+ * target may be a topic string or a callback(topic, val).
71
+ */
72
+ and: function Sandbox_mqtt_and(srcs, target) {
73
+ function combine(topic) {
74
+ const result = srcs.every((src) => she.getValue(src)) ? 1 : 0;
75
+ sink(target, topic ?? null, result);
76
+ }
77
+ combine(null);
78
+ she.mqttsub(srcs, { retain: true }, (topic) => combine(topic));
79
+ },
80
+ /**
81
+ * Publish the maximum of all source values to target.
82
+ * target may be a topic string or a callback(topic, val).
83
+ */
84
+ max: function Sandbox_mqtt_max(srcs, target) {
85
+ function combine(topic) {
86
+ let result = 0;
87
+ srcs.forEach((src) => {
88
+ const v = she.getValue(src);
89
+ if (v > result) result = v;
90
+ });
91
+ sink(target, topic ?? null, result);
92
+ }
93
+ combine(null);
94
+ she.mqttsub(srcs, { retain: true }, (topic) => combine(topic));
95
+ },
96
+ /**
97
+ * Publish the minimum of all source values to target.
98
+ * target may be a topic string or a callback(topic, val).
99
+ */
100
+ min: function Sandbox_mqtt_min(srcs, target) {
101
+ function combine(topic) {
102
+ const values = srcs.map((src) => she.getValue(src)).filter((v) => v !== undefined && v !== null);
103
+ const result = values.length ? Math.min(...values) : 0;
104
+ sink(target, topic ?? null, result);
105
+ }
106
+ combine(null);
107
+ she.mqttsub(srcs, { retain: true }, (topic) => combine(topic));
108
+ },
109
+ /**
110
+ * Pulse target to 1 for ms after src goes truthy, then reset to 0.
111
+ * Signature: timer(src, ms, target)
112
+ * target may be a topic string or a callback(topic, val).
113
+ * When target is a topic string, any lingering 1 from a previous run is
114
+ * cleared by an initial timeout on startup.
115
+ */
116
+ timer: function Sandbox_mqtt_timer(src, ms, target) {
117
+ const key = target;
118
+ she.mqttsub(src, { retain: false }, (topic, val) => {
119
+ if (val) {
120
+ she.clearTimeout(timerHandles.get(key));
121
+ sink(target, topic, 1);
122
+ timerHandles.set(
123
+ key,
124
+ she.setTimeout(() => {
125
+ sink(target, null, 0);
126
+ }, ms),
127
+ );
128
+ }
129
+ });
130
+ if (typeof target !== 'function') {
131
+ timerHandles.set(
132
+ key,
133
+ she.setTimeout(() => {
134
+ if (she.getValue(target)) she.setValue(target, 0);
135
+ }, ms),
136
+ );
137
+ }
138
+ },
131
139
  };
132
140
 
133
141
  /**
134
- * Fetch a URL and return a Promise that resolves to the response body.
135
- * Resolves to parsed JSON when the Content-Type is application/json, plain text otherwise.
136
- * Rejects on non-2xx responses.
137
- * @method fetch
138
- * @param {string} url
139
- * @param {RequestInit} [options]
140
- * @returns {Promise<string|object>}
142
+ * HTTP helpers fetch and webhook receiver.
143
+ * @namespace she.http
141
144
  */
142
- she.fetch = function Sandbox_fetch(url, options) {
143
- const TIMEOUT_MS = 30_000;
144
- let signal = options?.signal;
145
- let timer;
146
- if (!signal) {
147
- const ac = new AbortController();
148
- signal = ac.signal;
149
- timer = setTimeout(() => ac.abort(new Error(`she.fetch timed out after ${TIMEOUT_MS / 1000}s`)), TIMEOUT_MS);
150
- }
151
- return fetch(url, { ...options, signal }).then((r) => {
152
- if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);
153
- const ct = r.headers.get('content-type') || '';
154
- return ct.includes('json') ? r.json() : r.text();
155
- }).finally(() => clearTimeout(timer));
145
+ she.http = {
146
+ /**
147
+ * Fetch a URL and return a Promise that resolves to the response body.
148
+ * Resolves to parsed JSON when the Content-Type is application/json, plain text otherwise.
149
+ * Rejects on non-2xx responses.
150
+ * @param {string} url
151
+ * @param {RequestInit} [options]
152
+ * @returns {Promise<string|object>}
153
+ */
154
+ fetch: function Sandbox_http_fetch(url, options) {
155
+ const TIMEOUT_MS = 30_000;
156
+ let signal = options?.signal;
157
+ let timer;
158
+ if (!signal) {
159
+ const ac = new AbortController();
160
+ signal = ac.signal;
161
+ timer = setTimeout(
162
+ () => ac.abort(new Error(`she.http.fetch timed out after ${TIMEOUT_MS / 1000}s`)),
163
+ TIMEOUT_MS,
164
+ );
165
+ }
166
+ return fetch(url, { ...options, signal })
167
+ .then((r) => {
168
+ if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);
169
+ const ct = r.headers.get('content-type') || '';
170
+ return ct.includes('json') ? r.json() : r.text();
171
+ })
172
+ .finally(() => clearTimeout(timer));
173
+ },
174
+ /**
175
+ * Register a POST webhook endpoint at /api/<scriptName><path>.
176
+ * The endpoint auto-responds { ok: true } (200) when the callback resolves,
177
+ * or { error } (500) when it throws. The callback receives:
178
+ * callback(body, { params, query, headers })
179
+ * @param {string} path - Route path, e.g. '/webhook/mydevice'
180
+ * @param {function} callback
181
+ */
182
+ sub: function Sandbox_http_sub(path, callback) {
183
+ const { registerRoute } = require('../web/server');
184
+ const { scriptName } = ctx;
185
+ if (typeof path !== 'string') throw new TypeError('path must be a string');
186
+ if (typeof callback !== 'function') throw new TypeError('callback must be a function');
187
+ const fullPath = '/api/' + (scriptName || 'unknown') + path;
188
+ registerRoute('post', fullPath, (req, res) => {
189
+ let result;
190
+ try {
191
+ result = callback(req.body, { params: req.params, query: req.query, headers: req.headers });
192
+ } catch (err) {
193
+ res.status(500).json({ error: err.message });
194
+ return;
195
+ }
196
+ Promise.resolve(result)
197
+ .then(() => res.json({ ok: true }))
198
+ .catch((err) => res.status(500).json({ error: err.message }));
199
+ });
200
+ },
156
201
  };
157
202
  };
@@ -64,17 +64,20 @@ she.matter.send(nodeId, endpointId, cluster, cmd, [args]) → Promise<result>
64
64
 
65
65
  ### Helpers
66
66
  ```
67
- she.timer(src, target, ms) Pulse target=1 for ms after src goes truthy
68
- she.combineBool(srcs[], target) Publish OR of source values to target
69
- she.combineMax(srcs[], target) Publish maximum of source values to target
70
- she.link(src, target, [fn]) Alias for she.mqtt.link
71
- she.age(topic) Alias for she.mqtt.age
67
+ she.mqtt.timer(src, ms, topicOrCb) Pulse topicOrCb=1 for ms after src goes truthy
68
+ topicOrCb: topic string or callback(topic, val)
69
+ she.mqtt.or(srcs[], topicOrCb) Publish 1 if any source truthy, else 0
70
+ she.mqtt.and(srcs[], topicOrCb) Publish 1 if all sources truthy, else 0
71
+ she.mqtt.max(srcs[], topicOrCb) Publish maximum of source values
72
+ she.mqtt.min(srcs[], topicOrCb) Publish minimum of source values (0 if none set)
73
+ All topicOrCb: topic string or callback(topic, val)
72
74
  she.now() Current timestamp in ms
73
75
  she.debug / .info / .warn / .error Structured logging (prefixed with script name)
74
76
  she.global Shared mutable object across all scripts
75
- she.fetch(url, [opts]) HTTP/HTTPS fetch → Promise<string|object>
76
- Auto-parses JSON by Content-Type.
77
- Throws on non-2xx status.
77
+ she.http.fetch(url, [opts]) HTTP/HTTPS fetch → Promise<string|object>
78
+ Auto-parses JSON by Content-Type. Throws on non-2xx.
79
+ she.http.sub(path, cb) Register POST /api/<script><path> webhook endpoint
80
+ cb(body, { params, query, headers })
78
81
  she.config.latitude Read-only: geographic latitude from daemon config
79
82
  she.config.longitude Read-only: geographic longitude from daemon config
80
83
  ```