State variable not storing object

@Jim, no, I never opened one, sorry.

No problem. I should be able to create a simple app showing the problem and create a ticket. Thanks for bringing it up!

1 Like

Thank you.

Here was the thread I reported the issue in Jim.

This was when was I was developing the Blink camera DH that I ran into the issue.

I whipped up a simple Parent Child (DH) app to replicate the issue and found that state is now working as it should be, the context is global, whether called from a child or within a parent etc, appears to retain the values. Maybe it was a bug in the way state was being handled when I was writing the DH, it seems to be working fine now. atomicState and State are both able to handle nested maps and both retain context now. (good news)

There is however another issue (?) I found while testing this. After installed() is called by the platform, it immediately calls updated(), is that expected behavior?

Here are the Parent and Child DH apps to replicate:

PARENT SMARTAPP

definition(
    name: "Test Device Parent",
    namespace: "rboy",
    author: "RBoy",
    description: "Test Parent Child app",
    category: "Safety & Security",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan@2x.png",
    singleInstance: false)

preferences {
    page(name: "mainPage")
}

def mainPage() {
    dynamicPage(name: "mainPage", title: "Test Parent Child Device App", install: true, uninstall: true) {
        // Let the user know the current status
        section("Status") {
            def devices = getChildDevices().each { device ->
                log.trace "Found child ${device.displayName}"
                def stuff = device.readStuff()
                paragraph "${device.displayName}: $stuff"
            }
        }

        def physicalHubs = location.hubs.findAll { it.type == physicalgraph.device.HubType.PHYSICAL } // Ignore Virtual hubs
        if (physicalHubs.size() > 1) { // If there is more than one hub then select the hub otherwise we'll the default hub
            section("Hub Selection") {
                paragraph title: "", "Multiple SmartThings Hubs have been detected at this location. Please select the Hub."
                input name: "installHub", type: "hub", title: "Select the Hub", required: true
            }
        }
    }
}

def installed()
{
    log.debug "Installed: $settings"
    initialize()
}

def updated()
{
    log.debug "Updated: $settings"

    unsubscribe()
    unschedule()
    initialize()
}

def uninstalled() {
    log.trace "Uninstalled called"
    getChildDevices().each {device ->
        log.info "Deleting Device $device.displayName"
        deleteChildDevice(device.deviceNetworkId)
    }
}

def initialize() {
    def physicalHubs = location.hubs.findAll { it.type == physicalgraph.device.HubType.PHYSICAL } // Ignore Virtual hubs
    log.trace "Selected Hub ID ${installHub?.id}, All Hubs Types: ${location.hubs*.type}, Names: ${location.hubs*.name}, IDs: ${location.hubs*.id}, IPs: ${location.hubs*.localIP}, Total Hubs Found: ${location.hubs.size()}, Physical Hubs Found: ${physicalHubs.size()}"

    try {
        def existingDevices = getChildDevices()
        log.trace "Found devices $existingDevices"
        if(!existingDevices) {
            if ((physicalHubs.size() > 1) && !installHub) {
                log.error "Found more than one physical hub and user has NOT selected a hub in the SmartApp settings"
                throw new RuntimeException("Select Hub in SmartApp settings") // Lets not continue with out this settings
            }
            if (physicalHubs.size() < 1) {
                log.error "NO Physical hubs found at this location, please contact SmartThings support!"
                throw new RuntimeException("No physical hubs found") // Lets not continue with out this settings
            }
            
            (1..1).each {
                def id = 3000 + it
                log.info "Creating Device ID $id on Hub Id ${physicalHubs.size() > 1 ? installHub.id : physicalHubs[0].id}"
                def childDevice = addChildDevice("rboy", "Test Device", id.toString(), (physicalHubs.size() > 1 ? installHub.id : physicalHubs[0].id), [name: "Test Device $id", label: "Test Device $id", completedSetup: true])
            }

            existingDevices = getChildDevices()
        }

        log.trace "Working with devices $existingDevices"
        
        /*existingDevices.each { device ->
            def stuff = "DeviceID" + device.deviceNetworkId.toString()
            log.trace "Saving stuff to ${device.displayName}: $stuff"
            device.saveStuff(stuff)
            stuff = device.readStuff()
            log.debug "Read stuff from ${device.displayName}: $stuff"
        }*/
    } catch (e) {
        log.error "Error creating device: ${e}"
        throw e // Don't lose the exception here
    }
    
    atomicState.testVar = [name: "inInitialize", value: [loop: "testInitialize", loop1: "testInitialize"]]
    log.warn "From initialize: $atomicState.testVar"
    
    runIn(10, someFunc)
}

def someFunc() {
    log.warn "From someFunc: $atomicState.testVar"
    atomicState.testVar = [name: "inSomeFunc", value: [loop: "testSomeFunc", loop1: "testSomeFunc"]]
    log.warn "After setting someFunc: $atomicState.testVar"
}

def childFunc(child) {
    log.warn "From childFunc: $atomicState.testVar"
    child.log "From childFunc: $atomicState.testVar", "warn"
    atomicState.testVar = [name: "inChildFunc", value: [loop: "testChild", loop1: "testChild"]]
    log.warn "After setting childFunc: $atomicState.testVar"
    child.log "After setting childFunc: $atomicState.testVar", "warn"

    runIn(10, randomFunc)
}

def randomFunc() {
    log.warn "From randomFunc: $state.testVar"
    atomicState.testVar = [name: "inRandomFunc", value: [loop: "testRandomFunc", loop1: "testRandomFunc"]]
    log.warn "After setting randomFunc: $atomicState.testVar"
    
    runIn(10, someFunc)
}

CHILD DH:

metadata {
    definition (name: "Test Device", namespace: "rboy", author: "RBoy") {
        capability "Polling"
        capability "Refresh"

        // Calls from Parent to Child
        command "saveStuff", ["string"]
        command "readStuff"
    }

    tiles(scale: 2) {
        standardTile("refresh", "device.status", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
            state "refresh", action:"refresh.refresh", icon:"st.secondary.refresh"
        }
        
        main "refresh"
        details(["refresh"])
    }
}

def initialize() {
    log.trace "Initialize called settings: $settings"
    try {
        if (!state.init) {
            state.init = true
        }
        response(refresh()) // Get the updates
    } catch (e) {
        log.warn "updated() threw $e"
    }
}

def updated() {
    log.trace "Update called settings: $settings"
    try {
        if (!state.init) {
            state.init = true
        }
        response(refresh()) // Get the updates
    } catch (e) {
        log.warn "updated() threw $e"
    }
}

def refresh() {
	log.trace "Refresh called"
    parent.childFunc(this)
}

// PARENT CHILD INTERFACES
def void saveStuff(def stuff) {
    log.debug "SaveStuff called with: $stuff"
    state.stuff = stuff as String
}

def String readStuff() {
    log.debug "ReadStuff called, returning: ${state.stuff}"
    return state.stuff as String
}


// Print log message from parent
def void log(message, level = "trace") {
    switch (level) {
        case "trace":
        log.trace "LOG FROM PARENT>" + message
        break;

        case "debug":
        log.debug "LOG FROM PARENT>" + message
        break

        case "info":
        log.info "LOG FROM PARENT>" + message
        break

        case "warn":
        log.warn "LOG FROM PARENT>" + message
        break

        case "error":
        log.error "LOG FROM PARENT>" + message
        break

        default:
            log.error "LOG FROM PARENT>" + message
        break;
    }            
}

Are operations (reads and writes) to a single object in the data store linearizable? Do read/write operations happen atomically at the database?

My understanding is that:

  • state is read once at SmartApp start and written once at StartApp end, but only if modified.

  • atomicState is read on demand whenever requested and written on demand whenever requested. From experience I found that the atomicState setter only captures parent level updates to its members, i.e. atomicState.existingObject = x will cause an immediate database write/update, whereas atomicState.existingObject.property = x will not.

I also empirically found out that setting any member of state will cause the state to be written at the end of a SmartApp execution, so using atomicState to write something mixed up with using state to write something as well will cause the state to ultimately overwrite what was saved by atomicState.

1 Like

Thanks for that!

Whenever there is a read to atomicState/State, there is a read that receives an object from a data store. Similarly for writes, the atomicState/State object is written or updated at the database.

What I am basically asking is how these reads and writes happen at the database. Does each of these operations acquire locks so that all operations to the same object at the database happen atomically and do read operations reflect the result of applying all previous write/update operations (linearizability)?

You can test that. But if you write to atomicState and then read it, it will reflect the change. I doubt it there is any actual persistent lock on any table row, as the write itself is atomic (single UPDATE query) and the read is atomic itself (single SELECT query). If you are asking about two instances running simultaneously, I am pretty sure writing to atomicState in one instance would result in your data being available to a read from atomicState in the second, provided the read started after the write. What I do not know and haven’t tried is to find out if the data is written as a whole (i.e. even though updating one property, the whole atomicState gets written) and what happens if one instance updates a property and a second instance writes another property - does the second write overwrite the first? I can test that. I also do not know if writing a property to atomicState involves any internal read from db, update just the one property and write back.

1 Like

Perfect, that’s what I was looking for, thank you!

Yes, in that case the object gets overwritten by the second instance.

I was trying to figure out if that is possible too, but haven’t yet. I remember last time I tried to write a property of an atomicState object alone I could not observe the change. Instead, I had to write the whole object every single time (although I might have been doing something wrong…). If the whole object needs to get written every time, that does not sound very efficient…

I think it’s more of a nested property issue. atomicState.propA.propB = x will not trigger a db write. You need to set an object

def prop = atomicState.propA

then set its propB property

prop.propB = x

then set it back:

atomicState.propA = prop
1 Like