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