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