mirror of
https://github.com/zyphlar/LanternPowerMonitor.git
synced 2024-03-08 14:07:47 +00:00
Auto-detect frequency on first hub startup. If frequency is 50Hz, assume 230V. (This should work for 95% of cases)
This commit is contained in:
parent
94ebf5fa93
commit
a892c7f0e8
|
@ -3,7 +3,7 @@
|
|||
<groupId>com.lanternsoftware.currentmonitor</groupId>
|
||||
<artifactId>lantern-currentmonitor</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.7</version>
|
||||
<name>lantern-currentmonitor</name>
|
||||
|
||||
<properties>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package com.lanternsoftware.currentmonitor;
|
||||
|
||||
public class CalibrationResult {
|
||||
private final double voltageCalibrationFactor;
|
||||
private final int frequency;
|
||||
|
||||
public CalibrationResult(double _voltageCalibrationFactor, int _frequency) {
|
||||
voltageCalibrationFactor = _voltageCalibrationFactor;
|
||||
frequency = _frequency;
|
||||
}
|
||||
|
||||
public double getVoltageCalibrationFactor() {
|
||||
return voltageCalibrationFactor;
|
||||
}
|
||||
|
||||
public int getFrequency() {
|
||||
return frequency;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.lanternsoftware.currentmonitor;
|
||||
|
||||
public class CalibrationSample {
|
||||
public long time;
|
||||
public double voltage;
|
||||
}
|
|
@ -56,44 +56,71 @@ public class CurrentMonitor {
|
|||
chips.clear();
|
||||
pins.clear();
|
||||
gpio.shutdown();
|
||||
LOG.info("Current Monitor Stopped");
|
||||
LOG.info("Power Monitor Service Stopped");
|
||||
}
|
||||
|
||||
public void setDebug(boolean _debug) {
|
||||
debug = _debug;
|
||||
}
|
||||
|
||||
public double calibrateVoltage(double _curCalibration, float _voltage) {
|
||||
public CalibrationResult calibrateVoltage(double _curCalibration) {
|
||||
GpioPinAnalogInput voltagePin = getPin(0, 0);
|
||||
if (voltagePin == null)
|
||||
return 0.0;
|
||||
List<Double> samples = new ArrayList<>(120000);
|
||||
return null;
|
||||
int maxSamples = 120000;
|
||||
CalibrationSample[] samples = new CalibrationSample[maxSamples];
|
||||
int offset = 0;
|
||||
for (;offset < maxSamples; offset++) {
|
||||
samples[offset] = new CalibrationSample();
|
||||
}
|
||||
offset = 0;
|
||||
long intervalEnd = System.nanoTime() + 2000000000L; //Scan voltage for 2 seconds
|
||||
while (System.nanoTime() < intervalEnd) {
|
||||
samples.add(voltagePin.getValue());
|
||||
while (offset < maxSamples) {
|
||||
samples[offset].time = System.nanoTime();
|
||||
samples[offset].voltage = voltagePin.getValue();
|
||||
offset++;
|
||||
if (samples[offset-1].time > intervalEnd)
|
||||
break;
|
||||
}
|
||||
|
||||
double vOffset = 0.0;
|
||||
for (Double sample : samples) {
|
||||
vOffset += sample;
|
||||
for (CalibrationSample sample : samples) {
|
||||
vOffset += sample.voltage;
|
||||
}
|
||||
vOffset /= samples.size();
|
||||
vOffset /= offset;
|
||||
int cycles = 0;
|
||||
boolean under = true;
|
||||
if (samples[0].voltage > (vOffset * 1.3)) {
|
||||
cycles = 1;
|
||||
under = false;
|
||||
}
|
||||
double voltage;
|
||||
double vRms = 0.0;
|
||||
for (Double sample : samples) {
|
||||
sample -= vOffset;
|
||||
vRms += sample * sample;
|
||||
for (int sample = 0; sample < offset; sample++) {
|
||||
voltage = samples[sample].voltage - vOffset;
|
||||
vRms += voltage * voltage;
|
||||
if (under && (samples[sample].voltage > (vOffset * 1.3))) {
|
||||
cycles += 1;
|
||||
under = false;
|
||||
}
|
||||
else if (samples[sample].voltage < vOffset * 0.7) {
|
||||
under = true;
|
||||
}
|
||||
}
|
||||
vRms /= samples.size();
|
||||
vRms /= offset;
|
||||
|
||||
double oldVrms = _curCalibration * Math.sqrt(vRms);
|
||||
if (oldVrms < 20) {
|
||||
LOG.error("Could not get a valid voltage read, please check that your AC/AC transformer is connected");
|
||||
return 0.0;
|
||||
return null;
|
||||
}
|
||||
double newCal = (_voltage/oldVrms) * _curCalibration;
|
||||
int frequency = Math.round(cycles/((samples[offset-1].time-samples[0].time)/100000000f))*10;
|
||||
LOG.info("Detected Frequency: " + frequency);
|
||||
|
||||
double newCal = ((frequency > 55 ? 120:230)/oldVrms) * _curCalibration;
|
||||
double newVrms = newCal * Math.sqrt(vRms);
|
||||
LOG.info("Old Voltage Calibration: {} Old vRMS: {}", _curCalibration, oldVrms);
|
||||
LOG.info("New Voltage Calibration: {} New vRMS: {}", newCal, newVrms);
|
||||
return newCal;
|
||||
return new CalibrationResult(newCal, frequency);
|
||||
}
|
||||
|
||||
public void monitorPower(BreakerHub _hub, List<Breaker> _breakers, int _intervalMs, PowerListener _listener) {
|
||||
|
@ -101,6 +128,9 @@ public class CurrentMonitor {
|
|||
stopMonitoring();
|
||||
listener = _listener;
|
||||
List<Breaker> validBreakers = CollectionUtils.filter(_breakers, _b -> _b.getPort() > 0 && _b.getPort() < 16);
|
||||
if (CollectionUtils.isEmpty(validBreakers))
|
||||
return;
|
||||
LOG.info("Monitoring {} breakers for hub {}", CollectionUtils.size(validBreakers), _hub.getHub());
|
||||
sampler = new Sampler(_hub, validBreakers, _intervalMs, 2);
|
||||
LOG.info("Starting to monitor ports {}", CollectionUtils.transformToCommaSeparated(validBreakers, _b -> String.valueOf(_b.getPort())));
|
||||
executor.submit(sampler);
|
||||
|
@ -188,7 +218,7 @@ public class CurrentMonitor {
|
|||
while (true) {
|
||||
synchronized (this) {
|
||||
if (!running) {
|
||||
LOG.error("Power Monitoring Stopped");
|
||||
LOG.info("Power Monitoring Stopped");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,11 +164,8 @@ public class MonitorApp {
|
|||
breakerConfig = newConfig;
|
||||
List<Breaker> breakers = breakerConfig.getBreakersForHub(config.getHub());
|
||||
BreakerHub hub = breakerConfig.getHub(config.getHub());
|
||||
if (hub != null) {
|
||||
LOG.info("Monitoring {} breakers for hub {}", CollectionUtils.size(breakers), hub.getHub());
|
||||
if (CollectionUtils.size(breakers) > 0)
|
||||
monitor.monitorPower(hub, breakers, 1000, logger);
|
||||
}
|
||||
if (hub != null)
|
||||
monitor.monitorPower(hub, breakers, 1000, logger);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -270,19 +267,18 @@ public class MonitorApp {
|
|||
LOG.info("Breaker Config loaded");
|
||||
BreakerHub hub = breakerConfig.getHub(config.getHub());
|
||||
if (hub != null) {
|
||||
if (config.isNeedsCalibration() && (config.getAutoCalibrationVoltage() != 0.0)) {
|
||||
double newCal = monitor.calibrateVoltage(hub.getVoltageCalibrationFactor(), config.getAutoCalibrationVoltage());
|
||||
if (newCal != 0.0) {
|
||||
hub.setVoltageCalibrationFactor(newCal);
|
||||
if (config.isNeedsCalibration()) {
|
||||
CalibrationResult cal = monitor.calibrateVoltage(hub.getVoltageCalibrationFactor());
|
||||
if (cal != null) {
|
||||
hub.setVoltageCalibrationFactor(cal.getVoltageCalibrationFactor());
|
||||
hub.setFrequency(cal.getFrequency());
|
||||
config.setNeedsCalibration(false);
|
||||
ResourceLoader.writeFile(WORKING_DIR + "config.json", DaoSerializer.toJson(config));
|
||||
post(DaoSerializer.toZipBson(breakerConfig), "config");
|
||||
}
|
||||
}
|
||||
List<Breaker> breakers = breakerConfig.getBreakersForHub(config.getHub());
|
||||
LOG.info("Monitoring {} breakers for hub {}", CollectionUtils.size(breakers), hub.getHub());
|
||||
if (CollectionUtils.size(breakers) > 0)
|
||||
monitor.monitorPower(hub, breakers, 1000, logger);
|
||||
monitor.monitorPower(hub, breakers, 1000, logger);
|
||||
}
|
||||
monitor.submit(new PowerPoster());
|
||||
}
|
||||
|
|
|
@ -187,7 +187,7 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao {
|
|||
for (int offset = 0; offset < bytesInDay; offset += 4) {
|
||||
nanBuffer.putFloat(offset, Float.NaN);
|
||||
}
|
||||
for (int key : breakerKeys.keySet()) {
|
||||
for (int key : breakerKeys.values()) {
|
||||
dayReadings.computeIfAbsent(key, _k->nanArray);
|
||||
}
|
||||
for (Entry<Integer, byte[]> be : dayReadings.entrySet()) {
|
||||
|
|
|
@ -185,6 +185,14 @@ public class BreakerConfig implements IIdentical<BreakerConfig> {
|
|||
return null;
|
||||
}
|
||||
|
||||
public Meter getMeterForHub(int _hub) {
|
||||
Meter m = null;
|
||||
Breaker b = CollectionUtils.filterOne(getAllBreakers(), _b->_b.getHub() == _hub);
|
||||
if (b != null)
|
||||
m = CollectionUtils.filterOne(meters, _m->_m.getIndex() == b.getMeter());
|
||||
return (m != null) ? m : new Meter(getAccountId(), 0, "Main");
|
||||
}
|
||||
|
||||
public BreakerGroup findParentGroup(BreakerGroup _group) {
|
||||
for (BreakerGroup group : CollectionUtils.makeNotNull(breakerGroups)) {
|
||||
BreakerGroup parent = group.findParentGroup(_group);
|
||||
|
|
|
@ -11,6 +11,15 @@ public class Meter implements IIdentical<Meter> {
|
|||
private int index;
|
||||
private String name;
|
||||
|
||||
public Meter() {
|
||||
}
|
||||
|
||||
public Meter(int _accountId, int _index, String _name) {
|
||||
accountId = _accountId;
|
||||
index = _index;
|
||||
name = _name;
|
||||
}
|
||||
|
||||
public int getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
|
|
@ -75,15 +75,16 @@ public class ExportServlet extends SecureConsoleServlet {
|
|||
redirect(_rep, _req.getContextPath() + "/export");
|
||||
return;
|
||||
}
|
||||
StringBuilder header = new StringBuilder("Timestamp");
|
||||
StringBuilder header = new StringBuilder("\"Timestamp\"");
|
||||
for (BreakerEnergyArchive ba : CollectionUtils.makeNotNull(fday.getBreakers())) {
|
||||
Breaker b = breakers.get(Breaker.intKey(ba.getPanel(), ba.getSpace()));
|
||||
header.append(",");
|
||||
header.append(",\"");
|
||||
if (b != null) {
|
||||
header.append(b.getKey());
|
||||
header.append("-");
|
||||
header.append(b.getName());
|
||||
}
|
||||
header.append("\"");
|
||||
}
|
||||
header.append("\n");
|
||||
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
|
|
|
@ -1,50 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<#if inprogress><meta http-equiv="refresh" content="1"></#if>
|
||||
<title>Lantern Console</title>
|
||||
<link href="${link_prefix}bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="${link_prefix}bootstrap/css/style.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid login-container">
|
||||
<div class="row">
|
||||
<div class="col-1 col-lg-3 col-4k-4"></div>
|
||||
<div class="col-10 col-lg-6 col-4k-4">
|
||||
<div class="row mt-3">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<#list months as month>
|
||||
<div class="row">
|
||||
<div class="col-1 col-lg-3 col-4k-4"></div>
|
||||
<div class="col-10 col-lg-6 col-4k-4">
|
||||
<div class="row mt-3">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<#list months as month>
|
||||
<#if month.progress == 0>
|
||||
<form method="POST">
|
||||
</#if>
|
||||
<tr>
|
||||
<td>${month.name!}</td>
|
||||
<#if month.progress == 0>
|
||||
<form method="POST">
|
||||
<td><input type="hidden" name="month" value="${month.date}"/><input type="submit" class="btn-primary border-none px-2 py-1" value="Export"/></td>
|
||||
<#elseif month.progress == 1>
|
||||
<td>Queued</td>
|
||||
<#elseif month.progress < 100>
|
||||
<td>Progress ${month.progress!}%</td>
|
||||
<#else>
|
||||
<td>
|
||||
<a href="export/${month.date}/${month.fileName!}.bson.zip" class="btn-primary border-none text-decoration-none p-2" download>BSON</a>
|
||||
<a href="export/${month.date}/${month.fileName!}.json.zip" class="btn-primary border-none text-decoration-none p-2" download>JSON</a>
|
||||
<a href="export/${month.date}/${month.fileName!}.csv.zip" class="btn-primary border-none text-decoration-none p-2" download>CSV</a>
|
||||
</td>
|
||||
</#if>
|
||||
<tr>
|
||||
<td>${month.name!}</td>
|
||||
<#if month.progress == 0>
|
||||
<td><input type="hidden" name="month" value="${month.date}"/><input type="submit" class="btn-primary border-none px-2 py-1" value="Export"/></td>
|
||||
<#elseif month.progress == 1>
|
||||
<td>Queued</td>
|
||||
<#elseif month.progress < 100>
|
||||
<td>Progress ${month.progress!}%</td>
|
||||
<#else>
|
||||
<td>
|
||||
<a href="export/${month.date}/${month.fileName!}.bson.zip" class="btn-primary border-none text-decoration-none p-2" download>BSON</a>
|
||||
<a href="export/${month.date}/${month.fileName!}.json.zip" class="btn-primary border-none text-decoration-none p-2" download>JSON</a>
|
||||
<a href="export/${month.date}/${month.fileName!}.csv.zip" class="btn-primary border-none text-decoration-none p-2" download>CSV</a>
|
||||
</td>
|
||||
</#if>
|
||||
</tr>
|
||||
<#if month.progress == 0>
|
||||
</form>
|
||||
</#if>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-1 col-lg-3 col-4k-4"></div>
|
||||
</tr>
|
||||
<#if month.progress == 0>
|
||||
</form>
|
||||
</#if>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<div class="col-1 col-lg-3 col-4k-4"></div>
|
||||
</div>
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<#if inprogress!><meta http-equiv="refresh" content="1"></#if>
|
||||
<title>Lantern Power Monitor</title>
|
||||
<link rel="icon" type="image/png" href="${link_prefix}img/favicon.png">
|
||||
<link href="${link_prefix}bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
@ -17,7 +15,7 @@
|
|||
<div class="container-fluid">
|
||||
<div class="row header-menu py-2">
|
||||
<div class="col-2"></div>
|
||||
<div class="col-auto"><img class="img-fluid" alt="Logo" src="${link_prefix}img/logo_40.png"/></div>
|
||||
<div class="col-auto"><img class="img-fluid header-logo" alt="Logo" src="${link_prefix}img/logo_40.png"/></div>
|
||||
<div class="col"></div>
|
||||
<div class="col-auto mx-2"><a href="${link_prefix}" class="text-decoration-none header-link fmnu1">DATA EXPORT</a></div>
|
||||
<div class="col-auto mx-2"><a href="${link_prefix}logout" class="text-decoration-none header-link fmnu1">LOGOUT</a></div>
|
||||
|
|
|
@ -227,3 +227,7 @@
|
|||
.error {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: calc(1.5rem + .8vw)
|
||||
}
|
|
@ -1 +1 @@
|
|||
.scrollarea{overflow-y:auto}.fw-semibold{font-weight:600}.lh-tight{line-height:1.25}.menu_link{text-decoration:none!important}.menu_subitem{color:#404040}.menu_item,.menu_subitem:focus,.menu_subitem:hover{background-color:#1a9acc;color:#f0f0f0}.lpm_menu{z-index:1;background:#fff;min-height:100%;max-height:100%}#toggle_label{margin-top:.5rem;display:block;width:2rem;position:absolute;z-index:10}.blue,.title{color:#1a9acc}.kill{color:red}.max-w-300{max-width:min(100%,300px)}.max-w-400{max-width:min(100%,400px)}.max-w-500{max-width:min(100%,500px)}.max-w-600{max-width:min(100%,600px)}.max-w-700{max-width:min(100%,700px)}.max-w-800{max-width:min(100%,800px)}.max-w-900{max-width:min(100%,900px)}.max-w-1000{max-width:min(100%,1000px)}.max-w-1200{max-width:min(100%,1200px)}.align-col-right{display:flex!important;justify-content:flex-end!important;padding-right:1vw!important}.align-col-left{display:flex!important;justify-content:flex-start!important;padding-left:1vw!important}.title{font-size:2em}.sub-title{font-size:1.1em}@media (max-width:991px){#menu_toggle:checked~#toggle_label{position:fixed!important}.lpm_menu{display:none!important}#menu_toggle:checked~.lpm_menu{display:block!important}#menu_toggle:checked~.lpm_body{display:block!important;width:50%}}@media (min-width:992px){.hsy{margin-top:2.3rem!important}}@media (min-width:3000px){#menu_toggle:checked~.lpm_menu{display:block!important}#menu_toggle:checked~.lpm_body{display:block!important;width:50%}}.fm1{font-size:calc(1rem + 1.25vw)}.fm2{font-size:calc(.7rem + 1vw)}.fm3{font-size:calc(.8rem + .5vw)}.fmnu1{font-size:calc(1rem + .5vw)}.fmnu2{font-size:calc(.8rem + .5vw)}.login-bkgnd,.login-container{z-index:2;position:absolute}.login-bkgnd{z-index:1;width:100%;height:100%;top:0;left:0;background-image:url(../../img/pcb.png);background-size:cover;background-position:center}.login-input{width:100%}::placeholder{color:#a0a0a0;opacity:1}.btn-primary{background-color:#1a9acc;border-color:#1a9acc}.btn-primary:focus,.btn-primary:hover{background-color:#20c0ff;border-color:#20c0ff}.header-menu{background-color:#1a9acc}.header-link{color:#f0f0f0}.header-link:hover{color:#d0d0d0}.gso{border-style:none;width:10rem;height:2.5rem;background-image:url(../../img/gso.png);background-position:center;background-size:cover}.gso,.gso:hover{background-color:#0000}.gso:focus{background-image:url(../../img/gso_pressed.png)}.border-none{border-style:none}.error{color:#8b0000}
|
||||
.scrollarea{overflow-y:auto}.fw-semibold{font-weight:600}.lh-tight{line-height:1.25}.menu_link{text-decoration:none!important}.menu_subitem{color:#404040}.menu_item,.menu_subitem:focus,.menu_subitem:hover{background-color:#1a9acc;color:#f0f0f0}.lpm_menu{z-index:1;background:#fff;min-height:100%;max-height:100%}#toggle_label{margin-top:.5rem;display:block;width:2rem;position:absolute;z-index:10}.blue,.title{color:#1a9acc}.kill{color:red}.max-w-300{max-width:min(100%,300px)}.max-w-400{max-width:min(100%,400px)}.max-w-500{max-width:min(100%,500px)}.max-w-600{max-width:min(100%,600px)}.max-w-700{max-width:min(100%,700px)}.max-w-800{max-width:min(100%,800px)}.max-w-900{max-width:min(100%,900px)}.max-w-1000{max-width:min(100%,1000px)}.max-w-1200{max-width:min(100%,1200px)}.align-col-right{display:flex!important;justify-content:flex-end!important;padding-right:1vw!important}.align-col-left{display:flex!important;justify-content:flex-start!important;padding-left:1vw!important}.title{font-size:2em}.sub-title{font-size:1.1em}@media (max-width:991px){#menu_toggle:checked~#toggle_label{position:fixed!important}.lpm_menu{display:none!important}#menu_toggle:checked~.lpm_menu{display:block!important}#menu_toggle:checked~.lpm_body{display:block!important;width:50%}}@media (min-width:992px){.hsy{margin-top:2.3rem!important}}@media (min-width:3000px){#menu_toggle:checked~.lpm_menu{display:block!important}#menu_toggle:checked~.lpm_body{display:block!important;width:50%}}.fm1{font-size:calc(1rem + 1.25vw)}.fm2{font-size:calc(.7rem + 1vw)}.fm3{font-size:calc(.8rem + .5vw)}.fmnu1{font-size:calc(1rem + .5vw)}.fmnu2{font-size:calc(.8rem + .5vw)}.login-bkgnd,.login-container{z-index:2;position:absolute}.login-bkgnd{z-index:1;width:100%;height:100%;top:0;left:0;background-image:url(../../img/pcb.png);background-size:cover;background-position:center}.login-input{width:100%}::placeholder{color:#a0a0a0;opacity:1}.btn-primary{background-color:#1a9acc;border-color:#1a9acc}.btn-primary:focus,.btn-primary:hover{background-color:#20c0ff;border-color:#20c0ff}.header-menu{background-color:#1a9acc}.header-link{color:#f0f0f0}.header-link:hover{color:#d0d0d0}.gso{border-style:none;width:10rem;height:2.5rem;background-image:url(../../img/gso.png);background-position:center;background-size:cover}.gso,.gso:hover{background-color:#0000}.gso:focus{background-image:url(../../img/gso_pressed.png)}.border-none{border-style:none}.error{color:#8b0000}.header-logo{height:calc(1.5rem + .8vw)}
|
Loading…
Reference in New Issue
Block a user