Is there any way to make this show up as a thermostat device as opposed to a switch?
edit - I used Gemini to fix the device code, works perfectly now as a thermostat: (I have no idea how to code so if this can be cleaned up by all means do so 
/*
Envi Heater Device (Heat-only Thermostat)
Notes:
- Implements capability "Thermostat" (heat/off) plus Switch for convenience.
- Uses heatingSetpoint as the active setpoint (thermostatSetpoint mirrors it).
- Fan-related thermostat commands are stubbed (no-op) for compatibility.
*/
import groovy.json.JsonOutput
metadata {
definition(name: "Envi Heater Device", namespace: "bcn-israelforst", author: "Israel Forst") {
capability "Actuator"
capability "Sensor"
capability "TemperatureMeasurement"
capability "Thermostat"
capability "Switch"
capability "Refresh"
// Optional, keep if you use it elsewhere
attribute "available", "boolean"
}
preferences {
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
}
}
// ---- Lifecycle ----
void installed() {
logInfo "Installed"
initialize()
}
void updated() {
logInfo "Updated"
initialize()
}
private void initialize() {
// Declare supported thermostat modes/fan modes as JSON arrays
sendEvent(name: "supportedThermostatModes", value: JsonOutput.toJson(["heat", "off"]))
sendEvent(name: "supportedThermostatFanModes", value: JsonOutput.toJson(["auto"]))
// Ensure required attributes are never null
if (device.currentValue("thermostatFanMode") == null) {
sendEvent(name: "thermostatFanMode", value: "auto")
}
if (device.currentValue("thermostatMode") == null) {
sendEvent(name: "thermostatMode", value: "off")
}
if (device.currentValue("thermostatOperatingState") == null) {
sendEvent(name: "thermostatOperatingState", value: "idle")
}
// Provide sane defaults for setpoints if missing
def unit = "°${location.temperatureScale}"
def hsp = device.currentValue("heatingSetpoint")
if (hsp == null) {
hsp = (location.temperatureScale == "C") ? 20 : 68
sendEvent(name: "heatingSetpoint", value: hsp, unit: unit)
}
// thermostatSetpoint should mirror heatingSetpoint for heat-only
if (device.currentValue("thermostatSetpoint") == null) {
sendEvent(name: "thermostatSetpoint", value: hsp, unit: unit)
}
// Optional: populate coolingSetpoint to avoid nulls in some dashboards/apps
if (device.currentValue("coolingSetpoint") == null) {
def csp = (location.temperatureScale == "C") ? (hsp + 2) : (hsp + 5)
sendEvent(name: "coolingSetpoint", value: csp, unit: unit)
}
}
// ---- Hubitat required methods ----
void refresh() {
parent?.refreshChild(deviceId())
}
private String deviceId() {
// Remove 'envi-' prefix (assumes DNI format like: envi-)
return device.deviceNetworkId?.startsWith("envi-") ? device.deviceNetworkId.substring(5) : device.deviceNetworkId
}
// ---- Thermostat commands ----
void setHeatingSetpoint(BigDecimal temperature) {
logDebug "setHeatingSetpoint(${temperature})"
Integer temp = temperature?.toBigDecimal()?.setScale(0, BigDecimal.ROUND_HALF_UP) as Integer
if (temp == null) return
// Validate range (50-85°F); adjust if running in °C
if (location.temperatureScale == "C") {
// ~10-29°C (approx of 50-85°F); adjust if you prefer different limits
if (temp < 10 || temp > 29) {
logWarn "Temperature ${temp}°C outside valid range (10-29°C) - clamping"
temp = Math.max(10, Math.min(29, temp))
}
} else {
if (temp < 50 || temp > 85) {
logWarn "Temperature ${temp}°F outside valid range (50-85°F) - clamping"
temp = Math.max(50, Math.min(85, temp))
}
}
def unit = "°${location.temperatureScale}"
sendEvent(name: "heatingSetpoint", value: temp, unit: unit)
sendEvent(name: "thermostatSetpoint", value: temp, unit: unit)
// Keep a non-null coolingSetpoint (optional safety)
def csp = device.currentValue("coolingSetpoint")
if (csp == null) {
def newCsp = (location.temperatureScale == "C") ? (temp + 2) : (temp + 5)
sendEvent(name: "coolingSetpoint", value: newCsp, unit: unit)
}
parent?.childSetTemperature(deviceId(), temp)
}
void setThermostatSetpoint(BigDecimal temperature) {
// For heat-only, map thermostatSetpoint -> heatingSetpoint
logDebug "setThermostatSetpoint(${temperature})"
setHeatingSetpoint(temperature)
}
void setThermostatMode(String mode) {
logDebug "setThermostatMode(${mode})"
if (!mode) return
// Normalize a few common values
if (mode == "emergency heat") mode = "heat"
if (mode == "auto") mode = "heat"
if (mode == "cool") {
logWarn "cool mode not supported; switching off instead"
mode = "off"
}
if (mode == "heat") {
sendEvent(name: "thermostatMode", value: "heat")
// Operating state should reflect actual heater state; default to idle until parent reports otherwise
if (device.currentValue("thermostatOperatingState") == null) {
sendEvent(name: "thermostatOperatingState", value: "idle")
}
parent?.childTurnOn(deviceId())
} else if (mode == "off") {
sendEvent(name: "thermostatMode", value: "off")
sendEvent(name: "thermostatOperatingState", value: "idle")
parent?.childTurnOff(deviceId())
} else {
logWarn "Unsupported thermostatMode '${mode}' (supported: heat, off)"
}
}
// Capability ThermostatMode commands
void heat() { setThermostatMode("heat") }
void auto() { setThermostatMode("heat") } // mapped for compatibility
void cool() { setThermostatMode("off") } // mapped for compatibility
void emergencyHeat() { setThermostatMode("heat") } // mapped for compatibility
// Fan commands (no-op but present for compatibility with Thermostat capability)
void fanAuto() { setThermostatFanMode("auto") }
void fanOn() { setThermostatFanMode("auto") }
void fanCirculate() { setThermostatFanMode("auto") }
void setThermostatFanMode(String fanMode) {
// This device has no fan control; report a stable value
sendEvent(name: "thermostatFanMode", value: "auto")
}
// ---- Switch commands (map to thermostat) ----
// Note: off() serves both Switch and Thermostat capabilities
void on() {
logDebug "on()"
setThermostatMode("heat")
}
void off() {
logDebug "off()"
setThermostatMode("off")
}
// ---- Optional helper for parent updates ----
// Call these from parent when you know actual heater state
void setOperatingState(String state) {
// expected: "heating" or "idle"
if (state in ["heating", "idle"]) {
sendEvent(name: "thermostatOperatingState", value: state)
} else {
logWarn "Unsupported operating state '${state}'"
}
}
// ---- Logging helpers ----
private void logDebug(msg) { if (logEnable) log.debug "EnviHeaterDevice(${deviceId()}): ${msg}" }
private void logInfo(msg) { log.info "EnviHeaterDevice(${deviceId()}): ${msg}" }
private void logWarn(msg) { log.warn "EnviHeaterDevice(${deviceId()}): ${msg}" }