├── Documentation ├── main.png ├── boards.jpg ├── config.png ├── graph.png ├── instant.png ├── DSC_0002.JPG ├── DSC_0009.JPG ├── DSC_0012.JPG ├── DSC_0077.JPG ├── DSC_0078.JPG └── readings.png ├── public ├── images │ ├── add.jpg │ ├── delete.jpg │ ├── favicon.png │ ├── table.png │ └── settings.png ├── powermeter.css ├── realtime.html ├── usage.html ├── usage.js ├── circuit.html ├── circuit.js └── configure.html ├── Eagle └── V2.2 │ ├── PowerMeter.brd.png │ ├── PowerMeter.sch.png │ ├── PowerMeterCurrent.brd.png │ ├── PowerMeterCurrent.sch.png │ ├── PowerMeter_2019-03-02.zip │ ├── PowerMeterCurrent_2019-03-02.zip │ ├── PowerMeterCurrent_DigikeyBOM.txt │ └── PowerMeter_DigikeyBOM.txt ├── .bash_aliases ├── install.sh ├── package.json ├── node-server.sh ├── utils.js ├── README.md ├── server.js ├── reader.js └── cs5463.js /Documentation/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/main.png -------------------------------------------------------------------------------- /public/images/add.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/public/images/add.jpg -------------------------------------------------------------------------------- /Documentation/boards.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/boards.jpg -------------------------------------------------------------------------------- /Documentation/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/config.png -------------------------------------------------------------------------------- /Documentation/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/graph.png -------------------------------------------------------------------------------- /Documentation/instant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/instant.png -------------------------------------------------------------------------------- /public/images/delete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/public/images/delete.jpg -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/public/images/table.png -------------------------------------------------------------------------------- /Documentation/DSC_0002.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/DSC_0002.JPG -------------------------------------------------------------------------------- /Documentation/DSC_0009.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/DSC_0009.JPG -------------------------------------------------------------------------------- /Documentation/DSC_0012.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/DSC_0012.JPG -------------------------------------------------------------------------------- /Documentation/DSC_0077.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/DSC_0077.JPG -------------------------------------------------------------------------------- /Documentation/DSC_0078.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/DSC_0078.JPG -------------------------------------------------------------------------------- /Documentation/readings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Documentation/readings.png -------------------------------------------------------------------------------- /public/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/public/images/settings.png -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeter.brd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Eagle/V2.2/PowerMeter.brd.png -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeter.sch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Eagle/V2.2/PowerMeter.sch.png -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeterCurrent.brd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Eagle/V2.2/PowerMeterCurrent.brd.png -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeterCurrent.sch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Eagle/V2.2/PowerMeterCurrent.sch.png -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeter_2019-03-02.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Eagle/V2.2/PowerMeter_2019-03-02.zip -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeterCurrent_2019-03-02.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CircuitSetup/PiPowerMeter/master/Eagle/V2.2/PowerMeterCurrent_2019-03-02.zip -------------------------------------------------------------------------------- /.bash_aliases: -------------------------------------------------------------------------------- 1 | alias nodestart='sudo /etc/init.d/node-server.sh start' 2 | alias nodestop='sudo /etc/init.d/node-server.sh stop' 3 | alias nodetail='tail -n100 ~/nodejs.out.log' 4 | alias nodeerr='tail -n100 ~/nodejs.err.log' 5 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm list forever -g || sudo npm install -g forever 3 | if [ -f /etc/init.d/node-server.sh ]; then 4 | echo "node-server.sh already installed" 5 | # sudo unlink ~/app/node-server.sh 6 | else 7 | sudo mv ~/app/node-server.sh /etc/init.d/ 8 | cd /etc/init.d 9 | sudo chmod 755 node-server.sh 10 | sudo update-rc.d node-server.sh defaults 11 | cd ~/app 12 | echo "installed node-server.sh" 13 | fi 14 | if [ -f ~/.bash_aliases ]; then 15 | echo ".bash_aliases already installed" 16 | # sudo unlink ~/app/.bash_aliases 17 | else 18 | sudo mv ~/app/.bash_aliases ~ 19 | echo "installed .bash_aliases" 20 | fi 21 | echo "finished installing PiPowerMeter" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PiPowerMeter", 3 | "version": "1.0.1", 4 | "description": "raspberry pi power meter using CS5463 chip dependencies", 5 | "author": "Craig Jensen", 6 | "license": "BSD", 7 | "dependencies": { 8 | "sqlite3": ">=3.0.2", 9 | "twilio": ">=1.7.0", 10 | "express": ">=4.9.7", 11 | "body-parser": ">=1.9.0", 12 | "serve-favicon": ">=2.1.5", 13 | "method-override": ">=2.2.0", 14 | "on-finished": ">=2.1.0", 15 | "basic-auth": ">=1.0.0", 16 | "universal-analytics": ">=0.3.8", 17 | "mqtt": ">=1.4.1", 18 | "cs5463": "crjens/cs5463#4f3d415db6a7f61ef044d9d6ff74de67bc4d7658" 19 | }, 20 | "config":{ 21 | "unsafe-perm":true 22 | }, 23 | "scripts": { 24 | "postinstall": "/bin/bash ./install.sh" 25 | }, 26 | "engines": { "node": ">=0.10.28" } 27 | } 28 | -------------------------------------------------------------------------------- /node-server.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # /etc/init.d/node-server 3 | 4 | ### BEGIN INIT INFO 5 | # Provides: node-server 6 | # Required-Start: $remote_fs $syslog 7 | # Required-Stop: $remote_fs $syslog 8 | # Default-Start: 2 3 4 5 9 | # Default-Stop: 0 1 6 10 | ### END INIT INFO 11 | 12 | # change this to wherever your node app lives # 13 | path_to_node_app=/home/pi/app/server.js 14 | APP_DIR=/home/pi/app 15 | SERVER_JS_FILE=/home/pi/app/server.js 16 | FOREVER=forever 17 | USER=pi 18 | OUT=/home/pi 19 | NODE=/usr/bin/node 20 | 21 | # Carry out specific functions when asked to by the system 22 | case "$1" in 23 | start) 24 | echo "* starting node-server * " 25 | echo "* starting node-server * [`date`]" >> /var/log/node-server.log 26 | cd /home/pi/app 27 | # sudo $NODE $SERVER_JS_FILE >> /dev/null 2>&1& 28 | sudo $FOREVER start --workingDir $APP_DIR -a -o /dev/null -e $OUT/nodejs.err.log --killSignal=SIGTERM $SERVER_JS_FILE 29 | ;; 30 | stop) 31 | echo "* stopping node-server * " 32 | echo "* stopping node-server * [`date`]" >> /var/log/node-server.log 33 | # killall $NODE 34 | sudo $FOREVER stop --killSignal=SIGTERM $SERVER_JS_FILE 35 | ;; 36 | *) 37 | echo "Usage: /etc/init.d/node-server {start|stop}" 38 | exit 1 39 | ;; 40 | esac 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeterCurrent_DigikeyBOM.txt: -------------------------------------------------------------------------------- 1 | Qty Value Part Num Package Parts Mfg Name VID Vendor Part Num Description Pkg Code XREF 2 | 1 SSQ-108-02-T-D FE08-2 SV1 Samtec Inc DK SAM1193-08-ND CONN RCPT .100" 16POS DUAL TIN REF33 * 3 | 2 .1uF C1206C104K5RAC7867 C1206 C1, C2 Kemet DK 399-1249-1-ND CAP CER 0.1UF 50V 10% X7R 1206 1206 REF2 4 | 16 5 RC1206FR-075R1L R1206 R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, R13, R14, R15, R16 Yageo DK 311-5.10FRCT-ND RES SMD 5.1 OHM 1% 1/4W 1206 1206 * 5 | 1 120 RC1206FR-07120RL R1206 R17 Yageo DK 311-120FRCT-ND RES SMD 120 OHM 1% 1/4W 1206 1206 REF12 6 | 1 4067D 74HC4067D,652 SO24W IC1 NXP Semiconductors DK 568-2698-5-ND IC MUX/DEMUX 1X16 24SOIC REF31 * 7 | 1 4514D 74HC4514D,652 SO24W IC2 NXP Semiconductors DK 568-2703-5-ND IC 4-16 LINE DECOD/DEMUX 24-SOIC REF29 * 8 | 1 Board ID 67996-416HLF JP8Q JP1 FCI DK 609-3220-ND CONN HEADER 16POS .100 STR TIN REF30 * 9 | 4 MOUNT-PAD-ROUND3.2 * 3,2-PAD H1, H2, H3, H4 * * * * * * 10 | 16 STEREOJACK SJ1-3525N STX3100 X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12, X13, X14, X15, X16 CUI Inc DK CP1-3525N-ND CONN JACK STEREO R/A 5PIN 3.5MM REF32 * 11 | 16 green LTST-C150GKT LED_1206 LED1, LED2, LED3, LED4, LED5, LED6, LED7, LED8, LED9, LED10, LED11, LED12, LED13, LED14, LED15, LED16 Lite-On Inc DK 160-1169-1-ND LED GREEN CLEAR 1206 SMD 1206 REF18 12 | -------------------------------------------------------------------------------- /public/powermeter.css: -------------------------------------------------------------------------------- 1 | * { padding: 0; margin: 0; vertical-align: top; } 2 | 3 | html { 4 | 5 | width: 100%; 6 | height: 100%; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | body { 12 | font: 18px/1.5em "proxima-nova", Helvetica, Arial, sans-serif; 13 | overflow-x:hidden; 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | a { color: #069; } 19 | a:hover { color: #28b; } 20 | 21 | h3 { 22 | padding-left: 30px; 23 | font: normal 26px "omnes-pro", Helvetica, Arial, sans-serif; 24 | color: #666; 25 | } 26 | 27 | p { 28 | padding-top: 10px; 29 | } 30 | 31 | button { 32 | font-size: 18px; 33 | padding: 1px 7px; 34 | } 35 | 36 | input { 37 | font-size: 18px; 38 | } 39 | 40 | input[type=checkbox] { 41 | padding: 7px; 42 | } 43 | 44 | 45 | 46 | h2 { 47 | padding: 10px; 48 | vertical-align: middle; 49 | font-size: 42px; 50 | font-weight: bold; 51 | text-decoration: none; 52 | color: #000; 53 | text-align:center; 54 | } 55 | 56 | .content { 57 | width: 100%; 58 | height: 100%; 59 | padding: 10px; 60 | } 61 | 62 | .demo-container { 63 | box-sizing: border-box; 64 | width: 98%; 65 | height: 100%; 66 | padding: 20px 15px 15px 15px; 67 | background: #fff; 68 | } 69 | 70 | .placeholder100 { 71 | /*width: 95%; 72 | float: left;*/ 73 | width: 100%; 74 | height: 100%; 75 | font-size: 14px; 76 | line-height: 1.2em; 77 | padding: 5px; 78 | 79 | } 80 | 81 | .placeholder50 { 82 | width: 100%; 83 | height: 50%; 84 | font-size: 14px; 85 | line-height: 1.2em; 86 | padding: 5px; 87 | } 88 | 89 | #choices { 90 | float: right; 91 | } 92 | 93 | .header { 94 | padding: 10px; 95 | vertical-align: middle; 96 | font-size: 42px; 97 | font-weight: bold; 98 | text-decoration: none; 99 | color: #000; 100 | text-align:center; 101 | background: lightcyan; 102 | } 103 | 104 | .footer { 105 | padding: 10px; 106 | width: 100%; 107 | font-size: 14px; 108 | line-height: 1.2em; 109 | position: fixed; 110 | bottom: 0px; 111 | background: lightcyan; 112 | } 113 | 114 | .demo-table table { 115 | margin: 0 auto; 116 | } 117 | 118 | .demo-table table td { 119 | padding-right: 20px; 120 | padding-left: 20px; 121 | } 122 | 123 | 124 | .demo-table table td td { 125 | padding-right: 5px; 126 | padding-left: 5px; 127 | } 128 | 129 | .demo-table table tr { 130 | padding: 5px; 131 | } 132 | 133 | .demo-table table tr tr { 134 | padding: 3px; 135 | } 136 | 137 | .legendXXX table { 138 | border-spacing: 5px; 139 | } 140 | 141 | #buttons { 142 | padding-top: 10px; 143 | text-align: center; 144 | } 145 | 146 | 147 | 148 | .pieLabel { 149 | font-size: 14pt; 150 | } 151 | 152 | 153 | table, th, td { 154 | border: 1px solid black; 155 | border-collapse:collapse; 156 | } 157 | 158 | th,td 159 | { 160 | padding:5px; 161 | } 162 | 163 | th 164 | { 165 | text-align:left; 166 | } 167 | 168 | #dialog-form label, #dialog-form input { display:block; } 169 | 170 | #dialog-form { 171 | width: 500px; 172 | } 173 | 174 | .sensordata .text { 175 | 176 | width: 400px; 177 | } 178 | 179 | .size18x18 { 180 | 181 | width: 18px; 182 | height: 18px; 183 | } 184 | 185 | .hidden { 186 | visibility: hidden; 187 | } 188 | 189 | .buttons { 190 | margin-top: 20px; 191 | margin-left: 600px; 192 | margin-bottom: 20px; 193 | } 194 | 195 | .buttons2 { 196 | margin-top: 20px; 197 | margin-left: 50px; 198 | margin-bottom: 20px; 199 | } 200 | 201 | #probeDefTable { 202 | 203 | margin-top: 20px; 204 | } 205 | 206 | #configTable td input { 207 | width: 450px; 208 | } 209 | 210 | #configTable .twocol input { 211 | width: 100px; 212 | } 213 | 214 | .settingsiconright { 215 | position: absolute; 216 | top: 0px; 217 | right: 10px; 218 | } 219 | 220 | .settingsiconleft { 221 | position: absolute; 222 | top: 7px; 223 | right: 70px; 224 | } 225 | 226 | .updatedreading{ 227 | color: red; 228 | } 229 | 230 | #circuitsTable td { 231 | text-align: center; 232 | } 233 | 234 | #alert { 235 | width: 100px; 236 | } 237 | 238 | 239 | #configJson 240 | { 241 | border:1px solid #999999; 242 | width:100%; 243 | margin:5px 0; 244 | padding:3px; 245 | } 246 | 247 | .boxsizingBorder { 248 | -webkit-box-sizing: border-box; 249 | -moz-box-sizing: border-box; 250 | box-sizing: border-box; 251 | } -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | var hostName = require("os").hostname(); 2 | var twilio = require('twilio'); 3 | 4 | var fromText = "", toText = ""; 5 | var twilioSID = ""; 6 | var twilioAuthToken = ""; 7 | var exec = require('child_process').exec; 8 | var client = null;// = new twilio.RestClient(twilioSID, twilioAuthToken); 9 | var ipSent = false; 10 | 11 | 12 | 13 | var getNetworkIPs = (function (callback) { 14 | var ignoreRE = /^(127\.0\.0\.1|::1|fe80(:1)?::1(%.*)?)$/i; 15 | 16 | var cached; 17 | var command; 18 | var filterRE; 19 | 20 | switch (process.platform) { 21 | case 'win32': 22 | //case 'win64': // TODO: test 23 | command = 'ipconfig'; 24 | filterRE = /\bIPv[46][^:\r\n]+:\s*([^\s]+)/g; 25 | break; 26 | case 'darwin': 27 | command = 'ifconfig'; 28 | filterRE = /\binet\s+([^\s]+)/g; 29 | // filterRE = /\binet6\s+([^\s]+)/g; // IPv6 30 | break; 31 | default: 32 | command = 'ifconfig'; 33 | filterRE = /\binet\b[^:]+:\s*([^\s]+)/g; 34 | // filterRE = /\binet6[^:]+:\s*([^\s]+)/g; // IPv6 35 | break; 36 | } 37 | 38 | console.log('running: ' + command); 39 | 40 | exec(command, function (error, stdout, stderr) { 41 | if (error !== null) { 42 | console.log('exec error: ' + error); 43 | } else { 44 | 45 | cached = []; 46 | var ip; 47 | var matches = stdout.match(filterRE) || []; 48 | for (var i = 0; i < matches.length; i++) { 49 | ip = matches[i].replace(filterRE, '$1') 50 | if (!ignoreRE.test(ip)) { 51 | cached.push(ip); 52 | } 53 | } 54 | } 55 | 56 | callback(error, cached[0]); 57 | }); 58 | }); 59 | 60 | exports.InitializeTwilio = function (to, from, sid, token, deviceName, port) { 61 | 62 | if (to != null && from != null && sid != null && token != null && to != '' && from != '' && sid != '' && token != '') { 63 | 64 | console.log("initializing twilio: " + to + ", " + from + ", " + sid + ", " + token + ", " + deviceName); 65 | toText = to.toString(); 66 | fromText = from.toString(); 67 | twilioSID = sid.toString(); 68 | twilioAuthToken = token.toString(); 69 | if (deviceName != null) 70 | hostName = deviceName; 71 | client = new twilio.RestClient(twilioSID, twilioAuthToken); 72 | 73 | 74 | if (!ipSent) { 75 | ipSent = true; 76 | console.log('sending ip address'); 77 | getNetworkIPs(function (error, ip) { 78 | if (error) { 79 | console.log('ip error:', error); 80 | exports.sendText("ip error: " + error); 81 | } else { 82 | console.log("ip: " + ip); 83 | exports.sendText("ip: " + ip + ":" + port); 84 | } 85 | 86 | }, false); 87 | } 88 | 89 | //exports.sendText("Twilio initialized"); 90 | } 91 | 92 | } 93 | 94 | 95 | 96 | exports.sendText = function (msg) { 97 | 98 | if (client == null) { 99 | console.log("Twilio client not initialized"); 100 | 101 | } else { 102 | 103 | client.sms.messages.create({ 104 | to: toText, 105 | from: fromText, 106 | body: hostName + " " + msg 107 | }, function (error, message) { 108 | // The HTTP request to Twilio will run asynchronously. This callback 109 | // function will be called when a response is received from Twilio 110 | // The "error" variable will contain error information, if any. 111 | // If the request was successful, this value will be "falsy" 112 | if (!error) { 113 | // The second argument to the callback will contain the information 114 | // sent back by Twilio for the request. In this case, it is the 115 | // information about the text messsage you just sent: 116 | console.log('Success! The SID for this SMS message is:'); 117 | console.log(message.sid); 118 | 119 | console.log('Message sent on:'); 120 | console.log(message.dateCreated); 121 | } else { 122 | console.log('Oops! There was an error: ' + JSON.stringify(error)); 123 | } 124 | }); 125 | } 126 | 127 | 128 | } 129 | 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PiPowerMeter 2 | ===== 3 | 4 | PiPowerMeter is an energy usage monitor based on the Cirrus Logic CS5463 energy IC (http://www.cirrus.com/en/products/pro/detail/P1092.html) and a Raspberry Pi. It consists of two custom designed stacking pcb's. The main board houses the power supply, CS5463 IC, voltage sensors and supporting electronics. The current sensor board houses 16 multiplexed current input channels that allow monitoring up to 16 different circuits via standard clamp-on ct's. A single main board supports up to 8 stacked current sensor boards for a total monitoring capacity of up to 128 circuits. 5 | The system is controlled by a nodejs based program running on the Raspberry Pi and includes a self contained web based monitoring portal. Energy data are stored locally on the Raspberry Pi in a sqlite database making the system 100% stand-alone with no requirement for additional hardware or external servers. 6 | 7 | 8 | 9 | Features 10 | -------- 11 | - 100% stand alone system with no reliance on external hardware or servers 12 | - Ability to monitor up to 128 circuits via round-robin sampling 13 | - Uses simple off the shelf clamp-on current sensors 14 | - Highly accurate measurement of voltage, current, power usage and power factor based on CS5463 energy IC 15 | - Raspberry Pi based control system 16 | * All data stored locally in sqlite database 17 | * Web based monitoring portal for viewing energy usage and configuration 18 | * Ability to recieve text alerts for overloads or other events. 19 | 20 | 21 | ![hardware](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/DSC_0009.JPG) 22 | 23 | Screenshots 24 | ----------- 25 | - [Main](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/main.png) 26 | - [Daily](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/graph.png) 27 | - [Instantaneous](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/instant.png) 28 | - [Readings](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/readings.png) 29 | - [Configuration](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/config.png) 30 | 31 | 32 | Additional Images 33 | ----------------- 34 | - [Installed System](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/DSC_0077.JPG) 35 | - [Installed System 2](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/DSC_0078.JPG) 36 | - [Sensors](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/DSC_0002.JPG) 37 | - [Boards](https://raw.githubusercontent.com/crjens/PiPowerMeter/master/Documentation/DSC_0012.JPG) 38 | 39 | 40 | Install Instructions 41 | -------------------- 42 | 1. Any of the full size Raspberry Pi models with the 40 pin header are supported including: V1 A+, V1 B+, V2, V3 B and V3 B+. The additional memory and computing power of the V2/V3 models is recommended. 43 | 2. Start with latest Raspbian image from http://downloads.raspberrypi.org/raspbian_lite_latest 44 | 1. (verified with Raspbian Stretch 2018-11-13) 45 | 2. It's recommended that you use the Lite version because it's smaller and installs faster but you can use either. 46 | 3. login to Pi with Putty or other 47 | 1. the latest versions of Raspbian have ssh disabled. You can enable ssh via raspi-config or just create an empty file named 'ssh' in the boot partition of the sd card. 48 | 4. Install/Update Nodejs (use one of the two methods below depending your model of Raspberry Pi) 49 | 1. For Raspberry Pi 2 or Raspberry Pi 3 (64 bit only) 50 | 1. curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash - 51 | 2. sudo apt install -y nodejs 52 | 2. For Raspberry Pi all versions (32 or 64 bit) 53 | 1. wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.4/install.sh | bash 54 | 2. nvm install --lts 55 | 3. sudo cp -R $NVM_DIR/versions/node/$(nvm version)/* /usr/local/ 56 | 5. Install PiPowerMeter software into app directory 57 | 1. (Raspbian-Lite only) If using Raspbian-Lite you'll need to first install git. Raspbian-Full has git preinstalled so you can skip this step. 58 | 1. sudo apt-get -y install git 59 | 2. git clone https://github.com/crjens/PiPowerMeter.git app 60 | 3. cd app 61 | 4. npm install 62 | 6. run 'sudo raspi-config' 63 | 1. set locale and timezone under Localisation options 64 | 2. enable SPI under Interfacing Options 65 | 3. expand filesystem under Advanced options 66 | 4. change user password (optional) 67 | 5. reboot when prompted after exiting raspi-config 68 | 7. Open your browser to http://:3000 69 | -------------------------------------------------------------------------------- /Eagle/V2.2/PowerMeter_DigikeyBOM.txt: -------------------------------------------------------------------------------- 1 | Qty Value Part Num Package Parts Mfg Name VID Vendor Part Num Description Pkg Code XREF 2 | 6 .1uF C1206C104K5RAC7867 C1206 C1, C2, C4, C6, C10, C14 Kemet DK 399-1249-1-ND CAP CER 0.1UF 50V 10% X7R 1206 1206 REF2 3 | 1 1.1A 6V 0ZCC0110AF2C 1812 F1 Bel Fuse Inc DK 507-1500-1-ND PTC RESTTBLE 1.10A 16V CHIP 1812 1812 REF24 4 | 4 1k RC1206FR-071KL R1206 R5, R6, R19, R20 Yageo DK 311-1.00KFRCT-ND RES SMD 1K OHM 1% 1/4W 1206 1206 REF8 5 | 1 4.096MHz 9B-4.096MBBK-B HC49/S Q1 TXC CORPORATION DK 887-1876-ND CRYSTAL 4.0960MHZ 20PF T/H 1206 6 | 1 10K RC1206FR-0710KL R1206 R21 Yageo DK 311-10.0KFRCT-ND RES SMD 10K OHM 1% 1/4W 1206 1206 * 7 | 1 10u T491C106K016AT AVX-C C3 Kemet DK 399-3732-1-ND CAP TANT 10UF 16V 10% 2413 AVX-C REF7 8 | 2 22nF C1206C223K5RACTU C1206 C9, C13 Kemet DK 399-1242-1-ND CAP CER 0.022UF 50V 10% X7R 1206 1206 REF4 9 | 12 59k RC1206FR-071KL R1206 R7, R8, R9, R10, R11, R12, R13, R14, R15, R16, R17, R18 Yageo DK 311-1.00KFRCT-ND RES SMD 1K OHM 1% 1/4W 1206 1206 REF8 10 | 1 74HC138D SN74HC138NSR SO16 IC1 Texas Instruments DK 296-12886-1-ND IC DECODER/DEMUX 3-8 LINE 16SO 16-SOIC REF21 11 | 3 158 RC1206FR-07158RL R1206 R1, R2, R3 Yageo DK 311-158FRCT-ND RES SMD 158 OHM 1% 1/4W 1206 1206 * 12 | 4 220pF C1206C221J5GACTU C1206 C7, C8, C11, C12 Kemet DK 399-8167-1-ND CAP CER 220PF 50V 5% NP0 1206 1206 REF3 13 | 1 220uF ESH227M016AE3AA E2,5-6E C5 Kemet DK 399-6566-ND CAP ALUM 220UF 16V 20% RADIAL Radial, Can REF6 14 | 1 270 RC1206FR-07270RL R1206 R4 Yageo DK 311-270FRCT-ND RES SMD 270 OHM 1% 1/4W 1206 1206 * 15 | 1 4052D CD4052BNSR SO16 IC2 Texas Instruments DK 296-14116-1-ND IC MUX/DEMUX DUAL 4X1 16SO 16-SOIC * 16 | 1 ACIN 1935190 SCREWTERMINAL-5MM-5 JP1 Phoenix Contact DK 277-1580-ND TERM BLOCK PCB 5POS 5.0MM GREEN REF36 * 17 | 1 CS5463 CS5463-ISZ SSOP24 IC3 Cirrus Logic Inc DK 598-1096-5-ND IC ENERGY METERING 1PHASE 24SSOP 24-SSOP REF23 18 | 1 Current Brd Connector PPTC082LFBN-RC FE08-2 SV1 Sullins Connector Solutions DK S7076-ND CONN HEADER FMAL 16PS .1" DL TIN REF25 * 19 | 3 RSTA 2 RSTA 2 TE5 F2, F3, F4 Bel Fuse Inc. DK 507-1859-ND FUSE 2A 250/277V RADIAL Radial REF17 20 | 1 P6KE6.8A P6KE6.8A C1702-15 D1 Littelfuse Inc DK P6KE6.8ALFCT-ND TVS DIODE 5.8VWM 10.5VC DO204AC DO-15 REF18 21 | 1 RASPI_BOARD_B+_E4 M20-9762046 RASPI_BOARD_B+_EDGES_4DRILL X1 Harwin Inc DK 952-1896-ND 20+20 DIL VERT PIN HDR REF26 * 22 | 3 TV16E * TV16E T1, T2, T3 * * * * * * 23 | 1 VSK-S10 VSK-S5-5UA VSK-S10 PS1 CUI Inc DK 102-2386-ND CONV AC/DC 5W 5V 1A PCB * REF20 24 | 1 green LTST-C150GKT LED_1206 LED1 Lite-On Inc DK 160-1169-1-ND LED GREEN CLEAR 1206 SMD 1206 REF19 25 | -------------------------------------------------------------------------------- /public/realtime.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Power Meter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 124 | 125 | 126 | 127 | 128 | 132 | 141 | 142 |
143 |
144 |
145 |
146 | 147 | 148 |
149 | 150 | 174 | 175 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /public/usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Power Meter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 159 | 160 | 161 | 162 | 163 | 164 | 166 | 171 | 172 | 173 | 174 |
175 |
176 | 1 177 | 1 178 |
179 |
180 |
181 |
182 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /public/usage.js: -------------------------------------------------------------------------------- 1 | 2 | var toCurrency = function(amount) 3 | { 4 | return Globalize().format(amount, "c", Region); 5 | } 6 | 7 | var toFloat = function (amount, fixed) { 8 | return Globalize().format(amount, 'n' + fixed, Region); 9 | } 10 | 11 | 12 | var GetTimespanDate = function (timespan) { 13 | 14 | var start, end = new Date(); 15 | if (timespan == 'Hour') 16 | start = new Date(end.getTime() - 1000 * 60 * 60); 17 | else if (timespan == 'Day') 18 | start = new Date(end.getTime() - 1000 * 60 * 60 * 24); 19 | else if (timespan == 'Week') 20 | start = new Date(end.getTime() - 1000 * 60 * 60 * 24 * 7); 21 | else if (timespan == 'Month') { 22 | if (end.getMonth() == 0) 23 | start = new Date(end.getFullYear() - 1, 11, end.getDate(), end.getHours(), end.getMinutes(), end.getSeconds(), end.getMilliseconds()); 24 | else 25 | start = new Date(end.getFullYear(), end.getMonth() - 1, end.getDate(), end.getHours(), end.getMinutes(), end.getSeconds(), end.getMilliseconds()); 26 | } 27 | else if (timespan == 'Year') { 28 | start = new Date(end.getFullYear() - 1, end.getMonth(), end.getDate(), end.getHours(), end.getMinutes(), end.getSeconds(), end.getMilliseconds()); 29 | } 30 | else if (timespan == 'Custom') { 31 | start = $("#start").datetimepicker('getDate'); 32 | end = $("#end").datetimepicker('getDate'); 33 | } 34 | else { 35 | start = end = null; 36 | } 37 | 38 | 39 | if (timespan != 'Custom') { 40 | if (start != null) 41 | $("#start").addClass('dontSelectCustom').datetimepicker('setDate', start).removeClass('dontSelectCustom'); 42 | else 43 | $("#start").val(''); 44 | 45 | if (end != null) 46 | $("#end").addClass('dontSelectCustom').datetimepicker('setDate', end).removeClass('dontSelectCustom'); 47 | else 48 | $("#end").val(''); 49 | } 50 | 51 | return { Start: start, End: end }; 52 | } 53 | 54 | 55 | function labelFormatter(label, series) { 56 | var cost = ""; 57 | 58 | if (_start != null && _end != null) { 59 | var kw = parseFloat(series.data[0][1]) / 1000.0; 60 | var timespanMs = _end.getTime() - _start.getTime(); 61 | var hours = timespanMs / (1000 * 60 * 60); 62 | cost = " (" + toCurrency(kw * dollarsPerKWh * hours) + ")"; 63 | } 64 | 65 | return "
" + label + "
" + toFloat(series.percent, 0) + "%" + cost + "
"; 66 | } 67 | 68 | function labelFormatter2(label, series) { 69 | return "
" + label + "
" + toFloat(series.data[0][1], 1) + " W
"; 70 | } 71 | 72 | var options = { 73 | series: { 74 | pie: { 75 | show: true, 76 | radius: 1, 77 | label: { 78 | show: true, 79 | radius: 4 / 5, 80 | threshold: 0.02, 81 | formatter: labelFormatter, 82 | background: { 83 | //opacity: 0.5 84 | } 85 | } 86 | } 87 | }, 88 | legend: { 89 | show: false 90 | }, 91 | grid: { hoverable: true, clickable: true } 92 | }; 93 | 94 | var data = [], dollarsPerKWh = 0.0, _start, _end, results = null; 95 | var Region = "en-US"; // for currency format 96 | 97 | 98 | 99 | $(document).ready(function () { 100 | $('input[type=radio][name=order]').change(function () { 101 | draw(); 102 | }); 103 | }); 104 | 105 | 106 | var RefreshComparisonGraph = function (start, end, callback) { 107 | 108 | _start = start; 109 | _end = end; 110 | var placeholder = $("#placeholder3"); 111 | placeholder.empty(); 112 | data = []; 113 | 114 | placeholder.bind("plotclick", function (event, pos, item) { 115 | if (item) { 116 | var timespan = $('#Timespan option:selected').val(); 117 | var timespanDate = GetTimespanDate(timespan); 118 | 119 | var url = "/circuit.html#channel=" + item.series.label + "×pan=" + timespan; 120 | 121 | if (timespan == "Custom") 122 | url += "&start=" + timespanDate.Start.toISOString() + "&end=" + timespanDate.End.toISOString(); 123 | 124 | window.location.href = url; 125 | //alert("" + item.series.label + ": " + percent + "%"); 126 | } 127 | }); 128 | 129 | 130 | placeholder.bind("plothover", function (event, pos, obj) { 131 | 132 | if (!obj) { 133 | $("#tooltip").hide(); 134 | } else { 135 | 136 | var usage; 137 | var watts = obj.series.data[0][1]; 138 | 139 | var kwh = watts / 1000.0; 140 | var costPerHour = toCurrency(kwh * dollarsPerKWh); 141 | var costPerDay = toCurrency(kwh * dollarsPerKWh * 24); 142 | var costPerMonth = toCurrency(kwh * dollarsPerKWh * 24 * 30); 143 | var costPerYear = toCurrency(kwh * dollarsPerKWh * 24 * 365); 144 | 145 | var min = 0, max = 0, avg = 0; 146 | 147 | // find min/max in data array 148 | for (var i = 0; i < data.length; i++) { 149 | if (data[i].label == obj.series.label) { 150 | min = data[i].Min; 151 | max = data[i].Max; 152 | avg = data[i].Avg; 153 | } 154 | } 155 | 156 | usage = toFloat(min, 1) + ' / ' + toFloat(avg, 1) + ' / ' + toFloat(max, 1) + ' Watts (min/avg/max)'; 157 | 158 | $("#tooltip") 159 | .html("" + obj.series.label + "
Usage: " + usage + "
Cost: " + costPerHour + " / " + costPerDay + " / " + costPerMonth + " / " + costPerYear + " (hr/day/month/year)
") 160 | .css({ top: pos.pageY + 5, left: pos.pageX + 5 }) 161 | .fadeIn(200); 162 | } 163 | 164 | }); 165 | 166 | if ($("#tooltip").length == 0) { 167 | $("
").css({ 168 | position: "absolute", 169 | display: "none", 170 | border: "1px solid #fdd", 171 | padding: "2px", 172 | "background-color": "#fee", 173 | opacity: 0.80 174 | }).appendTo("body"); 175 | } 176 | 177 | var url = '/cumulative'; 178 | if (start != null && end != null) 179 | url += '?start=' + start.getTime() + '&end=' + end.getTime(); 180 | 181 | $.ajax({ 182 | url: url, 183 | type: 'get', 184 | dataType: 'json', 185 | cache: false, 186 | success: function (res) { 187 | 188 | results = res; 189 | draw(callback); 190 | 191 | } 192 | }); 193 | } 194 | 195 | var draw = function (callback) { 196 | if (results != null) { 197 | var placeholder = $("#placeholder3"); 198 | var order = $('input[name=order]:checked').val(); 199 | if (order != "Min" && order != "Max") 200 | order = "Watts"; 201 | var result = results.result; 202 | var j = 0; 203 | totalWatts = 0; 204 | data = []; 205 | for (var i = 0; i < result.length; i++) { 206 | if (result[i].Watts > 0) { // ignore negative values 207 | totalWatts += result[i].Watts; 208 | 209 | var val = result[i].Watts; 210 | if (order == "Min") 211 | val = result[i].Min; 212 | else if (order == "Max") 213 | val = result[i].Max; 214 | 215 | 216 | data[j++] = { label: result[i].CircuitId.toString(), data: val, Min: result[i].Min, Max: result[i].Max, Avg: result[i].Watts }; 217 | } 218 | } 219 | 220 | // sort descending 221 | data.sort(function (a, b) { 222 | if (a.data > b.data) return -1; 223 | if (a.data < b.data) return 1; 224 | return 0; 225 | }); 226 | 227 | dollarsPerKWh = results.CostPerKWH; 228 | Region = results.Region; 229 | var kwh = totalWatts / 1000.0; 230 | var costPerMonth = toCurrency(kwh * dollarsPerKWh * 24 * 30) + " / month"; 231 | 232 | $('.header').text(results.DeviceName + " Power Meter: " + costPerMonth); 233 | 234 | 235 | if (data.length == 0) { 236 | placeholder.append("

No Data found

"); 237 | //window.location.href = "/circuit.html"; 238 | } else { 239 | if (order == "Watts") 240 | options.series.pie.label.formatter = labelFormatter; 241 | else 242 | options.series.pie.label.formatter = labelFormatter2; 243 | 244 | var plot = $.plot(placeholder, data, options); 245 | maxWatts = results.MaxWatts; 246 | avgWatts = results.AvgWatts; 247 | minWatts = results.MinWatts; 248 | currentWatts = toFloat(totalWatts, 0); 249 | resizeGage(true); 250 | } 251 | } 252 | 253 | if ($.isFunction(callback)) 254 | callback(); 255 | } 256 | 257 | var lastWidth = 0, lastHeight = 0, currentWatts = 0, maxWatts = 0, avgWatts = 0, minWatts = 0, gage = null; 258 | 259 | var resizeGage = function (force) { 260 | var width = $(window).width(); 261 | var height = $(window).height(); 262 | 263 | if (force || ((width != lastWidth || height != lastHeight) && currentWatts > 0)) { 264 | lastHeight = height; 265 | lastWidth = width; 266 | 267 | var w = width / 5; 268 | if (w < 60) 269 | w = 60; 270 | 271 | var h = .8 * w; 272 | 273 | if ($("#gauge").length == 0) { 274 | $("
").css({ 275 | position: "absolute", 276 | "background-color": "white", 277 | opacity: 0.90 278 | }).appendTo("body"); 279 | } 280 | 281 | $("#gauge") 282 | .empty() 283 | .css({ width: w, height: h, top: $(window).height() - $('.footer').outerHeight() - h, left: $(window).width() - w }); 284 | 285 | gage = new JustGage({ 286 | id: "gauge", 287 | //value: currentWatts, 288 | value: avgWatts, 289 | min: minWatts, 290 | max: maxWatts, 291 | title: "Watts" 292 | }); 293 | } 294 | } 295 | 296 | var RedrawComparisonGraph = function () { 297 | var placeholder = $("#placeholder3"); 298 | var plot = $.plot(placeholder, data, options); 299 | 300 | resizeGage(false); 301 | } 302 | 303 | 304 | -------------------------------------------------------------------------------- /public/circuit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Power Meter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 228 | 229 | 230 | 231 | 232 | 234 | 239 | 240 |
241 |
242 |
243 |
244 | 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var express = require('express'); 3 | var bodyParser = require('body-parser'); 4 | var favicon = require('serve-favicon'); 5 | var methodOverride = require('method-override'); 6 | var power = require('./cs5463'); 7 | var db = require('./database'); 8 | var onFinished = require('on-finished') 9 | var basicAuth = require('basic-auth'); 10 | var path = require('path'); 11 | 12 | var ua; 13 | 14 | try { 15 | ua = require('universal-analytics'); 16 | } catch (e) { 17 | console.log("missing universal-analytics"); 18 | console.log(e); 19 | ua = null; 20 | } 21 | 22 | var username = "", password = "", compactRunning = false; 23 | 24 | var app = express(), server = null, httpPort = 3000; 25 | 26 | // Add timestamp to console messages 27 | (function (o) { 28 | if (o.__ts__) { return; } 29 | var slice = Array.prototype.slice; 30 | ['log', 'debug', 'info', 'warn', 'error'].forEach(function (f) { 31 | var _ = o[f]; 32 | o[f] = function () { 33 | var args = slice.call(arguments); 34 | args.unshift(new Date().toISOString()); 35 | return _.apply(o, args); 36 | }; 37 | }); 38 | o.__ts__ = true; 39 | })(console); 40 | 41 | var auth = function (req, res, next) { 42 | function unauthorized(res) { 43 | res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); 44 | return res.sendStatus(401); 45 | }; 46 | 47 | // bypass auth for local devices or empty username/password 48 | if ((username == "" && password == "") || req.ip.indexOf("127.0.0.") == 0) 49 | return next(); 50 | 51 | var user = basicAuth(req); 52 | 53 | if (!user || !user.name || !user.pass) { 54 | return unauthorized(res); 55 | }; 56 | 57 | if (user.name === username && user.pass === password) { 58 | return next(); 59 | } else { 60 | console.warn('login failure: [' + user.name + '][' + user.pass + ']'); 61 | return unauthorized(res); 62 | }; 63 | }; 64 | 65 | 66 | 67 | app.set('port', httpPort); 68 | if (ua != null) 69 | app.use(ua.middleware("UA-64954808-1", { cookieName: '_ga' })); 70 | app.use(express.static(path.join(__dirname, 'public'))); 71 | app.use(auth); 72 | app.use(logger); 73 | app.use(bodyParser.urlencoded({ extended: true })) 74 | app.use(favicon(__dirname + '/public/images/favicon.png')); 75 | app.use(methodOverride('X-HTTP-Method-Override')); 76 | app.use(logErrors); 77 | app.use(clientErrorHandler); 78 | app.use(errorHandler); 79 | 80 | // read configuration and start the web server 81 | // restart web server if port changes 82 | var StartServer = function () { 83 | 84 | var listen = function (port) { 85 | 86 | var connections = {}; 87 | 88 | console.log("Setting port: " + port); 89 | app.set('port', port); 90 | httpPort = port; 91 | 92 | var httpServer = app.listen(app.get('port'), function () { 93 | console.log('App Server running at port ' + app.get('port')); 94 | }); 95 | 96 | connections = {}; 97 | httpServer.on('connection', function (conn) { 98 | var key = conn.remoteAddress + ':' + conn.remotePort; 99 | connections[key] = conn; 100 | conn.on('close', function () { 101 | delete connections[key]; 102 | }); 103 | }); 104 | 105 | httpServer.closeconnections = function () { 106 | for (var key in connections) { 107 | console.log('closing connection: ' + key); 108 | connections[key].destroy(); 109 | } 110 | } 111 | 112 | return httpServer; 113 | }; 114 | 115 | db.readConfig(function (err, results) { 116 | if (err) { 117 | console.log('failed to read username and password from config: ' + err); 118 | } else { 119 | if (results["UserName"] != null && results["Password"] != null) { 120 | username = results["UserName"]; 121 | password = results["Password"]; 122 | } 123 | console.log('username: ' + username + " password: " + password); 124 | 125 | var port = parseInt(results["Port"], 10); 126 | 127 | if (isNaN(port)) { 128 | console.log("Invalid Port: (" + results["Port"] + ") using " + httpPort + " instead"); 129 | port = httpPort; 130 | } 131 | 132 | if (server == null) { 133 | server = listen(port); 134 | } 135 | else if (port != httpPort) { 136 | 137 | console.log("closing server because port changed from " + httpPort + " to " + port); 138 | server.close(function () { 139 | console.log("server closed - restarting on port " + port); 140 | server = listen(port); 141 | }); 142 | 143 | // closing the server only disables new connections so we also need to close existing connections 144 | server.closeconnections(); 145 | console.log('closed all existing connections'); 146 | } 147 | 148 | 149 | } 150 | }); 151 | }; 152 | 153 | function logger(req, res, next) { 154 | var start = (new Date()).getTime(); 155 | res._startTime = start; 156 | console.log('start: ' + req.method + " " + req.url); 157 | 158 | onFinished(res, function (err) { 159 | var duration = (new Date()).getTime() - res._startTime; 160 | console.log('end: ' + req.method + " " + req.url + " " + duration + " ms"); 161 | 162 | //req.visitor.debug(); 163 | if (req.visitor != null) 164 | req.visitor.timing(req.method, req.url, duration).send(); 165 | }) 166 | 167 | next(); 168 | } 169 | 170 | function logErrors(err, req, res, next) { 171 | console.log(err); 172 | console.error(err.stack); 173 | if (req.visitor != null) 174 | req.visitor.exception(err.message + "\r\n" + err.stack).send(); 175 | next(err); 176 | } 177 | 178 | function clientErrorHandler(err, req, res, next) { 179 | if (req.xhr) { 180 | res.send(500, { error: 'Server error' }); 181 | } else { 182 | next(err); 183 | } 184 | } 185 | 186 | function errorHandler(err, req, res, next) { 187 | res.status(500); 188 | res.render('error', { error: err }); 189 | } 190 | 191 | function isNumber(n) { 192 | return !isNaN(parseFloat(n)) && isFinite(n); 193 | } 194 | 195 | 196 | app.get('/powermeter.db', function (req, res) { 197 | 198 | var options = { 199 | root: __dirname, 200 | dotfiles: 'deny', 201 | headers: { 202 | 'x-timestamp': Date.now(), 203 | 'x-sent': true 204 | } 205 | }; 206 | 207 | var fileName = "powermeter.db"; 208 | db.lockWrites(true); 209 | res.sendFile(fileName, options, function (err) { 210 | db.lockWrites(false); 211 | if (err) 212 | next(err); 213 | else 214 | console.log('Sent:', fileName); 215 | }); 216 | }); 217 | 218 | app.get('/waveform', function (req, res) { 219 | var circuitId = req.query.circuitId; 220 | console.log("waveform(" + circuitId + ')'); 221 | res.send(power.ReadCircuit(circuitId)); 222 | }); 223 | app.get('/instant', function (req, res) { 224 | res.send(power.Readings()); 225 | }); 226 | app.get('/power', function (req, res, next) { 227 | var circuitId = req.query.circuitId; 228 | var start = req.query.start; 229 | var end = req.query.end; 230 | var groupBy = req.query.groupBy; 231 | var offset = req.query.offset; 232 | 233 | console.log("power(" + circuitId + ', ' + start + ', ' + end + ', ' + groupBy + ', ' + offset + ')'); 234 | var telemetry = []; 235 | power.ReadPower(circuitId, new Date(Number(start)), new Date(Number(end)), groupBy, offset, telemetry, function (err, result) { 236 | 237 | if (err) 238 | next(err); 239 | else { 240 | result.Telemetry = telemetry; 241 | res.send(result); 242 | } 243 | }); 244 | }); 245 | app.get('/state', function (req, res, next) { 246 | var circuitId = req.query.circuitId; 247 | 248 | console.log("state(" + circuitId + ')'); 249 | res.send(power.ReadState(circuitId)); 250 | }); 251 | app.get('/cumulative', function (req, res, next) { 252 | var start = req.query.start; 253 | var end = req.query.end; 254 | var order = req.query.order; 255 | 256 | console.log("cumulative(" + start + ', ' + end + ', ' + order + ')'); 257 | 258 | var startDate = null, endDate = null; 259 | if (start != null) 260 | startDate = new Date(Number(start)); 261 | 262 | if (end != null) 263 | endDate = new Date(Number(end)); 264 | 265 | var telemetry = []; 266 | 267 | power.Cumulative(startDate, endDate, order, telemetry, function (err, result) { 268 | if (err) { 269 | next(err); 270 | } 271 | else { 272 | result.Telemetry = telemetry; 273 | res.send(result); 274 | } 275 | }); 276 | }); 277 | app.get('/config', function (req, res, next) { 278 | var sendFile = req.query.file; 279 | power.GetCircuits(function (err, result) { 280 | if (err) 281 | next(err); 282 | else { 283 | if (sendFile) { 284 | res.set({ "Content-Disposition": "attachment; filename=config.json" }); 285 | } 286 | res.send(result); 287 | 288 | } 289 | }, true); 290 | }); 291 | app.post('/restart', function (req, res, next) { 292 | gracefulShutdown(); 293 | res.send("shutting down"); 294 | }); 295 | 296 | //app.get('/compact', function (req, res, next) { 297 | 298 | // if (compactRunning) 299 | // return res.send('Compact already running'); 300 | 301 | // var start = req.query.start; 302 | // var end = req.query.end; 303 | 304 | // if (start == null || end == null) 305 | // return res.send("invalid start or end date"); 306 | 307 | // compactRunning = true; 308 | 309 | // console.log("compact(" + start + ', ' + end + ')'); 310 | 311 | // var startDate = null, endDate = null; 312 | // if (isNumber(start)) 313 | // startDate = new Date(Number(start)); 314 | // else 315 | // startDate = new Date(start); 316 | 317 | // if (isNumber(end)) 318 | // endDate = new Date(Number(end)); 319 | // else 320 | // endDate = new Date(end); 321 | 322 | 323 | // db.compact(startDate, endDate, function (err) { 324 | // compactRunning = false; 325 | // if (err) { 326 | // next(err); 327 | // } 328 | // else { 329 | // res.send("compacted in: " + elapsed + " seconds"); 330 | // } 331 | // }); 332 | 333 | //}); 334 | app.get('/count', function (req, res, next) { 335 | var start = req.query.start; 336 | var end = req.query.end; 337 | 338 | if (start == null || end == null) 339 | return res.send("invalid start or end date"); 340 | 341 | console.log("count(" + start + ', ' + end + ')'); 342 | 343 | var startDate = null, endDate = null; 344 | if (isNumber(start)) 345 | startDate = new Date(Number(start)); 346 | else 347 | startDate = new Date(start); 348 | 349 | if (isNumber(end)) 350 | endDate = new Date(Number(end)); 351 | else 352 | endDate = new Date(end); 353 | 354 | 355 | db.count(startDate, endDate, function (err, result) { 356 | compactRunning = false; 357 | if (err) { 358 | next(err); 359 | } 360 | else { 361 | res.send("Compacted: " + result.Compacted + " Not Compacted: " + result.NotCompacted); 362 | } 363 | }); 364 | 365 | }); 366 | app.post('/enabled', function (req, res, next) { 367 | var config = req.body.config; 368 | 369 | if (config == null || config.length == 0) { 370 | console.log("invalid post config value= " + config); 371 | next("Invalid configuration"); 372 | res.send('error'); 373 | } else { 374 | console.log('circuit: ' + config.circuit + " enabled: " + config.enabled); 375 | power.UpdateCircuitEnabled(config.circuit, config.enabled); 376 | res.send('success'); 377 | } 378 | }); 379 | app.post('/config', function (req, res, next) { 380 | var config = req.body.config; 381 | 382 | if (config == null || config.length == 0) { 383 | console.log("invalid post config value= " + config); 384 | next("Invalid configuration"); 385 | res.send('error'); 386 | } else { 387 | power.ReplaceConfiguration(function (err) { 388 | if (err) 389 | res.send('error'); 390 | else { 391 | res.send('success'); 392 | } 393 | }, config); 394 | } 395 | }); 396 | 397 | app.post('/restoreconfig', function (req, res, next) { 398 | var config = req.body; 399 | 400 | if (config == null || config.PiPowerMeterConfig == null || config.PiPowerMeterConfig.Configuration == null || config.PiPowerMeterConfig.Configuration.Circuits == null) { 401 | console.log("invalid post config value= " + config); 402 | next("Invalid configuration"); 403 | res.send('error'); 404 | } else { 405 | config = config.PiPowerMeterConfig.Configuration; 406 | power.ReplaceConfiguration(function (err) { 407 | if (err) 408 | res.send('error'); 409 | else { 410 | // remove non config properties 411 | delete config.Circuits; 412 | delete config.DatabaseRows; 413 | delete config.DatabaseSize; 414 | delete config.SoftwareVersion; 415 | delete config.Uptime; 416 | delete config.VoltageFactor; 417 | 418 | power.ReplaceProbeDefConfiguration(function (err) { 419 | if (err) 420 | next(err); 421 | else 422 | res.send('success'); 423 | 424 | // reload config in case username/password changed 425 | StartServer(); 426 | 427 | }, config); 428 | } 429 | }, config.Circuits); 430 | } 431 | }); 432 | app.post('/probeDef', function (req, res, next) { 433 | var config = req.body.config; 434 | 435 | if (config == null) { 436 | console.log("invalid post config value= " + config); 437 | next("Invalid configuration"); 438 | } else { 439 | power.ReplaceProbeDefConfiguration(function (err) { 440 | if (err) 441 | next(err); 442 | else 443 | res.send('success'); 444 | 445 | // reload config in case username/password changed 446 | StartServer(); 447 | 448 | }, config); 449 | } 450 | }); 451 | app.post('/deleteCircuit', function (req, res, next) { 452 | var circuitId = req.body.circuitId; 453 | 454 | if (isNaN(circuitId)) { 455 | console.log("invalid circuitId value= " + circuitId); 456 | next("Invalid circuitId"); 457 | } else { 458 | power.DeleteCircuit(function (err) { 459 | if (err) 460 | next(err); 461 | else 462 | res.send('success'); 463 | }, circuitId); 464 | } 465 | }); 466 | app.post('/deleteProbe', function (req, res, next) { 467 | var probeId = req.body.probeId; 468 | 469 | if (isNaN(probeId)) { 470 | console.log("invalid probeId value= " + probeId); 471 | next("Invalid probeId"); 472 | } else { 473 | power.DeleteProbe(function (err) { 474 | if (err) 475 | next(err); 476 | else 477 | res.send('success'); 478 | }, probeId); 479 | } 480 | }); 481 | app.post('/update', function (req, res, next) { 482 | console.log('Updating source...'); 483 | var exec = require('child_process').exec; 484 | exec('git pull & npm install', function (error, stdout, stderr) { 485 | if (error) 486 | next(error); 487 | else { 488 | console.log('stdout: ' + stdout); 489 | console.log('stderr: ' + stderr); 490 | gracefulShutdown(); 491 | 492 | res.send('success'); 493 | } 494 | }); 495 | }); 496 | 497 | app.get('/log', function (req, res, next) { 498 | var options = { 499 | root: __dirname + '/../', 500 | dotfiles: 'deny', 501 | headers: { 502 | 'x-timestamp': Date.now(), 503 | 'x-sent': true 504 | } 505 | }; 506 | 507 | var fileName = "nodejs.err.log"; 508 | res.sendFile(fileName, options, function (err) { 509 | if (err) 510 | next(err); 511 | else 512 | console.log('Sent:', fileName); 513 | }); 514 | 515 | }); 516 | 517 | app.get('/', function (req, res, next) { 518 | var options = { 519 | root: __dirname + '/public/', 520 | dotfiles: 'deny', 521 | headers: { 522 | 'x-timestamp': Date.now(), 523 | 'x-sent': true 524 | } 525 | }; 526 | 527 | var fileName = "usage.html"; 528 | res.sendFile(fileName, options, function (err) { 529 | if (err) 530 | next(err); 531 | else 532 | console.log('Sent:', fileName); 533 | }); 534 | 535 | }); 536 | // Express route for any other unrecognised incoming requests 537 | app.get('*', function(req, res){ 538 | res.send(404, 'Unrecognized API call'); 539 | }); 540 | 541 | 542 | power.Start(); 543 | StartServer(); 544 | 545 | 546 | // this function is called when you want the server to die gracefully 547 | // i.e. wait for existing connections 548 | var gracefulShutdown = function () { 549 | console.log("Received kill signal, shutting down gracefully."); 550 | power.Stop(); 551 | if (server != null) { 552 | server.close(function () { 553 | console.log("Closed out remaining connections."); 554 | process.exit() 555 | }); 556 | 557 | server.closeconnections(); 558 | } 559 | 560 | // if after 561 | setTimeout(function () { 562 | console.error("Could not close connections in time, forcefully shutting down"); 563 | process.exit() 564 | }, 10 * 1000); 565 | } 566 | 567 | // listen for TERM signal .e.g. kill 568 | process.on('SIGTERM', gracefulShutdown); 569 | 570 | // listen for INT signal e.g. Ctrl-C 571 | process.on('SIGINT', gracefulShutdown); 572 | -------------------------------------------------------------------------------- /reader.js: -------------------------------------------------------------------------------- 1 | // This is a child process that should be forked from the parent and is responsible for 2 | // reading from the power meter hardware and sending messages back to its parent 3 | // every time it reads a new sample. On startup it enters a loop and sequentially 4 | // reads samples until told to stop by the parent 5 | 6 | 7 | 8 | var HardwareVersion = 0; 9 | var samples = 500; // number of instantaneous voltage and current samples to collect for each measurement 10 | var bytesPerSample = 10; 11 | var OutputPins, InputPins; 12 | var sampleBuffer = new Buffer(samples * bytesPerSample); 13 | var Mode, Config; 14 | var Epsilon60Hz = "01eb85", Epsilon50Hz = "01999a"; 15 | var Epsilon = Epsilon60Hz; // default to 60Hz 16 | 17 | var Registers = { 18 | Config: 0, 19 | CurrentDCOffset: 1, 20 | CurrentGain: 2, 21 | VoltageDCOffset: 3, 22 | VoltageGain: 4, 23 | CycleCount: 5, 24 | PulseRateE: 6, 25 | InstCurrent: 7, 26 | InstVoltage: 8, 27 | InstPower: 9, 28 | RealPower: 10, 29 | RmsCurrent: 11, 30 | RmsVoltage: 12, 31 | Epsilon: 13, // line frequency ratio 32 | PowerOffset: 14, 33 | Status: 15, 34 | CurrentACOffset: 16, 35 | VoltageACOffset: 17, 36 | Mode: 18, 37 | Temp: 19, 38 | AveReactivePower: 20, 39 | InstReactivePower: 21, 40 | PeakCurrent: 22, 41 | PeakVoltge: 23, 42 | ReactivePowerTriangle: 24, 43 | PowerFactor: 25, 44 | InterruptMask: 26, 45 | ApparentPower: 27, 46 | Control: 28, 47 | HarmonicActivePower: 29, 48 | FundamentalActivePower: 30, 49 | FundamentalReactivePower: 31 50 | }; 51 | 52 | var cs5463 = null; 53 | // comment below line for WebMatrix testing 54 | var cs5463 = require("cs5463"); 55 | 56 | 57 | var sleep = function (delayMs) { 58 | var s = new Date().getTime(); 59 | while ((new Date().getTime() - s) < delayMs) { 60 | //do nothing 61 | //console.log('sleeping'); 62 | } 63 | } 64 | 65 | var command = function (cmd, desc) { 66 | if (_DeviceOpen) { 67 | cs5463.send(cmd); 68 | if (desc != null) 69 | console.log('command: ' + desc + '(' + cmd + ')') 70 | } 71 | } 72 | 73 | var write = function (cmd, desc) { 74 | if (_DeviceOpen) { 75 | cs5463.send(cmd); 76 | if (desc != null) 77 | console.log('write: ' + desc + '(' + cmd + ')') 78 | } 79 | } 80 | 81 | var read = function (register, desc) { 82 | if (_DeviceOpen) { 83 | var cmd = (register << 1).toString(16) + 'FFFFFF'; 84 | while (cmd.length < 8) 85 | cmd = '0' + cmd; 86 | //console.log('cmd: ' + cmd) 87 | 88 | var result = cs5463.send(cmd); 89 | var ret = new Buffer(result, 'hex').slice(1); 90 | 91 | if (desc != null) 92 | console.log('read: ' + desc + '(' + cmd + ') -> ' + ret.toString('hex')); // + ' ' + result); 93 | 94 | return ret; 95 | } else { 96 | return null; 97 | } 98 | 99 | } 100 | 101 | var getCommand = function (register) { 102 | var c = (register << 1).toString(16); 103 | if (c.length == 1) 104 | c = '0' + c; 105 | return c + 'FFFFFF'; 106 | } 107 | 108 | var makeReadCommand = function (registers) { 109 | var cmd = ""; 110 | if (registers instanceof Array) { 111 | for (var i = 0; i < registers.length; i++) { 112 | cmd += getCommand(registers[i]); 113 | } 114 | } else { 115 | cmd = getCommand(registers); 116 | } 117 | 118 | return cmd; 119 | } 120 | 121 | var convert = function (buffer, binPt, neg) { 122 | 123 | var power = binPt; 124 | var result = 0; 125 | for (var i = 0; i < 3; i++) { 126 | var byte = buffer[i]; 127 | //console.log(byte.toString()) 128 | 129 | for (var j = 7; j >= 0; j--) { 130 | if (byte & (1 << j)) { 131 | 132 | var x; 133 | 134 | if (neg && i == 0 && j == 7) 135 | x = -Math.pow(2, power); 136 | else 137 | x = Math.pow(2, power); 138 | 139 | result += x; 140 | //console.log('(' + i + ',' + j + ')' + x); 141 | } 142 | power--; 143 | } 144 | } 145 | 146 | return result; 147 | } 148 | 149 | // board should be 0-7 150 | // currentchannel should be 0-15 151 | // voltagechannel should be 0-3 152 | var SetCircuit = function (board, currentChannel, voltageChannel) { 153 | 154 | //console.log('set: ' + board + ', ' + currentChannel + ', ' + voltageChannel); 155 | if (board < 0 || board > 8) { 156 | console.log('Invalid board: ' + board); 157 | return; 158 | } 159 | 160 | if (currentChannel < 0 || currentChannel > 15) { 161 | console.log('Invalid current channel: ' + currentChannel); 162 | return; 163 | } 164 | 165 | if (voltageChannel < 0 || voltageChannel > 3) { 166 | console.log('Invalid voltage channel: ' + voltageChannel); 167 | return; 168 | } 169 | 170 | if (_DeviceOpen) { 171 | 172 | // disable 173 | cs5463.DigitalWrite(OutputPins.disable, 1); 174 | 175 | // set board 176 | cs5463.DigitalWrite(OutputPins.board0, (board & 0x1)); 177 | cs5463.DigitalWrite(OutputPins.board1, (board & 0x2)); 178 | cs5463.DigitalWrite(OutputPins.board2, (board & 0x4)); 179 | 180 | // set current channel 181 | cs5463.DigitalWrite(OutputPins.channel0, (currentChannel & 0x1)); 182 | cs5463.DigitalWrite(OutputPins.channel1, (currentChannel & 0x2)); 183 | cs5463.DigitalWrite(OutputPins.channel2, (currentChannel & 0x4)); 184 | cs5463.DigitalWrite(OutputPins.channel3, (currentChannel & 0x8)); 185 | 186 | // set voltage channel 187 | cs5463.DigitalWrite(OutputPins.voltage0, (voltageChannel & 0x1)); 188 | cs5463.DigitalWrite(OutputPins.voltage1, (voltageChannel & 0x2)); 189 | 190 | // enable 191 | cs5463.DigitalWrite(OutputPins.disable, 0); 192 | } 193 | } 194 | 195 | var resultFromBuffer = function (buffer, index) { 196 | var offset = index * 4 + 1; 197 | return buffer.slice(offset, offset + 3); 198 | } 199 | 200 | var Samples60Hz = 0, Samples50Hz = 0; 201 | 202 | var ReadPower = function (iFactor, vFactor) { 203 | //console.log(iFactor +", " + vFactor); 204 | 205 | ResetIfNeeded(); 206 | 207 | if (!_DeviceOpen) 208 | return; 209 | 210 | var result = { 211 | vInst: [], 212 | iInst: [], 213 | tsInst: [], 214 | ts: new Date(), 215 | tsZC: [] 216 | }; 217 | 218 | var lastV = 0, lastTsZC = 0, lastTs = 0, totalTime = 0, totalCount = 0; 219 | sampleBuffer.fill(0); 220 | 221 | // do measurement 222 | var instSamples; 223 | try { 224 | instSamples = cs5463.ReadCycleWithInterrupts(sampleBuffer); 225 | if (instSamples <= 0) { 226 | console.log("ReadCycle returned: " + instSamples + ' samples'); 227 | return null; 228 | } 229 | } 230 | catch (err) { 231 | //console.log("ReadCycleWithInterrupts failed: " + err); 232 | console.error("ReadCycleWithInterrupts failed: " + err); 233 | return null; 234 | } 235 | 236 | 237 | 238 | //console.log("ReadCycle returned: " + instSamples + ' samples'); 239 | // convert buffer values for instantaneous current and voltage 240 | // buffer is formatted as follows: 241 | // bytes 0-2: Instantaneous current 242 | // bytes 3-5: Instantaneous voltage 243 | // bytes 6-9: timestamp 244 | for (var s = 0; s < instSamples; s++) { 245 | var offset = s * bytesPerSample; 246 | 247 | var iInst = convert(sampleBuffer.slice(offset, offset + 3), 0, true) * iFactor; 248 | var vInst = convert(sampleBuffer.slice(offset + 3, offset + 6), 0, true) * vFactor; 249 | var tsInst = sampleBuffer.readInt32LE(offset + 6) / 1000000.0; 250 | 251 | result.iInst.push(Number(iInst)); 252 | result.vInst.push(Number(vInst)); 253 | result.tsInst.push(Number(tsInst)); 254 | 255 | // frequency detect 256 | // look for zero crossing and ensure we didn't miss any samples 257 | if ((lastV > 0 && vInst < 0) || (lastV < 0 && vInst > 0)) { 258 | 259 | var tsZCInterpolated = lastTs + lastV * (tsInst - lastTs) / (lastV - vInst) 260 | if (lastTsZC > 0 && (tsInst - lastTs) < 0.375) { 261 | // Sample freq should be 4000Hz which is 0.25 ms per sample so use 0.375 for some margin 262 | // if sample freq > 0.375 ms we'll assume a sample was missed and throw out the reading 263 | 264 | // throw out any samples that are not between 40Hz and 70Hz 265 | // ex: (1/40) / 2 = 12.5 ms 266 | // ex: (1/70) / 2 = 7.1 ms 267 | var sampleTime = tsZCInterpolated - lastTsZC; 268 | if (sampleTime >= 7.1 && sampleTime <= 12.5) { 269 | totalCount++; 270 | totalTime += (tsZCInterpolated - lastTsZC); 271 | result.tsZC.push(Number(tsZCInterpolated)); 272 | } 273 | } 274 | lastTsZC = tsZCInterpolated; 275 | } 276 | lastV = vInst; 277 | lastTs = tsInst; 278 | } 279 | 280 | if (totalCount > 0) 281 | result.CalculatedFrequency = 1000 / ((totalTime / totalCount) * 2); //in Hz 282 | else 283 | result.CalculatedFrequency = 0; 284 | 285 | //console.log('CalculatedFrequency: ' + result.CalculatedFrequency); 286 | 287 | // only consider samples with at least 10 data points 288 | if (totalCount >= 10) { 289 | // Change fundamental frequency when we get at least 15 samples in a row 290 | if (result.CalculatedFrequency > 45 && result.CalculatedFrequency < 55) { 291 | Samples50Hz++; 292 | Samples60Hz = 0; 293 | 294 | if (Samples50Hz >= 15) 295 | Epsilon = Epsilon50Hz; 296 | } 297 | else if (result.CalculatedFrequency > 55 && result.CalculatedFrequency < 65) { 298 | Samples60Hz++ 299 | Samples50Hz = 0; 300 | 301 | if (Samples60Hz >= 15) 302 | Epsilon = Epsilon60Hz; 303 | } 304 | } 305 | 306 | 307 | 308 | // read average values over complete cycle 309 | var cmd = makeReadCommand( 310 | [Registers.RmsCurrent, 311 | Registers.RmsVoltage, 312 | Registers.RealPower, 313 | Registers.AveReactivePower, 314 | Registers.PowerFactor, 315 | Registers.PeakCurrent, 316 | Registers.PeakVoltge, 317 | Registers.Epsilon]); 318 | 319 | var r = new Buffer(cs5463.send(cmd), 'hex'); 320 | 321 | result.iRms = convert(resultFromBuffer(r, 0), -1, false) * iFactor; 322 | result.vRms = convert(resultFromBuffer(r, 1), -1, false) * vFactor; 323 | result.pAve = convert(resultFromBuffer(r, 2), 0, true) * vFactor * iFactor; 324 | result.qAve = convert(resultFromBuffer(r, 3), 0, true) * vFactor * iFactor; // average reactive power 325 | result.pf = convert(resultFromBuffer(r, 4), 0, true); 326 | result.iPeak = convert(resultFromBuffer(r, 5), 0, true) * iFactor; 327 | result.vPeak = convert(resultFromBuffer(r, 6), 0, true) * vFactor; 328 | result.freq = convert(resultFromBuffer(r, 7), 0, true) * 4000.0; 329 | 330 | //if (Math.abs(result.pAve) < 3.0) 331 | // result.pAve = 0; // noise 332 | 333 | //_pf = result.pf; 334 | 335 | return result; 336 | } 337 | 338 | var ResetIfNeeded = function () { 339 | 340 | var epsilon = read(Registers.Epsilon); 341 | var mode = read(Registers.Mode); 342 | var config = read(Registers.Config); 343 | var status = read(Registers.Status); 344 | 345 | // Check status of: 346 | // IOR and VOR 347 | // IROR, VROR, EOR, IFAULT, VSAG 348 | // TOD, VOD, IOD, LSD 349 | if ((status[0] & 0x03) || (status[1] & 0x7C) || (status[2] & 0x58)) { 350 | console.log('Resetting due to incorrect status: ' + status.toString('hex')); 351 | console.error('Resetting due to incorrect status: ' + status.toString('hex')); 352 | Reset(); 353 | } 354 | else if (epsilon.toString('hex') != Epsilon) { 355 | console.log('Resetting due to incorrect epsilon: ' + epsilon.toString('hex') + ' expected: ' + Epsilon); 356 | Reset(); 357 | } 358 | else if (mode.toString('hex') != Mode) { 359 | console.log('Resetting due to incorrect Mode: ' + mode.toString('hex') + ' expected: ' + Mode); 360 | Reset(); 361 | } 362 | else if (config.toString('hex') != Config) { 363 | console.log('Resetting due to incorrect Config: ' + config.toString('hex') + ' expected: ' + Config); 364 | Reset(); 365 | } else { 366 | //Reset(); 367 | //console.log('Reset not needed:' + epsilon.toString('hex') + " " + mode.toString('hex') + " " + config.toString('hex')); 368 | } 369 | } 370 | 371 | var DumpRegisters = function () { 372 | console.log("Register dump:"); 373 | for (var propertyName in Registers) { 374 | var val = Registers[propertyName]; 375 | //vconsole.log(val + ' - ' + propertyName + ': ' + read(val).toString('hex')); 376 | console.log(val + ' - ' + propertyName + ': ' + read(val).toString('hex')); 377 | } 378 | } 379 | 380 | var Reset = function () { 381 | 382 | console.log('RESET'); 383 | DumpRegisters(); 384 | 385 | // HARD RESET CHIP 386 | cs5463.DigitalPulse(OutputPins.reset, 0, 1, 100); 387 | 388 | sleep(500); 389 | 390 | write('FFFFFFFE', 'init serial port'); 391 | command('80', 'reset'); 392 | var s; 393 | do { 394 | if (!_DeviceOpen) 395 | return; 396 | 397 | s = read(15); // read status 398 | console.log('status: ' + s.toString('hex')); 399 | 400 | if (!(s[0] & 0x80)) 401 | sleep(500); 402 | } while (!(s[0] & 0x80)); 403 | 404 | 405 | write("5EFFFFFF", "clear status"); 406 | 407 | 408 | //write('64000060', 'hpf on'); 409 | //write('64000160', 'hpf on with voltage phase compensation'); 410 | read(18, 'read Mode register'); 411 | // 60 = 0110 0000 => High-Pass filters enabled on both current and voltage channels 412 | // E0 = 1110 0000 => one sample of current channel delay, High-Pass filters enabled on both current and voltage channels 413 | // E1 = 1110 0001 => one sample of current channel delay, High-Pass filters enabled on both current and voltage channels, auto line frequency measurement enabled 414 | //write('640000E0', 'hpf on with current phase compensation'); 415 | write('64' + Mode, 'hpf on with current phase compensation'); 416 | read(18, 'read Mode register'); 417 | 418 | read(0, 'read configuration register'); 419 | //write('40001001', 'interrupts set to high to low pulse'); 420 | //write('40C01001', 'interrupts set to high to low pulse with phase comp'); 421 | write('40' + Config, 'interrupts set to high to low pulse with phase comp'); 422 | // C0 = 1100 0000 => first 7 bits set delay in voltage channel relative to current channel (00-7F), 1100000 => 423 | // 10 = 0001 0000 => set interrupts to high to low pulse 424 | // 01 = 0000 0001 => set clock divider to 1 (default) 425 | read(0, 'read configuration register'); 426 | 427 | console.log('epsilon before: ' + convert(read(13), 0, true)); 428 | write('5A' + Epsilon, 'set epsilon to ' + Epsilon); 429 | console.log('epsilon after: ' + convert(read(13), 0, true)); 430 | 431 | console.log('initialized'); 432 | 433 | } 434 | 435 | 436 | 437 | 438 | // Kick off the main read loop 439 | var Open = function () { 440 | if (cs5463 != null) { 441 | Close(); 442 | cs5463.Open("/dev/spidev0.0", 2000000); // raspberry pi 443 | //cs5463.Open("/dev/spidev0.0", 1200000); // banana pi 444 | 445 | OutputPins = { 446 | channel0: 0, // header 11 - GPIO0 447 | channel1: 1, // header 12 - GPIO1 448 | channel2: 2, // header 13 - GPIO2 449 | channel3: 3, // header 15 - GPIO3 450 | board0: 4, // header 16 - GPIO4 451 | board1: 15, // header 18 - TxD 452 | board2: 9, // header 22 - GPIO6 453 | voltage0: 7, // header 7 - GPIO7 454 | voltage1: 16, // header 10 - RxD 455 | disable: 8, // header 3 - SDA0 (8 and 9 have internal pull-up resistors, use 15, 16 if that causes a problem) 456 | reset: 6 // header 22 - GPIO6 457 | } 458 | 459 | InputPins = { 460 | isr: 5 // Header 18 - GPIO5 (interrupt pin - connect to INT (20) on CS5463) 461 | } 462 | 463 | // enable output gpio pins 464 | for (var pin in OutputPins) { 465 | //console.log('pinmode(' + OutputPins[pin] + ') ' + pin); 466 | cs5463.PinMode(OutputPins[pin], 1); 467 | } 468 | 469 | _DeviceOpen = true; 470 | console.log("Device opened: Hardware version: " + HardwareVersion); 471 | 472 | Reset(); 473 | 474 | if (_DeviceOpen) { 475 | 476 | var intSetup = 0, intFallingEdge = 1, intRisingEdge = 2, intBothEdges = 3; 477 | var noResistor = 0, pullDownResistor = 1, pullUpResistor = 2; 478 | cs5463.InitializeISR(InputPins.isr, pullUpResistor, intFallingEdge); 479 | } 480 | } 481 | } 482 | 483 | //var DetectFrequency = function() { 484 | // SetCircuit(0, 0, 0); 485 | // var result = ReadPower(1, 1); 486 | // if (result) { 487 | 488 | // } 489 | //} 490 | 491 | var _DeviceOpen = false; 492 | var Close = function () { 493 | console.log("reader closed 1"); 494 | _DeviceOpen = false; 495 | if (cs5463 != null) 496 | cs5463.Close(); 497 | 498 | console.log("reader closed 2"); 499 | } 500 | 501 | // read from hardware 502 | process.on('message', function (data) { 503 | // console.log('reader received: ' + data.Action); 504 | if (data.Action == "Start") { 505 | HardwareVersion = data.HardwareVersion; 506 | Mode = data.Mode; 507 | Config = data.Config; 508 | Open(); 509 | } 510 | else if (data.Action == "Stop") { 511 | console.log("reader received stop"); 512 | Close(); 513 | } 514 | else if (data.Action == "Read") { 515 | //console.log("reader: Read"); 516 | //console.log(JSON.stringify(data)); 517 | for (var i = 0; i < data.Probes.length; i++) { 518 | 519 | var probe = data.Probes[i]; 520 | //console.log("reader: probe: " + probe.id); 521 | SetCircuit(probe.Board, probe.CurrentChannel, probe.VoltageChannel); 522 | 523 | var result = ReadPower(probe.iFactor, probe.vFactor); 524 | if (result == null || result.freq > 70 || result.freq < 40) 525 | result = null; 526 | else if ((probe.SourceType == 1 && result.pAve < 0.0) || // load cannot generate 527 | (probe.SourceType == 2 && result.pAve > 0.0)) { // source cannot consume 528 | result.iRms = 0.0; 529 | result.pAve = 0.0; 530 | result.qAve = 0.0; 531 | result.pf = 1.0; 532 | result.iPeak = 0.0; 533 | } 534 | 535 | probe.Result = result; 536 | 537 | if (Epsilon == Epsilon50Hz) 538 | data.Frequency = "50Hz"; 539 | else if (Epsilon == Epsilon60Hz) 540 | data.Frequency = "60Hz"; 541 | else 542 | data.Frequency = "Unknown"; 543 | } 544 | 545 | process.send(data); 546 | } 547 | 548 | }); 549 | 550 | -------------------------------------------------------------------------------- /public/circuit.js: -------------------------------------------------------------------------------- 1 | var ResetGraph = function () { 2 | $.ajax({ 3 | url: '/reset', 4 | type: 'get', 5 | dataType: 'json', 6 | cache: false, 7 | success: function (data) { } 8 | }); 9 | } 10 | 11 | var toCurrency = function (amount) { 12 | return Globalize().format(amount, "c", Region); 13 | } 14 | 15 | var toFloat = function (amount, fixed) { 16 | return Globalize().format(amount, 'n' + fixed, Region); 17 | } 18 | 19 | var html = ""; 20 | 21 | var selectTimespanOption = function (timespan) { 22 | if (timespan != null) { 23 | $('#Timespan option') 24 | .filter(function (index) { return $(this).text() === timespan; }) 25 | .prop('selected', true); 26 | } 27 | 28 | updateGroupBy(); 29 | } 30 | 31 | var InitializeGraph = function (channel, timespan, start, end, callback) { 32 | 33 | $.ajax({ 34 | url: '/config', 35 | type: 'get', 36 | dataType: 'json', 37 | cache: false, 38 | success: function (data) { 39 | Region = data.Region; 40 | if (Region == null || Region == "") 41 | Region = "en-US"; 42 | data = data.Circuits; 43 | var select = $("#circuits"); 44 | for (var i = 0; i < data.length; i++) { 45 | select.append(""); 46 | } 47 | 48 | if (channel != null) { 49 | $('#circuits option') 50 | .filter(function (index) { return $(this).text() === channel; }) 51 | .prop('selected', true); 52 | } 53 | 54 | selectTimespanOption(timespan); 55 | 56 | if (timespan == "Custom") { 57 | if (start != null && end != null && start != "" && end != "") { 58 | // set start and end time 59 | $("#start").addClass('dontSelectCustom').datetimepicker('setDate', new Date(start)).removeClass('dontSelectCustom'); 60 | $("#end").addClass('dontSelectCustom').datetimepicker('setDate', new Date(end)).removeClass('dontSelectCustom'); 61 | } else { 62 | $('#Timespan option') 63 | .filter(function (index) { return $(this).text() === "Hour"; }) 64 | .prop('selected', true); 65 | } 66 | } 67 | 68 | if ($.isFunction(callback)) 69 | callback(); 70 | } 71 | }); 72 | 73 | $("
").css({ 74 | position: "absolute", 75 | display: "none", 76 | border: "1px solid #fdd", 77 | padding: "2px", 78 | "background-color": "#fee", 79 | opacity: 0.80 80 | }).appendTo("body"); 81 | } 82 | 83 | var data, options, placeholder, lastTimespan=""; 84 | 85 | var RefreshGraph = function (circuitId, timespanDate, groupBy, callback) { 86 | //CurrentScale = currentScale; 87 | placeholder = $("#placeholder"); 88 | placeholder.empty(); 89 | $("#table").empty(); 90 | 91 | var elapsed = timespanDate.End - timespanDate.Start; 92 | 93 | if (elapsed == 0) { 94 | currentScale = 40; 95 | RefreshWaveformGraph(circuitId, currentScale, callback); 96 | } else { 97 | if (lastTimespan == "Instant") { 98 | $(window).trigger('resize'); 99 | console.log('resize'); 100 | } 101 | lastTimespan = timespanDate.timespan; 102 | 103 | //var groupBy; 104 | //if (elapsed <= 1000*60*60*24) // one day 105 | // groupBy = null; 106 | //else if (elapsed <= 1000*60*60*24*7) // one week 107 | // groupBy = 'hour'; 108 | //else if (elapsed <= 1000*60*60*24*31) // one month 109 | // groupBy = 'day'; 110 | //else 111 | //groupBy = 'day'; 112 | // groupBy = 'month'; 113 | 114 | RefreshPowerGraph(circuitId, timespanDate.Start, timespanDate.End, groupBy, callback); 115 | } 116 | } 117 | 118 | var GetBarGraphOptions = function (timeFormat, barWidth, minTickSize) { 119 | 120 | var options = { 121 | series: { 122 | bars: { 123 | show: true 124 | } 125 | }, 126 | bars: { 127 | align: "center", 128 | barWidth: barWidth 129 | }, 130 | xaxis: { 131 | //axisLabel: "Day of Month", 132 | axisLabelUseCanvas: true, 133 | axisLabelFontSizePixels: 12, 134 | axisLabelFontFamily: 'Verdana, Arial', 135 | axisLabelPadding: 10, 136 | //ticks: ticks' 137 | //mode: 'time', /*timezone: 'browser',*/timeformat: timeFormat, timeZoneOffset: (new Date()).getTimezoneOffset() 138 | mode: 'time', timezone: 'browser',timeformat: timeFormat 139 | }, 140 | yaxis: { 141 | axisLabel: "Average Power", 142 | axisLabelUseCanvas: true, 143 | axisLabelFontSizePixels: 12, 144 | axisLabelFontFamily: 'Verdana, Arial', 145 | axisLabelPadding: 3//, 146 | // tickFormatter: function (v, axis) { 147 | // return v + " Watts"; 148 | // } 149 | }, 150 | legend: { 151 | noColumns: 0, 152 | labelBoxBorderColor: "#000000", 153 | position: "nw" 154 | }, 155 | grid: { 156 | hoverable: true, 157 | borderWidth: 2, 158 | backgroundColor: { colors: ["#ffffff", "#EDF5FF"] } 159 | } 160 | }; 161 | 162 | if (minTickSize != null) 163 | options.xaxis.minTickSize = minTickSize; 164 | 165 | return options; 166 | } 167 | 168 | var GetLineGraphOptions = function (timeFormat) { 169 | 170 | return { 171 | series: { 172 | lines: { show: true }, 173 | points: { show: true, radius: 2}, 174 | shadowSize: 0 175 | }, 176 | xaxis: { mode: 'time', timezone: 'browser', timeformat: timeFormat }, 177 | yaxes: [{ tickFormatter: function (val, axis) { return val + " W"; } }], 178 | //yaxis: { min: -200, max: 200 }, //, zoomRange: [400, 400] }, 179 | selection: { mode: "x" }, 180 | crosshair: { mode: "x" }, 181 | grid: { hoverable: true, autoHighlight: false } 182 | 183 | //zoom: { interactive: true } 184 | }; 185 | } 186 | 187 | var RefreshPowerGraph = function (circuitId, start, end, groupBy, callback) { 188 | 189 | html = ""; 190 | //var v = [], c = []; 191 | var p = []; 192 | 193 | 194 | if (groupBy == null) 195 | groupBy = ""; 196 | 197 | var timeFormat, isLineGraph = false; 198 | if (groupBy.toLowerCase() == 'hour') { 199 | timeFormat = '%b %e
%I:%M %p'; 200 | options = GetBarGraphOptions(timeFormat, 1000 * 60 * 60); // barwidth= 1 hour 201 | } else if (groupBy.toLowerCase() == 'day') { 202 | timeFormat = '%b %e'; 203 | options = GetBarGraphOptions(timeFormat, 1000 * 60 * 60 * 24); // barwidth= 1 day 204 | } else if (groupBy.toLowerCase() == 'month') { 205 | timeFormat = '%b'; 206 | options = GetBarGraphOptions(timeFormat, 1000 * 60 * 60 * 24 * 30, [1, "month"]); // barwidth= 1 month 207 | } else { 208 | timeFormat = '%I:%M %p'; 209 | isLineGraph = true; 210 | options = GetLineGraphOptions(timeFormat); 211 | } 212 | 213 | var offset = (new Date()).getTimezoneOffset(); 214 | if (offset >= 0) 215 | offset = '-' + ('0' + (offset / 60)).slice(-2) + ':' + ('0' + (offset % 60)).slice(-2); 216 | else 217 | offset = ('0' + (-offset / 60)).slice(-2) + ':' + ('0' + (-offset % 60)).slice(-2); 218 | 219 | 220 | $.ajax({ 221 | url: '/power?circuitId=' + circuitId + '&start=' + start.getTime() + '&end=' + end.getTime() + '&groupBy=' + groupBy + '&offset=' + offset, 222 | type: 'get', 223 | dataType: 'json', 224 | cache: false, 225 | success: function (result) { 226 | 227 | $('.header').text(result.DeviceName + " Power Meter"); 228 | 229 | var min = 0, max = 0; 230 | if (result.ts.length > 0) { 231 | min = max = result.P[0]; 232 | for (var i = 0; i < result.ts.length; i++) { 233 | p.push([result.ts[i] * 1000, result.P[i]]); 234 | if (result.P[i] > max) 235 | max = result.P[i]; 236 | if (result.P[i] < min) 237 | min = result.P[i]; 238 | } 239 | } 240 | if (isLineGraph) { 241 | if (min > 0) 242 | options.yaxes[0].min = 0.0; 243 | if (max < 0) 244 | options.yaxes[0].max = 0.0; 245 | } 246 | 247 | var Kwh = (result.avg / 1000) * ((end.getTime() - start.getTime()) / (1000 * 60 * 60)); 248 | var cost = toCurrency(result.Cost * Kwh); 249 | 250 | html = "Min" + result.min + " watts" + 251 | "Avg" + result.avg + " watts" + 252 | "Max" + result.max + " watts" + 253 | "KWh" + toFloat(Kwh,2) + "" + 254 | "Cost" + cost + ""; 255 | 256 | 257 | data = [{ data: p, label: "Watts = -0000.00", color: "#5482FF"}]; 258 | 259 | placeholder.bind("plothover", function (event, pos, item) { 260 | latestPosition = pos; 261 | if (!updateLegendTimeout) { 262 | updateLegendTimeout = setTimeout(updateLegend, 50); 263 | } 264 | }); 265 | 266 | placeholder.dblclick(function () { 267 | options2.xaxis.min = null; 268 | options2.xaxis.max = null; 269 | plot = $.plot(placeholder, data, options); 270 | $('.legend table tbody').append(html); 271 | }); 272 | 273 | placeholder.bind("plotselected", function (event, ranges) { 274 | 275 | $("#selection").text(toFloat(ranges.xaxis.from,1) + " to " + toFloat(ranges.xaxis.to,1)); 276 | 277 | plot = $.plot(placeholder, data, $.extend(true, {}, options, { 278 | xaxis: { 279 | min: ranges.xaxis.from, 280 | max: ranges.xaxis.to 281 | } 282 | })); 283 | $('.legend table tbody').append(html); 284 | }); 285 | 286 | placeholder.bind("plotunselected", function (event) { 287 | $("#selection").text(""); 288 | }); 289 | 290 | var plot = $.plot(placeholder, data, options); 291 | 292 | if ($.isFunction(callback)) 293 | callback(); 294 | 295 | //var axes = plot.getAxes(); 296 | //var o = plot.pointOffset({ x: (axes.xaxis.max+axes.xaxis.min)/2, y: (axes.yaxis.max+axes.yaxis.min)/2 }); 297 | // Append it to the placeholder that Flot already uses for positioning 298 | $('.legend table tbody').append(html); 299 | 300 | placeholder.resize(function () { 301 | if (html != "") 302 | $('.legend table tbody').append(html); 303 | }); 304 | 305 | // placeholder.append("
" + html + "
"); 306 | 307 | 308 | var legends = placeholder.find(".legendLabel"); 309 | 310 | legends.each(function () { 311 | // fix the widths so they don't jump around 312 | $(this).css('width', $(this).width()); 313 | }); 314 | 315 | var updateLegendTimeout = null; 316 | var latestPosition = null; 317 | 318 | function updateLegend() { 319 | 320 | updateLegendTimeout = null; 321 | 322 | var pos = latestPosition; 323 | 324 | var axes = plot.getAxes(); 325 | if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max || 326 | pos.y < axes.yaxis.min || pos.y > axes.yaxis.max) { 327 | return; 328 | } 329 | 330 | var i, j, dataset = plot.getData(); 331 | for (i = 0; i < dataset.length; ++i) { 332 | 333 | var series = dataset[i]; 334 | 335 | // Find the nearest points, x-wise 336 | 337 | for (j = 0; j < series.data.length; ++j) { 338 | if (series.data[j][0] > pos.x) { 339 | break; 340 | } 341 | } 342 | 343 | if (series.data.length > 0) { 344 | // Now Interpolate 345 | var y, 346 | p1 = series.data[j - 1], 347 | p2 = series.data[j]; 348 | 349 | if (p1 == null) { 350 | y = p2[1]; 351 | } else if (p2 == null) { 352 | y = p1[1]; 353 | } else { 354 | //y = p1[1] + (p2[1] - p1[1]) * (pos.x - p1[0]) / (p2[0] - p1[0]); 355 | if ((pos.x - p1[0]) < (p2[0] - pos.x)) 356 | y = p1[1]; 357 | else 358 | y = p2[1]; 359 | } 360 | 361 | legends.eq(i).text(series.label.replace(/=.*/, "= " + toFloat(y,2))); 362 | } 363 | } 364 | 365 | 366 | } 367 | 368 | } 369 | }); 370 | } 371 | 372 | var RefreshWaveformGraph = function (circuitId, currentScale, callback) { 373 | 374 | options = { 375 | series: { 376 | lines: { show: true }, 377 | //points: { show: true }, 378 | shadowSize: 0 379 | }, 380 | xaxis: { min: 0, tickFormatter: function (val, axis) { return val + " ms"; } }, 381 | yaxes: [{ /*min: -200, max: 200,*/tickFormatter: function (val, axis) { return val + " V"; } }, 382 | { position: 0, min: -currentScale, max: currentScale, tickFormatter: function (val, axis) { return val + " A"; } }], 383 | //yaxis: { min: -200, max: 200 }, //, zoomRange: [400, 400] }, 384 | selection: { mode: "x" }, 385 | crosshair: { mode: "x" }, 386 | grid: { hoverable: true, autoHighlight: false } 387 | //zoom: { interactive: true } 388 | } 389 | 390 | var datasets = {}, v = [], c = []; 391 | html = ""; 392 | $.ajax({ 393 | url: '/waveform?circuitId=' + circuitId, 394 | type: 'get', 395 | dataType: 'json', 396 | cache: false, 397 | success: function (result) { 398 | 399 | if (result && result.DeviceName) 400 | $('.header').text(result.DeviceName + " Power Meter"); 401 | else 402 | $('.header').text("Power Meter"); 403 | 404 | 405 | data = []; 406 | if (result != null && result.Samples != null && result.Samples.length > 0) { 407 | //for (var i = 0; i < result.Samples[0].tsInst.length; i++) { 408 | // var now = result.Samples[0].tsInst[i]; 409 | // if (result.Samples[0].vInst.length >= i) 410 | // v.push([now, result.Samples[0].vInst[i]]); 411 | // if (result.Samples[0].iInst.length >= i) 412 | // c.push([now, result.Samples[0].iInst[i]]); 413 | //} 414 | 415 | var minTs = 0; 416 | $.each(result.Probes, function (index) { 417 | var obj = result.Probes[index]; 418 | //choiceContainer.append("
" + ""); 419 | 420 | var v = [], c = [], vref = [], zc = []; 421 | var sample = result.Samples[index]; 422 | 423 | var start = 0; 424 | // channel 0 rises at t=0 so find first (-) -> (+) zero crossing 425 | for (var i = 1; i < sample.tsInst.length; i++) { 426 | if ((obj.VoltageChannel == 0 && sample.vInst[i - 1] < 0 && sample.vInst[i] > 0) || 427 | (obj.VoltageChannel == 1 && sample.vInst[i - 1] > 0 && sample.vInst[i] < 0)) { 428 | start = i; 429 | break; 430 | } 431 | } 432 | 433 | for (var i = start; i < sample.tsInst.length; i++) { 434 | var now = sample.tsInst[i] - sample.tsInst[start]; 435 | if (sample.vInst.length >= i) 436 | v.push([now, sample.vInst[i]]); 437 | if (sample.iInst.length >= i) 438 | c.push([now, sample.iInst[i]]); 439 | 440 | if (i == sample.tsInst.length - 1 && (index == 0 || now < minTs)) 441 | minTs = now; 442 | } 443 | 444 | 445 | for (var i = 0; i < sample.tsZC.length; i++) { 446 | var now = sample.tsZC[i] - sample.tsInst[start]; 447 | if (now >= 0.0) 448 | zc.push([now, 0.0]); 449 | } 450 | 451 | //var channel = { v: v, c: c }; 452 | //datasets[obj.id] = channel; 453 | 454 | data.push({ data: v, label: "Probe" + obj.id + " Volts = -000.00", points: { show: true, radius: 2 } }); 455 | data.push({ data: c, label: "Probe" + obj.id + " Amps = -000.00", yaxis: 2, points: { show: true, radius: 2 } }); 456 | //data.push({ data: zc, label: "ZC" + obj.id , yaxis: 2, points: { show: true, radius: 2 } }); 457 | }); 458 | 459 | 460 | // find min length and truncate everything to match that 461 | $.each(data, function (index) { 462 | 463 | var i = data[index].data.length - 1; 464 | 465 | while (i >= 0 && data[index].data[i][0] > minTs) { 466 | i--; 467 | } 468 | 469 | data[index].data = data[index].data.slice(0, i); 470 | 471 | }); 472 | 473 | 474 | function getProbeValues(samples, property, includeTotal, digits) { 475 | var result = ""; 476 | var total = 0; 477 | for (var i = 0; i < samples.length; i++) { 478 | if (i > 0) 479 | result += ", "; 480 | result += toFloat(samples[i][property], digits); 481 | total += samples[i][property]; 482 | } 483 | 484 | if (includeTotal && samples.length > 1) { 485 | result += " (" + toFloat(total, digits) + ")"; 486 | } 487 | return result; 488 | } 489 | 490 | var html = "
Rms Voltage (volts)" + getProbeValues(result.Samples, "vRms", false, 1) + 491 | "
Rms Current (amps)" + getProbeValues(result.Samples, "iRms", false, 1) + 492 | "
Peak Voltage (volts)" + getProbeValues(result.Samples, "vPeak", false, 1) + 493 | "
Peak Current (amps)" + getProbeValues(result.Samples, "iPeak", false, 1) + 494 | "
Average real power (watts) " + getProbeValues(result.Samples, "pAve", true, 1) + 495 | "
Average reactive power (vars)" + getProbeValues(result.Samples, "qAve", true, 1) + 496 | "
Power factor" + getProbeValues(result.Samples, "pf", false, 6) + 497 | "
Timestamp" + (new Date(result.Samples[0].ts)).toLocaleString() + 498 | "
"; 499 | 500 | //var html = "
Rms Voltage" + result.Samples[0].vRms.toFixed(1) + " volts" + 501 | // "
Rms Current" + result.Samples[0].iRms.toFixed(1) + " amps" + 502 | // "
Peak Voltage" + result.Samples[0].vPeak.toFixed(1) + " volts" + 503 | // "
Peak Current" + result.Samples[0].iPeak.toFixed(1) + " amps" + 504 | // "
Average real power" + result.pAve.toFixed(1) + " watts" + 505 | // "
Average reactive power" + result.qAve.toFixed(1) + " vars" + 506 | // "
Power factor" + result.pf.toFixed(6) + 507 | // "
Timestamp" + (new Date(result.Samples[0].ts)).toLocaleString() + 508 | // "
"; 509 | 510 | /*$("#tooltip").html(html) 511 | //.css({ top: item.pageY + 5, left: item.pageX + 5 }) 512 | .css({ top: 200, left: 200 }) 513 | .fadeIn(200);*/ 514 | 515 | $("#table").html(html); 516 | 517 | if (lastTimespan != "Instant") { 518 | console.log('resize'); 519 | $(window).trigger('resize'); 520 | } 521 | 522 | } 523 | 524 | lastTimespan = "Instant"; 525 | 526 | 527 | 528 | 529 | 530 | 531 | placeholder.bind("plothover", function (event, pos, item) { 532 | latestPosition = pos; 533 | if (!updateLegendTimeout) { 534 | updateLegendTimeout = setTimeout(updateLegend, 50); 535 | } 536 | }); 537 | 538 | placeholder.dblclick(function () { 539 | options.xaxis.min = null; 540 | options.xaxis.max = null; 541 | plot = $.plot(placeholder, data, options); 542 | 543 | }); 544 | 545 | placeholder.bind("plotselected", function (event, ranges) { 546 | 547 | $("#selection").text(toFloat(ranges.xaxis.from, 1) + " to " + toFloat(ranges.xaxis.to, 1)); 548 | 549 | plot = $.plot(placeholder, data, $.extend(true, {}, options, { 550 | xaxis: { 551 | min: ranges.xaxis.from, 552 | max: ranges.xaxis.to 553 | } 554 | })); 555 | 556 | }); 557 | 558 | placeholder.bind("plotunselected", function (event) { 559 | $("#selection").text(""); 560 | }); 561 | 562 | var plot = $.plot(placeholder, data, options); 563 | 564 | if ($.isFunction(callback)) 565 | callback(); 566 | 567 | 568 | var legends = placeholder.find(".legendLabel"); 569 | 570 | legends.each(function () { 571 | // fix the widths so they don't jump around 572 | $(this).css('width', $(this).width()); 573 | }); 574 | 575 | var updateLegendTimeout = null; 576 | var latestPosition = null; 577 | 578 | function updateLegend() { 579 | 580 | updateLegendTimeout = null; 581 | 582 | var pos = latestPosition; 583 | 584 | var axes = plot.getAxes(); 585 | if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max || pos.y < axes.yaxis.min || pos.y > axes.yaxis.max) { 586 | return; 587 | } 588 | 589 | var i, j, dataset = plot.getData(); 590 | for (i = 0; i < dataset.length; ++i) { 591 | 592 | var series = dataset[i]; 593 | 594 | // Find the nearest points, x-wise 595 | 596 | for (j = 0; j < series.data.length; ++j) { 597 | if (series.data[j][0] > pos.x) { 598 | break; 599 | } 600 | } 601 | 602 | if (series.data.length > 0) { 603 | // Now Interpolate 604 | var y, p1 = series.data[j - 1], p2 = series.data[j]; 605 | 606 | if (p1 == null) { 607 | y = p2[1]; 608 | } else if (p2 == null) { 609 | y = p1[1]; 610 | } else { 611 | y = p1[1] + (p2[1] - p1[1]) * (pos.x - p1[0]) / (p2[0] - p1[0]); 612 | } 613 | 614 | legends.eq(i).text(series.label.replace(/=.*/, "= " + toFloat(y, 2))); 615 | 616 | } 617 | } 618 | } 619 | 620 | } 621 | }); 622 | } 623 | 624 | var ResizeGraphs = function () { 625 | if (data != null) { 626 | var plot = $.plot($("#placeholder"), data, options); 627 | if (html != "") 628 | $('.legend table tbody').append(html); 629 | } 630 | } -------------------------------------------------------------------------------- /public/configure.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Power Meter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 383 | 384 | 385 | 386 | 387 | 389 | 394 | 395 |
396 |
397 |

Circuits

398 |
399 |
400 |
401 |
402 | 403 | 404 |
405 |
406 |

Configuration

407 |
408 |
409 |
410 | 411 | 412 |
413 |
414 |

Import/Export Configuration

415 |
416 |
417 | Export:

418 | Import: 419 |
420 | 431 |
432 |
433 | 434 | 486 | 487 | 490 | 491 | 501 | 502 | 503 | 504 | 522 | 523 | 526 | 527 | 530 | 531 | 552 | 553 | 554 | 644 | 645 | 646 | 647 | 648 | -------------------------------------------------------------------------------- /cs5463.js: -------------------------------------------------------------------------------- 1 | var rollupTimeHr = 16; // hour at which rollups are sent 16 == 4pm UTC which is 9 am PST 2 | var _circuit = 0, Mode, Config; 3 | var costPerKWH = 0.0, deviceName="", region="en-US"; 4 | var configuration={}; 5 | var rollupEvent = null, runInterval = null; 6 | var bootTime = new Date(); 7 | 8 | var db = require('./database'); 9 | var netUtils = require('./utils.js'); 10 | var fs = require("fs"); 11 | var mqtt = null, mqttClient = null; 12 | 13 | 14 | Number.prototype.round = function (decimals) { 15 | return Number(Math.round(this + 'e' + decimals) + 'e-' + decimals).toFixed(decimals); 16 | }; 17 | 18 | 19 | // load currently installed software version and check for updates every hour 20 | var exec = require('child_process').exec, softwareVersion = null; 21 | (function checkForUpdates() { 22 | try { 23 | exec("git log -1 --format='%H %ad'", function (error, stdout, stderr) { 24 | if (error) 25 | console.error('unable to fetch installed software version: ' + error); 26 | else { 27 | try { 28 | var pos = stdout.trim().indexOf(" "); 29 | var currentSha = stdout.trim().substring(0, pos); 30 | var currentDate = (new Date(stdout.trim().substring(pos))).toISOString(); 31 | 32 | console.log('currentSha: ' + currentSha); 33 | console.log('currentDate: ' + currentDate); 34 | 35 | var obj = { Installed: { Sha: currentSha, Timestamp: currentDate } }; 36 | 37 | // get latest commit from github 38 | exec("curl https://api.github.com/repos/crjens/pipowermeter/git/refs/heads/master", function (error, stdout, stderr) { 39 | if (error) 40 | console.error('unable to fetch latest commit from github: ' + error); 41 | else { 42 | try { 43 | var json = JSON.parse(stdout.trim()); 44 | var latestSha = json.object.sha; 45 | console.log('latest software version: ' + latestSha); 46 | 47 | if (currentSha == latestSha) { 48 | console.log('software is up to date - will periodically check for updates'); 49 | obj.Latest = { Sha: currentSha, Timestamp: currentDate }; 50 | obj.UpdateRequired = false; 51 | softwareVersion = obj; 52 | } else { 53 | 54 | // load actual commit to get date 55 | exec("curl " + json.object.url, function (error, stdout, stderr) { 56 | if (error) 57 | console.error('unable to fetch commit ' + latestSha + ' from github: ' + error); 58 | else { 59 | try { 60 | var json = JSON.parse(stdout.trim()); 61 | 62 | console.log('latest software date: ' + json.author.date); 63 | 64 | obj.Latest = { Sha: json.sha, Timestamp: json.author.date }; 65 | obj.UpdateRequired = true; 66 | } catch (err) 67 | { 68 | console.error("Error checking for updates during latest commit proceesing: " + err); 69 | } 70 | } 71 | softwareVersion = obj; 72 | }); 73 | } 74 | } catch (err) { 75 | console.error("Error checking for updates during latest commit processing: " + err); 76 | } 77 | 78 | } 79 | }); 80 | } catch (err) { 81 | console.error("Error checking for updates during git log processing: " + err); 82 | } 83 | } 84 | }); 85 | } catch (err) { 86 | console.error("Error checking for updates: " + err); 87 | } 88 | 89 | setTimeout(checkForUpdates, 1000 * 60 * 60); 90 | })(); 91 | 92 | 93 | var FindProbeFactor = function (probeId) { 94 | if (configuration.Probes != null) { 95 | for (var i = 0; i < configuration.Probes.length; i++) { 96 | if (configuration.Probes[i].Name == probeId) 97 | return configuration.Probes[i].Factor; 98 | } 99 | } 100 | 101 | return null; 102 | } 103 | 104 | var loadConfiguration = function (callback) { 105 | 106 | console.log('loading configuration...'); 107 | // load circuits and filter out disabled ones 108 | db.getCircuits(function (err, data) { 109 | if (err) { 110 | console.log(err); 111 | } else { 112 | Mode = data.Mode; 113 | Config = data.Config; 114 | vFactor = data.VoltageScale; 115 | HardwareVersion = data.HardwareVersion; 116 | configuration.Probes = data.Probes; 117 | for (var i = 0; i < data.Circuits.length; i++) { 118 | data.Circuits[i].InstantEnabled = data.Circuits[i].Enabled; 119 | 120 | // set probe factors 121 | for (var j = 0; j < data.Circuits[i].Probes.length; j++) { 122 | data.Circuits[i].Probes[j].iFactor = FindProbeFactor(data.Circuits[i].Probes[j].Type); 123 | data.Circuits[i].Probes[j].vFactor = vFactor; 124 | } 125 | } 126 | configuration.Circuits = data.Circuits; 127 | deviceName = data.DeviceName; 128 | 129 | //console.log("configuration: " + JSON.stringify(configuration)); 130 | //console.log("configuration.Circuits: " + JSON.stringify(configuration.Circuits)); 131 | 132 | var port = data.Port; 133 | netUtils.InitializeTwilio(data.Text, data.Twilio, data.TwilioSID, data.TwilioAuthToken, deviceName, port); 134 | 135 | console.log('mqtt: ' + data.MqttServer); 136 | if (data.MqttServer != null) { 137 | try{ 138 | mqtt = require('mqtt'); 139 | mqttClient = mqtt.connect(data.MqttServer); 140 | } 141 | catch (err) { 142 | mqttClient = null; 143 | console.error("Error initializing MQTT server: " + data.MqttServer + ". Error: " + err); 144 | } 145 | } else { 146 | mqttClient = null; 147 | } 148 | } 149 | 150 | if (callback != null) 151 | callback(err); 152 | 153 | }); 154 | } 155 | 156 | var scheduleNextRollupMessage = function () { 157 | var now = new Date(); 158 | var ms = new Date(now.getFullYear(), now.getMonth(), now.getDate(), rollupTimeHr, 0, 0, 0) - now; 159 | //var ms = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 0, 0, 0) - now; 160 | 161 | if (ms < 0) 162 | ms += 86400000; // it's after time wait till tomorrow. 163 | //ms += 3600000; // it's after time wait till next hour 164 | 165 | console.log("next rollup in: " + ms); 166 | 167 | if (rollupEvent != null) 168 | clearTimeout(rollupEvent); 169 | 170 | rollupEvent = setTimeout(function() { 171 | rollupEvent=null; 172 | sendRollupText(function() { 173 | scheduleNextRollupMessage(); 174 | }); 175 | 176 | }, ms); 177 | } 178 | 179 | var sendRollupText = function (callback) { 180 | 181 | var d = new Date(); 182 | var curr_date = d.getDate(); 183 | var curr_month = d.getMonth() + 1; //Months are zero based 184 | var curr_year = d.getFullYear(); 185 | 186 | console.log("sending rollup mail"); 187 | 188 | db.rollup(function (err, data) { 189 | var message = "Rollup for " + curr_month + "/" + curr_date + "/" + curr_year; 190 | if (err) { 191 | console.log('error sending rollup: ' + err); 192 | } else { 193 | 194 | console.log(JSON.stringify(data)); 195 | 196 | message += "\nToday: " + (data.LastDay * 24 / 1000).round(1) + " KWh\n30 day avg: " + (data.LastMonth * 24 / 1000).round(1) + " KWh"; 197 | 198 | for (var i = 0; i < data.Circuits.length; i++) 199 | message += ("\n" + data.Circuits[i].CircuitId + ": " + (data.Circuits[i].Watts * 24 / 1000).round(1) + " KWh"); 200 | console.log('rollup: ' + message); 201 | 202 | netUtils.sendText(message); 203 | } 204 | 205 | if (callback) 206 | callback(); 207 | }); 208 | } 209 | 210 | 211 | function getFilesizeInBytes(filename) { 212 | var stats = fs.statSync(filename); 213 | var fileSizeInBytes = stats["size"]; 214 | return fileSizeInBytes; 215 | } 216 | 217 | // schedule rollup message 218 | scheduleNextRollupMessage(); 219 | 220 | 221 | var NextCircuit = function () { 222 | if (configuration != null && configuration.Circuits != null && configuration.Circuits.length > 0) { 223 | 224 | while (_circuit < configuration.Circuits.length && !configuration.Circuits[_circuit].InstantEnabled) 225 | _circuit++; 226 | 227 | if (_circuit >= configuration.Circuits.length) { 228 | _circuit = 0; 229 | 230 | while (_circuit < configuration.Circuits.length && !configuration.Circuits[_circuit].InstantEnabled) 231 | _circuit++; 232 | 233 | if (_circuit >= configuration.Circuits.length) { 234 | console.log("no circuits are enabled"); 235 | return null; 236 | } 237 | } 238 | 239 | return configuration.Circuits[_circuit++]; 240 | } 241 | return null; 242 | } 243 | 244 | var ReadNext = function () { 245 | if (_running) { 246 | var circuit = NextCircuit(); 247 | if (circuit != null) { 248 | reader.send({ Action: "Read", CircuitId: circuit.id, Probes: circuit.Probes }); 249 | } else { 250 | // schedule another read later 251 | setTimeout(ReadNext, 1000); 252 | } 253 | } 254 | } 255 | 256 | var FindCircuit = function (circuitId) { 257 | if (configuration != null && configuration.Circuits != null) { 258 | for (var i = 0; i < configuration.Circuits.length; i++) { 259 | if (configuration.Circuits[i].id == circuitId) { 260 | return configuration.Circuits[i]; 261 | } 262 | } 263 | } 264 | 265 | return null; 266 | } 267 | 268 | var FindProbeOffset = function (circuit, probeId) { 269 | for (var p = 0; p < circuit.Probes.length; p++) { 270 | if (circuit.Probes[p].id == probeId) { 271 | return p; 272 | } 273 | } 274 | 275 | return null; 276 | } 277 | 278 | var GetProbeAlertTime = function (probe) { 279 | if (probe == null || probe.Alert == null || probe.Alert.indexOf(",") == -1) 280 | return -1; 281 | 282 | var index = probe.Alert.indexOf(","); 283 | 284 | // min alert time is 30 minutes 285 | return Math.max(30, Number(probe.Alert.substring(0, index))); 286 | } 287 | 288 | var GetProbeAlertThreshold = function (probe) { 289 | if (probe == null || probe.Alert == null || probe.Alert.indexOf(",") == -1) 290 | return 0; 291 | 292 | var index = probe.Alert.indexOf(","); 293 | 294 | return Number(probe.Alert.substring(index+1)); 295 | } 296 | 297 | // create reader process 298 | var reader = require('child_process').fork(__dirname + '/reader.js'); 299 | console.log('spawned reader with pid: ' + reader.pid); 300 | var frequency = "unknown"; 301 | 302 | // Process read results from child process 303 | reader.on('message', function (data) { 304 | 305 | // copy out frequency 306 | frequency = data.Frequency; 307 | 308 | // find circuit 309 | var circuit = FindCircuit(data.CircuitId); 310 | 311 | if (circuit != null) { 312 | circuit.Samples = []; 313 | var pTotal=0, qTotal=0, overloadMsg = null; 314 | for (var i = 0; i < data.Probes.length; i++) { 315 | 316 | // update each probe 317 | var probe = data.Probes[i]; 318 | 319 | if (probe.Result != null) { 320 | var offset = FindProbeOffset(circuit, probe.id); 321 | 322 | if (offset != null) { 323 | circuit.Samples[offset] = probe.Result; 324 | pTotal += probe.Result.pAve; 325 | qTotal += probe.Result.qAve; 326 | } 327 | 328 | // check for overload 329 | if (probe.Breaker > 0 && probe.Result.iRms > probe.Breaker && (circuit.OverloadWarningSent == null || ((new Date()) - circuit.OverloadWarningSent) > 1000 * 60 * 60)) { 330 | if (overloadMsg == null) overloadMsg = ""; 331 | overloadMsg += " [Probe: " + i + ": iRms = " + probe.Result.iRms.round(1) + " amps / breaker = " + probe.Breaker + " amps]"; 332 | } 333 | 334 | // check for alert 335 | var alertTime = GetProbeAlertTime(probe); 336 | if (alertTime >= 0) { 337 | if (circuit.AlertLevelExceeded == null) { 338 | circuit.AlertLevelExceeded = new Date(); 339 | circuit.AlertTotalWatts = 0; 340 | circuit.AlertTotalSamples = 0; 341 | } 342 | 343 | if (probe.Result.pAve < GetProbeAlertThreshold(probe)) { 344 | circuit.AlertLevelExceeded = new Date(); 345 | circuit.AlertTotalWatts = 0; 346 | circuit.AlertTotalSamples = 0; 347 | } else { 348 | 349 | circuit.AlertTotalWatts += probe.Result.pAve; 350 | circuit.AlertTotalSamples++; 351 | 352 | if ((circuit.AlertWarningSent == null || ((new Date()) - circuit.AlertWarningSent) > 1000 * 60 * alertTime)) {// && ((new Date()) - circuit.AlertLevelExceeded) > 1000 * 60 * alertTime) { 353 | var elapsed = ((new Date()) - circuit.AlertLevelExceeded) / (1000 * 60); 354 | var avgWatts = circuit.AlertTotalWatts / circuit.AlertTotalSamples; 355 | //var msg = "Alert: " + circuit.Name + " has exceeded the threshold of " + GetProbeAlertThreshold(probe) + " watts for " + elapsed.round(0) + " minutes"; 356 | var msg = "Alert: Threshold exceeded on " + circuit.Name + " averaged " + avgWatts.round(1) + " watts for " + elapsed.round(0) + " minutes"; 357 | console.log(msg); 358 | netUtils.sendText(msg); 359 | circuit.AlertWarningSent = new Date(); 360 | } 361 | } 362 | } 363 | } 364 | } 365 | 366 | circuit.pTotal = Number(pTotal); 367 | circuit.qTotal = Number(qTotal); 368 | 369 | // send text if overloaded 370 | if (overloadMsg != null) { 371 | circuit.OverloadWarningSent = new Date(); 372 | var msg = "Overload on " + circuit.Name + overloadMsg; 373 | console.log(msg); 374 | netUtils.sendText(msg); 375 | } 376 | 377 | if (circuit.Samples.length > 0) { 378 | //console.log(JSON.stringify(circuit.Samples[0])); 379 | db.insert(circuit.id, circuit.Samples[0].iRms, circuit.Samples[0].vRms, pTotal, qTotal, circuit.Samples[0].pf, new Date(circuit.Samples[0].ts), circuit.Samples[0].CalculatedFrequency); 380 | console.log(circuit.Name + ' : V= ' + circuit.Samples[0].vRms.round(1) + ' I= ' + circuit.Samples[0].iRms.round(1) + ' P= ' + pTotal.round(1) + ' Q= ' + qTotal.round(1) + ' PF= ' + circuit.Samples[0].pf.round(4) + ' F= ' + circuit.Samples[0].CalculatedFrequency.round(3) + ' F2= ' + circuit.Samples[0].freq.round(3) + ' (' + circuit.Samples[0].tsInst.length + ' samples in ' + circuit.Samples[0].tsInst[circuit.Samples[0].tsInst.length-1] + 'ms)'); 381 | 382 | if (mqttClient != null) { 383 | try { 384 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/Name', circuit.Name); 385 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/Voltage', circuit.Samples[0].vRms.round(1)); 386 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/Current', circuit.Samples[0].iRms.round(2)); 387 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/Watts', pTotal.round(1)); 388 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/Vars', qTotal.round(1)); 389 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/PowerFactor', circuit.Samples[0].pf.round(4)); 390 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/Timestamp', circuit.Samples[0].ts); 391 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/Frequency', circuit.Samples[0].CalculatedFrequency.round(3)); 392 | if (circuit.LastDayKwh != null) 393 | mqttClient.publish('PiPowerMeter/' + circuit.id + '/LastDayKwh', circuit.LastDayKwh.round(1)); 394 | } 395 | catch (err) { 396 | console.error("Error writing to MQTT server: " + data.MqttServer + ". Error: " + err); 397 | } 398 | } 399 | } else { 400 | console.log(circuit.Name + ' ********* Read operation failed *******'); 401 | } 402 | } 403 | 404 | // start next read 405 | ReadNext(); 406 | 407 | // keep 24hr avg up to date 408 | updateState(); 409 | }); 410 | 411 | 412 | 413 | function numberWithCommas(x) { 414 | var parts = x.toString().split("."); 415 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); 416 | return parts.join("."); 417 | } 418 | 419 | 420 | var realtimeTimer = null; 421 | 422 | function ResetRealtimeTimer() { 423 | 424 | if (realtimeTimer != null) { 425 | clearTimeout(realtimeTimer); 426 | } 427 | 428 | realtimeTimer = setTimeout(function () { 429 | for (var i = 0; i < configuration.Circuits.length; i++) { 430 | configuration.Circuits[i].InstantEnabled = configuration.Circuits[i].Enabled; 431 | } 432 | realtimeTimer = null; 433 | }, 30000); 434 | } 435 | 436 | var _running = false; 437 | var Start = function () { 438 | _running = true; 439 | 440 | // Kick off the main read loop 441 | loadConfiguration(function (err) { 442 | if (err) { 443 | console.log('unable to load configuration: ' + err); 444 | } else { 445 | reader.send({ Action: "Start", HardwareVersion: HardwareVersion, Mode: Mode, Config: Config }); 446 | ReadNext(); 447 | } 448 | }); 449 | 450 | 451 | } 452 | 453 | var Stop = function () { 454 | _running = false; 455 | console.log('sending Stop to reader...'); 456 | reader.send({ Action: "Stop"}); 457 | console.log('running: sudo kill -9 ' + reader.pid); 458 | 459 | var exec = require('child_process').exec; 460 | exec('sudo kill -9 ' + reader.pid, function (error, stdout, stderr) { 461 | if (err) 462 | console.log('failed to kill reader'); 463 | else 464 | console.log('killed reader'); 465 | }); 466 | } 467 | 468 | 469 | var timeSince = function (date) { 470 | if (typeof date !== 'object') { 471 | date = new Date(date); 472 | } 473 | 474 | var seconds = Math.floor((new Date() - date) / 1000); 475 | var intervalType; 476 | 477 | var interval = Math.floor(seconds / 31536000); 478 | if (interval >= 1) { 479 | intervalType = 'year'; 480 | } else { 481 | interval = Math.floor(seconds / 2592000); 482 | if (interval >= 1) { 483 | intervalType = 'month'; 484 | } else { 485 | interval = Math.floor(seconds / 86400); 486 | if (interval >= 1) { 487 | intervalType = 'day'; 488 | } else { 489 | interval = Math.floor(seconds / 3600); 490 | if (interval >= 1) { 491 | intervalType = "hour"; 492 | } else { 493 | interval = Math.floor(seconds / 60); 494 | if (interval >= 1) { 495 | intervalType = "minute"; 496 | } else { 497 | interval = seconds; 498 | intervalType = "second"; 499 | } 500 | } 501 | } 502 | } 503 | } 504 | 505 | if (interval > 1 || interval === 0) { 506 | intervalType += 's'; 507 | } 508 | 509 | return interval + ' ' + intervalType; 510 | }; 511 | 512 | var stateLastUpdated = 0; 513 | var updateState = function () { 514 | var now = new Date(); // now 515 | var msPerHour = 1000 * 60 * 60; 516 | if (stateLastUpdated < now.getTime() - msPerHour) { 517 | stateLastUpdated = now.getTime(); 518 | 519 | var start = new Date(now - (msPerHour * 24)); // 24hr ago 520 | var telemetry = []; 521 | var CalcLastDayKwh = function(id) 522 | { 523 | db.minmaxavg(id, start, now, telemetry, function (err, result) { 524 | if (result) { 525 | var circuit = FindCircuit(id); 526 | if (circuit != null) { 527 | var kwh = (result[0].avg || 0) / 1000.0 * 24.0; 528 | console.log("setting lastkwh for ckt: " + id + " to " + kwh); 529 | circuit.LastDayKwh = kwh; 530 | } else { 531 | stateLastUpdated = 0; 532 | } 533 | } else { 534 | console.log("failed to set kwh for ckt: " + id); 535 | } 536 | }); 537 | } 538 | 539 | 540 | for (var i = 0; i < configuration.Circuits.length; i++) { 541 | CalcLastDayKwh(configuration.Circuits[i].id); 542 | } 543 | } 544 | }; 545 | 546 | 547 | 548 | var exports = { 549 | // waveform 550 | ReadCircuit: function (circuitId) { 551 | for (var i = 0; i < configuration.Circuits.length; i++) { 552 | if (configuration.Circuits[i].id == circuitId) { 553 | configuration.Circuits[i].DeviceName = deviceName || ""; 554 | return configuration.Circuits[i]; 555 | } 556 | } 557 | return null; 558 | }, 559 | // return inst power on circuit and Kwh used in last day 560 | ReadState: function (circuitId) { 561 | 562 | var circuit = FindCircuit(circuitId); 563 | if (circuit != null) { 564 | 565 | var res = { current: circuit.pTotal, last24Kwh: circuit.LastDayKwh }; 566 | console.log("returning : " + JSON.stringify(res)); 567 | return res; 568 | } 569 | 570 | return null; 571 | }, 572 | UpdateCircuitEnabled: function (circuitId, enabled) { 573 | if (circuitId == 'all' && enabled == 1) { 574 | console.log('resetting enabled flag'); 575 | for (var i = 0; i < configuration.Circuits.length; i++) { 576 | configuration.Circuits[i].InstantEnabled = configuration.Circuits[i].Enabled; 577 | } 578 | } else { 579 | for (var i = 0; i < configuration.Circuits.length; i++) { 580 | if (circuitId == 'all' || configuration.Circuits[i].id == circuitId) { 581 | var initial = configuration.Circuits[i].InstantEnabled; 582 | configuration.Circuits[i].InstantEnabled = parseInt(enabled, 10); 583 | 584 | console.log("ctk: " + i + " initial: " + initial + " final: " + configuration.Circuits[i].InstantEnabled); 585 | } 586 | } 587 | ResetRealtimeTimer(); 588 | } 589 | 590 | }, 591 | Readings: function () { 592 | 593 | ResetRealtimeTimer(); 594 | 595 | var result = []; 596 | for (var i = 0; i < configuration.Circuits.length; i++) { 597 | var ckt = configuration.Circuits[i]; 598 | 599 | if (ckt.Samples != null && ckt.Samples.length > 0) { 600 | 601 | var w = [], a = [], v = [], q = [], pf = [], l = [], ts = [], probe = [], breaker=[], f=[]; 602 | for (var p = 0; p < ckt.Probes.length; p++) { 603 | w.push(Number(ckt.Samples[p].pAve)); 604 | a.push(Number(ckt.Samples[p].iRms)); 605 | probe.push(ckt.Probes[p].id); 606 | breaker.push(Number(ckt.Probes[p].Breaker)); 607 | v.push(Number(ckt.Samples[p].vRms)); 608 | q.push(Number(ckt.Samples[p].qAve)); 609 | pf.push(Number(ckt.Samples[p].pf)); 610 | l.push(Number(ckt.Samples[p].iRms / ckt.Probes[p].Breaker)); 611 | ts.push(ckt.Samples[p].ts); 612 | f.push(Number(ckt.Samples[p].CalculatedFrequency)); 613 | } 614 | 615 | if (ckt.Probes.length > 1) { 616 | w.push(Number(ckt.pTotal)); 617 | q.push(Number(ckt.qTotal)); 618 | probe.push("All"); 619 | } 620 | 621 | result.push({ 622 | id: ckt.id, 623 | name: ckt.Name, 624 | breaker: breaker, 625 | enabled: ckt.InstantEnabled, 626 | watts: w, 627 | amps: a, 628 | probe: probe, 629 | volts: v, 630 | q: q, 631 | pf: pf, 632 | timestamp: ts, 633 | f: f, 634 | load: l, 635 | region: region 636 | }); 637 | } 638 | } 639 | return { Readings: result, DeviceName: deviceName } ; 640 | }, 641 | ReadPower: function (circuitId, start, end, groupBy, timeOffset, telemetry, callback) { 642 | db.read(circuitId, start, end, groupBy, timeOffset, telemetry, function (err, result) { 643 | if (err) { 644 | callback(err); 645 | } else { 646 | if (result != null) { 647 | result.Cost = costPerKWH; 648 | result.Region = region; 649 | result.DeviceName = deviceName; 650 | } 651 | 652 | // get min, max and average over interval 653 | db.minmaxavg(circuitId, start, end, telemetry, function (err2, result2) { 654 | if (result2) { 655 | result.min = result2[0].min || 0; 656 | result.max = result2[0].max || 0; 657 | result.avg = result2[0].avg || 0; 658 | } 659 | 660 | callback(err2, result); 661 | }); 662 | } 663 | }); 664 | }, 665 | GetCircuits: function (callback, strip) { 666 | db.getCircuits(function (err, _config) { 667 | if (_config != null) { 668 | costPerKWH = _config.Price; 669 | if (costPerKWH <= 0) 670 | costPerKWH = 0.1; // default to 10 / KWh 671 | 672 | region = _config.Region; 673 | if (region == null || region == "") 674 | region = "en-US"; // default 675 | 676 | if (_config.DeviceName != null) 677 | deviceName = _config.DeviceName; 678 | 679 | if (softwareVersion != null) 680 | _config.SoftwareVersion = softwareVersion; 681 | 682 | _config.Uptime = timeSince(bootTime); 683 | _config.DatabaseSize = numberWithCommas(getFilesizeInBytes('powermeter.db')); 684 | _config.Frequency = frequency; 685 | callback(err, _config); 686 | 687 | } else { 688 | callback(err); 689 | } 690 | }, strip); 691 | }, 692 | Cumulative: function (start, end, orderBy, telemetry, callback) { 693 | db.cumulative(start, end, orderBy, telemetry, function (err, result) { 694 | 695 | var results = {}; 696 | results.result = result; 697 | results.DeviceName = deviceName; 698 | 699 | db.minmaxavg("(select id from Circuits where IsMain=1)", start, end, telemetry, function (err, _val) { 700 | if (_val != null && _val.length == 1) { 701 | results.MaxWatts = _val[0].max; 702 | results.AvgWatts = _val[0].avg; 703 | results.MinWatts = _val[0].min; 704 | } 705 | else { 706 | results.MaxWatts = 0.0; 707 | results.AvgWatts = 0.0; 708 | results.MinWatts = 0.0; 709 | } 710 | 711 | 712 | if (costPerKWH == 0) { 713 | db.getCostPerKWh(function (err, cost, _region) { 714 | if (cost != null && cost.length == 1) 715 | costPerKWH = cost[0].Value; 716 | 717 | if (_region != null && _region.length == 1) 718 | region = _region[0].Value; 719 | 720 | results.CostPerKWH = costPerKWH; 721 | results.Region = region; 722 | callback(err, results); 723 | }); 724 | } else { 725 | results.CostPerKWH = costPerKWH; 726 | results.Region = region; 727 | callback(err, results); 728 | } 729 | }); 730 | }); 731 | }, 732 | Reset: function () { 733 | Reset(); 734 | return 0; 735 | }, 736 | GetConfiguration: function (callback) { 737 | db.getConfiguration(function (err, config) { 738 | 739 | if (config != null) { 740 | 741 | if (config.DeviceName != null) 742 | deviceName = config.DeviceName; 743 | 744 | for (index = 0; index < config.length; ++index) { 745 | config[index].HardwareVersion = HardwareVersion; 746 | config[index].Probes = probes; 747 | } 748 | 749 | callback(err, config); 750 | } else { 751 | callback(err); 752 | } 753 | 754 | 755 | }); 756 | }, 757 | ReplaceConfiguration: function (callback, config) { 758 | db.updateCircuits(config, function (err) { 759 | loadConfiguration(); 760 | callback(err); 761 | }); 762 | }, 763 | DeleteCircuit: function (callback, circuitId) { 764 | db.deleteCircuit(function (err) { 765 | loadConfiguration(); 766 | callback(err); 767 | }, circuitId); 768 | }, 769 | DeleteProbe: function (callback, probeId) { 770 | db.deleteProbe(function (err) { 771 | loadConfiguration(); 772 | callback(err); 773 | }, probeId); 774 | }, 775 | ReplaceProbeDefConfiguration: function (callback, config) { 776 | 777 | var array = []; 778 | for (var name in config) { 779 | array.push({ name: name, value: config[name] }); 780 | } 781 | 782 | 783 | var setVal = function (index) { 784 | if (index < array.length) { 785 | var name = array[index].name; 786 | var value = array[index].value; 787 | 788 | if (Object.prototype.toString.call(value) === '[object Array]') { 789 | value = JSON.stringify(value); 790 | } 791 | 792 | db.setConfig(name, value, function (err) { 793 | if (!err) 794 | setVal(index + 1); // next 795 | else 796 | callback(err); // error 797 | }); 798 | } else { 799 | loadConfiguration(function (err) { 800 | callback(err); // finished 801 | }); 802 | } 803 | } 804 | setVal(0); 805 | }, 806 | Stop: function () { 807 | Stop(); 808 | }, 809 | Start: function () { 810 | Start(); 811 | } 812 | }; 813 | 814 | module.exports = exports; 815 | --------------------------------------------------------------------------------