commit 20bd06016bea1fcbeea4bd9a94fd3836881e4d03 Author: Chris Pickett Date: Sat Mar 31 16:28:13 2018 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c245cd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# package lock +package-lock.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b442934 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MMM-ScreenLogic.js b/MMM-ScreenLogic.js new file mode 100644 index 0000000..9411aaa --- /dev/null +++ b/MMM-ScreenLogic.js @@ -0,0 +1,167 @@ +poolData = {}; + +Module.register("MMM-ScreenLogic",{ + defaults: { + showPoolTemp: true, + showSpaTemp: true, + showPH: true, + showOrp: true, + showSaltLevel: true, + showSaturation: true, + colored: true, + coldTemp: 84, + hotTemp: 90, + columns: 3, + contentClass: "light" + }, + + start: function() { + this.sendSocketNotification('SCREENLOGIC_CONFIG', this.config); + this.sendSocketNotification('SCREENLOGIC_UPDATE'); + }, + + getStyles: function() { + return ["screenlogic.css"]; + }, + + getDom: function() { + if (!poolData.status) { + var wrapper = document.createElement("div"); + wrapper.innerHTML = 'Loading...'; + wrapper.className += "dimmed light small"; + + return wrapper; + } else { + var table = document.createElement('table'); + table.className = "small"; + if (this.config.colored) { + table.className += " colored"; + } + + var contents = []; + + var row = document.createElement('tr'); + table.appendChild(row); + + if (this.config.showPoolTemp) { + var className = ""; + if (poolData.status.currentTemp[0] <= this.config.coldTemp) { + className += " cold-temp"; + } else if (poolData.status.currentTemp[0] >= this.config.hotTemp) { + className += " hot-temp"; + } + + contents.push({ + header: "Pool temp", + data: poolData.status.currentTemp[0] + "°" + (!isPoolActive(poolData.status) ? " (last)" : ""), + class: this.config.contentClass + className + }); + } + if (this.config.showSpaTemp) { + var className = ""; + if (poolData.status.currentTemp[1] <= this.config.coldTemp) { + className = " cold-temp"; + } else if (poolData.status.currentTemp[1] >= this.config.hotTemp) { + className = " hot-temp"; + } + + contents.push({ + header: "Spa temp", + data: poolData.status.currentTemp[1] + "°" + (!isSpaActive(poolData.status) ? " (last)" : ""), + class: this.config.contentClass + className + }); + } + if (this.config.showPH) { + contents.push({ + header: "pH", + data: poolData.status.pH, + class: this.config.contentClass + }); + } + if (this.config.showOrp) { + contents.push({ + header: "ORP", + data: poolData.status.orp, + class: this.config.contentClass + }); + } + if (this.config.showSaltLevel) { + contents.push({ + header: "Salt PPM", + data: poolData.status.saltPPM, + class: this.config.contentClass + }); + } + if (this.config.showSaturation) { + contents.push({ + header: "Saturation", + data: poolData.status.saturation, + class: this.config.contentClass + }); + } + + var headerRow = document.createElement('tr'); + var contentRow = document.createElement('tr'); + table.appendChild(headerRow); + table.appendChild(contentRow); + + var cols = 0; + for (var item in contents) { + var headerCell = document.createElement('th'); + headerCell.innerHTML = contents[item].header; + headerRow.appendChild(headerCell); + + var contentCell = document.createElement('td'); + contentCell.innerHTML = contents[item].data; + contentCell.className = contents[item].class; + contentRow.appendChild(contentCell); + + cols++; + if (cols % this.config.columns === 0) { + headerRow = document.createElement('tr'); + contentRow = document.createElement('tr'); + table.appendChild(headerRow); + table.appendChild(contentRow); + } + } + + return table; + } + }, + + socketNotificationReceived: function(notification, payload) { + if (notification === 'SCREENLOGIC_RESULT') { + poolData = payload; + this.updateDom(); + } + } +}); + +const SPA_CIRCUIT_ID = 500; +const POOL_CIRCUIT_ID = 505; + +function isPoolActive(status) { + for (var i = 0; i < status.circuitArray.length; i++) { + if (status.circuitArray[i].id === POOL_CIRCUIT_ID) { + return status.circuitArray[i].state === 1; + } + } +} + +function hasSpa(status) { + for (var i = 0; i < status.circuitArray.length; i++) { + if (status.circuitArray[i].id === SPA_CIRCUIT_ID) { + return true; + } + } + + return false; +} + +function isSpaActive(status) { + for (var i = 0; i < status.circuitArray.length; i++) { + if (status.circuitArray[i].id === SPA_CIRCUIT_ID) { + return status.circuitArray[i].state === 1; + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b902ea3 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# MMM-ScreenLogic +A MagicMirror² module used to get real-time values of crypto currencies. Tested with MagicMirror² v2.2.2 server, Chrome 65 on Windows 10 and Midori 0.4.3 on a Raspberry Pi Zero W with Raspbian Jessie. Tested with a Pentair ScreenLogic system running version 5.2 Build 736.0 Rel. + +## Installation +1. Navigate into your MagicMirror's `modules` folder and execute `git clone https://github.com/parnic/MMM-ScreenLogic.git`. +2. `cd MMM-ScreenLogic` +3. Execute `npm install` to install the node dependencies. +4. Add the module inside `config.js` placing it where you prefer. + +## Config +|Option|Type|Description|Default| +|---|---|---|---| +|`serverAddress`|String|The IPv4 address of a ScreenLogic unit to connect to. If not set, the system will search for a unit to connect to. If set, `serverPort` must also be set.| | +|`serverPort`|Integer|The port of a ScreenLogic unit to connect to (usually 80). If not set, the system will search for a unit to connect to. If set, `serverAddress` must also be set.| | +|`showPoolTemp`|Boolean|Whether you'd like to show pool temperature or not.|`true`| +|`showSpaTemp`|Boolean|Whether you'd like to show spa temperature or not.|`true`| +|`showPH`|Boolean|Whether you'd like to show pH level or not.|`true`| +|`showOrp`|Boolean|Whether you'd like to show ORP level or not.|`true`| +|`showSaltLevel`|Boolean|Whether you'd like to show salt level (in PPM) or not.|`true`| +|`showSaturation`|Boolean|Whether you'd like to show saturation/balance or not.|`true`| +|`colored`|Boolean|Whether you'd like colored output or not.|`true`| +|`coldTemp`|Integer|Show the temperature colored blue if it's at or below this level for pool/spa (requires option `colored`). This is in whatever scale your system is set to (Fahrenheit/Celsius).|`84`| +|`hotTemp`|Integer|Show the temperature colored red if it's at or above this level for pool/spa (requires option `colored`). This is in whatever scale your system is set to (Fahrenheit/Celsius).|`90`| +|`columns`|Integer|How many columns to use to display the data before starting a new row.|`3`| +|`contentClass`|String|The CSS class used to display content values (beneath the header).|`"light"`| + +Here is an example of an entry in config.js +``` +{ + module: 'MMM-ScreenLogic', + header: 'Pool info', + position: 'top_left', + config: { + showSpaTemp: false, + columns: 2, + contentClass: 'thin' + } +}, +``` + +## Screenshot +#### With color +![Screenshot with color](/screenshot.png?raw=true "colored: true") + +## Notes +Pull requests are very welcome! If you'd like to see any additional functionality, don't hesitate to let me know. + +This module only works with ScreenLogic controllers on the local network via either a UDP broadcast on 255.255.255.255 or a direct connection if you've specified an address and port in the configuration. + +The data is updated every 30 minutes. + +## Libraries +This uses a Node.JS library I created for interfacing with ScreenLogic controllers over the network: node-screenlogic, so feel free to check that out for more information. diff --git a/node_helper.js b/node_helper.js new file mode 100644 index 0000000..ffca67c --- /dev/null +++ b/node_helper.js @@ -0,0 +1,66 @@ +var NodeHelper = require('node_helper'); +var lastResult = {}; + +module.exports = NodeHelper.create({ + start: function() { + var self = this; + setInterval(function() { + self.doUpdate() + }, 30 * 60 * 1000); + }, + + doUpdate: function() { + var self = this; + getPoolData(this.config, function(poolData) { + lastResult = poolData; + self.sendSocketNotification('SCREENLOGIC_RESULT', poolData); + }); + }, + + socketNotificationReceived: function(notification, payload) { + if (notification === 'SCREENLOGIC_CONFIG') { + this.config = payload; + } + if (notification === 'SCREENLOGIC_UPDATE') { + this.doUpdate(); + } + } +}); + +const ScreenLogic = require('node-screenlogic'); + +function getPoolData(config, cb) { + if (typeof config === 'undefined' || !config.serverAddress || !config.serverPort) { + findServer(cb); + } else { + populateSystemData(new ScreenLogic.UnitConnection(config.serverPort, config.serverAddress), cb); + } +} + +function findServer(cb) { + var finder = new ScreenLogic.FindUnits(); + finder.on('serverFound', function(server) { + finder.close(); + populateSystemData(new ScreenLogic.UnitConnection(server), cb); + }); + + finder.search(); +} + +function populateSystemData(unit, cb) { + var poolData = {}; + + unit.on('loggedIn', function() { + unit.getControllerConfig(); + }).on('controllerConfig', function(config) { + poolData.degStr = config.degC ? 'C' : 'F'; + unit.getPoolStatus(); + }).on('poolStatus', function(status) { + poolData.status = status; + + unit.close(); + cb(poolData); + }); + + unit.connect(); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9d8b4b3 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "magic-mirror-module-screenlogic", + "version": "1.0.0", + "description": "Show data from Pentair ScreenLogic systems", + "main": "MMM-ScreenLogic.js", + "author": "Chris Pickett", + "repository": "https://github.com/parnic/MMM-ScreenLogic.git", + "license": "MIT", + "dependencies": { + "node-screenlogic": "~1.0.1" + } +} diff --git a/screenlogic.css b/screenlogic.css new file mode 100644 index 0000000..cf930b0 --- /dev/null +++ b/screenlogic.css @@ -0,0 +1,7 @@ +.MMM-ScreenLogic table.colored .cold-temp { + color: #BCDDFF; +} + +.MMM-ScreenLogic table.colored .hot-temp { + color: #FF8E99; +} diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..79bbd3b Binary files /dev/null and b/screenshot.png differ