Call command from command?

I come from other programming languages where this would work so forgive my ignorance.
Basically I call the FountainModeOn() from a tile click. When it gets to PoolPumpOff() that command turns off the pump via hubaction. My problem is PoolPumpOff() gets called but never returns via Parse(). If I use this any other way it works fine but handlers seem to have an issue when you want to call a command that gets a response and return back to the command to proceed.

Any pro devs that know a better way to handle this I’m all ears.

    def FountainModeOn()
    {
    	if (CheckModeState() == "true")
      {
        	if (device.currentState("PoolPump").stringValue == "on")
            {
            	PoolPumpOff()
           }
            if (device.currentState("PoolPump").stringValue == "off")
           {
       		def hubActions = [postFountainMode(), delayAction(10000), postFountainModeOff()]
        		hubActions
            }
         }
     }

I believe you need to add a sendHubCommand(hubActions) call instead of returning hubActions. Returning hubActions only works depending on the calling context.

I use hubAction in my commands every where and it works with no issue. Parse just doesn’t get called here.

Or are you stating something else?

Sorry… let’s back up…

So, does the actual physical device behave as you desire?

You’re just not getting a status update from the device via the parse() function based on the command you sent?

Here’s the full handler. So basically if I call PoolPumpOff() from a tile press the call makes it to the RPi and comes back through parse(). But if I call PoolPumpOff() from FountainModeOn() I never see it hit the RPi or parse(). Has to be something I’m missing.

I see it get called in the log below but it never makes it to the RPi so it never comes back to parse(). But again the same command works outside of the FountainModeOn() command.
e3a79e24-a7a4-4b50-aaea-d2eaeab3f833 5:05:40 PM: debug POST /macros/PoolPumpOff HTTP/1.1
Accept: /
User-Agent: Linux UPnP/1.0 SmartThings

e3a79e24-a7a4-4b50-aaea-d2eaeab3f833 5:05:40 PM: debug Executing hubAction on 192.168.1.171:8000
e3a79e24-a7a4-4b50-aaea-d2eaeab3f833 5:05:40 PM: debug Device Network Id set to c0a801ab:1f40

    /**
     *  Raspberry Pi Pool Controller
     *
     *  Copyright 2019
     *
     *  Control your pool equipment using a Raspberry Pi
     *
     *  
     *
     *      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.
     *
     */

    import groovy.json.JsonSlurper

    preferences {
        input("ip", "string", title: "IP Address", description: "192.168.x.x", required: true, displayDuringSetup: true)
        input("port", "string", title: "Port", description: "8000", defaultValue: 8000, required: true, displayDuringSetup: true)
    }

    metadata {
        definition(name: "Pool Controller", namespace: "jaswalters", author: "Jason Walters") {
            capability "Polling"
            capability "Refresh"
            capability "Temperature Measurement"
            capability "Switch"
            capability "Sensor"
            capability "Actuator"
            attribute "SuctionValvePos", "enum", ["SpaPos", "PoolPos"]
            attribute "ReturnValvePos", "enum", ["SpaPos", "PoolPos"]
            attribute "cpuPercentage", "string"
            attribute "memory", "string"
            attribute "diskUsage", "string"
            attribute "FountainMode", "string"
            attribute "RPiPower", "string"
            attribute "cpuTemperature", "string"
            attribute "memUsage", "string"
            attribute "PoolPump", "string"
            attribute "WaterFallMode", "string"
            attribute "SpaMode", "string"

            command "restart"
            command "FountainModeOn"
            command "WaterFallModeOn"
            command "SpaModeOn"
            command "WaterFallModeOn"
    		command "PoolPumpOff"
            command "PoolPumpOn"
        }

        simulator {
            // TODO: define status and reply messages here
        }

        tiles(scale: 1) {
            multiAttributeTile(name: "PoolPump", width: 1, height: 1) {
                tileAttribute("device.PoolPump", key: "PRIMARY_CONTROL") {
                    attributeState "on", label: 'On', action: "PoolPumpOff", icon: "https://img.icons8.com/ios/50/000000/pump-filled.png", backgroundColor: "#00a0dc"
                    attributeState "off", label: 'Off', action: "PoolPumpOn", icon: "https://img.icons8.com/ios/50/000000/pump.png", backgroundColor: "#ffffff"
                    attributeState "turningOn", label: 'Turning On', icon: "https://img.icons8.com/ios/50/000000/pump-filled.png", backgroundColor: "#00a0dc"
                    attributeState "turningOff", label: 'Turning Off', icon: "https://img.icons8.com/ios/50/000000/pump.png", backgroundColor: "#ffffff"
                }
            }
            valueTile("cpuTemperature", "device.cpuTemperature", width: 2, height: 2) {
                state "temperature", label: '${currentValue}°F', icon: "https://img.icons8.com/wired/64/000000/processor.png", unit: "F"
            }
            standardTile("RPiPower", "device.RPiPower", width: 2, height: 2, decoration: "flat") {
                state "off", label: 'Off', icon: "https://img.icons8.com/metro/50/000000/raspberry-pi.png", nextState: "on"
                state "on", label: 'On', icon: "https://img.icons8.com/metro/50/000000/raspberry-pi.png", nextState: "off"
                state "rebooting", label: 'Rebooting', icon: "https://img.icons8.com/metro/50/000000/raspberry-pi.png", nextState: "off"
            }
            valueTile("cpuPercentage", "device.cpuPercentage", width: 2, height: 2) {
                state "default", label: '${currentValue}% CPU', icon: "https://img.icons8.com/wired/64/000000/processor.png"
            }
            standardTile("FountainMode", "device.FountainMode", width: 2, height: 2, decoration: "flat") {
                state "off", action: "FountainModeOn", label: 'Fountain Mode Off', icon: "https://img.icons8.com/ios/24/000000/fountain.png"
                state "turningOn", label: 'Turning \nOn', icon: "https://img.icons8.com/ios/24/000000/fountain-filled.png", backgroundColor: "#1e9cbb"
                state "turningOff", label: 'Turning Off', icon: "https://img.icons8.com/ios/24/000000/fountain.png"
                state "on", action: "FountainModeOn", label: 'Fountain Mode On', icon: "https://img.icons8.com/ios/24/000000/fountain-filled.png", backgroundColor: "#1e9cbb"
            }
            standardTile("WaterFallMode", "device.WaterFallMode", width: 2, height: 2, decoration: "flat") {
                state "off", action: "WaterFallModeOn", label: 'WaterFall Mode \nOff', icon: "https://img.icons8.com/ios/50/000000/waterfall.png"
                state "turningOn", label: 'Turning \nOn', icon: "https://img.icons8.com/ios/50/000000/waterfall-filled.png", backgroundColor: "#1e9cbb"
                state "turningOff", label: 'Turning Off', icon: "https://img.icons8.com/ios/50/000000/waterfall.png"
                state "on", action: "WaterFallModeOn", label: 'WaterFall Mode \nOn', icon: "https://img.icons8.com/ios/50/000000/waterfall-filled.png", backgroundColor: "#1e9cbb"
            }
            standardTile("SpaMode", "device.SpaMode", width: 2, height: 2, decoration: "flat") {
                state "off", action: "SpaModeOn", label: 'Spa Mode \nOff', icon: "https://img.icons8.com/ios/50/000000/spa.png"
                state "turningOn", label: 'Turning \nOn', icon: "https://img.icons8.com/ios/50/000000/spa-filled.png", backgroundColor: "#1e9cbb"
                state "turningOff", label: 'Turning \nOff', icon: "https://img.icons8.com/ios/50/000000/spa.png"
                state "on", action: "SpaModeOn", label: 'Spa Mode \nOn', icon: "https://img.icons8.com/ios/50/000000/spa-filled.png", backgroundColor: "#1e9cbb"
            }
            valueTile("diskUsage", "device.diskUsage", width: 2, height: 2) {
                state "default", label: '${currentValue}% Used', unit: "Percent", icon: "https://img.icons8.com/ios/50/000000/sd.png"
            }
            valueTile("memUsage", "device.memUsage", width: 2, height: 2) {
                state "default", label: '${currentValue}% Used', icon: "https://img.icons8.com/ios/50/000000/brain.png"
            }
            standardTile("restart", "device.restart", width: 3, height: 1, inactiveLabel: false, decoration: "flat") {
                state "default", action: "restart", label: "Restart", displayName: "Restart"
            }
            standardTile("refresh", "device.refresh", width: 3, height: 1, inactiveLabel: false, decoration: "flat") {
                state "default", action: "polling.poll", icon: "st.secondary.refresh"
            }
            standardTile("emptyfiller", "null", width: 2, height: 2, decoration: "flat") {
                state "empty", label: '', defaultState: true
            }
            main "PoolPump"
            details(["PoolPump", "FountainMode", "WaterFallMode", "SpaMode", "RPiPower", "cpuTemperature", "cpuPercentage", "diskUsage", "emptyfiller", "memUsage", "restart", "refresh"])
        }
    }

    // ------------------------------------------------------------------

    // parse events into attributes
    def parse(String description) {
        def map = [: ]
        def descMap = parseDescriptionAsMap(description)
        def body = new String(descMap["body"].decodeBase64())
        def slurper = new JsonSlurper()
        def result = slurper.parseText(body)
        log.debug "result ${result}"

        if (result) {
            if (state.reboot == "true") {
                state.reboot = "false"
            }
            log.debug "Pool controller is up"
            sendEvent(name: "RPiPower", value: "on")
        } else {
            log.debug "The Raspberry Pi may be offline!"
        }

        log.debug "RPi Power Status: stringValue: ${device.currentState("RPiPower").stringValue}"

    	if (result.PoolPump == "On") {
            log.debug "PoolPump: ${result.PoolPump}"
            state.PoolPump = "on"
            sendEvent(name: "PoolPump", value: "on")
        }
        else if (result.PoolPump == "Off")
        {
        	log.debug "PoolPump: ${result.PoolPump}"
            state.PoolPump = "off"
            sendEvent(name: "PoolPump", value: "off")
        }
        
        if (result.cpu_temp) {
            log.debug "cpu_temp: ${result.cpu_temp}"
            sendEvent(name: "cpuTemperature", value: result.cpu_temp)
        }

        if (result.cpu_usage) {
            log.debug "cpu $result.cpu_usage"
            sendEvent(name: "cpuPercentage", value: result.cpu_usage)
        }

        if (result.mem_usage) {
            log.debug "mem_usage: ${result.mem_usage}"
            sendEvent(name: "memUsage", value: result.mem_usage)
        }

        if (result.disk_usage) {
            log.debug "disk_usage: ${result.disk_usage}"
            sendEvent(name: "diskUsage", value: result.disk_usage)
        }
        log.debug "mode ${result.Mode}"

        if (result.Mode && state.reboot != "true") {
            //log.debug "My state = ${state.refresh}"
            if (result.Mode == "FountainModeOff") {
                sendEvent(name: "FountainMode", value: "on")

                if (device.currentState("FountainMode").stringValue == "on") {
                    //Save the valve postion to state object
                    state.Mode = "FountainMode"
                    sendEvent(name: "WaterFallMode", value: "off")
                    sendEvent(name: "SpaMode", value: "off")
                }
            } else if (result.Mode == "WaterFallModeOff") {
                sendEvent(name: "WaterFallMode", value: "on")

                if (device.currentState("WaterFallMode").stringValue == "on") {
                    //Save the valve postion to state object
                    state.Mode = "WaterFallMode"
                    sendEvent(name: "FountainMode", value: "off")
                    sendEvent(name: "SpaMode", value: "off")
                }
            } else if (result.Mode == "SpaModeOff") {
                sendEvent(name: "SpaMode", value: "on")

                if (device.currentState("SpaMode").stringValue == "on") {
                    //Save the valve postion to state object
                    state.Mode = "SpaMode"
                    sendEvent(name: "FountainMode", value: "off")
                    sendEvent(name: "WaterFallMode", value: "off")
                }
            }

            if (result.Mode == "FountainMode")
            {
                sendEvent(name: "FountainMode", value: "turningOn")
                GetIncomingMode(state.Mode)
            } else if (result.Mode == "WaterFallMode") {
                sendEvent(name: "WaterFallMode", value: "turningOn")
                GetIncomingMode(state.Mode)
            } else if (result.Mode == "SpaMode") {
                sendEvent(name: "SpaMode", value: "turningOn")
                GetIncomingMode(state.Mode)
            }
            log.debug "Pool valves have been set to ${state.Mode}"
        }
            
        	if (state.refresh == "true")
            {
            	GetValveMode(state.Mode)	
                state.refresh = "false"
        	}
    }

    def PoolPumpOff()
    {
    	if (state.reboot != "true")
        {
        	sendEvent(name: "PoolPump", value: "turningOff")
            def uri = "/macros/PoolPumpOff"
            postAction(uri)
            log.debug "made it here"
        }
    }

    def PoolPumpOn()
    {
    	if (state.reboot != "true" && CheckModeState() == "true")
        {
        	sendEvent(name: "PoolPump", value: "turningOn")
            log.debug "fountain on"
            def uri = "/macros/PoolPumpOn"
            postAction(uri)
        }
    }

    def CheckModeState()
    {
    	if (device.currentState("SpaMode").stringValue == "on" || device.currentState("FountainMode").stringValue == "on" || device.currentState("WaterFallMode").stringValue == "on")
        {
        	return "true"
        }
        else
        {
        	return "false"
        }
    }

    //Sends post request to raspberry webiopi to turn on fountain via turnvlaves macro script
    def postFountainMode() {
        if (state.reboot != "true") {
            log.debug "fountain on"
            def uri = "/macros/FountainModeOn"
            postAction(uri)
        }

    }

    def FountainModeOn()
    {
    	if (CheckModeState() == "true")
        {
        	if (device.currentState("PoolPump").stringValue == "on")
            {
            	PoolPumpOff()
            }
            if (device.currentState("PoolPump").stringValue == "off")
            {
        		def hubActions = [postFountainMode(), delayAction(10000), postFountainModeOff()]
        		hubActions
            }
        }
    }

    def GetIncomingMode(Mode) {
        switch (Mode) {
            case "FountainMode":
                sendEvent(name: "FountainMode", value: "turningOff")
                break
            case "WaterFallMode":
                sendEvent(name: "WaterFallMode", value: "turningOff")
                break
            case "SpaMode":
                sendEvent(name: "SpaMode", value: "turningOff")
                break
        }
    }

    def postFountainModeOff() {
        if (state.reboot != "true" && device.currentState("RPiPower").stringValue == "on") {
            log.debug "fountain off"
            def uri = "/macros/FountainModeOff"
            postAction(uri)
        }
    }

    def postWaterFallModeOn() {
        if (state.reboot != "true" && device.currentState("RPiPower").stringValue == "on") {
            //sendEvent(name: "WaterFallMode", value: 'turningOn')
            def uri = "/macros/WaterFallModeOn"
            postAction(uri)
        }
    }

    def WaterFallModeOn()
    {
    if (CheckModeState() == "true")
        {
        	def hubActions = [postWaterFallModeOn(), delayAction(10000), postWaterFallModeOff()]
        	hubActions
    	}
    }

    def postWaterFallModeOff() {
        if (state.reboot != "true" && device.currentState("RPiPower").stringValue == "on") {
            def uri = "/macros/WaterFallModeOff"
            postAction(uri)
        }
    }

    def postSpaModeOn() {
        if (state.reboot != "true" && device.currentState("RPiPower").stringValue == "on") {
            def uri = "/macros/SpaModeOn"
            postAction(uri)
        }
    }

    def postSpaModeOff() {
        if (state.reboot != "true" && device.currentState("RPiPower").stringValue == "on") {
            def uri = "/macros/SpaModeOff"
            postAction(uri)
        }
    }

    def SpaModeOn()
    {
    if (CheckModeState() == "true")
        {
        	def hubActions = [postSpaModeOn(), delayAction(10000), postSpaModeOff()]
        	hubActions
        }
    }

    def poll() {
        log.debug "Executing 'poll'"
        log.debug "reboot ${state.reboot}"
        state.refresh = "true"
        if (state.reboot != "true")
        {
            sendEvent(name: "RPiPower", value: "off")
            sendEvent(name: "PoolPump", value: "off")
            sendEvent(name: "PoolPump", value: "${state.PoolPump}")
        }
        getRPiData()
    }

    def refresh() {
        log.debug "Executing 'refresh'"
        poll()
    }

    def restart() {
        log.debug "Restart was pressed"
        if (device.currentState("RPiPower").stringValue != "off") {
            state.reboot = "true"
            sendEvent(name: "RPiPower", value: "rebooting")
            def uri = "/macros/rebootRPi"
            postAction(uri)
        }
    }

    def GetGPIOStatus()
    {
    	def uri = "/macros/GetGPIOStatus"
        postAction(uri)
    }

    // Get Raspberry Pi stats
    private getRPiData() {
        def uri = "/macros/getRPiData"
        postAction(uri)
    }

    //Gets the postion of the valve from state.ReturnValvePos
    def GetValveMode(ValveState)
    {
            if (ValveState == "FountainMode") {
                sendEvent(name: "WaterFallMode", value: "off")
                sendEvent(name: "SpaMode", value: "off")
                sendEvent(name: "FountainMode", value: "on")
            } else if (ValveState == "SpaMode") {
                sendEvent(name: "FountainMode", value: "off")
                sendEvent(name: "WaterFallMode", value: "off")
                sendEvent(name: "SpaMode", value: "on")
            } else if (ValveState == "WaterFallMode") {
                sendEvent(name: "FountainMode", value: "off")
                sendEvent(name: "SpaMode", value: "off")
                sendEvent(name: "WaterFallMode", value: "on")
            }
    }

    private postAction(uri) {
        setDeviceNetworkId(ip, port)

        def hubAction = new physicalgraph.device.HubAction(
            method: "POST",
            path: uri
        )
        log.debug("Executing hubAction on " + getHostAddress())
        log.debug hubAction
        return hubAction
    }

    def parseDescriptionAsMap(description) {
        description.split(",").inject([: ]) {map,param ->
            def nameAndValue = param.split(":")
            map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
        }
    }

    private delayAction(long time) {
        new physicalgraph.device.HubAction("delay $time")
    }

    private setDeviceNetworkId(ip, port) {
        def iphex = convertIPtoHex(ip)
        def porthex = convertPortToHex(port)
        device.deviceNetworkId = "$iphex:$porthex"
        log.debug "Device Network Id set to ${iphex}:${porthex}"
    }

    private getHostAddress() {
        return "${ip}:${port}"
    }

    private String convertIPtoHex(ipAddress) {
        String hex = ipAddress.tokenize('.').collect {
            String.format('%02x', it.toInteger())
        }.join()
        return hex

    }

    private String convertPortToHex(port) {
        String hexport = port.toString().format('%04x', port.toInteger())
        return hexport
    }

Did you try what I mentioned before, using the sendHubCommand() call? Depending on the context of who calls the code, will determine whether or not simply returning a hub action will work or not. To get around that, you can use sendHubCommand() to make sure the hub action is actually performed. I had this issue when calling a parent device’s function to perform a similar network transaction like you are using.

Here is a snippet of my code from a Device Handler.

def sendEthernet(message) {
	log.debug "Executing 'sendEthernet' ${message}"
	if (settings.ip != null && settings.port != null) {
        sendHubCommand(new physicalgraph.device.HubAction(
            method: "POST",
            path: "/${message}?",
            headers: [ HOST: "${getHostAddress()}" ]
        ))
    }
    else {
        state.alertMessage = "ST_Anything Parent Device has not yet been fully configured. Click the 'Gear' icon, enter data for all fields, and click 'Done'"
        runIn(2, "sendAlert")   
    }
}

Let me try that.

Have you tried Remove the debug from the end of poolPumpoff (or save the action result and return it after debug)?

The way commands work if you don’t use sendHubCommand as suggested by @ogiewon is that there is an implicit execution of the return value from the command being executed on the device.

(And, of course, groovy uses the result of the last statement executed as implicit return value if you don’t use a return statement)

1 Like

Tony, if the other doesn’t work are you saying I can add a return to aid? I understand returns just need more insight or where you are going with it.

When you press a button or call a commands from a device object (like in a smartapp) you are executing a command.

When you call a method from a command handler, you are not executing that as a command, it is just a method call, so you have to capture and handle the sending the action either explicitly with sendhubaction or implicit by including it in the list of events you return.

Further, when the last statement in a method is a debug, the method doesn’t return any actions.

1 Like

Thank you guys. The sendHubCommand has gotten me closer as it now runs the method but it introduced another issue that I will track down.

1 Like