Establish Reference/Pointers to a $settings.switch object

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.

  • Search for “runIn” and you may find a helpful post.
  • Browse through the SmartThings GitHub (and you can also globally search for “runIn”) and find other SmartApps to see how they successfully pass a map or what workaround they perform. https://github.com/SmartThingsCommunity/SmartThingsPublic
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"        
}

To the best of my knowledge, you are indeed “missing something”.

The Device Objects (ie, along with their Events, Attributes and Commands) can only be accessed by their explicit input variables or input lists (ie, “Relay1”, “Relay2”, etc.).

There is (in my recollection / experience) no way to assign this reference nor access it indirectly through the “settings” map or similar references.

Repeat after me: Only the input variable can access the Object.

Why? - Because only the customer can set and change the selected inputs, and this ensures that the customer is in full control of which of his/her Devices that your SmartApp can access.

Make sense???

1 Like

I get all that, and all I’m trying to do is reference the item that is in the settings/preferences{} which was input by the user…

i.e a pointer to Relay1
Switch = Relay1
Switch.on()

if I am not mistaken, that can be done…

TurnOnSwitch(Relay1)

def TurnOnSwitch(Switch) {
Switch.on()
}

all of these things "REFERENCE the original user input “Setting.Relay1”

All we’re really trying to do here is avoid creating CRAPPY code and harcoding those object NAMES… i.e. Relay1, Relay2, … etc…

There must be some way…
Since I cant passit through a map via runIn, somehow I have to get my bearings back…
I’ve also considered and tried things like a state.map but haven’t had much luck there either…
Would be nice to just old the reference in state.activeSwitch and recall it on the callback… somehow

This passes the Switch Object by reference and, if you find it is working, it is permitted.


What’s not permitted is anything that could possibly bypass the security mechanism (and, yes, some perfectly “safe” scenarios are crippled as a side-effect).

For example, the state[] map is global (to the SmartApp) and also persistent between invocations.

So: If you could say state.usersSwitch = Relay1 then this is a security risk because if the customer subsequently updates the SmartApp’s preferences and points Relay1 to a different Device Object, and then state.usersSwitch will still point the old input which references an object the customer no longer wishes to permit you to use.

Wouldn’t this be safe if state.usersSwitch = Relay1 was just a pointer to a pointer to the Object? Sure… but that’s not SmartThings’s choice of implementation, due to their design of serializing (to JSON) all persistent data. Furthermore, ensuring that a dereferenced pointer was not illegally manipulated would require extra effort in the architecture.

It may be that you are either overestimating the functionality available in a SmartApp (remember, SmartApps are sandboxed), or are just temporarily too absorbed in your particular implementation choice to come up with a creative alternative.

For arbitrary example, instead of having 1 SmartApp, consider the model of the official Smart Lighting SmartApp and use a Parent-Child configuration. Each Irrigation Channel Child SmartApp manages one Relay (it’s own relay and the specific schedule and settings for that relay). The Parent is where any over-arching functionality can still live.

There are likely good examples out there (though, sadly, Smart Lighting has not been open sourced by SmartThings).

hmmm, not entirely true, you can do some interesting stuff with the settings map and in may ways there are many more efficient ways to capture inputs when you have large number of inputs. This is where groovy is at it’s best for run time references like settings."relay{i}". Mind you there are a few quirks and bugs in ST when using run time references like this one in an input variable (reported these in the past to ST somewhere on the forum and also provided a workaround for them)

You are however correct that the state map cannot store device objects because it’s serialized, the best way to handle it is to save the object id and correlated it back to the object. You’re also correct that you cannot access an object not selected by a user, but the story doesn’t end there.

You however don’t need to go rummaging through the entire settings map, which BTW is a VERY VERY expensive operation in ST to touch the settings map, so definitely not a good idea to iterate through it (and yes I’ve seen timeouts from the platform when there are too many accesses to the settings map), rather use more efficient ways to track your object id’s and references.

3 Likes

Well… I was sort of paraphrasing; and I’m focusing on the problem @klockk is experiencing … i.e., the inability to merge an input list of devices into a single list.

But please confirm for me:

  • Device Objects (i.e., the variable in an Input Statement) cannot be assigned to anything that gets serialized (such as state.* and the parameter to runIn() and perhaps a few other cases). Corrent?

  • Device Objects cannot be assigned to temporary variables (i.e., variables within the scope of a method); e.g., def mySwitch = input1. Or can they?

I know that various properties of the Device Object can be read and combined, which certainly can lead to useful code; but such properties do not include the ability to subscribe to Events nor issue Commands:

e.g.,

def data = []
switches?.each{data << getDeviceData(it, "switch")}
dimmers?.each{data << getDeviceData(it, "dimmer")}

Thanks for pointing this out. I was thinking that the fallback to the settings[] map was a goose-chase.

Correct, since they are serialized and stored to the DB objects are lost barring special cirumstances

Yes they can, the run time environment can manipulate any object and use most of groovy’s features barring a few special restrictions from ST (like reflections or using static objects)

The simple solution to what @klockk is trying to do (if I understood it correctly) is turn off a relay after x seconds/minute, the easiest way to do this would be to pass the relay number in the data parameter to runIn and then access that relay dynamically at runtime like settings."Relay{data.no}"?.off(), simple and efficient.

3 Likes

I agree.

Not coding nearly as frequently as you has me forgetting basic “groovyisms” like the evaluation of strings into variables. Regardless of the gyrations it took to get here, I think you’ve found and recommended an elegant, simple and applicable solution.

3 Likes

BINGO!
As I was trying to do in the first place!Grab the devise Id using getId methods, then later I can make reference to the original “Setting” by name using the format:

    log.debug "settingName is ${switchName}"
    log.debug "Relay is " + settings."${switchName}"
    log.debug "Relay state is " + settings."${switchName}".currentSwitch

This works Beautifully! Thank You for the simple suggestion in your text!

Now…

if things were all dynamic, and all II have is some device identifier, how would one get back to the “Setting” nae in the app without walking the settings map?

I’ve come up with this, but you suggest it is costly to do this…

def getSwitchById(id) {
  String keyName = null
  settings.each { 
      try {
          if ( it.value.hasCapability("Switch") ) {
              //if ( ${it.value.getId()}.is(${id}) ) {
              if ( "${it.value.getId()}" == "${id}" ) {
                log.debug "FOUND $it.key "
                keyName = "$it.key"
              }
          }
      }
      catch(Exception e) {
        //log.error "$it.value -->> $e"
      }
  }
  log.debug "getSwitchById: Returning ${keyName}"
  return keyName
}

One thought is to actually use that function, but build a deviceMap during installed() or updated() and store it in the stateMap, then I should only have to do a simply key lookup most of the time…

BINGO Again! Thanx… YOU, Sir, see what I’m trying to do…
But in the end, I don’t want a SET # of relays/switches… I’d like it to be variable… so the user can hit [+] and add another if they so choose to …

I finally got the one thing working… WooHoo! Thanx to you…

Much more elegant… :slight_smile:

    Integer minDelay = 1  // 'Cuz we want every call to behave the same with the MAP data...    
	//if (Relay1) {
    //  runIn(minDelay,turnOnSwitch, [overwrite: false, data: [Zone: Relay1.getId(), runTimeMin: Timer1.toInteger()]] )
    //  minDelay = minDelay+Timer1.toInteger()
    //}
	//if (Relay2) {
    //  runIn(60 * minDelay ,turnOnSwitch, [overwrite: false, data: [Zone: Relay2.getId(), runTimeMin: Timer2.toInteger()]])
    //  minDelay = minDelay+Timer2.toInteger()
    //}    
	//if (Relay3) {
    //  runIn(60 * minDelay ,turnOnSwitch, [overwrite: false, data: [Zone: Relay3.getId(), runTimeMin: Timer3.toInteger()]])
    //  minDelay = minDelay+Timer3.toInteger()
    //}        
	//if (Relay4) {
    //  runIn(60 * minDelay ,turnOnSwitch, [overwrite: false, data: [Zone: Relay4.getId(), runTimeMin: Timer4.toInteger()]])
    //  minDelay = minDelay+Timer4.toInteger()
    //}        
	//if (Relay5) {
    //  runIn(60 * minDelay ,turnOnSwitch, [overwrite: false, data: [Zone: Relay5.getId(), runTimeMin: Timer5.toInteger()]])
    //  minDelay = minDelay+Timer5.toInteger()
    //}            
	//if (Relay6) {
    //  runIn(60 * minDelay ,turnOnSwitch, [overwrite: false, data: [Zone: Relay6.getId(), runTimeMin: Timer6.toInteger()]])
    //  minDelay = minDelay+Timer6.toInteger()
    //} 
    // Much Better/smaller peice of code here.. In the future, we'll figure out what 1..n really is based on preference settings..
    Integer minMultiplier = 1 // If this was 0, the first call to runIn passes the Map different, so we wait 1 second
    for (int i in 1..6) {
	  if (settings."Relay${i}") {
        log.debug "Scheduling " + "Relay${i}" + " in $minDelay minutes"
        runIn(minMultiplier * minDelay,turnOnSwitch, [overwrite: false, data: [Zone: settings."Relay${i}".getId(), runTimeMin: settings."Timer${i}".toInteger()]] )
        minMultiplier = 60 // AFter the 1st execution, we change to 60 minutes
        minDelay = minDelay + settings."Timer${i}".toInteger() // Make sure we convert the whole thing to Minutes!!
      }
    }
1 Like

For simple automations I tend to find CoRE a suitable option to use and you can create as many as you’d like by adding more pistons (upto about 500)

I thought about using WebCoRE originally…
But figured this was a good time to learn to work with SmartAps and Groovy :slight_smile:
And since I might just share this with the rest of the Konnected community when I’m done… it seems better suited to be done here…