Modified fault key input format
This commit is contained in:
parent
d2846a3b54
commit
2a43c5211a
@ -450,6 +450,12 @@ function syncFieldValues(device) {
|
|||||||
select.value = String(values[key] ?? "");
|
select.value = String(values[key] ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const input of els.groups.querySelectorAll("[data-int-key]")) {
|
||||||
|
const key = input.dataset.intKey;
|
||||||
|
if (!key || document.activeElement === input || Object.prototype.hasOwnProperty.call(pending, key)) continue;
|
||||||
|
input.value = values[key] != null ? String(values[key]) : "";
|
||||||
|
}
|
||||||
|
|
||||||
for (const toggle of els.groups.querySelectorAll("[data-publish-key]")) {
|
for (const toggle of els.groups.querySelectorAll("[data-publish-key]")) {
|
||||||
const key = toggle.dataset.publishKey;
|
const key = toggle.dataset.publishKey;
|
||||||
const on = enabled.has(key);
|
const on = enabled.has(key);
|
||||||
@ -507,6 +513,9 @@ function fieldRowHtml(field, value, enabled, share = null) {
|
|||||||
if (field.type === "enum") {
|
if (field.type === "enum") {
|
||||||
valueCell = `<td class="field-value">${enumSelectHtml(field, value, "data-enum-key", { raw: true })}</td>`;
|
valueCell = `<td class="field-value">${enumSelectHtml(field, value, "data-enum-key", { raw: true })}</td>`;
|
||||||
editor = "";
|
editor = "";
|
||||||
|
} else if (field.group === "faults") {
|
||||||
|
valueCell = `<td class="field-value">${faultIntegerHtml(field, value, "data-int-key")}</td>`;
|
||||||
|
editor = "";
|
||||||
} else if (field.type === "boolean") {
|
} else if (field.type === "boolean") {
|
||||||
const on = value === 1 || value === true || value === "1";
|
const on = value === 1 || value === true || value === "1";
|
||||||
editor = `
|
editor = `
|
||||||
@ -604,6 +613,16 @@ function enumSelectHtml(field, value, attr, { raw = false } = {}) {
|
|||||||
return `<select ${attr}="${escapeHtml(field.key)}" aria-label="${escapeHtml(field.key)}">${options}</select>`;
|
return `<select ${attr}="${escapeHtml(field.key)}" aria-label="${escapeHtml(field.key)}">${options}</select>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function faultIntegerHtml(field, value, attr) {
|
||||||
|
const shown = value == null || value === "" ? "" : String(value);
|
||||||
|
return `<input type="number" step="1" ${attr}="${escapeHtml(field.key)}" value="${escapeHtml(shown)}" aria-label="${escapeHtml(field.key)}" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function faultControlHtml(field, value) {
|
||||||
|
if (field.type === "enum") return enumSelectHtml(field, value, "data-fault-key");
|
||||||
|
return faultIntegerHtml(field, value, "data-fault-key");
|
||||||
|
}
|
||||||
|
|
||||||
function faultFields() {
|
function faultFields() {
|
||||||
return state.schema.fields.filter((field) => field.group === "faults");
|
return state.schema.fields.filter((field) => field.group === "faults");
|
||||||
}
|
}
|
||||||
@ -616,7 +635,7 @@ function buildFaultRows() {
|
|||||||
(field) => `
|
(field) => `
|
||||||
<div class="fault-row">
|
<div class="fault-row">
|
||||||
<span class="fault-key">${escapeHtml(field.key)}</span>
|
<span class="fault-key">${escapeHtml(field.key)}</span>
|
||||||
${enumSelectHtml(field, null, "data-fault-key")}
|
${faultControlHtml(field, null)}
|
||||||
<button type="button" data-fault-publish="${escapeHtml(field.key)}">Publish</button>
|
<button type="button" data-fault-publish="${escapeHtml(field.key)}">Publish</button>
|
||||||
<input class="fault-log" data-fault-log="${escapeHtml(field.key)}" readonly />
|
<input class="fault-log" data-fault-log="${escapeHtml(field.key)}" readonly />
|
||||||
</div>
|
</div>
|
||||||
@ -627,10 +646,10 @@ function buildFaultRows() {
|
|||||||
|
|
||||||
function renderFaultSelects(device) {
|
function renderFaultSelects(device) {
|
||||||
const values = device.telemetry || {};
|
const values = device.telemetry || {};
|
||||||
for (const select of document.querySelectorAll("[data-fault-key]")) {
|
for (const control of document.querySelectorAll("[data-fault-key]")) {
|
||||||
if (document.activeElement === select) continue;
|
if (document.activeElement === control) continue;
|
||||||
const key = select.dataset.faultKey;
|
const key = control.dataset.faultKey;
|
||||||
if (values[key] != null) select.value = String(values[key]);
|
if (values[key] != null) control.value = String(values[key]);
|
||||||
}
|
}
|
||||||
const logs = state.faultLogs[device.id] || {};
|
const logs = state.faultLogs[device.id] || {};
|
||||||
for (const input of document.querySelectorAll("[data-fault-log]")) {
|
for (const input of document.querySelectorAll("[data-fault-log]")) {
|
||||||
@ -666,8 +685,8 @@ async function publishFaultKeys(keys) {
|
|||||||
if (!device) return;
|
if (!device) return;
|
||||||
const body = {};
|
const body = {};
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const select = document.querySelector(`[data-fault-key="${CSS.escape(key)}"]`);
|
const control = document.querySelector(`[data-fault-key="${CSS.escape(key)}"]`);
|
||||||
body[key] = Number(select?.value);
|
body[key] = Number(control?.value);
|
||||||
}
|
}
|
||||||
const data = await api(`/api/devices/${device.id}/faults/publish`, {
|
const data = await api(`/api/devices/${device.id}/faults/publish`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -1154,9 +1173,11 @@ document.getElementById("aux-boost").addEventListener("click", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("fault-card").addEventListener("change", async (event) => {
|
document.getElementById("fault-card").addEventListener("change", async (event) => {
|
||||||
const select = event.target.closest("[data-fault-key]");
|
const control = event.target.closest("[data-fault-key]");
|
||||||
if (!select) return;
|
if (!control) return;
|
||||||
await saveFaultValue(select.dataset.faultKey, Number(select.value));
|
const numeric = Number(control.value);
|
||||||
|
if (!Number.isFinite(numeric)) return;
|
||||||
|
await saveFaultValue(control.dataset.faultKey, Math.trunc(numeric));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("fault-card").addEventListener("click", async (event) => {
|
document.getElementById("fault-card").addEventListener("click", async (event) => {
|
||||||
@ -1245,6 +1266,13 @@ els.groups.addEventListener("change", async (event) => {
|
|||||||
await saveFaultValue(enumSelect.dataset.enumKey, Number(enumSelect.value));
|
await saveFaultValue(enumSelect.dataset.enumKey, Number(enumSelect.value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const intInput = event.target.closest("[data-int-key]");
|
||||||
|
if (intInput) {
|
||||||
|
const numeric = Number(intInput.value);
|
||||||
|
if (!Number.isFinite(numeric)) return;
|
||||||
|
await saveFaultValue(intInput.dataset.intKey, Math.trunc(numeric));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const publishToggle = event.target.closest("[data-publish-key]");
|
const publishToggle = event.target.closest("[data-publish-key]");
|
||||||
if (!publishToggle) return;
|
if (!publishToggle) return;
|
||||||
const device = selectedDevice();
|
const device = selectedDevice();
|
||||||
|
|||||||
@ -297,6 +297,7 @@
|
|||||||
<button type="button" id="fault-publish-all">Publish all</button>
|
<button type="button" id="fault-publish-all">Publish all</button>
|
||||||
<input id="fault-log-all" class="fault-log" data-fault-log="all" readonly />
|
<input id="fault-log-all" class="fault-log" data-fault-log="all" readonly />
|
||||||
</div>
|
</div>
|
||||||
|
<p class="muted">Publish writes each key as a client attribute and as telemetry.</p>
|
||||||
<div id="fault-rows"></div>
|
<div id="fault-rows"></div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -610,7 +610,8 @@ select {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fault-row select {
|
.fault-row select,
|
||||||
|
.fault-row input[type="number"] {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
@ -621,7 +622,8 @@ select {
|
|||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-table .field-value select {
|
.telemetry-table .field-value select,
|
||||||
|
.telemetry-table .field-value input[type="number"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 560px;
|
min-width: 560px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const db = require("./db");
|
const db = require("./db");
|
||||||
const thingsboard = require("./thingsboard");
|
const thingsboard = require("./thingsboard");
|
||||||
const { VirtualDevice } = require("./virtualDevice");
|
const { VirtualDevice } = require("./virtualDevice");
|
||||||
const { defaultTelemetry, defaultPublishedKeys, normalizeTelemetry, telemetrySchema } = require("./telemetry");
|
const { defaultTelemetry, defaultPublishedKeys, normalizeTelemetry, telemetrySchema, X_STATE_KEYS } = require("./telemetry");
|
||||||
const { defaultPlant } = require("./plant");
|
const { defaultPlant } = require("./plant");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
|
|
||||||
@ -46,6 +46,12 @@ function bindDevice(device) {
|
|||||||
device.lastError = error.message;
|
device.lastError = error.message;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
device.onFaultPersist = (faults) => {
|
||||||
|
thingsboard.writeTimeseries(device.id, faults).catch((error) => {
|
||||||
|
device.lastError = error.message;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
device.ensureXStates = () => thingsboard.ensureXStateClientAttributes(device.id, X_STATE_KEYS);
|
||||||
devices.set(device.id, device);
|
devices.set(device.id, device);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +227,7 @@ function updateTelemetry(id, partial, { publish = true } = {}) {
|
|||||||
return device.status();
|
return device.status();
|
||||||
}
|
}
|
||||||
|
|
||||||
function publishFaults(id, partial) {
|
async function publishFaults(id, partial) {
|
||||||
const device = getOrThrow(id);
|
const device = getOrThrow(id);
|
||||||
const clean = {};
|
const clean = {};
|
||||||
for (const [key, raw] of Object.entries(partial || {})) {
|
for (const [key, raw] of Object.entries(partial || {})) {
|
||||||
@ -230,6 +236,11 @@ function publishFaults(id, partial) {
|
|||||||
}
|
}
|
||||||
device.setTelemetry(clean);
|
device.setTelemetry(clean);
|
||||||
persistTelemetry(device);
|
persistTelemetry(device);
|
||||||
|
try {
|
||||||
|
await thingsboard.ensureXStateClientAttributes(device.id, X_STATE_KEYS);
|
||||||
|
} catch (error) {
|
||||||
|
device.lastError = error.message;
|
||||||
|
}
|
||||||
const sent = device.publishObject(clean);
|
const sent = device.publishObject(clean);
|
||||||
return { device: device.status(), sent };
|
return { device: device.status(), sent };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,31 +2,6 @@ function enumIndex(names) {
|
|||||||
return names.map((name, value) => ({ name, value }));
|
return names.map((name, value) => ({ name, value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const SENSOR_FAULTS = enumIndex([
|
|
||||||
"SensorFaultNoBitShiftVoltageGrid",
|
|
||||||
"SensorFaultNoBitShiftVoltageCtrl",
|
|
||||||
"SensorFaultNoBitShiftVoltageHotWater",
|
|
||||||
"SensorFaultNoBitShiftCurrentGrid",
|
|
||||||
"SensorFaultNoBitShiftCurrentSolar",
|
|
||||||
"SensorFaultNoBitShiftCurrentLoad",
|
|
||||||
"SensorFaultNoBitShiftCurrentHotWaterIn",
|
|
||||||
"SensorFaultNoBitShiftCurrentHotWaterOut",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const AC_BUCK_FAULTS = enumIndex([
|
|
||||||
"AcBuckFaultNoBitShiftSystemFault",
|
|
||||||
"AcBuckFaultNoBitShiftPwmBreakInput",
|
|
||||||
"AcBuckFaultNoBitShiftSettingsUpdated",
|
|
||||||
"AcBuckFaultNoBitShiftOutputVoltageWhenNotRequested",
|
|
||||||
"AcBuckFaultNoBitShiftOutputCurrentWhenNotRequested",
|
|
||||||
"AcBuckFaultNoBitShiftVoltageGridHigh",
|
|
||||||
"AcBuckFaultNoBitShiftCurrentHotWaterInHigh",
|
|
||||||
"AcBuckFaultNoBitShiftCurrentHotWaterOutHigh",
|
|
||||||
"AcBuckFaultNoBitShiftAdcCycleReadCountTooHigh",
|
|
||||||
"AcBuckFaultNoBitShiftZcdFault",
|
|
||||||
"AcBuckFaultNoBitShiftPowerSupplyLow",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const AC_BUCK_STATES = enumIndex([
|
const AC_BUCK_STATES = enumIndex([
|
||||||
"enumAcBuckStateFault",
|
"enumAcBuckStateFault",
|
||||||
"enumAcBuckStateStandby",
|
"enumAcBuckStateStandby",
|
||||||
@ -37,18 +12,6 @@ const AC_BUCK_STATES = enumIndex([
|
|||||||
"enumAcBuckStateRampDownToStandby",
|
"enumAcBuckStateRampDownToStandby",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SYS_FAULTS = enumIndex([
|
|
||||||
"SystemFaultNoBitShiftGridVoltage",
|
|
||||||
"SystemFaultNoBitShiftGridFrequency",
|
|
||||||
"SystemFaultNoBitShiftTemperature",
|
|
||||||
"SystemFaultNoBitShiftPowerSupply",
|
|
||||||
"SystemFaultNoBitShiftHotWaterCurrentHighIn",
|
|
||||||
"SystemFaultNoBitShiftHotWaterCurrentHighOut",
|
|
||||||
"SystemFaultNoBitShiftSensorFault",
|
|
||||||
"SystemFaultNoBitShiftRS485Fault",
|
|
||||||
"SystemFaultNoBitShiftReboot",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const TELEMETRY_FIELDS = [
|
const TELEMETRY_FIELDS = [
|
||||||
{ key: "solarPower_s", label: "Solar generation", unit: "W", group: "power", type: "number", default: 3200 },
|
{ key: "solarPower_s", label: "Solar generation", unit: "W", group: "power", type: "number", default: 3200 },
|
||||||
{ key: "loadPower_s", label: "Home load", unit: "W", group: "power", type: "number", default: 1500 },
|
{ key: "loadPower_s", label: "Home load", unit: "W", group: "power", type: "number", default: 1500 },
|
||||||
@ -99,25 +62,61 @@ const TELEMETRY_FIELDS = [
|
|||||||
{ key: "rlyInSel_s", label: "Control load heating", unit: "", group: "status", type: "boolean", default: 0 },
|
{ key: "rlyInSel_s", label: "Control load heating", unit: "", group: "status", type: "boolean", default: 0 },
|
||||||
{ key: "xCntlLoadState", label: "Control load state", unit: "", group: "status", type: "integer", default: 0 },
|
{ key: "xCntlLoadState", label: "Control load state", unit: "", group: "status", type: "integer", default: 0 },
|
||||||
|
|
||||||
{ key: "xAcBuckFault", label: "AC buck fault", unit: "", group: "faults", type: "enum", default: null, options: AC_BUCK_FAULTS },
|
{ key: "xAcBuckFault", label: "AC buck fault", unit: "", group: "faults", type: "integer", default: null },
|
||||||
{ key: "xAcBuckState", label: "AC buck state", unit: "", group: "faults", type: "enum", default: null, options: AC_BUCK_STATES },
|
{ key: "xAcBuckState", label: "AC buck state", unit: "", group: "faults", type: "enum", default: null, options: AC_BUCK_STATES },
|
||||||
{ key: "xSensorFault", label: "Sensor fault", unit: "", group: "faults", type: "enum", default: null, options: SENSOR_FAULTS },
|
{ key: "xSensorFault", label: "Sensor fault", unit: "", group: "faults", type: "integer", default: null },
|
||||||
{ key: "xSysFault", label: "System fault", unit: "", group: "faults", type: "enum", default: null, options: SYS_FAULTS },
|
{ key: "xSysFault", label: "System fault", unit: "", group: "faults", type: "integer", default: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
const GROUPS = [
|
const GROUPS = [
|
||||||
{ id: "status", title: "Status", subtitle: "Tank temp, battery SoC, aux / control-load flags" },
|
{ id: "status", title: "Status", subtitle: "Tank temp, battery SoC, aux / control-load flags" },
|
||||||
{ id: "faults", title: "Faults", subtitle: "Same enum as the Faults card. Publish sends the enum index." },
|
{ id: "faults", title: "Faults", subtitle: "xAcBuckState is an enum. The other three keys take an integer. Publish sends the value as a client attribute and as telemetry." },
|
||||||
{ id: "power", title: "Power", subtitle: "Instantaneous watts published as *_s" },
|
{ id: "power", title: "Power", subtitle: "Instantaneous watts published as *_s" },
|
||||||
{ id: "flows", title: "Energy flows", subtitle: "Node-to-node power used by the live flow UI" },
|
{ id: "flows", title: "Energy flows", subtitle: "Node-to-node power used by the live flow UI" },
|
||||||
{ id: "energy", title: "Energy today", subtitle: "Watt-hours for daily totals" },
|
{ id: "energy", title: "Energy today", subtitle: "Watt-hours for daily totals" },
|
||||||
{ id: "validity", title: "Validity", subtitle: "vPwrReadings_s bitmask for live power keys" },
|
{ id: "validity", title: "Validity", subtitle: "vPwrReadings_s bitmask for live power keys" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const FAULT_KEYS = TELEMETRY_FIELDS.filter((field) => field.group === "faults").map((field) => field.key);
|
||||||
|
|
||||||
|
const X_STATE_KEYS = [
|
||||||
|
"xSysFault",
|
||||||
|
"xSensorFault",
|
||||||
|
"xAcBuckFault",
|
||||||
|
"xAuxRelayStatus",
|
||||||
|
"xHoursLastHot",
|
||||||
|
"xThermoState",
|
||||||
|
"xHotWaterStatus",
|
||||||
|
"xCntlLoadState",
|
||||||
|
"xAcBuckState",
|
||||||
|
];
|
||||||
|
|
||||||
function telemetryKeys() {
|
function telemetryKeys() {
|
||||||
return TELEMETRY_FIELDS.map((field) => field.key);
|
return TELEMETRY_FIELDS.map((field) => field.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickFaultTelemetry(payload) {
|
||||||
|
const faults = {};
|
||||||
|
for (const key of FAULT_KEYS) {
|
||||||
|
const value = payload?.[key];
|
||||||
|
if (value == null || (typeof value === "number" && Number.isNaN(value))) continue;
|
||||||
|
faults[key] = value;
|
||||||
|
}
|
||||||
|
return faults;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withXStateDefaults(payload, telemetry = {}) {
|
||||||
|
const body = { ...(payload || {}) };
|
||||||
|
for (const key of X_STATE_KEYS) {
|
||||||
|
const current = body[key];
|
||||||
|
if (current != null && !(typeof current === "number" && Number.isNaN(current))) continue;
|
||||||
|
const existing = telemetry[key];
|
||||||
|
const numeric = existing == null || existing === "" ? 0 : Number(existing);
|
||||||
|
body[key] = Number.isFinite(numeric) ? Math.trunc(numeric) : 0;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
function defaultTelemetry() {
|
function defaultTelemetry() {
|
||||||
const values = {};
|
const values = {};
|
||||||
for (const field of TELEMETRY_FIELDS) {
|
for (const field of TELEMETRY_FIELDS) {
|
||||||
@ -184,7 +183,11 @@ function telemetrySchema() {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
TELEMETRY_FIELDS,
|
TELEMETRY_FIELDS,
|
||||||
GROUPS,
|
GROUPS,
|
||||||
|
FAULT_KEYS,
|
||||||
|
X_STATE_KEYS,
|
||||||
telemetryKeys,
|
telemetryKeys,
|
||||||
|
pickFaultTelemetry,
|
||||||
|
withXStateDefaults,
|
||||||
defaultTelemetry,
|
defaultTelemetry,
|
||||||
defaultPublishedKeys,
|
defaultPublishedKeys,
|
||||||
normalizeTelemetry,
|
normalizeTelemetry,
|
||||||
|
|||||||
@ -312,6 +312,61 @@ async function writeSharedAttributes(deviceId, attrs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getClientAttributes(deviceId, keys) {
|
||||||
|
try {
|
||||||
|
return await withAuth(async (token) => {
|
||||||
|
const { data } = await http.get(`/api/plugins/telemetry/DEVICE/${deviceId}/values/attributes/CLIENT_SCOPE`, {
|
||||||
|
headers: authHeaders(token),
|
||||||
|
params: { keys: keys.join(",") },
|
||||||
|
});
|
||||||
|
const values = {};
|
||||||
|
for (const item of data || []) {
|
||||||
|
if (item?.key != null && item.value != null && item.value !== "") {
|
||||||
|
values[item.key] = item.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw tbError(error, "Failed to read ThingsBoard client attributes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeClientAttributes(deviceId, attrs) {
|
||||||
|
try {
|
||||||
|
await withAuth(async (token) => {
|
||||||
|
await http.post(`/api/plugins/telemetry/${deviceId}/CLIENT_SCOPE`, attrs, {
|
||||||
|
headers: { ...authHeaders(token), "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw tbError(error, "Failed to write ThingsBoard client attributes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureXStateClientAttributes(deviceId, keys) {
|
||||||
|
const existing = await getClientAttributes(deviceId, keys);
|
||||||
|
const missing = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
if (existing[key] == null) missing[key] = 0;
|
||||||
|
}
|
||||||
|
if (!Object.keys(missing).length) return missing;
|
||||||
|
await writeClientAttributes(deviceId, missing);
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeTimeseries(deviceId, values) {
|
||||||
|
try {
|
||||||
|
await withAuth(async (token) => {
|
||||||
|
await http.post(`/api/plugins/telemetry/DEVICE/${deviceId}/timeseries/ANY`, values, {
|
||||||
|
headers: { ...authHeaders(token), "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw tbError(error, "Failed to write ThingsBoard timeseries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createDevice,
|
createDevice,
|
||||||
deleteDevice,
|
deleteDevice,
|
||||||
@ -321,5 +376,8 @@ module.exports = {
|
|||||||
getDeviceCredentials,
|
getDeviceCredentials,
|
||||||
adoptDeviceByName,
|
adoptDeviceByName,
|
||||||
writeSharedAttributes,
|
writeSharedAttributes,
|
||||||
|
writeClientAttributes,
|
||||||
|
ensureXStateClientAttributes,
|
||||||
|
writeTimeseries,
|
||||||
ping,
|
ping,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const mqtt = require("mqtt");
|
const mqtt = require("mqtt");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { normalizeTelemetry, normalizePublishedKeys } = require("./telemetry");
|
const { normalizeTelemetry, normalizePublishedKeys, pickFaultTelemetry, withXStateDefaults } = require("./telemetry");
|
||||||
const {
|
const {
|
||||||
SHARED_KEYS,
|
SHARED_KEYS,
|
||||||
normalizePlant,
|
normalizePlant,
|
||||||
@ -43,6 +43,7 @@ class VirtualDevice {
|
|||||||
this.onTelemetryChange = null;
|
this.onTelemetryChange = null;
|
||||||
this.onPlantChange = null;
|
this.onPlantChange = null;
|
||||||
this.onSharedWrite = null;
|
this.onSharedWrite = null;
|
||||||
|
this.onFaultPersist = null;
|
||||||
this.startHeatLoop();
|
this.startHeatLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +81,20 @@ class VirtualDevice {
|
|||||||
this.client.subscribe("v1/devices/me/attributes/response/+");
|
this.client.subscribe("v1/devices/me/attributes/response/+");
|
||||||
this.requestSharedAttributes();
|
this.requestSharedAttributes();
|
||||||
this.startAttrPoll();
|
this.startAttrPoll();
|
||||||
|
const startPublishing = () => {
|
||||||
if (this.telemetryEnabled) this.startTelemetry();
|
if (this.telemetryEnabled) this.startTelemetry();
|
||||||
this.publishTelemetry();
|
this.publishTelemetry();
|
||||||
|
};
|
||||||
|
if (this.ensureXStates) {
|
||||||
|
this.ensureXStates()
|
||||||
|
.then(startPublishing)
|
||||||
|
.catch((error) => {
|
||||||
|
this.lastError = error.message;
|
||||||
|
startPublishing();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
startPublishing();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on("reconnect", () => {
|
this.client.on("reconnect", () => {
|
||||||
@ -405,10 +418,11 @@ class VirtualDevice {
|
|||||||
|
|
||||||
publishTelemetry() {
|
publishTelemetry() {
|
||||||
if (!this.client || !this.connected) return;
|
if (!this.client || !this.connected) return;
|
||||||
const payload = this.telemetryPayload();
|
const payload = withXStateDefaults(this.telemetryPayload(), this.telemetry);
|
||||||
if (!Object.keys(payload).length) return;
|
if (!Object.keys(payload).length) return;
|
||||||
this.client.publish("v1/devices/me/telemetry", JSON.stringify(payload), { qos: 1 });
|
this.client.publish("v1/devices/me/telemetry", JSON.stringify(payload), { qos: 1 });
|
||||||
this.lastPublishedAt = Date.now();
|
this.lastPublishedAt = Date.now();
|
||||||
|
this.persistFaultChannels(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
publishKeys(keys) {
|
publishKeys(keys) {
|
||||||
@ -425,11 +439,19 @@ class VirtualDevice {
|
|||||||
body[key] = value;
|
body[key] = value;
|
||||||
}
|
}
|
||||||
if (!Object.keys(body).length) return false;
|
if (!Object.keys(body).length) return false;
|
||||||
this.client.publish("v1/devices/me/telemetry", JSON.stringify(body), { qos: 1 });
|
const outbound = withXStateDefaults(body, this.telemetry);
|
||||||
|
this.client.publish("v1/devices/me/telemetry", JSON.stringify(outbound), { qos: 1 });
|
||||||
this.lastPublishedAt = Date.now();
|
this.lastPublishedAt = Date.now();
|
||||||
|
this.persistFaultChannels(outbound);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
persistFaultChannels(payload) {
|
||||||
|
const faults = pickFaultTelemetry(payload);
|
||||||
|
if (!Object.keys(faults).length || !this.onFaultPersist) return;
|
||||||
|
this.onFaultPersist(faults);
|
||||||
|
}
|
||||||
|
|
||||||
publishAttributes(attrs) {
|
publishAttributes(attrs) {
|
||||||
if (!this.client || !this.connected) return;
|
if (!this.client || !this.connected) return;
|
||||||
this.client.publish("v1/devices/me/attributes", JSON.stringify(attrs));
|
this.client.publish("v1/devices/me/attributes", JSON.stringify(attrs));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user