diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java
index d5368eb08..4acf4b50a 100644
--- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java
+++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java
@@ -33,6 +33,7 @@
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.INSTRUCTIONS_ISSUED_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OCPP_CHARGERS_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OSCP_CAPACITY_GROUPS_KEY;
+import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OSCP_CAPACITY_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceItem.META_AVAILABLE_CREDIT;
import static net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceItem.newItem;
import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument;
@@ -103,7 +104,7 @@
* Default implementation of {@link SnfInvoicingSystem}.
*
* @author matt
- * @version 1.3
+ * @version 1.4
*/
public class DefaultSnfInvoicingSystem implements SnfInvoicingSystem, SnfTaxCodeResolver {
@@ -144,6 +145,7 @@ public class DefaultSnfInvoicingSystem implements SnfInvoicingSystem, SnfTaxCode
private String instructionsIssuedKey = INSTRUCTIONS_ISSUED_KEY;
private String ocppChargersKey = OCPP_CHARGERS_KEY;
private String oscpCapacityGroupsKey = OSCP_CAPACITY_GROUPS_KEY;
+ private String oscpCapacityKey = OSCP_CAPACITY_KEY;
private String dnp3DataPointsKey = DNP3_DATA_POINTS_KEY;
private String accountCreditKey = AccountBalance.ACCOUNT_CREDIT_KEY;
private int deliveryTimeoutSecs = DEFAULT_DELIVERY_TIMEOUT;
@@ -312,6 +314,15 @@ public SnfInvoice generateInvoice(Long userId, LocalDate startDate, LocalDate en
}
items.add(item);
}
+ if ( usage.getOscpCapacity().compareTo(BigInteger.ZERO) > 0 ) {
+ SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, oscpCapacityKey,
+ new BigDecimal(usage.getOscpCapacity()), usage.getOscpCapacityCost());
+ item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, OSCP_CAPACITY_KEY));
+ if ( !dryRun ) {
+ invoiceItemDao.save(item);
+ }
+ items.add(item);
+ }
if ( usage.getDnp3DataPoints().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, dnp3DataPointsKey,
new BigDecimal(usage.getDnp3DataPoints()), usage.getDnp3DataPointsCost());
@@ -756,6 +767,30 @@ public void setOscpCapacityGroupsKey(String oscpCapacityGroupsKey) {
"oscpCapacityGroupsKey");
}
+ /**
+ * Get the item key for OSCP Capacity.
+ *
+ * @return the key, never {@literal null}; defaults to
+ * {@link NodeUsages#OSCP_CAPACITY_KEY}
+ * @since 1.4
+ */
+ public String getOscpCapacityKey() {
+ return oscpCapacityKey;
+ }
+
+ /**
+ * Set the item key for OSCP Capacity.
+ *
+ * @param oscpCapacityKey
+ * the oscpCapacityKey to set
+ * @throws IllegalArgumentException
+ * if the argument is {@literal null}
+ * @since 1.4
+ */
+ public void setOscpCapacityKey(String oscpCapacityKey) {
+ this.oscpCapacityKey = requireNonNullArgument(oscpCapacityKey, "oscpCapacityKey");
+ }
+
/**
* Get the item key for DNP3 Data Points.
*
diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java
index 74c08ed48..c9495a6e3 100644
--- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java
+++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java
@@ -56,12 +56,12 @@
*
*
* @author matt
- * @version 2.4
+ * @version 2.5
*/
public class NodeUsage extends BasicLongEntity
implements InvoiceUsageRecord, Differentiable, NodeUsages {
- private static final long serialVersionUID = 5442178658317850821L;
+ private static final long serialVersionUID = 7976017078304093207L;
/**
* Comparator that sorts {@link NodeUsage} objects by {@code id} in
@@ -76,6 +76,7 @@ public class NodeUsage extends BasicLongEntity
private BigInteger instructionsIssued;
private BigInteger ocppChargers;
private BigInteger oscpCapacityGroups;
+ private BigInteger oscpCapacity;
private BigInteger dnp3DataPoints;
private final NodeUsageCost costs;
private BigDecimal totalCost;
@@ -86,6 +87,7 @@ public class NodeUsage extends BasicLongEntity
private BigInteger[] instructionsIssuedTiers;
private BigInteger[] ocppChargersTiers;
private BigInteger[] oscpCapacityGroupsTiers;
+ private BigInteger[] oscpCapacityTiers;
private BigInteger[] dnp3DataPointsTiers;
private NodeUsageCost[] costsTiers;
@@ -139,6 +141,7 @@ public NodeUsage(Long nodeId, Instant created) {
setInstructionsIssued(BigInteger.ZERO);
setOcppChargers(BigInteger.ZERO);
setOscpCapacityGroups(BigInteger.ZERO);
+ setOscpCapacity(BigInteger.ZERO);
setDnp3DataPoints(BigInteger.ZERO);
setTotalCost(BigDecimal.ZERO);
this.costs = new NodeUsageCost();
@@ -161,6 +164,10 @@ public String toString() {
builder.append(datumDaysStored);
builder.append(", instructionsIssued=");
builder.append(instructionsIssued);
+ builder.append(", oscpCapacityGroups=");
+ builder.append(oscpCapacityGroups);
+ builder.append(", oscpCapacity=");
+ builder.append(oscpCapacity);
builder.append(", datumPropertiesInCost=");
builder.append(costs.getDatumPropertiesInCost());
builder.append(", datumOutCost=");
@@ -173,6 +180,8 @@ public String toString() {
builder.append(costs.getOcppChargersCost());
builder.append(", oscpCapacityGroupsCost=");
builder.append(costs.getOscpCapacityGroupsCost());
+ builder.append(", oscpCapacityCost=");
+ builder.append(costs.getOscpCapacityCost());
builder.append(", dnp3DataPointsCost=");
builder.append(costs.getDnp3DataPointsCost());
builder.append(", totalCost=");
@@ -206,6 +215,7 @@ public boolean isSameAs(NodeUsage other) {
&& Objects.equals(instructionsIssued, other.instructionsIssued)
&& Objects.equals(ocppChargers, other.ocppChargers)
&& Objects.equals(oscpCapacityGroups, other.oscpCapacityGroups)
+ && Objects.equals(oscpCapacity, other.oscpCapacity)
&& Objects.equals(dnp3DataPoints, other.dnp3DataPoints);
// @formatter:on
}
@@ -554,6 +564,7 @@ private static List tiersCostBreakdown(BigInteger[] counts, NodeUsage
* {@link #INSTRUCTIONS_ISSUED_KEY}
* {@link #OCPP_CHARGERS_KEY}
* {@link #OSCP_CAPACITY_GROUPS_KEY}
+ * {@link #OSCP_CAPACITY_KEY}
* {@link #DNP3_DATA_POINTS_KEY}
*
*
@@ -567,6 +578,7 @@ public Map> getTiersCostBreakdown() {
result.put(INSTRUCTIONS_ISSUED_KEY, getInstructionsIssuedTiersCostBreakdown());
result.put(OCPP_CHARGERS_KEY, getOcppChargersTiersCostBreakdown());
result.put(OSCP_CAPACITY_GROUPS_KEY, getOscpCapacityGroupsTiersCostBreakdown());
+ result.put(OSCP_CAPACITY_KEY, getOscpCapacityTiersCostBreakdown());
result.put(DNP3_DATA_POINTS_KEY, getDnp3DataPointsTiersCostBreakdown());
return result;
}
@@ -584,6 +596,7 @@ public Map> getTiersCostBreakdown() {
* {@link #INSTRUCTIONS_ISSUED_KEY}
* {@link #OCPP_CHARGERS_KEY}
* {@link #OSCP_CAPACITY_GROUPS_KEY}
+ * {@link #OSCP_CAPACITY_KEY}
* {@link #DNP3_DATA_POINTS_KEY}
*
*
@@ -603,6 +616,8 @@ public Map getUsageInfo() {
costs.getOcppChargersCost()));
result.put(OSCP_CAPACITY_GROUPS_KEY, new UsageInfo(OSCP_CAPACITY_GROUPS_KEY,
new BigDecimal(oscpCapacityGroups), costs.getOscpCapacityGroupsCost()));
+ result.put(OSCP_CAPACITY_KEY, new UsageInfo(OSCP_CAPACITY_KEY, new BigDecimal(oscpCapacity),
+ costs.getOscpCapacityCost()));
result.put(DNP3_DATA_POINTS_KEY, new UsageInfo(DNP3_DATA_POINTS_KEY,
new BigDecimal(dnp3DataPoints), costs.getDnp3DataPointsCost()));
return result;
@@ -1182,4 +1197,120 @@ public void setInstructionsIssuedCostTiers(BigDecimal[] instructionsIssuedCostTi
costsTiers[i].setInstructionsIssuedCost(val);
}
}
+
+ /**
+ * Get the OSCP capacity.
+ *
+ * @return the count
+ * @since 2.5
+ */
+ public BigInteger getOscpCapacity() {
+ return oscpCapacity;
+ }
+
+ /**
+ * Set the count of OSCP capacity .
+ *
+ * @param oscpCapacity
+ * the count to set; if {@literal null} then {@literal 0} will be
+ * stored
+ * @since 2.5
+ */
+ public void setOscpCapacity(BigInteger oscpCapacity) {
+ if ( oscpCapacity == null ) {
+ oscpCapacity = BigInteger.ZERO;
+ }
+ this.oscpCapacity = oscpCapacity;
+ }
+
+ /**
+ * Get the cost of OSCP capacity.
+ *
+ * @return the cost
+ * @since 2.5
+ */
+ public BigDecimal getOscpCapacityCost() {
+ return costs.getOscpCapacityCost();
+ }
+
+ /**
+ * Set the cost of OSCP capacity.
+ *
+ * @param oscpCapacityCost
+ * the cost to set
+ * @since 2.5
+ */
+ public void setOscpCapacityCost(BigDecimal oscpCapacityCost) {
+ costs.setOscpCapacityCost(oscpCapacityCost);
+ }
+
+ /**
+ * Get the OSCP capacity tier cost breakdown.
+ *
+ * @return the costs, never {@literal null}
+ * @since 2.5
+ */
+ @JsonIgnore
+ public List getOscpCapacityTiersCostBreakdown() {
+ return tiersCostBreakdown(oscpCapacityTiers, costsTiers, NodeUsageCost::getOscpCapacityCost);
+ }
+
+ /**
+ * Get the OSCP capacity, per tier.
+ *
+ * @return the counts
+ * @since 2.5
+ */
+ public BigInteger[] getOscpCapacityTiers() {
+ return oscpCapacityTiers;
+ }
+
+ /**
+ * Set the OSCP capacity, per tier.
+ *
+ * @param oscpCapacityTiers
+ * the counts to set
+ * @since 2.5
+ */
+ public void setOscpCapacityTiers(BigInteger[] oscpCapacityTiers) {
+ this.oscpCapacityTiers = oscpCapacityTiers;
+ }
+
+ /**
+ * Set the OSCP capacity, per tier, as decimals.
+ *
+ * @param oscpCapacityTiers
+ * the counts to set
+ * @since 2.5
+ */
+ public void setOscpCapacityTiersNumeric(BigDecimal[] oscpCapacityTiers) {
+ this.oscpCapacityTiers = decimalsToIntegers(oscpCapacityTiers);
+ }
+
+ /**
+ * Get the cost of OSCP capacity, per tier.
+ *
+ * @return the cost
+ * @since 2.5
+ */
+ public BigDecimal[] getOscpCapacityCostTiers() {
+ return getTierCostValues(costsTiers, NodeUsageCost::getOscpCapacityCost);
+ }
+
+ /**
+ * Set the cost of OSCP capacity, per tier.
+ *
+ * @param oscpCapacityCostTiers
+ * the costs to set
+ * @since 2.5
+ */
+ public void setOscpCapacityCostTiers(BigDecimal[] oscpCapacityCostTiers) {
+ prepCostsTiers(oscpCapacityCostTiers);
+ for ( int i = 0; i < costsTiers.length; i++ ) {
+ BigDecimal val = (oscpCapacityCostTiers != null && i < oscpCapacityCostTiers.length
+ ? oscpCapacityCostTiers[i]
+ : null);
+ costsTiers[i].setOscpCapacityCost(val);
+ }
+ }
}
diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java
index e1ab2ff67..b8234c018 100644
--- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java
+++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java
@@ -22,6 +22,7 @@
package net.solarnetwork.central.user.billing.snf.domain;
+import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Objects;
@@ -34,9 +35,11 @@
*
*
* @author matt
- * @version 1.3
+ * @version 1.4
*/
-public class NodeUsageCost {
+public class NodeUsageCost implements Serializable {
+
+ private static final long serialVersionUID = 2764937975318244422L;
private BigDecimal datumPropertiesInCost;
private BigDecimal datumDaysStoredCost;
@@ -44,6 +47,7 @@ public class NodeUsageCost {
private BigDecimal instructionsIssuedCost;
private BigDecimal ocppChargersCost;
private BigDecimal oscpCapacityGroupsCost;
+ private BigDecimal oscpCapacityCost;
private BigDecimal dnp3DataPointsCost;
/**
@@ -51,7 +55,7 @@ public class NodeUsageCost {
*/
public NodeUsageCost() {
this(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
- BigDecimal.ZERO, BigDecimal.ZERO);
+ BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
}
/**
@@ -159,6 +163,40 @@ public NodeUsageCost(String datumPropertiesInCost, String datumOutCost, String d
new BigDecimal(dnp3DataPointsCost));
}
+ /**
+ * Constructor.
+ *
+ *
+ * This constructor converts all costs to {@link BigDecimal} values.
+ *
+ *
+ * @param datumPropertiesInCost
+ * the properties in cost
+ * @param datumOutCost
+ * the datum out cost
+ * @param datumDaysStoredCost
+ * the days stored cost
+ * @param instructionsIssuedCost
+ * the instructions issued code
+ * @param ocppChargersCost
+ * the OCPP Chargers cost
+ * @param oscpCapacityGroupsCost
+ * the OSCP Capacity Groups cost
+ * @param oscpCapacityCost
+ * the OSCP capacity cost
+ * @param dnp3DataPointsCost
+ * the DNP3 Data Points cost
+ * @since 1.4
+ */
+ public NodeUsageCost(String datumPropertiesInCost, String datumOutCost, String datumDaysStoredCost,
+ String instructionsIssuedCost, String ocppChargersCost, String oscpCapacityGroupsCost,
+ String oscpCapacityCost, String dnp3DataPointsCost) {
+ this(new BigDecimal(datumPropertiesInCost), new BigDecimal(datumOutCost),
+ new BigDecimal(datumDaysStoredCost), new BigDecimal(instructionsIssuedCost),
+ new BigDecimal(ocppChargersCost), new BigDecimal(oscpCapacityGroupsCost),
+ new BigDecimal(oscpCapacityCost), new BigDecimal(dnp3DataPointsCost));
+ }
+
/**
* Constructor.
*
@@ -253,10 +291,46 @@ public NodeUsageCost(BigDecimal datumPropertiesInCost, BigDecimal datumOutCost,
setDnp3DataPointsCost(dnp3DataPointsCost);
}
+ /**
+ * Constructor.
+ *
+ * @param datumPropertiesInCost
+ * the properties in cost
+ * @param datumOutCost
+ * the datum out cost
+ * @param datumDaysStoredCost
+ * the days stored cost
+ * @param instructionsIssuedCost
+ * the instructions issued cost
+ * @param ocppChargersCost
+ * the OCPP Chargers cost
+ * @param oscpCapacityGroupsCost
+ * the OSCP Capacity Groups cost
+ * @param oscpCapacityCost
+ * the OSCP capacity cost
+ * @param dnp3DataPointsCost
+ * the DNP3 Data Points cost
+ * @since 1.4
+ */
+ public NodeUsageCost(BigDecimal datumPropertiesInCost, BigDecimal datumOutCost,
+ BigDecimal datumDaysStoredCost, BigDecimal instructionsIssuedCost,
+ BigDecimal ocppChargersCost, BigDecimal oscpCapacityGroupsCost, BigDecimal oscpCapacityCost,
+ BigDecimal dnp3DataPointsCost) {
+ super();
+ setDatumPropertiesInCost(datumPropertiesInCost);
+ setDatumOutCost(datumOutCost);
+ setDatumDaysStoredCost(datumDaysStoredCost);
+ setInstructionsIssuedCost(instructionsIssuedCost);
+ setOcppChargersCost(ocppChargersCost);
+ setOscpCapacityGroupsCost(oscpCapacityGroupsCost);
+ setOscpCapacityCost(oscpCapacityCost);
+ setDnp3DataPointsCost(dnp3DataPointsCost);
+ }
+
@Override
public int hashCode() {
return Objects.hash(datumDaysStoredCost, datumOutCost, datumPropertiesInCost, ocppChargersCost,
- oscpCapacityGroupsCost, dnp3DataPointsCost);
+ oscpCapacityGroupsCost, oscpCapacityCost, dnp3DataPointsCost);
}
@Override
@@ -273,6 +347,7 @@ public boolean equals(Object obj) {
&& Objects.equals(datumPropertiesInCost, other.datumPropertiesInCost)
&& Objects.equals(ocppChargersCost, other.ocppChargersCost)
&& Objects.equals(oscpCapacityGroupsCost, other.oscpCapacityGroupsCost)
+ && Objects.equals(oscpCapacityCost, other.oscpCapacityCost)
&& Objects.equals(dnp3DataPointsCost, other.dnp3DataPointsCost);
}
@@ -290,6 +365,8 @@ public String toString() {
builder.append(ocppChargersCost);
builder.append(", oscpCapacityGroupsCost=");
builder.append(oscpCapacityGroupsCost);
+ builder.append(", oscpCapacityCost=");
+ builder.append(oscpCapacityCost);
builder.append(", dnp3DataPointsCost=");
builder.append(dnp3DataPointsCost);
builder.append("}");
@@ -425,6 +502,30 @@ public void setOscpCapacityGroupsCost(BigDecimal oscpCapacityGroupsCost) {
this.oscpCapacityGroupsCost = oscpCapacityGroupsCost;
}
+ /**
+ * Get the OSCP capacity cost.
+ *
+ * @return the cost, never {@literal null}
+ * @since 1.4
+ */
+ public BigDecimal getOscpCapacityCost() {
+ return oscpCapacityCost;
+ }
+
+ /**
+ * Set the OSCP capacity cost.
+ *
+ * @param oscpCapacityCost
+ * the cost to set
+ * @since 1.4
+ */
+ public void setOscpCapacityCost(BigDecimal oscpCapacityCost) {
+ if ( oscpCapacityCost == null ) {
+ oscpCapacityCost = BigDecimal.ZERO;
+ }
+ this.oscpCapacityCost = oscpCapacityCost;
+ }
+
/**
* Get the DNP3 Data Points cost.
*
diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java
index 7d3ebdff5..4e604672b 100644
--- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java
+++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java
@@ -49,8 +49,11 @@ public enum NodeUsageType implements NodeUsages {
/** OSCP Capacity Group usage. */
OscpCapacityGroups(6, OSCP_CAPACITY_GROUPS_KEY),
+ /** OSCP Capacity usage. */
+ OscpCapacity(7, OSCP_CAPACITY_KEY),
+
/** DNP3 Data Points usage. */
- Dnp3DataPoints(7, DNP3_DATA_POINTS_KEY),
+ Dnp3DataPoints(8, DNP3_DATA_POINTS_KEY),
;
@@ -97,6 +100,7 @@ public static NodeUsageType forKey(String key) {
case INSTRUCTIONS_ISSUED_KEY -> InstructionsIssued;
case OCPP_CHARGERS_KEY -> OcppChargers;
case OSCP_CAPACITY_GROUPS_KEY -> OscpCapacityGroups;
+ case OSCP_CAPACITY_KEY -> OscpCapacity;
case DNP3_DATA_POINTS_KEY -> Dnp3DataPoints;
default -> throw new IllegalArgumentException("Unknown NodeUsageType key value: " + key);
};
diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java
index fe7471c59..749dfd839 100644
--- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java
+++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java
@@ -26,7 +26,7 @@
* Node usage constants.
*
* @author matt
- * @version 1.3
+ * @version 1.4
*/
public interface NodeUsages {
@@ -48,6 +48,9 @@ public interface NodeUsages {
/** A key to use for OSCP Capacity Group usage. */
String OSCP_CAPACITY_GROUPS_KEY = "oscp-cap-groups";
+ /** A key to use for OSCP capacity usage. */
+ String OSCP_CAPACITY_KEY = "oscp-cap";
+
/** A key to use for DNP3 data points usage. */
String DNP3_DATA_POINTS_KEY = "dnp3-data-points";
diff --git a/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml b/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml
index d62f3885b..ea7ce13ae 100644
--- a/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml
+++ b/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml
@@ -33,6 +33,7 @@
, nu.instr_issued AS node_usage_instr_issued
, nu.ocpp_chargers AS node_usage_ocpp_chargers
, nu.oscp_cap_groups AS node_usage_oscp_cap_groups
+ , nu.oscp_cap AS node_usage_oscp_cap
, nu.dnp3_data_points AS node_usage_dnp3_data_points
, nu.prop_in_tiers AS node_usage_prop_in_tiers
@@ -41,6 +42,7 @@
, nu.instr_issued_tiers AS node_usage_instr_issued_tiers
, nu.ocpp_chargers_tiers AS node_usage_ocpp_chargers_tiers
, nu.oscp_cap_groups_tiers AS node_usage_oscp_cap_groups_tiers
+ , nu.oscp_cap_tiers AS node_usage_oscp_cap_tiers
, nu.dnp3_data_points_tiers AS node_usage_dnp3_data_points_tiers
@@ -51,6 +53,7 @@
+
@@ -59,6 +62,7 @@
+
@@ -70,6 +74,7 @@
, nu.instr_issued_cost AS node_usage_instr_issued_cost
, nu.ocpp_chargers_cost AS node_usage_ocpp_chargers_cost
, nu.oscp_cap_groups_cost AS node_usage_oscp_cap_groups_cost
+ , nu.oscp_cap_cost AS node_usage_oscp_cap_cost
, nu.dnp3_data_points_cost AS node_usage_dnp3_data_points_cost
, nu.total_cost AS node_usage_total_cost
@@ -79,6 +84,7 @@
, nu.instr_issued_tiers_cost AS node_usage_instr_issued_tiers_cost
, nu.ocpp_chargers_tiers_cost AS node_usage_ocpp_chargers_tiers_cost
, nu.oscp_cap_groups_tiers_cost AS node_usage_oscp_cap_groups_tiers_cost
+ , nu.oscp_cap_tiers_cost AS node_usage_oscp_cap_tiers_cost
, nu.dnp3_data_points_tiers_cost AS node_usage_dnp3_data_points_tiers_cost
@@ -89,6 +95,7 @@
+
@@ -98,6 +105,7 @@
+
diff --git a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java
index 8e2ecdb73..a0e709a02 100644
--- a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java
+++ b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java
@@ -26,7 +26,9 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import java.math.BigDecimal;
+import java.sql.Array;
import java.sql.Timestamp;
+import java.sql.Types;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -39,6 +41,7 @@
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.test.context.ContextConfiguration;
import net.solarnetwork.central.test.AbstractCentralTransactionalTest;
import net.solarnetwork.central.user.billing.snf.domain.Account;
@@ -196,6 +199,32 @@ protected UUID setupDatumStream(Long nodeId, String sourceId) {
return streamId;
}
+ protected UUID setupDatumStream(Long nodeId, String sourceId, String iProps[], String aProps[]) {
+ jdbcTemplate.execute("""
+ INSERT INTO solardatm.da_datm_meta (stream_id, node_id, source_id, names_i, names_a)
+ VALUES (?,?,?,?,?)
+ ON CONFLICT (node_id, source_id) DO NOTHING
+ """, (PreparedStatementCallback) ps -> {
+ ps.setObject(1, UUID.randomUUID());
+ ps.setObject(2, nodeId, Types.BIGINT);
+ ps.setString(3, sourceId);
+
+ Array iPropsArray = ps.getConnection().createArrayOf("text", iProps);
+ ps.setArray(4, iPropsArray);
+ iPropsArray.free();
+
+ Array aPropsArray = ps.getConnection().createArrayOf("text", aProps);
+ ps.setArray(5, aPropsArray);
+ aPropsArray.free();
+ ps.execute();
+ return null;
+ });
+ UUID streamId = jdbcTemplate.queryForObject(
+ "select stream_id from solardatm.da_datm_meta where node_id = ? and source_id = ?",
+ UUID.class, nodeId, sourceId);
+ return streamId;
+ }
+
protected UUID addAuditDatumMonthly(Long nodeId, String sourceId, Instant date, long propCount,
long datumQueryCount, int datumCount, short datumHourlyCount, short datumDailyCount,
boolean monthPresent) {
@@ -217,6 +246,37 @@ protected void addAuditAccumulatingDatumDaily(Long nodeId, String sourceId, Inst
datumCount, datumHourlyCount, datumDailyCount, datumMonthlyCount);
}
+ protected void addAggregateDatumDaily(UUID streamId, Instant date, BigDecimal[] iData,
+ BigDecimal[][] iStats, BigDecimal[] aData, BigDecimal[][] aStats) {
+ jdbcTemplate.execute("""
+ INSERT INTO solardatm.agg_datm_daily
+ (stream_id, ts_start, data_i, stat_i, data_a, read_a)
+ VALUES (?,?,?,?,?,?)
+ """, (PreparedStatementCallback) ps -> {
+ ps.setObject(1, streamId);
+ ps.setTimestamp(2, Timestamp.from(date));
+
+ Array iDataArray = ps.getConnection().createArrayOf("numeric", iData);
+ ps.setArray(3, iDataArray);
+ iDataArray.free();
+
+ Array iStatsArray = ps.getConnection().createArrayOf("numeric", iStats);
+ ps.setArray(4, iStatsArray);
+ iStatsArray.free();
+
+ Array aDataArray = ps.getConnection().createArrayOf("numeric", aData);
+ ps.setArray(5, aDataArray);
+ aDataArray.free();
+
+ Array aStatsArray = ps.getConnection().createArrayOf("numeric", aStats);
+ ps.setArray(6, aStatsArray);
+ aStatsArray.free();
+
+ ps.execute();
+ return null;
+ });
+ }
+
protected void assertAccountBalance(Long accountId, BigDecimal chargeTotal,
BigDecimal paymentTotal) {
getSqlSessionTemplate().flushStatements();
diff --git a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java
index 34471992c..5104d1f40 100644
--- a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java
+++ b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java
@@ -40,6 +40,8 @@
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
@@ -1053,4 +1055,207 @@ public void usageForUser_oneNodeOneSource_withInstructions() {
}
}
+ @Test
+ public void usageForUser_oneNodeOneSource_withOscpCap() {
+ // GIVEN
+ final LocalDate month = LocalDate.of(2024, 4, 1);
+ final String sourceId = "S1";
+ setupDatumStream(nodeId, sourceId);
+
+ // add 10 days worth of audit data
+ final int numDays = 10;
+ for ( int dayOffset = 0; dayOffset < numDays; dayOffset++ ) {
+ Instant day = month.plusDays(dayOffset).atStartOfDay(TEST_ZONE).toInstant();
+ addAuditAccumulatingDatumDaily(nodeId, sourceId, day, 1000000, 2000000, 3000000, 4000000);
+ addAuditDatumMonthly(nodeId, sourceId, day, 100000, 200000, 300000, (short) 400000,
+ (short) 500000, true);
+ }
+
+ final Long fpId = OscpTestUtils.saveFlexibilityProviderAuthId(jdbcTemplate, userId,
+ randomUUID().toString());
+ final Long cpId = OscpTestUtils.saveCapacityProvider(jdbcTemplate, userId, fpId, "CP");
+ final Long coId = OscpTestUtils.saveCapacityOptimizer(jdbcTemplate, userId, fpId, "CO");
+ final int numOscpCapacityGroups = 150;
+ final int numOscpFlexibilityAssets = 1;
+ final String[] iProps = new String[] { "watts" };
+ final String[] eProps = new String[] { "wattHours" };
+ final Instant startDay = month.atStartOfDay(ZoneOffset.UTC).toInstant();
+ for ( int i = 0; i < numOscpCapacityGroups; i++ ) {
+ Long cgId = OscpTestUtils.saveCapacityGroup(jdbcTemplate, userId, "CG-%d".formatted(i),
+ "CG-%d".formatted(i), cpId, coId);
+ for ( int j = 0; j < numOscpFlexibilityAssets; j++ ) {
+ String faIdent = "CG-%d Asset-%d".formatted(i, j);
+
+ UUID streamId = setupDatumStream(nodeId, faIdent, iProps, eProps);
+
+ BigDecimal energy = BigDecimal.ZERO;
+ for ( int t = 0; t < numDays; t++ ) {
+ BigDecimal power = new BigDecimal(1_000_000L * (t + 1));
+ BigDecimal[] iData = new BigDecimal[] { power };
+ BigDecimal[][] iStats = new BigDecimal[][] {
+ // count, min, max
+ new BigDecimal[] { new BigDecimal(10L), new BigDecimal(0L), power } };
+
+ BigDecimal[] aData = new BigDecimal[] { new BigDecimal(1_000_000L * (t + 1)) };
+ BigDecimal end = energy.add(power);
+ BigDecimal[][] aStats = new BigDecimal[][] {
+ // diff, start, end
+ new BigDecimal[] { power, energy, end } };
+ addAggregateDatumDaily(streamId, startDay.plus(t, ChronoUnit.DAYS), iData, iStats,
+ aData, aStats);
+ energy = end;
+ }
+
+ OscpTestUtils.saveFlexibilityAsset(jdbcTemplate, userId, faIdent, faIdent, cgId, nodeId,
+ faIdent, iProps, eProps);
+ }
+ }
+
+ debugRows("solardatm.aud_acc_datm_daily", "ts_start");
+ debugRows("solardatm.agg_datm_daily", "stream_id,ts_start");
+ debugQuery(format(
+ "select * from solarbill.billing_usage_tier_details(%d, '2024-04-01'::timestamp, '2024-05-01'::timestamp, '2024-04-01'::date)",
+ userId));
+
+ debugQuery("""
+ SELECT COUNT(*) AS oscp_cg_count
+ FROM solaroscp.oscp_cg_conf WHERE user_id = %d AND enabled = TRUE
+ """.formatted(userId));
+
+ // WHEN
+ UsageTiers tiers = dao.effectiveUsageTiers(month);
+ Map> tierMap = tiers.tierMap();
+
+ List r1 = dao.findNodeUsageForAccount(userId, month, month.plusMonths(1));
+ List r2 = dao.findUsageForAccount(userId, month, month.plusMonths(1));
+
+ // THEN
+ int i = 0;
+ for ( List results : Arrays.asList(r1, r2) ) {
+ assertThat("Results non-null with single result", results, hasSize(1));
+ NodeUsage usage = results.get(0);
+ if ( i == 0 ) {
+ assertThat("Node ID present for node-level usage", usage.getId(), equalTo(nodeId));
+ assertThat("Node usage description is node name", usage.getDescription(),
+ equalTo(format("Test Node %d", nodeId)));
+ } else {
+ assertThat("No node ID for account-level usage", usage.getId(), nullValue());
+ }
+ assertThat("Properties in count aggregated", usage.getDatumPropertiesIn(),
+ equalTo(BigInteger.valueOf(100000L * numDays)));
+ assertThat("Datum out count aggregated", usage.getDatumOut(),
+ equalTo(BigInteger.valueOf(200000L * numDays)));
+ assertThat("Datum stored count aggregated", usage.getDatumDaysStored(),
+ equalTo(BigInteger.valueOf((1000000L + 2000000L + 3000000L + 4000000L) * numDays)));
+
+ // see tiersForDate_202211
+ Map> tiersBreakdown = usage.getTiersCostBreakdown();
+ List propsInTiersCost = tiersBreakdown.get(NodeUsage.DATUM_PROPS_IN_KEY);
+ assertThat("Properties in cost tier count", propsInTiersCost, hasSize(2));
+ List datumOutTiersCost = tiersBreakdown.get(NodeUsage.DATUM_OUT_KEY);
+ assertThat("Datum out cost tier count", datumOutTiersCost, hasSize(1));
+ List datumStoredTiersCost = tiersBreakdown.get(NodeUsage.DATUM_DAYS_STORED_KEY);
+ assertThat("Datum stored cost tier count", datumStoredTiersCost, hasSize(2));
+
+ if ( i == 0 ) {
+ List ocppChargersTiersCost = tiersBreakdown.get(NodeUsage.OCPP_CHARGERS_KEY);
+ assertThat("No node-level OCPP charger costs", ocppChargersTiersCost, hasSize(0));
+ List oscpCapacityGroupsTiersCost = tiersBreakdown
+ .get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY);
+ assertThat("No node-level OSCP capacity group costs", oscpCapacityGroupsTiersCost,
+ hasSize(0));
+ List dnp3DataPointsTiersCost = tiersBreakdown
+ .get(NodeUsage.DNP3_DATA_POINTS_KEY);
+ assertThat("No node-level DNP3 Data Points costs", dnp3DataPointsTiersCost, hasSize(0));
+ } else {
+ List ocppChargersTiersCost = tiersBreakdown.get(NodeUsage.OCPP_CHARGERS_KEY);
+ assertThat("No account-level OCPP charger costs", ocppChargersTiersCost, hasSize(0));
+ List oscpCapacityGroupsTiersCost = tiersBreakdown
+ .get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY);
+ assertThat("Account-level OSCP capacity group costs", oscpCapacityGroupsTiersCost,
+ hasSize(2));
+ List oscpCapacityTiersCost = tiersBreakdown.get(NodeUsage.OSCP_CAPACITY_KEY);
+ assertThat("Account-level OSCP capacity costs", oscpCapacityTiersCost, hasSize(4));
+ List dnp3DataPointsTiersCost = tiersBreakdown
+ .get(NodeUsage.DNP3_DATA_POINTS_KEY);
+ assertThat("No account-level DNP3 Data Points costs", dnp3DataPointsTiersCost,
+ hasSize(0));
+ /*-
+ datum-props-in=[
+ NamedCost{name=Tier 1, quantity=500000, cost=2.500000},
+ NamedCost{name=Tier 2, quantity=500000, cost=1.500000}],
+ datum-out=[
+ NamedCost{name=Tier 1, quantity=2000000, cost=0.2000000}],
+ datum-days-stored=[
+ NamedCost{name=Tier 1, quantity=10000000, cost=0.50000000},
+ NamedCost{name=Tier 2, quantity=90000000, cost=0.90000000}],
+ ocpp-chargers=[
+ NamedCost{name=Tier 1, quantity=250, cost=500},
+ NamedCost{name=Tier 2, quantity=1750, cost=1750}],
+ oscp-cap-groups=[
+ NamedCost{name=Tier 1, quantity=100, cost=200},
+ NamedCost{name=Tier 2, quantity=50, cost=75.0}]
+ oscp-cap=[
+ NamedCost{name=Tier 1, quantity=6000000, cost=180.000000}
+ NamedCost{name=Tier 2, quantity=34000000, cost=850.00000},
+ NamedCost{name=Tier 3, quantity=60000000, cost=1050.000000},
+ NamedCost{name=Tier 4, quantity=1400000000, cost=14000.00000}]
+ */
+ // @formatter:off
+ assertThat("Properties in cost", usage.getDatumPropertiesInCost().setScale(3), equalTo(
+ new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(0).getCost())
+ .add( new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(1).getCost()))
+ .setScale(3)
+ ));
+ assertThat("Properties in cost tiers", propsInTiersCost, contains(
+ NamedCost.forTier(1, "500000", new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(0).getCost()).toString()),
+ NamedCost.forTier(2, "500000", new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(1).getCost()).toString())));
+
+ assertThat("Datum out cost", usage.getDatumOutCost().setScale(3), equalTo(
+ new BigDecimal("2000000").multiply(tierMap.get(NodeUsage.DATUM_OUT_KEY).get(0).getCost())
+ .setScale(3)
+ ));
+ assertThat("Datum out cost tiers", datumOutTiersCost, contains(
+ NamedCost.forTier(4, "2000000", new BigDecimal("2000000").multiply(tierMap.get(NodeUsage.DATUM_OUT_KEY).get(0).getCost()).toString())));
+
+ assertThat("Datum stored cost", usage.getDatumDaysStoredCost().setScale(3), equalTo(
+ new BigDecimal("10000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(0).getCost())
+ .add( new BigDecimal("90000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(1).getCost()))
+ .setScale(3)
+ ));
+ assertThat("Datum stored cost tiers", datumStoredTiersCost, contains(
+ NamedCost.forTier(1, "10000000", new BigDecimal("10000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(0).getCost()).toString()),
+ NamedCost.forTier(2, "90000000", new BigDecimal("90000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(1).getCost()).toString())));
+
+ assertThat("OSCP Capacity Groups cost", usage.getOscpCapacityGroupsCost().setScale(3), equalTo(
+ new BigDecimal("100") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(0).getCost())
+ .add( new BigDecimal("50") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(1).getCost()))
+ .setScale(3)
+ ));
+ assertThat("OSCP Capacity Groups cost tiers", oscpCapacityGroupsTiersCost, contains(
+ NamedCost.forTier(1, "100", new BigDecimal("100") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(0).getCost()).toString()),
+ NamedCost.forTier(2, "50", new BigDecimal("50") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(1).getCost()).toString())
+ ));
+
+ assertThat("OSCP Capacity cost", usage.getOscpCapacityCost().setScale(3), equalTo(
+ new BigDecimal("6000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(0).getCost())
+ .add( new BigDecimal("34000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(1).getCost()))
+ .add( new BigDecimal("60000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(2).getCost()))
+ .add( new BigDecimal("1400000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(3).getCost()))
+ .setScale(3)
+ ));
+ assertThat("OSCP Capacity cost tiers", oscpCapacityTiersCost, contains(
+ NamedCost.forTier(1, "6000000", new BigDecimal("6000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(0).getCost()).toString()),
+ NamedCost.forTier(2, "34000000", new BigDecimal("34000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(1).getCost()).toString()),
+ NamedCost.forTier(3, "60000000", new BigDecimal("60000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(2).getCost()).toString()),
+ NamedCost.forTier(4, "1400000000", new BigDecimal("1400000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(3).getCost()).toString())
+ ));
+
+ // @formatter:on
+ }
+ i++;
+ }
+
+ }
+
}
diff --git a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java
index f1cd99821..6514a45fa 100644
--- a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java
+++ b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java
@@ -22,6 +22,8 @@
package net.solarnetwork.central.user.billing.snf.test;
+import java.math.BigDecimal;
+import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Types;
@@ -32,7 +34,7 @@
* Testing utilities for Flexibility Provider.
*
* @author matt
- * @version 1.0
+ * @version 1.1
*/
public final class OscpTestUtils {
@@ -164,4 +166,75 @@ public static Long saveCapacityGroup(JdbcOperations jdbcOps, Long userId, String
return holder.getKeyAs(Long.class);
}
+ /**
+ * Save a new Capacity Optimizer.
+ *
+ * @param jdbcOps
+ * the JDBC template to use
+ * @param userId
+ * the user ID
+ * @param name
+ * the display name
+ * @param ident
+ * the unique idnentifier
+ * @param cgId
+ * the Capacity Group ID
+ * @param nodeId
+ * the node ID
+ * @param sourceId
+ * the source ID
+ * @param iProps
+ * the instantaneous properties
+ * @param eProps
+ * the accumulating properties
+ * @return the new ID
+ * @since 1.1
+ */
+ public static Long saveFlexibilityAsset(JdbcOperations jdbcOps, Long userId, String name,
+ String ident, Long cgId, Long nodeId, String sourceId, String[] iProps, String[] eProps) {
+ GeneratedKeyHolder holder = new GeneratedKeyHolder();
+ jdbcOps.update((con) -> {
+ PreparedStatement stmt = con.prepareStatement("""
+ INSERT INTO solaroscp.oscp_asset_conf (
+ user_id, enabled, cname, ident, cg_id, node_id, source_id
+ , iprops, iprops_unit, iprops_mult
+ , eprops, eprops_unit, eprops_mult
+ , audience, category)
+ VALUES (
+ ?, ?, ?, ?, ?, ?, ?
+ , ?, ?, ?
+ , ?, ?, ?
+ , ?, ?
+ ) RETURNING id
+ """, Statement.RETURN_GENERATED_KEYS);
+ stmt.setObject(1, userId, Types.BIGINT);
+ stmt.setBoolean(2, true);
+ stmt.setString(3, name);
+ stmt.setString(4, ident);
+ stmt.setObject(5, cgId, Types.BIGINT);
+ stmt.setObject(6, nodeId, Types.BIGINT);
+ stmt.setString(7, sourceId);
+
+ Array iPropsArray = con.createArrayOf("text", iProps);
+ stmt.setArray(8, iPropsArray);
+ iPropsArray.free();
+
+ stmt.setInt(9, 'P'); // kW
+ stmt.setBigDecimal(10, new BigDecimal("0.001"));
+
+ Array ePropsArray = con.createArrayOf("text", eProps);
+ stmt.setArray(11, ePropsArray);
+ ePropsArray.free();
+
+ stmt.setInt(12, 'E'); // kWh
+ stmt.setBigDecimal(13, new BigDecimal("0.001"));
+
+ stmt.setInt(14, 'o'); // Capacity Optimizer
+ stmt.setInt(15, 'v'); // Charging
+
+ return stmt;
+ }, holder);
+ return holder.getKeyAs(Long.class);
+ }
+
}