2 * Copyright (c) 2010-2024 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 PriceComponent {
65 SPOT_PRICE("spotprice", null),
66 GRID_TARIFF("gridtariff", DatahubTariff.GRID_TARIFF),
67 SYSTEM_TARIFF("systemtariff", DatahubTariff.SYSTEM_TARIFF),
68 TRANSMISSION_GRID_TARIFF("transmissiongridtariff", DatahubTariff.TRANSMISSION_GRID_TARIFF),
69 ELECTRICITY_TAX("electricitytax", DatahubTariff.ELECTRICITY_TAX),
70 REDUCED_ELECTRICITY_TAX("reducedelectricitytax", DatahubTariff.REDUCED_ELECTRICITY_TAX);
72 private static final Map<String, PriceComponent> NAME_MAP = Stream.of(values())
73 .collect(Collectors.toMap(PriceComponent::toString, Function.identity()));
76 private @Nullable DatahubTariff datahubTariff;
78 private PriceComponent(String name, @Nullable DatahubTariff datahubTariff) {
80 this.datahubTariff = datahubTariff;
84 public String toString() {
88 public static PriceComponent fromString(final String name) {
89 PriceComponent myEnum = NAME_MAP.get(name.toLowerCase());
91 throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
92 name, Arrays.asList(values())));
97 public @Nullable DatahubTariff getDatahubTariff() {
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.");
110 boolean isReducedElectricityTax = handler.isReducedElectricityTax();
112 return getPrices(Arrays.stream(PriceComponent.values())
113 .filter(component -> component != (isReducedElectricityTax ? PriceComponent.ELECTRICITY_TAX
114 : PriceComponent.REDUCED_ELECTRICITY_TAX))
115 .collect(Collectors.toSet()));
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");
126 Set<PriceComponent> priceComponentsSet;
128 priceComponentsSet = new HashSet<PriceComponent>(
129 Arrays.stream(priceComponents.split(",")).map(PriceComponent::fromString).toList());
130 } catch (IllegalArgumentException e) {
131 logger.warn("{}", e.getMessage());
135 return getPrices(priceComponentsSet);
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());
146 return priceCalculator.calculatePrice(start, end, power);
147 } catch (MissingPriceException e) {
148 logger.warn("{}", e.getMessage());
149 return BigDecimal.ZERO;
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());
161 Map<String, Object> intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
162 duration, QuantityType.valueOf(1000, Units.WATT));
164 // Create new result with stripped price information.
165 Map<String, Object> result = new HashMap<>();
166 Object value = intermediateResult.get("CheapestStart");
168 result.put("CheapestStart", value);
170 value = intermediateResult.get("MostExpensiveStart");
172 result.put("MostExpensiveStart", value);
175 } catch (MissingPriceException | IllegalArgumentException e) {
176 logger.warn("{}", e.getMessage());
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());
190 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
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 = "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());
207 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
209 } catch (MissingPriceException | IllegalArgumentException e) {
210 logger.warn("{}", e.getMessage());
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());
226 PriceCalculator priceCalculator = new PriceCalculator(getPrices());
229 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
230 } catch (MissingPriceException | IllegalArgumentException e) {
231 logger.warn("{}", e.getMessage());
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.");
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());
250 prices = handler.getSpotPrices();
251 spotPricesRequired = true;
253 spotPricesRequired = false;
254 prices = new HashMap<>();
257 for (PriceComponent priceComponent : PriceComponent.values()) {
258 DatahubTariff datahubTariff = priceComponent.getDatahubTariff();
259 if (datahubTariff == null) {
263 if (priceComponents.contains(priceComponent)) {
264 Map<Instant, BigDecimal> tariffMap = handler.getTariffs(datahubTariff);
265 mergeMaps(prices, tariffMap, !spotPricesRequired);
272 private void mergeMaps(Map<Instant, BigDecimal> destinationMap, Map<Instant, BigDecimal> sourceMap,
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);
287 * Static get prices method for DSL rule compatibility.
290 * @param priceComponents Comma-separated list of price components to include in prices.
291 * @return Map of prices
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);
298 return serviceActions.getPrices();
301 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
306 * Static get prices method for DSL rule compatibility.
309 * @param start Start time
310 * @param end End time
311 * @param power Constant power consumption
312 * @return Map of prices
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;
319 if (actions instanceof EnergiDataServiceActions serviceActions) {
320 return serviceActions.calculatePrice(start, end, power);
322 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
332 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration);
334 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
345 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
347 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
359 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
362 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
373 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
375 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
380 public void setThingHandler(@Nullable ThingHandler handler) {
381 if (handler instanceof EnergiDataServiceHandler serviceHandler) {
382 this.handler = serviceHandler;
387 public @Nullable ThingHandler getThingHandler() {