]> git.basschouten.com Git - openhab-addons.git/blob
52cba2cc14b37a4c0be90114b64a4537c9b64fd3
[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.action;
14
15 import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.time.Duration;
19 import java.time.Instant;
20 import java.util.Arrays;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.Set;
27 import java.util.function.Function;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
30
31 import javax.measure.quantity.Energy;
32 import javax.measure.quantity.Power;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.energidataservice.internal.DatahubTariff;
37 import org.openhab.binding.energidataservice.internal.PriceCalculator;
38 import org.openhab.binding.energidataservice.internal.exception.MissingPriceException;
39 import org.openhab.binding.energidataservice.internal.handler.EnergiDataServiceHandler;
40 import org.openhab.core.automation.annotation.ActionInput;
41 import org.openhab.core.automation.annotation.ActionOutput;
42 import org.openhab.core.automation.annotation.RuleAction;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.unit.Units;
45 import org.openhab.core.thing.binding.ThingActions;
46 import org.openhab.core.thing.binding.ThingActionsScope;
47 import org.openhab.core.thing.binding.ThingHandler;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context.
53  *
54  * @author Jacob Laursen - Initial contribution
55  */
56 @ThingActionsScope(name = "energidataservice")
57 @NonNullByDefault
58 public class EnergiDataServiceActions implements ThingActions {
59
60     private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class);
61
62     private @Nullable EnergiDataServiceHandler handler;
63
64     private enum PriceElement {
65         SPOT_PRICE("spotprice"),
66         NET_TARIFF("nettariff"),
67         SYSTEM_TARIFF("systemtariff"),
68         ELECTRICITY_TAX("electricitytax"),
69         TRANSMISSION_NET_TARIFF("transmissionnettariff");
70
71         private static final Map<String, PriceElement> NAME_MAP = Stream.of(values())
72                 .collect(Collectors.toMap(PriceElement::toString, Function.identity()));
73
74         private String name;
75
76         private PriceElement(String name) {
77             this.name = name;
78         }
79
80         @Override
81         public String toString() {
82             return name;
83         }
84
85         public static PriceElement fromString(final String name) {
86             PriceElement myEnum = NAME_MAP.get(name.toLowerCase());
87             if (null == myEnum) {
88                 throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
89                         name, Arrays.asList(values())));
90             }
91             return myEnum;
92         }
93     }
94
95     @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description")
96     public @ActionOutput(name = "prices", type = "java.util.Map<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> getPrices() {
97         return getPrices(Arrays.stream(PriceElement.values()).collect(Collectors.toSet()));
98     }
99
100     @RuleAction(label = "@text/action.get-prices.label", description = "@text/action.get-prices.description")
101     public @ActionOutput(name = "prices", type = "java.util.Map<java.time.Instant, java.math.BigDecimal>") Map<Instant, BigDecimal> getPrices(
102             @ActionInput(name = "priceElements", label = "@text/action.get-prices.priceElements.label", description = "@text/action.get-prices.priceElements.description") @Nullable String priceElements) {
103         if (priceElements == null) {
104             logger.warn("Argument 'priceElements' is null");
105             return Map.of();
106         }
107
108         Set<PriceElement> priceElementsSet;
109         try {
110             priceElementsSet = new HashSet<PriceElement>(
111                     Arrays.stream(priceElements.split(",")).map(PriceElement::fromString).toList());
112         } catch (IllegalArgumentException e) {
113             logger.warn("{}", e.getMessage());
114             return Map.of();
115         }
116
117         return getPrices(priceElementsSet);
118     }
119
120     @RuleAction(label = "@text/action.calculate-price.label", description = "@text/action.calculate-price.description")
121     public @ActionOutput(name = "price", type = "java.math.BigDecimal") BigDecimal calculatePrice(
122             @ActionInput(name = "start", type = "java.time.Instant") Instant start,
123             @ActionInput(name = "end", type = "java.time.Instant") Instant end,
124             @ActionInput(name = "power", type = "QuantityType<Power>") QuantityType<Power> power) {
125         PriceCalculator priceCalculator = new PriceCalculator(getPrices());
126
127         try {
128             return priceCalculator.calculatePrice(start, end, power);
129         } catch (MissingPriceException e) {
130             logger.warn("{}", e.getMessage());
131             return BigDecimal.ZERO;
132         }
133     }
134
135     @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
136     public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
137             @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
138             @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
139             @ActionInput(name = "duration", type = "java.time.Duration") Duration duration) {
140         PriceCalculator priceCalculator = new PriceCalculator(getPrices());
141
142         try {
143             Map<String, Object> intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
144                     duration, QuantityType.valueOf(1000, Units.WATT));
145
146             // Create new result with stripped price information.
147             Map<String, Object> result = new HashMap<>();
148             Object value = intermediateResult.get("CheapestStart");
149             if (value != null) {
150                 result.put("CheapestStart", value);
151             }
152             value = intermediateResult.get("MostExpensiveStart");
153             if (value != null) {
154                 result.put("MostExpensiveStart", value);
155             }
156             return result;
157         } catch (MissingPriceException | IllegalArgumentException e) {
158             logger.warn("{}", e.getMessage());
159             return Map.of();
160         }
161     }
162
163     @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
164     public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
165             @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
166             @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
167             @ActionInput(name = "duration", type = "java.time.Duration") Duration duration,
168             @ActionInput(name = "power", type = "QuantityType<Power>") QuantityType<Power> power) {
169         PriceCalculator priceCalculator = new PriceCalculator(getPrices());
170
171         try {
172             return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
173         } catch (MissingPriceException | IllegalArgumentException e) {
174             logger.warn("{}", e.getMessage());
175             return Map.of();
176         }
177     }
178
179     @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
180     public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
181             @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
182             @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
183             @ActionInput(name = "totalDuration", type = "java.time.Duration") Duration totalDuration,
184             @ActionInput(name = "durationPhases", type = "java.util.List<java.time.Duration>") List<Duration> durationPhases,
185             @ActionInput(name = "energyUsedPerPhase", type = "QuantityType<Energy>") QuantityType<Energy> energyUsedPerPhase) {
186         PriceCalculator priceCalculator = new PriceCalculator(getPrices());
187
188         try {
189             return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
190                     energyUsedPerPhase);
191         } catch (MissingPriceException | IllegalArgumentException e) {
192             logger.warn("{}", e.getMessage());
193             return Map.of();
194         }
195     }
196
197     @RuleAction(label = "@text/action.calculate-cheapest-period.label", description = "@text/action.calculate-cheapest-period.description")
198     public @ActionOutput(name = "result", type = "java.util.Map<String, Object>") Map<String, Object> calculateCheapestPeriod(
199             @ActionInput(name = "earliestStart", type = "java.time.Instant") Instant earliestStart,
200             @ActionInput(name = "latestEnd", type = "java.time.Instant") Instant latestEnd,
201             @ActionInput(name = "durationPhases", type = "java.util.List<java.time.Duration>") List<Duration> durationPhases,
202             @ActionInput(name = "powerPhases", type = "java.util.List<QuantityType<Power>>") List<QuantityType<Power>> powerPhases) {
203         if (durationPhases.size() != powerPhases.size()) {
204             logger.warn("Number of duration phases ({}) is different from number of consumption phases ({})",
205                     durationPhases.size(), powerPhases.size());
206             return Map.of();
207         }
208         PriceCalculator priceCalculator = new PriceCalculator(getPrices());
209
210         try {
211             return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
212         } catch (MissingPriceException | IllegalArgumentException e) {
213             logger.warn("{}", e.getMessage());
214             return Map.of();
215         }
216     }
217
218     private Map<Instant, BigDecimal> getPrices(Set<PriceElement> priceElements) {
219         EnergiDataServiceHandler handler = this.handler;
220         if (handler == null) {
221             logger.warn("EnergiDataServiceActions ThingHandler is null.");
222             return Map.of();
223         }
224
225         Map<Instant, BigDecimal> prices;
226         boolean spotPricesRequired;
227         if (priceElements.contains(PriceElement.SPOT_PRICE)) {
228             if (priceElements.size() > 1 && !handler.getCurrency().equals(CURRENCY_DKK)) {
229                 logger.warn("Cannot calculate sum when spot price currency is {}", handler.getCurrency());
230                 return Map.of();
231             }
232             prices = handler.getSpotPrices();
233             spotPricesRequired = true;
234         } else {
235             spotPricesRequired = false;
236             prices = new HashMap<>();
237         }
238
239         if (priceElements.contains(PriceElement.NET_TARIFF)) {
240             Map<Instant, BigDecimal> netTariffMap = handler.getTariffs(DatahubTariff.NET_TARIFF);
241             mergeMaps(prices, netTariffMap, !spotPricesRequired);
242         }
243
244         if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) {
245             Map<Instant, BigDecimal> systemTariffMap = handler.getTariffs(DatahubTariff.SYSTEM_TARIFF);
246             mergeMaps(prices, systemTariffMap, !spotPricesRequired);
247         }
248
249         if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) {
250             Map<Instant, BigDecimal> electricityTaxMap = handler.getTariffs(DatahubTariff.ELECTRICITY_TAX);
251             mergeMaps(prices, electricityTaxMap, !spotPricesRequired);
252         }
253
254         if (priceElements.contains(PriceElement.TRANSMISSION_NET_TARIFF)) {
255             Map<Instant, BigDecimal> transmissionNetTariffMap = handler
256                     .getTariffs(DatahubTariff.TRANSMISSION_NET_TARIFF);
257             mergeMaps(prices, transmissionNetTariffMap, !spotPricesRequired);
258         }
259
260         return prices;
261     }
262
263     private void mergeMaps(Map<Instant, BigDecimal> destinationMap, Map<Instant, BigDecimal> sourceMap,
264             boolean createNew) {
265         for (Entry<Instant, BigDecimal> source : sourceMap.entrySet()) {
266             Instant key = source.getKey();
267             BigDecimal sourceValue = source.getValue();
268             BigDecimal destinationValue = destinationMap.get(key);
269             if (destinationValue != null) {
270                 destinationMap.put(key, sourceValue.add(destinationValue));
271             } else if (createNew) {
272                 destinationMap.put(key, sourceValue);
273             }
274         }
275     }
276
277     /**
278      * Static get prices method for DSL rule compatibility.
279      *
280      * @param actions
281      * @param priceElements Comma-separated list of price elements to include in prices.
282      * @return Map of prices
283      */
284     public static Map<Instant, BigDecimal> getPrices(@Nullable ThingActions actions, @Nullable String priceElements) {
285         if (actions instanceof EnergiDataServiceActions serviceActions) {
286             if (priceElements != null && !priceElements.isBlank()) {
287                 return serviceActions.getPrices(priceElements);
288             } else {
289                 return serviceActions.getPrices();
290             }
291         } else {
292             throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
293         }
294     }
295
296     /**
297      * Static get prices method for DSL rule compatibility.
298      *
299      * @param actions
300      * @param start Start time
301      * @param end End time
302      * @param power Constant power consumption
303      * @return Map of prices
304      */
305     public static BigDecimal calculatePrice(@Nullable ThingActions actions, @Nullable Instant start,
306             @Nullable Instant end, @Nullable QuantityType<Power> power) {
307         if (start == null || end == null || power == null) {
308             return BigDecimal.ZERO;
309         }
310         if (actions instanceof EnergiDataServiceActions serviceActions) {
311             return serviceActions.calculatePrice(start, end, power);
312         } else {
313             throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
314         }
315     }
316
317     public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
318             @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration) {
319         if (actions instanceof EnergiDataServiceActions serviceActions) {
320             if (earliestStart == null || latestEnd == null || duration == null) {
321                 return Map.of();
322             }
323             return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration);
324         } else {
325             throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
326         }
327     }
328
329     public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
330             @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration duration,
331             @Nullable QuantityType<Power> power) {
332         if (actions instanceof EnergiDataServiceActions serviceActions) {
333             if (earliestStart == null || latestEnd == null || duration == null || power == null) {
334                 return Map.of();
335             }
336             return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
337         } else {
338             throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
339         }
340     }
341
342     public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
343             @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable Duration totalDuration,
344             @Nullable List<Duration> durationPhases, @Nullable QuantityType<Energy> energyUsedPerPhase) {
345         if (actions instanceof EnergiDataServiceActions serviceActions) {
346             if (earliestStart == null || latestEnd == null || totalDuration == null || durationPhases == null
347                     || energyUsedPerPhase == null) {
348                 return Map.of();
349             }
350             return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
351                     energyUsedPerPhase);
352         } else {
353             throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
354         }
355     }
356
357     public static Map<String, Object> calculateCheapestPeriod(@Nullable ThingActions actions,
358             @Nullable Instant earliestStart, @Nullable Instant latestEnd, @Nullable List<Duration> durationPhases,
359             @Nullable List<QuantityType<Power>> powerPhases) {
360         if (actions instanceof EnergiDataServiceActions serviceActions) {
361             if (earliestStart == null || latestEnd == null || durationPhases == null || powerPhases == null) {
362                 return Map.of();
363             }
364             return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
365         } else {
366             throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
367         }
368     }
369
370     @Override
371     public void setThingHandler(@Nullable ThingHandler handler) {
372         if (handler instanceof EnergiDataServiceHandler serviceHandler) {
373             this.handler = serviceHandler;
374         }
375     }
376
377     @Override
378     public @Nullable ThingHandler getThingHandler() {
379         return handler;
380     }
381 }