EDIT if you are interested in this, please get the latest code from GitHub:
I've an Emporia energy monitor for my electrical system and solar, and thanks to @ke7lvb I have a driver for it. [RELEASE] Emporia Vue V2 driver for Hubitat
Now the Emporia has a system to use "Excess Solar" to charge my car which works well. Emporia is also supposed to have as system to use "Excess Solar" in conjunction with my ecobee thermostat, but it's very confusing, not well documented, not configurable, and seems tied with time-of-use energy pricing which I don't have. I tried to set it up and it bumped up the heat intolerably yesterday when the sun was shining, which didn't make me popular with SWMBO.
I decided to ask my buddy ChatGPT to write something simpler.
"I want a hubitat app that T hours before sunset, if I'm sending more than E kw of power from my solar back to the grid, lowers the temperature on my ecobee thermostat by Temp degrees. (But only if it's in air conditioning mode). When the excess solar drops below E2, it turns the temperature back up to what it was, but only if the temperature setpoint hasn't already been changed (manually, or by some other process)"
She gave me something that looked useful. Then I asked for a refinement:
"Let's give a configuration option of celcius (vs fahrenheit). Let's also deal with the situation where there's clouds at startmonitoring so the power threshold isn't reached at that time, by continuing to check every TD minutes (default 15 minutes) until sunset, if it doesn't get lowered at the first check."
The app she came up with is below. I can't really test it since it's not air conditioning season quite yet here. But, I think it looks good. If you're interested in this, can you have a glance at the code and let me know what you think? I'll post a follow up when I have a chance to test it, and get it into github.
I haven't customized the code at all yet, this version is pure ChatGPT's o4-mini-high model. Here's a link to the chat:
/**
* Solar-Driven AC Setback (with Celsius & re-checks)
*
* Author: Your Name
* Date: 2025-04-28
*
* Lowers Ecobee cooling setpoint by X° when solar export > E kW
* starting T hours before sunset, and restores it when export < E2.
* Includes option for Celsius vs Fahrenheit and will re-check
* every TD minutes until sunset if export threshold isn't hit initially.
*/
definition(
name: "Solar-Driven AC Setback",
namespace: "yournamespace",
author: "Your Name",
description: "Drop Ecobee coolingSetpoint by Temp° when solar export > E between sunsetāT and sunrise, with C/F option & repeated checks.",
category: "Green Energy",
iconUrl: "",
iconX2Url: ""
)
preferences {
section("Select your devices") {
input "solarMeter", "capability.powerMeter",
title: "Solar Power Meter (reports watts)", required: true
input "thermostat", "capability.thermostat",
title: "Ecobee Thermostat", required: true
}
section("Thresholds & offsets") {
input "offsetHours", "decimal",
title: "T hours before sunset to start watching", defaultValue: 2.0, required: true
input "thresholdHigh", "decimal",
title: "High threshold E (kW export)", defaultValue: 1.0, required: true
input "thresholdLow", "decimal",
title: "Low threshold E2 (kW export)", defaultValue: 0.5, required: true
input "tempChange", "decimal",
title: "Drop cooling setpoint by (°)", defaultValue: 2.0, required: true
}
section("Options") {
input "useCelsius", "bool",
title: "Use Celsius (uncheck for Fahrenheit)", defaultValue: false
input "checkInterval", "number",
title: "Re-check interval TD (minutes) until sunset", defaultValue: 15, required: true
}
}
def installed() { initialize() }
def updated() {
unsubscribe()
unschedule()
initialize()
}
private initialize() {
state.lowered = false
state.monitoring = false
state.originalSetpoint = null
state.loweredSetpoint = null
state.sunsetTime = null
subscribe(solarMeter, "power", onPowerEvent)
subscribe(thermostat, "thermostatMode", onThermostatMode)
subscribe(location, "sunriseTime", resetDaily)
scheduleDailyOffset()
}
def scheduleDailyOffset() {
def sun = getSunriseAndSunset()
def sunset = sun.sunset
state.sunsetTime = sunset.time
def offsetMs = (offsetHours.toDouble() * 3600 * 1000) as long
def runTime = new Date(sunset.time - offsetMs)
log.debug "Scheduling startMonitoring at ${runTime}"
schedule(runTime, "startMonitoring")
}
def startMonitoring() {
state.monitoring = true
log.info "Starting solar-driven setback monitoring at ${new Date()} until sunset (${new Date(state.sunsetTime)})"
// immediate check
def currentPower = (solarMeter.currentValue("power") as Number)?.toDouble() ?: 0.0
handlePower(currentPower)
// if not lowered yet, schedule re-checks
if (!state.lowered) {
scheduleNextCheck()
}
}
private scheduleNextCheck() {
def now = new Date().time
if (!state.lowered && now < state.sunsetTime) {
log.debug "Scheduling next threshold check in ${checkInterval} minutes"
runIn(checkInterval.toInteger() * 60, "thresholdCheck")
}
}
def thresholdCheck() {
if (!state.monitoring) return
def power = (solarMeter.currentValue("power") as Number)?.toDouble() ?: 0.0
handlePower(power)
if (!state.lowered) {
scheduleNextCheck()
}
}
def onPowerEvent(evt) {
if (!state.monitoring) return
handlePower((evt.value as Number).toDouble())
}
private handlePower(powerWatts) {
def highW = thresholdHigh.toDouble() * 1000
def lowW = thresholdLow.toDouble() * 1000
def mode = thermostat.currentThermostatMode
if (!state.lowered && mode == "cool" && powerWatts > highW) {
lowerSetpoint()
}
else if (state.lowered && powerWatts < lowW) {
restoreSetpoint()
}
}
def onThermostatMode(evt) {
if (state.monitoring && !state.lowered && evt.value == "cool") {
def p = (solarMeter.currentValue("power") as Number)?.toDouble() ?: 0.0
handlePower(p)
}
}
private lowerSetpoint() {
def curr = (thermostat.currentValue("coolingSetpoint") as Number).toDouble()
state.originalSetpoint = curr
def newSet = (curr - tempChange.toDouble()).round(1)
thermostat.setCoolingSetpoint(newSet)
state.loweredSetpoint = newSet
state.lowered = true
log.info "Lowered cooling setpoint from ${state.originalSetpoint}${unit()} to ${newSet}${unit()}."
}
private restoreSetpoint() {
def curr = (thermostat.currentValue("coolingSetpoint") as Number).toDouble()
if (curr != state.loweredSetpoint) {
log.warn "Setpoint was changed manually (is ${curr}${unit()}), not restoring."
} else {
thermostat.setCoolingSetpoint(state.originalSetpoint)
log.info "Restored cooling setpoint to ${state.originalSetpoint}${unit()}."
state.lowered = false
}
state.monitoring = false
}
def resetDaily(evt) {
log.debug "Sunriseāresetting daily state."
state.lowered = false
state.monitoring = false
state.originalSetpoint = null
state.loweredSetpoint = null
state.sunsetTime = null
scheduleDailyOffset()
}
private String unit() {
useCelsius ? "°C" : "°F"
}