From aae8c1f516867765d4205e04187db8bcc9fb9ff8 Mon Sep 17 00:00:00 2001 From: Robert Strouse Date: Fri, 14 May 2021 08:36:59 -0700 Subject: [PATCH] Fix Issue #292 - Added signature for VS pump Added message protocol for UltraTemp and cooling setponts. --- controller/Equipment.ts | 10 ++++ controller/State.ts | 4 ++ controller/boards/EasyTouchBoard.ts | 50 +++++++++++++++++ controller/boards/IntelliCenterBoard.ts | 54 +++++++++++++++++++ controller/boards/SystemBoard.ts | 13 +++++ controller/comms/messages/Messages.ts | 10 ++++ .../comms/messages/config/PumpMessage.ts | 1 + .../messages/status/HeaterStateMessage.ts | 43 +++++++++++++++ defaultConfig.json | 9 ++++ web/services/state/State.ts | 9 +++- 10 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 controller/comms/messages/status/HeaterStateMessage.ts diff --git a/controller/Equipment.ts b/controller/Equipment.ts index 841995a4..f60906cc 100644 --- a/controller/Equipment.ts +++ b/controller/Equipment.ts @@ -985,6 +985,10 @@ export class Body extends EqItem { public set setPoint(val: number) { this.setDataVal('setPoint', val); } public get heatMode(): number { return this.data.heatMode; } public set heatMode(val: number) { this.setDataVal('heatMode', val); } + public get heatSetpoint(): number { return this.data.setPoint; } + public set heatSetpoint(val: number) { this.setDataVal('setPoint', val); } + public get coolSetpoint(): number { return this.data.coolSetpoint; } + public set coolSetpoint(val: number) { this.setDataVal('coolSetpoint', val); } public getHeatModes() { return sys.board.bodies.getHeatModes(this.id); } //public async setHeatModeAsync(mode: number) { return sys.board.bodies.setHeatModeAsync(this, mode); } //public setHeatSetpoint(setPoint: number) { sys.board.bodies.setHeatSetpointAsync(this, setPoint); } @@ -1423,6 +1427,12 @@ export class Valve extends EqItem { export class HeaterCollection extends EqItemCollection { constructor(data: any, name?: string) { super(data, name || "heaters"); } public createItem(data: any): Heater { return new Heater(data); } + public getItemByAddress(address: number, add?: boolean, data?: any): Heater { + let itm = this.find(elem => elem.address === address && typeof elem.address !== 'undefined'); + if (typeof itm !== 'undefined') return itm; + if (typeof add !== 'undefined' && add) return this.add(data || { id: this.data.length + 1, address: address }); + return this.createItem(data || { id: this.data.length + 1, address: address }); + } } export class Heater extends EqItem { public dataName = 'heaterConfig'; diff --git a/controller/State.ts b/controller/State.ts index b7dd67ca..2eacc87c 100644 --- a/controller/State.ts +++ b/controller/State.ts @@ -1217,6 +1217,10 @@ export class BodyTempState extends EqState { } public get setPoint(): number { return this.data.setPoint; } public set setPoint(val: number) { this.setDataVal('setPoint', val); } + public get heatSetpoint(): number { return this.data.setPoint; } + public set heatSetpoint(val: number) { this.setDataVal('setPoint', val); } + public get coolSetpoint(): number { return this.data.coolSetpoint; } + public set coolSetpoint(val: number) { this.setDataVal('coolSetpoint', val); } public get isOn(): boolean { return this.data.isOn; } public set isOn(val: boolean) { this.setDataVal('isOn', val); } public emitData(name: string, data: any) { webApp.emitToClients('body', this.data); } diff --git a/controller/boards/EasyTouchBoard.ts b/controller/boards/EasyTouchBoard.ts index d2dbcf2a..25f64510 100644 --- a/controller/boards/EasyTouchBoard.ts +++ b/controller/boards/EasyTouchBoard.ts @@ -1012,6 +1012,56 @@ class TouchBodyCommands extends BodyCommands { conn.queueSendMessage(out); }); } + public async setSetpoints(body: Body, obj: any): Promise { + return new Promise((resolve, reject) => { + let setPoint = typeof obj.setPoint !== 'undefined' ? parseInt(obj.setPoint, 10) : parseInt(obj.heatSetpoint, 10); + if (isNaN(setPoint)) return Promise.reject(new InvalidEquipmentDataError(`Invalid ${body.name} setpoint ${obj.setPoint || obj.heatSetpoint}`, 'body', obj)); + // [16,34,136,4],[POOL HEAT Temp,SPA HEAT Temp,Heat Mode,0,2,56] + // 165,33,16,34,136,4,89,99,7,0,2,71 Request + // 165,33,34,16,1,1,136,1,130 Controller Response + const tempUnits = state.temps.units; + switch (tempUnits) { + case 0: // fahrenheit + if (setPoint < 40 || setPoint > 104) { + logger.warn(`Setpoint of ${setPoint} is outside acceptable range.`); + return; + } + break; + case 1: // celsius + if (setPoint < 4 || setPoint > 40) { + logger.warn( + `Setpoint of ${setPoint} is outside of acceptable range.` + ); + return; + } + break; + } + const body1 = sys.bodies.getItemById(1); + const body2 = sys.bodies.getItemById(2); + let temp1 = body1.setPoint || 100; + let temp2 = body2.setPoint || 100; + body.id === 1 ? temp1 = setPoint : temp2 = setPoint; + const mode1 = body1.heatMode; + const mode2 = body2.heatMode; + const out = Outbound.create({ + dest: 16, + action: 136, + payload: [temp1, temp2, mode2 << 2 | mode1, 0], + retries: 3, + response: true, + onComplete: (err, msg) => { + if (err) reject(err); + body.setPoint = setPoint; + let bstate = state.temps.bodies.getItemById(body.id); + bstate.setPoint = setPoint; + state.temps.emitEquipmentChange(); + resolve(bstate); + } + + }); + conn.queueSendMessage(out); + }); + } public async setHeatSetpointAsync(body: Body, setPoint: number): Promise { return new Promise((resolve, reject) => { // [16,34,136,4],[POOL HEAT Temp,SPA HEAT Temp,Heat Mode,0,2,56] diff --git a/controller/boards/IntelliCenterBoard.ts b/controller/boards/IntelliCenterBoard.ts index 4caead26..e9bdf4ed 100644 --- a/controller/boards/IntelliCenterBoard.ts +++ b/controller/boards/IntelliCenterBoard.ts @@ -2985,6 +2985,58 @@ class IntelliCenterBodyCommands extends BodyCommands { let body3 = sys.bodies.getItemById(3); let body4 = sys.bodies.getItemById(4); + let temp1 = sys.bodies.getItemById(1).setPoint || 100; + let temp2 = sys.bodies.getItemById(2).setPoint || 100; + let temp3 = sys.bodies.getItemById(3).setPoint || 100; + let temp4 = sys.bodies.getItemById(4).setPoint || 100; + switch (body.id) { + case 1: + byte2 = 18; + temp1 = setPoint; + break; + case 2: + byte2 = 20; + temp2 = setPoint; + break; + case 3: + byte2 = 19; + temp3 = setPoint; + break; + case 4: + byte2 = 21; + temp4 = setPoint; + break; + } + // 6 15 17 18 21 22 24 25 + //[255, 0, 255][165, 63, 15, 16, 168, 41][0, 0, 18, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0, 89, 100, 98, 100, 0, 0, 0, 0, 15, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0][5, 243] + //[255, 0, 255][165, 63, 15, 16, 168, 41][0, 0, 18, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 235, 27, 167, 1, 0, 0, 89, 81, 98, 103, 5, 0, 0, 0, 15, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0][6, 48] + let out = Outbound.create({ + action: 168, + response: IntelliCenterBoard.getAckResponse(168), + retries: 5, + payload: [0, 0, byte2, 1, 0, 0, 129, 0, 0, 0, 0, 0, 0, 0, 176, 89, 27, 110, 3, 0, 0, + temp1, temp3, temp2, temp4, body1.heatMode || 0, body2.heatMode || 0, body3.heatMode || 0, body4.heatMode || 0, 15, + sys.general.options.pumpDelay ? 1 : 0, sys.general.options.cooldownDelay ? 1 : 0, 0, 100, 0, 0, 0, 0, sys.general.options.manualPriority ? 1 : 0, sys.general.options.manualHeat ? 1 : 0] + }); + return new Promise((resolve, reject) => { + out.onComplete = (err, msg) => { + if (err) reject(err); + else { + let bstate = state.temps.bodies.getItemById(body.id); + body.setPoint = bstate.setPoint = setPoint; + resolve(bstate); + } + }; + conn.queueSendMessage(out); + }); + } + public async setCoolSetpointAsync(body: Body, setPoint: number): Promise { + let byte2 = 18; + let body1 = sys.bodies.getItemById(1); + let body2 = sys.bodies.getItemById(2); + let body3 = sys.bodies.getItemById(3); + let body4 = sys.bodies.getItemById(4); + let temp1 = sys.bodies.getItemById(1).setPoint || 100; let temp2 = sys.bodies.getItemById(2).setPoint || 100; let temp3 = sys.bodies.getItemById(3).setPoint || 100; @@ -3368,6 +3420,8 @@ class IntelliCenterHeaterCommands extends HeaterCommands { let solarInstalled = htypes.solar > 0; let heatPumpInstalled = htypes.heatpump > 0; let gasHeaterInstalled = htypes.gas > 0; + let ultratempInstalled = htypes.ultratemp > 0; + // RKS: 09-26-20 This is a hack to maintain backward compatability with fw versions 1.04 and below. if (parseFloat(sys.equipment.controllerFirmware) > 1.04) { sys.board.valueMaps.heatSources = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]); diff --git a/controller/boards/SystemBoard.ts b/controller/boards/SystemBoard.ts index da8a4728..9f042a46 100644 --- a/controller/boards/SystemBoard.ts +++ b/controller/boards/SystemBoard.ts @@ -1258,6 +1258,15 @@ export class BodyCommands extends BoardCommands { sys.board.heaters.syncHeaterStates(); return Promise.resolve(bstate); } + public async setCoolSetpointAsync(body: Body, setPoint: number): Promise { + let bdy = sys.bodies.getItemById(body.id); + let bstate = state.temps.bodies.getItemById(body.id); + bdy.coolSetpoint = bstate.setPoint = setPoint; + state.emitEquipmentChanges(); + sys.board.heaters.syncHeaterStates(); + return Promise.resolve(bstate); + } + public getHeatModes(bodyId: number) { let heatModes = []; // RKS: 09-26-20 This will need to be overloaded in IntelliCenterBoard when the other heater types are identified. (e.g. ultratemp, hybrid, maxetherm, and mastertemp) @@ -2660,6 +2669,7 @@ export class HeaterCommands extends BoardCommands { if (inst[type.name] === 'undefined') inst[type.name] = 0; inst[type.name] = inst[type.name] + 1; inst.total++; + if (type.hasCoolSetpoint) inst['hasCoolSetpoint'] = true; } } return inst; @@ -2942,6 +2952,9 @@ export class HeaterCommands extends BoardCommands { } //else if (heater.coolingEnabled && state.time.isNight) } + break; + case 'ultratemp': + break; case 'gas': if (mode === 'heater') { diff --git a/controller/comms/messages/Messages.ts b/controller/comms/messages/Messages.ts index efc84897..00ba2f2f 100755 --- a/controller/comms/messages/Messages.ts +++ b/controller/comms/messages/Messages.ts @@ -19,6 +19,7 @@ import { PumpMessage } from "./config/PumpMessage"; import { VersionMessage } from "./status/VersionMessage"; import { PumpStateMessage } from "./status/PumpStateMessage"; import { EquipmentStateMessage } from "./status/EquipmentStateMessage"; +import { HeaterStateMessage } from "./status/HeaterStateMessage"; import { ChlorinatorStateMessage } from "./status/ChlorinatorStateMessage"; import { ChlorinatorMessage } from "./config/ChlorinatorMessage"; import { ExternalMessage } from "./config/ExternalMessage"; @@ -51,6 +52,7 @@ export enum Protocol { Chlorinator = 'chlorinator', IntelliChem = 'intellichem', IntelliValve = 'intellivalve', + Heater = 'heater', Unidentified = 'unidentified' } export class Message { @@ -351,6 +353,7 @@ export class Inbound extends Message { case Protocol.IntelliChem: case Protocol.IntelliValve: case Protocol.Broadcast: + case Protocol.Heater: case Protocol.Unidentified: ndx = this.pushBytes(this.preamble, bytes, ndx, 3); ndx = this.pushBytes(this.header, bytes, ndx, 6); @@ -365,6 +368,8 @@ export class Inbound extends Message { if (this.source >= 96 && this.source <= 111) this.protocol = Protocol.Pump; else if (this.dest >= 96 && this.dest <= 111) this.protocol = Protocol.Pump; + else if (this.source >= 112 && this.source <= 127) this.protocol = Protocol.Heater; + else if (this.dest >= 112 && this.dest <= 127) this.protocol = Protocol.Heater; else if (this.dest >= 144 && this.dest <= 158) this.protocol = Protocol.IntelliChem; else if (this.source >= 144 && this.source <= 158) this.protocol = Protocol.IntelliChem; else if (this.source == 12 || this.dest == 12) this.protocol = Protocol.IntelliValve; @@ -423,6 +428,7 @@ export class Inbound extends Message { case Protocol.Pump: case Protocol.IntelliChem: case Protocol.IntelliValve: + case Protocol.Heater: case Protocol.Unidentified: if (this.datalen - this.payload.length <= 0) return ndx; // We don't need any more payload. ndx = this.pushBytes(this.payload, bytes, ndx, this.datalen - this.payload.length); @@ -450,6 +456,7 @@ export class Inbound extends Message { case Protocol.Pump: case Protocol.IntelliValve: case Protocol.IntelliChem: + case Protocol.Heater: case Protocol.Unidentified: // If we don't have enough bytes to make the terminator then continue on and // hope we get them on the next go around. @@ -752,6 +759,9 @@ export class Inbound extends Message { else this.processBroadcast(); break; + case Protocol.Heater: + HeaterStateMessage.process(this); + break; case Protocol.Chlorinator: ChlorinatorStateMessage.process(this); break; diff --git a/controller/comms/messages/config/PumpMessage.ts b/controller/comms/messages/config/PumpMessage.ts index 7c406d38..105bd649 100755 --- a/controller/comms/messages/config/PumpMessage.ts +++ b/controller/comms/messages/config/PumpMessage.ts @@ -54,6 +54,7 @@ export class PumpMessage { pump.isActive = true; PumpMessage.processVSF_IT(msg); break; + case 255: // vs 3050 on old panels. case 128: // vs case 134: // vs Ultra Efficiency pump.type = 128; diff --git a/controller/comms/messages/status/HeaterStateMessage.ts b/controller/comms/messages/status/HeaterStateMessage.ts new file mode 100644 index 00000000..9a5bf944 --- /dev/null +++ b/controller/comms/messages/status/HeaterStateMessage.ts @@ -0,0 +1,43 @@ +/* nodejs-poolController. An application to control pool equipment. +Copyright (C) 2016, 2017, 2018, 2019, 2020. Russell Goldin, tagyoureit. russ.goldin@gmail.com + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +import { Inbound, Protocol } from "../Messages"; +import { state, BodyTempState, HeaterState } from "../../../State"; +import { sys, ControllerType, Heater } from "../../../Equipment"; + +export class HeaterStateMessage { + public static process(msg: Inbound) { + if (msg.protocol === Protocol.Heater) { + switch (msg.action) { + case 114: // This is a message from a master controlling the heater + break; + case 115: + HeaterStateMessage.processHeaterStatus(msg); + break; + } + } + } + public static processHeaterStatus(msg: Inbound) { + let heater: Heater; + heater = sys.heaters.getItemByAddress(msg.source); + + // We need to decode the message. For a 2 of + //[165, 1, 15, 16, 2, 29][16, 42, 3, 0, 0, 0, 0, 0, 0, 32, 0, 0, 2, 0, 88, 88, 0, 241, 95, 100, 24, 246, 0, 0, 0, 0, 0, 40, 0][4, 221] + //[165, 0, 112, 16, 114, 10][144, 0, 0, 0, 0, 0, 0, 0, 0, 0][2, 49] // OCP to Heater + //[165, 0, 16, 112, 115, 10][160, 1, 0, 3, 0, 0, 0, 0, 0, 0][2, 70] // Heater Reply + msg.isProcessed = true; + } +} \ No newline at end of file diff --git a/defaultConfig.json b/defaultConfig.json index 35f7ef44..a11f5ccc 100755 --- a/defaultConfig.json +++ b/defaultConfig.json @@ -219,6 +219,15 @@ "excludeSource": [], "excludeDest": [] }, + "heater": { + "enabled": true, + "includeActions": [], + "excludeActions": [], + "includeSource": [], + "includeDest": [], + "excludeSource": [], + "excludeDest": [] + }, "unidentified": { "enabled": true, "includeSource": [], diff --git a/web/services/state/State.ts b/web/services/state/State.ts index f4b68a6d..11bf3311 100755 --- a/web/services/state/State.ts +++ b/web/services/state/State.ts @@ -175,10 +175,17 @@ export class StateRoute { }); app.put('/state/body/setPoint', async (req, res, next) => { // RKS: 06-24-20 -- Changed this so that users can send in the body id, circuit id, or the name. + // RKS: 05-14-21 -- Added cooling setpoints for the body. try { let body = sys.bodies.findByObject(req.body); if (typeof body === 'undefined') return next(new ServiceParameterError(`Cannot set body setPoint. You must supply a valid id, circuit, name, or type for the body`, 'body', 'id', req.body.id)); - let tbody = await sys.board.bodies.setHeatSetpointAsync(body, parseInt(req.body.setPoint, 10)); + if (typeof req.body.coolSetpoint !== 'undefined' && !isNaN(parseInt(req.body.coolSetpoint, 10))) + await sys.board.bodies.setCoolSetpointAsync(body, parseInt(req.body.coolSetpoint, 10)); + if (typeof req.body.heatSetpoint !== 'undefined' && !isNaN(parseInt(req.body.heatSetpoint, 10))) + await sys.board.bodies.setHeatSetpointAsync(body, parseInt(req.body.heatSetpoint, 10)); + else if (typeof req.body.setPoint !== 'undefined' && !isNaN(parseInt(req.body.setPoint, 10))) + await sys.board.bodies.setHeatSetpointAsync(body, parseInt(req.body.heatSetpoint, 10)); + let tbody = state.temps.bodies.getItemById(body.id); return res.status(200).send(tbody.get(true)); } catch (err) { next(err); } });