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.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;
52 * {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context.
54 * @author Jacob Laursen - Initial contribution
56 @ThingActionsScope(name = "energidataservice")
58 public class EnergiDataServiceActions implements ThingActions {
60 private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class);
62 private @Nullable EnergiDataServiceHandler handler;
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");
71 private static final Map<String, PriceElement> NAME_MAP = Stream.of(values())
72 .collect(Collectors.toMap(PriceElement::toString, Function.identity()));
76 private PriceElement(String name) {
81 public String toString() {
85 public static PriceElement fromString(final String name) {
86 PriceElement myEnum = NAME_MAP.get(name.toLowerCase());
88 throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
89 name, Arrays.asList(values())));
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()));
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");
108 Set<PriceElement> priceElementsSet;
110 priceElementsSet = new HashSet<PriceElement>(
111 Arrays.stream(priceElements.split(",")).map(PriceElement::fromString).toList());
112 } catch (IllegalArgumentException e) {
113 logger.warn("{}", e.getMessage());
117 return getPrices(priceElementsSet);
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());
128 return priceCalculator.calculatePrice(start, end, power);
129 } catch (MissingPriceException e) {
130 logger.warn("{}", e.getMessage());
131 return BigDecimal.ZERO;
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());
143 Map<String, Object> intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
144 duration, QuantityType.valueOf(1000, Units.WATT));
146 // Create new result with stripped price information.
147 Map<String, Object> result = new HashMap<>();
148 Object value = intermediateResult.get("CheapestStart");
150 result.put("CheapestStart", value);
152 value = intermediateResult.get("MostExpensiveStart");
154 result.put("MostExpensiveStart", value);
157 } catch (MissingPriceException | IllegalArgumentException e) {
158 logger.warn("{}", e.getMessage());
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());
172 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
173 } catch (MissingPriceException | IllegalArgumentException e) {
174 logger.warn("{}", e.getMessage());
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());
189 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
191 } catch (MissingPriceException | IllegalArgumentException e) {
192 logger.warn("{}", e.getMessage());
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());
208 PriceCalculator priceCalculator = new PriceCalculator(getPrices());
211 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
212 } catch (MissingPriceException | IllegalArgumentException e) {
213 logger.warn("{}", e.getMessage());
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.");
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());
232 prices = handler.getSpotPrices();
233 spotPricesRequired = true;
235 spotPricesRequired = false;
236 prices = new HashMap<>();
239 if (priceElements.contains(PriceElement.NET_TARIFF)) {
240 Map<Instant, BigDecimal> netTariffMap = handler.getTariffs(DatahubTariff.NET_TARIFF);
241 mergeMaps(prices, netTariffMap, !spotPricesRequired);
244 if (priceElements.contains(PriceElement.SYSTEM_TARIFF)) {
245 Map<Instant, BigDecimal> systemTariffMap = handler.getTariffs(DatahubTariff.SYSTEM_TARIFF);
246 mergeMaps(prices, systemTariffMap, !spotPricesRequired);
249 if (priceElements.contains(PriceElement.ELECTRICITY_TAX)) {
250 Map<Instant, BigDecimal> electricityTaxMap = handler.getTariffs(DatahubTariff.ELECTRICITY_TAX);
251 mergeMaps(prices, electricityTaxMap, !spotPricesRequired);
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);
263 private void mergeMaps(Map<Instant, BigDecimal> destinationMap, Map<Instant, BigDecimal> sourceMap,
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);
278 * Static get prices method for DSL rule compatibility.
281 * @param priceElements Comma-separated list of price elements to include in prices.
282 * @return Map of prices
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);
289 return serviceActions.getPrices();
292 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
297 * Static get prices method for DSL rule compatibility.
300 * @param start Start time
301 * @param end End time
302 * @param power Constant power consumption
303 * @return Map of prices
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;
310 if (actions instanceof EnergiDataServiceActions serviceActions) {
311 return serviceActions.calculatePrice(start, end, power);
313 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
323 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration);
325 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
336 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
338 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
350 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
353 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
364 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
366 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
371 public void setThingHandler(@Nullable ThingHandler handler) {
372 if (handler instanceof EnergiDataServiceHandler serviceHandler) {
373 this.handler = serviceHandler;
378 public @Nullable ThingHandler getThingHandler() {