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.osgi.service.component.annotations.Component;
49 import org.osgi.service.component.annotations.ServiceScope;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * {@link EnergiDataServiceActions} provides actions for getting energy data into a rule context.
56 * @author Jacob Laursen - Initial contribution
58 @Component(scope = ServiceScope.PROTOTYPE, service = EnergiDataServiceActions.class)
59 @ThingActionsScope(name = "energidataservice")
61 public class EnergiDataServiceActions implements ThingActions {
63 private final Logger logger = LoggerFactory.getLogger(EnergiDataServiceActions.class);
65 private @Nullable EnergiDataServiceHandler handler;
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);
75 private static final Map<String, PriceComponent> NAME_MAP = Stream.of(values())
76 .collect(Collectors.toMap(PriceComponent::toString, Function.identity()));
79 private @Nullable DatahubTariff datahubTariff;
81 private PriceComponent(String name, @Nullable DatahubTariff datahubTariff) {
83 this.datahubTariff = datahubTariff;
87 public String toString() {
91 public static PriceComponent fromString(final String name) {
92 PriceComponent myEnum = NAME_MAP.get(name.toLowerCase());
94 throw new IllegalArgumentException(String.format("'%s' has no corresponding value. Accepted values: %s",
95 name, Arrays.asList(values())));
100 public @Nullable DatahubTariff getDatahubTariff() {
101 return datahubTariff;
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.");
113 boolean isReducedElectricityTax = handler.isReducedElectricityTax();
115 return getPrices(Arrays.stream(PriceComponent.values())
116 .filter(component -> component != (isReducedElectricityTax ? PriceComponent.ELECTRICITY_TAX
117 : PriceComponent.REDUCED_ELECTRICITY_TAX))
118 .collect(Collectors.toSet()));
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");
129 Set<PriceComponent> priceComponentsSet;
131 priceComponentsSet = new HashSet<>(
132 Arrays.stream(priceComponents.split(",")).map(PriceComponent::fromString).toList());
133 } catch (IllegalArgumentException e) {
134 logger.warn("{}", e.getMessage());
138 return getPrices(priceComponentsSet);
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());
149 return priceCalculator.calculatePrice(start, end, power);
150 } catch (MissingPriceException e) {
151 logger.warn("{}", e.getMessage());
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());
164 Map<String, Object> intermediateResult = priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd,
165 duration, QuantityType.valueOf(1000, Units.WATT));
167 // Create new result with stripped price information.
168 Map<String, Object> result = new HashMap<>();
169 Object value = intermediateResult.get("CheapestStart");
171 result.put("CheapestStart", value);
173 value = intermediateResult.get("MostExpensiveStart");
175 result.put("MostExpensiveStart", value);
178 } catch (MissingPriceException | IllegalArgumentException e) {
179 logger.warn("{}", e.getMessage());
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());
193 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
194 } catch (MissingPriceException | IllegalArgumentException e) {
195 logger.warn("{}", e.getMessage());
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());
210 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
212 } catch (MissingPriceException | IllegalArgumentException e) {
213 logger.warn("{}", e.getMessage());
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());
229 PriceCalculator priceCalculator = new PriceCalculator(getPrices());
232 return priceCalculator.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
233 } catch (MissingPriceException | IllegalArgumentException e) {
234 logger.warn("{}", e.getMessage());
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.");
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());
253 prices = handler.getSpotPrices();
254 spotPricesRequired = true;
256 spotPricesRequired = false;
257 prices = new HashMap<>();
260 for (PriceComponent priceComponent : PriceComponent.values()) {
261 DatahubTariff datahubTariff = priceComponent.getDatahubTariff();
262 if (datahubTariff == null) {
266 if (priceComponents.contains(priceComponent)) {
267 Map<Instant, BigDecimal> tariffMap = handler.getTariffs(datahubTariff);
268 mergeMaps(prices, tariffMap, !spotPricesRequired);
275 private void mergeMaps(Map<Instant, BigDecimal> destinationMap, Map<Instant, BigDecimal> sourceMap,
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);
290 * Static get prices method for DSL rule compatibility.
293 * @param priceComponents Comma-separated list of price components to include in prices.
294 * @return Map of prices
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);
301 return serviceActions.getPrices();
304 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
309 * Static get prices method for DSL rule compatibility.
312 * @param start Start time
313 * @param end End time
314 * @param power Constant power consumption
315 * @return Map of prices
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) {
322 if (actions instanceof EnergiDataServiceActions serviceActions) {
323 return serviceActions.calculatePrice(start, end, power);
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 if (actions instanceof EnergiDataServiceActions serviceActions) {
332 if (earliestStart == null || latestEnd == null || duration == null) {
335 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration);
337 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
348 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, duration, power);
350 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
362 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, totalDuration, durationPhases,
365 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
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) {
376 return serviceActions.calculateCheapestPeriod(earliestStart, latestEnd, durationPhases, powerPhases);
378 throw new IllegalArgumentException("Instance is not an EnergiDataServiceActions class.");
383 public void setThingHandler(@Nullable ThingHandler handler) {
384 if (handler instanceof EnergiDataServiceHandler serviceHandler) {
385 this.handler = serviceHandler;
390 public @Nullable ThingHandler getThingHandler() {