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