2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.energidataservice.internal.action;
15 import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;
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;
25 import java.util.Map.Entry;
27 import java.util.function.Function;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
31 import javax.measure.quantity.Energy;
32 import javax.measure.quantity.Power;
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;
51 * {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context.
53 * @author Jacob Laursen - Initial contribution
55 @ThingActionsScope(name = "energidataservice")
57 public class EnergiDataServiceActions implements ThingActions {
59 private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class);
61 private @Nullable EnergiDataServiceHandler handler;
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");
70 private static final Map<String, PriceElement> NAME_MAP = Stream.of(values())
71 .collect(Collectors.toMap(PriceElement::toString, Function.identity()));
75 private PriceElement(String name) {
80 public String toString() {
84 public static PriceElement fromString(final String name) {
85 PriceElement myEnum = NAME_MAP.get(name.toLowerCase());
87 throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
88 name, Arrays.asList(values())));
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()));
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");
107 Set<PriceElement> priceElementsSet;
109 priceElementsSet = new HashSet<PriceElement>(
110 Arrays.stream(priceElements.split(",")).map(PriceElement::fromString).toList());
111 } catch (IllegalArgumentException e) {
112 logger.warn("{}", e.getMessage());
116 return getPrices(priceElementsSet);
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());
127 return priceCalculator.calculatePrice(start, end, power);
128 } catch (MissingPriceException e) {
129 logger.warn("{}", e.getMessage());
130 return BigDecimal.ZERO;
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());
142 Map<String, Object> intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
143 duration, QuantityType.valueOf(1000, Units.WATT));
145 // Create new result with stripped price information.
146 Map<String, Object> result = new HashMap<>();
147 Object value = intermediateResult.get("CheapestStart");
149 result.put("CheapestStart", value);
151 value = intermediateResult.get("MostExpensiveStart");
153 result.put("MostExpensiveStart", value);
156 } catch (MissingPriceException | IllegalArgumentException e) {
157 logger.warn("{}", e.getMessage());
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());
171 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
172 } catch (MissingPriceException | IllegalArgumentException e) {
173 logger.warn("{}", e.getMessage());
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());
188 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
190 } catch (MissingPriceException | IllegalArgumentException e) {
191 logger.warn("{}", e.getMessage());
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());
207 PriceCalculator priceCalculator = new PriceCalculator(getPrices());
210 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
211 } catch (MissingPriceException | IllegalArgumentException e) {
212 logger.warn("{}", e.getMessage());
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.");
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());
231 prices = handler.getSpotPrices();
232 spotPricesRequired = true;
234 spotPricesRequired = false;
235 prices = new HashMap<>();
238 if (priceElements.contains(PriceElement.NET_TARIFF)) {
239 Map<Instant, BigDecimal> netTariffMap = handler.getNetTariffs();
240 mergeMaps(prices, netTariffMap, !spotPricesRequired);
243 if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) {
244 Map<Instant, BigDecimal> systemTariffMap = handler.getSystemTariffs();
245 mergeMaps(prices, systemTariffMap, !spotPricesRequired);
248 if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) {
249 Map<Instant, BigDecimal> electricityTaxMap = handler.getElectricityTaxes();
250 mergeMaps(prices, electricityTaxMap, !spotPricesRequired);
253 if (priceElements.contains(PriceElement.TRANSMISSION_NET_TARIFF)) {
254 Map<Instant, BigDecimal> transmissionNetTariffMap = handler.getTransmissionNetTariffs();
255 mergeMaps(prices, transmissionNetTariffMap, !spotPricesRequired);
261 private void mergeMaps(Map<Instant, BigDecimal> destinationMap, Map<Instant, BigDecimal> sourceMap,
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);
276 * Static get prices method for DSL rule compatibility.
279 * @param priceElements Comma-separated list of price elements to include in prices.
280 * @return Map of prices
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);
287 return serviceActions.getPrices();
290 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
295 * Static get prices method for DSL rule compatibility.
298 * @param start Start time
299 * @param end End time
300 * @param power Constant power consumption
301 * @return Map of prices
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;
308 if (actions instanceof EnergiDataServiceActions serviceActions) {
309 return serviceActions.calculatePrice(start, end, power);
311 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
321 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration);
323 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
334 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
336 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
348 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
351 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
362 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
364 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
369 public void setThingHandler(@Nullable ThingHandler handler) {
370 if (handler instanceof EnergiDataServiceHandler serviceHandler) {
371 this.handler = serviceHandler;
376 public @Nullable ThingHandler getThingHandler() {