/**
* Driver: Dynamic ip updater script
* Author: lgkahn
*
* Change Log:
*
* 2025.01.05 - Initial implementation
* v2 add lastupdate and lastupdatecheck attributes, get rid of duplicate check. reorder inputs
* v 2.1 add last result attribute, add actuator capability so it can be added to a dashboard to see status attributes.
*/
public static String version() { return "v2.1"; }
// Metadata -------------------------------------------------------------------------------------------------------------------
metadata {
definition(name: "Dynamic IP Updater Script", namespace: "lgkahn", author: "lgkahn", importUrl: "https://raw.githubusercontent.com/lgkahn/ecowitt/master/ddnsfree_updater.groovy") {
attribute "lastUpdate", "string"
attribute "dynamicIPResult","string"
attribute "currentIP", "string"
attribute "lastUpdate", "string"
attribute "lastUpdateCheck", "string"
attribute "nextUpdateCheck", "string"
attribute "lastResult", "string"
capability "Actuator"
command "DDNSUpdate"
}
preferences {
input(name: "DDNSName", type: "text", title: "Dynamic DNS Name to use to resolve a changing ip address.", description: "Enter DDNS Name", required: false)
input(name: "DDNSRefreshTime", type: "number", title: "How often (in Hours) to check/resolve the DDNS Name to discover if your IP address changed? (Range 1 - 1440, Default 24)?", range: "1..1440", defaultValue: 3, required: false)
input(name: "DDNSUsername", type: "String", title: "DDNS Username ?", required: true)
input(name: "DDNSPassword", type: "password", title: "DDNS Password ?", required: true)
input(name: "DDNSBaseURL", type: "String", title: "DDNS URL ?", required: true)
input(name: "logLevel", type: "enum", title: "Log Verbosity", description: "Default: 'Debug' for 30 min and 'Info' thereafter", options: [0:"Error", 1:"Warning", 2:"Info", 3:"Debug", 4:"Trace"], multiple: false, defaultValue: 3, required: true);
}
}
// Preferences ----------------------------------------------------------------------------------------------------------------
import groovy.json.JsonSlurper;
void publicIpReq(cookie){
params = [
uri: "https://api.ipify.org?format=json",
headers: [
Accept: "application/json"
]
]
if(debugEnable)log.debug params
asynchttpGet("getPublicIp", params)
}
@SuppressWarnings('unused')
void getPublicIp(resp, data){
try{
if (resp.getStatus() == 200){
if (debugEnable) log.debug resp.data
def jSlurp = new JsonSlurper()
Map ipData = (Map)jSlurp.parseText((String)resp.data)
logDebug( "got ip = ${ipData.ip}")
attributeUpdateString(ipData.ip, "currentIP");
} else {
if (!warnSuppress) log.warn "Status ${resp.getStatus()} while fetching Public IP"
}
} catch (Exception ex){
if (!warnSuppress) log.warn ex
}
}
// ------------------------------------------------------------
Integer logGetLevel() {
//
// Get the log level as an Integer:
//
// 0) log only Errors
// 1) log Errors and Warnings
// 2) log Errors, Warnings and Info
// 3) log Errors, Warnings, Info and Debug
// 4) log Errors, Warnings, Info, Debug and Trace/diagnostic (everything)
//
// If the level is not yet set in the driver preferences, return a default of 2 (Info)
// Declared public because it's being used by the child-devices as well
//
if (settings.logLevel != null) return (settings.logLevel.toInteger());
return (2);
}
// ------------------------------------------------------------
private Boolean attributeUpdateString(String val, String attribute) {
if ((device.currentValue(attribute) as String) != val) {
sendEvent(name: attribute, value: val);
return (true);
}
return (false);
}
// ------------------------------------------------------------
def DDNSUpdate()
{
logDebug( "Calling IP_Update()")
def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone)
sendEvent(name: "lastUpdateCheck", value: now)
IP_Check()
}
// -------------------------------------------------------------
private String IP_Check() {
//
// Get the gateway address (either MAC or IP) from the properties and, if valid and not done already, update the driver DNI
// Return "error") invalid address entered by the user
// null) same address as before
// "") new valid address
//
logDebug("IP_Check()");
// first get current publicip
publicIpReq()
dni = device.currentValue("currentIP")
logDebug( "got public ip = $dni")
if (dni) {
// now resolv dynmaic ip
logDebug("calling dns Update url = $params")
def String ddnsname = settings.DDNSName
def Number ddnsupdatetime = settings.DDNSRefreshTime
logDebug("DDNS Name = $ddnsname")
logDebug("DDNS Refresh Time = $ddnsupdatetime")
if ((ddnsname != null) && (ddnsname != ""))
{
logDebug("Got ddns name $ddnsname")
Map params = [
uri: "https://8.8.8.8/resolve?name=$ddnsname&type=A",
contentType: "text/plain",
timeout: 20
]
logDebug("calling dns query url = $params")
asynchttpGet("nsCallback", params)
}
}
}
// Conversion -----------------------------------------------------------------------------------------------------------------
// Logging --------------------------------------------------------------------------------------------------------------------
void logDebugOff() {
//
// runIn() callback to disable "Debug" logging after 30 minutes
// Cannot be private
//
if (logGetLevel() > 2) device.updateSetting("logLevel", [type: "enum", value: "2"]);
}
// ------------------------------------------------------------
private void logError(String str) { log.error(str); }
private void logWarning(String str) { if (logGetLevel() > 0) log.warn(str); }
private void logInfo(String str) { if (logGetLevel() > 1) log.info(str); }
private void logDebug(String str) { if (logGetLevel() > 2) log.debug(str); }
private void logTrace(String str) { if (logGetLevel() > 3) log.trace(str); }
// ------------------------------------------------------------
void installed() {
//
// Called once when the driver is created
//
try {
logDebug("installed()");
publicIpReq()
}
catch (Exception e) {
logError("Exception in installed(): ${e}");
}
}
// -------------------------------------------------------------
def updateCallback(resp, data)
{
logDebug("in ddns update callback")
// test change
if (resp == null)
logError("Dynamic DNS update failed return was null!")
else
{
def String checkString = "good " + device.currentValue("currentIP")
log.debug "check string = $checkString"
if ((resp.data == "nochg") || (resp.data == "good") || (resp.data == checkString))
{
logInfo("Dynamic DNS succesfully updated!")
def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone)
sendEvent(name: "lastUpdate", value: now)
sendEvent(name: "lastResult", value: resp.data)
}
else
{
logError("Dynamic DNS update failed - return code = ${resp.data}!")
sendEvent(name: "lastResult", value: resp.data)
}
} // not null
}
// --------------------------------------------------------------
def performUpdate(base, uname, pwd, newIP, host)
{
logDebug( "in peformUpdate")
Map params = [
uri: "${base}hostname=${host}&myip=${newIP}&username=${uname}&pwd=${pwd}",
contentType: "text/plain",
timeout: 20
]
logDebug("calling dns Update url = $params")
asynchttpGet("updateCallback", params)
}
// -----------------------------------------------------------
def nsCallback(resp, data)
{
// test change
logDebug "in callback"
if (resp == null)
{
logError("Error - dynamic ip name ${settings.DDNSName} does not resolve!")
}
else
{
def jSlurp = new JsonSlurper()
Map ipData = (Map)jSlurp.parseText((String)resp.data)
logDebug( "ipdata = $ipData")
if ((ipData == null) || (ipData.Status != 0))
{
logError("Error - dynamic ip name ${settings.DDNSName} does not resolve!")
}
else
{
def String oldIP = ipData.Answer.data[0]
sendEvent(name:"dynamicIPResult", value:ipData.Answer.data[0])
// now compare ip to our own and if different reset and log
if ((oldIP != null) && (oldIP != ""))
{
def String currentIP = device.currentValue("currentIP")
logInfo("Comparing resolved IP: $oldIP to $currentIP")
if (currentIP != oldIP)
{
logInfo("IP address has Changed !!! Re-registering !")
def String baseURL = device.getSetting("DDNSBaseURL")
def String uname = device.getSetting("DDNSUsername")
def String pwd = device.getSetting("DDNSPassword")
def String host = device.getSetting("DDNSName")
performUpdate(baseURL,uname,pwd,currentIP,host)
}
else logInfo("No change - Update not necessary.")
}
}
}
}
// --------------------------------------------------
void DNSCheckCallback()
{
logInfo("Dns Update Check Callback Startup")
def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone)
sendEvent(name: "lastUpdateCheck", value: now)
updated()
}
// ---------------------------------------------
void updated() {
//
// Called everytime the user saves the driver preferences
//
try {
logDebug("updated()");
publicIpReq()
// Unschedule possible previous runIn() calls
unschedule();
// lgk if ddns name resolve this first and do ip check before dniupdatr.. ALSO schedule the re-check.
def String ddnsname = settings.DDNSName
def Number ddnsupdatetime = settings.DDNSRefreshTime
logDebug("DDNS Name = $ddnsname")
logDebug("DDNS Refresh Time = $ddnsupdatetime")
if ((ddnsname != null) && (ddnsname != ""))
{
logDebug("Got ddns name $ddnsname")
// now resolve
Map params = [
uri: "https://8.8.8.8/resolve?name=$ddnsname&type=A",
contentType: "text/plain",
timeout: 20
]
logDebug("calling dns Update url = $params")
asynchttpGet("nsCallback", params)
}
// now schedule next run of update
if ((ddnsupdatetime != null) && (ddnsupdatetime != 00))
{
use(groovy.time.TimeCategory) {
def thesecs = ddnsupdatetime * 3600
logInfo("Rescheduling IP Address Check to run again in $thesecs seconds.")
runIn(thesecs, "DNSCheckCallback");
def Integer ntime = ddnsupdatetime
def next = new Date() + ntime.hour
def now = next.format('MM/dd/yyyy h:mm a',location.timeZone)
sendEvent(name: "nextUpdateCheck", value: now)
}
}
// Turn off debug log in 30 minutes
if (logGetLevel() > 2) runIn(1800, logDebugOff);
}
catch (Exception e) {
logError("Exception in updated(): ${e}");
}
}
// ------------------------------------------------------------
void uninstalled() {
//
// Called once when the driver is deleted
//
}