UNPKG

@jpadie/node-red-virtual-matter-devices

Version:
708 lines 28.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseEndpoint = void 0; require("@matter/main"); const server_1 = require("../server/server"); const behaviors_1 = require("@matter/main/behaviors"); const main_1 = require("@matter/main"); class BaseEndpoint { withs = []; mapping = {}; config = {}; node; endpoint; context; Context; attributes = {}; name = ""; state = false; skip = false; confirmations = {}; awaitingConfirmation = {}; device = null; Behaviours = {}; filters; constructor(node, config, name = "") { this.node = node; this.config = config; for (const item in this.config) { if (!isNaN(+this.config[item])) { this.config[item] = +this.config[item]; } } if (!Object.hasOwn(this.config, "telemetryInterval") || this.config.telemetryInterval < 60) { this.config.telemetryInterval = 60; } this.Context = node.context(); this.context = this.Context.get("attributes") || {}; this.name = this.config.name || name; this.attributes = { id: this.node.id, bridgedDeviceBasicInformation: { vendorName: "matter.js virtual device", nodeLabel: this.name, productName: this.name, productLabel: this.name, serialNumber: `${node.id}`.substring(0, 32), reachable: true, uniqueId: this.node.id } }; this.node.status({ fill: "grey", shape: "dot", text: "offline" }); this.withs.push(behaviors_1.BridgedDeviceBasicInformationServer); } addFilter(name, callback) { if (Object.hasOwn(this.filters, name)) { this.filters[name].push(callback); } else { this.filters = Object.assign(this.filters, { [name]: [callback] }); } } removeFilter(name, callback) { if (Object.hasOwn(this.filters, name)) { let index = this.filters[name].indexOf(callback); if (index > -1) { this.filters[name][index].splice(index, 1); } } } callFilters(name, ...args) { if (Object.hasOwn(this.filters, name)) { for (let func of this.filters[name]) { func(...args); } } } getEnumKeyByEnumValue(myEnum, enumValue) { let keys = Object.keys(myEnum).filter(x => myEnum[x] == enumValue); return keys.length > 0 ? keys[0] : null; } addBehaviour(behaviour) { let e; if (Object.hasOwn(this.Behaviours, behaviour)) { e = new this.Behaviours[behaviour](this.config); this.attributes = Object.assign(this.attributes, e.attributes); this.mapping = Object.assign(this.mapping, e.mapping); this.withs.push(e.withs); } else { this.node.error(`Behaviour ${behaviour} is unknown`); } } setSerialNumber(value) { if (this.attributes.bridgedDeviceBasicInformation.serialNumber.includes("-")) { let dash = this.attributes.bridgedDeviceBasicInformation.serialNumber.indexOf("-"); this.attributes.bridgedDeviceBasicInformation.serialNumber = value + this.attributes.bridgedDeviceBasicInformation.serialNumber.slice(dash + 1); } else { this.attributes.bridgedDeviceBasicInformation.serialNumber = (value + this.attributes.bridgedDeviceBasicInformation.serialNumber).substring(0, 32); } } prune(item) { if (Object.hasOwn(this.mapping, item)) { if (!delete this.mapping[item]) { this.node.error("Error deleting mapping item: " + item); } } if (Object.hasOwn(this.context, item)) { delete this.context[item]; } } regularUpdate() { if (this.config.regularUpdates) { setInterval(() => { let update = {}; for (const item in this.context) { let value = this.getVerbose(item, this.context[item]); if (value != this.context[item]) { update[`${item}_in_words`] = value; } } this.node.send({ payload: { ...this.context, ...update }, topic: "regular update" }); }, this.config.telemetryInterval * 1000); } } zigbee() { return false; } async deploy() { if (this.device) { try { this.endpoint = await new main_1.Endpoint(this.device.with(...this.withs), this.attributes); } catch (e) { this.node.error(e); console.trace(e); this.node.error(`device is : ${JSON.stringify(this.device, null, 2)}`); this.node.error(`withs are : ${JSON.stringify(this.withs, null, 2)}`); this.node.error(`attributes are: ${JSON.stringify(this.attributes, null, 2)}`); } } } cleanUp() { this.node.debug("cleaning up old context entries"); let mappings = Object.keys(this.mapping); this.node.debug(`Permitted keys: ${JSON.stringify(mappings, null, 2)}`); for (let item in this.context) { this.node.debug(`testing context item: ${item}`); if (item == "lastHeardFrom") continue; if (item.includes("_in_words")) continue; if (!mappings.includes(item)) { this.node.debug(`Found prohibited item: ${item}`); this.prune(item); this.prune(`${item}_in_words`); } } this.saveContext(); } dumpSchemas() { for (const item of ["config", "attributes", "context"]) { this.node.debug(`${item} for ${this.name}`); this.node.debug(JSON.stringify(this[item], null, 2)); } } async getEndpoint() { if (server_1.matterHub) { try { this.cleanUp(); await this.deploy(); this.dumpSchemas(); server_1.matterHub.addDevice(this.endpoint); await this.endpoint.construction; this.regularUpdate(); this.listen(); this.setStatus(); this.setDefault("lastHeardFrom", ""); this.saveContext(); setTimeout(async () => { await this.syncContext(); }, 2000); } catch (e) { this.node.error(e); console.trace(); } } else { this.node.status({ fill: "grey", shape: "dot", text: "No Matter Hub Configured" }); } } async clearConfirmation(item) { this.node.debug(`checking for confirmation for item ${item}. Current confirmations awaiting are ${JSON.stringify(this.awaitingConfirmation, null, 2)}`); if (Object.hasOwn(this.awaitingConfirmation, item)) { delete this.awaitingConfirmation[item]; clearTimeout(this.confirmations[item]); this.node.debug(`Confirmation ${item} cleared`); return true; } this.node.debug(`No confirmations cleared for ${item}`); return false; } async awaitConfirmation(item, value) { this.node.debug(`setting a confirmation await flag for item ${item}`); this.clearConfirmation(item); this.awaitingConfirmation[item] = value; this.confirmations[item] = setTimeout((item) => { this.node.debug(`timeout for confirmation ${item}. Clearing now.`); this.clearConfirmation(item); }, 1000, item); this.saveContext(); } now() { let n = Date.now(); let d = new Date(n); return d.toUTCString(); } saveContext() { this.Context.set('attributes', this.context); } async getStatusText() { let keys = Object.keys(this.mapping); let text = await this.getVerbose(keys[0], this.context[keys[0]]) + (this.mapping[keys[0]].unit || ""); this.node.debug(`top level status text: ${text}`); return text; } async setStatus() { this.node.status({ fill: "green", shape: "dot", text: await this.getStatusText() }); } setDefault(item, value) { if (!Object.hasOwn(this.context, item) || this.context[item] == null || this.context[item] == "") { this.context[item] = value; } } refine(value, decimals = 0) { if (Object.is(value, undefined) || Object.is(value, null)) { value = 0; } if (Number.isNaN(value)) { return value; } else { if (decimals == 0) { return Math.round(value); } else { const e = Math.pow(10, decimals); return Math.round(e * value) / e; } } } matterRefine(item, value) { this.node.debug("refining values for matter"); this.node.debug("item: " + item); this.node.debug("value: " + value); let ret = value; if (value != 0) { if (Object.hasOwn(this.mapping[item], "matter")) { if (Object.hasOwn(this.mapping[item].matter, "valueType")) { switch (this.mapping[item].matter.valueType) { case "int": ret = this.refine(ret, 0); break; case "float": if (Object.hasOwn(this.mapping[item].matter, "valueDecimals")) { ret = this.refine(ret, this.mapping[item].matter.valueDecimals); } else { ret = this.refine(ret, 2); } break; case "boolean": ret = value ? true : false; break; default: } } } } if (Object.hasOwn(this.mapping[item], "permittedValues")) { if (!this.mapping[item].permittedValues.includes(ret)) { this.node.debug(`Dropping update as value ${ret} is not a permitted value`); return null; } } this.node.debug("refined values for matter"); this.node.debug("item: " + item); this.node.debug("value: " + ret); return ret; } contextRefine(item, value) { let ret = value; this.node.debug(`Refining value for context: item ${item} and value ${value}`); if (Object.hasOwn(this.mapping, item)) { if (Object.hasOwn(this.mapping[item], "context")) { if (Object.hasOwn(this.mapping[item].context, "valueType")) { switch (this.mapping[item].context.valueType) { case "int": ret = this.refine(value); break; case "float": if (Object.hasOwn(this.mapping[item].context, "valueDecimals")) { ret = this.refine(value, this.mapping[item].context.valueDecimals); } else { ret = this.refine(value, 2); } break; default: ret = value; } } } } if (Object.hasOwn(this.mapping[item], "permittedValues")) { if (!this.mapping[item].permittedValues.includes(ret)) { this.node.debug(`Dropping update as value ${ret} is not a permitted value`); return null; } } this.node.debug(`Refined value for context: item ${item} and value ${ret}`); return ret; } getVerbose(item, value) { switch (item) { default: return value; } } preProcessDeviceChanges(value, item) { if (item) return value; return value; } async preProcessOutputReport(report) { return report; } async listenForChange() { await this.endpoint.construction; for (const item in this.mapping) { let keys = Object.keys(this.mapping[item]); let key = keys[0]; if (typeof this.mapping[item][key] == "string") { let s = `${this.mapping[item][key]}$Changed`; this.node.debug(`listening at ${key}.${s}`); try { this.endpoint.events[key][s].on(async (value) => { this.node.debug(`Message received from Matter key: ${key}, item: ${s}, value: ${value}`); let v = this.preProcessDeviceChanges(value, s); if ((this.skip)) { this.skip = false; } else { v = this.matterToContext(item, v); if (v === null) { this.node.debug(`skipping as not a permitted value or otherwise NULL`); return; } this.context[item] = v; this.node.debug(`setting context item ${item} to ${value}`); this.context.lastHeardFrom = this.now(); if (!await this.clearConfirmation(item)) { this.node.debug(`no confirmations awaiting so this is a matter originating message and can be reported`); let v = this.getVerbose(item, this.context[item]); let report = { [item]: this.context[item], unit: this.mapping[item].unit, lastHeardFrom: this.context.lastHeardFrom, messageSource: "Matter" }; if (v !== this.context[item]) { report = Object.assign(report, { [`${item}_in_words`]: v }); } if (this.mapping[item].unit == "") { delete report.unit; } this.node.debug(`About to send report of the matter change. Report is ${JSON.stringify(report, null, 2)}`); report = await this.preProcessOutputReport(report); this.node.debug(`Report of a matter change has been pre-processed. Report is ${JSON.stringify(report, null, 2)}`); this.node.send({ payload: report }); this.node.debug(`Report of a matter change has been sent and is now being post-processed`); this.listenForChange_postProcess(report); } else { this.node.debug(`confirmations awaiting for item ${item} so this is a not a matter originating instruction and should not be reported`); } this.saveContext(); this.setStatus(); } }); } catch (e) { this.node.error("error in setting up listener"); this.node.error(e); console.trace(); } } else if (typeof this.mapping[item][key] == "object") { let ks = Object.keys(this.mapping[item][key]); let k = ks[0]; let s = `${k}$Changed`; this.node.debug(`listening at ${key}.${s}`); try { this.endpoint.events[key][s].on(async (value) => { if (await this.clearConfirmation(item) === true) { this.node.debug(`Confirmations awaiting for this matter change. so this does not originate from matter and should not be reported`); return; } this.node.debug(`no confirmations awaiting so this is a matter originating message and can be reported`); value = this.preProcessDeviceChanges(value, s); if (this.skip) { this.skip = false; } else { if (typeof value == "number") { if (typeof this.mapping[item].multiplier == "object") { this.context[item] = this.mapping[item].multiplier[1](value); } else { this.context[item] = value / this.mapping[item].multiplier; } } else { this.context[item] = value; } this.context.lastHeardFrom = this.now(); if (!this.clearConfirmation(item)) { let report = { [item]: this.getVerbose(item, this.context[item]), unit: this.mapping[item].unit, lastHeardFrom: this.context.lastHeardFrom }; if (this.mapping[item].unit == "") delete report.unit; report = await this.preProcessOutputReport(report); this.node.send({ payload: report }); this.listenForChange_postProcess(report); } this.saveContext(); this.setStatus(); } }); } catch (e) { this.node.error("error in setting up listener"); this.node.error(e); console.trace(); } } } } listenForChange_postProcess(report = {}) { if (report) return; return; } async preProcessNodeRedInput(item, value) { return { a: item, b: value }; } async processIncomingItem(item, value) { this.node.debug(`after pre processing item is now ${item} value is now ${value}`); let updates = []; if (Object.hasOwn(this.mapping, item)) { let v; if (this.context[item] != value) { v = this.contextToMatter(item, value); if (v == null) return; const keys = Object.keys(this.mapping[item]); let key = keys[0]; if (typeof this.mapping[item][key] == "string") { updates.push({ [key]: { [this.mapping[item][key]]: v } }); } else if (typeof this.mapping[item][key] == "object") { const ks = Object.keys(this.mapping[item][key]); let k = ks[0]; updates.push({ [key]: { [k]: { [this.mapping[item][key][k]]: v } } }); } await this.awaitConfirmation(item, v); } } if (updates.length > 0) { this.node.debug("raw updates: "); this.node.debug(JSON.stringify(updates, null, 2)); this.node.debug(`transforming array into object`); let u = {}; for (let i = 0; i < updates.length; i++) { u = Object.assign(u, updates[i]); } u = await this.preProcessMatterUpdate(u); this.node.debug(`Object created from array: ${JSON.stringify(u, null, 2)}`); return u; } else { this.node.debug(`Zero length update`); return {}; } } async preProcessMatterUpdate(update) { return update; } async processIncomingMessages(msg, send, done) { let update = {}; if (this.config.passThroughMessage) { send(msg); } try { if (typeof msg.payload == "object") { for (let item in msg.payload) { this.node.debug(`processing incoming message item ${item}`); if (!Object.keys(this.mapping).includes(item)) { this.node.debug(`skipping processing incoming message item ${item} as not in mapping`); continue; } let { a, b } = await this.preProcessNodeRedInput(item, msg.payload[item]); if (Array.isArray(a)) { for (let i = 0; i < a.length; i++) { let u = await this.processIncomingItem(a[i], b[i]); this.node.debug(`receiving processed item: {${JSON.stringify(u, null, 2)}}`); update = Object.assign(update, u); this.node.debug(`adding to update object: {${JSON.stringify(update, null, 2)}}`); } } else { let u = await this.processIncomingItem(a, b); this.node.debug(`receiving processed item: {${JSON.stringify(u, null, 2)}}`); update = Object.assign(update, u); this.node.debug(`adding to update object: {${JSON.stringify(update, null, 2)}}`); } } this.node.debug(`full update for matter device : ${JSON.stringify(update, null, 2)}`); if (Object.keys(update).length > 0) { update = await this.preProcessMatterUpdate(update); try { await this.endpoint.construction; this.node.debug("requested update"); this.node.debug(JSON.stringify(update, null, 2)); await this.endpoint.set(update); } catch (e) { this.node.error(e); this.node.debug(JSON.stringify(update, null, 2)); console.trace(); } } } } catch (e) { if (e instanceof Error) { this.node.error(e); this.node.error(JSON.stringify(e.stack, null, 2)); if (done) { done(e); } } else { this.node.error(e); if (done) { done(e); } } } } listenForMessages() { this.node.on("input", (msg, send, done) => { this.node.debug("incoming message in listen for messages"); this.node.debug(JSON.stringify(msg, null, 2)); if (Object.hasOwn(msg, "payload") && typeof msg.payload == "object") { msg.payload = Object.assign(msg.payload, { messageSource: "node-red input" }); } else { Object.assign(msg, { payload: { messageSource: "node-red input" } }); } this.processIncomingMessages(msg, send, done); }); } listen() { this.listenForChange(); this.listenForMessages(); this.listenForClose(); } listenForClose() { this.node.on("close", async () => { await server_1.matterHub.timeToClose(); }); } lcFirst(val) { return val.charAt(0).toLowerCase() + val.slice(1); } matterToContext(item, value) { this.node.debug(`Converting Matter value ${value} for item ${item} to Context value`); let v = value; if (typeof value == "number" && Object.hasOwn(this.mapping[item], "multiplier")) { if (typeof this.mapping[item].multiplier == "object") { v = this.mapping[item].multiplier[1](value); } else { v = value / this.mapping[item].multiplier; } } this.node.debug(`Converted Matter value to unrefined context value. Value: ${v} for item ${item} `); return this.contextRefine(item, v); } contextToMatter(item, value) { this.node.debug(`Converting a Context value to a Matter value. Item: ${item} Value: ${value}`); let v = value; if (!Object.hasOwn(this.mapping, item)) { this.node.debug(`Cannot proceed as there is no mapping for item ${item}`); return value; } if (typeof value == "number" && Object.hasOwn(this.mapping[item], "multiplier")) { if (typeof this.mapping[item].multiplier == "object") { v = this.mapping[item].multiplier[0](v); } else { v = this.mapping[item].multiplier * v; } if (Object.hasOwn(this.mapping[item], "min")) { v = Math.max(this.mapping[item].min, v); } if (Object.hasOwn(this.mapping[item], "max")) { v = Math.min(this.mapping[item].max, v); } v = this.matterRefine(item, v); } return v; } async syncContext() { this.node.debug("Syncing Context"); await this.endpoint.construction; let state; try { state = this.endpoint.state; } catch (e) { this.node.error(`Problem retrieving endpoint state`); this.node.error(e); console.trace(); return; } this.node.debug(`Endpoint state: ${JSON.stringify(state, null, 2)}`); for (let item in this.mapping) { const keys = Object.keys(this.mapping[item]); const key = keys[0]; const subkey = this.mapping[item][key]; let c; if (typeof subkey != "object") { if (Object.hasOwn(state, key)) { if (Object.hasOwn(state[key], subkey)) { c = state[key][subkey]; this.node.debug(`retrieved state of ${key} ${subkey} (item: ${item}) as ${c}`); } } } else { const _keys = Object.keys(subkey); const _key = _keys[0]; const _value = subkey[_key]; if (Object.hasOwn(state, key)) { if (Object.hasOwn(state[key], _key)) { if (Object.hasOwn(state[key][_key], _value)) { c = state[key][_key][_value]; this.node.debug(`retrieved state of ${key} ${_key} ${_value} (item: ${item}) as ${c}`); } } } } if (c == undefined) { c = 0; } let v = this.matterToContext(item, c); if (v === null) { this.node.debug(`not setting context for ${item} as value reported as null or not permitted (which should not happen...)`); continue; } else { this.context[item] = v; } } this.saveContext(); this.setStatus(); } } exports.BaseEndpoint = BaseEndpoint; //# sourceMappingURL=BaseEndpoint.js.map