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:
MarkBryanMilligan 2022-02-10 13:21:18 -06:00
parent 94ebf5fa93
commit a892c7f0e8
13 changed files with 143 additions and 86 deletions

View File

@ -3,7 +3,7 @@
<groupId>com.lanternsoftware.currentmonitor</groupId> <groupId>com.lanternsoftware.currentmonitor</groupId>
<artifactId>lantern-currentmonitor</artifactId> <artifactId>lantern-currentmonitor</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<version>1.0.6</version> <version>1.0.7</version>
<name>lantern-currentmonitor</name> <name>lantern-currentmonitor</name>
<properties> <properties>

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
package com.lanternsoftware.currentmonitor;
public class CalibrationSample {
public long time;
public double voltage;
}

View File

@ -56,44 +56,71 @@ public class CurrentMonitor {
chips.clear(); chips.clear();
pins.clear(); pins.clear();
gpio.shutdown(); gpio.shutdown();
LOG.info("Current Monitor Stopped"); LOG.info("Power Monitor Service Stopped");
} }
public void setDebug(boolean _debug) { public void setDebug(boolean _debug) {
debug = _debug; debug = _debug;
} }
public double calibrateVoltage(double _curCalibration, float _voltage) { public CalibrationResult calibrateVoltage(double _curCalibration) {
GpioPinAnalogInput voltagePin = getPin(0, 0); GpioPinAnalogInput voltagePin = getPin(0, 0);
if (voltagePin == null) if (voltagePin == null)
return 0.0; return null;
List<Double> samples = new ArrayList<>(120000); 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 long intervalEnd = System.nanoTime() + 2000000000L; //Scan voltage for 2 seconds
while (System.nanoTime() < intervalEnd) { while (offset < maxSamples) {
samples.add(voltagePin.getValue()); samples[offset].time = System.nanoTime();
samples[offset].voltage = voltagePin.getValue();
offset++;
if (samples[offset-1].time > intervalEnd)
break;
} }
double vOffset = 0.0; double vOffset = 0.0;
for (Double sample : samples) { for (CalibrationSample sample : samples) {
vOffset += sample; 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; double vRms = 0.0;
for (Double sample : samples) { for (int sample = 0; sample < offset; sample++) {
sample -= vOffset; voltage = samples[sample].voltage - vOffset;
vRms += sample * sample; 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); double oldVrms = _curCalibration * Math.sqrt(vRms);
if (oldVrms < 20) { if (oldVrms < 20) {
LOG.error("Could not get a valid voltage read, please check that your AC/AC transformer is connected"); 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); double newVrms = newCal * Math.sqrt(vRms);
LOG.info("Old Voltage Calibration: {} Old vRMS: {}", _curCalibration, oldVrms); LOG.info("Old Voltage Calibration: {} Old vRMS: {}", _curCalibration, oldVrms);
LOG.info("New Voltage Calibration: {} New vRMS: {}", newCal, newVrms); 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) { public void monitorPower(BreakerHub _hub, List<Breaker> _breakers, int _intervalMs, PowerListener _listener) {
@ -101,6 +128,9 @@ public class CurrentMonitor {
stopMonitoring(); stopMonitoring();
listener = _listener; listener = _listener;
List<Breaker> validBreakers = CollectionUtils.filter(_breakers, _b -> _b.getPort() > 0 && _b.getPort() < 16); 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); sampler = new Sampler(_hub, validBreakers, _intervalMs, 2);
LOG.info("Starting to monitor ports {}", CollectionUtils.transformToCommaSeparated(validBreakers, _b -> String.valueOf(_b.getPort()))); LOG.info("Starting to monitor ports {}", CollectionUtils.transformToCommaSeparated(validBreakers, _b -> String.valueOf(_b.getPort())));
executor.submit(sampler); executor.submit(sampler);
@ -188,7 +218,7 @@ public class CurrentMonitor {
while (true) { while (true) {
synchronized (this) { synchronized (this) {
if (!running) { if (!running) {
LOG.error("Power Monitoring Stopped"); LOG.info("Power Monitoring Stopped");
break; break;
} }
} }

View File

@ -164,11 +164,8 @@ public class MonitorApp {
breakerConfig = newConfig; breakerConfig = newConfig;
List<Breaker> breakers = breakerConfig.getBreakersForHub(config.getHub()); List<Breaker> breakers = breakerConfig.getBreakersForHub(config.getHub());
BreakerHub hub = breakerConfig.getHub(config.getHub()); BreakerHub hub = breakerConfig.getHub(config.getHub());
if (hub != null) { if (hub != null)
LOG.info("Monitoring {} breakers for hub {}", CollectionUtils.size(breakers), hub.getHub()); monitor.monitorPower(hub, breakers, 1000, logger);
if (CollectionUtils.size(breakers) > 0)
monitor.monitorPower(hub, breakers, 1000, logger);
}
} }
break; break;
} }
@ -270,19 +267,18 @@ public class MonitorApp {
LOG.info("Breaker Config loaded"); LOG.info("Breaker Config loaded");
BreakerHub hub = breakerConfig.getHub(config.getHub()); BreakerHub hub = breakerConfig.getHub(config.getHub());
if (hub != null) { if (hub != null) {
if (config.isNeedsCalibration() && (config.getAutoCalibrationVoltage() != 0.0)) { if (config.isNeedsCalibration()) {
double newCal = monitor.calibrateVoltage(hub.getVoltageCalibrationFactor(), config.getAutoCalibrationVoltage()); CalibrationResult cal = monitor.calibrateVoltage(hub.getVoltageCalibrationFactor());
if (newCal != 0.0) { if (cal != null) {
hub.setVoltageCalibrationFactor(newCal); hub.setVoltageCalibrationFactor(cal.getVoltageCalibrationFactor());
hub.setFrequency(cal.getFrequency());
config.setNeedsCalibration(false); config.setNeedsCalibration(false);
ResourceLoader.writeFile(WORKING_DIR + "config.json", DaoSerializer.toJson(config)); ResourceLoader.writeFile(WORKING_DIR + "config.json", DaoSerializer.toJson(config));
post(DaoSerializer.toZipBson(breakerConfig), "config"); post(DaoSerializer.toZipBson(breakerConfig), "config");
} }
} }
List<Breaker> breakers = breakerConfig.getBreakersForHub(config.getHub()); List<Breaker> breakers = breakerConfig.getBreakersForHub(config.getHub());
LOG.info("Monitoring {} breakers for hub {}", CollectionUtils.size(breakers), hub.getHub()); monitor.monitorPower(hub, breakers, 1000, logger);
if (CollectionUtils.size(breakers) > 0)
monitor.monitorPower(hub, breakers, 1000, logger);
} }
monitor.submit(new PowerPoster()); monitor.submit(new PowerPoster());
} }

View File

@ -187,7 +187,7 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao {
for (int offset = 0; offset < bytesInDay; offset += 4) { for (int offset = 0; offset < bytesInDay; offset += 4) {
nanBuffer.putFloat(offset, Float.NaN); nanBuffer.putFloat(offset, Float.NaN);
} }
for (int key : breakerKeys.keySet()) { for (int key : breakerKeys.values()) {
dayReadings.computeIfAbsent(key, _k->nanArray); dayReadings.computeIfAbsent(key, _k->nanArray);
} }
for (Entry<Integer, byte[]> be : dayReadings.entrySet()) { for (Entry<Integer, byte[]> be : dayReadings.entrySet()) {

View File

@ -185,6 +185,14 @@ public class BreakerConfig implements IIdentical<BreakerConfig> {
return null; 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) { public BreakerGroup findParentGroup(BreakerGroup _group) {
for (BreakerGroup group : CollectionUtils.makeNotNull(breakerGroups)) { for (BreakerGroup group : CollectionUtils.makeNotNull(breakerGroups)) {
BreakerGroup parent = group.findParentGroup(_group); BreakerGroup parent = group.findParentGroup(_group);

View File

@ -11,6 +11,15 @@ public class Meter implements IIdentical<Meter> {
private int index; private int index;
private String name; private String name;
public Meter() {
}
public Meter(int _accountId, int _index, String _name) {
accountId = _accountId;
index = _index;
name = _name;
}
public int getAccountId() { public int getAccountId() {
return accountId; return accountId;
} }

View File

@ -75,15 +75,16 @@ public class ExportServlet extends SecureConsoleServlet {
redirect(_rep, _req.getContextPath() + "/export"); redirect(_rep, _req.getContextPath() + "/export");
return; return;
} }
StringBuilder header = new StringBuilder("Timestamp"); StringBuilder header = new StringBuilder("\"Timestamp\"");
for (BreakerEnergyArchive ba : CollectionUtils.makeNotNull(fday.getBreakers())) { for (BreakerEnergyArchive ba : CollectionUtils.makeNotNull(fday.getBreakers())) {
Breaker b = breakers.get(Breaker.intKey(ba.getPanel(), ba.getSpace())); Breaker b = breakers.get(Breaker.intKey(ba.getPanel(), ba.getSpace()));
header.append(","); header.append(",\"");
if (b != null) { if (b != null) {
header.append(b.getKey()); header.append(b.getKey());
header.append("-"); header.append("-");
header.append(b.getName()); header.append(b.getName());
} }
header.append("\"");
} }
header.append("\n"); header.append("\n");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

View File

@ -1,50 +1,36 @@
<!DOCTYPE html> <div class="row">
<html lang="en"> <div class="col-1 col-lg-3 col-4k-4"></div>
<head> <div class="col-10 col-lg-6 col-4k-4">
<meta charset="utf-8"> <div class="row mt-3">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <table class="table">
<meta name="viewport" content="width=device-width, initial-scale=1"> <tbody>
<#if inprogress><meta http-equiv="refresh" content="1"></#if> <#list months as month>
<title>Lantern Console</title> <#if month.progress == 0>
<link href="${link_prefix}bootstrap/css/bootstrap.min.css" rel="stylesheet"> <form method="POST">
<link href="${link_prefix}bootstrap/css/style.min.css" rel="stylesheet"> </#if>
</head> <tr>
<body> <td>${month.name!}</td>
<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>
<#if month.progress == 0> <#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> </#if>
<tr> </tr>
<td>${month.name!}</td> <#if month.progress == 0>
<#if month.progress == 0> </form>
<td><input type="hidden" name="month" value="${month.date}"/><input type="submit" class="btn-primary border-none px-2 py-1" value="Export"/></td> </#if>
<#elseif month.progress == 1> </#list>
<td>Queued</td> </tbody>
<#elseif month.progress < 100> </table>
<td>Progress ${month.progress!}%</td> </div>
<#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>
</div> </div>
</body> <div class="col-1 col-lg-3 col-4k-4"></div>
</html> </div>

View File

@ -4,9 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content=""> <#if inprogress!><meta http-equiv="refresh" content="1"></#if>
<meta name="author" content="">
<title>Lantern Power Monitor</title> <title>Lantern Power Monitor</title>
<link rel="icon" type="image/png" href="${link_prefix}img/favicon.png"> <link rel="icon" type="image/png" href="${link_prefix}img/favicon.png">
<link href="${link_prefix}bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="${link_prefix}bootstrap/css/bootstrap.min.css" rel="stylesheet">
@ -17,7 +15,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row header-menu py-2"> <div class="row header-menu py-2">
<div class="col-2"></div> <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"></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}" 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> <div class="col-auto mx-2"><a href="${link_prefix}logout" class="text-decoration-none header-link fmnu1">LOGOUT</a></div>

View File

@ -227,3 +227,7 @@
.error { .error {
color: darkred; color: darkred;
} }
.header-logo {
height: calc(1.5rem + .8vw)
}

View File

@ -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)}