smart-home-engine 0.0.1 → 0.11.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/web/assets/codicon-DCmgc-ay.ttf +0 -0
  4. package/dist/web/assets/index-BbwiXmS-.css +1 -0
  5. package/dist/web/assets/index-DD-XScWV.js +140 -0
  6. package/dist/web/assets/monaco-langs-DZ6hB11b.js +1423 -0
  7. package/dist/web/assets/monaco-langs-DyX1CsEw.css +1 -0
  8. package/dist/web/assets/tsMode-DxTbjAE2.js +16 -0
  9. package/dist/web/index.html +164 -0
  10. package/dist/web/monacoeditorwork/editor.worker.bundle.js +13519 -0
  11. package/dist/web/monacoeditorwork/ts.worker.bundle.js +256353 -0
  12. package/package.json +85 -10
  13. package/src/config.js +56 -0
  14. package/src/elastic.js +19 -0
  15. package/src/index.js +1192 -0
  16. package/src/influx.js +25 -0
  17. package/src/lib/mqtt-wildcards.js +34 -0
  18. package/src/lib/parse-payload.js +29 -0
  19. package/src/lib/redis.js +74 -0
  20. package/src/lib/shedb-core.js +447 -0
  21. package/src/lib/shedb-worker.js +126 -0
  22. package/src/lib/state-store.js +97 -0
  23. package/src/lib/storage.js +74 -0
  24. package/src/matter/controller.js +307 -0
  25. package/src/sandbox/api.js +57 -0
  26. package/src/sandbox/elastic-sandbox.js +88 -0
  27. package/src/sandbox/influx-sandbox.js +107 -0
  28. package/src/sandbox/matter-sandbox.js +92 -0
  29. package/src/sandbox/shedb-sandbox.js +89 -0
  30. package/src/sandbox/stdlib.js +132 -0
  31. package/src/scripts/hello.js +3 -0
  32. package/src/web/ai-api.js +443 -0
  33. package/src/web/auth.js +186 -0
  34. package/src/web/config-api.js +34 -0
  35. package/src/web/deps-api.js +138 -0
  36. package/src/web/git-api.js +188 -0
  37. package/src/web/log-ws.js +78 -0
  38. package/src/web/matter-api.js +102 -0
  39. package/src/web/mqtt-api.js +65 -0
  40. package/src/web/scripts-api.js +192 -0
  41. package/src/web/server.js +139 -0
  42. package/src/web/shedb-api.js +140 -0
  43. package/src/web/shedb.js +168 -0
  44. package/index.js +0 -0
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const elastic = require('../elastic');
4
+
5
+ /**
6
+ * Sandbox module — adds she.elastic.* to every script context.
7
+ *
8
+ * All methods return Promises and are no-ops (returning empty / null results)
9
+ * when Elasticsearch is not configured (no --elastic.node in config).
10
+ *
11
+ * she.elastic API:
12
+ * she.elastic.search(index, query) → Promise<{ hits, total }>
13
+ * she.elastic.get(index, id) → Promise<object|null>
14
+ * she.elastic.index(index, doc, [id]) → Promise<{ id }>
15
+ * she.elastic.find(index, field, text, size) → Promise<object[]>
16
+ */
17
+ module.exports = function (she) {
18
+ she.elastic = {
19
+ /**
20
+ * Search documents in an Elasticsearch index.
21
+ * @param {string} index
22
+ * @param {object} query Elasticsearch query DSL object
23
+ * @returns {Promise<{ hits: object[], total: number }>}
24
+ */
25
+ async search(index, query) {
26
+ const client = elastic.getClient();
27
+ if (!client) return { hits: [], total: 0 };
28
+ const result = await client.search({ index, query });
29
+ return {
30
+ hits: result.hits.hits.map((h) => ({ id: h._id, ...h._source })),
31
+ total: result.hits.total?.value ?? result.hits.hits.length,
32
+ };
33
+ },
34
+
35
+ /**
36
+ * Retrieve a single document by ID.
37
+ * @param {string} index
38
+ * @param {string} id
39
+ * @returns {Promise<object|null>}
40
+ */
41
+ async get(index, id) {
42
+ const client = elastic.getClient();
43
+ if (!client) return null;
44
+ try {
45
+ const result = await client.get({ index, id });
46
+ return result._source ?? null;
47
+ } catch (err) {
48
+ if (err.statusCode === 404) return null;
49
+ throw err;
50
+ }
51
+ },
52
+
53
+ /**
54
+ * Index (create or replace) a document.
55
+ * @param {string} index
56
+ * @param {object} doc
57
+ * @param {string} [id] omit to let Elasticsearch auto-generate an ID
58
+ * @returns {Promise<{ id: string }>}
59
+ */
60
+ async index(index, doc, id) {
61
+ const client = elastic.getClient();
62
+ if (!client) return { id: null };
63
+ const params = { index, document: doc };
64
+ if (id !== undefined) params.id = id;
65
+ const result = await client.index(params);
66
+ return { id: result._id };
67
+ },
68
+
69
+ /**
70
+ * Full-text match search across a single field.
71
+ * @param {string} index
72
+ * @param {string} field
73
+ * @param {string} text
74
+ * @param {number} [size=10]
75
+ * @returns {Promise<object[]>}
76
+ */
77
+ async find(index, field, text, size) {
78
+ const client = elastic.getClient();
79
+ if (!client) return [];
80
+ const result = await client.search({
81
+ index,
82
+ size: size ?? 10,
83
+ query: { match: { [field]: text } },
84
+ });
85
+ return result.hits.hits.map((h) => ({ id: h._id, ...h._source }));
86
+ },
87
+ };
88
+ };
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const influx = require('../influx');
4
+
5
+ /**
6
+ * Sandbox module — adds she.influx.* to every script context.
7
+ *
8
+ * All methods return Promises and are no-ops (returning empty results) when
9
+ * InfluxDB is not configured (no --influx.url / --influx.token in config).
10
+ *
11
+ * she.influx API:
12
+ * she.influx.query(fluxQuery) → Promise<object[]>
13
+ * she.influx.write(measurement, fields, tags, ts) → Promise<void>
14
+ * she.influx.getLast(topic, n) → Promise<{ ts, val }[]>
15
+ * she.influx.getRange(topic, from, to) → Promise<{ ts, val }[]>
16
+ */
17
+ module.exports = function (she) {
18
+ she.influx = {
19
+ /**
20
+ * Execute a Flux query against InfluxDB.
21
+ * @param {string} fluxQuery
22
+ * @returns {Promise<object[]>}
23
+ */
24
+ query(fluxQuery) {
25
+ const client = influx.getClient();
26
+ const opts = influx.getOpts();
27
+ if (!client) return Promise.resolve([]);
28
+ const queryApi = client.getQueryApi(opts.org);
29
+ return new Promise((resolve, reject) => {
30
+ const rows = [];
31
+ queryApi.queryRows(fluxQuery, {
32
+ next(row, tableMeta) {
33
+ rows.push(tableMeta.toObject(row));
34
+ },
35
+ error: reject,
36
+ complete() {
37
+ resolve(rows);
38
+ },
39
+ });
40
+ });
41
+ },
42
+
43
+ /**
44
+ * Write a single data point to InfluxDB.
45
+ * @param {string} measurement
46
+ * @param {object} fields e.g. { temperature: 21.5 }
47
+ * @param {object} [tags] e.g. { room: 'living' }
48
+ * @param {Date|number} [timestamp]
49
+ * @returns {Promise<void>}
50
+ */
51
+ write(measurement, fields, tags, timestamp) {
52
+ const client = influx.getClient();
53
+ const opts = influx.getOpts();
54
+ if (!client) return Promise.resolve();
55
+ const { Point } = require('@influxdata/influxdb-client');
56
+ const writeApi = client.getWriteApi(opts.org, opts.bucket, 'ns');
57
+ const point = new Point(measurement);
58
+ if (tags) {
59
+ Object.entries(tags).forEach(([k, v]) => point.tag(k, v));
60
+ }
61
+ Object.entries(fields).forEach(([k, v]) => {
62
+ if (typeof v === 'boolean') {
63
+ point.booleanField(k, v);
64
+ } else if (typeof v === 'number') {
65
+ point.floatField(k, v);
66
+ } else {
67
+ point.stringField(k, String(v));
68
+ }
69
+ });
70
+ if (timestamp !== undefined) point.timestamp(timestamp);
71
+ writeApi.writePoint(point);
72
+ return writeApi.close();
73
+ },
74
+
75
+ /**
76
+ * Return the last N recorded values for an MQTT topic.
77
+ * Assumes data was stored with a "topic" tag and value in the "_value" field.
78
+ * @param {string} topic
79
+ * @param {number} n
80
+ * @returns {Promise<{ ts: number, val: any }[]>}
81
+ */
82
+ getLast(topic, n) {
83
+ const opts = influx.getOpts();
84
+ if (!opts) return Promise.resolve([]);
85
+ const safeTopic = topic.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
86
+ const flux = `from(bucket: "${opts.bucket}")` + ` |> range(start: -30d)` + ` |> filter(fn: (r) => r["topic"] == "${safeTopic}")` + ` |> tail(n: ${Number(n)})`;
87
+ return this.query(flux).then((rows) => rows.map((r) => ({ ts: new Date(r._time).getTime(), val: r._value })));
88
+ },
89
+
90
+ /**
91
+ * Return all recorded values for an MQTT topic within a time range.
92
+ * @param {string} topic
93
+ * @param {Date|string|number} from
94
+ * @param {Date|string|number} to
95
+ * @returns {Promise<{ ts: number, val: any }[]>}
96
+ */
97
+ getRange(topic, from, to) {
98
+ const opts = influx.getOpts();
99
+ if (!opts) return Promise.resolve([]);
100
+ const safeTopic = topic.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
101
+ const start = new Date(from).toISOString();
102
+ const stop = new Date(to).toISOString();
103
+ const flux = `from(bucket: "${opts.bucket}")` + ` |> range(start: ${start}, stop: ${stop})` + ` |> filter(fn: (r) => r["topic"] == "${safeTopic}")`;
104
+ return this.query(flux).then((rows) => rows.map((r) => ({ ts: new Date(r._time).getTime(), val: r._value })));
105
+ },
106
+ };
107
+ };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Matter sandbox module — adds she.matter.* to every script context.
5
+ *
6
+ * Called by loadSandbox() in index.js; receives (she, { scriptDomain, scriptName }).
7
+ *
8
+ * she.matter API:
9
+ * she.matter.sub(nodeId, endpointId, clusterName, attrName, cb)
10
+ * → listenerId (number)
11
+ * she.matter.unsub(listenerId)
12
+ * → void
13
+ * she.matter.get(nodeId, endpointId, clusterName, attrName)
14
+ * → Promise<value>
15
+ * she.matter.send(nodeId, endpointId, clusterName, command, args?)
16
+ * → Promise<result>
17
+ *
18
+ * All subscriptions registered by a script are automatically cancelled on
19
+ * hot-reload (cleanup() is called from unloadScript() in index.js).
20
+ */
21
+
22
+ const controller = require('../matter/controller');
23
+
24
+ module.exports = function (she, { scriptDomain, scriptName }) {
25
+ she.matter = {
26
+ /**
27
+ * Subscribe to an attribute change on a paired Matter device.
28
+ *
29
+ * @param {string} nodeId Decimal NodeId string
30
+ * @param {number} endpointId
31
+ * @param {string} clusterName camelCase cluster name, e.g. "onOff"
32
+ * @param {string} attrName camelCase attribute name, e.g. "onOff"
33
+ * @param {Function} callback (value, oldValue) => void
34
+ * @returns {number} listenerId
35
+ */
36
+ sub(nodeId, endpointId, clusterName, attrName, callback) {
37
+ try {
38
+ return controller.subscribeAttribute(scriptName, nodeId, endpointId, clusterName, attrName, callback);
39
+ } catch (err) {
40
+ scriptDomain.emit('error', err);
41
+ }
42
+ },
43
+
44
+ /**
45
+ * Cancel a specific subscription.
46
+ *
47
+ * @param {number} listenerId Returned by she.matter.sub()
48
+ */
49
+ unsub(listenerId) {
50
+ controller.unsubscribe(scriptName, listenerId);
51
+ },
52
+
53
+ /**
54
+ * Read a single attribute value from a paired Matter device.
55
+ *
56
+ * @param {string} nodeId
57
+ * @param {number} endpointId
58
+ * @param {string} clusterName camelCase cluster name
59
+ * @param {string} attrName camelCase attribute name
60
+ * @returns {Promise<unknown>}
61
+ */
62
+ get(nodeId, endpointId, clusterName, attrName) {
63
+ return controller.getAttribute(nodeId, endpointId, clusterName, attrName);
64
+ },
65
+
66
+ /**
67
+ * Invoke a cluster command on a paired Matter device.
68
+ *
69
+ * @param {string} nodeId
70
+ * @param {number} endpointId
71
+ * @param {string} clusterName camelCase cluster name
72
+ * @param {string} command camelCase command name
73
+ * @param {object} [args={}]
74
+ * @returns {Promise<unknown>}
75
+ */
76
+ send(nodeId, endpointId, clusterName, command, args) {
77
+ return controller.sendCommand(nodeId, endpointId, clusterName, command, args ?? {});
78
+ },
79
+ };
80
+ };
81
+
82
+ /**
83
+ * Remove all Matter subscriptions for a script on hot-reload.
84
+ * Called from unloadScript() in index.js.
85
+ *
86
+ * @param {string} scriptName
87
+ */
88
+ function cleanup(scriptName) {
89
+ controller.cleanup(scriptName);
90
+ }
91
+
92
+ module.exports.cleanup = cleanup;
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * sheDB sandbox module — adds she.db.* to every script context.
5
+ *
6
+ * Called by loadSandbox() in index.js; receives (she, { scriptDomain, scriptName }).
7
+ *
8
+ * she.db API:
9
+ * she.db.get(id) → document or undefined
10
+ * she.db.set(id, doc) → void (create/overwrite)
11
+ * she.db.extend(id, partial) → void (deep merge)
12
+ * she.db.delete(id) → void
13
+ * she.db.prop(id, method, prop, val)→ void (method: 'set'|'create'|'del')
14
+ * she.db.sub(pattern, callback) → void (callback(id, doc))
15
+ * she.db.query(filter, mapFn, reduceFn) → Array (ad-hoc synchronous query)
16
+ */
17
+
18
+ const { getCore, addListener, removeListenersByScript } = require('../web/shedb');
19
+
20
+ module.exports = function (she, { scriptDomain, scriptName }) {
21
+ const core = getCore();
22
+
23
+ // sheDB may not be initialised (--db-path not given or startup still in progress).
24
+ // We expose stubs that are no-ops / return undefined so user scripts don't crash.
25
+ she.db = {
26
+ get(id) {
27
+ return core ? core.get(id) : undefined;
28
+ },
29
+
30
+ set(id, doc) {
31
+ if (core) core.set(id, doc);
32
+ },
33
+
34
+ extend(id, partial) {
35
+ if (core) core.extend(id, partial);
36
+ },
37
+
38
+ delete(id) {
39
+ if (core) core.del(id);
40
+ },
41
+
42
+ /**
43
+ * Set/create/del a nested property on a document.
44
+ * @param {string} id
45
+ * @param {'set'|'create'|'del'} method
46
+ * @param {string} prop - dot-notation path, e.g. 'config.network.ip'
47
+ * @param {*} val - value (not used for 'del')
48
+ */
49
+ prop(id, method, prop, val) {
50
+ if (core) core.prop(id, { method, prop, val });
51
+ },
52
+
53
+ /**
54
+ * Subscribe to document changes matching an MQTT wildcard pattern.
55
+ * The callback fires when any matching document is created, updated, or deleted.
56
+ * Subscriptions are automatically removed when the script is hot-reloaded.
57
+ *
58
+ * @param {string} pattern - MQTT wildcard, e.g. 'devices/#'
59
+ * @param {Function} callback - called as callback(id, doc) where doc is null on delete
60
+ */
61
+ sub(pattern, callback) {
62
+ if (!core) return;
63
+ // Wrap in script domain so errors don't crash the process
64
+ const wrapped = scriptDomain.bind(callback);
65
+ addListener(pattern, wrapped, scriptName);
66
+ },
67
+
68
+ /**
69
+ * Ad-hoc synchronous query — does NOT persist; runs immediately.
70
+ *
71
+ * @param {string|null} filter - MQTT wildcard to pre-filter document IDs (null = all)
72
+ * @param {Function} mapFn - called as mapFn(doc, emit); call emit(item) to add to result
73
+ * @param {Function} [reduceFn]- called as reduceFn(resultArray); return value replaces result
74
+ * @returns {Array}
75
+ */
76
+ query(filter, mapFn, reduceFn) {
77
+ if (!core) return [];
78
+ return core.adhocQuery(filter, mapFn, reduceFn);
79
+ },
80
+ };
81
+ };
82
+
83
+ /**
84
+ * Called by unloadScript() in index.js to clean up subscriptions for the given script file.
85
+ * @param {string} scriptFile - absolute file path (matches scriptName used when registering)
86
+ */
87
+ module.exports.cleanup = function (scriptFile) {
88
+ removeListenersByScript(scriptFile);
89
+ };
@@ -0,0 +1,132 @@
1
+ /* eslint-disable func-name-matching, func-names, camelcase */
2
+
3
+ module.exports = function (she) {
4
+ /**
5
+ * @method now
6
+ * @returns {number} ms since epoch
7
+ */
8
+ she.now = function Sandbox_now() {
9
+ return new Date().getTime();
10
+ };
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
+ }
35
+ 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
+ }
55
+ combine();
56
+ she.mqttsub(srcs, { retain: true }, combine);
57
+ };
58
+
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
+ };
79
+
80
+ const timeouts = {};
81
+ /**
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
+ clearTimeout(timeouts[target]);
92
+ if (!she.getValue(target)) {
93
+ she.setValue(target, 1);
94
+ }
95
+ timeouts[target] = setTimeout(() => {
96
+ if (she.getValue(target)) {
97
+ she.setValue(target, 0);
98
+ }
99
+ }, time);
100
+ }
101
+ });
102
+
103
+ timeouts[target] = 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.
112
+ * @namespace she.mqtt
113
+ */
114
+ she.mqtt = {
115
+ /** Subscribe to one or more MQTT topics. Same signature as she.mqttsub(). */
116
+ sub: (...args) => she.mqttsub(...args),
117
+ /** Publish an MQTT message. Same signature as she.mqttpub(). */
118
+ pub: (...args) => she.mqttpub(...args),
119
+ /** Get the last-known value for a topic. */
120
+ get: (topic) => she.getValue(topic),
121
+ /** Set a value on one or more topics. */
122
+ set: (topic, val) => she.setValue(topic, val),
123
+ /** Get a specific property from a topic's state object. */
124
+ getProp: (...args) => she.getProp(...args),
125
+ /** Forward value changes from source topic(s) to target topic(s). */
126
+ link: (...args) => she.link(...args),
127
+ /** Seconds since the topic's value last changed. */
128
+ age: (topic) => she.age(topic),
129
+ /** Register a callback for MQTT connection lifecycle events ('connect' or 'disconnect'). */
130
+ on: (event, cb) => she._registerMqttEvent(event, cb),
131
+ };
132
+ };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ she.info('ready');