]> git.basschouten.com Git - openhab-addons.git/blob
67e988909bc2f1816d6e7d17aaed44f2ef0cf830
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.energidataservice.internal;
14
15 import java.math.BigDecimal;
16 import java.math.RoundingMode;
17 import java.time.Duration;
18 import java.time.Instant;
19 import java.time.temporal.ChronoUnit;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26
27 import javax.measure.quantity.Energy;
28 import javax.measure.quantity.Power;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.openhab.binding.energidataservice.internal.exception.MissingPriceException;
32 import org.openhab.core.library.types.QuantityType;
33 import org.openhab.core.library.unit.Units;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 /**
38  * Provides calculations based on price maps.
39  * This is the current stage of evolution.
40  * Ideally this binding would simply provide data in a well-defined format for
41  * openHAB core. Operations on this data could then be implemented in core.
42  * This way there would be a unified interface from rules, and the calculations
43  * could be reused between different data providers (bindings).
44  * 
45  * @author Jacob Laursen - Initial contribution
46  */
47 @NonNullByDefault
48 public class PriceCalculator {
49
50     private final Logger logger = LoggerFactory.getLogger(PriceCalculator.class);
51
52     private final Map<Instant, BigDecimal> priceMap;
53
54     public PriceCalculator(Map<Instant, BigDecimal> priceMap) {
55         this.priceMap = priceMap;
56     }
57
58     /**
59      * Calculate cheapest period from list of durations with specified amount of energy
60      * used per phase.
61      *
62      * @param earliestStart Earliest allowed start time.
63      * @param latestEnd Latest allowed end time.
64      * @param totalDuration Total duration to fit.
65      * @param durationPhases List of {@link Duration}'s representing different phases of using power.
66      * @param energyUsedPerPhase Amount of energy used per phase.
67      *
68      * @return Map containing resulting values
69      */
70     public Map<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration totalDuration,
71             Collection<Duration> durationPhases, QuantityType<Energy> energyUsedPerPhase) throws MissingPriceException {
72         QuantityType<Energy> energyInWattHour = energyUsedPerPhase.toUnit(Units.WATT_HOUR);
73         if (energyInWattHour == null) {
74             throw new IllegalArgumentException(
75                     "Invalid unit " + energyUsedPerPhase.getUnit() + ", expected energy unit");
76         }
77         // watts = (kWh × 1,000) ÷ hrs
78         int numerator = energyInWattHour.intValue() * 3600;
79         List<QuantityType<Power>> consumptionPhases = new ArrayList<>();
80         Duration remainingDuration = totalDuration;
81         for (Duration phase : durationPhases) {
82             consumptionPhases.add(QuantityType.valueOf(numerator / phase.getSeconds(), Units.WATT));
83             remainingDuration = remainingDuration.minus(phase);
84         }
85         if (remainingDuration.isNegative()) {
86             throw new IllegalArgumentException("totalDuration must be equal to or greater than sum of phases");
87         }
88         if (!remainingDuration.isZero()) {
89             List<Duration> durationsWithTermination = new ArrayList<>(durationPhases);
90             durationsWithTermination.add(remainingDuration);
91             consumptionPhases.add(QuantityType.valueOf(0, Units.WATT));
92             return calculateCheapestPeriod(earliestStart, latestEnd, durationsWithTermination, consumptionPhases);
93         }
94         return calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, consumptionPhases);
95     }
96
97     /**
98      * Calculate cheapest period from duration with linear power usage.
99      *
100      * @param earliestStart Earliest allowed start time.
101      * @param latestEnd Latest allowed end time.
102      * @param duration Duration to fit.
103      * @param power Power consumption for the duration of time.
104      *
105      * @return Map containing resulting values
106      */
107     public Map<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd, Duration duration,
108             QuantityType<Power> power) throws MissingPriceException {
109         return calculateCheapestPeriod(earliestStart, latestEnd, List.of(duration), List.of(power));
110     }
111
112     /**
113      * Calculate cheapest period from list of durations with corresponding list of consumption
114      * per duration.
115      *
116      * @param earliestStart Earliest allowed start time.
117      * @param latestEnd Latest allowed end time.
118      * @param durationPhases List of {@link Duration}'s representing different phases of using power.
119      * @param consumptionPhases Corresponding List of power consumption for the duration of time.
120      *
121      * @return Map containing resulting values
122      */
123     public Map<String, Object> calculateCheapestPeriod(Instant earliestStart, Instant latestEnd,
124             Collection<Duration> durationPhases, Collection<QuantityType<Power>> consumptionPhases)
125             throws MissingPriceException {
126         if (durationPhases.size() != consumptionPhases.size()) {
127             throw new IllegalArgumentException("Number of phases do not match");
128         }
129         Map<String, Object> result = new HashMap<>();
130         Duration totalDuration = durationPhases.stream().reduce(Duration.ZERO, Duration::plus);
131         Instant calculationStart = earliestStart;
132         Instant calculationEnd = earliestStart.plus(totalDuration);
133         BigDecimal lowestPrice = BigDecimal.valueOf(Double.MAX_VALUE);
134         BigDecimal highestPrice = BigDecimal.ZERO;
135         Instant cheapestStart = Instant.MIN;
136         Instant mostExpensiveStart = Instant.MIN;
137
138         while (calculationEnd.compareTo(latestEnd) <= 0) {
139             BigDecimal currentPrice = BigDecimal.ZERO;
140             Duration minDurationUntilNextHour = Duration.ofHours(1);
141             Instant atomStart = calculationStart;
142
143             Iterator<Duration> durationIterator = durationPhases.iterator();
144             Iterator<QuantityType<Power>> consumptionIterator = consumptionPhases.iterator();
145             while (durationIterator.hasNext()) {
146                 Duration atomDuration = durationIterator.next();
147                 QuantityType<Power> atomConsumption = consumptionIterator.next();
148
149                 Instant atomEnd = atomStart.plus(atomDuration);
150                 Instant hourStart = atomStart.truncatedTo(ChronoUnit.HOURS);
151                 Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS);
152
153                 // Get next intersection with hourly rate change.
154                 Duration durationUntilNextHour = Duration.between(atomStart, hourEnd);
155                 if (durationUntilNextHour.compareTo(minDurationUntilNextHour) < 0) {
156                     minDurationUntilNextHour = durationUntilNextHour;
157                 }
158
159                 BigDecimal atomPrice = calculatePrice(atomStart, atomEnd, atomConsumption);
160                 currentPrice = currentPrice.add(atomPrice);
161                 atomStart = atomEnd;
162             }
163
164             if (currentPrice.compareTo(lowestPrice) < 0) {
165                 lowestPrice = currentPrice;
166                 cheapestStart = calculationStart;
167             }
168             if (currentPrice.compareTo(highestPrice) > 0) {
169                 highestPrice = currentPrice;
170                 mostExpensiveStart = calculationStart;
171             }
172
173             // Now fast forward to next hourly rate intersection.
174             calculationStart = calculationStart.plus(minDurationUntilNextHour);
175             calculationEnd = calculationStart.plus(totalDuration);
176         }
177
178         if (!cheapestStart.equals(Instant.MIN)) {
179             result.put("CheapestStart", cheapestStart);
180             result.put("LowestPrice", lowestPrice);
181             result.put("MostExpensiveStart", mostExpensiveStart);
182             result.put("HighestPrice", highestPrice);
183         }
184
185         return result;
186     }
187
188     /**
189      * Calculate total price from 'start' to 'end' given linear power consumption.
190      *
191      * @param start Start time
192      * @param end End time
193      * @param power The current power consumption.
194      */
195     public BigDecimal calculatePrice(Instant start, Instant end, QuantityType<Power> power)
196             throws MissingPriceException {
197         QuantityType<Power> quantityInWatt = power.toUnit(Units.WATT);
198         if (quantityInWatt == null) {
199             throw new IllegalArgumentException("Invalid unit " + power.getUnit() + ", expected power unit");
200         }
201         BigDecimal watt = new BigDecimal(quantityInWatt.intValue());
202         if (watt.equals(BigDecimal.ZERO)) {
203             return BigDecimal.ZERO;
204         }
205
206         Instant current = start;
207         BigDecimal result = BigDecimal.ZERO;
208         while (current.isBefore(end)) {
209             Instant hourStart = current.truncatedTo(ChronoUnit.HOURS);
210             Instant hourEnd = hourStart.plus(1, ChronoUnit.HOURS);
211
212             BigDecimal currentPrice = priceMap.get(hourStart);
213             if (currentPrice == null) {
214                 throw new MissingPriceException("Price missing at " + hourStart.toString());
215             }
216
217             Instant currentStart = hourStart;
218             if (start.isAfter(hourStart)) {
219                 currentStart = start;
220             }
221             Instant currentEnd = hourEnd;
222             if (end.isBefore(hourEnd)) {
223                 currentEnd = end;
224             }
225
226             // E(kWh) = P(W) × t(hr) / 1000
227             Duration duration = Duration.between(currentStart, currentEnd);
228             BigDecimal contribution = currentPrice.multiply(watt).multiply(
229                     new BigDecimal(duration.getSeconds()).divide(new BigDecimal(3600000), 9, RoundingMode.HALF_UP));
230             result = result.add(contribution);
231             logger.trace("Period {}-{}: {} @ {}", currentStart, currentEnd, contribution, currentPrice);
232
233             current = hourEnd;
234         }
235
236         return result;
237     }
238 }