Files
MMM-IntelliCenter/node_helper.js
Parnic 254c3cad31
All checks were successful
Node.js CI / build (18.x) (push) Successful in 18s
Node.js CI / build (20.x) (push) Successful in 5s
Node.js CI / build (22.x) (push) Successful in 6s
Update node-intellicenter, log timeouts
2025-01-08 21:23:05 -06:00

451 lines
14 KiB
JavaScript

/* global module require clearInterval clearTimeout setTimeout setInterval config */
var NodeHelper = require("node_helper");
const { FindUnits, Unit } = require("node-intellicenter");
const messages = require("node-intellicenter/messages");
const Log = require("logger");
const reconnectDelayMs = 10 * 1000;
const unitFinderTimeoutMs = 5 * 1000;
let foundUnit;
const poolData = {
poolTemp: 0,
spaTemp: 0,
poolSetPoint: 0,
spaSetPoint: 0,
poolHeaterStatus: false,
spaHeaterStatus: false,
poolStatus: false,
spaStatus: false,
phVal: 0,
lastPHVal: 0,
phTank: 0,
orp: 0,
lastOrpVal: 0,
saltPPM: 0,
saturation: 0,
freezeMode: false,
circuits: {},
};
let poolObjnam = "B1101";
let spaObjnam = "B1202";
let refreshTimer;
let unitFinderRetry;
let unitReconnectTimer;
let intellichemObjnam = "";
let chlorinatorObjnam = "";
let freezeObjnam = "";
let initialConnectDone = false;
module.exports = NodeHelper.create({
setCircuit(circuitState) {
if (!foundUnit) {
return;
}
Log.info(
`[MMM-IntelliCenter] setting circuit ${circuitState.id} to ${!!circuitState.state}`,
);
foundUnit.send(
messages.SetObjectStatus(circuitState.id, !!circuitState.state),
);
},
setHeatpoint(heatpoint) {
if (!foundUnit) {
return;
}
let heatObjnam = "";
if (heatpoint.body === "spa") {
heatObjnam = spaObjnam;
} else if (heatpoint.body === "pool") {
heatObjnam = poolObjnam;
}
if (!heatObjnam) {
Log.error(
`[MMM-IntelliCenter] unable to determine objnam from given heatpoint body ${heatpoint.body} - expected "spa" or "pool"`,
);
return;
}
Log.info(
`[MMM-IntelliCenter] setting heatpoint for body ${heatObjnam} to ${heatpoint.temperature} deg`,
);
foundUnit.send(messages.SetSetpoint(heatObjnam, heatpoint.temperature));
},
setHeatstate(heatstate) {
if (!foundUnit) {
return;
}
let heatObjnam = "";
if (heatstate.body === "spa") {
heatObjnam = spaObjnam;
} else if (heatstate.body === "pool") {
heatObjnam = poolObjnam;
}
if (!heatObjnam) {
Log.error(
`[MMM-IntelliCenter] unable to determine objnam from given heatstate body ${heatstate.body} - expected "spa" or "pool"`,
);
return;
}
Log.info(
`[MMM-IntelliCenter] setting heat state for body ${heatObjnam} to ${!!heatstate.state}`,
);
foundUnit.send(messages.SetHeatMode(heatObjnam, !!heatstate.state));
},
setLightcmd(lightCmd) {
if (!foundUnit) {
return;
}
Log.info(`[MMM-IntelliCenter] sending light command ${lightCmd}`);
// NYI in node-intellicenter
// foundUnit.send(messages.SendLightCommand(lightCmd));
},
notifyReconnecting() {
this.sendSocketNotification("INTELLICENTER_RECONNECTING");
},
socketNotificationReceived(notification, payload) {
if (notification === "INTELLICENTER_CONFIG") {
if (!this.config) {
this.config = payload;
this.connect(
(status) => {
this.sendSocketNotification("INTELLICENTER_RESULT", status);
},
() => {
this.notifyReconnecting();
},
);
} else if (poolData) {
this.sendSocketNotification("INTELLICENTER_RESULT", poolData);
}
// If we don't have a status yet, assume the initial connection is still in progress and this socket notification will be delivered when setup is done
}
if (notification === "INTELLICENTER_CIRCUIT") {
this.setCircuit(payload);
}
if (notification === "INTELLICENTER_HEATPOINT") {
this.setHeatpoint(payload);
}
if (notification === "INTELLICENTER_HEATSTATE") {
this.setHeatstate(payload);
}
if (notification === "INTELLICENTER_LIGHTCMD") {
this.setLightcmd(payload);
}
},
resetFoundUnit() {
foundUnit = null;
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
if (unitFinderRetry) {
clearInterval(unitFinderRetry);
unitFinderRetry = null;
}
if (unitReconnectTimer) {
clearTimeout(unitReconnectTimer);
unitReconnectTimer = null;
}
},
setupUnit(cb, reconnectCb) {
Log.info("[MMM-IntelliCenter] initial connection to unit...");
initialConnectDone = false;
const reset = () => {
reconnectCb();
this.resetFoundUnit();
unitReconnectTimer = setTimeout(() => {
this.connect(cb, reconnectCb);
}, reconnectDelayMs);
};
foundUnit
.on("error", (e) => {
Log.error("[MMM-IntelliCenter] error in unit connection.");
Log.error(e);
})
.on("close", () => {
Log.error(
`[MMM-IntelliCenter] unit connection closed unexpectedly. restarting the connection process in ${reconnectDelayMs / 1000} seconds`,
);
reset();
})
.on("timeout", () => {
Log.error("[MMM-IntelliCenter] unit connection timed out.");
})
.on("notify", (msg) => {
for (const obj of msg.objectList) {
if (obj.objnam === intellichemObjnam) {
Log.info("[MMM-IntelliCenter] received chemical update");
if (obj.params.ORPVAL) {
poolData.orp = parseInt(obj.params.ORPVAL);
}
if (obj.params.PHVAL) {
poolData.phVal = parseFloat(obj.params.PHVAL);
}
if (obj.params.PHTNK) {
poolData.phTank = parseInt(obj.params.PHTNK);
}
if (obj.params.QUALTY) {
poolData.saturation = parseFloat(obj.params.QUALTY);
}
if (poolData.phVal !== 0) {
poolData.lastPHVal = poolData.phVal;
}
if (poolData.orp !== 0) {
poolData.lastOrpVal = poolData.orp;
}
} else if (obj.objnam === poolObjnam) {
Log.info("[MMM-IntelliCenter] received pool update");
if (obj.params.LOTMP) {
poolData.poolSetPoint = parseInt(obj.params.LOTMP);
}
// todo: is MODE the right check for this?
if (obj.params.MODE) {
poolData.poolHeaterStatus = obj.params.MODE === "11";
}
if (obj.params.STATUS) {
poolData.poolStatus = obj.params.STATUS === "ON";
}
if (obj.params.LSTTMP) {
poolData.poolTemp = parseInt(obj.params.LSTTMP);
}
} else if (obj.objnam === spaObjnam) {
Log.info("[MMM-IntelliCenter] received spa update");
if (obj.params.LOTMP) {
poolData.spaSetPoint = parseInt(obj.params.LOTMP);
}
// todo: is MODE the right check for this?
if (obj.params.MODE) {
poolData.spaHeaterStatus = obj.params.MODE === "11";
}
if (obj.params.STATUS) {
poolData.spaStatus = obj.params.STATUS === "ON";
}
if (obj.params.LSTTMP) {
poolData.spaTemp = parseInt(obj.params.LSTTMP);
}
} else if (obj.objnam === chlorinatorObjnam) {
Log.info("[MMM-IntelliCenter] received chlorinator update");
if (obj.params.SALT) {
poolData.saltPPM = parseInt(obj.params.SALT);
}
} else if (obj.objnam === freezeObjnam) {
Log.info("[MMM-IntelliCenter] received freeze-protection update");
if (obj.params.STATUS) {
poolData.freezeMode = obj.params.STATUS === "ON";
}
} else if (Object.keys(poolData.circuits).includes(obj.objnam)) {
Log.info(
`[MMM-IntelliCenter] received update for circuit ${obj.objnam}`,
);
if (obj.params.STATUS) {
poolData.circuits[obj.objnam].status = obj.params.STATUS === "ON";
}
} else {
Log.info(
`[MMM-IntelliCenter] received update for untracked object: ${obj.objnam}`,
);
}
}
if (initialConnectDone) {
cb(poolData);
}
})
.once("connected", async () => {
Log.info(
"[MMM-IntelliCenter] logged into unit. getting system configuration...",
);
const sysinfo = await foundUnit.send(messages.GetSystemConfiguration());
const bodyUpdates = [];
for (const obj of sysinfo.answer) {
if (obj.params.OBJTYP === "BODY" && obj.params.SUBTYP === "POOL") {
const ichem = obj.params.OBJLIST?.find(
(obj) => obj.params.SUBTYP === "ICHEM",
);
intellichemObjnam = ichem?.objnam;
poolObjnam = obj.objnam;
bodyUpdates.push(obj.objnam);
} else if (
obj.params.OBJTYP === "BODY" &&
obj.params.SUBTYP === "SPA"
) {
spaObjnam = obj.objnam;
bodyUpdates.push(obj.objnam);
}
}
Log.info("[MMM-IntelliCenter] getting chemical status...");
const chemstatus = await foundUnit.send(messages.GetChemicalStatus());
for (const obj of chemstatus.objectList) {
if (obj.params.SUBTYP === "ICHLOR") {
chlorinatorObjnam = obj.objnam;
}
}
Log.info("[MMM-IntelliCenter] getting circuit information...");
const circuits = await foundUnit.send(messages.GetCircuitStatus());
const freezeCirc = circuits.objectList?.find(
(obj) => obj.params?.SUBTYP === "FRZ",
);
if (freezeCirc) {
freezeObjnam = freezeCirc.objnam;
Log.info(
`[MMM-IntelliCenter] registering for freeze-protection updates...`,
);
await foundUnit.send(
messages.SubscribeToUpdates(freezeObjnam, "STATUS"),
);
}
if (this.config.controls?.length > 0) {
for (const circuit of circuits.objectList) {
const wantedControl = this.config.controls.find(
(c) => c.id === circuit.objnam,
);
if (!wantedControl) {
continue;
}
poolData.circuits[circuit.objnam] = {
status: false,
name: circuit.params.SNAME,
};
}
}
for (const circuit of Object.keys(poolData.circuits)) {
Log.info(`[MMM-IntelliCenter] registering for ${circuit} updates...`);
await foundUnit.send(messages.SubscribeToUpdates(circuit, "STATUS"));
}
if (bodyUpdates.length > 0) {
for (const obj of bodyUpdates) {
Log.info(
`[MMM-IntelliCenter] registering for ${obj === poolObjnam ? "pool" : obj === spaObjnam ? "spa" : obj} updates...`,
);
await foundUnit.send(
messages.SubscribeToUpdates(obj, [
"LOTMP",
"MODE",
"STATUS",
"LSTTMP",
]),
);
}
}
if (chlorinatorObjnam) {
Log.info(
"[MMM-IntelliCenter] registering for chlorinator updates...",
);
// can also check PRIM, SEC, and SUPER
// PRIM: percentage output going to primary body (probably pool) on 1-100 scale
// SEC: percentage output going to secondary body (probably spa) on 1-100 scale
// SUPER: "ON" or "OFF" for whether currently in superchlorination mode or not
await foundUnit.send(
messages.SubscribeToUpdates(chlorinatorObjnam, "SALT"),
);
}
if (intellichemObjnam) {
Log.info("[MMM-IntelliCenter] registering for chemical updates...");
await foundUnit.send(
messages.SubscribeToUpdates(intellichemObjnam, [
"PHVAL",
"PHTNK",
"ORPVAL",
"QUALTY",
]),
);
}
Log.info("[MMM-IntelliCenter] finished initial setup.");
initialConnectDone = true;
cb(poolData);
});
foundUnit.connect();
},
findServer(cb, reconnectCb) {
Log.info("[MMM-IntelliCenter] starting search for local units");
const finder = new FindUnits(this.config.multicastInterface);
finder
.on("serverFound", (server) => {
finder.close();
Log.info(
`[MMM-IntelliCenter] local unit found at ${server.addressStr}:${server.port}`,
);
foundUnit = new Unit(server.addressStr, server.port);
this.setupUnit(cb, reconnectCb);
clearInterval(unitFinderRetry);
unitFinderRetry = null;
})
.on("error", (e) => {
Log.error(
`[MMM-IntelliCenter] error trying to find a server. scheduling a retry in ${reconnectDelayMs / 1000} seconds`,
);
Log.error(e);
this.resetFoundUnit();
setTimeout(() => {
this.findServer(cb);
}, reconnectDelayMs);
});
finder.search();
unitFinderRetry = setInterval(() => {
Log.info(
`[MMM-IntelliCenter] didn't find any units within ${unitFinderTimeoutMs / 1000} seconds, trying again...`,
);
finder.search();
}, unitFinderTimeoutMs);
},
connect(cb, reconnectCb) {
if (
!foundUnit &&
typeof config !== "undefined" &&
this.config.serverAddress &&
this.config.serverPort
) {
Log.info(
`[MMM-IntelliCenter] connecting directly to configured unit at ${this.config.serverAddress}:${this.config.serverPort}`,
);
foundUnit = new Unit(this.config.serverAddress, this.config.serverPort);
}
if (foundUnit) {
this.setupUnit(cb, reconnectCb);
} else {
this.findServer(cb, reconnectCb);
}
},
});