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 */ /* global Module Log document */
let poolData = {}; let poolData;
Module.register("MMM-IntelliCenter", { Module.register("MMM-IntelliCenter", {
defaults: { defaults: {
@ -19,7 +19,7 @@ Module.register("MMM-IntelliCenter", {
columns: 3, columns: 3,
contentClass: "light", contentClass: "light",
showPHTankLevel: true, showPHTankLevel: true,
pHTankLevelMax: 6, pHTankLevelMax: 7,
serverAddress: "", serverAddress: "",
serverPort: 0, serverPort: 0,
multicastInterface: "", multicastInterface: "",
@ -44,7 +44,7 @@ Module.register("MMM-IntelliCenter", {
}, },
getDom() { getDom() {
if (!poolData.status) { if (!poolData) {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.innerHTML = "Loading IntelliCenter..."; wrapper.innerHTML = "Loading IntelliCenter...";
wrapper.className += "dimmed light small text-center"; wrapper.className += "dimmed light small text-center";
@ -75,37 +75,37 @@ Module.register("MMM-IntelliCenter", {
if (this.config.showPoolTemp) { if (this.config.showPoolTemp) {
let className = ""; let className = "";
if (poolData.status.currentTemp[0] <= this.config.coldTemp) { if (poolData.poolTemp <= this.config.coldTemp) {
className += " cold-temp"; className += " cold-temp";
} else if (poolData.status.currentTemp[0] >= this.config.hotTemp) { } else if (poolData.poolTemp >= this.config.hotTemp) {
className += " hot-temp"; className += " hot-temp";
} }
contents.push({ contents.push({
header: "Pool temp", 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, class: this.config.contentClass + className,
}); });
} }
if (this.config.showSpaTemp) { if (this.config.showSpaTemp) {
let className = ""; let className = "";
if (poolData.status.currentTemp[1] <= this.config.coldTemp) { if (poolData.spaTemp <= this.config.coldTemp) {
className = " cold-temp"; className = " cold-temp";
} else if (poolData.status.currentTemp[1] >= this.config.hotTemp) { } else if (poolData.spaTemp >= this.config.hotTemp) {
className = " hot-temp"; className = " hot-temp";
} }
contents.push({ contents.push({
header: "Spa temp", 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, class: this.config.contentClass + className,
}); });
} }
if (this.config.showPH) { if (this.config.showPH) {
let dataStr = poolData.status.pH; let dataStr = poolData.lastPHVal;
if (this.config.showPHTankLevel) { if (this.config.showPHTankLevel) {
const percent = Math.round( const percent = Math.round(
((poolData.status.pHTank - 1) / this.config.pHTankLevelMax) * 100, ((poolData.phTank - 1) / this.config.pHTankLevelMax) * 100,
); );
let cls = ""; let cls = "";
if (this.config.colored) { if (this.config.colored) {
@ -134,21 +134,21 @@ Module.register("MMM-IntelliCenter", {
if (this.config.showOrp) { if (this.config.showOrp) {
contents.push({ contents.push({
header: "ORP", header: "ORP",
data: poolData.status.orp, data: poolData.lastOrpVal.toString(),
class: this.config.contentClass, class: this.config.contentClass,
}); });
} }
if (this.config.showSaltLevel) { if (this.config.showSaltLevel) {
contents.push({ contents.push({
header: "Salt PPM", header: "Salt PPM",
data: poolData.status.saltPPM, data: poolData.saltPPM.toString(),
class: this.config.contentClass, class: this.config.contentClass,
}); });
} }
if (this.config.showSaturation) { if (this.config.showSaturation) {
contents.push({ contents.push({
header: "Saturation", header: "Saturation",
data: poolData.status.saturation, data: poolData.saturation.toString(),
class: this.config.contentClass, class: this.config.contentClass,
}); });
} }
@ -158,23 +158,24 @@ Module.register("MMM-IntelliCenter", {
if (controlObj.type === "circuit") { if (controlObj.type === "circuit") {
let { name } = controlObj; let { name } = controlObj;
for (const circuit in poolData.controllerConfig.bodyArray) { // todo: rework how controls are specified
if ( // for (const circuit in poolData.controllerConfig.bodyArray) {
poolData.controllerConfig.bodyArray[circuit].circuitId === // if (
controlObj.id // poolData.controllerConfig.bodyArray[circuit].circuitId ===
) { // controlObj.id
if (!name) { // ) {
name = poolData.controllerConfig.bodyArray[circuit].name; // if (!name) {
} // name = poolData.controllerConfig.bodyArray[circuit].name;
} // }
} // }
// }
let on = false; let on = false;
for (const circuit in poolData.status.circuitArray) { // for (const circuit in poolData.status.circuitArray) {
if (poolData.status.circuitArray[circuit].id === controlObj.id) { // if (poolData.status.circuitArray[circuit].id === controlObj.id) {
on = poolData.status.circuitArray[circuit].state !== 0; // on = poolData.status.circuitArray[circuit].state !== 0;
} // }
} // }
let cls = ""; let cls = "";
if (this.config.colored) { if (this.config.colored) {
@ -190,15 +191,18 @@ Module.register("MMM-IntelliCenter", {
class: this.config.contentClass, class: this.config.contentClass,
}); });
} else if (controlObj.type === "heatpoint") { } else if (controlObj.type === "heatpoint") {
if ( const body = controlObj.body.toLowerCase();
controlObj.body < 0 || if (body !== "pool" && body !== "spa") {
controlObj.body > poolData.status.setPoint.length Log.warn(
) { "Invalid body specified for heatpoint. Valid bodies: pool, spa",
Log.warn("Invalid body specified for heatpoint"); );
continue; continue;
} }
const temperature = poolData.status.setPoint[controlObj.body]; const temperature =
body === "pool"
? poolData.poolSetPoint.toString()
: poolData.spaSetPoint.toString();
let dataHtml = '<div class="temperature-container">'; 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>`; 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, class: this.config.contentClass,
}); });
} else if (controlObj.type === "heatmode") { } else if (controlObj.type === "heatmode") {
if ( const body = controlObj.body.toLowerCase();
controlObj.body < 0 || if (body !== "pool" && body !== "spa") {
controlObj.body > poolData.status.heatMode.length Log.warn(
) { "Invalid body specified for heatmode. Valid bodies: pool, spa",
Log.warn("Invalid body specified for heatmode"); );
continue; continue;
} }
const on = poolData.status.heatMode[controlObj.body] !== 0; const on =
body === "pool"
? poolData.poolHeaterStatus
: poolData.spaHeaterStatus;
const mode = const mode =
typeof controlObj.heatMode === "number" ? controlObj.heatMode : 3; typeof controlObj.heatMode === "number" ? controlObj.heatMode : 3;
@ -245,7 +252,7 @@ Module.register("MMM-IntelliCenter", {
let headerRow = null; let headerRow = null;
let contentRow = null; let contentRow = null;
if (this.config.showFreezeMode && poolData.status.freezeMode !== 0) { if (this.config.showFreezeMode && poolData.freezeMode) {
const row = document.createElement("tr"); const row = document.createElement("tr");
table.appendChild(row); table.appendChild(row);
row.className = "cold-temp"; row.className = "cold-temp";
@ -284,49 +291,30 @@ Module.register("MMM-IntelliCenter", {
if (notification === "INTELLICENTER_RESULT") { if (notification === "INTELLICENTER_RESULT") {
poolData = payload; poolData = payload;
this.updateDom(); this.updateDom();
showReconnectOverlay(false); this.showReconnectOverlay(false);
} else if ( } else if (
notification === "INTELLICENTER_CIRCUIT_DONE" || notification === "INTELLICENTER_CIRCUIT_DONE" ||
notification === "INTELLICENTER_HEATSTATE_DONE" || notification === "INTELLICENTER_HEATSTATE_DONE" ||
notification === "INTELLICENTER_HEATPOINT_DONE" notification === "INTELLICENTER_HEATPOINT_DONE"
) { ) {
poolData.status = payload.status; poolData = payload;
this.updateDom(); this.updateDom();
showReconnectOverlay(false); this.showReconnectOverlay(false);
} else if (notification === "INTELLICENTER_RECONNECTING") { } 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 reconnectDelayMs = 10 * 1000;
const unitFinderTimeoutMs = 5 * 1000; const unitFinderTimeoutMs = 5 * 1000;
let foundUnit = false; 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 refreshTimer;
let unitFinderRetry; let unitFinderRetry;
let unitReconnectTimer; let unitReconnectTimer;
let intellichemObjnam = "";
let chlorinatorObjnam = "";
let initialConnectDone = false;
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
setCircuit(circuitState) { setCircuit(circuitState) {
@ -75,7 +97,7 @@ module.exports = NodeHelper.create({
this.notifyReconnecting(); this.notifyReconnecting();
}, },
); );
} else if (poolData.status) { } else if (poolData) {
this.sendSocketNotification("INTELLICENTER_RESULT", 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 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) { setupUnit(cb, reconnectCb) {
Log.info("[MMM-IntelliCenter] initial connection to unit..."); Log.info("[MMM-IntelliCenter] initial connection to unit...");
initialConnectDone = false;
foundUnit foundUnit
.on("error", (e) => { .on("error", (e) => {
@ -137,38 +160,155 @@ module.exports = NodeHelper.create({
this.connect(cb, reconnectCb); this.connect(cb, reconnectCb);
}, reconnectDelayMs); }, reconnectDelayMs);
}) })
.once("connected", () => { .on("notify", (msg) => {
Log.info( // todo: how to find freezeMode on/off?
"[MMM-IntelliCenter] logged into unit. getting basic configuration...", for (const obj of msg.objectList) {
); if (obj.objnam === intellichemObjnam) {
foundUnit.send(new messages.GetSystemInformation()).then(() => { Log.info("[MMM-IntelliCenter] received chemical update");
Log.info("[MMM-IntelliCenter] got it!");
}); 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( Log.info(
"[MMM-IntelliCenter] configuration received. adding client...", "[MMM-IntelliCenter] logged into unit. getting system configuration...",
); );
poolData.controllerConfig = config; const sysinfo = await foundUnit.send(messages.GetSystemConfiguration());
poolData.degStr = this.config.degC ? "C" : "F"; const bodyUpdates = [];
foundUnit.addClient(1234); for (const obj of sysinfo.answer) {
}) if (obj.params.OBJTYP === "BODY" && obj.params.SUBTYP === "POOL") {
.once("addClient", () => { const ichem = obj.params.OBJLIST?.find(
Log.info( (obj) => obj.params.SUBTYP === "ICHEM",
"[MMM-IntelliCenter] client added successfully and listening for changes", );
); intellichemObjnam = ichem?.objnam;
foundUnit.getPoolStatus();
// Connection seems to time out every 10 minutes without some sort of request made poolObjnam = obj.objnam;
refreshTimer = setInterval( bodyUpdates.push(obj.objnam);
() => { } else if (
foundUnit.pingServer(); obj.params.OBJTYP === "BODY" &&
}, obj.params.SUBTYP === "SPA"
1 * 60 * 1000, ) {
); spaObjnam = obj.objnam;
}) bodyUpdates.push(obj.objnam);
.on("poolStatus", (status) => { }
Log.info("[MMM-IntelliCenter] received pool status update"); }
poolData.status = status;
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); 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", "repository": "https://github.com/parnic/MMM-IntelliCenter.git",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"node-intellicenter": "^0.0.2" "node-intellicenter": "^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"markdownlint-cli2": "^0.17.1",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"stylelint": "^16.12.0", "stylelint": "^16.12.0",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^36.0.1",
"stylelint-prettier": "^5.0.2" "stylelint-prettier": "^5.0.2"
}, },
"scripts": { "scripts": {
"lint": "eslint . && stylelint intellicenter.css && prettier . --check", "lint": "eslint . && stylelint intellicenter.css && prettier . --check && markdownlint-cli2 **/*.md",
"lint:fix": "eslint . --fix && stylelint intellicenter.css --fix && prettier . --write" "lint:fix": "eslint . --fix && stylelint intellicenter.css --fix && prettier . --write && markdownlint-cli2 --fix **/*.md"
} }
} }