[core] Switch DateTimeType to Instant internally for consistent time-zone handling (#3583)

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2024-12-08 16:06:29 +01:00 committed by GitHub
parent a35041ac7b
commit b31ff66bba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 340 additions and 255 deletions

View File

@ -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<Rule> 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<ZonedDateTime> 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

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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<Item> 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<String> namespaces = splitAndFilterNamespaces(namespaceSelector, locale);
final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders);
@ -256,7 +262,7 @@ public class ItemResource implements RESTResource {
}
Stream<EnrichedItemDTO> 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<EnrichedItemDTO> 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<String> 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<? extends org.openhab.core.semantics.Tag> 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);
}

View File

@ -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,

View File

@ -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<Item> 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<Item> itemFilter,
@Nullable UriBuilder uriBuilder, @Nullable Locale locale, List<Item> parents) {
@Nullable UriBuilder uriBuilder, @Nullable Locale locale, @Nullable ZoneId zoneId, List<Item> 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<Item> itemFilter, @Nullable UriBuilder uriBuilder, @Nullable Locale locale,
List<Item> parents) {
@Nullable ZoneId zoneId, List<Item> 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]);

View File

@ -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);
}

View File

@ -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<SitemapProvider> sitemapProviders = new ArrayList<>();
@ -107,8 +109,9 @@ public class SitemapSubscriptionService implements ModelRepositoryChangeListener
@Activate
public SitemapSubscriptionService(Map<String, Object> 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);

View File

@ -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<SitemapProvider> 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<Item> 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)) {

View File

@ -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<Widget> widgets;
private Set<Item> items;
private final HashSet<String> 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<Widget> widgets) {
public WidgetsChangeListener(String sitemapName, String pageId, final ItemUIRegistry itemUIRegistry,
final TimeZoneProvider timeZoneProvider, EList<Widget> 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<Item> 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();

View File

@ -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));

View File

@ -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",

View File

@ -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

View File

@ -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.",

View File

@ -12,10 +12,5 @@
in the reverse
direction.</description>
</parameter>
<parameter name="timezone" type="text">
<label>Time Zone</label>
<description>A time zone to be applied on the state towards the item.</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -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

View File

@ -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<Object[]> 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<String, Object> 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);
}

View File

@ -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<State> 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()));
}
}

View File

@ -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<State> 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);
}

View File

@ -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<ItemUIProvider> itemUIProviders = new HashSet<>();
private final ItemRegistry itemRegistry;
private final TimeZoneProvider timeZoneProvider;
private final Map<Widget, Widget> 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 {

View File

@ -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

View File

@ -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<Item> 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<Item> 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();
}
}
}

View File

@ -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<DateTimeType> {
@ -73,48 +73,65 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable<D
private static final DateTimeFormatter FORMATTER_TZ_RFC = DateTimeFormatter
.ofPattern(DATE_FORMAT_PATTERN_WITH_TZ_RFC);
private ZonedDateTime zonedDateTime;
private Instant instant;
/**
* Creates a new {@link DateTimeType} representing the current
* instant from the system clock.
*/
public DateTimeType() {
this(ZonedDateTime.now());
this(Instant.now());
}
/**
* Creates a new {@link DateTimeType} with the given value.
*
* @param instant
*/
public DateTimeType(Instant instant) {
this.instant = instant;
}
/**
* Creates a new {@link DateTimeType} with the given value.
* The time-zone information will be discarded, only the
* resulting {@link Instant} is preserved.
*
* @param zoned
*/
public DateTimeType(ZonedDateTime zoned) {
this.zonedDateTime = ZonedDateTime.from(zoned).withFixedOffsetZone();
instant = zoned.toInstant();
}
public DateTimeType(String zonedValue) {
ZonedDateTime date;
try {
// direct parsing (date and time)
try {
if (DATE_PARSE_PATTERN_WITH_SPACE.matcher(zonedValue).matches()) {
date = parse(zonedValue.substring(0, 10) + "T" + zonedValue.substring(11));
instant = parse(zonedValue.substring(0, 10) + "T" + zonedValue.substring(11));
} else {
date = parse(zonedValue);
instant = parse(zonedValue);
}
} catch (DateTimeParseException fullDtException) {
// time only
try {
date = parse("1970-01-01T" + zonedValue);
instant = parse("1970-01-01T" + zonedValue);
} catch (DateTimeParseException timeOnlyException) {
try {
long epoch = Double.valueOf(zonedValue).longValue();
int length = (int) (Math.log10(epoch >= 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<D
} catch (DateTimeParseException invalidFormatException) {
throw new IllegalArgumentException(zonedValue + " is not in a valid format.", invalidFormatException);
}
zonedDateTime = date.withFixedOffsetZone();
}
public ZonedDateTime getZonedDateTime() {
return zonedDateTime;
}
/**
* Get curent object represented as an {@link Instant}
* @deprecated
* Get object represented as a {@link ZonedDateTime} with system
* default time-zone applied
*
* @return an {@link Instant} representation of the current object
* @return a {@link ZonedDateTime} representation of the object
*/
@Deprecated
public ZonedDateTime getZonedDateTime() {
return getZonedDateTime(ZoneId.systemDefault());
}
/**
* Get object represented as a {@link ZonedDateTime} with the
* the provided time-zone applied
*
* @return a {@link ZonedDateTime} representation of the object
*/
public ZonedDateTime getZonedDateTime(ZoneId zoneId) {
return instant.atZone(zoneId);
}
/**
* Get the {@link Instant} value of the object
*
* @return the {@link Instant} value of the object
*/
public Instant getInstant() {
return zonedDateTime.toInstant();
return instant;
}
public static DateTimeType valueOf(String value) {
@ -145,6 +178,11 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable<D
@Override
public String format(@Nullable String pattern) {
return format(pattern, ZoneId.systemDefault());
}
public String format(@Nullable String pattern, ZoneId zoneId) {
ZonedDateTime zonedDateTime = instant.atZone(zoneId);
if (pattern == null) {
return DateTimeFormatter.ofPattern(DATE_PATTERN).format(zonedDateTime);
}
@ -153,42 +191,48 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable<D
}
public String format(Locale locale, String pattern) {
return String.format(locale, pattern, zonedDateTime);
return String.format(locale, pattern, getZonedDateTime());
}
/**
* Create a {@link DateTimeType} being the translation of the current object to the locale time zone
* @deprecated
* Create a {@link DateTimeType} being the translation of the current object to the locale time zone
*
* @return a {@link DateTimeType} translated to the locale time zone
* @throws DateTimeException if the converted zone ID has an invalid format or the result exceeds the supported date
* range
* @throws ZoneRulesException if the converted zone region ID cannot be found
*/
@Deprecated
public DateTimeType toLocaleZone() throws DateTimeException, ZoneRulesException {
return toZone(ZoneId.systemDefault());
return new DateTimeType(instant);
}
/**
* Create a {@link DateTimeType} being the translation of the current object to a given zone
* @deprecated
* Create a {@link DateTimeType} being the translation of the current object to a given zone
*
* @param zone the target zone as a string
* @return a {@link DateTimeType} translated to the given zone
* @throws DateTimeException if the zone has an invalid format or the result exceeds the supported date range
* @throws ZoneRulesException if the zone is a region ID that cannot be found
*/
@Deprecated
public DateTimeType toZone(String zone) throws DateTimeException, ZoneRulesException {
return toZone(ZoneId.of(zone));
return new DateTimeType(instant);
}
/**
* Create a {@link DateTimeType} being the translation of the current object to a given zone
* @deprecated
* Create a {@link DateTimeType} being the translation of the current object to a given zone
*
* @param zoneId the target {@link ZoneId}
* @return a {@link DateTimeType} translated to the given zone
* @throws DateTimeException if the result exceeds the supported date range
*/
@Deprecated
public DateTimeType toZone(ZoneId zoneId) throws DateTimeException {
return new DateTimeType(zonedDateTime.withZoneSameInstant(zoneId));
return new DateTimeType(instant);
}
@Override
@ -198,7 +242,11 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable<D
@Override
public String toFullString() {
String formatted = zonedDateTime.format(FORMATTER_TZ_RFC);
return toFullString(ZoneId.systemDefault());
}
public String toFullString(ZoneId zoneId) {
String formatted = instant.atZone(zoneId).format(FORMATTER_TZ_RFC);
if (formatted.contains(".")) {
String sign = "";
if (formatted.contains("+")) {
@ -219,7 +267,7 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable<D
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getZonedDateTime().hashCode();
result = prime * result + instant.hashCode();
return result;
}
@ -235,15 +283,15 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable<D
return false;
}
DateTimeType other = (DateTimeType) obj;
return zonedDateTime.compareTo(other.zonedDateTime) == 0;
return instant.compareTo(other.instant) == 0;
}
@Override
public int compareTo(DateTimeType o) {
return zonedDateTime.compareTo(o.getZonedDateTime());
return instant.compareTo(o.getInstant());
}
private ZonedDateTime parse(String value) throws DateTimeParseException {
private Instant parse(String value) throws DateTimeParseException {
ZonedDateTime date;
try {
date = ZonedDateTime.parse(value, PARSER_TZ_RFC);
@ -260,6 +308,6 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable<D
}
}
return date;
return date.toInstant();
}
}

View File

@ -14,7 +14,8 @@ package org.openhab.core.library.types;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.ZonedDateTime;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -36,36 +37,36 @@ public class DateTimeGroupFunctionTest {
@Test
public void testLatestFunction() {
ZonedDateTime expectedDateTime = ZonedDateTime.now();
Instant expectedDateTime = Instant.now();
Set<Item> 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<Item> 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 {

View File

@ -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<Arguments> 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<Arguments> 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") //
);
}
}

View File

@ -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"));