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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/web/assets/codicon-DCmgc-ay.ttf +0 -0
- package/dist/web/assets/index-BbwiXmS-.css +1 -0
- package/dist/web/assets/index-DD-XScWV.js +140 -0
- package/dist/web/assets/monaco-langs-DZ6hB11b.js +1423 -0
- package/dist/web/assets/monaco-langs-DyX1CsEw.css +1 -0
- package/dist/web/assets/tsMode-DxTbjAE2.js +16 -0
- package/dist/web/index.html +164 -0
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +13519 -0
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +256353 -0
- package/package.json +85 -10
- package/src/config.js +56 -0
- package/src/elastic.js +19 -0
- package/src/index.js +1192 -0
- package/src/influx.js +25 -0
- package/src/lib/mqtt-wildcards.js +34 -0
- package/src/lib/parse-payload.js +29 -0
- package/src/lib/redis.js +74 -0
- package/src/lib/shedb-core.js +447 -0
- package/src/lib/shedb-worker.js +126 -0
- package/src/lib/state-store.js +97 -0
- package/src/lib/storage.js +74 -0
- package/src/matter/controller.js +307 -0
- package/src/sandbox/api.js +57 -0
- package/src/sandbox/elastic-sandbox.js +88 -0
- package/src/sandbox/influx-sandbox.js +107 -0
- package/src/sandbox/matter-sandbox.js +92 -0
- package/src/sandbox/shedb-sandbox.js +89 -0
- package/src/sandbox/stdlib.js +132 -0
- package/src/scripts/hello.js +3 -0
- package/src/web/ai-api.js +443 -0
- package/src/web/auth.js +186 -0
- package/src/web/config-api.js +34 -0
- package/src/web/deps-api.js +138 -0
- package/src/web/git-api.js +188 -0
- package/src/web/log-ws.js +78 -0
- package/src/web/matter-api.js +102 -0
- package/src/web/mqtt-api.js +65 -0
- package/src/web/scripts-api.js +192 -0
- package/src/web/server.js +139 -0
- package/src/web/shedb-api.js +140 -0
- package/src/web/shedb.js +168 -0
- 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
|
+
};
|