]> git.basschouten.com Git - openhab-addons.git/blob
7d4fd103fad1d65c4329cd242b474edd99cd4773
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ntp.internal.handler;
14
15 import static org.openhab.binding.ntp.internal.NtpBindingConstants.*;
16
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;
28
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;
48
49 /**
50  *
51  * The NTP Refresh Service polls the configured timeserver with a configurable
52  * interval and posts a new event of type ({@link DateTimeType}.
53  *
54  * The {@link NtpHandler} is responsible for handling commands, which are sent
55  * to one of the channels.
56  *
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
62  */
63 @NonNullByDefault
64 public class NtpHandler extends BaseThingHandler {
65
66     /** timeout for requests to the NTP server */
67     private static final int NTP_TIMEOUT = 30000;
68
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);
71
72     private final Logger logger = LoggerFactory.getLogger(NtpHandler.class);
73
74     private final TimeZoneProvider timeZoneProvider;
75
76     /** for publish purposes */
77     private DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ);
78
79     private NtpThingConfiguration configuration = new NtpThingConfiguration();
80
81     private @Nullable ScheduledFuture<?> refreshJob;
82
83     private @Nullable ZoneId timeZoneId;
84
85     /** NTP refresh counter */
86     private int refreshNtpCount = 0;
87     /** NTP system time delta */
88     private long timeOffset;
89
90     public NtpHandler(final Thing thing, final TimeZoneProvider timeZoneProvider) {
91         super(thing);
92         this.timeZoneProvider = timeZoneProvider;
93     }
94
95     @Override
96     public void handleCommand(ChannelUID channelUID, Command command) {
97         if (command == RefreshType.REFRESH) {
98             logger.debug("Refreshing channel '{}' for '{}'.", channelUID.getId(), getThing().getUID());
99             refreshTimeDate();
100         }
101     }
102
103     @Override
104     public void initialize() {
105         logger.debug("Initializing NTP handler for '{}'.", getThing().getUID());
106
107         configuration = getConfigAs(NtpThingConfiguration.class);
108
109         refreshNtpCount = 0;
110
111         if (configuration.timeZone != null) {
112             logger.debug("{} with timezone '{}' set in configuration setting '{}'", getThing().getUID(),
113                     configuration.timeZone, PROPERTY_TIMEZONE);
114             try {
115                 timeZoneId = ZoneId.of(configuration.timeZone);
116             } catch (DateTimeException e) {
117                 timeZoneId = null;
118                 logger.debug("{} using default timezone '{}', because configuration setting '{}' is invalid: {}",
119                         getThing().getUID(), timeZoneProvider.getTimeZone(), PROPERTY_TIMEZONE, e.getMessage());
120             }
121         } else {
122             timeZoneId = null;
123             logger.debug("{} using default timezone '{}', because configuration setting '{}' is null.",
124                     getThing().getUID(), timeZoneProvider.getTimeZone(), PROPERTY_TIMEZONE);
125         }
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);
133                 try {
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);
139                 }
140             } else {
141                 logger.debug("No date format set in config for channel '{}'. Using default format.", CHANNEL_STRING);
142                 dateTimeFormat = DateTimeFormatter.ofPattern(DATE_PATTERN_WITH_TZ);
143             }
144         } else {
145             logger.debug("Missing channel: '{}'", CHANNEL_STRING);
146         }
147         dateTimeFormat.withZone(zoneId);
148
149         logger.debug(
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);
153
154         refreshJob = scheduler.scheduleWithFixedDelay(() -> {
155             try {
156                 refreshTimeDate();
157             } catch (Exception e) {
158                 logger.debug("Exception occurred during refresh: {}", e.getMessage(), e);
159             }
160         }, 0, configuration.refreshInterval, TimeUnit.SECONDS);
161     }
162
163     @Override
164     public void dispose() {
165         logger.debug("Disposing NTP handler for '{}'.", getThing().getUID());
166         ScheduledFuture<?> job = refreshJob;
167         if (job != null) {
168             job.cancel(true);
169         }
170         refreshJob = null;
171         super.dispose();
172     }
173
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;
181         } else {
182             networkTimeInMillis = System.currentTimeMillis() + timeOffset;
183             refreshNtpCount--;
184         }
185
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)));
191     }
192
193     /**
194      * Queries the given timeserver <code>hostname</code> and returns the time
195      * in milliseconds.
196      *
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
200      *         error occurs.
201      */
202     private long getTime(String hostname, int port) {
203         try {
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();
209
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);
216             return serverMillis;
217         } catch (UnknownHostException uhe) {
218             logger.debug(
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) {
224             logger.debug(
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 + "\"]");
229         }
230
231         return System.currentTimeMillis();
232     }
233 }