40 Commits

Author SHA1 Message Date
a59fae5f8c Updated dependency to latest
Preparing for v1.4.0 release
2020-05-25 13:41:13 -05:00
f315bcd6a5 Allowed eslint to fix style violations 2020-05-25 13:25:18 -05:00
a0e2569743 Updated readme with new events 2020-05-25 13:25:01 -05:00
9205e3b62b add .on(‘error’) handlers that emit the error (#21)
- propagate internal errors back to caller by emitting them so they can properly be handled by callers
2020-05-25 13:19:28 -05:00
8e0cb20620 Added missing events and corrected heat mode event 2020-04-02 13:29:43 -05:00
98edd602ec Updated to support non-Windows platforms 2020-03-29 10:54:06 -05:00
ba1fb3fb6c Updated dependencies to fix vulnerabilities 2020-03-28 16:32:08 -05:00
6a1d31c224 Made eslint happy again 2020-03-04 21:58:13 -06:00
43725ae083 Fixed remote login
I forgot that this message sometimes receives a string instead of a buffer.

Fixes #19
2020-03-04 21:56:06 -06:00
81b3a61c28 Added SLEquipmentConfigurationMessage
I am not documenting this in the readme just yet because I don't really know how to interpret the data provided, but it seems to be necessary if we want to be able to change pump speeds in the future or read any historical data about the equipment. I'm not sure how much I'm going to be able to figure out about these arrays since the data appears random to me at the moment, but perhaps others will be able to figure out what's in here.
2020-02-22 11:29:29 -06:00
da9864462b Renamed to Example for user-friendliness 2020-02-22 11:27:30 -06:00
a2d39e7463 Added some helpers to interpret the controllerType field
These aren't particularly useful, at least not to me, but I found them while perusing the official app, so I figured they couldn't hurt to have.
2020-02-22 11:27:09 -06:00
ed99d411b2 Minor optimization to set buffer size appropriately
This avoids unnecessary allocations/reallocations while decoding a message by pre-sizing the buffer to the amount we know it will require. I feel like there's probably a better way to handle this, but this works for now.
2020-02-10 21:36:13 -06:00
5a55c56ac1 I'm reading that this is supposed to be submitted to source control 2020-02-09 15:29:17 -06:00
e24f49285c Added tests for some of the SLMessage utilities 2020-02-09 14:51:23 -06:00
ba19b6802e Added support for reading arrays from screenlogic messages 2020-02-09 14:51:10 -06:00
c9afb53810 SLMessage now supports instancing from an encoded SLMessage
This allows the base class to be evaluated by unit tests in a way that mocks what the library does with actual messages received from pool equipment.
2020-02-09 14:39:43 -06:00
22357f11e2 Encapsulate alignment into a function to make things slightly easier
This just returns the extra amount needed to add onto the string/array.

Also fixed up writeSLArray() not adding slack like it should have been. This wasn't an issue previously because the only place that currently uses this function is the password feature which is a fixed 16byte length, so it was already aligned. An upcoming commit will be using this, however, so I needed the alignment to be correct.

Finally, the read/write string functions were doing unnecessary conditional checks so I removed them. skipWrite() already does nothing if you tell it to skip 0, and adding 0 to readOffset won't do anything, so neither conditional is necessary at this point.
2020-02-09 14:28:03 -06:00
22858061f7 eslint didn't like these not having semi-colons
I should probably turn the semi-colon linter off since I really dislike the semi-colons everywhere, but it's probably too late for that at this point in the project.
2020-02-09 14:22:32 -06:00
9337068826 Don't change any equipment state when running tests
This is handy for testing certain mutators, but shouldn't happen every time we run tests. It can be enabled by the user on a case-by-case basis.
2020-02-09 14:21:54 -06:00
0397e8ad8f Added support for controlling the salt cell generator output levels
Closes #17
2020-02-08 15:33:13 -06:00
c310885598 Updated changelog and package.json for release 2019-12-27 15:57:36 -06:00
661d8db173 Added helper methods for interpreting the equipFlags value
Provided by @mikemucc in #15. Also updated the readme with the new information.
2019-12-27 15:57:21 -06:00
e567e9c821 gatewayName conversion was stripping final character (#14) 2019-11-30 12:01:29 -06:00
b8ed58e070 Updated changelog and package.json for release 2019-11-26 17:41:26 -06:00
757a3be7d1 Set heat mode (#13)
* Function to set the heater mode + readme updates

* making the test set the heater to 'heat pump'
2019-11-24 16:53:53 -06:00
1dcbe3883b Updated readme with latest Pentair equipment firmware version 2019-07-30 12:09:31 -05:00
b95f6ff777 Fixed spelling of Caribbean
I knew that first one didn't look right, but it's what the official app's source code had, so I went with it.
2019-07-29 21:48:20 -05:00
5ec3fbb802 Added ability to send light commands (on, off, set colors, etc.)
This implementation is barebones and only does what I was able to find easily in the official app's source code. I'd love to do more with this, so any pull requests adding functionality would be welcomed.

Closes https://github.com/parnic/node-screenlogic/issues/4
2019-07-29 21:37:36 -05:00
3b774923eb Updated changelog 2019-06-22 10:12:04 -05:00
64edc5879b Added handling of the system disliking function arguments
Updated readme with failure events that are triggered
2019-06-22 10:10:13 -05:00
cf3d3cba78 Fixed eslint complaint from commit 3824437 2019-06-21 00:13:11 -05:00
1aa1dddb49 Added ability to set pool or spa heat setpoint
Fixes #9
2019-06-21 00:12:29 -05:00
3824437d7a Factor out circuitData() and use it for is{Spa,Pool}Active (#8) 2019-06-14 16:53:42 -05:00
cd2354c34f Updated changelog and package.json for release 2019-03-26 20:56:30 -05:00
25188ddce9 To be safe, only pass the chunk of the buffer that's relevant 2019-03-26 19:16:23 -05:00
d70bc88f7c Actually fixed large message handling
We shouldn't be trying to read the message length out of the middle of a message when there are multiple segments.

Fixes https://github.com/parnic/node-screenlogic/issues/5 (for real this time, I hope...I don't have a way to test)
2019-03-26 07:10:18 -05:00
08c5457f2c Fixed handling of messages larger than 1024 bytes
Fixes https://github.com/parnic/node-screenlogic/issues/5
2019-03-25 20:02:58 -05:00
398a687cc6 Well that was obviously the wrong date 2019-02-23 10:23:05 -06:00
8727d53440 Revved for NPM 2019-02-22 18:58:10 -06:00
23 changed files with 2565 additions and 43 deletions

7
.vscode/launch.json vendored
View File

@ -7,8 +7,11 @@
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}\\example.js"
"name": "Example",
"program": "${workspaceFolder}/example.js",
"skipFiles": [
"<node_internals>/**"
]
}
]
}

View File

@ -4,7 +4,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v1.2.0 - 2017-06-20
## v1.4.0 - 2020-05-25
### Added
* Support for controlling the salt cell generator's output levels.
* Helper methods for interpreting `controllerType`.
* Experimental support for an Equipment Configuration message (not documented as of yet - `SLEquipmentConfigurationMessage` / `getEquipmentConfiguration()`). This message returns arrays of various information about the equipment, but I don't know how to interpret the information in those arrays yet. Any assistance with decoding this information would be hugely helpful.
* `error` handler on all objects for reacting to unhandled node errors.
### Fixed
* VSCode "Example" configuration can now be launched on non-Windows platforms.
### Changed
* Minor memory/performance optimizations.
* Running tests no longer changes any state of any pool equipment.
## v1.3.1 - 2019-12-27
### Added
* Several methods added to SLControllerConfigMessage for interpreting the equipFlags value.
### Fixed
* server.gatewayName no longer cuts off the last character of the name. #14 - thanks @mikemucc
## v1.3.0 - 2019-11-26
### Added
* Ability to set heat setpoint.
* Ability to set heat mode.
* Event for supplying incorrect parameters to `set` functions.
* Ability to send limited selection of light commands.
## v1.2.1 - 2019-03-26
### Fixed
* Messages larger than 1024 bytes are now handled properly.
## v1.2.0 - 2019-02-22
### Added
* Remote connection through Pentair servers
* Connecting to password-protected systems (this is only enforced by the ScreenLogic system on remote connections)

121
README.md
View File

@ -2,7 +2,7 @@
This is a Node.JS library for interfacing with Pentair ScreenLogic systems over your local network or remotely through the Pentair dispatcher. Local connections require a Pentair ScreenLogic device on the same network (a network which supports UDP broadcasts).
Tested on node v8.11.1, v10.15.1 with a system on firmware version 5.2 Build 736.0 Rel
Tested on node v8.11.1, v10.15.1 with a system on firmware versions 5.2 Build 736.0 Rel, 5.2 Build 738.0 Rel
# Usage
@ -99,6 +99,8 @@ finder.on('serverFound', function(server) {
})
```
* `error` - Indicates that an unhandled error was caught (such as the connection timing out)
## RemoteLogin
### constructor(systemName)
@ -123,6 +125,7 @@ Closes the connection
### Events
* `gatewayFound` - Indicates that the search for the named unit has completed (may or may not be successful). Event handler receives a [`SLGetGatewayDataMessage`](#slgetgatewaydatamessage) argument.
* `error` - Indicates that an unhandled error was caught (such as the connection timing out)
## UnitConnection
@ -192,6 +195,24 @@ Requests controller configuration from the connected unit. Emits the `controller
Activates or deactivates a circuit. See [`SLSetCircuitStateMessage`](#slsetcircuitstatemessage) documentation for argument values. Emits the `circuitStateChanged` event when response is acknowledged.
### setSetPoint(controllerId, bodyType, temperature)
Sets the heating setpoint for any body. See [`SLSetHeatSetPointMessage`](#slsetheatsetpointmessage) documentation for argument values. Emits the `setPointChanged` event when response is acknowledged.
### setHeatMode(controllerId, bodyType, heatMode)
Sets the preferred heat mode. See [`SLSetHeatModeMessage`](#slsetheatmodemessage) documentation for argument values. Emits the `heatModeChanged` event when response is acknowledged.
### sendLightCommand(controllerId, command)
Sends a lighting command. See [`SLLightControlMessage`](#sllightcontrolmessage) documentation for argument values. Emits the `sentLightCommand` event when response is acknowledged.
Note that better/more complete handling of lighting is desired, but I have yet to find all the commands I need to implement to make that happen. This currently sends each command to all lights and there is no ability to send to an individual light. Pull requests adding more functionality here would be most welcome.
### setSaltCellOutput(controllerId, poolOutput, spaOutput)
Sets the salt cell's output levels. See [`SLSetSaltCellConfigMessage`](#slsetsaltcellconfigmessage) documentation for argument values. Emits the `setSaltCellConfig` event when response is acknowledged.
### Events
* `loggedIn` - Indicates that a connection to the server has been established and the login process completed. `get` methods can be called once this event has been emitted.
@ -201,6 +222,13 @@ Activates or deactivates a circuit. See [`SLSetCircuitStateMessage`](#slsetcircu
* `saltCellConfig` - Indicates that a response to `getSaltCellConfig()` has been received. Event handler receives a [`SLSaltCellConfigMessage`](#slsaltcellconfigmessage) object.
* `controllerConfig` - Indicates that a response to `getControllerConfig()` has been received. Event handler receives a [`SLControllerConfigMessage`](#slcontrollerconfigmessage) object.
* `circuitStateChanged` - Indicates that a response to `setCircuitState()` has been received. Event handler receives a [`SLSetCircuitStateMessage`](#slsetcircuitstatemessage) object.
* `setPointChanged` - Indicates that a response to `setSetPoint()` has been received. Event handler receives a [`SLSetHeatSetPointMessage`](#slsetheatsetpointmessage) object.
* `heatModeChanged` - Indicates that a response to `setHeatMode()` has been received. Event handler receives a [`SLSetHeatModeMessage`](#slsetheatmodemessage) object.
* `sentLightCommand` - Indicates that a response to `sendLightCommand()` has been received. Event handler receives a [`SLLightControlMessage`](#sllightcontrolmessage) object.
* `setSaltCellConfig` - Indicates that a response to `setSaltCellOutput()` has been received. Event handler receives a [`SLSetSaltCellConfigMessage`](#slsetsaltcellconfigmessage) object.
* `loginFailed` - Indicates that a remote login attempt via supplying a system address and password to `UnitConnection` has failed likely due to the incorrect password being used.
* `badParameter` - Indicates that a bad parameter has been supplied to a function. This can be triggered, for example, by sending the wrong controller ID to a `set` function.
* `error` - Indicates that an unhandled error was caught (such as the connection timing out)
### Properties
@ -302,17 +330,48 @@ Passed as an argument to the emitted `saltCellConfig` event handler.
### Properties
* `installed` - boolean indicating whether a salt cell is installed or not
* `status` - integer
* `level1` - integer
* `level2` - integer
* `status` - integer bitmask
* `level1` - integer indicating the output level of the salt cell for the pool. I believe this operates on a 0-100 scale
* `level2` - integer indicating the output level of the salt cell for the spa. I believe this operates on a 0-100 scale
* `salt` - integer indicating salt level in parts-per-million
* `flags` - integer
* `flags` - integer bitmask
* `superChlorTimer` - integer
## SLSetSaltCellConfigMessage
Passed as an argument to the emitted `setSaltCellConfig` event. The passed version is empty, however, since the response is just an acknowledgement of receipt of the set command.
### Properties
* `controllerId` - integer indicating the ID of the controller to send this command to.
* Note that while `SLControllerConfigMessage` includes a controllerId, this ID, in my experience, should always be 0.
* `poolOutput` - integer indicating the output level of the salt cell for the pool. I believe this operates on a 0-100 scale.
* `spaOutput` - integer indicating the output level of the salt cell for the spa. I believe this operates on a 0-100 scale.
## SLControllerConfigMessage
Passed as an argument to the emitted `controllerConfig` event handler.
### hasSolar()
Returns a bool indicating whether the system has solar present. (Helper method for interpreting the value in `equipFlags`.)
### hasSolarAsHeatpump()
Returns a bool indicating whether the system has a solar heatpump (UltraTemp, ThermalFlo) present. (Helper method for interpreting the value in `equipFlags`.)
### hasChlorinator()
Returns a bool indicating whether the system has a chlorinator present. (Helper method for interpreting the value in `equipFlags`.)
### hasCooling()
Returns a bool indicating whether the system has a cooler present. (Helper method for interpreting the value in `equipFlags`.)
### hasIntellichem()
Returns a bool indicating whether the system has an IntelliChem chemical management system present. (Helper method for interpreting the value in `equipFlags`.)
### Properties
* `controllerId` - integer indicating the controller's ID
@ -322,7 +381,7 @@ Passed as an argument to the emitted `controllerConfig` event handler.
* `controllerType` - byte
* `hwType` - byte
* `controllerData` - byte
* `equipFlags` - integer
* `equipFlags` - integer indicating the type(s) of equipment present in the system (see helper methods above for interpreting these values)
* `genCircuitName` - string indicating the circuit name
* `bodyArray` - array (size number-of-circuits) holding circuit data
* `circuitId` - integer indicating circuit ID (e.g.: 500 is spa, 505 is pool)
@ -355,6 +414,56 @@ Passed as an argument to the emitted `circuitStateChanged` event. The passed ver
* This ID can be retrieved from `SLControllerConfigMessage`'s `bodyArray` property.
* `circuitState` - integer indicating whether to switch the circuit on (`1`) or off (`0`).
## SLSetHeatSetPointMessage
Passed as an argument to the emitted `setPointChanged` event. The passed version is empty, however, since the response is just an acknowledgement of receipt of the set command.
### Properties
* `controllerId` - integer indicating the ID of the controller to send this command to.
* Note that while `SLControllerConfigMessage` includes a controllerId, this ID, in my experience, should always be 0.
* `bodyType` - integer indicating the type of body to set the setpoint of. The pool is body `0` and the spa is body `1`.
* `temperature` - integer indicating the desired setpoint. This is presumably in whatever units your system is set to (celsius or fahrenheit).
## SLSetHeatModeMessage
Passed as an argument to the emitted `heatModeChanged` event. The passed version is empty, however, since the response is just an acknowledgement of receipt of the set command.
### Properties
* `controllerId` - integer indicating the ID of the controller to send this command to.
* Note that while `SLControllerConfigMessage` includes a controllerId, this ID, in my experience, should always be 0.
* `bodyType` - integer indicating the type of body to set the setpoint of. The pool is body `0` and the spa is body `1`.
* `heatMode` - integer indicating the desired heater mode. Valid values are: 0: "Off", 1: "Solar", 2 : "Solar Preferred", 3 : "Heat Pump", 4: "Don't Change"
## SLLightControlMessage
Passed as an argument to the emitted `sentLightCommand` event. The passed version is empty, however, since the response is just an acknowledgement of receipt of the light command.
### Properties
* `controllerId` - integer indicating the ID of the controller to send this command to.
* Note that while `SLControllerConfigMessage` includes a controllerId, this ID, in my experience, should always be 0.
* `command` - integer indicating which command to send to the lights. Valid values are:
* ScreenLogic.LIGHT_CMD_LIGHTS_OFF
* ScreenLogic.LIGHT_CMD_LIGHTS_ON
* ScreenLogic.LIGHT_CMD_COLOR_SET
* ScreenLogic.LIGHT_CMD_COLOR_SYNC
* ScreenLogic.LIGHT_CMD_COLOR_SWIM
* ScreenLogic.LIGHT_CMD_COLOR_MODE_PARTY
* ScreenLogic.LIGHT_CMD_COLOR_MODE_ROMANCE
* ScreenLogic.LIGHT_CMD_COLOR_MODE_CARIBBEAN
* ScreenLogic.LIGHT_CMD_COLOR_MODE_AMERICAN
* ScreenLogic.LIGHT_CMD_COLOR_MODE_SUNSET
* ScreenLogic.LIGHT_CMD_COLOR_MODE_ROYAL
* ScreenLogic.LIGHT_CMD_COLOR_SET_SAVE
* ScreenLogic.LIGHT_CMD_COLOR_SET_RECALL
* ScreenLogic.LIGHT_CMD_COLOR_BLUE
* ScreenLogic.LIGHT_CMD_COLOR_GREEN
* ScreenLogic.LIGHT_CMD_COLOR_RED
* ScreenLogic.LIGHT_CMD_COLOR_WHITE
* ScreenLogic.LIGHT_CMD_COLOR_PURPLE
## SLGetGatewayDataMessage
Passed as an argument to the emitted `gatewayFound` event. Contains information about the remote unit's status and access properties.

49
heatmodetest.js Executable file
View File

@ -0,0 +1,49 @@
'use strict';
const ScreenLogic = require('./index');
// use this to find and connect to units local to the network this is running on
var finder = new ScreenLogic.FindUnits();
finder.on('serverFound', function(server) {
finder.close();
connect(new ScreenLogic.UnitConnection(server));
});
finder.search();
// use this if you want to use a direct connection to a known unit
// connect(new ScreenLogic.UnitConnection(80, '10.0.0.85'));
// use this to remote connect to a system by name (going through the Pentair servers)
// const systemName = 'Pentair: xx-xx-xx';
// const password = '1234';
// var remote = new ScreenLogic.RemoteLogin(systemName);
// remote.on('gatewayFound', function(unit) {
// remote.close();
// if (unit && unit.gatewayFound) {
// console.log('unit ' + remote.systemName + ' found at ' + unit.ipAddr + ':' + unit.port);
// connect(new ScreenLogic.UnitConnection(unit.port, unit.ipAddr, password));
// } else {
// console.log('no unit found by that name');
// }
// });
// remote.connect();
// generic connection method used by all above examples
function connect(client) {
client.on('loggedIn', function() {
this.getVersion();
}).on('version', function(version) {
this.setHeatMode(0, 0, 3);
console.log(' version=' + version.version);
}).on('heatModeChanged', function() {
client.close();
}).on('loginFailed', function() {
console.log(' unable to login (wrong password?)');
client.close();
});
client.connect();
}

View File

@ -15,6 +15,8 @@ class FindUnits extends EventEmitter {
_this.foundServer(message, remote);
}).on('close', function() {
// console.log('finder closed');
}).on('error', function(e) {
_this.emit('error', e);
});
}
@ -36,7 +38,7 @@ class FindUnits extends EventEmitter {
port: message.readInt16LE(8),
gatewayType: message.readUInt8(10),
gatewaySubtype: message.readUInt8(11),
gatewayName: message.toString('utf8', 12, 28),
gatewayName: message.toString('utf8', 12, 29),
};
// console.log(' type: ' + server.type + ', host: ' + server.address + ':' + server.port + ',
@ -70,6 +72,8 @@ class RemoteLogin extends EventEmitter {
_this.onClientMessage(msg);
}).on('close', function(had_error) {
// console.log('remote login server connection closed');
}).on('error', function(e) {
_this.emit('error', e);
});
}
@ -124,10 +128,30 @@ class UnitConnection extends EventEmitter {
this.password = password;
this.client = new net.Socket();
var _this = this;
var buffer = Buffer.alloc(1024);
var bufferIdx = 0;
var expectedMsgLen = 0;
this.client.on('data', function(msg) {
_this.onClientMessage(msg);
if (buffer.length < msg.length + bufferIdx) {
buffer = Buffer.alloc(msg.length + buffer.length, buffer);
}
if (bufferIdx === 0) {
expectedMsgLen = msg.readInt32LE(4) + 8;
}
msg.copy(buffer, bufferIdx);
bufferIdx = bufferIdx + msg.length;
if (bufferIdx === expectedMsgLen) {
_this.onClientMessage(buffer.slice(0, expectedMsgLen));
bufferIdx = 0;
}
}).on('close', function(had_error) {
// console.log('unit connection closed');
}).on('error', function(e) {
_this.emit('error', e);
});
}
@ -184,10 +208,30 @@ class UnitConnection extends EventEmitter {
this.client.write(new messages.SLVersionMessage().toBuffer());
}
getEquipmentConfiguration() {
this.client.write(new messages.SLEquipmentConfigurationMessage().toBuffer());
}
setCircuitState(controllerId, circuitId, circuitState) {
this.client.write(new messages.SLSetCircuitStateMessage(controllerId, circuitId, circuitState).toBuffer());
}
setSetPoint(controllerId, bodyType, temperature) {
this.client.write(new messages.SLSetHeatSetPointMessage(controllerId, bodyType, temperature).toBuffer());
}
setHeatMode(controllerId, bodyType, heatMode) {
this.client.write(new messages.SLSetHeatModeMessage(controllerId, bodyType, heatMode).toBuffer());
}
sendLightCommand(controllerId, command) {
this.client.write(new messages.SLLightControlMessage(controllerId, command).toBuffer());
}
setSaltCellOutput(controllerId, poolOutput, spaOutput) {
this.client.write(new messages.SLSetSaltCellConfigMessage(controllerId, poolOutput, spaOutput).toBuffer());
}
onClientMessage(msg) {
// console.log('received message of length ' + msg.length);
if (msg.length < 4) {
@ -229,10 +273,33 @@ class UnitConnection extends EventEmitter {
// console.log(" it's circuit toggle ack");
this.emit('circuitStateChanged', new messages.SLSetCircuitStateMessage());
break;
case messages.SLSetHeatSetPointMessage.getResponseId():
// console.log(" it's a setpoint ack");
this.emit('setPointChanged', new messages.SLSetHeatSetPointMessage());
break;
case messages.SLSetHeatModeMessage.getResponseId():
// console.log(" it's a heater mode ack");
this.emit('heatModeChanged', new messages.SLSetHeatModeMessage());
break;
case messages.SLLightControlMessage.getResponseId():
// console.log(" it's a light control ack");
this.emit('sentLightCommand', new messages.SLLightControlMessage());
break;
case messages.SLSetSaltCellConfigMessage.getResponseId():
// console.log(" it's a set salt cell config ack");
this.emit('setSaltCellConfig', new messages.SLSetSaltCellConfigMessage());
break;
case messages.SLEquipmentConfigurationMessage.getResponseId():
this.emit('equipmentConfiguration', new messages.SLEquipmentConfigurationMessage(msg));
break;
case 13:
// console.log(" it's a login failure.");
this.emit('loginFailed');
break;
case 31:
// console.log(" it's a parameter failure.");
this.emit('badParameter');
break;
default:
// console.log(" it's unknown. type: " + msgType);
break;
@ -250,4 +317,22 @@ module.exports = {
FindUnits,
RemoteLogin,
UnitConnection,
LIGHT_CMD_LIGHTS_OFF: 0,
LIGHT_CMD_LIGHTS_ON: 1,
LIGHT_CMD_COLOR_SET: 2,
LIGHT_CMD_COLOR_SYNC: 3,
LIGHT_CMD_COLOR_SWIM: 4,
LIGHT_CMD_COLOR_MODE_PARTY: 5,
LIGHT_CMD_COLOR_MODE_ROMANCE: 6,
LIGHT_CMD_COLOR_MODE_CARIBBEAN: 7,
LIGHT_CMD_COLOR_MODE_AMERICAN: 8,
LIGHT_CMD_COLOR_MODE_SUNSET: 9,
LIGHT_CMD_COLOR_MODE_ROYAL: 10,
LIGHT_CMD_COLOR_SET_SAVE: 11,
LIGHT_CMD_COLOR_SET_RECALL: 12,
LIGHT_CMD_COLOR_BLUE: 13,
LIGHT_CMD_COLOR_GREEN: 14,
LIGHT_CMD_COLOR_RED: 15,
LIGHT_CMD_COLOR_WHITE: 16,
LIGHT_CMD_COLOR_PURPLE: 17,
};

View File

@ -6,7 +6,11 @@ const MSG_ID = 14;
exports.SLChallengeMessage = class SLChallengeMessage extends SLMessage {
constructor(buf) {
super(0, MSG_ID);
var size;
if (buf) {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (buf) {
this._wroteSize = true;

View File

@ -6,7 +6,12 @@ const MSG_ID = 12592;
exports.SLChemDataMessage = class SLChemDataMessage extends SLMessage {
constructor(buf) {
super(0, MSG_ID);
var size;
if (buf) {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (!buf) {
this.writeInt32LE(0); // controller index
} else {

View File

@ -6,7 +6,12 @@ const MSG_ID = 12532;
exports.SLControllerConfigMessage = class SLControllerConfigMessage extends SLMessage {
constructor(buf) {
super(0, MSG_ID);
var size;
if (buf) {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (!buf) {
this.writeInt32LE(0);
this.writeInt32LE(0);
@ -82,4 +87,44 @@ exports.SLControllerConfigMessage = class SLControllerConfigMessage extends SLMe
static getResponseId() {
return MSG_ID + 1;
}
hasSolar() {
return !!(this.equipFlags & 0x1);
}
hasSolarAsHeatpump() {
return !!(this.equipFlags & 0x2);
}
hasChlorinator() {
return !!(this.equipFlags & 0x4);
}
hasCooling() {
return !!(this.equipFlags & 0x800);
}
hasIntellichem() {
return !!(this.equipFlags & 0x8000);
}
isEasyTouch() {
return this.controllerType === 14 || this.controllerType === 13;
}
isIntelliTouch() {
return this.controllerType !== 14 && this.controllerType !== 13 && this.controllerType !== 10;
}
isEasyTouchLite() {
return this.controllerType === 13 && (this.hwType & 4) !== 0;
}
isDualBody() {
return this.controllerType === 5;
}
isChem2() {
return this.controllerType === 252 && this.hwType === 2;
}
};

View File

@ -0,0 +1,51 @@
'use strict';
const SLMessage = require('./SLMessage.js').SLMessage;
const MSG_ID = 12566;
exports.SLEquipmentConfigurationMessage = class SLEquipmentConfigurationMessage extends SLMessage {
constructor(buf) {
var size;
if (buf) {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (!buf) {
this.writeInt32LE(0);
this.writeInt32LE(0);
} else {
this._wroteSize = true;
this.writeBuffer(buf, 0);
this.decode();
}
}
decode() {
super.decode();
this.controllerType = this.readUInt8();
this.hardwareType = this.readUInt8();
this.readUInt8();
this.readUInt8();
this.controllerData = this.readInt32LE();
this.versionDataArray = this.readSLArray();
this.speedDataArray = this.readSLArray();
this.valveDataArray = this.readSLArray();
this.remoteDataArray = this.readSLArray();
this.sensorDataArray = this.readSLArray();
this.delayDataArray = this.readSLArray();
this.macroDataArray = this.readSLArray();
this.miscDataArray = this.readSLArray();
this.lightDataArray = this.readSLArray();
this.flowDataArray = this.readSLArray();
this.sgDataArray = this.readSLArray();
this.spaFlowDataArray = this.readSLArray();
}
static getResponseId() {
return MSG_ID + 1;
}
};

View File

@ -6,7 +6,11 @@ const MSG_ID = 18003;
exports.SLGetGatewayDataMessage = class SLGetGatewayDataMessage extends SLMessage {
constructor(buf) {
super(0, MSG_ID);
var size;
if (buf && typeof buf === 'object') {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (typeof buf === 'string') {
this.writeSLString(buf);

View File

@ -0,0 +1,25 @@
'use strict';
const SLMessage = require('./SLMessage.js').SLMessage;
const MSG_ID = 12556;
exports.SLLightControl = class SLLightControl extends SLMessage {
constructor(controllerIndex, command) {
super(0, MSG_ID);
this.controllerIndex = controllerIndex;
this.command = command;
}
encode() {
this.writeInt32LE(this.controllerIndex || 0);
this.writeInt32LE(this.command || 0);
super.encode();
}
static getResponseId() {
return MSG_ID + 1;
}
};

View File

@ -3,12 +3,27 @@
const SmartBuffer = require('smart-buffer').SmartBuffer;
exports.SLMessage = class SLMessage extends SmartBuffer {
constructor(senderId, messageId) {
super();
this.writeUInt16LE(senderId);
this.writeUInt16LE(messageId);
constructor(senderId, messageId, size) {
var options;
if (size) {
options = {
size: size,
};
}
super(options);
this._wroteSize = false;
if (typeof senderId === 'number' || typeof senderId === 'undefined') {
this.writeUInt16LE(senderId || 0);
this.writeUInt16LE(messageId || 0);
this._wroteSize = false;
} else if (senderId) {
this._wroteSize = true;
var buffer = senderId;
this.writeBuffer(buffer, 0);
this.decode();
}
}
toBuffer() {
@ -27,17 +42,13 @@ exports.SLMessage = class SLMessage extends SmartBuffer {
writeSLString(str) {
this.writeInt32LE(str.length);
this.writeString(str);
if (str.length % 4 !== 0) {
this.skipWrite(4 - str.length % 4);
}
this.skipWrite(SLMessage.slackForAlignment(str.length));
}
readSLString() {
var len = this.readInt32LE();
var str = this.readString(len);
if (len % 4 !== 0) {
this.readOffset += 4 - len % 4;
}
this.readOffset += SLMessage.slackForAlignment(len);
return str;
}
@ -48,9 +59,25 @@ exports.SLMessage = class SLMessage extends SmartBuffer {
writeSLArray(arr) {
this.writeInt32LE(arr.length);
for (var i = 0; i < arr.length; i++) {
this.writeUInt8(arr[i]);
}
this.skipWrite(SLMessage.slackForAlignment(arr.length));
}
readSLArray() {
var len = this.readInt32LE();
var retval = new Array(len);
for (var i = 0; i < len; i++) {
retval[i] = this.readUInt8();
}
this.readOffset += SLMessage.slackForAlignment(len);
return retval;
}
skipWrite(num) {
@ -66,5 +93,9 @@ exports.SLMessage = class SLMessage extends SmartBuffer {
this.dataLength = this.readInt32LE();
}
static slackForAlignment(val) {
return (4 - val % 4) % 4;
}
encode() {}
};

View File

@ -9,7 +9,12 @@ const POOL_CIRCUIT_ID = 505;
exports.SLPoolStatusMessage = class SLPoolStatusMessage extends SLMessage {
constructor(buf) {
super(0, MSG_ID);
var size;
if (buf) {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (!buf) {
this.writeInt32LE(0);
} else {
@ -87,20 +92,21 @@ exports.SLPoolStatusMessage = class SLPoolStatusMessage extends SLMessage {
return this.ok === 3;
}
isSpaActive() {
circuitData(id) {
for (let i = 0; i < this.circuitArray.length; i++) {
if (this.circuitArray[i].id === SPA_CIRCUIT_ID) {
return this.circuitArray[i].state === 1;
if (this.circuitArray[i].id === id) {
return this.circuitArray[i];
}
}
return undefined;
}
isSpaActive() {
return this.circuitData(SPA_CIRCUIT_ID).state === 1;
}
isPoolActive() {
for (let i = 0; i < this.circuitArray.length; i++) {
if (this.circuitArray[i].id === POOL_CIRCUIT_ID) {
return this.circuitArray[i].state === 1;
}
}
return this.circuitData(POOL_CIRCUIT_ID).state === 1;
}
static getResponseId() {

View File

@ -6,7 +6,12 @@ const MSG_ID = 12572;
exports.SLSaltCellConfigMessage = class SLSaltCellConfigMessage extends SLMessage {
constructor(buf) {
super(0, MSG_ID);
var size;
if (buf) {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (!buf) {
this.writeInt32LE(0); // controller index
} else {

29
messages/SLSetHeatMode.js Normal file
View File

@ -0,0 +1,29 @@
'use strict';
const SLMessage = require('./SLMessage.js').SLMessage;
const MSG_ID = 12538;
exports.SLSetHeatMode = class SLSetHeatMode extends SLMessage {
constructor(controllerIndex, bodyType, heatMode) {
super(0, MSG_ID);
this.controllerIndex = controllerIndex;
this.bodyType = bodyType;
this.heatMode = heatMode;
// heatmodes:
// 0: "Off", 1: "Solar", 2 : "Solar Preferred", 3 : "Heat Pump", 4: "Don't Change"
}
encode() {
this.writeInt32LE(this.controllerIndex || 0);
this.writeInt32LE(this.bodyType || 0);
this.writeInt32LE(this.heatMode || 0);
super.encode();
}
static getResponseId() {
return MSG_ID + 1;
}
};

View File

@ -0,0 +1,27 @@
'use strict';
const SLMessage = require('./SLMessage.js').SLMessage;
const MSG_ID = 12528;
exports.SLSetHeatSetPoint = class SLSetHeatSetPoint extends SLMessage {
constructor(controllerIndex, bodyType, temperature) {
super(0, MSG_ID);
this.controllerIndex = controllerIndex;
this.bodyType = bodyType;
this.temperature = temperature;
}
encode() {
this.writeInt32LE(this.controllerIndex || 0);
this.writeInt32LE(this.bodyType || 0);
this.writeInt32LE(this.temperature || 0);
super.encode();
}
static getResponseId() {
return MSG_ID + 1;
}
};

View File

@ -0,0 +1,29 @@
'use strict';
const SLMessage = require('./SLMessage.js').SLMessage;
const MSG_ID = 12576;
exports.SLSetSaltCellConfigMessage = class SLSetSaltCellConfigMessage extends SLMessage {
constructor(controllerIndex, poolOutput, spaOutput) {
super(0, MSG_ID);
this.controllerIndex = controllerIndex;
this.poolOutput = poolOutput;
this.spaOutput = spaOutput;
}
encode() {
this.writeInt32LE(this.controllerIndex || 0);
this.writeInt32LE(this.poolOutput || 0);
this.writeInt32LE(this.spaOutput || 0);
this.writeInt32LE(0);
this.writeInt32LE(0);
super.encode();
}
static getResponseId() {
return MSG_ID + 1;
}
};

View File

@ -6,7 +6,12 @@ const MSG_ID = 8120;
exports.SLVersionMessage = class SLVersionMessage extends SLMessage {
constructor(buf) {
super(0, MSG_ID);
var size;
if (buf) {
size = buf.readInt32LE(4) + 8;
}
super(0, MSG_ID, size);
if (buf) {
this._wroteSize = true;
this.writeBuffer(buf, 0);

View File

@ -9,3 +9,9 @@ exports.SLSaltCellConfigMessage = require('./SLSaltCellConfigMessage.js').SLSalt
exports.SLVersionMessage = require('./SLVersionMessage.js').SLVersionMessage;
exports.SLSetCircuitStateMessage = require('./SLSetCircuitStateMessage.js').SLSetCircuitStateMessage;
exports.SLGetGatewayDataMessage = require('./SLGetGatewayDataMessage.js').SLGetGatewayDataMessage;
exports.SLSetHeatSetPointMessage = require('./SLSetHeatSetPoint.js').SLSetHeatSetPoint;
exports.SLSetHeatModeMessage = require('./SLSetHeatMode.js').SLSetHeatMode;
exports.SLLightControlMessage = require('./SLLightControl.js').SLLightControl;
exports.SLSetSaltCellConfigMessage = require('./SLSetSaltCellConfigMessage.js').SLSetSaltCellConfigMessage;
exports.SLEquipmentConfigurationMessage =
require('./SLEquipmentConfigurationMessage.js').SLEquipmentConfigurationMessage;

1795
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "node-screenlogic",
"description": "Tool for connecting to Pentair ScreenLogic systems on the local network",
"version": "1.1.0",
"version": "1.4.0",
"main": "index.js",
"license": "MIT",
"repository": "https://github.com/parnic/node-screenlogic.git",
@ -12,12 +12,12 @@
"swimmingpool"
],
"dependencies": {
"smart-buffer": "~4.0.1"
"smart-buffer": "~4.1.0"
},
"devDependencies": {
"eslint": "^4.19.1",
"eslint": "^6.8.0",
"eslint-config-strongloop": "^2.1.0",
"mocha": "^5.1.1"
"mocha": "^7.1.1"
},
"scripts": {
"test": "mocha test/*.spec.js",

175
test/slmessage.spec.js Normal file
View File

@ -0,0 +1,175 @@
'use strict';
const SLMessage = require('../messages/SLMessage.js').SLMessage;
const assert = require('assert');
function slMessageLen(str) {
// strings have length prefixed on them as an int32 for an additional 4b.
// strings are dword aligned, so if str.length is 21, dword alignment pushes it up to 24
return 4 + str.length + SLMessage.slackForAlignment(str.length);
}
describe('SLMessage utilities', () => {
// message header = senderId, messageId, bodyLen.
// senderId and messageId are int16's, so 2b each. bodyLen is an int32, so 4b. total 8b.
let msgHeaderLen = 8;
it('sets senderId and messageId properly', function() {
{
let msg = new SLMessage(123, 456);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.senderId, 123);
assert.strictEqual(decodedMsg.messageId, 456);
assert.strictEqual(decodedMsg.dataLength, 0);
}
{
let msg = new SLMessage(0, 65534);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.senderId, 0);
assert.strictEqual(decodedMsg.messageId, 65534);
assert.strictEqual(decodedMsg.dataLength, 0);
}
{
let msg = new SLMessage();
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.senderId, 0);
assert.strictEqual(decodedMsg.messageId, 0);
assert.strictEqual(decodedMsg.dataLength, 0);
}
{
let msg = new SLMessage(123);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.senderId, 123);
assert.strictEqual(decodedMsg.messageId, 0);
assert.strictEqual(decodedMsg.dataLength, 0);
}
{
let msg = new SLMessage(0);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.senderId, 0);
assert.strictEqual(decodedMsg.messageId, 0);
assert.strictEqual(decodedMsg.dataLength, 0);
}
});
it('encodes and decodes SLStrings', function() {
{
let msg = new SLMessage();
let testStr = 'this is a test string';
msg.writeSLString(testStr);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.readSLString(), testStr, 'did not receive serialized message properly');
assert.strictEqual(SLMessage.slackForAlignment(testStr.length), 3);
// SLString byte length = 4 + 21 + 3 = 28b
assert.strictEqual(slMessageLen(testStr),
4 + testStr.length + SLMessage.slackForAlignment(testStr.length));
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(testStr), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let testStr = '1';
msg.writeSLString(testStr);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.readSLString(), testStr, 'did not receive serialized message properly');
assert.strictEqual(SLMessage.slackForAlignment(testStr.length), 3);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(testStr), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let testStr = '12';
msg.writeSLString(testStr);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.readSLString(), testStr, 'did not receive serialized message properly');
assert.strictEqual(SLMessage.slackForAlignment(testStr.length), 2);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(testStr), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let testStr = '123';
msg.writeSLString(testStr);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.readSLString(), testStr, 'did not receive serialized message properly');
assert.strictEqual(SLMessage.slackForAlignment(testStr.length), 1);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(testStr), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let testStr = '1234';
msg.writeSLString(testStr);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.strictEqual(decodedMsg.readSLString(), testStr, 'did not receive serialized message properly');
assert.strictEqual(SLMessage.slackForAlignment(testStr.length), 0);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(testStr), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
});
it('encodes and decodes SLArrays', function() {
{
let msg = new SLMessage();
let list = [];
msg.writeSLArray(list);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.deepStrictEqual(decodedMsg.readSLArray(), list);
assert.strictEqual(SLMessage.slackForAlignment(list.length), 0);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(list), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let list = [1];
msg.writeSLArray(list);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.deepStrictEqual(decodedMsg.readSLArray(), list);
assert.strictEqual(SLMessage.slackForAlignment(list.length), 3);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(list), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let list = [1, 2];
msg.writeSLArray(list);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.deepStrictEqual(decodedMsg.readSLArray(), list);
assert.strictEqual(SLMessage.slackForAlignment(list.length), 2);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(list), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let list = [1, 2, 3];
msg.writeSLArray(list);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.deepStrictEqual(decodedMsg.readSLArray(), list);
assert.strictEqual(SLMessage.slackForAlignment(list.length), 1);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(list), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
{
let msg = new SLMessage();
let list = [1, 2, 3, 4];
msg.writeSLArray(list);
let decodedMsg = new SLMessage(msg.toBuffer());
assert.deepStrictEqual(decodedMsg.readSLArray(), list);
assert.strictEqual(SLMessage.slackForAlignment(list.length), 0);
assert.strictEqual(decodedMsg.readOffset, msgHeaderLen + slMessageLen(list), 'read offset was invalid');
assert.strictEqual(decodedMsg.dataLength, decodedMsg.readOffset - 8);
}
});
});

View File

@ -25,10 +25,10 @@ describe('Unit', () => {
unit.close();
});
let circuit;
// let circuit;
it('gets pool status', done => {
unit.on('poolStatus', status => {
circuit = status.circuitArray[0];
/* circuit = */status.circuitArray[0];
done();
});
@ -63,10 +63,12 @@ describe('Unit', () => {
unit.getVersion();
});
/* uncomment this and the `circuit` stuff above to test setting state
it('sets circuit state', done => {
unit.on('circuitStateChanged', () => {
done();
});
unit.setCircuitState(0, circuit.id, circuit.state);
});
*/
});