Rheem EcoNet Integration maintained by Kris Linquist

Anybody else get their RS485 ESP32 hardware yet? :slight_smile:

I've got everything working except vacation mode.

My stuff hasn't arrived yet. Looking forward to getting it working!

Didn’t receive mine either. Shipping estimate was out a bit, so I don’t expect to see anything before a couple of weeks…

I am really looking forward to getting it though!!!

Same here

Thank you for your service sir, the case of those strings threw me off. This is great work!

1 Like

Just got my shipment notice from m5-stack, so I’m guessing it’s 10-14 days out. Psyched to try this out.

Same here. I guess they might have shipped them at the same time! :slight_smile:

1 Like

@klinquist, I have updated the app to support the use of either Fahrenheit or Celsius. Is there any interest in adding this to the main code?

/**
 *  Rheem EcoNet Water Heater
 *
 *  Originally by Dominick Meglio
 *  Forked by Kris Linquist in 2022
 *
 */

preferences {
    input("tempUnit", "enum", options:["C", "F"], defaultValue:"C", title: "Unit", description: "The unit to use for temperature", required: true, displayDuringSetup: true)
}

metadata {
    definition (name: "Rheem Econet Water Heater (New)", namespace: "klinquist.rheem", author: "kris@linquist.net") {
        capability "Initialize"
        capability "Thermostat"
        capability "Actuator"
        capability "Sensor"
        capability "ThermostatHeatingSetpoint"
        capability "ThermostatSetpoint"
        capability "ThermostatOperatingState"
        capability "ThermostatMode"
        
        command "setWaterHeaterMode", [[name:"Mode*","type":"ENUM","description":"Mode","constraints":["Heat Pump", "Energy Saver", "High Demand", "Normal", "Vacation", "Off"]]]
        
        attribute "waterHeaterMode", "ENUM"
        //attribute "waterHeaterMode", "ENUM", ["Heat Pump", "Energy Saver", "High Demand", "Normal", "Vacation", "Off"]
        
    }
}

import groovy.transform.Field
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import java.text.SimpleDateFormat

@Field static String apiUrl = "ssl://rheem.clearblade.com:1884"
@Field static String systemKey = "e2e699cb0bb0bbb88fc8858cb5a401"
@Field static String systemSecret = "E2E699CB0BE6C6FADDB1B0BC9A20"

def installed() {
    initialize()
}

def updated() {
    initialize()
}

def mqttConnectUntilSuccessful() {
    try {
        interfaces.mqtt.connect(apiUrl, parent.getClientId(), parent.getAccessToken(), systemKey, cleanSession: false)
        pauseExecution(3000)
        interfaces.mqtt.subscribe("user/${parent.getAccountId()}/device/reported", 2)
        interfaces.mqtt.subscribe("user/${parent.getAccountId()}/device/desired", 2)
        if (state.queuedMessage != null) {
            interfaces.mqtt.publish("user/${parent.getAccountId()}/device/desired", state.queuedMessage)
            state.queuedMessage = null
         }
        return true
    }
    catch (e)
    {
        log.warn "Lost connection to MQTT, retrying in 15 seconds"
        runIn(15, "mqttConnectUntilSuccessful")
        return false
    }
}

def initialize() {    
    if (device.getDataValue("enabledDisabled") == "true")
        sendEvent(name: "supportedThermostatModes", value: ["off", "heat"])
    else if (device.getDataValue("tempOnly") == "true")
        sendEvent(name: "supportedThermostatModes", value: [])
    else
        sendEvent(name: "supportedThermostatModes", value: ["off", "heat", "emergency heat", "auto"])
    sendEvent(name: "supportedThermostatFanModes", value: [])
    if (interfaces.mqtt.isConnected())
        interfaces.mqtt.disconnect()
    mqttConnectUntilSuccessful()

}

def publishWithRetry(msg) {
    def payload = buildMQTTMessage()
    for (property in msg.keySet()) {
        payload."$property" = msg[property]
    }
    if (interfaces.mqtt.isConnected()) {
        interfaces.mqtt.publish("user/${parent.getAccountId()}/device/desired", JsonOutput.toJson(payload))
    } 
    else {
        log.error "Not connected to MQTT, reconnecting and queuing command"
        state.queuedMessage = JsonOutput.toJson(payload)
        mqttConnectUntilSuccessful()
    }
}

def mqttClientStatus(String message) {
    if (message == "Status: Connection succeeded") {
        parent.logDebug "Connected to MQTT"
    }
    else if (message.contains("Connection lost") || message.contains("Client is not connected") || message.startsWith("Error:")) {
        parent.logDebug "Lost MQTT connection, reconnecting."
        try {
            interfaces.mqtt.disconnect() // Guarantee we're disconnected
        }
        catch (e) {
        }
        mqttConnectUntilSuccessful()
    }
    else
        log.warn "Status: " + message
}

def parse(String message) {
    def topic = interfaces.mqtt.parseMessage(message)
    log.info topic
    def payload =  new JsonSlurper().parseText(topic.payload) 

    if ("rheem:" + payload?.device_name + ":" + payload?.serial_number == device.deviceNetworkId) {
        parent.logDebug "MQTT Message was: ${topic.payload}"
        if (payload."@SETPOINT" != null) {
            //Conver to Celcius if unit is "C"
            def setPointTemp = payload."@SETPOINT"
            if (tempUnit=="C")
                setPointTemp = (setPointTemp-32)*5/9
                setPointTemp = Math.round(setPointTemp)
                setPointTemp = setPointTemp.toInteger()
            
            device.sendEvent(name: "heatingSetpoint", value: setPointTemp, unit: tempUnit)
            device.sendEvent(name: "thermostatSetpoint", value: setPointTemp, unit: tempUnit)
            device.sendEvent(name: "unit", value: tempUnit)
        }
        if (device.getDataValue("enabledDisabled") == "true" && payload."@ACTIVE" != null) {
            def mode = payload."@ACTIVE" == true ? "heat" : "off"
            device.sendEvent(name: "thermostatMode", value: mode)
        }
        if (payload."@MODE" != null) {
            if (!payload."@MODE".toString().isInteger()) {
                device.sendEvent(name: "thermostatMode", value: parent.translateThermostatMode(payload."@MODE".status))
                device.sendEvent(name: "waterHeaterMode", value: payload."@MODE".status)
            }
            else {
                def mode = translateEnumToWaterHeaderMode(payload."@MODE")
                device.sendEvent(name: "thermostatMode", value: parent.translateThermostatMode(mode))
                device.sendEvent(name: "waterHeaterMode", value: mode)
            }
        }

        if (payload."@RUNNING" != null) {
            def isRunning = payload."@RUNNING".toUpperCase().contains("RUNNING")
            device.sendEvent(name: "thermostatOperatingState", value: isRunning ? "heating" : "idle")    
        }
    }
}

def getDeviceName() {
    return device.deviceNetworkId.split(':')[1]
}

def getSerialNumber() {
    return device.deviceNetworkId.split(':')[2]
}

def buildMQTTMessage() {
    def sdf = new SimpleDateFormat("Y-M-d'T'H:m:s.S")
    def payload = [
        transactionId: "ANDROID_"+sdf.format(new Date()),
        device_name: getDeviceName(),
        serial_number: getSerialNumber()
    ]
    return payload
}

def setCoolingSetpoint(temperature) {
    log.error "setCoolingSetpoint called but not supported"
}

def setSchedule(obj) {
    log.error "setSchedule called but not supported"
}

def setThermostatFanMode(fanmode) {
    log.error "setThermostatFanMode called but not supported"
}
    
def setHeatingSetpoint(temperature) {
    //Conver back to Farenhite if unit is "C"
    if (tempUnit=="C")
        temperature=(temperature*9/5)+32
    
    def minTemp = new BigDecimal(device.getDataValue("minTemp"))
    def maxTemp = new BigDecimal(device.getDataValue("maxTemp"))
    if (temperature < minTemp)
        temperature = minTemp
    else if (temperature > maxTemp)
        temperature = maxTemp

    
    publishWithRetry(["@SETPOINT": temperature])
}

def setThermostatMode(thermostatmode) {
    if (device.getDataValue("enabledDisabled") == "true") {
        def onOff = thermostatmode == "off" ? 0 : 1
        publishWithRetry(["@ENABLED": onOff])
    }
    else if (device.getDataValue("tempOnly") != "true") {
        publishWithRetry(["@MODE": translateThermostatModeToEnum(thermostatmode)])
    }
    else
        log.error "setThermostatMode called but not supported"
}

def setWaterHeaterMode(waterheatermode) {
    if (device.getDataValue("enabledDisabled") == "true") {
        def onOff = thermostatmode == "off" ? 0 : 1
        publishWithRetry(["@ENABLED": onOff])
    }
    else if (device.getDataValue("tempOnly") != "true") {
        if (waterheatermode != "Vacation") {
            log.debug "Setting away mode to false and waiting 15 seconds"
            parent.setAwayMode(false)
            pauseExecution(15000)
        }
        log.debug "Setting mode to ${waterheatermode}"
        def onOff = thermostatmode == "off" ? 0 : 1
        publishWithRetry(["@ENABLED": onOff, "@MODE": translateWaterHeaterModeToEnum(waterheatermode)])
    }
    else
        log.error "setWaterHeaterMode called but not supported"
}

def translateWaterHeaterModeToEnum(waterheatermode) {
    switch (waterheatermode) {
        case "Off":
            return 0
        case "Energy Saver":
            return 1
        case "Heat Pump":
            return 2
        case "High Demand":
            return 3
        case "Normal":
            return 4
        case "Vacation":
            return 5
    }
}

def translateEnumToWaterHeaderMode(enumVal) {
    switch (enumVal) {
        case 0:
            return "OFF"
        case 1:
            return "ENERGY SAVING"
        case 2:
            return "HEAT PUMP ONLY"
        case 3:
            return "HIGH DEMAND"
        case 4:
            return "ELECTRIC"
        case 5:
            return "VACATION"
    }
}

def translateThermostatModeToEnum(waterheatermode) {
    switch (waterheatermode) {
        case "off":
            return 0
        case "auto":
            return 1
        case "heat":
            if (parent.hasHeatPump(device))
                return 2
            return 4
        case "emergency heat":
            return 3
    }
}

def auto() {
    if (device.getDataValue("tempOnly") != "true") {
        setThermostatMode("auto")
    }
    else
        log.error "auto called but not supported"
}

def emergencyHeat() {
    if (device.getDataValue("tempOnly") != "true") {
        setThermostatMode("emergency heat")
    }
    else
        log.error "emergencyHeat called but not supported"
}

def off() {
    if (device.getDataValue("tempOnly") != "true") {
        setThermostatMode("off")
    }
    else
        log.error "off called but not supported"
}

def heat() {
    if (device.getDataValue("tempOnly") != "true") {
        setThermostatMode("heat")
    }
    else
        log.error "heat called but not supported"
}

def cool() {
    log.error "cool called but not supported"
}

def fanAuto() {
    log.error" fanAuto called but not supported"
}

def fanCirculate() {
    log.error" fanCirculate called but not supported"
}

def fanOn() {
    log.error" fanOn called but not supported"
}
1 Like

Yes, could you open up a pull request?

1 Like

Will do!

Thanks @Sebastien I made a few changes and merged it in.

1 Like

Awesome! Thank you!

I was wondering the other day. Is there a way to get the energy consumption data from it? We can see in the app and I keep pulling it manually. Would be nice if it could be polled…

I haven’t checked yet to see if they have an API…

I have power usage as part of the ESPHome implementation :slight_smile:

2 Likes

Can’t wait for the hardware to arrrive. I’ve got that itch. @klinquist, did you ever get Vacation mode working too?

I just got my M5 hardware today. I hope to get it setup asap...

I got my ESP M5 hardware connected with home assistant. Have you published a Hubitat driver? If so, I am willing to try it out.

1 Like

It appears the latest 1.0.6 HPM update is not loading. I got some error message that said I needed to check that the package was in use in devices. It seems unusual for HPM that I would have to delete the device to update the App/Driver. I tried an HPM Repair but got the same result.

LJ

Directions for M5 Hardware

Installation of the hardware can be found here .

And you use this page to configure it for your wifi

Then....

  1. If you set up this device in HomeAssistant, edit the yaml for the ESPHome and remove the api: section. This removes the encryption.
  2. Paste this file into Libraries Code on your Hubitat: https://raw.githubusercontent.com/klinquist/hubitat-public/esphome-econet/ESPHome/ESPHome-API-Library.groovy
  3. Paste this file into Drivers Code on your Hubitat: https://raw.githubusercontent.com/klinquist/hubitat-public/esphome-econet/ESPHome/ESPHome-Rheem-Waterheater.groovy
  4. Devices -> Add Device -> Virtual -> ESPHome Rheem Water Heater, give it a name
  5. Find the device in your device list and add the ESPHome's IP address in the Preferences section. You should see the Current States populate within a few seconds.
    That should be it! Writing vacation mode still does not work, but everything else should...

Weird. Maybe you could try manually updating?

Go into Drivers Code, find the Rheem entry, paste this in and hit Save. https://raw.githubusercontent.com/klinquist/hubitat-rheem/main/drivers/Rheem_EcoNet_Water_Heater.groovy

OK, I know almost nothing about Home Assistant. Where do I get the ESPHome yaml code?

Without editing the Home Assistant Yaml code, it appears that I am communicating with the ESP and able to change states on the water heater. I am also getting updates from it. But I do have some messages in the log that appear there might be issues...

dev:992024-02-29 09:18:06.297 AMwarn[W][component:215]: Components should block for at most 20-30ms.
dev:992024-02-29 09:18:06.245 AMwarn[W][component:214]: Component econet took a long time for an operation (0.05 s).
dev:992024-02-29 09:17:36.987 AMwarnESPHome received unexpected message type #25 (expected #47)
dev:992024-02-29 09:17:36.956 AMwarnESPHome received unexpected message type #25 (expected #47)
dev:992024-02-29 09:17:36.922 AMwarnESPHome received unexpected message type #25 (expected #47)