2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ntp.internal.handler;
15 import static org.openhab.binding.ntp.internal.NtpBindingConstants.*;
17 import java.io.IOException;
18 import java.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.time.DateTimeException;
21 import java.time.Instant;
22 import java.time.ZoneId;
23 import java.time.ZonedDateTime;
24 import java.time.format.DateTimeFormatter;
25 import java.util.Objects;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import org.apache.commons.net.ntp.NTPUDPClient;
30 import org.apache.commons.net.ntp.TimeInfo;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.ntp.internal.config.NtpStringChannelConfiguration;
34 import org.openhab.binding.ntp.internal.config.NtpThingConfiguration;
35 import org.openhab.core.i18n.TimeZoneProvider;
36 import org.openhab.core.library.types.DateTimeType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.Channel;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
51 * The NTP Refresh Service polls the configured timeserver with a configurable
52 * interval and posts a new event of type ({@link DateTimeType}.
54 * The {@link NtpHandler} is responsible for handling commands, which are sent
55 * to one of the channels.
57 * @author Marcel Verpaalen - Initial contribution OH2 ntp binding
58 * @author Thomas.Eichstaedt-Engelen - OH1 ntp binding (getTime routine)
59 * @author Markus Rathgeb - Add locale provider
60 * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
61 * @author Laurent Garnier - null annotations, TimeZoneProvider, configuration settings cleanup
64 public class NtpHandler extends BaseThingHandler {
66 /** timeout for requests to the NTP server */
67 private static final int NTP_TIMEOUT = 30000;
69 public static final String DATE_PATTERN_WITH_TZ = "yyyy-MM-dd HH:mm:ss z";
70 private static final DateTimeFormatter DATE_FORMATTER_WITH_TZ = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ);
72 private final Logger logger = LoggerFactory.getLogger(NtpHandler.class);
74 private final TimeZoneProvider timeZoneProvider;
76 /** for publish purposes */
77 private DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ);
79 private NtpThingConfiguration configuration = new NtpThingConfiguration();
81 private @Nullable ScheduledFuture<?> refreshJob;
83 private @Nullable ZoneId timeZoneId;
85 /** NTP refresh counter */
86 private int refreshNtpCount = 0;
87 /** NTP system time delta */
88 private long timeOffset;
90 public NtpHandler(final Thing thing, final TimeZoneProvider timeZoneProvider) {
92 this.timeZoneProvider = timeZoneProvider;
96 public void handleCommand(ChannelUID channelUID, Command command) {
97 if (command == RefreshType.REFRESH) {
98 logger.debug("Refreshing channel '{}' for '{}'.", channelUID.getId(), getThing().getUID());
104 public void initialize() {
105 logger.debug("Initializing NTP handler for '{}'.", getThing().getUID());
107 configuration = getConfigAs(NtpThingConfiguration.class);
111 if (configuration.timeZone != null) {
112 logger.debug("{} with timezone '{}' set in configuration setting '{}'", getThing().getUID(),
113 configuration.timeZone, PROPERTY_TIMEZONE);
115 timeZoneId = ZoneId.of(configuration.timeZone);
116 } catch (DateTimeException e) {
118 logger.debug("{} using default timezone '{}', because configuration setting '{}' is invalid: {}",
119 getThing().getUID(), timeZoneProvider.getTimeZone(), PROPERTY_TIMEZONE, e.getMessage());
123 logger.debug("{} using default timezone '{}', because configuration setting '{}' is null.",
124 getThing().getUID(), timeZoneProvider.getTimeZone(), PROPERTY_TIMEZONE);
126 ZoneId zoneId = Objects.requireNonNullElse(timeZoneId, timeZoneProvider.getTimeZone());
127 Channel stringChannel = getThing().getChannel(CHANNEL_STRING);
128 if (stringChannel != null) {
129 String dateTimeFormatString = stringChannel.getConfiguration()
130 .as(NtpStringChannelConfiguration.class).DateTimeFormat;
131 if (!dateTimeFormatString.isEmpty()) {
132 logger.debug("Date format set in config for channel '{}': {}", CHANNEL_STRING, dateTimeFormatString);
134 dateTimeFormat = DateTimeFormatter.ofPattern(dateTimeFormatString);
135 } catch (IllegalArgumentException ex) {
136 logger.debug("Invalid date format set in config for channel '{}'. Using default format. ({})",
137 CHANNEL_STRING, ex.getMessage());
138 dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ);
141 logger.debug("No date format set in config for channel '{}'. Using default format.", CHANNEL_STRING);
142 dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ);
145 logger.debug("Missing channel: '{}'", CHANNEL_STRING);
147 dateTimeFormat.withZone(zoneId);
150 "Initialized NTP handler '{}' with configuration: host '{}', port {}, refresh interval {}, refresh frequency {}, timezone {}.",
151 getThing().getUID(), configuration.hostname, configuration.serverPort, configuration.refreshInterval,
152 configuration.refreshNtp, zoneId);
154 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
157 } catch (Exception e) {
158 logger.debug("Exception occurred during refresh: {}", e.getMessage(), e);
160 }, 0, configuration.refreshInterval, TimeUnit.SECONDS);
164 public void dispose() {
165 logger.debug("Disposing NTP handler for '{}'.", getThing().getUID());
166 ScheduledFuture<?> job = refreshJob;
174 private synchronized void refreshTimeDate() {
175 long networkTimeInMillis;
176 if (refreshNtpCount <= 0) {
177 networkTimeInMillis = getTime(configuration.hostname, configuration.serverPort);
178 timeOffset = networkTimeInMillis - System.currentTimeMillis();
179 logger.debug("{} delta system time: {}", getThing().getUID(), timeOffset);
180 refreshNtpCount = configuration.refreshNtp;
182 networkTimeInMillis = System.currentTimeMillis() + timeOffset;
186 ZoneId zoneId = Objects.requireNonNullElse(timeZoneId, timeZoneProvider.getTimeZone());
187 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(networkTimeInMillis), zoneId);
188 updateState(CHANNEL_DATE_TIME, new DateTimeType(zoned));
189 dateTimeFormat.withZone(zoneId);
190 updateState(CHANNEL_STRING, new StringType(dateTimeFormat.format(zoned)));
194 * Queries the given timeserver <code>hostname</code> and returns the time
197 * @param hostname the timeserver hostname to query
198 * @param port the timeserver port to query
199 * @return the time in milliseconds or the current time of the system if an
202 private long getTime(String hostname, int port) {
204 NTPUDPClient timeClient = new NTPUDPClient();
205 timeClient.setDefaultTimeout(NTP_TIMEOUT);
206 InetAddress inetAddress = InetAddress.getByName(hostname);
207 TimeInfo timeInfo = timeClient.getTime(inetAddress, port);
208 timeInfo.computeDetails();
210 long serverMillis = timeInfo.getReturnTime() + timeInfo.getOffset();
211 ZoneId zoneId = Objects.requireNonNullElse(timeZoneId, timeZoneProvider.getTimeZone());
212 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(serverMillis), zoneId);
213 logger.debug("{} Got time update from host '{}': {}.", getThing().getUID(), hostname,
214 zoned.format(DATE_FORMATTER_WITH_TZ));
215 updateStatus(ThingStatus.ONLINE);
217 } catch (UnknownHostException uhe) {
219 "{} The given hostname '{}' of the timeserver is unknown -> returning current sytem time instead. ({})",
220 getThing().getUID(), hostname, uhe.getMessage());
221 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
222 "@text/offline.comm-error-unknown-host [\"" + hostname + "\"]");
223 } catch (IOException ioe) {
225 "{} Couldn't establish network connection to host '{}' -> returning current sytem time instead. ({})",
226 getThing().getUID(), hostname, ioe.getMessage());
227 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
228 "@text/offline.comm-error-connection [\"" + hostname + "\"]");
231 return System.currentTimeMillis();