Update summaries when billing rate configuration changes. Refactor some of the billing rate variable names to be more clear.

This commit is contained in:
MarkBryanMilligan 2021-08-11 14:12:27 -05:00
parent cb774d1950
commit 77ceec745c
17 changed files with 264 additions and 39 deletions

View File

@ -1,6 +1,7 @@
package com.lanternsoftware.dataaccess.currentmonitor;
import com.lanternsoftware.datamodel.currentmonitor.Account;
import com.lanternsoftware.datamodel.currentmonitor.BillingRate;
import com.lanternsoftware.datamodel.currentmonitor.Breaker;
import com.lanternsoftware.datamodel.currentmonitor.BreakerConfig;
import com.lanternsoftware.datamodel.currentmonitor.BreakerGroup;
@ -28,6 +29,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -159,6 +161,32 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao {
putBreakerGroupEnergy(summary);
}
private void rebuildSummaries(int _accountId, Collection<BillingRate> _rates) {
logger.info("Rebuilding summaries due to a change in {} rates", CollectionUtils.size(_rates));
HubPowerMinute firstMinute = proxy.queryOne(HubPowerMinute.class, new DaoQuery("account_id", _accountId), DaoSort.sort("minute"));
if (firstMinute == null)
return;
TimeZone tz = getTimeZoneForAccount(_accountId);
Map<String, BillingRate> rates = CollectionUtils.transformToMap(_rates, _r->String.format("%d%d", DaoSerializer.toLong(_r.getBeginEffective()), DaoSerializer.toLong(_r.getEndEffective())));
for (BillingRate rate : rates.values()) {
Date start = rate.getBeginEffective();
Date end = rate.getEndEffective();
Date now = new Date();
if ((start == null) || start.before(firstMinute.getMinuteAsDate()))
start = firstMinute.getMinuteAsDate();
if ((end == null) || end.after(now))
end = now;
rebuildSummaries(_accountId, start, end);
if (rate.isRecursAnnually()) {
while (end.before(now)) {
start = DateUtils.addYears(start, 1, tz);
end = DateUtils.addYears(end, 1, tz);
rebuildSummaries(_accountId, start, end);
}
}
}
}
@Override
public void rebuildSummaries(int _accountId) {
HubPowerMinute firstMinute = proxy.queryOne(HubPowerMinute.class, new DaoQuery("account_id", _accountId), DaoSort.sort("minute"));
@ -228,8 +256,20 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao {
DaoQuery configQuery = new DaoQuery("_id", String.valueOf(_config.getAccountId()));
BreakerConfig oldConfig = proxy.queryOne(BreakerConfig.class, configQuery);
if (oldConfig != null) {
proxy.saveEntity("config_archive", DaoSerializer.toDaoEntity(oldConfig));
_config.setVersion(oldConfig.getVersion() + 1);
if (NullUtils.isNotIdentical(_config, oldConfig)) {
DaoEntity oldEntity = DaoSerializer.toDaoEntity(oldConfig);
oldEntity.put("_id", String.format("%d-%d", oldConfig.getAccountId(), oldConfig.getVersion()));
oldEntity.put("account_id", oldConfig.getAccountId());
oldEntity.put("archive_date", DaoSerializer.toLong(new Date()));
proxy.saveEntity("config_archive", oldEntity);
executor.submit(() -> {
List<BillingRate> changedRates = new ArrayList<>(_config.getBillingRates());
changedRates.removeAll(CollectionUtils.makeNotNull(oldConfig.getBillingRates()));
if (!changedRates.isEmpty())
rebuildSummaries(_config.getAccountId(), changedRates);
});
}
}
proxy.save(_config);
}

View File

@ -1,7 +0,0 @@
package com.lanternsoftware.datamodel.currentmonitor;
public enum BillingMode {
ANY_DIRECTION,
CONSUMPTION,
PRODUCTION;
}

View File

@ -5,13 +5,14 @@ import com.lanternsoftware.util.dao.annotations.DBSerializable;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
import java.util.TimeZone;
@DBSerializable
public class BillingRate {
private int meter;
private int dayBillingCycleStart;
private BillingMode mode;
private GridFlow flow;
private double rate;
private BillingCurrency currency;
private int timeOfDayStart;
@ -38,12 +39,12 @@ public class BillingRate {
dayBillingCycleStart = _dayBillingCycleStart;
}
public BillingMode getMode() {
return mode;
public GridFlow getFlow() {
return flow;
}
public void setMode(BillingMode _mode) {
mode = _mode;
public void setFlow(GridFlow _flow) {
flow = _flow;
}
public double getRate() {
@ -118,8 +119,8 @@ public class BillingRate {
recursAnnually = _recursAnnually;
}
public boolean isApplicable(BillingMode _mode, int _meter, double _monthKWh, Date _time, TimeZone _tz) {
if ((mode != BillingMode.ANY_DIRECTION) && (mode != _mode))
public boolean isApplicable(GridFlow _mode, int _meter, double _monthKWh, Date _time, TimeZone _tz) {
if ((flow != GridFlow.BOTH) && (flow != _mode))
return false;
if ((meter != -1) && (_meter != meter))
return false;
@ -160,11 +161,24 @@ public class BillingRate {
return rate * _kWh;
}
@Override
public boolean equals(Object _o) {
if (this == _o) return true;
if (_o == null || getClass() != _o.getClass()) return false;
BillingRate that = (BillingRate) _o;
return meter == that.meter && dayBillingCycleStart == that.dayBillingCycleStart && Double.compare(that.rate, rate) == 0 && timeOfDayStart == that.timeOfDayStart && timeOfDayEnd == that.timeOfDayEnd && Double.compare(that.monthKWhStart, monthKWhStart) == 0 && Double.compare(that.monthKWhEnd, monthKWhEnd) == 0 && recursAnnually == that.recursAnnually && flow == that.flow && currency == that.currency && Objects.equals(beginEffective, that.beginEffective) && Objects.equals(endEffective, that.endEffective);
}
@Override
public int hashCode() {
return Objects.hash(meter, dayBillingCycleStart, flow, rate, currency, timeOfDayStart, timeOfDayEnd, monthKWhStart, monthKWhEnd, beginEffective, endEffective, recursAnnually);
}
public BillingRate duplicate() {
BillingRate r = new BillingRate();
r.setMeter(meter);
r.setDayBillingCycleStart(dayBillingCycleStart);
r.setMode(mode);
r.setFlow(flow);
r.setRate(rate);
r.setCurrency(currency);
r.setTimeOfDayStart(timeOfDayStart);

View File

@ -1,10 +1,13 @@
package com.lanternsoftware.datamodel.currentmonitor;
import com.lanternsoftware.util.IIdentical;
import com.lanternsoftware.util.dao.annotations.DBSerializable;
import java.util.Objects;
@DBSerializable()
public class Breaker {
public class Breaker implements IIdentical<Breaker> {
private static final int TANDEM_BREAKER_MASK = 3072;
private static final int SPACE_MASK = 1023;
private static final int TANDEM_BREAKER_A_MASK = 1024;
@ -137,7 +140,7 @@ public class Breaker {
}
public double getLowPassFilter() {
return Math.abs(lowPassFilter) < 0.05 ? 1.6: lowPassFilter;
return Math.abs(lowPassFilter) < 0.05 ? 1.6 : lowPassFilter;
}
public void setLowPassFilter(double _lowPassFilter) {
@ -161,7 +164,7 @@ public class Breaker {
}
public double getCalibrationFactor() {
return calibrationFactor == 0.0?1.0:calibrationFactor;
return calibrationFactor == 0.0 ? 1.0 : calibrationFactor;
}
public void setCalibrationFactor(double _calibrationFactor) {
@ -196,15 +199,15 @@ public class Breaker {
}
public static int portToChip(int _port) {
return (_port < 9)?1:0;
return (_port < 9) ? 1 : 0;
}
public static int portToPin(int _port) {
return (_port < 9)?_port-1:_port-8;
return (_port < 9) ? _port - 1 : _port - 8;
}
public static int toPort(int _chip, int _pin) {
return (_chip == 0)?_pin+8:_pin+1;
return (_chip == 0) ? _pin + 8 : _pin + 1;
}
public static boolean isTandemBreakerA(int _space) {
@ -224,7 +227,7 @@ public class Breaker {
}
public static int toSpace(int _id) {
return _id & (TANDEM_BREAKER_MASK |SPACE_MASK);
return _id & (TANDEM_BREAKER_MASK | SPACE_MASK);
}
public static String toSpaceDisplay(int _space) {
@ -238,4 +241,23 @@ public class Breaker {
public int getSpaceIndex() {
return space & SPACE_MASK;
}
@Override
public boolean equals(Object _o) {
if (this == _o) return true;
if (_o == null || getClass() != _o.getClass()) return false;
Breaker breaker = (Breaker) _o;
return panel == breaker.panel && space == breaker.space;
}
@Override
public boolean isIdentical(Breaker _o) {
if (this == _o) return true;
return panel == _o.panel && space == _o.space && meter == _o.meter && hub == _o.hub && port == _o.port && sizeAmps == _o.sizeAmps && Double.compare(_o.calibrationFactor, calibrationFactor) == 0 && Double.compare(_o.lowPassFilter, lowPassFilter) == 0 && doublePower == _o.doublePower && Objects.equals(name, _o.name) && Objects.equals(description, _o.description) && polarity == _o.polarity && type == _o.type && Objects.equals(key, _o.key);
}
@Override
public int hashCode() {
return Objects.hash(panel, space);
}
}

View File

@ -1,6 +1,7 @@
package com.lanternsoftware.datamodel.currentmonitor;
import com.lanternsoftware.util.CollectionUtils;
import com.lanternsoftware.util.IIdentical;
import com.lanternsoftware.util.NullUtils;
import com.lanternsoftware.util.dao.DaoSerializer;
import com.lanternsoftware.util.dao.annotations.DBSerializable;
@ -8,9 +9,10 @@ import com.lanternsoftware.util.dao.annotations.PrimaryKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@DBSerializable(autogen = false)
public class BreakerConfig {
public class BreakerConfig implements IIdentical<BreakerConfig> {
@PrimaryKey
private int accountId;
private List<Meter> meters;
@ -182,4 +184,23 @@ public class BreakerConfig {
public BillingCurrency getCurrency() {
return CollectionUtils.getFirst(CollectionUtils.transformToSet(billingRates, BillingRate::getCurrency));
}
@Override
public boolean equals(Object _o) {
if (this == _o) return true;
if (_o == null || getClass() != _o.getClass()) return false;
BreakerConfig that = (BreakerConfig) _o;
return accountId == that.accountId && CollectionUtils.isEqual(meters, that.meters) && CollectionUtils.isEqual(panels, that.panels) && CollectionUtils.isEqual(breakerHubs, that.breakerHubs) && CollectionUtils.isEqual(breakerGroups, that.breakerGroups) && CollectionUtils.isEqual(billingRates, that.billingRates);
}
@Override
public boolean isIdentical(BreakerConfig _o) {
if (this == _o) return true;
return accountId == _o.accountId && CollectionUtils.isIdentical(meters, _o.meters) && CollectionUtils.isIdentical(panels, _o.panels) && CollectionUtils.isIdentical(breakerHubs, _o.breakerHubs) && CollectionUtils.isIdentical(breakerGroups, _o.breakerGroups) && CollectionUtils.isEqual(billingRates, _o.billingRates);
}
@Override
public int hashCode() {
return Objects.hash(accountId, meters, panels, breakerHubs, breakerGroups, billingRates, version);
}
}

View File

@ -2,18 +2,18 @@ package com.lanternsoftware.datamodel.currentmonitor;
import com.lanternsoftware.util.CollectionUtils;
import com.lanternsoftware.util.IIdentical;
import com.lanternsoftware.util.NullUtils;
import com.lanternsoftware.util.dao.annotations.DBSerializable;
import com.lanternsoftware.util.dao.annotations.PrimaryKey;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@DBSerializable()
public class BreakerGroup {
public class BreakerGroup implements IIdentical<BreakerGroup> {
@PrimaryKey private String id;
private int accountId;
private String name;
@ -186,6 +186,15 @@ public class BreakerGroup {
return Objects.equals(id, that.id);
}
@Override
public boolean isIdentical(BreakerGroup _o) {
if (this == _o)
return true;
if (_o == null)
return false;
return NullUtils.isEqual(id, _o.id) && accountId == _o.accountId && NullUtils.isEqual(name, _o.name) && CollectionUtils.isIdentical(subGroups, _o.subGroups) && CollectionUtils.isIdentical(breakers, _o.breakers);
}
@Override
public int hashCode() {
return Objects.hash(id);

View File

@ -96,8 +96,8 @@ public class BreakerGroupEnergy {
double secondFromGrid;
for (Map.Entry<MeterMinute, MeterMinuteValues> meter : meters.entrySet()) {
double monthkWh = monthFromGrid/3600000;
List<BillingRate> consumptionRates = CollectionUtils.filter(_rates, _r->_r.isApplicable(BillingMode.CONSUMPTION, meter.getKey().meter, monthkWh, meter.getKey().minute, timezone));
List<BillingRate> productionRates = CollectionUtils.filter(_rates, _r->_r.isApplicable(BillingMode.PRODUCTION, meter.getKey().meter, monthkWh, meter.getKey().minute, timezone));
List<BillingRate> consumptionRates = CollectionUtils.filter(_rates, _r->_r.isApplicable(GridFlow.FROM, meter.getKey().meter, monthkWh, meter.getKey().minute, timezone));
List<BillingRate> productionRates = CollectionUtils.filter(_rates, _r->_r.isApplicable(GridFlow.TO, meter.getKey().meter, monthkWh, meter.getKey().minute, timezone));
for (int i = 0; i < 60; i++) {
secondFromGrid = meter.getValue().usage[i] - meter.getValue().solar[i];
monthFromGrid += secondFromGrid;
@ -351,7 +351,7 @@ public class BreakerGroupEnergy {
return charge(_selectedBreakers, true);
}
public double charge(Set<String> _selectedBreakers, BillingMode _mode) {
public double charge(Set<String> _selectedBreakers, GridFlow _mode) {
return charge(_selectedBreakers, true, _mode);
}
@ -359,7 +359,7 @@ public class BreakerGroupEnergy {
return charge(_selectedBreakers, _includeSubgroups, null);
}
public double charge(Set<String> _selectedBreakers, boolean _includeSubgroups, BillingMode _mode) {
public double charge(Set<String> _selectedBreakers, boolean _includeSubgroups, GridFlow _mode) {
double charge = 0.0;
if (_includeSubgroups) {
for (BreakerGroupEnergy group : CollectionUtils.makeNotNull(subGroups)) {
@ -368,7 +368,7 @@ public class BreakerGroupEnergy {
}
if ((energyBlocks != null) && ((_selectedBreakers == null) || _selectedBreakers.contains(getGroupId()))) {
for (EnergyBlock energy : energyBlocks) {
if ((_mode == null) || ((_mode == BillingMode.PRODUCTION) && energy.getCharge() < 0.0) || (_mode == BillingMode.CONSUMPTION && energy.getCharge() > 0.0))
if ((_mode == null) || ((_mode == GridFlow.TO) && energy.getCharge() < 0.0) || (_mode == GridFlow.FROM && energy.getCharge() > 0.0))
charge += energy.getCharge();
}
}

View File

@ -1,10 +1,13 @@
package com.lanternsoftware.datamodel.currentmonitor;
import com.lanternsoftware.util.IIdentical;
import com.lanternsoftware.util.dao.annotations.DBSerializable;
import java.util.Objects;
@DBSerializable
public class BreakerHub {
public class BreakerHub implements IIdentical<BreakerHub> {
private int hub;
private double voltageCalibrationFactor;
private double portCalibrationFactor;
@ -50,4 +53,23 @@ public class BreakerHub {
public void setBluetoothMac(String _bluetoothMac) {
bluetoothMac = _bluetoothMac;
}
@Override
public boolean equals(Object _o) {
if (this == _o) return true;
if (_o == null || getClass() != _o.getClass()) return false;
BreakerHub that = (BreakerHub) _o;
return hub == that.hub;
}
@Override
public boolean isIdentical(BreakerHub _o) {
if (this == _o) return true;
return hub == _o.hub && Double.compare(_o.voltageCalibrationFactor, voltageCalibrationFactor) == 0 && Double.compare(_o.portCalibrationFactor, portCalibrationFactor) == 0 && frequency == _o.frequency && Objects.equals(bluetoothMac, _o.bluetoothMac);
}
@Override
public int hashCode() {
return Objects.hash(hub);
}
}

View File

@ -1,9 +1,12 @@
package com.lanternsoftware.datamodel.currentmonitor;
import com.lanternsoftware.util.IIdentical;
import com.lanternsoftware.util.dao.annotations.DBSerializable;
import java.util.Objects;
@DBSerializable
public class BreakerPanel {
public class BreakerPanel implements IIdentical<BreakerPanel> {
private int accountId;
private String name;
private int index;
@ -49,4 +52,23 @@ public class BreakerPanel {
public void setMeter(int _meter) {
meter = _meter;
}
@Override
public boolean equals(Object _o) {
if (this == _o) return true;
if (_o == null || getClass() != _o.getClass()) return false;
BreakerPanel that = (BreakerPanel) _o;
return accountId == that.accountId && index == that.index;
}
@Override
public boolean isIdentical(BreakerPanel _o) {
if (this == _o) return true;
return accountId == _o.accountId && index == _o.index && spaces == _o.spaces && meter == _o.meter && Objects.equals(name, _o.name);
}
@Override
public int hashCode() {
return Objects.hash(accountId, index);
}
}

View File

@ -0,0 +1,7 @@
package com.lanternsoftware.datamodel.currentmonitor;
public enum GridFlow {
BOTH,
FROM,
TO;
}

View File

@ -1,9 +1,12 @@
package com.lanternsoftware.datamodel.currentmonitor;
import com.lanternsoftware.util.IIdentical;
import com.lanternsoftware.util.dao.annotations.DBSerializable;
import java.util.Objects;
@DBSerializable
public class Meter {
public class Meter implements IIdentical<Meter> {
private int accountId;
private int index;
private String name;
@ -31,4 +34,24 @@ public class Meter {
public void setName(String _name) {
name = _name;
}
@Override
public boolean equals(Object _o) {
if (this == _o) return true;
if (_o == null || getClass() != _o.getClass()) return false;
Meter meter = (Meter) _o;
return accountId == meter.accountId && index == meter.index;
}
@Override
public boolean isIdentical(Meter _o) {
if (this == _o) return true;
if (_o == null) return false;
return accountId == _o.accountId && index == _o.index && Objects.equals(name, _o.name);
}
@Override
public int hashCode() {
return Objects.hash(accountId, index);
}
}

View File

@ -1,7 +1,7 @@
package com.lanternsoftware.datamodel.currentmonitor.dao;
import com.lanternsoftware.datamodel.currentmonitor.BillingCurrency;
import com.lanternsoftware.datamodel.currentmonitor.BillingMode;
import com.lanternsoftware.datamodel.currentmonitor.GridFlow;
import com.lanternsoftware.datamodel.currentmonitor.BillingRate;
import com.lanternsoftware.util.dao.AbstractDaoSerializer;
import com.lanternsoftware.util.dao.DaoEntity;
@ -29,7 +29,7 @@ public class BillingRateSerializer extends AbstractDaoSerializer<BillingRate>
DaoEntity d = new DaoEntity();
d.put("meter", _o.getMeter());
d.put("day_billing_cycle_start", _o.getDayBillingCycleStart());
d.put("mode", DaoSerializer.toEnumName(_o.getMode()));
d.put("flow", DaoSerializer.toEnumName(_o.getFlow()));
d.put("rate", _o.getRate());
d.put("unit", DaoSerializer.toEnumName(_o.getCurrency()));
d.put("time_of_day_start", _o.getTimeOfDayStart());
@ -48,7 +48,7 @@ public class BillingRateSerializer extends AbstractDaoSerializer<BillingRate>
BillingRate o = new BillingRate();
o.setMeter(DaoSerializer.getInteger(_d, "meter"));
o.setDayBillingCycleStart(DaoSerializer.getInteger(_d, "day_billing_cycle_start"));
o.setMode(DaoSerializer.getEnum(_d, "mode", BillingMode.class));
o.setFlow(DaoSerializer.getEnum(_d, "flow", GridFlow.class, GridFlow.BOTH));
o.setRate(DaoSerializer.getDouble(_d, "rate"));
o.setCurrency(DaoSerializer.getEnum(_d, "unit", BillingCurrency.class));
o.setTimeOfDayStart(DaoSerializer.getInteger(_d, "time_of_day_start"));

View File

@ -1,8 +1,10 @@
package com.lanternsoftware.currentmonitor.servlet;
import com.lanternsoftware.currentmonitor.context.Globals;
import com.lanternsoftware.util.dao.auth.AuthCode;
import com.lanternsoftware.datamodel.currentmonitor.BreakerConfig;
import com.lanternsoftware.util.dao.auth.AuthCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
@ -10,6 +12,8 @@ import javax.servlet.http.HttpServletResponse;
@WebServlet("/config/*")
public class ConfigServlet extends SecureServlet {
private static final Logger logger = LoggerFactory.getLogger(ConfigServlet.class);
@Override
protected void get(AuthCode _authCode, HttpServletRequest _req, HttpServletResponse _rep) {
if (isPath(_req, 0, "bin"))
@ -29,6 +33,7 @@ public class ConfigServlet extends SecureServlet {
_rep.setStatus(401);
return;
}
logger.info("Received config for account {}", config.getAccountId());
Globals.dao.putConfig(config);
}
}

View File

@ -177,6 +177,30 @@ public class CollectionUtils {
return _arr[_arr.length - 1];
}
public static <T> boolean isEqual(Collection<T> _l1, Collection<T> _l2) {
if (size(_l1) != size(_l2))
return false;
Iterator<T> i1 = _l1.iterator();
Iterator<T> i2 = _l2.iterator();
while (i1.hasNext()) {
if (NullUtils.isNotEqual(i1.next(), i2.next()))
return false;
}
return true;
}
public static <T extends IIdentical<T>> boolean isIdentical(Collection<T> _l1, Collection<T> _l2) {
if (size(_l1) != size(_l2))
return false;
Iterator<T> i1 = _l1.iterator();
Iterator<T> i2 = _l2.iterator();
while (i1.hasNext()) {
if (NullUtils.isNotIdentical(i1.next(), i2.next()))
return false;
}
return true;
}
public static <T> boolean contains(Collection<T> _coll, T _t) {
if (_coll == null)
return false;

View File

@ -1,11 +1,11 @@
package com.lanternsoftware.util;
import java.sql.Time;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
public abstract class DateUtils {
@ -405,6 +405,14 @@ public abstract class DateUtils {
return dateFormat(_format, _tz).format(_dt);
}
public static String formatDate(int _format, TimeZone _tz, Date _dt) {
if (_dt == null)
return null;
DateFormat format = DateFormat.getDateInstance(_format, Locale.getDefault());
format.setTimeZone(_tz);
return format.format(_dt);
}
public static Date parse(String _format, String _date) {
return parse(_format, TimeZone.getTimeZone("UTC"), _date);
}

View File

@ -0,0 +1,5 @@
package com.lanternsoftware.util;
public interface IIdentical<T> {
boolean isIdentical(T _other);
}

View File

@ -23,6 +23,16 @@ public class NullUtils {
return (b == null);
}
public static <T extends IIdentical<T>> boolean isNotIdentical(T a, T b) {
return !isIdentical(a, b);
}
public static <T extends IIdentical<T>> boolean isIdentical(T a, T b) {
if (a != null)
return (b != null) && a.isIdentical(b);
return (b == null);
}
public static <T> boolean isNotEqual(T a, T b, IEquals<T> _equals) {
return !isEqual(a, b, _equals);
}