Why isn't my map persisting through the app?

Hi,

I’m pretty new to Groovy, but believe I’ve mostly figured out how to develop when I need.

What I’m trying to do now is write an app that counts the number of motion events from sunset to sunrise and send out a notification of the count at sunrise.

I have it working with a single motion sensor, but I’m trying to get fancy and do it for any number of motion sensors, and using a map to store the count.

The problem I’m having is that the map doesn’t persist through different functions. So when I try to call a “get” method on my map and assign it to an integer, the app dies because I’m trying to assign a null to an integer.

Here’s the app:

/**
 *  Count Motion Events
 *
 *  Copyright 2014 Top Lertpanyavit
 *
 *  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.
 *
 */
definition(
    name: "Count Motion Events",
    namespace: "topl",
    author: "Top Lertpanyavit",
    description: "Counts number of motion events for a motion sensor from sunset until sunrise and sends push notification of count at sunrise.",
    category: "My Apps",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")


preferences {
    section("Motion sensor(s) to monitor..."){
        input "motions", "capability.motionSensor", multiple: true, required: true
    }
    section ("Zip code for sunset and sunset times (optional, defaults to location coordinates)...") {
        input "zipCode", "text", title: "Zip code", required: false
    }
    section("Notify me...") {
        input "pushNotification", "bool", title: "Push notification"
    }
}

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

def updated() {
    log.debug "Updated with settings: ${settings}"
    unsubscribe()
    initialize()
}

def initialize() {
    subscribe(motions, "motion.active", motionActiveHandler)

    // Initialize countMap
    // Set counts to 0
    state.countMap = [:]
    motions.each { motion -> state.countMap.put("${motion.displayName}", 0)
        int count = state.countMap.get("${motion.displayName}")
        log.debug "${motion.displayName}: $count"
    }    

    setInitialConditions()
}

// Set initial conditions
def setInitialConditions() {
    log.debug "Executing setInitialConditions()"

    def sunInfo = getSunriseAndSunset(zipCode: zipCode);
    def now = new Date()
    def sunriseTime = sunInfo.sunrise
    def sunsetTime = sunInfo.sunset

    log.debug "Now: $now"
    log.debug "Sunrise Time: $sunriseTime"
    log.debug "Sunset Time: $sunsetTime"
    
    if (sunriseTime.after(now)) {
        log.debug "runOnce sunriseHandler() at $sunriseTime"
        runOnce(sunriseTime, sunriseHandler)
        log.debug "runOnce sunsetHandler() at $sunsetTime"
        runOnce(sunsetTime, sunsetHandler)
        sunsetHandler()
    }
    else if (sunsetTime.after(now)) {
        log.debug "runOnce sunsetHandler() at $sunsetTime"
        runOnce(sunsetTime, sunsetHandler)
        sunriseHandler()
    }
    else {
        sunsetHandler()
    }
    
    // Schedule next sunset/rise times check
    scheduleGetSunTimes()
}

// Schedule sunset/rise times check
def scheduleGetSunTimes() {
    log.debug "Executing scheduleGetSunTimes()"
    // Scheduling for 1am local time every day
    def scheduleTime = timeTodayAfter(new Date(), "01:00", location.timeZone);
    log.debug "schedule getSunTimes() at $scheduleTime"
    schedule(scheduleTime, getSunTimes)
}

// Get sunset/rise times
def getSunTimes() {
    log.debug "Executing getSunTimes()"

    def sunInfo = getSunriseAndSunset(zipCode: zipCode);
    def now = new Date()
    def sunriseTime = sunInfo.sunrise
    def sunsetTime = sunInfo.sunset

    log.debug "Now: $now"
    log.debug "Sunrise Time: $sunriseTime"
    log.debug "Sunset Time: $sunsetTime"
    
    if (sunriseTime.after(now)) {
        log.debug "runOnce sunriseHandler() at $sunriseTime"
        runOnce(sunriseTime, sunriseHandler)
    }

    if (sunsetTime.after(now)) {
        log.debug "runOnce sunsetHandler() at $sunsetTime"
        runOnce(sunsetTime, sunsetHandler)
    }
    // Schedule next sunset/rise times check
    scheduleGetSunTimes()
}

def motionActiveHandler(evt) {
    log.debug "Executing motionActiveHandler()"
    log.debug "$evt.name: $evt.value"

    // Debug block
    motions.each { motion ->
        int count = state.countMap.get("${motion.displayName}")
        log.debug "${motion.displayName}: $count"
    }    

    int count = state.countMap.get("${evt.displayName}")
    log.debug "$evt.displayName count: $count"
    state.countMap.put("${evt.displayName}", count + 1)
}

def sunriseHandler() {
    log.debug "Executing sunriseHandler()"
    countMap.each { name, count -> sendMessage("$name detected $count motion events last night") }
    unschedule("sunriseHandler") // Temporary work-around for scheduling bug
}

def sunsetHandler() {
    log.debug "Executing sunsetHandler()"
    log.debug "Resetting count to 0"
    motions.each { motion -> state.countMap.put("${motion.displayName}", 0) }    
    unschedule("sunsetHandler") // Temporary work-around for scheduling bug
}

def sendMessage(msg) {
    log.info(msg)
    if (pushNotification) {
        sendPush(msg)
    }
}

The issue is in this section of code:

def motionActiveHandler(evt) {
    log.debug "Executing motionActiveHandler()"
    log.debug "$evt.name: $evt.value"

    // Debug block
    motions.each { motion ->
        int count = state.countMap.get("${motion.displayName}")
        log.debug "${motion.displayName}: $count"
    }    

    int count = state.countMap.get("${evt.displayName}")
    log.debug "$evt.displayName count: $count"
    state.countMap.put("${evt.displayName}", count + 1)
}

When I try to retrieve the count values from the map again, it’s showing all values to be null.

Any help?

Thanks!
Top

I think the problem is that events do not have a “displayName”. Events do have a deviceId. So I think you should probaby do something like this to initialize it:

state.countMap.put(motion.id, 0)

As a side note, you can just do it like this instead of put("${motion.id}", 0) for simplicity/clarity.

Then when you get/increment the counts, you can do something like this:

int count = state.countMap.get(evt.deviceId)
log.debug "$evt.deviceId count: $count"
state.countMap.put(evt.deviceId, count+1)

I haven’t tested this, but I think it explains why you were seeing nulls. Let me know how it works.

Thanks for your feedback, Matt. I’ll give that a try this evening.

While tinkering with it some more over the weekend, I managed to hack a more compute intensive, non-map solution this way:

...
def initialize() {
	subscribe(motions, "motion.active", motionActiveHandler)

    // Initialize countMap
    // Set counts to 0
    int i = 0
    for (motion in motions) {
        state."name${i}"  = "${motion.displayName}"
        state."count${i}" = 0
        i++
    }

    setInitialConditions()
}
...
def motionActiveHandler(evt) {
	log.debug "Executing motionActiveHandler()"
	log.debug "$evt.displayName: $evt.name = $evt.value"

    for (int i = 0; i < motions.size(); i++) {
        def name = state."name${i}"
        def count = state."count${i}"
        
        if ("${name}" == "${evt.displayName}") {
            count++
            state."count${i}" = count
            log.debug "Motion detected by $name — current count is $count"
            break
        }
    }
}
...

The above does work since I’m using individual variables now, although inelegant.

Regarding your comment that events don’t have “displayName”, the above does rely on “evt.displayName” in the if expression, which works correctly based on the simulating and also getting the right outputs from last night’s events. So it seems that events do have a “displayName”…?

Thanks!

Cheers.
Top

Hi Matt,

No luck.

Here’s what I did:

def initialize() {
	subscribe(motions, "motion.active", motionActiveHandler)

    // Initialize countMap
    // Set counts to 0
    state.countMap = [:]
    motions.each { motion -> state.countMap.put("${motion.id}", 0)
        int count = state.countMap.get("${motion.id}")
        log.debug "${motion.displayName}: $count"
    }
    
    setInitialConditions()
}

I had to do “state.countMap.put(”${motion.id}", 0}" otherwise I’d get a syntax error.

Still, I can’t retrieve the data again, and still get null.

def motionActiveHandler(evt) {
	log.debug "Executing motionActiveHandler()"
	log.debug "$evt.displayName: $evt.name = $evt.value"

    motions.each { motion ->
        int count = state.countMap.get("${motion.id}")
        log.debug "${motion.displayName}: $count"
    }
}

Not sure why it doesn’t work as I get this error on the “int count = …” line:

7:07:21 PM PDT: error org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'null' with class 'null' to class 'int'. Try 'java.lang.Integer' instead @ line 136

Thanks!
Top

Finally got it to work.

Matt’s pointer in the right direction helped, but I found that my statement that I needed to do the following was incorrect:

state.countMap.put("$motion.displayName", 0)

Matt’s code was correct that I just needed to do:

state.countMap.put(motion.displayName, 0)

The quote was messing things up.

This is what finally worked:

/**
 *  Count Motion Events
 *
 *  Copyright 2014 Top Lertpanyavit
 *
 *  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.
 *
 */
definition(
    name: "Count Motion Events",
    namespace: "topl",
    author: "Top Lertpanyavit",
    description: "Counts number of motion events for a motion sensor from sunset until sunrise and sends push notification of count at sunrise.",
    category: "My Apps",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")


preferences {
	section("Motion sensor(s) to monitor..."){
		input "motions", "capability.motionSensor", multiple: true, required: true
	}
	section ("Zip code for sunset and sunset times (optional, defaults to location coordinates)...") {
		input "zipCode", "text", title: "Zip code", required: false
	}
    section("Notify me...") {
        input "pushNotification", "bool", title: "Push notification"
    }
}

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

def updated() {
	log.debug "Updated with settings: ${settings}"
	unsubscribe()
	initialize()
}

def initialize() {
	subscribe(motions, "motion.active", motionActiveHandler)

    // Initialize countMap
    // Set counts to 0
    state.countMap = [:]
    motions.each { it -> state.countMap.put(it.displayName, 0) }

    setInitialConditions()
}

// Set initial conditions
def setInitialConditions() {
	log.debug "Executing setInitialConditions()"

	def sunInfo = getSunriseAndSunset(zipCode: zipCode);
	def now = new Date()
	def sunriseTime = sunInfo.sunrise
	def sunsetTime = sunInfo.sunset

	log.debug "Now: $now"
	log.debug "Sunrise Time: $sunriseTime"
	log.debug "Sunset Time: $sunsetTime"
	
	if (sunriseTime.after(now)) {
		log.debug "runOnce sunriseHandler() at $sunriseTime"
        runOnce(sunriseTime, sunriseHandler)
		log.debug "runOnce sunsetHandler() at $sunsetTime"
		runOnce(sunsetTime, sunsetHandler)
        sunsetHandler()
	}
    else if (sunsetTime.after(now)) {
		log.debug "runOnce sunsetHandler() at $sunsetTime"
		runOnce(sunsetTime, sunsetHandler)
        sunriseHandler()
	}
    else {
    	sunsetHandler()
	}
    
	// Schedule next sunset/rise times check
	scheduleGetSunTimes()
}

// Schedule sunset/rise times check
def scheduleGetSunTimes() {
	log.debug "Executing scheduleGetSunTimes()"
    
    // Scheduling for 1am local time every day
	def scheduleTime = timeTodayAfter(new Date(), "01:00", location.timeZone);
	log.debug "schedule getSunTimes() at $scheduleTime"
	schedule(scheduleTime, getSunTimes)
}

// Get sunset/rise times
def getSunTimes() {
	log.debug "Executing getSunTimes()"

	def sunInfo = getSunriseAndSunset(zipCode: zipCode);
	def now = new Date()
	def sunriseTime = sunInfo.sunrise
	def sunsetTime = sunInfo.sunset

	log.debug "Now: $now"
	log.debug "Sunrise Time: $sunriseTime"
	log.debug "Sunset Time: $sunsetTime"
	
	if (sunriseTime.after(now)) {
		log.debug "runOnce sunriseHandler() at $sunriseTime"
		runOnce(sunriseTime, sunriseHandler)
	}

	if (sunsetTime.after(now)) {
		log.debug "runOnce sunsetHandler() at $sunsetTime"
		runOnce(sunsetTime, sunsetHandler)
	}
	// Schedule next sunset/rise times check
	scheduleGetSunTimes()
}

def motionActiveHandler(evt) {
	log.debug "Executing motionActiveHandler()"
	log.debug "$evt.displayName: $evt.value"

    def count = state.countMap.get(evt.displayName)
    count++
    state.countMap.put(evt.displayName, count)
    log.debug "Motion detected by ${evt.displayName} — current count is $count"
}

def sunriseHandler() {
	log.debug "Executing sunriseHandler()"
    
    for (e in state.countMap) {
        sendMessage ("$e.key detected $e.value motion events last night")
    }

    unschedule("sunriseHandler") // Temporary work-around for scheduling bug
}

def sunsetHandler() {
	log.debug "Executing sunsetHandler()"
    log.debug "Resetting count to 0"

    motions.each { it -> state.countMap.put(it.displayName, 0) }
   
    unschedule("sunsetHandler") // Temporary work-around for scheduling bug
}

def sendMessage(msg) {
	log.info(msg)
    if (pushNotification) {
        sendPush(msg)
    }
}

Thanks!
Top

Sorry I led you down the wrong path, but glad you got it working!