Porting Custom SmartApp from ST: eventsSince replacement

Hello,

I'm trying to port over this SmartApp from ST. It controls our homes hot water recirculating pump based on motion detected in the master bathroom.

/**
 *  Recirculating Pump Thermostat
 *
 *  Copyright 2017 Jeremy Akers
 *
 *  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: "Recirculating Pump Thermostat",
    namespace: "jeremy.akers",
    author: "Jeremy Akers",
    description: "Recirculating Pump Thermostat",
    category: "Green Living",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png")

preferences {
	section("Choose water line temperature sensor... "){
		input "watersensor", "capability.temperatureMeasurement", title: "Water Line Sensor"
	}
    section("Choose ambient temperature sensor... "){
		input "ambientsensor", "capability.temperatureMeasurement", title: "Ambient Sensor"
	}
	section("Select the recirc pump switch(es)... "){
		input "outlets", "capability.switch", title: "Outlets", multiple: true
	}
	section("Desired water temp at faucet:"){
		input "desiredwatertemp", "number", title: "Set Temp"
	}
	section("Set the desired temperature difference..."){
		input "threshold", "decimal", title: "Set Temp"
	}
	section("Solar Tank Temp Limit (Recirc on above this temp):"){
		input "solarlimit", "decimal", title: "Set Temp"
	}
	section("Minimum water line temp:"){
		input "minlinetemp", "decimal", title: "Set Temp"
	}
	section("When there's been movement from..."){
		input "motion", "capability.motionSensor", title: "Motion", multiple: true, required: true
	}
	section("Within this number of minutes..."){
		input "minutes", "number", title: "Minutes", required: false
	}
    section("Turn off recirc after delay (minutes)..."){
		input "delayminutes", "decimal", title: "Minutes", required: false
	}
    section("Don't re-activate recirc for at least this many minutes after turning on:"){
		input "stayoffminutes", "number", title: "Minutes", required: false
	}    
}

def installed()
{
	subscribe(watersensor, "temperature", temperatureHandler)
    //subscribe(ambientsensor, "temperature", temperatureHandler)  // We don't need to "subscribe" to updates from this sensor.
	if (motion) {
		subscribe(motion, "motion", motionHandler)
	}
    atomicState.previousWaterTemp = watersensor.currentTemperature
    atomicState.previousWaterTempTime = now()
    atomicState.tempWhenOn = null
    atomicState.timeWhenOn = null
}

def updated()
{
	unsubscribe()
	subscribe(watersensor, "temperature", temperatureHandler)
    //subscribe(ambientsensor, "temperature", temperatureHandler) // We don't need to "subscribe" to updates from this sensor.
	if (motion) {
		subscribe(motion, "motion", motionHandler)
	}
    atomicState.previousWaterTemp = watersensor.currentTemperature
    atomicState.previousWaterTempTime = now()
    atomicState.tempWhenOn = null
    atomicState.timeWhenOn = null
}

def temperatureHandler(evt)
{
	def ambientTemp = ambientsensor.currentTemperature
	def isActive = hasBeenRecentMotion()
    def maxTemp = ((evt.doubleValue >= 105) ? evt.doubleValue + 1 : 105)
    def percentChange = (evt.doubleValue - atomicState.previousWaterTemp) / (maxTemp - evt.doubleValue)
    def solarTemp = getSolarTemp()
    
    logAndNotify("Previous temp: ${atomicState.previousWaterTemp}, new temp: ${evt.doubleValue}, percentChange: ${percentChange}, Solar temp: ${solarTemp}")
    
    if(solarTemp > solarlimit)
    {
    	logAndNotify("Solar tank hotter than preset: ${solarlimit}. Turning on recirc to heat attic tank.")
    	outlets.on()
    }
    else
    {
        if(atomicState.previousWaterTemp != null && atomicState.previousWaterTemp > 0 && percentChange >= 0.05)
    	{
    		logAndNotify("Temperature has risen significantly. Turning off.")
        	turnOffSwitch()
    	}
    	else if (isActive) 
    	{
    		logAndNotify("TemperatureHandler calling Evaluate")
			evaluate(evt.doubleValue, ambientTemp)
		}
		else 
    	{
	    	log.debug("TemperatureHandler outlets OFF")
			turnOffSwitch()
		}
	}
    atomicState.previousWaterTemp = evt.doubleValue
    atomicState.previousWaterTempTime = now()
}

def motionHandler(evt)
{
	def lastTemp = watersensor.currentTemperature
    def watersensorevents = watersensor.eventsSince(new Date(now() - (60000 * 60))).flatten().findAll() // All water sensor events in past 60 minutes.
    def ambientTemp = ambientsensor.currentTemperature
    def ambientevents = ambientsensor.eventsSince(new Date(now() - (60000 * 60))).flatten().findAll() // All ambient sensor events in past 60 minutes.
    def switchEvents = outlets.eventsSince(new Date(now() - (60000 * stayoffminutes))).flatten().findAll{ it.value=="on" } // All switch events in past ${stayoffminutes} minutes.
    def isActive = hasBeenRecentMotion()
    
    if (!(watersensorevents.find()))
    {
    	logAndNotify("No water temperature sensor events in past hour. Assuming dead sensor. Setting water line temp to 70 degrees.")
        lastTemp = 70
    }
    if (!(ambientevents.find()))
    {
    	logAndNotify("No ambient temperature sensor events in past hour. Assuming dead sensor. Setting ambient temp to 70 degrees.")
        ambientTemp = 70
    }

    if(solarTemp > solarlimit)
    {
    	logAndNotify("Solar tank hotter than preset: ${solarlimit}. Turning on recirc to heat attic tank.")
    	outlets.on()
    }
    else
   	{
	    log.debug("Event value: ${evt.value}, isActive: ${isActive}")
    	//switchEvents.each { log.trace "Recirc switchEvents value iterator: ${it.value}" }
		if ((evt.value == "active" || isActive)) 
    	{   
			if (!(switchEvents.find { it.value == "on" }) && lastTemp != null && ambientTemp != null) 
	        {
	        	logAndNotify("MotionHandler calling Evaluate")
				evaluate(lastTemp, ambientTemp)
			}
	        else
	        {
	        	logAndNotify("motionHandler: opted not to switch on since outlet had recently already been on in the past ${stayoffminutes} minutes.")
	        }
		} 
	    else 
	    {
	    	logAndNotify("MotionHandler outlets OFF")
			turnOffSwitch()
		}
    }
}

private evaluate(waterTemp, ambientTemp)
{
	def solarTemp = getSolarTemp()
    def myDesiredWaterTemp = (desiredwatertemp > solarTemp) ? desiredwatertemp : solarTemp
	def desiredTemp = (ambientTemp + myDesiredWaterTemp) / 2

    logAndNotify("Ambient Temp: ${ambientTemp}, Desired Water Temp: ${myDesiredWaterTemp}, Desired Sensor Temp: ${desiredTemp}, Solar tank temp: ${solarTemp}")
    if (solarTemp != null && solarTemp > 0 && desiredTemp > solarTemp)
    {
    	desiredTemp = solarTemp
    }
    
    if(desiredTemp < minlinetemp)
    {
    	desiredTemp = minlinetemp
    }
    logAndNotify("Recirc: Evaluate: Water Temp: $waterTemp, Desired/Solar Temp: $desiredTemp, Threshold: $threshold")
    
    	if (waterTemp < 105 && waterTemp <= desiredTemp + threshold) {
        	log.info("Recirc EVALUATE outlets ON")
			turnOnSwitch()
		}
		else //if (waterTemp >= desiredTemp + (threshold * 2) || waterTemp >= 105) 
        {
        	log.info("Recirc EVALUATE outlets OFF")
			turnOffSwitch()
		}
}

private hasBeenRecentMotion()
{
	def isActive = false
	if (motion && minutes) {
		def deltaMinutes = minutes as Long
		if (deltaMinutes) {
			def motionEvents = motion.eventsSince(new Date(now() - (60000 * deltaMinutes))).flatten()
			log.trace "Recirc: Found ${motionEvents?.size() ?: 0} events in the last $deltaMinutes minutes"
            //motionEvents.each { log.trace "Recirc value iterator: ${it.value}" }
			if (motionEvents.find { it.value == "active" }) {
				isActive = true
			}
		}
	}
	else {
		isActive = true
	}
	isActive
}

def turnOffAfterDelay() {
	logAndNotify("Outlets OFF after delay of ${delayminutes}")
    turnOffSwitch()
}

def turnOffSwitch() {
	def currSwitches = outlets.currentSwitch
    def onSwitches = currSwitches.findAll { switchVal ->
        switchVal == "on" ? true : false
    }
    if(onSwitches.size() > 0)
    {
		logAndNotify("Outlets OFF")
    	outlets.off()
    }
    else
    {
    	log.debug("Outlets already OFF, skipping.")
    }
}

def turnOnSwitch(waterTemp) {
	def currSwitches = outlets.currentSwitch
    def offSwitches = currSwitches.findAll { switchVal ->
        switchVal == "off" ? true : false
    }
    if(offSwitches.size() > 0)
    {
		logAndNotify("Outlets ON")
        atomicState.tempWhenOn = waterTemp
        atomicState.timeWhenOn = now()
    	outlets.on()
        runIn(delayminutes*60, turnOffAfterDelay) // Turn off after ${delayminutes} minutes.
    }
    else
    {
    	log.debug("Outlets already ON, skipping.")
    }
}

def logAndNotify(message)
{
	log.info(message)
    sendNotificationEvent(message)
}

def getSolarTemp()
{
	def solarTemp = -1
	def dateTime = new Date()
    def formattedDate = dateTime.format("yyyy-MM-dd HH:mm:ss", location.timeZone)
	def currentTimeEpoch = dateTime.getTime() - 60000
    def params = [
        uri:  'http://www.log-alert.com/logalert/feed/',
        path: 'data.json',
        contentType: 'application/json',
        query: [id:1571, start:currentTimeEpoch, dp:400]
    ]
    log.debug "Current time epoch: ${currentTimeEpoch}, formatted: ${formattedDate}"
    try {
        httpGet(params) {resp ->
            def dataItem = resp.data[0]
            solarTemp = dataItem[1]
            logAndNotify("Solar tank temp: ${solarTemp}")
        }
    } catch (e) {
        log.error "Error getting solar tank temp: $e"
    }
    solarTemp
}

And I get this error when the motion event fires:

java.lang.IllegalArgumentException: Command 'eventsSince' is not supported by device. on line 131 (motionHandler)

Line 131 is this line in motion handler:

def switchEvents = outlets.eventsSince(new Date(now() - (60000 * stayoffminutes))).flatten().findAll{ it.value=="on" } // All switch events in past ${stayoffminutes} minutes.

It appears Hubitat doesn't support the "eventsSince" method? or is there some code missing from that device's driver to enable that functionality?

Thanks,
-Jeremy

I think the problem is that it's being done on a list with more than one device since "outlets" has multiple: true set. You might have to loop through the list and call it on each device.

Maybe you could do something like this:

def switchEvents = outlets.collect { it.eventsSince(new Date(now() - (60000 * stayoffminutes)))}.flatten().findAll{ it.value=="on" } // All switch events in past ${stayoffminutes} minutes.

Continuing the discussion from Porting Custom SmartApp from ST: eventsSince replacement:

Sorry about creating a new thread. The forum would not let me reply to the old one so I had to reply as a "New Topic". It wouldn't let me respond to the original thread. It kept giving me this error:

"Something has gone wrong. Perhaps this topic was closed or deleted while you were looking at it?"

Screenshot_2018-09-15%20Porting%20Custom%20SmartApp%20from%20ST%20eventsSince%20replacement

I tried refreshing the page, closing my browser, and even tried multiple different browsers (Chrome and Firefox).

Anyway: I simply wanted to reply to @putnamjwp and offer thanks for the help: That solution seems to have taken care of the error. I still find it odd that the original code worked fine in SmartThings implementation of Groovy, so I'm not sure why it wouldn't work in Hubitat's implementation.

Thanks again,
-Jeremy

1 Like

The methods in question aren't native groovy methods,,so implementation details get down to st documentation, which usually doesnt contain that level of detail...