/***************************************************************************************************************** * Copyright Daniel Terryn * * Name: NTP Client to retrieve Date/Time from local NTP server.... * * Date: 2019-09-23 * * Version: 1.20 * * Author: Daniel Terryn * * Description: A driver to retrieve the current time from an NTP server and update the hub.... * * License: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. ***************************************************************************************************************** * Change History: * * Date Who What * ---- --- ---- * 2019-09-22 Daniel Terryn Original Creation * 2019-09-23 Daniel Terryn Send event when time set, fixed NTP time calculation, more choices for time difference configuration, refactoring, show NTP Server Date as Event, add force command * * */ double SECONDS_1900_TO_EPOCH() {return 2208988800.0 as double} metadata { definition (name: "NTP Client", author: "dan.t", namespace: "dan.t", importUrl: "https://raw.githubusercontent.com/danTapps/Hubitat/master/Drivers/NTP%20Client/NTP_client.groovy") { capability "Actuator" capability "Initialize" capability "Refresh" attribute "lastNTPdate", "string" attribute "lastHubDate", "string" attribute "lastDiffMS", "number" attribute "updateHubTimeTo", "string" command "force" } preferences { input ( name: "ntpIP", type: "text", title: "NTP Server IP Address", description: "IP Address in form 192.168.1.226", required: true, displayDuringSetup: true ) input ( name: "ntpPort", type: "text", title: "NTP Server Port", description: "port in form of 123", required: true, displayDuringSetup: true, default: 123 ) input ( name: "minTimeDiff", title: "Minimum time difference between Hub and NTP Server to update time.", type: "enum", options: [ 180000 : "3 Minutes", 300000 : "5 Minutes", 600000 : "10 Minutes", 1200000 : "20 Minutes", 1800000 : "30 Minutes", 2400000 : "40 Minutes", 3000000 : "50 Minutes", 3600000 : "1 Hour", 7200000 : "2 Hours", 10800000 : "3 Hours" ], displayDuringSetup: true, required: true ) input ( name: 'pollInterval', type: 'enum', title: 'Update interval (in minutes)', options: ['10', '30', '60', '120', '300'], required: true, displayDuringSetup: true ) input ( name: "configLoggingLevel", title: "Live Logging Level:\nMessages with this level and higher will be logged.", type: "enum", options: [ "0" : "None", "1" : "Error", "2" : "Warning", "3" : "Info", "4" : "Debug", "5" : "Trace" ], defaultValue: "3", displayDuringSetup: true, required: false ) } } def installed() { logger("Executing 'installed()'", "info") initialize() } def initialize() { logger("Executing 'initialize()'", "debug") updated() } def updated() { logger("Executing 'updated()'", "debug") configure() } def configure() { logger("Executing 'configure()'", "info") state.loggingLevel = (configLoggingLevel) ? configLoggingLevel.toInteger() : 3 updateDeviceNetworkID() unschedule() if (Integer.parseInt(settings.pollInterval) < 61) schedule("0 0/${settings.pollInterval} * 1/1 * ? *", refresh) else schedule("0 0 0/${Integer.parseInt(settings.pollInterval)/60} 1/1 * ? *", refresh) refresh() } def parse(description) { logger("Executing 'parse() ${description}'", "debug") try { def encrResponse = parseLanMessage(description).payload byte[] rawBytes = hubitat.helper.HexUtils.hexStringToByteArray(encrResponse); def hubTimeMS = now() def newTimeMS = getNTPTimeMS(rawBytes, hubTimeMS) logger("NTP Server returned time of ${newTimeMS} aka ${new Date(newTimeMS.toLong())}", "info") logger("Hub is ${hubTimeMS} aka ${new Date(hubTimeMS)}", "info") def timeDiff = newTimeMS - hubTimeMS as long if (timeDiff < 0) timeDiff = timeDiff * -1 logger("Time Diff is ${timeDiff} ms", "debug") logger("minTimeDiff is ${minTimeDiff} ms", "debug") sendEvent(name: "lastNTPdate", value: (new Date(newTimeMS.toLong())).toString()) sendEvent(name: "lastHubDate", value: (new Date(hubTimeMS)).toString()) sendEvent(name: "lastDiffMS", value: timeDiff) if ((timeDiff >= Long.parseLong(minTimeDiff)) || (state?.force == true)) { if (state?.force == true) logger("Force Update Hub Time to ${(new Date(newTimeMS.toLong())).toString()}", "info") else logger("Update Hub Time to ${(new Date(newTimeMS.toLong())).toString()}", "info") sendEvent(name: "updateHubTimeTo", value: (new Date(newTimeMS.toLong())).toString()) location.hub.updateSystemTime(new Date(newTimeMS.toLong())) } state.force = false } catch (error) { logger("${error}", "warn") } } def refresh() { //def SECONDS_1900_TO_EPOCH = 2208988800.0 as double logger("Executing 'refresh()'", "debug") byte[] rawBytes = [0x1b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0] rawBytes = encodeTimestamp(rawBytes, 40, (now()/1000)+SECONDS_1900_TO_EPOCH()) String stringBytes = hubitat.helper.HexUtils.byteArrayToHexString(rawBytes) def myHubAction = new hubitat.device.HubAction(stringBytes, hubitat.device.Protocol.LAN, [type: hubitat.device.HubAction.Type.LAN_TYPE_UDPCLIENT, destinationAddress: "${ntpIP}:${ntpPort}", encoding: hubitat.device.HubAction.Encoding.HEX_STRING]) sendHubCommand(myHubAction) } def force() { logger("Executing 'force()'", "debug") state.force = true logger("Forcing Date/Time update in 2 seconds", "info") runIn(2, "refresh", [overwrite: true]) } def getTimestamp(byte[] array, int pointer) { logger("Executing 'getTimestamp()'", "debug") def r = 0.0 as double for(int i=0; i<8; i++) { r += unsignedByteToShort(array[pointer+i]) * Math.pow(2, (3-i)*8) } return r } short unsignedByteToShort(byte b) { if((b & 0x80)==0x80) return (short) (128 + (b & 0x7f)) else return (short) b } def getNTPTimeMS(byte[] array, destinationTimestamp) { logger("Executing 'getNTPTimeMS()'", "debug") // See the packet format diagram in RFC 2030 for details /* byte[] referenceIdentifier = [0, 0, 0, 0] def leapIndicator = (byte) ((array[0] >> 6) & 0x3) def version = (byte) ((array[0] >> 3) & 0x7) def mode = (byte) (array[0] & 0x7) def stratum = unsignedByteToShort(array[1]) def pollInterval = array[2] def precision = array[3] def rootDelay = (array[4] * 256.0) + unsignedByteToShort(array[5]) + (unsignedByteToShort(array[6]) / 256.0) + (unsignedByteToShort(array[7]) / 65536.0) def rootDispersion = (unsignedByteToShort(array[8]) * 256.0) + unsignedByteToShort(array[9]) + (unsignedByteToShort(array[10]) / 256.0) + (unsignedByteToShort(array[11]) / 65536.0) referenceIdentifier[0] = array[12]; referenceIdentifier[1] = array[13]; referenceIdentifier[2] = array[14]; referenceIdentifier[3] = array[15]; */ referenceTimestamp = getTimestamp(array, 16) originateTimestamp = getTimestamp(array, 24) receiveTimestamp = getTimestamp(array, 32) transmitTimestamp = getTimestamp(array, 40) return (now() + (((receiveTimestamp - originateTimestamp) + (transmitTimestamp - ((destinationTimestamp/1000) + SECONDS_1900_TO_EPOCH()))) / 2)*1000) } def encodeTimestamp(array,pointer, timestamp) { logger("Executing 'encodeTimestamp()'", "debug") // Converts a double into a 64-bit fixed point for(i=0; i<8; i++) { // 2^24, 2^16, 2^8, .. 2^-32 double base = Math.pow(2, (3-i)*8); // Capture byte value array[pointer+i] = (byte) (timestamp / base); // Subtract captured value from remaining total timestamp = timestamp - (unsignedByteToShort(array[pointer+i]) * base); } array[7+pointer] = 0x0; return array; } def getHostAddress() { logger("Executing 'getHostAddress()'", "debug") logger("Using ip: ${ntpIP} and port: ${ntpPort} for device: ${device.id}", "debug") return ntpIP + ":" + ntpPort } def updateDeviceNetworkID() { logger("Executing 'updateDeviceNetworkID'", "debug") if(device.deviceNetworkId!=getDNIfromIPandPort(ntpIP, ntpPort)) { logger("setting deviceNetworkID = ${getDNIfromIPandPort(ntpIP, ntpPort)}", "debug") device.setDeviceNetworkId(getDNIfromIPandPort(ntpIP, ntpPort)) } } def getDNIfromIPandPort(ipAddress, port) { logger("Executing 'getDNIfromIPandPort'", "debug") def iphex = convertIPtoHex(ipAddress) def porthex = convertPortToHex(port) return "${iphex}:${porthex}" } def convertIPtoHex(ipAddress) { logger("Executing 'convertIPtoHex'", "debug") String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() return hex } def convertPortToHex(port) { logger("Executing 'convertPortToHex'", "debug") String hexport = port.toString().format( '%04x', port.toInteger() ) return hexport } /** * logger() * * Wrapper function for all logging. **/ private logger(msg, level = "debug") { switch(level) { case "error": if (state.loggingLevel >= 1) log.error msg break case "warn": if (state.loggingLevel >= 2) log.warn msg break case "info": if (state.loggingLevel >= 3) log.info msg break case "debug": if (state.loggingLevel >= 4) log.debug msg break case "trace": if (state.loggingLevel >= 5) log.trace msg break default: log.debug msg break } }