Establish Reference/Pointers to a $settings.switch object

Been spending hours trying to figure this out, but cant…

Workign in a SmartApp, but seems everytime I try to access $settings, it’s always null…??

Even here:

def installed() {
log.debug “installed with settings: $settings”

// subscribe to events, create scheduled jobs.
}

I tried to look through $settings.inputDevices, and still there’s nothing… What Gives??

def devices = settings.inputDevices

//log.debug

// Find Devices with a specific ID:
//devices.findAll( { it.getDeviceNetworkId() == “${data.Zone}” } ).each {
for (device in devices) {
log.debug “Device ID: ${device.getDeviceNetworkId()}”
if ( “${device.getDeviceNetworkId()}” == “${data.Zone}”) {
log.debug “Found device: ID: ${it.id}, Label: ${it.label}, Name: ${it.name}”
}

Please provide your entire SmartApp (ideally a simple test SmartApp) so that it is much easier to understand the context.

You see, then someone can just copy-paste into their own IDE and confirm the results you are getting.

Be sure to surround your code with three back quotes (```) on the line before and after the code block. That will keep it properly formatted.

Or link to GitHub or Pastebin.

1 Like

This isn’t rocket science I don’t think, so you should be able to understand my code at this point…

 / *
  *  Author: Khile T. Klock (klockksr@gmail.com)  - Leveraged from Andrew Dumaresq (dumaresq@gmail.com)
  */
 definition(
    name: "Sprinkler Konntroler",
    namespace: "klockk",
    author: "Khile T. Klock",
    description: "This will turn on your sprinkler schedule at a specified time, depending on moister from a spruce sensor and the weather forecast",
    category: "Green Living",
    iconUrl: "http://cdn.device-icons.smartthings.com/Outdoor/outdoor12-icn.png",
    iconX2Url: "http://cdn.device-icons.smartthings.com/Outdoor/outdoor12-icn@2x.png"
    //http://cdn.device-icons.smartthings.com/Outdoor/outdoor12-icn@2x.png
 )

preferences {
	section("Schedule") {
		input name: "startTime", title: "Start Time?", type: "time"
	}

    section("Relays to turn on?") {
		input "Relay1", "capability.switch", title: "Phisical Relay 1", required: false
                input "Timer1", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay2", "capability.switch", title: "Phisical Relay 2", required: false
                input "Timer2", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay3", "capability.switch", title: "Phisical Relay 3", required: false
                input "Timer3", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay4", "capability.switch", title: "Phisical Relay 4", required: false
                input "Timer4", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay5", "capability.switch", title: "Phisical Relay 5", required: false
                input "Timer5", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay6", "capability.switch", title: "Phisical Relay 6", required: false
                input "Timer6", title: "Minutes to water", type: "number", required: false, defaultValue: 0
	}
    
    section("Moisture Sensor") {
		input "sensor1", "capability.sensor", required: false
        input name: "highHumidity", title: "How Wet is too Wet (in %)?", type: "number", required: false
	}
    
    section("Zip code..."){
		input "zipcode", "text", title: "Zipcode?", required: false
	}
    
     section( "Notifications" ) {
        input("recipients", "contact", title: "Send notifications to") {
            input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
            input "phone1", "phone", title: "Send a Text Message?", required: false
        }
    }    
 }

def installed() {
	log.debug "Installed with settings: ${settings}" // Docs indicate this should work, but it results in null
	log.debug "Installed with settings: "
    log.debug "    Relay1: ${Relay1} for ${Timer1} min"  // Docs indicate should be able to reference $settings.Relay1 but that doesn't work....
    log.debug "    Relay2: ${Relay2} for ${Timer2} min"
    log.debug "    Relay3: ${Relay3} for ${Timer3} min"
    log.debug "    Relay4: ${Relay4} for ${Timer4} min"
    log.debug "    Relay5: ${Relay5} for ${Timer5} min"
    log.debug "    Relay6: ${Relay6} for ${Timer6} min"
    log.debug "    Run at: $startTime"  
    schedule(startTime, "startTimerCallback")    
}

def updated() {
	log.debug "Updated with settings: ${settings}" // Docs indicate this should work, but it results in null
    log.debug "Updated settings: "
    log.debug "    Relay1: ${Relay1} for ${Timer1} min"
    log.debug "    Relay2: ${Relay2} for ${Timer2} min"
    log.debug "    Relay3: ${Relay3} for ${Timer3} min"
    log.debug "    Relay4: ${Relay4} for ${Timer4} min"
    log.debug "    Relay5: ${Relay5} for ${Timer5} min"
    log.debug "    Relay6: ${Relay6} for ${Timer6} min"    
    log.debug "    Run at: $startTime"  
	unschedule()
    schedule(startTime, "startTimerCallback")    
 }

 def initialize() {
  data.Zone = null
  data.runTimeMin = 0
 }

 private isStormy(json) {
	def STORMY = ['rain', 'snow', 'showers', 'sprinkles', 'precipitation']

	def forecast = json?.forecast?.txt_forecast?.forecastday?.first()
	log.debug "Checking Response"
	if (forecast) {
		def text = forecast?.fcttext?.toLowerCase()
		if (text) {
            log.debug "reponse is: ${text}"
			def result = false
			for (int i = 0; i < STORMY.size() && !result; i++) {
				result = text.contains(STORMY[i])
			}
			return result
		} else {
			return false
		}
	} else {
		log.warn "Did not get a forecast: $json"
		return false
	}
 }

 def startTimerCallback() {
    log.debug "startTimerCallback: Begin"    
    sendNotificationEvent("${app.label}: Checking to see if we should Water the lawn")

    if ((sensor1 && highHumidity) && sensor1.currentHumidity > highHumidity) {
      int hours = 48
      def yesterday = new Date(now() - (/* 1000 * 60 * 60 */ 3600000 * hours).toLong())  
      def lastHumDate = sensor1.latestState('humidity').date
      if (lastHumDate < yesterday) {
   	log.warning "${app.label}: Please check sensor ${sensor1}, no humidity reports for ${hours}+ hours"
          sendNotificationEvent("${app.label}: Please check sensor ${sensor1}, no humidity reports for ${hours}+ hours")
      }
      sendNotificationEvent("${app.label}: Not Watering, because ${sensor1} is at ${sensor1.currentHumidity}")
      log.debug "Not Watering, because ${sensor1} is at ${sensor1.currentHumidity} the cut off is ${highHumidity}"
      return
    }
    if (zipcode) {
      def response = getWeatherFeature("forecast", zipcode)
      if (isStormy(response)) {
        log.debug "Got Rain not Wattering"
    	sendNotificationEvent("${app.label}: Not Watering, the forcast calls for rain.")        
        return
      }
    }
    if (sensor1 && highHumidity) {
      log.debug "The Humidity is: ${sensor1.currentHumidity} and our cut off is ${highHumidity} so we are watering."
    }
    
    sendNotificationEvent("${app.label}: All Checks passed, initiating Sprinkler Konntrol for ${app.label}.")
    log.debug "All Checks passed, initiating Sprinkler Konntrol for ${app.label}."
    
	if (Relay1) {
      runIn(0,turnOnSwitch, [data: [Zone: Relay1, runTimeMin: Timer1]] ) // Passing this data map works beautifully...
    }
	//if (Relay2) {
    //  runIn(##,turnOnSwitch, [data: [Zone: Relay2, runTimeMin: Timer2]])
    //}    
    log.debug "startTimerCallback: End"      
 } 

 def StopTimerCallback(data) {
     log.debug "StopTimerCallback: Begin"    
     log.debug "Relay is $data.Zone" // This is no longer a reference to the device object...??
     // 11:39:00 AM: debug Relay is [id:4ce19db3-a62b-4929-8720-b3055705fb9b, name:Konnected Switch, label:Sprinklers - Front Yard]
    //def devices = settings.inputDevices
    
    //log.debug 
    
// Thought I might try  to re-establish a pointer to the device by enumerating through them, but that didnt work either...
    // Find Devices with a specific ID:
    //devices.findAll( { it.getDeviceNetworkId() == "${data.Zone}" } ).each {
    //for (device in devices) {
    //    log.debug "Device ID: ${device.getDeviceNetworkId()}"
    //    if ( "${device.getDeviceNetworkId()}" == "${data.Zone}") {
    //      log.debug "Found device: ID: ${it.id}, Label: ${it.label}, Name: ${it.name}"
    //      Relay = it
    //      log.debug "Relay is $Relay"        
	//      turnOffSwitch(it)
            turnOffSwitch(data.Zone) // THis doesnt work...
     //   }
    //}    
    log.debug "StopTimerCallback: End"    
 }

 def turnOnSwitch(data) {
    log.debug "turnOnSwitch: Begin"   
    log.debug "Relay is $data.Zone"
    log.debug "runTime is $data.runTimeMin"    
	log.debug "Relay is ${data.Zone.currentSwitch}"
    
    if (data.Zone) {
      sendNotificationEvent("${app.label}: Watering for $data.runTimeMin minutes.")
      log.debug "Watering ($Relay1.label) for $data.runTimeMin minutes."
      data.Zone.on()
      
      // Turn the Relay Back off After specified period of Minutes
      //  We'll pass in the target relay as part of the data map in the runIn() function, but when the Callback occurs, it seems it's no longer a reference to the object...??
      //runIn(60 * data.runTimeMin.toInteger(), StopTimerCallback, [data: [Zone: data.Zone.getDeviceNetworkId()]])
      runIn(60 * data.runTimeMin.toInteger(), StopTimerCallback, [data: [Zone: data.Zone]])
    }
    log.debug "turnOnSwitch: End"        
 }

 def turnOffSwitch(Relay) {
    log.debug "turnOffSwitch: Begin" 
    log.debug "Relay is $Relay" // 	11:39:00 AM: debug Relay is null
	log.debug "Relay is ${Relay.currentSwitch}"
    
    if (Relay.currentSwitch == "on") {
      Relay.off()
    }
    log.debug "turnOffSwitch: End"     
 }

I don’t think the updated() method gets settings as a parameter. It is available as a global in your SmartApp.

Installed() is only called once when the SmartApp is first installed and only will get settings that are provided with the initial install.

2 Likes

Bottom line though is that it’s null no mater what…??

My REAL issue here though is… the StartTimerCallback() initiates fine and has access to everything It should, but in ## mins when the StopTimerCallback() is called, I cant seem to get a pointer to the “switch” device I’ve referenced…

I’ve tried several methods…

When the StopTimerCallback gets called, it gets the data map passed to it, but the Zone item doesn’t seem to be a pointer to the ACTUAL Switch I need to turn off…??

10:37:01 PM: error groovy.lang.MissingPropertyException: No such property: currentSwitch for class: java.lang.String @line 215 (turnOffSwitch)
10:37:01 PM: debug Relay is Sprinklers - Front Yard
10:37:01 PM: debug turnOffSwitch: Begin
10:37:01 PM: debug Relay label is Sprinklers - Front Yard
10:37:01 PM: debug Relay name is Konnected Switch
10:37:01 PM: debug Relay id is 4ce19db3-a62b-4929-8720-b3055705fb9b
10:37:01 PM: debug Relay is [id:4ce19db3-a62b-4929-8720-b3055705fb9b, name:Konnected Switch, label:Sprinklers - Front Yard]
10:37:01 PM: debug StopTimerCallback: Begin

Just what am I missing here???

Very strange. It looks like data types are getting confused. I would try using a local variable for data.Zone and pass that I the callback data.

As for the first question about settings.inputDevices, that is null because don’t have any input named “inputDevices” if you want a list of the devices, you can build one in the installed and updated methods from the settings data (Relay*) and store it in the state container for use in other methods.

1 Like

Good thing - because I’m not a rocket scientist.

I’m not intentionally being a jerk. It’s just that if you want my help, then I’m telling you what I need in order to assist you. Someone else may have a different way of approaching your problem. If I don’t see an obvious issue with a few minutes observation of your code, then my next step is to compile the code in my own IDE in order to replicate the issue and attempt a few experiments.


ButL The code you provided does not even compile/save when pasted into the IDE as a new SmartApp…

startup failed: script_app_metadata_9fd06fef_4a22_44d8_8b99_492eb96dda47: 170: expecting '}', found 'doesnt' @ line 170, column 45. rnOffSwitch(data.Zone) / THis doesnt wor ^ 1 error

Let me fix that for you in my Post Copy comments… LOL

        turnOffSwitch(data.Zone) / THis doesnt work...  <<-- OOps.... I added a slash for you in the original post

I originally was passing the “Relay” object pointer to the TurnOn() or TurnOff() Calls, but then it “SEEMS” like I cant do that and get the RunIN() function to properly work… so then I switched to passing a data map…

Trial and Error… one step at a time…

Eventually, I might want to have a variable # of “Relays” allowing the user to add them as needed… based on their setup… but I’m trying to keep things simple right now, and get the basic logic working, then I can just add in the loops and recursion for more advances setups… :slight_smile:

But yes, it seems like something is different after the RunIn() callback is executed by SmartThings? I assume in the background, ST is just scheduling another instance of the SmartApp to execute later with a different entry point…

Correct.

And if you are relying on the global state map for persistent data, you cannot ensure it is synchronized between instances; use of atomicState can help; but should not be overused. Settings should be fine, since it is only updated by the user explicitly; during install or update.

SO I am passing my OWN data map as part of the RUnIn() call… but it seems like the reference in the map is broken…

What gets passed in with:
runIn(60 * data.runTimeMin.toInteger(), StopTimerCallback, [data: [Zone: data.Zone]])

Turns into:
[id:4ce19db3-a62b-4929-8720-b3055705fb9b, name:Konnected Switch, label:Sprinklers - Front Yard]

When we try to reference it… with:
def StopTimerCallback(data) {
log.debug “Relay is $data.Zone”

So… Obviously tryign to READ SwitchState fails and so does tryign to call .off() :confused:

What is it I am missing here???

At the risk of making myself nearly useless… I recall this or similar problem coming up a few times, but don’t recall the exact explanation or solution, so there’s likely an answer already in the forum or hopefully someone is watching this Topic.

The explanation will probably make sense if it describes the convoluted ways that “Devices” are not really Objects, though they look like Objects. There’s a whole mystery abstraction / security layer(s) involved.

1 Like

LOL… At least I might not be alone! :confused:

THAT’S why I was trying to enumerate through $settings, or possibly only the “Switches” in $settings…
In THAT case, I could just pass the getNetworkID() value… walk through the items in $settings, compare and when they matched… work with that “Object” reference… and be done…
this has stumped me for many many hours tryign various things… and ofcourse, this is my first time programming in Groovy… which isn’t so… :wink:

Here is one relevant Topic about how runIn() data parameters are serialized to JSON…

If I conclude correctly, you simply can’t successfully pass real “objects” to runIn(); so, dereferencing the JSON data to the settings.* object has been mentioned as a solution in a few similar Topics.

Looking over my notes and some prior forum messages, it appears that in general passing device objects in this map doesn’t work because it all gets serialized (essentially converted to JSON strings).

RunIn(0,…) seems to behave differently… i suspect there is some optimization that causes this to be executed without serialization.

For this particular app, i would probably just pass the zone number (1-6) and use that to determine which switch to control.

2 Likes

I beat Tony to the punch by just a few seconds :stopwatch: :blush:

2 Likes

And I happen to find that thread before I found this…

I don’t want to just pass a Zone # and then figure it out, because I eventually want the # of Zones to be dynamic… i.e. the user can add and remove Zones as appropriate, so I’ll never know which # of zones they would have…

Like I said, I would have just tried to enumerate through the ${settings} set and find the switch matching the input NetworkDeviceID, but I cant seem to enumerate through the settings either… :s

Now if I can just figure out the proper method to pass $data MAP to parseJason(), I might be in shape… Hmm…

So I tried to attack it from another angle, and just pass a # and store the device reference in a map at initialization time, but apparently you cant do that?? I get errors when I do this… buut DONT if I use .id, .name, .label, etc… so it doesn’t like the direct reference??

def initialize() {
 def deviceMap = [:]
 
 deviceMap.put(1,${settings.Relay1}) // Throws an error, and stops
 deviceMap.put(2,${settings.Relay2})
 deviceMap.put(3,${settings.Relay3})
 deviceMap.put(4,${settings.Relay4})
 deviceMap.put(5,${settings.Relay5})
 deviceMap.put(6,${settings.Relay6})
}

Played more… and here’s where I am with this part… same question still persists regarding locating the switches in $settings

def initialize() {
 def deviceMap = [:]
 
 deviceMap.put(1,settings.Relay1)
 deviceMap.put(2,settings.Relay2)
 //deviceMap.put(3,${settings.Relay3})
 //deviceMap.put(4,${settings.Relay4})
 //deviceMap.put(5,${settings.Relay5})
 //deviceMap.put(6,${settings.Relay6})
  
    Relay = deviceMap[1] //  groovy.lang.MissingPropertyException: No such property: Relay for class: script_app_ff08c4c0afc5e33b24b66a84237aed3c8a4a6cbb9e6a87685386e7df7b9cdda0 @line 119 (initialize)
    Relay.on([delay: 100])
    deviceMap[1].on([delay: 100]) // Works.....???
}

Why can I access deviceMap one way but not the other???

Maybe @gausnes can comment on why one works and not the other and also clarify if runIn(0, ...) is valid/expected.

On a side note I thought the delay attribute was defunct.

2 Likes

I’ve spend all day and half the night trying various methods…
state maps, blah blah… every angle seems to return me to an error or null…

I’ve FINALLY figured out how to enumerate through $settings… and now can identify the switches from preferences{} and by passing in a previously saved deviceID I can identify the specific switch I want to work with… but it still seems like I am missing how too recreate a variable that references the actual device so I can check it’s state, turn if off or on… this is driving me insane…? (Am I alone??)

I would hope that this sort of routine would yield me a reference/pointer to the actual switch so I can call it’s methods down the road…?? But I’m missing something…

preferences {
	section("Schedule") {
		input name: "startTime", title: "Start Time?", type: "time"
	}

    section("Relays to turn on?") {
		input "Relay1", "capability.switch", title: "Phisical Relay 1", required: false
                input "Timer1", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay2", "capability.switch", title: "Phisical Relay 2", required: false
                input "Timer2", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay3", "capability.switch", title: "Phisical Relay 3", required: false
                input "Timer3", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay4", "capability.switch", title: "Phisical Relay 4", required: false
                input "Timer4", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay5", "capability.switch", title: "Phisical Relay 5", required: false
                input "Timer5", title: "Minutes to water", type: "number", required: false, defaultValue: 0

		input "Relay6", "capability.switch", title: "Phisical Relay 6", required: false
                input "Timer6", title: "Minutes to water", type: "number", required: false, defaultValue: 0
	}
    
    section("Moisture Sensor") {
		input "sensor1", "capability.sensor", required: false
        input name: "highHumidity", title: "How Wet is too Wet (in %)?", type: "number", required: false
	}
    
    section("Zip code..."){
		input "zipcode", "text", title: "Zipcode?", required: false
	}
    
     section( "Notifications" ) {
        input("recipients", "contact", title: "Send notifications to") {
            input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
            input "phone1", "phone", title: "Send a Text Message?", required: false
        }
    }    
}

def installed() {
	//log.debug "Installed with settings: ${settings}"
	log.debug "Installed with settings: "
    log.debug "    Relay1: ${Relay1} for ${Timer1} min"
    log.debug "    Relay2: ${Relay2} for ${Timer2} min"
    log.debug "    Relay3: ${Relay3} for ${Timer3} min"
    log.debug "    Relay4: ${Relay4} for ${Timer4} min"
    log.debug "    Relay5: ${Relay5} for ${Timer5} min"
    log.debug "    Relay6: ${Relay6} for ${Timer6} min"
    log.debug "    Run at: $startTime"  
    //initialize()
    schedule(startTime, "startTimerCallback")    
}

def updated() {
	log.debug "Updated with settings: ${settings}"
    log.debug "Updated settings: "
    log.debug "    Relay1: ${Relay1} for ${Timer1} min - ${Relay1.getId()}"
    log.debug "    Relay2: ${Relay2} for ${Timer2} min - ${Relay2.getId()}"
    log.debug "    Relay3: ${Relay3} for ${Timer3} min - ${Relay3.getId()}"
    log.debug "    Relay4: ${Relay4} for ${Timer4} min - ${Relay4.getId()}"
    log.debug "    Relay5: ${Relay5} for ${Timer5} min - ${Relay5.getId()}"
    log.debug "    Relay6: ${Relay6} for ${Timer6} min - ${Relay6.getId()}"
    log.debug "    Run at: $startTime"  
	unschedule()
    //initialize()
     
    schedule(startTime, "startTimerCallback")    
}

def getSwitchById(id) {
    settings.each { 
      try {
        //def supportedCaps = it.value.capabilities
        //supportedCaps.each {cap ->
        //  log.debug "Device $it.key supports the ${cap.name} capability"
        //}
        if ( it.value.hasCapability("Switch") ) {
          //log.debug "Switch: $it.key = $it.value - ${it.value.getId()}" 
          if ( "${it.value.getId()}" == "${id}" ) {
              log.debug "FOUND $it.value!"
              Switch = it.value
            }
        }
      }
      catch(e) {
        //log.error "$it.value -->> $e"
      }
    }
  return Switch
}

:
:

def StopTimerCallback(data) {
    //initialize()
    log.debug "StopTimerCallback: Begin"    
    log.debug "Relay is $data"
    log.debug "Relay is $data.Zone" // Here's our DeviceId we passed!
    log.debug "Relay state is ${getSwitchById(data.Zone).currentSwitch}" // But I cant get State!?

    //  turnOffSwitch([data: [Zone: data.Zone]])
    log.debug "StopTimerCallback: End"    
}

def startTimerCallback() {
    log.debug "startTimerCallback: Begin"    
    sendNotificationEvent("${app.label}: Checking to see if we should Water the lawn")

    sendNotificationEvent("${app.label}: All Checks passed, initiating Sprinkler Konntrol for ${app.label}.")
    log.debug "All Checks passed, initiating Sprinkler Konntrol for ${app.label}."
    
      runIn(0,turnOnSwitch, [data: [Zone: Relay1, runTimeMin: Timer1]] )
    //if (Relay2) {
    //  runIn(##,turnOnSwitch, [data: [Zone: Relay2, runTimeMin: Timer2]])
    //}    
    log.debug "startTimerCallback: End"      
} 

def turnOnSwitch(data) {
    log.debug "turnOnSwitch: Begin"  
    log.debug "Relay is $data.Zone"
    log.debug "runTime is $data.runTimeMin"    
    
    if (data.Zone) {
      sendNotificationEvent("${app.label}: Watering for $data.runTimeMin minutes.")
      log.debug "Watering ($data.Zone) for $data.runTimeMin minutes."
      if (data.Zone.currentSwitch == "on") {
        log.warn "Uh Oh: ($data.Zone) is already on!?"
      }
      else {
        data.Zone.on()
      }
      
      // Turn the Relay Back off After specified period of Minutes - Passing the DeviceId in the map!!
      runIn(60 * data.runTimeMin.toInteger(), StopTimerCallback, [data: [Zone: data.Zone.getId()]])
    }
    log.debug "turnOnSwitch: End"        
}