diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java index ef4be6eb8..70fb22582 100644 --- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/RuleResource.java @@ -25,6 +25,7 @@ import java.util.Date; import java.util.List; import java.util.Map; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Stream; import javax.annotation.security.RolesAllowed; @@ -76,6 +77,7 @@ import org.openhab.core.common.registry.RegistryChangedRunnableListener; import org.openhab.core.config.core.ConfigUtil; import org.openhab.core.config.core.Configuration; import org.openhab.core.events.Event; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.DTOMapper; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.RESTConstants; @@ -133,6 +135,7 @@ public class RuleResource implements RESTResource { private final RuleManager ruleManager; private final RuleRegistry ruleRegistry; private final ManagedRuleProvider managedRuleProvider; + private final TimeZoneProvider timeZoneProvider; private final RegistryChangedRunnableListener resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>( () -> lastModified = null); @@ -144,11 +147,13 @@ public class RuleResource implements RESTResource { final @Reference DTOMapper dtoMapper, // final @Reference RuleManager ruleManager, // final @Reference RuleRegistry ruleRegistry, // - final @Reference ManagedRuleProvider managedRuleProvider) { + final @Reference ManagedRuleProvider managedRuleProvider, // + final @Reference TimeZoneProvider timeZoneProvider) { this.dtoMapper = dtoMapper; this.ruleManager = ruleManager; this.ruleRegistry = ruleRegistry; this.managedRuleProvider = managedRuleProvider; + this.timeZoneProvider = timeZoneProvider; this.ruleRegistry.addRegistryChangeListener(resetLastModifiedChangeListener); } @@ -419,10 +424,10 @@ public class RuleResource implements RESTResource { + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]") @QueryParam("from") @Nullable String from, @Parameter(description = "End time of the simulated rule executions. Will default to 30 days after the start time. Must be less than 180 days after the given start time. [" + DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]") @QueryParam("until") @Nullable String until) { - final ZonedDateTime fromDate = from == null || from.isEmpty() ? ZonedDateTime.now() : parseTime(from); - final ZonedDateTime untilDate = until == null || until.isEmpty() ? fromDate.plusDays(31) : parseTime(until); + final ZonedDateTime fromDate = parseTime(from, ZonedDateTime::now); + final ZonedDateTime untilDate = parseTime(until, () -> fromDate.plusDays(31)); - if (daysBetween(fromDate, untilDate) >= 180) { + if (ChronoUnit.DAYS.between(fromDate, untilDate) >= 180) { return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "Simulated time span must be smaller than 180 days."); } @@ -431,13 +436,12 @@ public class RuleResource implements RESTResource { return Response.ok(ruleExecutions.toList()).build(); } - private static ZonedDateTime parseTime(String sTime) { + private ZonedDateTime parseTime(@Nullable String sTime, Supplier defaultSupplier) { + if (sTime == null || sTime.isEmpty()) { + return defaultSupplier.get(); + } final DateTimeType dateTime = new DateTimeType(sTime); - return dateTime.getZonedDateTime(); - } - - private static long daysBetween(ZonedDateTime d1, ZonedDateTime d2) { - return ChronoUnit.DAYS.between(d1, d2); + return dateTime.getZonedDateTime(timeZoneProvider.getTimeZone()); } @GET diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java index 71bc3eb51..a31c72a5a 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/DateTimeTriggerHandler.java @@ -178,8 +178,7 @@ public class DateTimeTriggerHandler extends BaseTriggerModuleHandler cronExpression = CronAdjuster.REBOOT; } else if (value instanceof DateTimeType dateTimeType) { boolean itemIsTimeOnly = dateTimeType.toString().startsWith("1970-01-01T"); - cronExpression = dateTimeType.getZonedDateTime().withZoneSameInstant(ZoneId.systemDefault()) - .plusSeconds(offset.longValue()) + cronExpression = dateTimeType.getZonedDateTime(ZoneId.systemDefault()).plusSeconds(offset.longValue()) .format(timeOnly || itemIsTimeOnly ? CRON_TIMEONLY_FORMATTER : CRON_FORMATTER); startScheduler(); } else { diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java index dbb929a2c..b5c6db632 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/ItemStateConditionHandler.java @@ -13,6 +13,7 @@ package org.openhab.core.automation.internal.module.handler; import java.time.Duration; +import java.time.Instant; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -158,9 +159,9 @@ public class ItemStateConditionHandler extends BaseConditionModuleHandler implem Item item = itemRegistry.getItem(itemName); State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state); State itemState = item.getState(); - if (itemState instanceof DateTimeType type) { - ZonedDateTime itemTime = type.getZonedDateTime(); - ZonedDateTime compareTime = getCompareTime(state); + if (itemState instanceof DateTimeType dateTimeState) { + Instant itemTime = dateTimeState.getInstant(); + Instant compareTime = getCompareTime(state); return itemTime.compareTo(compareTime) <= 0; } else if (itemState instanceof QuantityType qtState) { if (compareState instanceof DecimalType type) { @@ -195,9 +196,9 @@ public class ItemStateConditionHandler extends BaseConditionModuleHandler implem Item item = itemRegistry.getItem(itemName); State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state); State itemState = item.getState(); - if (itemState instanceof DateTimeType type) { - ZonedDateTime itemTime = type.getZonedDateTime(); - ZonedDateTime compareTime = getCompareTime(state); + if (itemState instanceof DateTimeType dateTimeState) { + Instant itemTime = dateTimeState.getInstant(); + Instant compareTime = getCompareTime(state); return itemTime.compareTo(compareTime) >= 0; } else if (itemState instanceof QuantityType qtState) { if (compareState instanceof DecimalType type) { @@ -252,36 +253,36 @@ public class ItemStateConditionHandler extends BaseConditionModuleHandler implem eventSubscriberRegistration.unregister(); } - private ZonedDateTime getCompareTime(String input) { + private Instant getCompareTime(String input) { if (input.isBlank()) { // no parameter given, use now - return ZonedDateTime.now(); + return Instant.now(); } try { - return ZonedDateTime.parse(input); + return ZonedDateTime.parse(input).toInstant(); } catch (DateTimeParseException ignored) { } try { return LocalDateTime.parse(input, DateTimeFormatter.ISO_LOCAL_DATE_TIME) - .atZone(timeZoneProvider.getTimeZone()); + .atZone(timeZoneProvider.getTimeZone()).toInstant(); } catch (DateTimeParseException ignored) { } try { int dayPosition = input.indexOf("D"); if (dayPosition == -1) { // no date in string, add period symbol and time separator - return ZonedDateTime.now().plus(Duration.parse("PT" + input)); + return Instant.now().plus(Duration.parse("PT" + input)); } else if (dayPosition == input.length() - 1) { // day is the last symbol, only add the period symbol - return ZonedDateTime.now().plus(Duration.parse("P" + input)); + return Instant.now().plus(Duration.parse("P" + input)); } else { // add period symbol and time separator - return ZonedDateTime.now().plus(Duration + return Instant.now().plus(Duration .parse("P" + input.substring(0, dayPosition + 1) + "T" + input.substring(dayPosition + 1))); } } catch (DateTimeParseException e) { logger.warn("Couldn't get a comparable time from '{}', using now", input); } - return ZonedDateTime.now(); + return Instant.now(); } } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java index ebe4a1b05..9d8e666a0 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java @@ -13,6 +13,7 @@ package org.openhab.core.io.rest.core.internal.item; import java.time.Instant; +import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; @@ -57,6 +58,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.auth.Role; import org.openhab.core.common.registry.RegistryChangedRunnableListener; import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.DTOMapper; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.LocaleService; @@ -180,6 +182,7 @@ public class ItemResource implements RESTResource { private final MetadataRegistry metadataRegistry; private final MetadataSelectorMatcher metadataSelectorMatcher; private final SemanticTagRegistry semanticTagRegistry; + private final TimeZoneProvider timeZoneProvider; private final RegistryChangedRunnableListener resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>( () -> lastModified = null); @@ -198,7 +201,8 @@ public class ItemResource implements RESTResource { final @Reference ManagedItemProvider managedItemProvider, final @Reference MetadataRegistry metadataRegistry, final @Reference MetadataSelectorMatcher metadataSelectorMatcher, - final @Reference SemanticTagRegistry semanticTagRegistry) { + final @Reference SemanticTagRegistry semanticTagRegistry, + final @Reference TimeZoneProvider timeZoneProvider) { this.dtoMapper = dtoMapper; this.eventPublisher = eventPublisher; this.itemBuilderFactory = itemBuilderFactory; @@ -208,6 +212,7 @@ public class ItemResource implements RESTResource { this.metadataRegistry = metadataRegistry; this.metadataSelectorMatcher = metadataSelectorMatcher; this.semanticTagRegistry = semanticTagRegistry; + this.timeZoneProvider = timeZoneProvider; this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener); this.metadataRegistry.addRegistryChangeListener(resetLastModifiedMetadataChangeListener); @@ -240,6 +245,7 @@ public class ItemResource implements RESTResource { @QueryParam("fields") @Parameter(description = "limit output to the given fields (comma separated)") @Nullable String fields, @DefaultValue("false") @QueryParam("staticDataOnly") @Parameter(description = "provides a cacheable list of values not expected to change regularly and checks the If-Modified-Since header, all other parameters are ignored except \"metadata\"") boolean staticDataOnly) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); final Set namespaces = splitAndFilterNamespaces(namespaceSelector, locale); final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders); @@ -256,7 +262,7 @@ public class ItemResource implements RESTResource { } Stream itemStream = getItems(type, tags).stream() // - .map(item -> EnrichedItemDTOMapper.map(item, false, null, uriBuilder, locale)) // + .map(item -> EnrichedItemDTOMapper.map(item, false, null, uriBuilder, locale, zoneId)) // .peek(dto -> addMetadata(dto, namespaces, null)) // .peek(dto -> dto.editable = isEditable(dto.name)); itemStream = dtoMapper.limitToFields(itemStream, @@ -267,7 +273,7 @@ public class ItemResource implements RESTResource { } Stream itemStream = getItems(type, tags).stream() // - .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale)) // + .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale, zoneId)) // .peek(dto -> addMetadata(dto, namespaces, null)) // .peek(dto -> dto.editable = isEditable(dto.name)) // .peek(dto -> { @@ -318,6 +324,7 @@ public class ItemResource implements RESTResource { @DefaultValue("true") @QueryParam("recursive") @Parameter(description = "get member items if the item is a group item") boolean recursive, @PathParam("itemname") @Parameter(description = "item name") String itemname) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); final Set namespaces = splitAndFilterNamespaces(namespaceSelector, locale); // get item @@ -326,7 +333,7 @@ public class ItemResource implements RESTResource { // if it exists if (item != null) { EnrichedItemDTO dto = EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder(uriInfo, httpHeaders), - locale); + locale, zoneId); addMetadata(dto, namespaces, null); dto.editable = isEditable(dto.name); if (dto instanceof EnrichedGroupItemDTO enrichedGroupItemDTO) { @@ -424,6 +431,7 @@ public class ItemResource implements RESTResource { @PathParam("itemname") @Parameter(description = "item name") String itemname, @Parameter(description = "valid item state (e.g. ON, OFF)", required = true) String value) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); // get Item Item item = getItem(itemname); @@ -436,7 +444,7 @@ public class ItemResource implements RESTResource { if (state != null) { // set State and report OK eventPublisher.post(ItemEventFactory.createStateEvent(itemname, state)); - return getItemResponse(null, Status.ACCEPTED, null, locale, null); + return getItemResponse(null, Status.ACCEPTED, null, locale, zoneId, null); } else { // State could not be parsed return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "State could not be parsed: " + value); @@ -739,6 +747,7 @@ public class ItemResource implements RESTResource { @PathParam("itemname") @Parameter(description = "item name") String itemname, @Parameter(description = "item data", required = true) @Nullable GroupItemDTO item) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); // If we didn't get an item bean, then return! if (item == null) { @@ -763,12 +772,12 @@ public class ItemResource implements RESTResource { // item does not yet exist, create it managedItemProvider.add(newItem); return getItemResponse(uriBuilder(uriInfo, httpHeaders), Status.CREATED, itemRegistry.get(itemname), - locale, null); + locale, zoneId, null); } else if (managedItemProvider.get(itemname) != null) { // item already exists as a managed item, update it managedItemProvider.update(newItem); return getItemResponse(uriBuilder(uriInfo, httpHeaders), Status.OK, itemRegistry.get(itemname), locale, - null); + zoneId, null); } else { // Item exists but cannot be updated logger.warn("Cannot update existing item '{}', because is not managed.", itemname); @@ -872,7 +881,8 @@ public class ItemResource implements RESTResource { @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @PathParam("itemName") @Parameter(description = "item name") String itemName, @PathParam("semanticClass") @Parameter(description = "semantic class") String semanticClassName) { - Locale locale = localeService.getLocale(language); + final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); Class semanticClass = semanticTagRegistry .getTagClassById(semanticClassName); @@ -886,7 +896,7 @@ public class ItemResource implements RESTResource { } EnrichedItemDTO dto = EnrichedItemDTOMapper.map(foundItem, false, null, uriBuilder(uriInfo, httpHeaders), - locale); + locale, zoneId); dto.editable = isEditable(dto.name); return JSONResponse.createResponse(Status.OK, dto, null); } @@ -935,8 +945,8 @@ public class ItemResource implements RESTResource { * @return Response configured to represent the Item in depending on the status */ private Response getItemResponse(final @Nullable UriBuilder uriBuilder, Status status, @Nullable Item item, - Locale locale, @Nullable String errormessage) { - Object entity = null != item ? EnrichedItemDTOMapper.map(item, true, null, uriBuilder, locale) : null; + Locale locale, ZoneId zoneId, @Nullable String errormessage) { + Object entity = null != item ? EnrichedItemDTOMapper.map(item, true, null, uriBuilder, locale, zoneId) : null; return JSONResponse.createResponse(status, entity, errormessage); } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java index 4c7143996..8564a8333 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java @@ -340,7 +340,7 @@ public class PersistenceResource implements RESTResource { private ZonedDateTime convertTime(String sTime) { DateTimeType dateTime = new DateTimeType(sTime); - return dateTime.getZonedDateTime(); + return dateTime.getZonedDateTime(timeZoneProvider.getTimeZone()); } private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, @Nullable String timeBegin, diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java index 487083e09..1cc0e4019 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java @@ -12,6 +12,7 @@ */ package org.openhab.core.io.rest.core.item; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -29,7 +30,9 @@ import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.dto.ItemDTO; import org.openhab.core.items.dto.ItemDTOMapper; +import org.openhab.core.library.items.DateTimeItem; import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.transform.TransformationException; import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationService; @@ -63,28 +66,39 @@ public class EnrichedItemDTOMapper { * @param uriBuilder if present the URI builder contains one template that will be replaced by the specific item * name * @param locale locale (can be null) + * @param zoneId time-zone id (can be null) * @return item DTO object */ public static EnrichedItemDTO map(Item item, boolean drillDown, @Nullable Predicate itemFilter, - @Nullable UriBuilder uriBuilder, @Nullable Locale locale) { + @Nullable UriBuilder uriBuilder, @Nullable Locale locale, @Nullable ZoneId zoneId) { ItemDTO itemDTO = ItemDTOMapper.map(item); - return map(item, itemDTO, drillDown, itemFilter, uriBuilder, locale, new ArrayList<>()); + return map(item, itemDTO, drillDown, itemFilter, uriBuilder, locale, zoneId, new ArrayList<>()); } private static EnrichedItemDTO mapRecursive(Item item, @Nullable Predicate itemFilter, - @Nullable UriBuilder uriBuilder, @Nullable Locale locale, List parents) { + @Nullable UriBuilder uriBuilder, @Nullable Locale locale, @Nullable ZoneId zoneId, List parents) { ItemDTO itemDTO = ItemDTOMapper.map(item); - return map(item, itemDTO, true, itemFilter, uriBuilder, locale, parents); + return map(item, itemDTO, true, itemFilter, uriBuilder, locale, zoneId, parents); } private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, boolean drillDown, @Nullable Predicate itemFilter, @Nullable UriBuilder uriBuilder, @Nullable Locale locale, - List parents) { + @Nullable ZoneId zoneId, List parents) { if (item instanceof GroupItem) { // only add as parent item if it is a group, otherwise duplicate memberships trigger false warnings parents.add(item); } - String state = item.getState().toFullString(); + String state; + if (item instanceof DateTimeItem dateTimeItem && zoneId != null) { + DateTimeType dateTime = dateTimeItem.getStateAs(DateTimeType.class); + if (dateTime == null) { + state = item.getState().toFullString(); + } else { + state = dateTime.toFullString(zoneId); + } + } else { + state = item.getState().toFullString(); + } String transformedState = considerTransformation(item, locale); if (state.equals(transformedState)) { transformedState = null; @@ -117,7 +131,8 @@ public class EnrichedItemDTOMapper { "Recursive group membership found: {} is a member of {}, but it is also one of its ancestors.", member.getName(), groupItem.getName()); } else if (itemFilter == null || itemFilter.test(member)) { - members.add(mapRecursive(member, itemFilter, uriBuilder, locale, new ArrayList<>(parents))); + members.add( + mapRecursive(member, itemFilter, uriBuilder, locale, zoneId, new ArrayList<>(parents))); } } memberDTOs = members.toArray(new EnrichedItemDTO[0]); diff --git a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java index 85af584aa..4fd1695c8 100644 --- a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java +++ b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java @@ -55,31 +55,32 @@ public class EnrichedItemDTOMapperTest extends JavaTest { subGroup.addMember(stringItem); } - EnrichedGroupItemDTO dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, false, null, null, null); + EnrichedGroupItemDTO dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, false, null, null, null, + null); assertThat(dto.members.length, is(0)); - dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, null, null, null); + dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, null, null, null, null); assertThat(dto.members.length, is(3)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(1)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, - i -> CoreItemFactory.NUMBER.equals(i.getType()), null, null); + i -> CoreItemFactory.NUMBER.equals(i.getType()), null, null, null); assertThat(dto.members.length, is(1)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, - i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null); + i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null, null); assertThat(dto.members.length, is(2)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(0)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, - i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null); + i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null, null); assertThat(dto.members.length, is(2)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(0)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, i -> CoreItemFactory.NUMBER.equals(i.getType()) || i.getType().equals(CoreItemFactory.STRING) || i instanceof GroupItem, - null, null); + null, null, null); assertThat(dto.members.length, is(2)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(1)); } @@ -92,7 +93,7 @@ public class EnrichedItemDTOMapperTest extends JavaTest { groupItem1.addMember(groupItem2); groupItem2.addMember(groupItem1); - assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null)); + assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null)); assertLogMessage(EnrichedItemDTOMapper.class, LogLevel.ERROR, "Recursive group membership found: group1 is a member of group2, but it is also one of its ancestors."); @@ -108,7 +109,7 @@ public class EnrichedItemDTOMapperTest extends JavaTest { groupItem2.addMember(groupItem3); groupItem3.addMember(groupItem1); - assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null)); + assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null)); assertLogMessage(EnrichedItemDTOMapper.class, LogLevel.ERROR, "Recursive group membership found: group1 is a member of group3, but it is also one of its ancestors."); @@ -124,7 +125,7 @@ public class EnrichedItemDTOMapperTest extends JavaTest { groupItem1.addMember(numberItem); groupItem2.addMember(numberItem); - EnrichedItemDTOMapper.map(groupItem1, true, null, null, null); + EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null); assertNoLogMessage(EnrichedItemDTOMapper.class); } @@ -139,7 +140,7 @@ public class EnrichedItemDTOMapperTest extends JavaTest { groupItem1.addMember(groupItem3); groupItem2.addMember(groupItem3); - EnrichedItemDTOMapper.map(groupItem1, true, null, null, null); + EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null); assertNoLogMessage(EnrichedItemDTOMapper.class); } diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java index 4c5e7e47e..c5e831464 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/SitemapSubscriptionService.java @@ -30,6 +30,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.events.Event; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.sitemap.internal.SitemapEvent; import org.openhab.core.io.rest.sitemap.internal.WidgetsChangeListener; import org.openhab.core.items.GroupItem; @@ -87,6 +88,7 @@ public class SitemapSubscriptionService implements ModelRepositoryChangeListener } private final ItemUIRegistry itemUIRegistry; + private final TimeZoneProvider timeZoneProvider; private final List sitemapProviders = new ArrayList<>(); @@ -107,8 +109,9 @@ public class SitemapSubscriptionService implements ModelRepositoryChangeListener @Activate public SitemapSubscriptionService(Map config, final @Reference ItemUIRegistry itemUIRegistry, - BundleContext bundleContext) { + final @Reference TimeZoneProvider timeZoneProvider, BundleContext bundleContext) { this.itemUIRegistry = itemUIRegistry; + this.timeZoneProvider = timeZoneProvider; this.bundleContext = bundleContext; applyConfig(config); } @@ -264,7 +267,7 @@ public class SitemapSubscriptionService implements ModelRepositoryChangeListener String sitemapWithPageId = getScopeIdentifier(sitemapName, pageId); ListenerRecord listener = pageChangeListeners.computeIfAbsent(sitemapWithPageId, v -> { WidgetsChangeListener newListener = new WidgetsChangeListener(sitemapName, pageId, itemUIRegistry, - collectWidgets(sitemapName, pageId)); + timeZoneProvider, collectWidgets(sitemapName, pageId)); ServiceRegistration registration = bundleContext.registerService(EventSubscriber.class.getName(), newListener, null); return new ListenerRecord(newListener, registration); diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java index 5d5c1eeca..a8b199a4e 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java @@ -62,6 +62,7 @@ import org.openhab.core.auth.Role; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.events.Event; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.RESTConstants; @@ -189,6 +190,7 @@ public class SitemapResource private final ItemUIRegistry itemUIRegistry; private final SitemapSubscriptionService subscriptions; private final LocaleService localeService; + private final TimeZoneProvider timeZoneProvider; private final java.util.List sitemapProviders = new ArrayList<>(); @@ -204,9 +206,11 @@ public class SitemapResource public SitemapResource( // final @Reference ItemUIRegistry itemUIRegistry, // final @Reference LocaleService localeService, // + final @Reference TimeZoneProvider timeZoneProvider, // final @Reference SitemapSubscriptionService subscriptions) { this.itemUIRegistry = itemUIRegistry; this.localeService = localeService; + this.timeZoneProvider = timeZoneProvider; this.subscriptions = subscriptions; broadcaster = new SseBroadcaster<>(); @@ -596,7 +600,7 @@ public class SitemapResource boolean isMapview = "mapview".equalsIgnoreCase(widgetTypeName); Predicate itemFilter = (i -> CoreItemFactory.LOCATION.equals(i.getType())); bean.item = EnrichedItemDTOMapper.map(item, isMapview, itemFilter, - UriBuilder.fromUri(uri).path("items/{itemName}"), locale); + UriBuilder.fromUri(uri).path("items/{itemName}"), locale, timeZoneProvider.getTimeZone()); bean.state = itemUIRegistry.getState(widget).toFullString(); // In case the widget state is identical to the item state, its value is set to null. if (bean.state != null && bean.state.equals(bean.item.state)) { diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java index 891c0250b..3b5f30bb9 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java @@ -27,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.events.Event; import org.openhab.core.events.EventSubscriber; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper; import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService.SitemapSubscriptionCallback; import org.openhab.core.items.Item; @@ -65,6 +66,7 @@ public class WidgetsChangeListener implements EventSubscriber { private final String sitemapName; private final String pageId; private final ItemUIRegistry itemUIRegistry; + private final TimeZoneProvider timeZoneProvider; private EList widgets; private Set items; private final HashSet filterItems = new HashSet<>(); @@ -79,11 +81,12 @@ public class WidgetsChangeListener implements EventSubscriber { * @param itemUIRegistry the ItemUIRegistry which is needed for the functionality * @param widgets the list of widgets that are part of the page. */ - public WidgetsChangeListener(String sitemapName, String pageId, ItemUIRegistry itemUIRegistry, - EList widgets) { + public WidgetsChangeListener(String sitemapName, String pageId, final ItemUIRegistry itemUIRegistry, + final TimeZoneProvider timeZoneProvider, EList widgets) { this.sitemapName = sitemapName; this.pageId = pageId; this.itemUIRegistry = itemUIRegistry; + this.timeZoneProvider = timeZoneProvider; updateItemsAndWidgets(widgets); } @@ -248,7 +251,8 @@ public class WidgetsChangeListener implements EventSubscriber { .substring(widget.eClass().getInstanceTypeName().lastIndexOf(".") + 1); boolean drillDown = "mapview".equalsIgnoreCase(widgetTypeName); Predicate itemFilter = (i -> CoreItemFactory.LOCATION.equals(i.getType())); - event.item = EnrichedItemDTOMapper.map(itemToBeSent, drillDown, itemFilter, null, null); + event.item = EnrichedItemDTOMapper.map(itemToBeSent, drillDown, itemFilter, null, null, + timeZoneProvider.getTimeZone()); // event.state is an adjustment of the item state to the widget type. stateToBeSent = itemBelongsToWidget ? state : itemToBeSent.getState(); diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java index ab657fdf2..7a5ef4f4c 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java @@ -42,6 +42,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService; import org.openhab.core.items.GenericItem; @@ -119,6 +120,7 @@ public class SitemapResourceTest extends JavaTest { private @Mock @NonNullByDefault({}) HttpHeaders headersMock; private @Mock @NonNullByDefault({}) Sitemap defaultSitemapMock; private @Mock @NonNullByDefault({}) ItemUIRegistry itemUIRegistryMock; + private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; private @Mock @NonNullByDefault({}) LocaleService localeServiceMock; private @Mock @NonNullByDefault({}) HttpServletRequest requestMock; private @Mock @NonNullByDefault({}) SitemapProvider sitemapProviderMock; @@ -129,10 +131,12 @@ public class SitemapResourceTest extends JavaTest { @BeforeEach public void setup() throws Exception { - subscriptions = new SitemapSubscriptionService(Collections.emptyMap(), itemUIRegistryMock, bundleContextMock); + subscriptions = new SitemapSubscriptionService(Collections.emptyMap(), itemUIRegistryMock, timeZoneProviderMock, + bundleContextMock); subscriptions.addSitemapProvider(sitemapProviderMock); - sitemapResource = new SitemapResource(itemUIRegistryMock, localeServiceMock, subscriptions); + sitemapResource = new SitemapResource(itemUIRegistryMock, localeServiceMock, timeZoneProviderMock, + subscriptions); when(uriInfoMock.getAbsolutePathBuilder()).thenReturn(UriBuilder.fromPath(SITEMAP_PATH)); when(uriInfoMock.getBaseUriBuilder()).thenReturn(UriBuilder.fromPath(SITEMAP_PATH)); diff --git a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java index 757197bdf..fa4797a3e 100644 --- a/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java +++ b/bundles/org.openhab.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilder.java @@ -12,7 +12,6 @@ */ package org.openhab.core.io.rest.sse.internal; -import java.time.DateTimeException; import java.util.HashMap; import java.util.IllegalFormatException; import java.util.Locale; @@ -28,6 +27,7 @@ import javax.ws.rs.sse.OutboundSseEvent.Builder; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.LocaleService; import org.openhab.core.io.rest.sse.internal.dto.StateDTO; import org.openhab.core.items.Item; @@ -68,13 +68,16 @@ public class SseItemStatesEventBuilder { private final ItemRegistry itemRegistry; private final LocaleService localeService; + private final TimeZoneProvider timeZoneProvider; private final StartLevelService startLevelService; @Activate public SseItemStatesEventBuilder(final @Reference ItemRegistry itemRegistry, - final @Reference LocaleService localeService, final @Reference StartLevelService startLevelService) { + final @Reference LocaleService localeService, final @Reference TimeZoneProvider timeZoneProvider, + final @Reference StartLevelService startLevelService) { this.itemRegistry = itemRegistry; this.localeService = localeService; + this.timeZoneProvider = timeZoneProvider; this.startLevelService = startLevelService; } @@ -187,12 +190,6 @@ public class SseItemStatesEventBuilder { if (quantityState != null) { state = quantityState; } - } else if (state instanceof DateTimeType type) { - // Translate a DateTimeType state to the local time zone - try { - state = type.toLocaleZone(); - } catch (DateTimeException e) { - } } // The following exception handling has been added to work around a Java bug with formatting @@ -200,7 +197,11 @@ public class SseItemStatesEventBuilder { // This also handles IllegalFormatConversionException, which is a subclass of // IllegalArgument. try { - displayState = state.format(pattern); + if (state instanceof DateTimeType dateTimeState) { + displayState = dateTimeState.format(pattern, timeZoneProvider.getTimeZone()); + } else { + displayState = state.format(pattern); + } } catch (IllegalArgumentException e) { logger.debug( "Unable to format value '{}' of item {} using format pattern '{}': {}, displaying raw state", diff --git a/bundles/org.openhab.core.io.rest.sse/src/test/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilderTest.java b/bundles/org.openhab.core.io.rest.sse/src/test/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilderTest.java index 0057551fc..4d5d0395e 100644 --- a/bundles/org.openhab.core.io.rest.sse/src/test/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilderTest.java +++ b/bundles/org.openhab.core.io.rest.sse/src/test/java/org/openhab/core/io/rest/sse/internal/SseItemStatesEventBuilderTest.java @@ -27,6 +27,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.LocaleService; import org.openhab.core.items.Item; import org.openhab.core.items.ItemRegistry; @@ -76,6 +77,7 @@ public class SseItemStatesEventBuilderTest { private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock; private @Mock @NonNullByDefault({}) LocaleService localeServiceMock; + private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; private @Mock @NonNullByDefault({}) StartLevelService startLevelServiceMock; private @Mock @NonNullByDefault({}) Item itemMock; @@ -112,7 +114,7 @@ public class SseItemStatesEventBuilderTest { Mockito.when(itemMock.getName()).thenReturn(ITEM_NAME); sseItemStatesEventBuilder = new SseItemStatesEventBuilder(itemRegistryMock, localeServiceMock, - startLevelServiceMock); + timeZoneProviderMock, startLevelServiceMock); } @AfterEach diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java index 2534fc6c8..48c43b259 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfile.java @@ -12,14 +12,11 @@ */ package org.openhab.core.thing.internal.profiles; -import java.time.DateTimeException; import java.time.Duration; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.Instant; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.internal.i18n.I18nProviderImpl; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.thing.profiles.ProfileCallback; import org.openhab.core.thing.profiles.ProfileContext; @@ -36,22 +33,18 @@ import org.slf4j.LoggerFactory; /** * Applies the given parameter "offset" to a {@link DateTimeType} state. * - * Options for the "timezone" parameter are provided by the {@link I18nProviderImpl}. - * * @author Christoph Weitkamp - Initial contribution */ @NonNullByDefault public class TimestampOffsetProfile implements StateProfile { static final String OFFSET_PARAM = "offset"; - static final String TIMEZONE_PARAM = "timezone"; private final Logger logger = LoggerFactory.getLogger(TimestampOffsetProfile.class); private final ProfileCallback callback; private final Duration offset; - private @Nullable ZoneId timeZone; public TimestampOffsetProfile(ProfileCallback callback, ProfileContext context) { this.callback = callback; @@ -68,19 +61,6 @@ public class TimestampOffsetProfile implements StateProfile { OFFSET_PARAM); offset = Duration.ZERO; } - - String timeZoneParam = toStringOrNull(context.getConfiguration().get(TIMEZONE_PARAM)); - logger.debug("Configuring profile with {} parameter '{}'", TIMEZONE_PARAM, timeZoneParam); - if (timeZoneParam == null || timeZoneParam.isBlank()) { - timeZone = null; - } else { - try { - timeZone = ZoneId.of(timeZoneParam); - } catch (DateTimeException e) { - logger.debug("Error setting time zone '{}': {}", timeZoneParam, e.getMessage()); - timeZone = null; - } - } } private @Nullable String toStringOrNull(@Nullable Object value) { @@ -98,20 +78,20 @@ public class TimestampOffsetProfile implements StateProfile { @Override public void onCommandFromItem(Command command) { - callback.handleCommand((Command) applyOffsetAndTimezone(command, false)); + callback.handleCommand((Command) applyOffset(command, false)); } @Override public void onCommandFromHandler(Command command) { - callback.sendCommand((Command) applyOffsetAndTimezone(command, true)); + callback.sendCommand((Command) applyOffset(command, true)); } @Override public void onStateUpdateFromHandler(State state) { - callback.sendUpdate((State) applyOffsetAndTimezone(state, true)); + callback.sendUpdate((State) applyOffset(state, true)); } - private Type applyOffsetAndTimezone(Type type, boolean towardsItem) { + private Type applyOffset(Type type, boolean towardsItem) { if (type instanceof UnDefType) { // we cannot adjust UNDEF or NULL values, thus we simply return them without reporting an error or warning return type; @@ -120,20 +100,15 @@ public class TimestampOffsetProfile implements StateProfile { Duration finalOffset = towardsItem ? offset : offset.negated(); Type result; if (type instanceof DateTimeType timeType) { - ZonedDateTime zdt = timeType.getZonedDateTime(); + Instant instant = timeType.getInstant(); // apply offset if (!Duration.ZERO.equals(offset)) { // we do not need apply an offset equals to 0 - zdt = zdt.plus(finalOffset); + instant = instant.plus(finalOffset); } - // apply time zone - ZoneId localTimeZone = timeZone; - if (localTimeZone != null && !zdt.getZone().equals(localTimeZone) && towardsItem) { - zdt = zdt.withZoneSameInstant(localTimeZone); - } - result = new DateTimeType(zdt); + result = new DateTimeType(instant); } else { logger.warn( "Offset '{}' cannot be applied to the incompatible state '{}' sent from the binding. Returning original state.", diff --git a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/timestampOffsetProfile.xml b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/timestampOffsetProfile.xml index 9206970ce..92ebd7303 100644 --- a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/timestampOffsetProfile.xml +++ b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/config/timestampOffsetProfile.xml @@ -12,10 +12,5 @@ in the reverse direction. - - - A time zone to be applied on the state towards the item. - true - diff --git a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles.properties b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles.properties index e4837fd5e..8b2c08f35 100644 --- a/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles.properties +++ b/bundles/org.openhab.core.thing/src/main/resources/OH-INF/i18n/SystemProfiles.properties @@ -21,7 +21,5 @@ profile-type.system.timestamp-change.label = Timestamp on Change profile-type.system.timestamp-offset.label = Timestamp Offset profile.config.system.timestamp-offset.offset.label = Offset profile.config.system.timestamp-offset.offset.description = Offset to be applied on the state towards the item. The negative offset will be applied in the reverse direction. -profile.config.system.timestamp-offset.timezone.label = Time Zone -profile.config.system.timestamp-offset.timezone.description = A time zone to be applied on the state. profile-type.system.timestamp-trigger.label = Timestamp on Trigger profile-type.system.timestamp-update.label = Timestamp on Update diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfileTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfileTest.java index 1ba18de79..431d7e7f7 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfileTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampOffsetProfileTest.java @@ -15,14 +15,12 @@ package org.openhab.core.thing.internal.profiles; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; -import java.time.ZoneOffset; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -45,28 +43,23 @@ public class TimestampOffsetProfileTest { public static class ParameterSet { public final long seconds; - public final @Nullable String timeZone; - public ParameterSet(long seconds, @Nullable String timeZone) { + public ParameterSet(long seconds) { this.seconds = seconds; - this.timeZone = timeZone; } } public static Collection parameters() { return List.of(new Object[][] { // - { new ParameterSet(0, null) }, // - { new ParameterSet(30, null) }, // - { new ParameterSet(-30, null) }, // - { new ParameterSet(0, "Europe/Berlin") }, // - { new ParameterSet(30, "Europe/Berlin") }, // - { new ParameterSet(-30, "Europe/Berlin") } }); + { new ParameterSet(0) }, // + { new ParameterSet(30) }, // + { new ParameterSet(-30) } }); } @Test public void testUNDEFOnStateUpdateFromHandler() { ProfileCallback callback = mock(ProfileCallback.class); - TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(60), null); + TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(60)); State state = UnDefType.UNDEF; offsetProfile.onStateUpdateFromHandler(state); @@ -82,8 +75,7 @@ public class TimestampOffsetProfileTest { @MethodSource("parameters") public void testOnCommandFromItem(ParameterSet parameterSet) { ProfileCallback callback = mock(ProfileCallback.class); - TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(parameterSet.seconds), - parameterSet.timeZone); + TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(parameterSet.seconds)); Command cmd = DateTimeType.valueOf("2021-03-30T10:58:47.033+0000"); offsetProfile.onCommandFromItem(cmd); @@ -94,17 +86,15 @@ public class TimestampOffsetProfileTest { Command result = capture.getValue(); DateTimeType updateResult = (DateTimeType) result; DateTimeType expectedResult = new DateTimeType( - ((DateTimeType) cmd).getZonedDateTime().minusSeconds(parameterSet.seconds)); - assertEquals(ZoneOffset.UTC, updateResult.getZonedDateTime().getOffset()); - assertEquals(expectedResult.getZonedDateTime(), updateResult.getZonedDateTime()); + ((DateTimeType) cmd).getInstant().minusSeconds(parameterSet.seconds)); + assertEquals(expectedResult.getInstant(), updateResult.getInstant()); } @ParameterizedTest @MethodSource("parameters") public void testOnCommandFromHandler(ParameterSet parameterSet) { ProfileCallback callback = mock(ProfileCallback.class); - TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(parameterSet.seconds), - parameterSet.timeZone); + TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(parameterSet.seconds)); Command cmd = new DateTimeType("2021-03-30T10:58:47.033+0000"); offsetProfile.onCommandFromHandler(cmd); @@ -115,20 +105,15 @@ public class TimestampOffsetProfileTest { Command result = capture.getValue(); DateTimeType updateResult = (DateTimeType) result; DateTimeType expectedResult = new DateTimeType( - ((DateTimeType) cmd).getZonedDateTime().plusSeconds(parameterSet.seconds)); - String timeZone = parameterSet.timeZone; - if (timeZone != null) { - expectedResult = expectedResult.toZone(timeZone); - } - assertEquals(expectedResult.getZonedDateTime(), updateResult.getZonedDateTime()); + ((DateTimeType) cmd).getInstant().plusSeconds(parameterSet.seconds)); + assertEquals(expectedResult.getInstant(), updateResult.getInstant()); } @ParameterizedTest @MethodSource("parameters") public void testOnStateUpdateFromHandler(ParameterSet parameterSet) { ProfileCallback callback = mock(ProfileCallback.class); - TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(parameterSet.seconds), - parameterSet.timeZone); + TimestampOffsetProfile offsetProfile = createProfile(callback, Long.toString(parameterSet.seconds)); State state = new DateTimeType("2021-03-30T10:58:47.033+0000"); offsetProfile.onStateUpdateFromHandler(state); @@ -139,21 +124,14 @@ public class TimestampOffsetProfileTest { State result = capture.getValue(); DateTimeType updateResult = (DateTimeType) result; DateTimeType expectedResult = new DateTimeType( - ((DateTimeType) state).getZonedDateTime().plusSeconds(parameterSet.seconds)); - String timeZone = parameterSet.timeZone; - if (timeZone != null) { - expectedResult = expectedResult.toZone(timeZone); - } - assertEquals(expectedResult.getZonedDateTime(), updateResult.getZonedDateTime()); + ((DateTimeType) state).getInstant().plusSeconds(parameterSet.seconds)); + assertEquals(expectedResult.getInstant(), updateResult.getInstant()); } - private TimestampOffsetProfile createProfile(ProfileCallback callback, String offset, @Nullable String timeZone) { + private TimestampOffsetProfile createProfile(ProfileCallback callback, String offset) { ProfileContext context = mock(ProfileContext.class); Map properties = new HashMap<>(); properties.put(TimestampOffsetProfile.OFFSET_PARAM, offset); - if (timeZone != null) { - properties.put(TimestampOffsetProfile.TIMEZONE_PARAM, timeZone); - } when(context.getConfiguration()).thenReturn(new Configuration(properties)); return new TimestampOffsetProfile(callback, context); } diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampProfileTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampProfileTest.java index 988d7d1cd..3bf76b3ed 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampProfileTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampProfileTest.java @@ -15,7 +15,7 @@ package org.openhab.core.thing.internal.profiles; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; -import java.time.ZonedDateTime; +import java.time.Instant; import java.time.temporal.ChronoUnit; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -40,7 +40,7 @@ public class TimestampProfileTest extends JavaTest { ProfileCallback callback = mock(ProfileCallback.class); TimestampUpdateProfile timestampProfile = new TimestampUpdateProfile(callback); - ZonedDateTime now = ZonedDateTime.now(); + Instant now = Instant.now(); timestampProfile.onStateUpdateFromHandler(new DecimalType(23)); ArgumentCaptor capture = ArgumentCaptor.forClass(State.class); @@ -48,7 +48,7 @@ public class TimestampProfileTest extends JavaTest { State result = capture.getValue(); DateTimeType updateResult = (DateTimeType) result; - ZonedDateTime timestamp = updateResult.getZonedDateTime(); + Instant timestamp = updateResult.getInstant(); long difference = ChronoUnit.MINUTES.between(now, timestamp); assertTrue(difference < 1); } @@ -66,7 +66,7 @@ public class TimestampProfileTest extends JavaTest { State result = capture.getValue(); DateTimeType changeResult = (DateTimeType) result; - waitForAssert(() -> assertTrue(ZonedDateTime.now().isAfter(changeResult.getZonedDateTime()))); + waitForAssert(() -> assertTrue(Instant.now().isAfter(changeResult.getInstant()))); // The state is unchanged, no additional call to the callback timestampProfile.onStateUpdateFromHandler(new DecimalType(23)); @@ -77,6 +77,6 @@ public class TimestampProfileTest extends JavaTest { verify(callback, times(2)).sendUpdate(capture.capture()); result = capture.getValue(); DateTimeType updatedResult = (DateTimeType) result; - assertTrue(updatedResult.getZonedDateTime().isAfter(changeResult.getZonedDateTime())); + assertTrue(updatedResult.getInstant().isAfter(changeResult.getInstant())); } } diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampTriggerProfileTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampTriggerProfileTest.java index 3275c57f8..7bb9a05d0 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampTriggerProfileTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/profiles/TimestampTriggerProfileTest.java @@ -15,7 +15,7 @@ package org.openhab.core.thing.internal.profiles; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; -import java.time.ZonedDateTime; +import java.time.Instant; import java.time.temporal.ChronoUnit; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -38,14 +38,14 @@ public class TimestampTriggerProfileTest { ProfileCallback callback = mock(ProfileCallback.class); TriggerProfile profile = new TimestampTriggerProfile(callback); - ZonedDateTime now = ZonedDateTime.now(); + Instant now = Instant.now(); profile.onTriggerFromHandler(CommonTriggerEvents.PRESSED); ArgumentCaptor capture = ArgumentCaptor.forClass(State.class); verify(callback, times(1)).sendUpdate(capture.capture()); State result = capture.getValue(); DateTimeType updateResult = (DateTimeType) result; - ZonedDateTime timestamp = updateResult.getZonedDateTime(); + Instant timestamp = updateResult.getInstant(); long difference = ChronoUnit.MINUTES.between(now, timestamp); assertTrue(difference < 1); } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java index 92994e1e2..86d5b7261 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java @@ -12,8 +12,7 @@ */ package org.openhab.core.ui.internal.items; -import java.time.DateTimeException; -import java.time.ZonedDateTime; +import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; @@ -40,6 +39,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.config.core.ConfigurableService; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; @@ -138,6 +138,7 @@ public class ItemUIRegistryImpl implements ItemUIRegistry { protected final Set itemUIProviders = new HashSet<>(); private final ItemRegistry itemRegistry; + private final TimeZoneProvider timeZoneProvider; private final Map defaultWidgets = Collections.synchronizedMap(new WeakHashMap<>()); @@ -154,8 +155,10 @@ public class ItemUIRegistryImpl implements ItemUIRegistry { } @Activate - public ItemUIRegistryImpl(@Reference ItemRegistry itemRegistry) { + public ItemUIRegistryImpl(final @Reference ItemRegistry itemRegistry, + final @Reference TimeZoneProvider timeZoneProvider) { this.itemRegistry = itemRegistry; + this.timeZoneProvider = timeZoneProvider; } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) @@ -455,12 +458,6 @@ public class ItemUIRegistryImpl implements ItemUIRegistry { quantityState = convertStateToWidgetUnit(quantityState, w); state = quantityState; } - } else if (state instanceof DateTimeType type) { - // Translate a DateTimeType state to the local time zone - try { - state = type.toLocaleZone(); - } catch (DateTimeException ignored) { - } } // The following exception handling has been added to work around a Java bug with formatting @@ -474,10 +471,20 @@ public class ItemUIRegistryImpl implements ItemUIRegistry { String type = matcher.group(1); String function = matcher.group(2); String value = matcher.group(3); - formatPattern = type + "(" + function + "):" + state.format(value); - transformFailbackValue = state.toString(); + formatPattern = type + "(" + function + "):"; + if (state instanceof DateTimeType dateTimeState) { + formatPattern += dateTimeState.format(value, timeZoneProvider.getTimeZone()); + transformFailbackValue = dateTimeState.toFullString(timeZoneProvider.getTimeZone()); + } else { + formatPattern += state.format(value); + transformFailbackValue = state.toString(); + } } else { - formatPattern = state.format(formatPattern); + if (state instanceof DateTimeType dateTimeState) { + formatPattern = dateTimeState.format(formatPattern, timeZoneProvider.getTimeZone()); + } else { + formatPattern = state.format(formatPattern); + } } } catch (IllegalArgumentException e) { logger.warn("Exception while formatting value '{}' of item {} with format '{}': {}", state, @@ -1138,9 +1145,9 @@ public class ItemUIRegistryImpl implements ItemUIRegistry { } catch (NumberFormatException e) { logger.debug("matchStateToValue: Decimal format exception: ", e); } - } else if (state instanceof DateTimeType type) { - ZonedDateTime val = type.getZonedDateTime(); - ZonedDateTime now = ZonedDateTime.now(); + } else if (state instanceof DateTimeType dateTimeState) { + Instant val = dateTimeState.getInstant(); + Instant now = Instant.now(); long secsDif = ChronoUnit.SECONDS.between(val, now); try { diff --git a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java index 5cc7388ce..cc845feb6 100644 --- a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java +++ b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java @@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.text.DecimalFormatSymbols; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.TimeZone; @@ -36,6 +37,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; @@ -99,22 +101,24 @@ public class ItemUIRegistryImplTest { // we need to get the decimal separator of the default locale for our tests private static final char SEP = (new DecimalFormatSymbols().getDecimalSeparator()); private static final String ITEM_NAME = "Item"; + private static final String DEFAULT_TIME_ZONE = "GMT-6"; private @NonNullByDefault({}) ItemUIRegistryImpl uiRegistry; private @Mock @NonNullByDefault({}) ItemRegistry registryMock; + private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; private @Mock @NonNullByDefault({}) Widget widgetMock; private @Mock @NonNullByDefault({}) Item itemMock; @BeforeEach public void setup() throws Exception { - uiRegistry = new ItemUIRegistryImpl(registryMock); + uiRegistry = new ItemUIRegistryImpl(registryMock, timeZoneProviderMock); when(widgetMock.getItem()).thenReturn(ITEM_NAME); when(registryMock.getItem(ITEM_NAME)).thenReturn(itemMock); + when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of(DEFAULT_TIME_ZONE)); - // Set default time zone to GMT-6 - TimeZone.setDefault(TimeZone.getTimeZone("GMT-6")); + TimeZone.setDefault(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); } @Test diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeGroupFunction.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeGroupFunction.java index 393d0677a..dc87b2eb7 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeGroupFunction.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeGroupFunction.java @@ -12,7 +12,7 @@ */ package org.openhab.core.library.types; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -41,12 +41,12 @@ public interface DateTimeGroupFunction extends GroupFunction { @Override public State calculate(@Nullable Set items) { if (items != null && !items.isEmpty()) { - ZonedDateTime max = null; + Instant max = null; for (Item item : items) { DateTimeType itemState = item.getStateAs(DateTimeType.class); if (itemState != null) { - if (max == null || max.isBefore(itemState.getZonedDateTime())) { - max = itemState.getZonedDateTime(); + if (max == null || max.isBefore(itemState.getInstant())) { + max = itemState.getInstant(); } } } @@ -84,12 +84,12 @@ public interface DateTimeGroupFunction extends GroupFunction { @Override public State calculate(@Nullable Set items) { if (items != null && !items.isEmpty()) { - ZonedDateTime max = null; + Instant max = null; for (Item item : items) { DateTimeType itemState = item.getStateAs(DateTimeType.class); if (itemState != null) { - if (max == null || max.isAfter(itemState.getZonedDateTime())) { - max = itemState.getZonedDateTime(); + if (max == null || max.isAfter(itemState.getInstant())) { + max = itemState.getInstant(); } } } diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java index 1809c24de..3c499481b 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java @@ -16,7 +16,6 @@ import java.time.DateTimeException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -39,6 +38,7 @@ import org.openhab.core.types.State; * @author Laurent Garnier - added methods toLocaleZone and toZone * @author Gaƫl L'hopital - added ability to use second and milliseconds unix time * @author Jimmy Tanagra - implement Comparable + * @author Jacob Laursen - Refactored to use {@link Instant} internally */ @NonNullByDefault public class DateTimeType implements PrimitiveType, State, Command, Comparable { @@ -73,48 +73,65 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable= 0 ? epoch : epoch * -1) + 1); - Instant i; // Assume that below 12 digits we're in seconds if (length < 12) { - i = Instant.ofEpochSecond(epoch); + instant = Instant.ofEpochSecond(epoch); } else { - i = Instant.ofEpochMilli(epoch); + instant = Instant.ofEpochMilli(epoch); } - date = ZonedDateTime.ofInstant(i, ZoneOffset.UTC); } catch (NumberFormatException notANumberException) { // date only if (zonedValue.length() == 10) { - date = parse(zonedValue + "T00:00:00"); + instant = parse(zonedValue + "T00:00:00"); } else { - date = parse(zonedValue.substring(0, 10) + "T00:00:00" + zonedValue.substring(10)); + instant = parse(zonedValue.substring(0, 10) + "T00:00:00" + zonedValue.substring(10)); } } } @@ -122,21 +139,37 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable items = new HashSet<>(); items.add(new TestItem("TestItem1", new DateTimeType(expectedDateTime))); items.add(new TestItem("TestItem2", UnDefType.UNDEF)); - items.add(new TestItem("TestItem3", new DateTimeType(expectedDateTime.minusDays(10)))); - items.add(new TestItem("TestItem4", new DateTimeType(expectedDateTime.minusYears(1)))); + items.add(new TestItem("TestItem3", new DateTimeType(expectedDateTime.minus(10, ChronoUnit.DAYS)))); + items.add(new TestItem("TestItem4", new DateTimeType(expectedDateTime.minus(366, ChronoUnit.DAYS)))); items.add(new TestItem("TestItem5", UnDefType.UNDEF)); items.add(new TestItem("TestItem6", new DateTimeType(expectedDateTime.minusSeconds(1)))); GroupFunction function = new DateTimeGroupFunction.Latest(); State state = function.calculate(items); - assertTrue(expectedDateTime.isEqual(((DateTimeType) state).getZonedDateTime())); + assertTrue(expectedDateTime.equals(((DateTimeType) state).getInstant())); } @Test public void testEarliestFunction() { - ZonedDateTime expectedDateTime = ZonedDateTime.now(); + Instant expectedDateTime = Instant.now(); Set items = new HashSet<>(); items.add(new TestItem("TestItem1", new DateTimeType(expectedDateTime))); items.add(new TestItem("TestItem2", UnDefType.UNDEF)); - items.add(new TestItem("TestItem3", new DateTimeType(expectedDateTime.plusDays(10)))); - items.add(new TestItem("TestItem4", new DateTimeType(expectedDateTime.plusYears(1)))); + items.add(new TestItem("TestItem3", new DateTimeType(expectedDateTime.plus(10, ChronoUnit.DAYS)))); + items.add(new TestItem("TestItem4", new DateTimeType(expectedDateTime.plus(366, ChronoUnit.DAYS)))); items.add(new TestItem("TestItem5", UnDefType.UNDEF)); items.add(new TestItem("TestItem6", new DateTimeType(expectedDateTime.plusSeconds(1)))); GroupFunction function = new DateTimeGroupFunction.Earliest(); State state = function.calculate(items); - assertTrue(expectedDateTime.isEqual(((DateTimeType) state).getZonedDateTime())); + assertTrue(expectedDateTime.equals(((DateTimeType) state).getInstant())); } private static class TestItem extends GenericItem { diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java index 9b42d301c..f999114b8 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java @@ -13,6 +13,7 @@ package org.openhab.core.library.types; import static java.util.Map.entry; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -30,11 +31,13 @@ import java.util.Map; import java.util.Objects; import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; @@ -183,19 +186,19 @@ public class DateTimeTypeTest { { new ParameterSet(TimeZone.getTimeZone("UTC"), initTimeMap(), TimeZone.getTimeZone("UTC"), "2014-03-30T10:58:47.033+0000", "2014-03-30T10:58:47.033+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), initTimeMap(), TimeZone.getTimeZone("CET"), - "2014-03-30T10:58:47.033+0200", "2014-03-30T08:58:47.033+0000") }, + "2014-03-30T08:58:47.033+0000", "2014-03-30T08:58:47.033+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "2014-03-30T10:58:47.23", "2014-03-30T10:58:47.230+0000", "2014-03-30T10:58:47.230+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "2014-03-30T10:58:47UTC", "2014-03-30T10:58:47.000+0000", "2014-03-30T10:58:47.000+0000") }, { new ParameterSet(TimeZone.getTimeZone("CET"), initTimeMap(), TimeZone.getTimeZone("UTC"), - "2014-03-30T10:58:47.033+0000", "2014-03-30T12:58:47.033+0200") }, + "2014-03-30T12:58:47.033+0200", "2014-03-30T12:58:47.033+0200") }, { new ParameterSet(TimeZone.getTimeZone("CET"), initTimeMap(), TimeZone.getTimeZone("CET"), "2014-03-30T10:58:47.033+0200", "2014-03-30T10:58:47.033+0200") }, { new ParameterSet(TimeZone.getTimeZone("CET"), "2014-03-30T10:58:47CET", "2014-03-30T10:58:47.000+0200", "2014-03-30T10:58:47.000+0200") }, { new ParameterSet(TimeZone.getTimeZone("GMT+5"), "2014-03-30T10:58:47.000Z", - "2014-03-30T10:58:47.000+0000", "2014-03-30T15:58:47.000+0500") }, + "2014-03-30T15:58:47.000+0500", "2014-03-30T15:58:47.000+0500") }, { new ParameterSet(TimeZone.getTimeZone("GMT+2"), null, null, "2014-03-30T10:58:47", "2014-03-30T10:58:47.000+0200", "2014-03-30T10:58:47.000+0200", null, "%1$td.%1$tm.%1$tY %1$tH:%1$tM", "30.03.2014 10:58") }, @@ -203,15 +206,15 @@ public class DateTimeTypeTest { "2014-03-30T10:58:47.033+0000", "2014-03-30T10:58:47.033+0000") }, // Parameter set with an invalid time zone id as input, leading to GMT being considered { new ParameterSet(TimeZone.getTimeZone("CET"), initTimeMap(), TimeZone.getTimeZone("+02:00"), - "2014-03-30T10:58:47.033+0000", "2014-03-30T12:58:47.033+0200") }, + "2014-03-30T12:58:47.033+0200", "2014-03-30T12:58:47.033+0200") }, // Parameter set with an invalid time zone id as input, leading to GMT being considered { new ParameterSet(TimeZone.getTimeZone("GMT+2"), initTimeMap(), TimeZone.getTimeZone("GML"), - "2014-03-30T10:58:47.033+0000", "2014-03-30T12:58:47.033+0200") }, + "2014-03-30T12:58:47.033+0200", "2014-03-30T12:58:47.033+0200") }, { new ParameterSet(TimeZone.getTimeZone("GMT-2"), initTimeMap(), TimeZone.getTimeZone("GMT+3"), null, - "2014-03-30T10:58:47.033+0300", "2014-03-30T05:58:47.033-0200", Locale.GERMAN, - "%1$tA %1$td.%1$tm.%1$tY %1$tH:%1$tM", "Sonntag 30.03.2014 10:58") }, + "2014-03-30T05:58:47.033-0200", "2014-03-30T05:58:47.033-0200", Locale.GERMAN, + "%1$tA %1$td.%1$tm.%1$tY %1$tH:%1$tM", "Sonntag 30.03.2014 05:58") }, { new ParameterSet(TimeZone.getTimeZone("GMT-2"), initTimeMap(), TimeZone.getTimeZone("GMT-4"), - "2014-03-30T10:58:47.033-0400", "2014-03-30T12:58:47.033-0200") }, + "2014-03-30T12:58:47.033-0200", "2014-03-30T12:58:47.033-0200") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "10:58:47", "1970-01-01T10:58:47.000+0000", "1970-01-01T10:58:47.000+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "10:58", "1970-01-01T10:58:00.000+0000", @@ -282,29 +285,37 @@ public class DateTimeTypeTest { DateTimeType dt1 = new DateTimeType("2019-06-12T17:30:00Z"); DateTimeType dt2 = new DateTimeType("2019-06-12T17:30:00+0000"); DateTimeType dt3 = new DateTimeType("2019-06-12T19:30:00+0200"); + DateTimeType dt4 = new DateTimeType("2019-06-12T19:30:00+0200"); assertThat(dt1, is(dt2)); ZonedDateTime zdt1 = dt1.getZonedDateTime(); ZonedDateTime zdt2 = dt2.getZonedDateTime(); ZonedDateTime zdt3 = dt3.getZonedDateTime(); + ZonedDateTime zdt4 = dt4.getZonedDateTime(ZoneId.of("UTC")); assertThat(zdt1.getZone(), is(zdt2.getZone())); assertThat(zdt1, is(zdt2)); assertThat(zdt1, is(zdt3.withZoneSameInstant(zdt1.getZone()))); assertThat(zdt2, is(zdt3.withZoneSameInstant(zdt2.getZone()))); + assertThat(zdt1, is(zdt4)); } @Test public void instantParsingTest() { - DateTimeType dt1 = new DateTimeType("2019-06-12T17:30:00Z"); - DateTimeType dt2 = new DateTimeType("2019-06-12T17:30:00+0000"); - DateTimeType dt3 = new DateTimeType("2019-06-12T19:30:00+0200"); + DateTimeType dt1 = new DateTimeType(Instant.parse("2019-06-12T17:30:00Z")); + DateTimeType dt2 = new DateTimeType("2019-06-12T17:30:00Z"); + DateTimeType dt3 = new DateTimeType("2019-06-12T17:30:00+0000"); + DateTimeType dt4 = new DateTimeType("2019-06-12T19:30:00+0200"); assertThat(dt1, is(dt2)); + assertThat(dt2, is(dt3)); + assertThat(dt3, is(dt4)); Instant i1 = dt1.getInstant(); Instant i2 = dt2.getInstant(); Instant i3 = dt3.getInstant(); + Instant i4 = dt4.getInstant(); assertThat(i1, is(i2)); - assertThat(i1, is(i3)); + assertThat(i2, is(i3)); + assertThat(i3, is(i4)); } @Test @@ -370,22 +381,21 @@ public class DateTimeTypeTest { } @ParameterizedTest - @MethodSource("parameters") - public void changingZoneTest(ParameterSet parameterSet) { - TimeZone.setDefault(parameterSet.defaultTimeZone); - DateTimeType dt = createDateTimeType(parameterSet); - DateTimeType dt2 = dt.toLocaleZone(); - assertEquals(parameterSet.expectedResultLocalTZ, dt2.toFullString()); - dt2 = dt.toZone(parameterSet.defaultTimeZone.toZoneId()); - assertEquals(parameterSet.expectedResultLocalTZ, dt2.toFullString()); + @MethodSource("provideTestCasesForFormatWithZone") + void formatWithZone(String instant, @Nullable String pattern, ZoneId zoneId, String expected) { + DateTimeType dt = new DateTimeType(Instant.parse(instant)); + String actual = dt.format(pattern, zoneId); + assertThat(actual, is(equalTo(expected))); } - @ParameterizedTest - @MethodSource("parameters") - public void changingZoneThrowsExceptionTest(ParameterSet parameterSet) { - TimeZone.setDefault(parameterSet.defaultTimeZone); - DateTimeType dt = createDateTimeType(parameterSet); - assertThrows(DateTimeException.class, () -> dt.toZone("XXX")); + private static Stream provideTestCasesForFormatWithZone() { + return Stream.of( // + Arguments.of("2024-11-11T20:39:01Z", null, ZoneId.of("UTC"), "2024-11-11T20:39:01"), // + Arguments.of("2024-11-11T20:39:01Z", "%1$td.%1$tm.%1$tY %1$tH:%1$tM", ZoneId.of("Europe/Paris"), + "11.11.2024 21:39"), // + Arguments.of("2024-11-11T20:39:01Z", "%1$td.%1$tm.%1$tY %1$tH:%1$tM", ZoneId.of("US/Alaska"), + "11.11.2024 11:39") // + ); } private DateTimeType createDateTimeType(ParameterSet parameterSet) throws DateTimeException { @@ -414,4 +424,25 @@ public class DateTimeTypeTest { return LocalDateTime.of(year, month + 1, dayOfMonth, hourOfDay, minute, second, durationInNano); } + + @ParameterizedTest + @MethodSource("provideTestCasesForToFullStringWithZone") + void toFullStringWithZone(String instant, ZoneId zoneId, String expected) { + DateTimeType dt = new DateTimeType(Instant.parse(instant)); + String actual = dt.toFullString(zoneId); + assertThat(actual, is(equalTo(expected))); + } + + private static Stream provideTestCasesForToFullStringWithZone() { + return Stream.of( // + Arguments.of("2024-11-11T20:39:00Z", ZoneId.of("UTC"), "2024-11-11T20:39:00.000+0000"), // + Arguments.of("2024-11-11T20:39:00.000000000Z", ZoneId.of("UTC"), "2024-11-11T20:39:00.000+0000"), // + Arguments.of("2024-11-11T20:39:00.000000001Z", ZoneId.of("UTC"), "2024-11-11T20:39:00.000000001+0000"), // + Arguments.of("2024-11-11T20:39:00.123000000Z", ZoneId.of("UTC"), "2024-11-11T20:39:00.123+0000"), // + Arguments.of("2024-11-11T20:39:00.123456000Z", ZoneId.of("UTC"), "2024-11-11T20:39:00.123456+0000"), // + Arguments.of("2024-11-11T20:39:00.123456789Z", ZoneId.of("UTC"), "2024-11-11T20:39:00.123456789+0000"), // + Arguments.of("2024-11-11T20:39:00.123Z", ZoneId.of("Europe/Paris"), "2024-11-11T21:39:00.123+0100"), // + Arguments.of("2024-11-11T04:59:59.999Z", ZoneId.of("America/New_York"), "2024-11-10T23:59:59.999-0500") // + ); + } } diff --git a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperWithTransformOSGiTest.java b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperWithTransformOSGiTest.java index e6b02a8cb..1302963cd 100644 --- a/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperWithTransformOSGiTest.java +++ b/itests/org.openhab.core.io.rest.core.tests/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperWithTransformOSGiTest.java @@ -57,7 +57,7 @@ public class EnrichedItemDTOMapperWithTransformOSGiTest extends JavaOSGiTest { item1.setState(new DecimalType("12.34")); item1.setStateDescriptionService(stateDescriptionServiceMock); - EnrichedItemDTO enrichedDTO = EnrichedItemDTOMapper.map(item1, false, null, null, null); + EnrichedItemDTO enrichedDTO = EnrichedItemDTOMapper.map(item1, false, null, null, null, null); assertThat(enrichedDTO, is(notNullValue())); assertThat(enrichedDTO.name, is("Item1")); assertThat(enrichedDTO.state, is("12.34"));