Mostly working, still a bit more to go

This commit is contained in:
2025-01-05 12:31:07 -06:00
parent c7f8d903d3
commit d305b3414c
5 changed files with 1188 additions and 116 deletions

3
.markdownlint-cli2.jsonc Normal file
View File

@ -0,0 +1,3 @@
{
"gitignore": true,
}

View File

@ -1,6 +1,6 @@
/* global Module Log document */
let poolData = {};
let poolData;
Module.register("MMM-IntelliCenter", {
defaults: {
@ -19,7 +19,7 @@ Module.register("MMM-IntelliCenter", {
columns: 3,
contentClass: "light",
showPHTankLevel: true,
pHTankLevelMax: 6,
pHTankLevelMax: 7,
serverAddress: "",
serverPort: 0,
multicastInterface: "",
@ -44,7 +44,7 @@ Module.register("MMM-IntelliCenter", {
},
getDom() {
if (!poolData.status) {
if (!poolData) {
const wrapper = document.createElement("div");
wrapper.innerHTML = "Loading IntelliCenter...";
wrapper.className += "dimmed light small text-center";
@ -75,37 +75,37 @@ Module.register("MMM-IntelliCenter", {
if (this.config.showPoolTemp) {
let className = "";
if (poolData.status.currentTemp[0] <= this.config.coldTemp) {
if (poolData.poolTemp <= this.config.coldTemp) {
className += " cold-temp";
} else if (poolData.status.currentTemp[0] >= this.config.hotTemp) {
} else if (poolData.poolTemp >= this.config.hotTemp) {
className += " hot-temp";
}
contents.push({
header: "Pool temp",
data: `${poolData.status.currentTemp[0]}&deg;${!isPoolActive(poolData.status) ? " (last)" : ""}`,
data: `${poolData.poolTemp}&deg;${!poolData.poolStatus ? " (last)" : ""}`,
class: this.config.contentClass + className,
});
}
if (this.config.showSpaTemp) {
let className = "";
if (poolData.status.currentTemp[1] <= this.config.coldTemp) {
if (poolData.spaTemp <= this.config.coldTemp) {
className = " cold-temp";
} else if (poolData.status.currentTemp[1] >= this.config.hotTemp) {
} else if (poolData.spaTemp >= this.config.hotTemp) {
className = " hot-temp";
}
contents.push({
header: "Spa temp",
data: `${poolData.status.currentTemp[1]}&deg;${!isSpaActive(poolData.status) ? " (last)" : ""}`,
data: `${poolData.spaTemp}&deg;${!poolData.spaStatus ? " (last)" : ""}`,
class: this.config.contentClass + className,
});
}
if (this.config.showPH) {
let dataStr = poolData.status.pH;
let dataStr = poolData.lastPHVal;
if (this.config.showPHTankLevel) {
const percent = Math.round(
((poolData.status.pHTank - 1) / this.config.pHTankLevelMax) * 100,
((poolData.phTank - 1) / this.config.pHTankLevelMax) * 100,
);
let cls = "";
if (this.config.colored) {
@ -134,21 +134,21 @@ Module.register("MMM-IntelliCenter", {
if (this.config.showOrp) {
contents.push({
header: "ORP",
data: poolData.status.orp,
data: poolData.lastOrpVal.toString(),
class: this.config.contentClass,
});
}
if (this.config.showSaltLevel) {
contents.push({
header: "Salt PPM",
data: poolData.status.saltPPM,
data: poolData.saltPPM.toString(),
class: this.config.contentClass,
});
}
if (this.config.showSaturation) {
contents.push({
header: "Saturation",
data: poolData.status.saturation,
data: poolData.saturation.toString(),
class: this.config.contentClass,
});
}
@ -158,23 +158,24 @@ Module.register("MMM-IntelliCenter", {
if (controlObj.type === "circuit") {
let { name } = controlObj;
for (const circuit in poolData.controllerConfig.bodyArray) {
if (
poolData.controllerConfig.bodyArray[circuit].circuitId ===
controlObj.id
) {
if (!name) {
name = poolData.controllerConfig.bodyArray[circuit].name;
}
}
}
// todo: rework how controls are specified
// for (const circuit in poolData.controllerConfig.bodyArray) {
// if (
// poolData.controllerConfig.bodyArray[circuit].circuitId ===
// controlObj.id
// ) {
// if (!name) {
// name = poolData.controllerConfig.bodyArray[circuit].name;
// }
// }
// }
let on = false;
for (const circuit in poolData.status.circuitArray) {
if (poolData.status.circuitArray[circuit].id === controlObj.id) {
on = poolData.status.circuitArray[circuit].state !== 0;
}
}
// for (const circuit in poolData.status.circuitArray) {
// if (poolData.status.circuitArray[circuit].id === controlObj.id) {
// on = poolData.status.circuitArray[circuit].state !== 0;
// }
// }
let cls = "";
if (this.config.colored) {
@ -190,15 +191,18 @@ Module.register("MMM-IntelliCenter", {
class: this.config.contentClass,
});
} else if (controlObj.type === "heatpoint") {
if (
controlObj.body < 0 ||
controlObj.body > poolData.status.setPoint.length
) {
Log.warn("Invalid body specified for heatpoint");
const body = controlObj.body.toLowerCase();
if (body !== "pool" && body !== "spa") {
Log.warn(
"Invalid body specified for heatpoint. Valid bodies: pool, spa",
);
continue;
}
const temperature = poolData.status.setPoint[controlObj.body];
const temperature =
body === "pool"
? poolData.poolSetPoint.toString()
: poolData.spaSetPoint.toString();
let dataHtml = '<div class="temperature-container">';
dataHtml += `<button id="sl-temp-up-${controlObj.body}" class="temperature control-off" onclick="setHeatpoint(this, 1)" data-body="${controlObj.body}" data-temperature="${temperature}"><div class="content">+</div></button>`;
@ -210,15 +214,18 @@ Module.register("MMM-IntelliCenter", {
class: this.config.contentClass,
});
} else if (controlObj.type === "heatmode") {
if (
controlObj.body < 0 ||
controlObj.body > poolData.status.heatMode.length
) {
Log.warn("Invalid body specified for heatmode");
const body = controlObj.body.toLowerCase();
if (body !== "pool" && body !== "spa") {
Log.warn(
"Invalid body specified for heatmode. Valid bodies: pool, spa",
);
continue;
}
const on = poolData.status.heatMode[controlObj.body] !== 0;
const on =
body === "pool"
? poolData.poolHeaterStatus
: poolData.spaHeaterStatus;
const mode =
typeof controlObj.heatMode === "number" ? controlObj.heatMode : 3;
@ -245,7 +252,7 @@ Module.register("MMM-IntelliCenter", {
let headerRow = null;
let contentRow = null;
if (this.config.showFreezeMode && poolData.status.freezeMode !== 0) {
if (this.config.showFreezeMode && poolData.freezeMode) {
const row = document.createElement("tr");
table.appendChild(row);
row.className = "cold-temp";
@ -284,49 +291,30 @@ Module.register("MMM-IntelliCenter", {
if (notification === "INTELLICENTER_RESULT") {
poolData = payload;
this.updateDom();
showReconnectOverlay(false);
this.showReconnectOverlay(false);
} else if (
notification === "INTELLICENTER_CIRCUIT_DONE" ||
notification === "INTELLICENTER_HEATSTATE_DONE" ||
notification === "INTELLICENTER_HEATPOINT_DONE"
) {
poolData.status = payload.status;
poolData = payload;
this.updateDom();
showReconnectOverlay(false);
this.showReconnectOverlay(false);
} else if (notification === "INTELLICENTER_RECONNECTING") {
showReconnectOverlay(true);
this.showReconnectOverlay(true);
}
},
showReconnectOverlay(show) {
const element = document.querySelector(".MMM-IntelliCenter .reconnecting");
if (!element || !element.classList) {
return;
}
if (show) {
element.classList.remove("d-none");
} else {
element.classList.add("d-none");
}
},
});
function showReconnectOverlay(show) {
const element = document.querySelector(".MMM-IntelliCenter .reconnecting");
if (!element || !element.classList) {
return;
}
if (show) {
element.classList.remove("d-none");
} else {
element.classList.add("d-none");
}
}
const SPA_CIRCUIT_ID = 500;
const POOL_CIRCUIT_ID = 505;
function isPoolActive(status) {
for (let i = 0; i < status.circuitArray.length; i += 1) {
if (status.circuitArray[i].id === POOL_CIRCUIT_ID) {
return status.circuitArray[i].state === 1;
}
}
}
function isSpaActive(status) {
for (let i = 0; i < status.circuitArray.length; i += 1) {
if (status.circuitArray[i].id === SPA_CIRCUIT_ID) {
return status.circuitArray[i].state === 1;
}
}
}

View File

@ -17,10 +17,32 @@ const Log = require("logger");
const reconnectDelayMs = 10 * 1000;
const unitFinderTimeoutMs = 5 * 1000;
let foundUnit = false;
const poolData = {};
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,
};
let poolObjnam = "B1101";
let spaObjnam = "B1202";
let refreshTimer;
let unitFinderRetry;
let unitReconnectTimer;
let intellichemObjnam = "";
let chlorinatorObjnam = "";
let initialConnectDone = false;
module.exports = NodeHelper.create({
setCircuit(circuitState) {
@ -75,7 +97,7 @@ module.exports = NodeHelper.create({
this.notifyReconnecting();
},
);
} else if (poolData.status) {
} 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
@ -112,6 +134,7 @@ module.exports = NodeHelper.create({
setupUnit(cb, reconnectCb) {
Log.info("[MMM-IntelliCenter] initial connection to unit...");
initialConnectDone = false;
foundUnit
.on("error", (e) => {
@ -137,38 +160,155 @@ module.exports = NodeHelper.create({
this.connect(cb, reconnectCb);
}, reconnectDelayMs);
})
.once("connected", () => {
Log.info(
"[MMM-IntelliCenter] logged into unit. getting basic configuration...",
);
foundUnit.send(new messages.GetSystemInformation()).then(() => {
Log.info("[MMM-IntelliCenter] got it!");
});
.on("notify", (msg) => {
// todo: how to find freezeMode on/off?
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: HTSRC probably not the right check for this
if (obj.params.HTSRC) {
poolData.poolHeaterStatus = obj.params.HTSRC !== "00000";
}
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: HTSRC probably not the right check for this
if (obj.params.HTSRC) {
poolData.spaHeaterStatus = obj.params.HTSRC !== "00000";
}
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 {
Log.info(
`[MMM-IntelliCenter] received update for untracked object: ${obj.objnam}`,
);
}
}
if (initialConnectDone) {
cb(poolData);
}
})
.once("controllerConfig", (config) => {
.once("connected", async () => {
Log.info(
"[MMM-IntelliCenter] configuration received. adding client...",
"[MMM-IntelliCenter] logged into unit. getting system configuration...",
);
poolData.controllerConfig = config;
poolData.degStr = this.config.degC ? "C" : "F";
foundUnit.addClient(1234);
})
.once("addClient", () => {
Log.info(
"[MMM-IntelliCenter] client added successfully and listening for changes",
);
foundUnit.getPoolStatus();
// Connection seems to time out every 10 minutes without some sort of request made
refreshTimer = setInterval(
() => {
foundUnit.pingServer();
},
1 * 60 * 1000,
);
})
.on("poolStatus", (status) => {
Log.info("[MMM-IntelliCenter] received pool status update");
poolData.status = status;
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;
}
}
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",
"HTSRC",
"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);
});

948
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,18 +7,19 @@
"repository": "https://github.com/parnic/MMM-IntelliCenter.git",
"license": "MIT",
"dependencies": {
"node-intellicenter": "^0.0.2"
"node-intellicenter": "^0.1.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"eslint": "^9.17.0",
"markdownlint-cli2": "^0.17.1",
"prettier": "^3.4.2",
"stylelint": "^16.12.0",
"stylelint-config-standard": "^36.0.1",
"stylelint-prettier": "^5.0.2"
},
"scripts": {
"lint": "eslint . && stylelint intellicenter.css && prettier . --check",
"lint:fix": "eslint . --fix && stylelint intellicenter.css --fix && prettier . --write"
"lint": "eslint . && stylelint intellicenter.css && prettier . --check && markdownlint-cli2 **/*.md",
"lint:fix": "eslint . --fix && stylelint intellicenter.css --fix && prettier . --write && markdownlint-cli2 --fix **/*.md"
}
}