diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java index e8ba50350..c413acb4a 100644 --- a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java @@ -38,8 +38,9 @@ public class RESTConstants { * Version 4: OH3, refactored extensions to addons (#1560) * Version 5: transparent charts (#2502) * Version 6: extended chart period parameter format (#3863) + * Version 7: extended chart period parameter format to cover past and future */ - public static final String API_VERSION = "6"; + public static final String API_VERSION = "7"; public static final CacheControl CACHE_CONTROL = new CacheControl(); static { diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext index 376798e47..37dad8bd6 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext @@ -80,7 +80,7 @@ Chart: (('icon=' icon=Icon) | ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | ('staticIcon=' staticIcon=Icon))? & - ('service=' service=STRING)? & ('refresh=' refresh=INT)? & ('period=' period=ID) & + ('service=' service=STRING)? & ('refresh=' refresh=INT)? & ('period=' period=Period) & ('legend=' legend=BOOLEAN_OBJECT)? & ('forceasitem=' forceAsItem=BOOLEAN_OBJECT)? & ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & @@ -222,6 +222,9 @@ Icon returns ecore::EString: IconName: (ID '-')* ID; +Period: + (ID '-')? ID; + ColorArray: ((conditions+=Condition ('AND' conditions+=Condition)*) '=')? (arg=STRING); diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java index cb03665c9..3dcc4c647 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java @@ -70,6 +70,7 @@ import org.slf4j.LoggerFactory; * @author Chris Jackson - Initial contribution * @author Holger Reichert - Support for themes, DPI, legend hiding * @author Laurent Garnier - Extend support to ISO8601 format for chart period parameter + * @author Laurent Garnier - Extend support to past and future for chart period parameter */ @Component(immediate = true, service = { ChartServlet.class, Servlet.class }, configurationPid = "org.openhab.chart", // property = Constants.SERVICE_PID + "=org.openhab.chart") @@ -100,7 +101,7 @@ public class ChartServlet extends HttpServlet { // The URI of this servlet public static final String SERVLET_PATH = "/chart"; - private static final Duration DEFAULT_PERIOD = Duration.ofDays(1); + protected static final Duration DEFAULT_PERIOD = Duration.ofDays(1); protected static final Map CHART_PROVIDERS = new ConcurrentHashMap<>(); @@ -216,6 +217,7 @@ public class ChartServlet extends HttpServlet { String periodParam = req.getParameter("period"); String timeBeginParam = req.getParameter("begin"); String timeEndParam = req.getParameter("end"); + // To avoid ambiguity you are not allowed to specify period, begin and end time at the same time. if (periodParam != null && timeBeginParam != null && timeEndParam != null) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, @@ -223,8 +225,6 @@ public class ChartServlet extends HttpServlet { return; } - // Read out the parameter period, begin and end and save them. - TemporalAmount period = convertToTemporalAmount(periodParam, DEFAULT_PERIOD); ZonedDateTime timeBegin = null; ZonedDateTime timeEnd = null; @@ -248,18 +248,11 @@ public class ChartServlet extends HttpServlet { } } - // Set begin and end time and check legality. - if (timeBegin == null && timeEnd == null) { - timeEnd = ZonedDateTime.now(timeZoneProvider.getTimeZone()); - timeBegin = timeEnd.minus(period); - logger.debug("No begin or end is specified, use now as end and now-period as begin."); - } else if (timeEnd == null) { - timeEnd = timeBegin.plus(period); - logger.debug("No end is specified, use begin + period as end."); - } else if (timeBegin == null) { - timeBegin = timeEnd.minus(period); - logger.debug("No begin is specified, use end-period as begin"); - } else if (timeEnd.isBefore(timeBegin)) { + PeriodPastFuture period = getPeriodPastFuture(periodParam); + PeriodBeginEnd beginEnd = getPeriodBeginEnd(timeBegin, timeEnd, period, + ZonedDateTime.now(timeZoneProvider.getTimeZone())); + + if (beginEnd.begin() != null && beginEnd.end() != null && beginEnd.end().isBefore(beginEnd.begin())) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "The end is before the begin."); return; } @@ -308,9 +301,9 @@ public class ChartServlet extends HttpServlet { logger.debug("chart building with width {} height {} dpi {}", width, height, dpi); try { - BufferedImage chart = provider.createChart(serviceName, req.getParameter("theme"), timeBegin, timeEnd, - height, width, req.getParameter("items"), req.getParameter("groups"), dpi, yAxisDecimalPattern, - legend); + BufferedImage chart = provider.createChart(serviceName, req.getParameter("theme"), beginEnd.begin(), + beginEnd.end(), height, width, req.getParameter("items"), req.getParameter("groups"), dpi, + yAxisDecimalPattern, legend); // Set the content type to that provided by the chart provider res.setContentType("image/" + provider.getChartType()); try (ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(res.getOutputStream())) { @@ -351,7 +344,8 @@ public class ChartServlet extends HttpServlet { public void destroy() { } - public static TemporalAmount convertToTemporalAmount(@Nullable String periodParam, TemporalAmount defaultPeriod) { + protected static @Nullable TemporalAmount convertToTemporalAmount(@Nullable String periodParam, + @Nullable TemporalAmount defaultPeriod) { TemporalAmount period = defaultPeriod; String convertedPeriod = convertPeriodToISO8601(periodParam); if (convertedPeriod != null) { @@ -383,4 +377,70 @@ public class ChartServlet extends HttpServlet { } return "P" + newPeriod; } + + protected static PeriodPastFuture getPeriodPastFuture(@Nullable String periodParam) { + String periodParamPast = null; + String periodParamFuture = null; + TemporalAmount defaultPeriodPast = DEFAULT_PERIOD; + TemporalAmount defaultPeriodFuture = null; + if (periodParam != null) { + int idx = periodParam.indexOf("-"); + if (idx < 0) { + periodParamPast = periodParam; + } else { + if (idx > 0) { + periodParamPast = periodParam.substring(0, idx); + } else { + defaultPeriodPast = null; + defaultPeriodFuture = DEFAULT_PERIOD; + } + periodParamFuture = periodParam.substring(idx + 1); + } + } + TemporalAmount periodPast = convertToTemporalAmount(periodParamPast, defaultPeriodPast); + TemporalAmount periodFuture = convertToTemporalAmount(periodParamFuture, defaultPeriodFuture); + return new PeriodPastFuture(periodPast, periodFuture); + } + + protected static PeriodBeginEnd getPeriodBeginEnd(@Nullable ZonedDateTime begin, @Nullable ZonedDateTime end, + PeriodPastFuture period, ZonedDateTime now) { + ZonedDateTime timeBegin = begin; + ZonedDateTime timeEnd = end; + TemporalAmount periodPast = period.past(); + TemporalAmount periodFuture = period.future(); + + if (timeBegin == null && timeEnd == null) { + timeBegin = timeEnd = now; + if (periodPast != null) { + timeBegin = timeBegin.minus(periodPast); + } + if (periodFuture != null) { + timeEnd = timeEnd.plus(periodFuture); + } + } else if (timeBegin != null && timeEnd == null) { + timeEnd = timeBegin; + if (periodPast != null) { + timeEnd = timeEnd.plus(periodPast); + } + if (periodFuture != null) { + timeEnd = timeEnd.plus(periodFuture); + } + } else if (timeBegin == null && timeEnd != null) { + timeBegin = timeEnd; + if (periodFuture != null) { + timeBegin = timeBegin.minus(periodFuture); + } + if (periodPast != null) { + timeBegin = timeBegin.minus(periodPast); + } + } + + return new PeriodBeginEnd(Objects.requireNonNull(timeBegin), Objects.requireNonNull(timeEnd)); + } + + record PeriodPastFuture(@Nullable TemporalAmount past, @Nullable TemporalAmount future) { + } + + record PeriodBeginEnd(ZonedDateTime begin, ZonedDateTime end) { + } } diff --git a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java index 716afcb37..21ce9197a 100644 --- a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java +++ b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/chart/ChartServletPeriodParamTest.java @@ -12,14 +12,19 @@ */ package org.openhab.core.ui.internal.chart; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; import java.time.Duration; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAmount; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; +import org.openhab.core.ui.internal.chart.ChartServlet.PeriodBeginEnd; +import org.openhab.core.ui.internal.chart.ChartServlet.PeriodPastFuture; /** * @author Laurent Garnier - Initial contribution @@ -30,26 +35,31 @@ public class ChartServletPeriodParamTest { @Test public void convertToTemporalAmountFromNull() { TemporalAmount period = ChartServlet.convertToTemporalAmount(null, Duration.ZERO); + assertNotNull(period); assertEquals(0, period.get(ChronoUnit.SECONDS)); } @Test public void convertToTemporalAmountFromHours() { TemporalAmount period = ChartServlet.convertToTemporalAmount("h", Duration.ZERO); + assertNotNull(period); assertEquals(1 * 60 * 60, period.get(ChronoUnit.SECONDS)); period = ChartServlet.convertToTemporalAmount("12h", Duration.ZERO); + assertNotNull(period); assertEquals(12 * 60 * 60, period.get(ChronoUnit.SECONDS)); } @Test public void convertToTemporalAmountFromDays() { TemporalAmount period = ChartServlet.convertToTemporalAmount("D", Duration.ZERO); + assertNotNull(period); assertEquals(1, period.get(ChronoUnit.DAYS)); assertEquals(0, period.get(ChronoUnit.MONTHS)); assertEquals(0, period.get(ChronoUnit.YEARS)); period = ChartServlet.convertToTemporalAmount("4D", Duration.ZERO); + assertNotNull(period); assertEquals(4, period.get(ChronoUnit.DAYS)); assertEquals(0, period.get(ChronoUnit.MONTHS)); assertEquals(0, period.get(ChronoUnit.YEARS)); @@ -58,11 +68,13 @@ public class ChartServletPeriodParamTest { @Test public void convertToTemporalAmountFromWeeks() { TemporalAmount period = ChartServlet.convertToTemporalAmount("W", Duration.ZERO); + assertNotNull(period); assertEquals(7, period.get(ChronoUnit.DAYS)); assertEquals(0, period.get(ChronoUnit.MONTHS)); assertEquals(0, period.get(ChronoUnit.YEARS)); period = ChartServlet.convertToTemporalAmount("2W", Duration.ZERO); + assertNotNull(period); assertEquals(14, period.get(ChronoUnit.DAYS)); assertEquals(0, period.get(ChronoUnit.MONTHS)); assertEquals(0, period.get(ChronoUnit.YEARS)); @@ -71,11 +83,13 @@ public class ChartServletPeriodParamTest { @Test public void convertToTemporalAmountFromMonths() { TemporalAmount period = ChartServlet.convertToTemporalAmount("M", Duration.ZERO); + assertNotNull(period); assertEquals(0, period.get(ChronoUnit.DAYS)); assertEquals(1, period.get(ChronoUnit.MONTHS)); assertEquals(0, period.get(ChronoUnit.YEARS)); period = ChartServlet.convertToTemporalAmount("3M", Duration.ZERO); + assertNotNull(period); assertEquals(0, period.get(ChronoUnit.DAYS)); assertEquals(3, period.get(ChronoUnit.MONTHS)); assertEquals(0, period.get(ChronoUnit.YEARS)); @@ -84,11 +98,13 @@ public class ChartServletPeriodParamTest { @Test public void convertToTemporalAmountFromYears() { TemporalAmount period = ChartServlet.convertToTemporalAmount("Y", Duration.ZERO); + assertNotNull(period); assertEquals(0, period.get(ChronoUnit.DAYS)); assertEquals(0, period.get(ChronoUnit.MONTHS)); assertEquals(1, period.get(ChronoUnit.YEARS)); period = ChartServlet.convertToTemporalAmount("2Y", Duration.ZERO); + assertNotNull(period); assertEquals(0, period.get(ChronoUnit.DAYS)); assertEquals(0, period.get(ChronoUnit.MONTHS)); assertEquals(2, period.get(ChronoUnit.YEARS)); @@ -97,11 +113,131 @@ public class ChartServletPeriodParamTest { @Test public void convertToTemporalAmountFromISO8601() { TemporalAmount period = ChartServlet.convertToTemporalAmount("P2Y3M4D", Duration.ZERO); + assertNotNull(period); assertEquals(4, period.get(ChronoUnit.DAYS)); assertEquals(3, period.get(ChronoUnit.MONTHS)); assertEquals(2, period.get(ChronoUnit.YEARS)); period = ChartServlet.convertToTemporalAmount("P1DT12H30M15S", Duration.ZERO); + assertNotNull(period); assertEquals(36 * 60 * 60 + 30 * 60 + 15, period.get(ChronoUnit.SECONDS)); } + + @Test + public void getPeriodPastFutureByDefault() { + PeriodPastFuture period = ChartServlet.getPeriodPastFuture(null); + assertNotNull(period.past()); + assertEquals(ChartServlet.DEFAULT_PERIOD, period.past()); + assertNull(period.future()); + + period = ChartServlet.getPeriodPastFuture("-"); + assertNull(period.past()); + assertNotNull(period.future()); + assertEquals(ChartServlet.DEFAULT_PERIOD, period.future()); + } + + @Test + public void getPeriodPastFutureWithOnlyPast() { + Period duration = Period.ofDays(2); + + PeriodPastFuture period = ChartServlet.getPeriodPastFuture("2D"); + assertNotNull(period.past()); + assertEquals(duration, period.past()); + assertNull(period.future()); + + period = ChartServlet.getPeriodPastFuture("2D-"); + assertNotNull(period.past()); + assertEquals(duration, period.past()); + assertNull(period.future()); + } + + @Test + public void getPeriodPastFutureWithOnlyFuture() { + Period duration = Period.ofMonths(3); + + PeriodPastFuture period = ChartServlet.getPeriodPastFuture("-3M"); + assertNull(period.past()); + assertNotNull(period.future()); + assertEquals(duration, period.future()); + } + + @Test + public void getPeriodPastFutureWithPastAndFuture() { + Period duration1 = Period.ofDays(2); + Period duration2 = Period.ofMonths(3); + + PeriodPastFuture period = ChartServlet.getPeriodPastFuture("2D-3M"); + assertNotNull(period.past()); + assertEquals(duration1, period.past()); + assertNotNull(period.future()); + assertEquals(duration2, period.future()); + } + + @Test + public void getPeriodBeginEndWithBeginAndEnd() { + ZonedDateTime now = ZonedDateTime.of(2024, 4, 9, 12, 0, 0, 0, ZoneId.systemDefault()); + ZonedDateTime begin = ZonedDateTime.of(2024, 4, 9, 11, 30, 0, 0, ZoneId.systemDefault()); + ZonedDateTime end = ZonedDateTime.of(2024, 4, 9, 13, 30, 0, 0, ZoneId.systemDefault()); + + PeriodBeginEnd beginEnd = ChartServlet.getPeriodBeginEnd(begin, end, ChartServlet.getPeriodPastFuture("2D-3M"), + now); + assertEquals(begin, beginEnd.begin()); + assertEquals(end, beginEnd.end()); + } + + @Test + public void getPeriodBeginEndWithBeginButNotEnd() { + ZonedDateTime now = ZonedDateTime.of(2024, 4, 9, 12, 0, 0, 0, ZoneId.systemDefault()); + ZonedDateTime begin = ZonedDateTime.of(2024, 4, 9, 11, 30, 0, 0, ZoneId.systemDefault()); + + PeriodBeginEnd beginEnd = ChartServlet.getPeriodBeginEnd(begin, null, ChartServlet.getPeriodPastFuture("2D-3M"), + now); + assertEquals(begin, beginEnd.begin()); + assertEquals(ZonedDateTime.of(2024, 7, 11, 11, 30, 0, 0, ZoneId.systemDefault()), beginEnd.end()); + + beginEnd = ChartServlet.getPeriodBeginEnd(begin, null, ChartServlet.getPeriodPastFuture("2D"), now); + assertEquals(begin, beginEnd.begin()); + assertEquals(ZonedDateTime.of(2024, 4, 11, 11, 30, 0, 0, ZoneId.systemDefault()), beginEnd.end()); + + beginEnd = ChartServlet.getPeriodBeginEnd(begin, null, ChartServlet.getPeriodPastFuture("-3M"), now); + assertEquals(begin, beginEnd.begin()); + assertEquals(ZonedDateTime.of(2024, 7, 9, 11, 30, 0, 0, ZoneId.systemDefault()), beginEnd.end()); + } + + @Test + public void getPeriodBeginEndWithEndButNotBegin() { + ZonedDateTime now = ZonedDateTime.of(2024, 4, 9, 12, 0, 0, 0, ZoneId.systemDefault()); + ZonedDateTime end = ZonedDateTime.of(2024, 7, 11, 11, 30, 0, 0, ZoneId.systemDefault()); + + PeriodBeginEnd beginEnd = ChartServlet.getPeriodBeginEnd(null, end, ChartServlet.getPeriodPastFuture("2D-3M"), + now); + assertEquals(ZonedDateTime.of(2024, 4, 9, 11, 30, 0, 0, ZoneId.systemDefault()), beginEnd.begin()); + assertEquals(end, beginEnd.end()); + + beginEnd = ChartServlet.getPeriodBeginEnd(null, end, ChartServlet.getPeriodPastFuture("2D"), now); + assertEquals(ZonedDateTime.of(2024, 7, 9, 11, 30, 0, 0, ZoneId.systemDefault()), beginEnd.begin()); + assertEquals(end, beginEnd.end()); + + beginEnd = ChartServlet.getPeriodBeginEnd(null, end, ChartServlet.getPeriodPastFuture("-3M"), now); + assertEquals(ZonedDateTime.of(2024, 4, 11, 11, 30, 0, 0, ZoneId.systemDefault()), beginEnd.begin()); + assertEquals(end, beginEnd.end()); + } + + @Test + public void getPeriodBeginEndWithPeriodButNotBeginEnd() { + ZonedDateTime now = ZonedDateTime.of(2024, 4, 9, 12, 0, 0, 0, ZoneId.systemDefault()); + + PeriodBeginEnd beginEnd = ChartServlet.getPeriodBeginEnd(null, null, ChartServlet.getPeriodPastFuture("2D-3M"), + now); + assertEquals(ZonedDateTime.of(2024, 4, 7, 12, 0, 0, 0, ZoneId.systemDefault()), beginEnd.begin()); + assertEquals(ZonedDateTime.of(2024, 7, 9, 12, 0, 0, 0, ZoneId.systemDefault()), beginEnd.end()); + + beginEnd = ChartServlet.getPeriodBeginEnd(null, null, ChartServlet.getPeriodPastFuture("2D"), now); + assertEquals(ZonedDateTime.of(2024, 4, 7, 12, 0, 0, 0, ZoneId.systemDefault()), beginEnd.begin()); + assertEquals(ZonedDateTime.of(2024, 4, 9, 12, 0, 0, 0, ZoneId.systemDefault()), beginEnd.end()); + + beginEnd = ChartServlet.getPeriodBeginEnd(null, null, ChartServlet.getPeriodPastFuture("-3M"), now); + assertEquals(ZonedDateTime.of(2024, 4, 9, 12, 0, 0, 0, ZoneId.systemDefault()), beginEnd.begin()); + assertEquals(ZonedDateTime.of(2024, 7, 9, 12, 0, 0, 0, ZoneId.systemDefault()), beginEnd.end()); + } }