Carrier Infinity / Bryant Evolution compatibility?

@swerb73, I would be interested in this. I have been using an integration between Infinitude and HomeAssistant for a year or so now, but I’m running into some issues, and started looking to see if anyone had made progress on a DH/SA for Infinitude. When I looked about a year ago for any integrations with SmartThings, there was not much out there, so I’m glad to see some progress has been made.

1 Like

I am interested as well… I have been waiting for long time for st and carrier infinity intregation.

Hi all, I’ll work to get some basic instructions, the DH, and SA packaged up and posted here this week/latest this weekend. Hopefully someone can help to document the instructions, limitations, workarounds, etc - and heck of course any improvements to the SA/DH are much appreciated.

2 Likes

Thank you!

Hi all, ok, here’s what I’ve come up with. First of all I welcome anyone that wants to more formally publish this somewhere including instructions, etc.

Requirements:

  • Infinitude (GitHub - nebulous/infinitude: Open control of Carrier/Bryant thermostats), doesn’t require serial integration to higher resolution access to the thermostat, air handler, heat pump and other devices (RS485 interface). You’re welcome to integrate via RS485 to get at this data but it isn’t really used by this integration and further you MUST complete the proxy integration from the wall stat for this to work. I’ve tested this on my 5 zone system and all zones are supported.
  • You must STOP using the Bryant/Carrier iOS/Android app to control the thermostat, while it sometimes work it usually breaks infinitude (there are open bugs on this) requiring you to manually clear the infinitude cache and restart – not good. Once you have infinitude up and running you really don’t need the app because you can either use the web interface or the DH via smartthings.
  • You’re welcome to continue to use all scheduling via your thermostat, these seem to remain working, but if you really want the power of geo fencing I’d recommend you disable all scheduling (I set all zones to away at 00:15 [12:15 am]) then control it with something like Webcore. I’m happy to publish my Webcore, it’s highly custom to the way my family interacts with our house (presence, motion, doors open/closed, etc) but I’m happy to share.

Install Instructions:

  • First: confirm you have Infinitude working from link above, I recommend you configure the updates to be applied ~ 15s
  • Second: install SmartApp via Smarthings API
  • Third: Install Device Handler via Smartthings API
  • Fourth: Configure SmartApp with the IP/Port of Infinitude, if all works as expected it will discover your zones and configure your themostats inside smartthings using the supplied DH
  • Fifth: Enjoy

Usage:

  • You must set the furnace climate mode (heat/cool/auto/fan only/off) via Infinitude web interface (haven’t implemented via DH/SmartApp yet), I usually leave it in heat/cool and don’t use auto, not sure how well infinitude works with that yet…
  • Once configured the DH can be used to change the profile by clicking the Profile tile in the top left, the order is Home, Away, Sleep, Wake, then Home…
  • You can also use the DH to change the heat/cool setpoint using the respective up/down arrows. NOTE: This will also change the Profile to manual (Bryant/Carrier behavior) and set the Hold time to Midnight.
  • NOTE: I haven’t been able to syn the config changes to Infinitude very well, there is a ~ 15 second delay for every change/touch in the DH. It seems to queue up changes you make and get them all applied but it can take a while, please wait 15-60s for it to fully apply).
  • Pressing Refresh Tile will refresh the SmartApp connection for all zones back to Infinitude, again NOTE that Infinitude may not adjust for 15-60s depending on your configuration.

The SmartApp:

1 Like

The Device Handler:

2 Likes

Great progress! One question if you switch over to this do you lose the emailed system alert messages? Ie if my unit is clogged I get an email notification from Bryant…

1 Like

Yes, they appear to remain intact. I believe the wall stat communicates this info, Infinitude passes it on (which is why I like to keep infinitude configured to sync changes) then the email notifications come out from the cloud instance like normal.

You can shut this “sync” off, which some recommend, to avoid the the issue I point in in Requirements bullet point 2.

1 Like

@swerb73 congrats on getting this done! BTW, I tried to pull in the code you pasted but the SmartThings IDE is reporting errors because of the special characters used for double and single quotes. Can you try posting to https://pastebin.com and copy the pastebin link here in this thread? Thanks.

There were a couple of bugs that I fixed, also had an issue on Android with icons not showing up so I had to remove the icons from the tiles. Anyway, here’s a working version without icons:
SmartApp…

/*
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: "Infinitude Integration",
    namespace: "Infinitude",
    author: "zraken, swerb73",
    description: "Infinitude Integration for Carrier/Bryant Thermostats",
    category: "SmartThings Labs",
    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 {
    page(name: "prefLogIn", title: "Infinitude Server")
    page(name: "pausePage", title: "Infinitude retrieving…")
    page(name: "prefListDevice", title: "Infinitude Zones")
}

/* Preferences */
def prefLogIn() {
    def showUninstall = configURL != null
    return dynamicPage(name: "prefLogIn", title: "Click next to proceed…", nextPage: "pausePage", uninstall: showUninstall, install: false) {
        section("Server URL") {
            input(name: "configURL", type: "text", title: "Local network ip_address:port", defaultValue: "192.168.2.138:3000", description: "Infinitude LAN Server Address")
        }
    }
}

def pausePage() {
    state.SystemRunning = 0
    state.thermostatList = syncSystem()
    log.debug "Query complete"

    return dynamicPage(name: "pausePage", title: "Configure", nextPage: "prefListDevice", uninstall: false, install: false) {
        section("Advanced Options") {
            input(name: "polling", title: "Server Polling (in Minutes)", type: "number", description: "in minutes", defaultValue: "5", range: "1..120")
        }
    }
}
def prefListDevice() {
    if (state.thermostatList) {
        log.debug "Got a list"
        return dynamicPage(name: "prefListDevice", title: "Thermostats", install: true, uninstall: true) {
            section("Select which thermostat/zones to use") {
                input(name: "selectedThermostats", type: "enum", required: false, multiple: true, metadata: [values: state.thermostatList])
            }
        }
    } else {
        log.debug "Empty list returned"
        return dynamicPage(name: "prefListDevice", title: "Error!", install: false, uninstall: true) {
            section("") {
                paragraph "Could not find any devices "
            }
        }
    }
}

/* Initialization */
def installed() {
    initialize()
}
def updated() {
    unschedule()
    unsubscribe()
    initialize()
}
def uninstalled() {
    unschedule()
    unsubscribe()
    getAllChildDevices().each {
        deleteChildDevice(it.deviceNetworkId)
    }
}

def initialize() {
    // Set initial states
    state.data = [:]
    state.setData = [:]
    state.SystemRunning = 1

    //selectedThermostats.each { dni, val ->
    def devices = selectedThermostats.collect {
        dni ->
            log.debug "Processing DNI: ${dni} with Value: {val}"
        def d = getChildDevice(dni)
        if (!d) {
            d = addChildDevice("SmartThingsMod", "Infinitude Thermostat DEF", dni, null, ["label": "Stat: " + dni.split("\\|")[3]])
            log.debug "----->created ${d.displayName} with id $dni"
        } else {
            log.debug "found ${d.displayName} with id $dni already exists"
        }
        return d
    }
    log.debug "Completed creating devices"

    pollTask()
}

import groovy.json.JsonSlurper
def httpCallback(physicalgraph.device.HubResponse hubResponseX) {
    setLookupInfo()

    //log.debug "httpCallback - Status: {$hubResponseX.status}"
    //log.debug "httpCallback - Body: {$hubResponseX.json}"

    def object = new groovy.json.JsonSlurper().parseText(hubResponseX.body)
    state.thermostatList = [:]
    state.data = [:]
    state.outsideairtemp = 0

    def systemName = "Thermostat"

    if (hubResponseX.status == 200) {
        log.debug "-----APIRESP(systems/id/status) 3"
        state.outsideAirTemp = object.oat[0]
        object.zones[0].zone.each {
            zone ->
                if (zone.enabled[0] == "on") {
                    def dni = [app.id, systemName, zone.id[0], zone.name[0] ].join('|')
                    log.debug "DNI:: " + dni

					state.thermostatList[dni] = systemName + ":" + zone.name[0]

                    //Get the current status of each device
                    state.data[dni] = [
                        temperature: zone.rt[0],
                        humidity: zone.rh[0],
                        coolingSetpoint: zone.clsp[0],
                        heatingSetpoint: zone.htsp[0],
                        thermostatFanMode: zone.fan[0],
                        //thermostatOperatingState: zone.zoneconditioning[0],
                        thermostatOperatingState: object.mode[0],
                        thermostatActivityState: zone.currentActivity[0],
                        thermostatHoldStatus: zone.hold[0],
                        thermostatHoldUntil: zone.otmr[0],
                        //thermostatDamper: zone.damperposition[0],
                        thermostatZoneId: zone.id[0]
                    ]
                    log.debug "===== " + zone.name[0] + " ====="
                    if (state.SystemRunning) {
                        refreshChild(dni)
                    }
                    /*
                                           log.debug "Temperature: " + state.data[dni].temperature
                                           log.debug "Humidity: " + state.data[dni].humidity
                                           log.debug "Heat set point: " + state.data[dni].heatingSetpoint
                                           log.debug "Cooling set point: " + state.data[dni].coolingSetpoint
                                           log.debug "Fan: " + state.data[dni].thermostatFanMode
                                           log.debug "Operating state: " + state.data[dni].thermostatOperatingState
                                           log.debug "Current schedule mode: " + state.data[dni].thermostatActivityState
                                           log.debug "Current Hold Status: " + state.data[dni].thermostatHoldStatus
                                           log.debug "Hold Until: " + state.data[dni].thermostatHoldUntil
                                           log.debug "Current Damper Position: " + state.data[dni].thermostatDamper
                                           log.debug "Current Outside Temp: " + state.outsideAirTemp
                                           log.debug "Zone ID: " + state.data[dni].thermostatZoneId
                                           log.debug "=====Done=====" */
                }
        }
    } else {
        log.debug "API request failed"
    }

    log.debug state.thermostatList
    //xxxxx return thermostatList
}

private syncSystem() {
    def result = new physicalgraph.device.HubAction(
        method: "GET",
        path: "/api/status",
        headers: [
            "HOST": configURL
        ],
        null, [callback: httpCallback]
    )
    log.debug "syncSystem called"
    try {
        sendHubCommand(result)
    } catch (all) {
        log.error "Error executing internal web request: $all"
    }
}

private changeHtsp(zoneId, heatingSetPoint) {

    //First Adjust the Manual Comfort Profile
    def result = new physicalgraph.device.HubAction(
        method: "GET",
        path: "/api/" + zoneId + "/activity/manual",
        headers: [
            "HOST": configURL
        ],
        query: [htsp: heatingSetPoint]
    )
    log.debug "HTTP GET Parameters: " + result
    try {
        sendHubCommand(result)
    } catch (all) {
        log.error "Error executing internal web request: $all"
    }

    //Now tell the zone to use the Manual Comfort Profile
    def NowDate = new Date(now())
    log.debug "Now = ${NowDate.format('dd-MM-yy HH:mm',location.timeZone)}"
    NowDate.set(minute: NowDate.minutes + 15)
    def HoldTime = NowDate.format('HH:mm', location.timeZone)
    log.debug "Later = " + HoldTime

    result = new physicalgraph.device.HubAction(
        method: "GET",
        path: "/api/" + zoneId + "/hold",
        headers: [
            "HOST": configURL
        ],
        query: [activity: "manual", until: "24:00"]
    )
    log.debug "HTTP GET Parameters: " + result
    try {
        sendHubCommand(result)
    } catch (all) {
        log.error "Error executing internal web request: $all"
    }
}

private changeClsp(zoneId, coolingSetpoint) {

    //First Adjust the Manual Comfort Profile
    def result = new physicalgraph.device.HubAction(
        method: "GET",
        path: "/api/" + zoneId + "/activity/manual",
        headers: [
            "HOST": configURL
        ],
        query: [clsp: coolingSetpoint]
    )
    log.debug "HTTP GET Parameters: " + result
    try {
        sendHubCommand(result)
    } catch (all) {
        log.error "Error executing internal web request: $all"
    }

    //Now tell the zone to use the Manual Comfort Profile
    def NowDate = new Date(now())
    log.debug "Now = ${NowDate.format('dd-MM-yy HH:mm',location.timeZone)}"
    NowDate.set(minute: NowDate.minutes + 15)
    def HoldTime = NowDate.format('HH:mm', location.timeZone)
    log.debug "Later = " + HoldTime

    result = new physicalgraph.device.HubAction(
        method: "GET",
        path: "/api/" + zoneId + "/hold",
        headers: [
            "HOST": configURL
        ],
        query: [activity: "manual", until: "24:00"]
    )
    log.debug "HTTP GET Parameters: " + result
    try {
        sendHubCommand(result)
    } catch (all) {
        log.error "Error executing internal web request: $all"
    }
}

private changeProfile(zoneId, nextProfile) {
    //Now tell the zone to use the nextProfile Comfort Profile
    log.debug "Changing Profile for Zone " + zoneId + " to " + nextProfile

    def result = new physicalgraph.device.HubAction(
        method: "GET",
        path: "/api/" + zoneId + "/hold",
        headers: [
            "HOST": configURL
        ],
        query: [activity: nextProfile, until: "24:00"]
    )
    log.debug "HTTP GET Parameters: " + result
    try {
        sendHubCommand(result)
    } catch (all) {
        log.error "Error executing internal web request: $all"
    }
}

def setLookupInfo() {
    state.lookup = [
        thermostatOperatingState: [:],
        thermostatFanMode: [:],
        thermostatMode: [:],
        activity: [:],
        coolingSetPointHigh: [:],
        coolingSetPointLow: [:],
        heatingSetPointHigh: [:],
        heatingSetPointLow: [:],
        differenceSetPoint: [:],
        temperatureRangeF: [:]
    ]
    state.lookup.thermostatMode["off"] = "off"
    state.lookup.thermostatMode["cool"] = "cool"
    state.lookup.thermostatMode["heat"] = "heat"
    state.lookup.thermostatMode["fanonly"] = "off"
    state.lookup.thermostatMode["auto"] = "auto"
    state.lookup.thermostatOperatingState["heat"] = "heating"
    state.lookup.thermostatOperatingState["hpheat"] = "heating"
    state.lookup.thermostatOperatingState["cool"] = "cooling"
    state.lookup.thermostatOperatingState["off"] = "idle"
    state.lookup.thermostatOperatingState["fanonly"] = "fan only"
    state.lookup.thermostatFanMode["off"] = "auto"
    state.lookup.thermostatFanMode["low"] = "circulate"
    state.lookup.thermostatFanMode["med"] = "on"
    state.lookup.thermostatFanMode["high"] = "on"
}

// lookup value translation
def lookupInfo(lookupName, lookupValue, lookupMode) {
    if (lookupName == "thermostatFanMode") {
        if (lookupMode) {
            return state.lookup.thermostatFanMode.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatFanMode.find {
                it.value == lookupValue.toString()
            } ?.key
        }
    }
    if (lookupName == "thermostatMode") {
        if (lookupMode) {
            return state.lookup.thermostatMode.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatMode.find {
                it.value == lookupValue.toString()
            } ?.key
        }
    }
    if (lookupName == "thermostatOperatingState") {
        if (lookupMode) {
            return state.lookup.thermostatOperatingState.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatOperatingState.find {
                it.value == lookupValue.toString()
            } ?.key
        }
    }
}
def pollTask() {
    syncSystem()
    runIn(60 * settings.polling, pollTask)
}
def refreshChild(dni) {
    log.debug "Refreshing for child " + dni
    //def dni = [ app.id, systemID, zone.’@id’.text().toString() ].join(’|’)
    def d = getChildDevice(dni)
    if (d) {
        log.debug "–Refreshing Child Zone ID: " + state.data[dni].thermostatZoneId
        d.zUpdate(state.data[dni].temperature,
            state.data[dni].thermostatOperatingState,
            state.data[dni].humidity,
            state.data[dni].heatingSetpoint,
            state.data[dni].coolingSetpoint,
            state.data[dni].thermostatFanMode,
            state.data[dni].thermostatActivityState,
            state.outsideAirTemp,
            state.data[dni].thermostatHoldStatus,
            state.data[dni].thermostatHoldUntil,
            state.data[dni].thermostatDamper,
            state.data[dni].thermostatZoneId)
        log.debug "Data sent to DH"
    } else {
        log.debug "Skipping refresh for unused thermostat"
    }
}
1 Like

Device Handler (Part 1: code is too long to post in one shot and the forum is also preventing me from making another post untll someone replies! So, part deux will have to wait)

/*

Copyright 2015 SmartThings

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.
(Based on) Ecobee Thermostat
Author: SmartThings
Date: 2013-06-13
*/
metadata {
    definition(name: "Infinitude Thermostat", namespace: "SmartThingsMod", author: "SmartThingsMod") {
        capability "Actuator"
        capability "Thermostat"
        capability "Temperature Measurement"
        capability "Sensor"
        capability "Refresh"
        capability "Relative Humidity Measurement"
        capability "Health Check"

        command "generateEvent"
        command "resumeProgram"
        command "switchMode"
        command "switchFanMode"
        command "lowerHeatingSetpoint"
        command "raiseHeatingSetpoint"
        command "lowerCoolSetpoint"
        command "raiseCoolSetpoint"
        // To satisfy some SA/rules that incorrectly using poll instead of Refresh
        command "poll"
        command "profileUpdate"

        attribute "thermostat", "string"
        attribute "maxCoolingSetpoint", "number"
        attribute "minCoolingSetpoint", "number"
        attribute "maxHeatingSetpoint", "number"
        attribute "minHeatingSetpoint", "number"
        attribute "deviceTemperatureUnit", "string"
        attribute "deviceAlive", "enum", ["true", "false"]
        attribute "thermostatSchedule", "string"
        attribute "damperPosition", "number"
        attribute "holdStatus", "string"
        attribute "holdUntil", "string"
        attribute "outsideTemp", "number"
        attribute "zoneId", "string"
    }

    tiles {
        multiAttributeTile(name: "temperature", type: "generic", width: 3, height: 2, canChangeIcon: true) {
            tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
                attributeState("temperature", label: '\n${currentValue}°', icon: "st.alarm.temperature.normal", backgroundColors: [
                    // Celsius
                    [value: 0, color: "#153591"],
                    [value: 7, color: "#1e9cbb"],
                    [value: 15, color: "#90d2a7"],
                    [value: 23, color: "#44b621"],
                    [value: 28, color: "#f1d801"],
                    [value: 35, color: "#d04e00"],
                    [value: 37, color: "#bc2323"],
                    // Fahrenheit
                    [value: 40, color: "#153591"],
                    [value: 44, color: "#1e9cbb"],
                    [value: 59, color: "#90d2a7"],
                    [value: 74, color: "#44b621"],
                    [value: 84, color: "#f1d801"],
                    [value: 95, color: "#d04e00"],
                    [value: 96, color: "#bc2323"]
                ])
            }
            tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
                attributeState "humidity", label: '${currentValue}%', icon: "st.Weather.weather12"
            }
        }

        standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width: 2, height: 1, decoration: "flat") {
            state "heatingSetpoint", label: 'Heat Up', action: "raiseHeatingSetpoint" , icon: "st.thermostat.thermostat-up"
        }
        valueTile("thermostat", "device.thermostat", width: 2, height: 1, decoration: "flat") {
            state "thermostat", label: 'Activity:\n${currentValue}', backgroundColor: "#ffffff"
        }
        standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "heatingSetpoint", label: 'Cool Up', action: "raiseCoolSetpoint", icon: "st.thermostat.thermostat-up"
        }

        valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "heatingSetpoint", label: '${currentValue}°', backgroundColor: "#e86d13"
        }

        standardTile("thermostatSchedule", "device.thermostatSchedule", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "home", action: "profileUpdate", label: 'Profile: Home', nextState: "changing" /*, icon: "st.Home.home4" */
            state "away", action: "profileUpdate", label: 'Profile: Away', nextState: "changing" /*, icon: "st.Home.home2" */
            state "sleep", action: "profileUpdate", label: 'Profile: Sleep', nextState: "changing" /*, icon: "st.Home.home1" */
            state "wake", action: "profileUpdate", label: 'Profile: Wake', nextState: "changing" /*, icon: "st.Home.home1" */
            state "manual", action: "profileUpdate", label: 'Profile: Manual', nextState: "changing" /*, icon: "st.Home.home4" */
            state "changing", label: 'Updating...' /*, icon: "st.motion.motion.active" */
        }

        valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "coolingSetpoint", label: '${currentValue}°', backgroundColor: "#00a0dc"
        }

        standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "heatingSetpoint", label: 'Heat Down', action: "lowerHeatingSetpoint", icon: "st.thermostat.thermostat-down"
        }
        standardTile("refresh", "device.thermostatMode", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "default", label: 'Refresh', action: "refresh.refresh" /*, icon: "st.secondary.refresh"*/
        }
        standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "coolingSetpoint", label: 'Cool Down', action: "lowerCoolSetpoint", icon: "st.thermostat.thermostat-down"
        }
        valueTile("fanMode", "device.thermostatFanMode", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "fanMode", label: 'Fan Mode:\n${currentValue}', backgroundColor: "ffffff"
        }
        valueTile("outsideTemp", "device.outsideAirTemp", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "outsideTemp", label: '${currentValue}° outside', backgroundColor: "#ffffff" /*, icon: "st.Weather.weather14" */
        }
        valueTile("holdStatus", "device.thermostatHoldStatus", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "holdStatus", label: 'Hold:\n${currentValue}', backgroundColor: "#ffffff"
        }
        valueTile("holdUntil", "device.thermostatHoldUntil", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "holdUntil", label: 'Hold Until:\n${currentValue}', backgroundColor: "#ffffff"
        }
        valueTile("damperPosition", "device.thermostatDamper", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "damperPosition", label: 'Damper:\n${currentValue}', backgroundColor: "#ffffff"
        }
        valueTile("zoneId", "device.zoneId", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "zoneId", label: 'Zone ID:\n${currentValue}', backgroundColor: "#ffffff"
        }



        // Not Displaying These        
        standardTile("resumeProgram", "device.resumeProgram", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "resume", action: "resumeProgram", nextState: "updating", label: 'Resume', icon: "st.samsung.da.oven_ic_send"
            state "updating", label: "Working", icon: "st.secondary.secondary"
        }
        standardTile("mode", "device.thermostatMode", width: 2, height: 1, inactiveLabel: false, decoration: "flat") {
            state "off", action: "switchMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off"
            state "heat", action: "switchMode", nextState: "updating", icon: "st.thermostat.heat"
            state "cool", action: "switchMode", nextState: "updating", icon: "st.thermostat.cool"
            state "auto", action: "switchMode", nextState: "updating", icon: "st.thermostat.auto"
            state "emergency heat", action: "switchMode", nextState: "updating", icon: "st.thermostat.emergency-heat"
            state "updating", label: "Updating...", icon: "st.secondary.secondary"
        }
        // Not Displaying These   



        main "temperature"
        details(["temperature", "thermostatSchedule", "raiseHeatingSetpoint", "raiseCoolSetpoint",
            "thermostat", "heatingSetpoint", "coolingSetpoint", "fanMode", "lowerHeatingSetpoint",
            "lowerCoolSetpoint", "damperPosition", "holdStatus", "holdUntil", "refresh", "outsideTemp",
            "zoneId"
        ])
    }

    preferences {
        //input "holdType", "enum", title: "Hold Type",
        // description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)",
        // required: false, options:["Temporary", "Permanent"]
        //input "deadbandSetting", "number", title: "Minimum temperature difference between the desired Heat and Cool " +
        // "temperatures in Auto mode:\nNote! This must be the same as configured on the thermostat",
        // description: "temperature difference °F", defaultValue: 5,
        // required: false
    }

}

void installed() {
    // The device refreshes every 5 minutes by default so if we miss 2 refreshes we can consider it offline
    // Using 12 minutes because in testing, device health team found that there could be "jitter"
    sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "cloud"], displayed: false)
}

// Device Watch will ping the device to proactively determine if the device has gone offline
// If the device was online the last time we refreshed, trigger another refresh as part of the ping.
def ping() {
    def isAlive = device.currentValue("deviceAlive") == "true" ? true : false
    if (isAlive) {
        refresh()
    }
}

// parse events into attributes
def parse(String description) {
    log.debug "Parsing ‘${description}'"
}

def refresh() {
    log.debug "refresh"
    sendEvent([name: "thermostat", value: "updating"])
    poll2()
}
void poll() {}
void poll2() {
    log.debug "Executing poll using parent SmartApp"
    //log.debug "Id: " + device.id + ", Name: " + device.name + ", Label: " + device.label + ", NetworkId: " + device.deviceNetworkId
    //parent.refreshChild(device.deviceNetworkId)
    parent.syncSystem()
}

def profileUpdate() {
    sendEvent([name: "thermostat", value: "updating"])
    def currentProfile = device.currentValue("thermostatSchedule")
    def currentZone = device.currentValue("zoneId")
    def nextProfile = "changing"

    log.debug "-- Entering Profile Update -- " + currentProfile

    if (currentProfile == "manual") {
        nextProfile = "home"
    }
    if (currentProfile == "home") {
        nextProfile = "away"
    }
    if (currentProfile == "away") {
        nextProfile = "sleep"
    }
    if (currentProfile == "sleep") {
        nextProfile = "awake"
    }
    if (currentProfile == "awake") {
        nextProfile = "home"
    }

    log.debug "Profile Update for Zone : " + currentZone + " from: " + currentProfile + " to: " + nextProfile
    parent.changeProfile(currentZone, nextProfile)
    sendEvent([name: "thermostatSchedule", value: nextProfile])
    runIn(15, "refresh", [overwrite: true])
}

def zUpdate(temp, systemStatus, hum, hsp, csp, fan, currSched, oat, hold, otmr, damperposition, zoneid) {
    log.debug "zupdate: " + temp + ", " + systemStatus + ", " + hum + ", " + hsp + ", " + csp + ", " + fan + ", " + currSched + ", " + oat + ", " + hold + ", " + otmr + ", " + damperposition + ", " + zoneid
    sendEvent([name: "temperature", value: temp, unit: "F"])
    sendEvent([name: "thermostat", value: systemStatus])
    sendEvent([name: "humidity", value: hum])
    sendEvent([name: "heatingSetpoint", value: hsp])
    sendEvent([name: "coolingSetpoint", value: csp])
    sendEvent([name: "thermostatFanMode", value: fan])
    sendEvent([name: "outsideAirTemp", value: oat])
    sendEvent([name: "thermostatSchedule", value: currSched])
    sendEvent([name: "thermostatHoldStatus", value: hold])
    sendEvent([name: "thermostatHoldUntil", value: otmr])
    sendEvent([name: "thermostatDamper", value: damperposition])
    sendEvent([name: "zoneId", value: zoneid])

}
def generateEvent(Map results) {
    if (results) {
        def linkText = getLinkText(device)
        def supportedThermostatModes = ["off"]
        def thermostatMode = null
        def locationScale = getTemperatureScale()

        results.each {
            name,
            value ->
            def event = [name: name, linkText: linkText, handlerName: name]
            def sendValue = value

            if (name == "temperature" || name == "heatingSetpoint" || name == "coolingSetpoint") {
                sendValue = getTempInLocalScale(value, "F") // API return temperature values in F
                event << [value: sendValue, unit: locationScale]
            } else if (name == "maxCoolingSetpoint" || name == "minCoolingSetpoint" || name == "maxHeatingSetpoint" || name == "minHeatingSetpoint") {
                // Old attributes, keeping for backward compatibility
                sendValue = getTempInLocalScale(value, "F") // API return temperature values in F
                event << [value: sendValue, unit: locationScale, displayed: false]
                // Store min/max setpoint in device unit to avoid conversion rounding error when updating setpoints
                device.updateDataValue(name + "Fahrenheit", "${value}")
            } else if (name == "heatMode" || name == "coolMode" || name == "autoMode" || name == "auxHeatMode") {
                if (value == true) {
                    supportedThermostatModes << ((name == "auxHeatMode") ? "emergency heat" : name - "Mode")
                }
                return // as we don't want to send this event here, proceed to next name/value pair
            } else if (name == "thermostatFanMode") {
                sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false)
                event << [value: value, data: [supportedThermostatFanModes: fanModes()]]
            } else if (name == "humidity") {
                event << [value: value, displayed: false, unit: "%"]
            } else if (name == "deviceAlive") {
                event['displayed'] = false
            } else if (name == "thermostatMode") {
                thermostatMode = (value == "auxHeatOnly") ? "emergency heat" : value.toLowerCase()
                return // as we don't want to send this event here, proceed to next name/value pair
            } else {
                event << [value: value.toString()]
            }
            event << [descriptionText: getThermostatDescriptionText(name, sendValue, linkText)]
            sendEvent(event)
        }
        if (state.supportedThermostatModes != supportedThermostatModes) {
            state.supportedThermostatModes = supportedThermostatModes
            sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
        }
        if (thermostatMode) {
            sendEvent(name: "thermostatMode", value: thermostatMode, data: [supportedThermostatModes: state.supportedThermostatModes], linkText: linkText,
                descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode")
        }
        generateSetpointEvent()
        generateStatusEvent()
    }
}

//return descriptionText to be shown on mobile activity feed
private getThermostatDescriptionText(name, value, linkText) {
    if (name == "temperature") {
        return "temperature is {value}°{location.temperatureScale}"

    } else if (name == "heatingSetpoint") {
        return "heating setpoint is ${value}°${location.temperatureScale}"

    } else if (name == "coolingSetpoint") {
        return "cooling setpoint is ${value}°${location.temperatureScale}"

    } else if (name == "thermostatMode") {
        return "thermostat mode is ${value}"

    } else if (name == "thermostatFanMode") {
        return "thermostat fan mode is ${value}"

    } else if (name == "humidity") {
        return "humidity is ${value} %"
    } else {
        return "${name} = ${value}"
    }
}

void setHeatingSetpoint(setpoint) {
    log.debug "***setHeatingSetpoint($setpoint)"
    if (setpoint) {
        state.heatingSetpoint = setpoint.toDouble()
        runIn(2, "updateSetpoints", [overwrite: true])
    }
}

def setCoolingSetpoint(setpoint) {
    log.debug "***setCoolingSetpoint($setpoint)"
    if (setpoint) {
        state.coolingSetpoint = setpoint.toDouble()
        runIn(2, "updateSetpoints", [overwrite: true])
    }
}

def updateSetpoints() {
    def deviceScale = "F" //API return/expects temperature values in F
    def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null]
    def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
    def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
    if (state.heatingSetpoint) {
        data = enforceSetpointLimits("heatingSetpoint", [targetValue: state.heatingSetpoint,
            heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint
        ])
    }
    if (state.coolingSetpoint) {
        heatingSetpoint = data.targetHeatingSetpoint ? getTempInLocalScale(data.targetHeatingSetpoint, deviceScale) : heatingSetpoint
        coolingSetpoint = data.targetCoolingSetpoint ? getTempInLocalScale(data.targetCoolingSetpoint, deviceScale) : coolingSetpoint
        data = enforceSetpointLimits("coolingSetpoint", [targetValue: state.coolingSetpoint,
            heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint
        ])
    }
    state.heatingSetpoint = null
    state.coolingSetpoint = null
    updateSetpoint(data)
}

void resumeProgram() {
    log.debug "resumeProgram() is called"

    sendEvent("name": "thermostat", "value": "resuming schedule", "description": statusText, displayed: false)
    def deviceId = device.deviceNetworkId.split(/\./).last()
    if (parent.resumeProgram(deviceId)) {
        sendEvent("name": "thermostat", "value": "setpoint is updating", "description": statusText, displayed: false)
        sendEvent("name": "resumeProgram", "value": "resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
    } else {
        sendEvent("name": "thermostat", "value": "failed resume click refresh", "description": statusText, displayed: false)
        log.error "Error resumeProgram() check parent.resumeProgram(deviceId)"
    }
    //xyzrunIn(5, "refresh", [overwrite: true])
}

def modes() {
    return state.supportedThermostatModes
}

def fanModes() {
    // Ecobee does not report its supported fanModes; use hard coded values
    ["on", "auto"]
}

def switchSchedule() {
    //TODO
}
def switchMode() {
    def currentMode = device.currentValue("thermostatMode")
    def modeOrder = modes()
    if (modeOrder) {
        def next = {
            modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0]
        }
        def nextMode = next(currentMode)
        switchToMode(nextMode)
    } else {
        log.warn "supportedThermostatModes not defined"
    }
}

def switchToMode(mode) {
    log.debug "switchToMode: ${mode}"
    def deviceId = device.deviceNetworkId.split(/./).last()
    // Thermostat's mode for "emergency heat" is "auxHeatOnly"
    if (!(parent.setMode(((mode == "emergency heat") ? "auxHeatOnly" : mode), deviceId))) {
        log.warn "Error setting mode:$mode"
        // Ensure the DTH tile is reset
        generateModeEvent(device.currentValue("thermostatMode"))
    }
    //XYZ runIn(5, "refresh", [overwrite: true])
}

def switchFanMode() {
    def currentFanMode = device.currentValue("thermostatFanMode")
    def fanModeOrder = fanModes()
    def next = {
        fanModeOrder[fanModeOrder.indexOf(it) + 1] ?: fanModeOrder[0]
    }
    switchToFanMode(next(currentFanMode))
}

def switchToFanMode(fanMode) {
    log.debug "switchToFanMode: $fanMode"
    def heatingSetpoint = getTempInDeviceScale("heatingSetpoint")
    def coolingSetpoint = getTempInDeviceScale("coolingSetpoint")
    def deviceId = device.deviceNetworkId.split(/./).last()
    def sendHoldType = holdType ? ((holdType == "Temporary") ? "nextTransition" : "indefinite") : "indefinite"

    if (!(parent.setFanMode(heatingSetpoint, coolingSetpoint, deviceId, sendHoldType, fanMode))) {
        log.warn "Error setting fanMode:fanMode"
        // Ensure the DTH tile is reset
        generateFanModeEvent(device.currentValue("thermostatFanMode"))
    }
    //XYZ runIn(5, "refresh", [overwrite: true])
}

def getDataByName(String name) {
    state[name] ?: device.getDataValue(name)
}

def setThermostatMode(String mode) {
    log.debug "setThermostatMode($mode)"
    def supportedModes = modes()
    if (supportedModes) {
        mode = mode.toLowerCase()
        def modeIdx = supportedModes.indexOf(mode)
        if (modeIdx < 0) {
            log.warn("Thermostat mode $mode not valid for this thermostat")
            return
        }
        mode = supportedModes[modeIdx]
        switchToMode(mode)
    } else {
        log.warn "supportedThermostatModes not defined"
    }
}

def setThermostatFanMode(String mode) {
    log.debug "setThermostatFanMode($mode)"
    mode = mode.toLowerCase()
    def supportedFanModes = fanModes()
    def modeIdx = supportedFanModes.indexOf(mode)
    if (modeIdx < 0) {
        log.warn("Thermostat fan mode $mode not valid for this thermostat")
        return
    }
    mode = supportedFanModes[modeIdx]
    switchToFanMode(mode)
}

def generateModeEvent(mode) {
    sendEvent(name: "thermostatMode", value: mode, data: [supportedThermostatModes: device.currentValue("supportedThermostatModes")],
        isStateChange: true, descriptionText: "device.displayName is in {mode} mode")
}

def generateFanModeEvent(fanMode) {
    sendEvent(name: "thermostatFanMode", value: fanMode, data: [supportedThermostatFanModes: device.currentValue("supportedThermostatFanModes")],
        isStateChange: true, descriptionText: "device.displayName fan is in {fanMode} mode")
}

def generateOperatingStateEvent(operatingState) {
    sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "device.displayName is {operatingState}", displayed: true)
}
2 Likes

Here’s your trigger for the next post…

@zraken,
Thanks for the hint, SmartApp: https://pastebin.com/xmAgia7y
Device Handler: https://pastebin.com/QcyjB1g4

Does this work better?

@zraken, Can we collaborate through the pastebin and keep one copy updated?

I’m not following the changes you made, were you able to download my code and update it, should I update my end?

I just kept fixing errors as I noticed them on the debug log, the main one was an issue with character encoding of quotes. There were additional issues with icons not working on my android phone, one issue with damper (I don’t have one in my System and it was throwing an exception while decoding response), current activity was decoded incorrectly and the function “runIn” was referred to in multiple places as “runin” (lower case “i”). I might be forgetting one or two more. Oh and I did code formatting so it is easier to read,
Anyway here is part 2 of device handler:

def off() {
    setThermostatMode("off")
}
def heat() {
    setThermostatMode("heat")
}
def emergencyHeat() {
    setThermostatMode("emergency heat")
}
def cool() {
    setThermostatMode("cool")
}
def auto() {
    setThermostatMode("auto")
}

def fanOn() {
    setThermostatFanMode("on")
}
def fanAuto() {
    setThermostatFanMode("auto")
}
def fanCirculate() {
    setThermostatFanMode("circulate")
}

// =============== Setpoints ===============
def generateSetpointEvent() {
    def mode = device.currentValue("thermostatMode")
    def setpoint = getTempInLocalScale("heatingSetpoint") // (mode == "heat") || (mode == "emergency heat")
    def coolingSetpoint = getTempInLocalScale("coolingSetpoint")

    if (mode == "cool") {
        setpoint = coolingSetpoint
    } else if ((mode == "auto") || (mode == "off")) {
        setpoint = roundC((setpoint + coolingSetpoint) / 2)
    } // else (mode == "heat") || (mode == "emergency heat")
    sendEvent("name": "thermostatSetpoint", "value": setpoint, "unit": location.temperatureScale)
}

def raiseHeatingSetpoint() {
    sendEvent([name: "thermostat", value: "updating"])
    alterSetpoint(true, "heatingSetpoint")
    def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
    def currentZone = device.currentValue("zoneId")
    log.debug "Raising Htsp on Zone: " + currentZone + " to: " + heatingSetpoint
    parent.changeHtsp(currentZone, heatingSetpoint)
    runIn(15, "refresh", [overwrite: true])
}

def lowerHeatingSetpoint() {
    sendEvent([name: "thermostat", value: "updating"])
    alterSetpoint(false, "heatingSetpoint")
    def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
    def currentZone = device.currentValue("zoneId")
    log.debug "Lowering Htsp on Zone: " + currentZone + " to: " + heatingSetpoint
    parent.changeHtsp(currentZone, heatingSetpoint)
    runIn(15, "refresh", [overwrite: true])
}

def raiseCoolSetpoint() {
    sendEvent([name: "thermostat", value: "updating"])
    alterSetpoint(true, "coolingSetpoint")
    def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
    def currentZone = device.currentValue("zoneId")
    log.debug "Raising Csp on Zone: " + currentZone + " to: " + coolingSetpoint
    parent.changeClsp(currentZone, coolingSetpoint)
    runIn(15, "refresh", [overwrite: true])
}

def lowerCoolSetpoint() {
    sendEvent([name: "thermostat", value: "updating"])
    alterSetpoint(false, "coolingSetpoint")
    def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
    def currentZone = device.currentValue("zoneId")
    log.debug "Lowering Csp on Zone: " + currentZone + " to: " + coolingSetpoint
    parent.changeClsp(currentZone, coolingSetpoint)
    runIn(15, "refresh", [overwrite: true])
}

// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false
def alterSetpoint(raise, setpoint) {
    // don't allow setpoint change if thermostat is off
    if (device.currentValue("thermostatMode") == "off") {
        return
    }
    def locationScale = getTemperatureScale()
    def deviceScale = "F"
    def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
    def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
    def targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint
    def delta = (locationScale == "F") ? 1 : 0.5
    targetValue += raise ? delta : -delta

    def data = enforceSetpointLimits(setpoint, [targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint], raise)
    // update UI without waiting for the device to respond, this to give user a smoother UI experience
    // also, as runIn's have to overwrite and user can change heating/cooling setpoint separately separate runIn's have to be used
    if (data.targetHeatingSetpoint) {
        sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, deviceScale),
            unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
    }
    if (data.targetCoolingSetpoint) {
        sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale),
            unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
    }
    runIn(5, "updateSetpoint", [data: data, overwrite: true])
}

def enforceSetpointLimits(setpoint, data, raise = null) {
    def locationScale = getTemperatureScale()
    def minSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("minHeatingSetpointFahrenheit") : device.getDataValue("minCoolingSetpointFahrenheit")
    def maxSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("maxHeatingSetpointFahrenheit") : device.getDataValue("maxCoolingSetpointFahrenheit")
    minSetpoint = minSetpoint ? Double.parseDouble(minSetpoint) : ((setpoint == "heatingSetpoint") ? 45 : 65) // default 45 heat, 65 cool
    maxSetpoint = maxSetpoint ? Double.parseDouble(maxSetpoint) : ((setpoint == "heatingSetpoint") ? 79 : 92) // default 79 heat, 92 cool
    def deadband = deadbandSetting ? deadbandSetting : 5 // °F
    def delta = (locationScale == "F") ? 1 : 0.5
    def targetValue = getTempInDeviceScale(data.targetValue, locationScale)
    def heatingSetpoint = getTempInDeviceScale(data.heatingSetpoint, locationScale)
    def coolingSetpoint = getTempInDeviceScale(data.coolingSetpoint, locationScale)
    // Enforce min/mix for setpoints
    if (targetValue > maxSetpoint) {
        targetValue = maxSetpoint
    } else if (targetValue < minSetpoint) {
        targetValue = minSetpoint
    } else if ((raise != null) && ((setpoint == "heatingSetpoint" && targetValue == heatingSetpoint) ||
            (setpoint == "coolingSetpoint" && targetValue == coolingSetpoint))) {
        // Ensure targetValue differes from old. When location scale differs from device,
        // converting between C -> F -> C may otherwise result in no change.
        targetValue += raise ? delta : -delta
    }
    // Enforce deadband between setpoints
    if (setpoint == "heatingSetpoint") {
        heatingSetpoint = targetValue
        coolingSetpoint = (heatingSetpoint + deadband > coolingSetpoint) ? heatingSetpoint + deadband : coolingSetpoint
    }
    if (setpoint == "coolingSetpoint") {
        coolingSetpoint = targetValue
        heatingSetpoint = (coolingSetpoint - deadband < heatingSetpoint) ? coolingSetpoint - deadband : heatingSetpoint
    }
    return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint]
}

def updateSetpoint(data) {
    def deviceId = device.deviceNetworkId.split(/\./).last()
    def sendHoldType = holdType ? ((holdType == "Temporary") ? "nextTransition" : "indefinite") : "indefinite"

    /* if (parent.setHold(data.targetHeatingSetpoint, data.targetCoolingSetpoint, deviceId, sendHoldType)) {
    log.debug "alterSetpoint succeed to change setpoints:${data}"
    } else {
    log.error "Error alterSetpoint"
    }
    */

    //XYZ runIn(5, "refresh", [overwrite: true])
}

def generateStatusEvent() {
    def mode = device.currentValue("thermostatMode")
    def heatingSetpoint = device.currentValue("heatingSetpoint")
    def coolingSetpoint = device.currentValue("coolingSetpoint")
    def temperature = device.currentValue("temperature")
    def statusText = "Right Now: Idle"
    def operatingState = "idle"

    if (mode == "heat" || mode == "emergency heat") {
        if (temperature < heatingSetpoint) {
            statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
            operatingState = "heating"
        }
    } else if (mode == "cool") {
        if (temperature > coolingSetpoint) {
            statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
            operatingState = "cooling"
        }
    } else if (mode == "auto") {
        if (temperature < heatingSetpoint) {
            statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
            operatingState = "heating"
        } else if (temperature > coolingSetpoint) {
            statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
            operatingState = "cooling"
        }
    } else if (mode == "off") {
        statusText = "Right Now: Off"
    } else {
        statusText = "?"
    }

    sendEvent("name": "thermostat", "value": statusText, "description": statusText, displayed: true)
    sendEvent("name": "thermostatOperatingState", "value": operatingState, "description": operatingState, displayed: false)
}

def generateActivityFeedsEvent(notificationMessage) {
    sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}

// Get stored temperature from currentState in current local scale
def getTempInLocalScale(state) {
    def temp = device.currentState(state)
    def scaledTemp = convertTemperatureIfNeeded(temp.value.toBigDecimal(), temp.unit).toDouble()
    return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
}

// Get/Convert temperature to current local scale
def getTempInLocalScale(temp, scale) {
    def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble()
    return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
}

// Get stored temperature from currentState in device scale
def getTempInDeviceScale(state) {
    def temp = device.currentState(state)
    if (temp && temp.value && temp.unit) {
        return getTempInDeviceScale(temp.value.toBigDecimal(), temp.unit)
    }
    return 0
}

def getTempInDeviceScale(temp, scale) {
    if (temp && scale) {
        //API return/expects temperature values in F
        return ("F" == scale) ? temp : celsiusToFahrenheit(temp).toDouble().round(0).toInteger()
    }
    return 0
}

def roundC(tempC) {
    return (Math.round(tempC.toDouble() * 2)) / 2
}
1 Like

@swerb73 best way to collaborate is through github. I’ve uploaded the files here:

Ahh, great @zraken, Github is probably the way to go.

I’ll grab your copy later today and see if I have any issues.

I was about to start working through adding functionality to the DH to allow you to control the main thermostat climate mode (Heat/Cool/Auto/FanOnly/Off). The challenge I ran into is the current SmartApp uses /api/status whereas the only way to get this info is through /api/config, as well as setting it that way in Infinitude. I have an open feature request in Infinitude to add this info to api/status but no progress yet.

Think we can modify the SA/DH to get at this and allow users to set it through the DH?

Also, maybe we can get other contributors that can help me better sync the DH/SA refresh status with infinitude, it works now but it isn’t ideal…

1 Like

@swerb73 just pushed a major update!
Android users are currently unable to see any ST hosted icons, so I uploaded relevant icons to github and referenced those in the DH. Also, deleted zone id, zone number, outside temp, and current profile tiles and added a row of buttons at the top to select “Home/Away/Sleep/Wake/Auto” modes with just a tap. Moved the refresh tile up to 2nd row. The background will change to green for the selected mode plus for Auto the currently active mode will get a Blueish background. Added text info for humidity and outside temp. to the main multi-attribute tile. Note that because Infinitude refreshes at some random time, the 15 second refresh in the DH may fire too soon at times and not show the latest mode in use - a manual refresh will fix that (until there’s a deterministic way to do the refresh). The namespace for the app and DH is now InfinitudeST.
BTW, on Infinitude side it keeps refreshing a few files very frequently and may wear out the SD card too soon (if you are using an SD card say with a Raspberry PI). I moved the file to tmpFS to prevent the write-wear.
I can add you as a contributor to my Repo so you can make changes.

1 Like

Wow, great job @zraken, thank you!

Question 1, before I start hacking away, any ideas about the icon sizes on iOS, see my screenshots below. They are larger than yours and don’t look quite right? I noted you’re calling the icons on github for display, is there a way to size them correctly for iOS?

Question 2, I do like seeing zone name and zone id, any thoughts to adding that on the gear/configure screen maybe?

Question 3, I noticed the “Auto” doesn’t really do anything on my end, well I don’t use auto, I like to use Heat or Cool only, should we change this to indicate which mode is running or are you trying to do something else there?

Question 4, if I missed what you’re trying to do in Q3 above (Auto), I’d still like to get at the overall system mode, this is only available in /api/config, not in api/status which we started with SA with in the first place. Thoughts?

Question 5, note the thermostat tile on my home screen, how can we fix the text color to be white so it shows properly, see screenshot below.

I’d love to be a contributor, I can help move things forward.

Screenshots:

Thanks!

1 Like
  1. The icons look big and ugly! Really unfortunate that they look different on the two platforms. One way to make it better for IOS might be to edit the icons and add a border with the actual icon shrunk down. if find a way to add a configuration item to select different icons (don’t know yet how that could be done).
  2. The zone name appears on the top of the screen already (see the top blue portion of your screen grab), granted it does not show the ID number but I didn’t think that was important (but that’s just me!). Maybe re-purpose the bottom left tile (damper) for it? BTW, do you get anything useful from the damper tile? In any case I have not removed any of the tiles related code so it is easy to put the tiles back or reorder them by changing the one line that selects the tile order.
  3. Ok, maybe this only makes sense to me… The Auto basically gets you out of Manual Hold mode. If you press any of the other icons on the left it will put you in that mode (say Sleep) until midnight and now if you press Auto it will get you beck to “Per Schedule” mode. Also when you are in Auto/Per Schedule mode one of the left icons (whichever one is active based on schedule) will have a blue background. You can see that in my screenshot (Home has the blue/violet background).
  4. System mode = Heat/Cool/Auto/Fanonly/Off right? Did you already have that working? Is changing this supported in Infinitude? (if not I can add that to infinitude). Let me know if you are talking about something I accidentally removed or something new you would like to add.
  5. I have no idea (yet), need to look into that. BTW, was it different with your earlier DH? Also, it is already white on my Samsung galaxy/Android!

As for making this better, let’s come up with a list of changes needed and then divide and conquer via Github.

1 Like
  1. OK, I’ll poke around at a way to do this, resize them on the fly, vs. having to build brand new icons.
  2. Ahh, duh, I like the zone ID since I sometimes troubleshoot with it, that was why I was thinking about hiding it behind the gear/configure icon? I do like Damper info as well, it shows me how far the damper is open (0 is close, 15 is fully open), helps when seeing how/why air is flowing in a damper controlled environment.
  3. Ahh, Auto, that’s basically Resume from Hold, returning back to the preconfigured schedule. I like it, maybe it needs a better word. The tile also looks odd to me, word auto is one line too low.
  4. I didn’t have that working but wanted to add it. Basically I wanted to see the configuration of the system and be able to change it from any stat. “mode” in /api/status is the current running mode of a zone which is different than the “mode” of the entire system which you can only get by calling /api/config (this isn’t yet supported/used in the SmartApp). You can change it like this: http://IP:PORT/api/config?mode=fanonly (or heat/cool/auto/off).
  5. No, it was the same, I just haven’t figured out how to change those icons in the dashboard. Damn Android!

I may need some pointers on Github, I’ve never directly contributed, always download locally and hack away… I’ll shoot you a PM.

1 Like