Emporia Home Energy Monitor

hubitat energy cost calculator. Can someone update this app to include child branch circuits for me? So that I can get real time cost per room or branch circuit I already had the Emporia integrated into Hubitat I just need to add Oklahoma OG&E cost to each room in circuit. I just wanted to work with my Emporia Home Energy monitor and show all the bran they are set up as child branches according to the driver the community one. I get the code working up to this point but now it's deleting the table and not updating the cost. I am not good at programming so Google AI got me this far so I would appreciate your guys help in getting this done. What I'm trying to do is update the live data In the table without the table deleting or the screen scrolling back up to the top.

/**
 *  Energy Cost Calculator - Part 1 (Table Deletion Fixed)
 *  
 *  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://apache.org
 */

import java.util.regex.*

definition(
    name: "Energy Cost Calculator",
    namespace: "thebearmay",
    author: "Jean P. May, Jr.",
    description: "Calculate energy cost over various time frames with naturally sorted circuits, live kW, Voltage columns, and scroll-safe updates.",
    category: "Utility",
    iconUrl: "",
    iconX2Url: "",
    installOnHub: true
)

mappings {
    path("/rateInput") { action: [GET: "renderRatePage"] }
    path("/saveRate") { action: [POST: "processRateUpdate"] }
    path("/tableData") { action: [GET: "renderRawTableData"] }
}

preferences {
    page(name: "mainPage")
    page(name: "variableLinkPage")
}

def mainPage() {
    dynamicPage(name: "mainPage", title: "Energy Cost Calculator", install: true, uninstall: true) {
        if(!state.accessToken) { 
            try {
                createAccessToken() 
            } catch(e) {
                log.error "OAuth Token generation failed! Please ensure you clicked 'Enable OAuth' in the App Code settings screen."
            }
        }
        
        section("App Name") {
            label title: "Assign an optional name for this application instance:", required: false
        }
        
        section("Device Selection") {
            input name: "energyDevices", type: "capability.sensor", title: "Select Energy Devices to Measure Cost", multiple: true, required: true, submitOnChange: true
        }

        section() {
            href url: (getFullLocalApiServerUrl() + "/rateInput?access_token=" + state.accessToken), title: "Click here to update the rate information", style: "external"
        }
        
        section() {
            if(energyDevices) {
                href name: "vLinkPage", page: "variableLinkPage", title: "Click here to add and/or remove variable links", description: ""
            }
        }

        if(energyDevices) {
            section() {
                def rateVal = state.rate ?: 0.11
                paragraph "<div style='text-align:center; font-weight:bold; color:#22c55e;'>Current Pricing is \$" + rateVal + " per KWH</div>"
                
                def tableHtml = buildTelemetryTable()
                paragraph tableHtml
                
                input name: "btnRefresh", type: "button", title: "Refresh Table Data"
            }
        }
        
        section() {
            input name: "advancedOptions", type: "bool", title: "Click here to access advanced options/utilities", defaultValue: false, submitOnChange: true
            if(advancedOptions) {
                input name: "resetStats", type: "bool", title: "Reset all calculated historic telemetry statistics?", defaultValue: false, submitOnChange: true
                if(resetStats) {
                    appButtonHandler("btnReset")
                    app.updateSetting("resetStats", false)
                }
            }
        }
    }
}
/**
 *  Energy Cost Calculator - Part 2 (Table Deletion Fixed)
 */

def renderRatePage() {
    def currentRate = state.rate ?: 0.11
    def html = "<html><head><style>body{font-family:sans-serif; background:#1a1a1a; color:#fff; padding:30px;} .card{background:#2a2a2a; padding:20px; border-radius:8px; max-width:400px; margin:auto; box-shadow:0 4px 10px rgba(0,0,0,0.5);} input[type=text]{width:100%; padding:10px; margin:10px 0; border-radius:4px; border:1px solid #444; background:#333; color:#fff;} input[type=submit]{background:#22c55e; color:white; border:none; padding:10px 20px; border-radius:4px; cursor:pointer; font-weight:bold; width:100%;}</style></head><body>"
    html += "<div class='card'><h2>Update Energy Rate</h2>"
    html += "<form action='saveRate?access_token=" + state.accessToken + "' method='POST'>"
    html += "<p>Set your current electric rate cost per kWh:</p>"
    html += "<input type='text' name='newRate' value='" + currentRate + "'>"
    html += "<input type='submit' value='Save Rate Changes'>"
    html += "</form></div></body></html>"
    
    render(contentType: "text/html", data: html, status: 200)
}

def processRateUpdate() {
    if (params.newRate) {
        try {
            state.rate = params.newRate.toDouble()
            log.info "Energy Cost Engine pricing model updated to: \$" + state.rate
        } catch (e) {
            log.error "Failed to parse electric rate option value: " + e.message
        }
    }
    def html = "<html><head><script>window.close();</script></head><body style='font-family:sans-serif; background:#1a1a1a; color:#fff; text-align:center; padding-top:50px;'><h2>Rate Saved Successfully!</h2><p>You can close this tab now.</p></body></html>"
    
    render(contentType: "text/html", data: html, status: 200)
}

// FIX: Spits out the frozen compiled string text straight from cache memory. 
// It is physically impossible to drop data or delete the table here because it never evaluates device lists from background loops.
def renderRawTableData() {
    if (state.cachedRowsHtml == null || state.cachedRowsHtml == "") {
        state.cachedRowsHtml = generateTableRowsOnly()
    }
    render(contentType: "text/html", data: state.cachedRowsHtml, status: 200)
}

def variableLinkPage() {
    dynamicPage(name: "variableLinkPage", title: "Hub Variable Mapping Matrix") {
        section("Map Circuits to Hub Variables (Optional)") {
            if (energyDevices) {
                energyDevices.each { dev ->
                    def sanitizedName = dev.displayName.replaceAll("'", "")
                    input name: "vToday_" + dev.id, type: "sidebar", title: sanitizedName + " - Today's Cost Variable", required: false
                }
            }
        }
    }
}

def installed() {
    initialize()
}

def updated() {
    unsubscribe()
    initialize()
}

def initialize() {
    if(!state.accessToken) {
        createAccessToken()
    }
    if(state.deviceData == null) {
        state.stateDeviceNames = [:]
        state.deviceData = [:]
    }
    if (energyDevices) {
        energyDevices.each { dev ->
            subscribe(dev, "energy", energyHandler)
            subscribe(dev, "power", powerHandler)
            subscribe(dev, "voltage", voltageHandler)
            
            def devIdStr = dev.id.toString()
            state.stateDeviceNames[devIdStr] = dev.displayName.replaceAll("'", "")
            if(state.deviceData[devIdStr] == null) {
                def deviceKwh = dev.currentValue("energy") != null ? dev.currentValue("energy").toDouble() : 0.0
                state.deviceData[devIdStr] = [
                    todayEnergy: 0.0, yesterdayEnergy: 0.0,
                    weekEnergy: 0.0, lastWeekEnergy: 0.0,
                    monthEnergy: 0.0, lastMonthEnergy: 0.0,
                    lastResetDay: new Date().getDate()
                ]
                state["last_kwh_" + devIdStr] = deviceKwh
            }
        }
    }
    state.cachedRowsHtml = generateTableRowsOnly()
}

def energyHandler(evt) {
    def devId = evt.deviceId.toString()
    if(state.deviceData == null) state.deviceData = [:]
    def data = state.deviceData[devId]
    if(!data) {
        state.deviceData[devId] = [
            todayEnergy: 0.0, yesterdayEnergy: 0.0,
            weekEnergy: 0.0, lastWeekEnergy: 0.0,
            monthEnergy: 0.0, lastMonthEnergy: 0.0,
            lastResetDay: new Date().getDate()
        ]
        data = state.deviceData[devId]
    }

    checkDateRollovers()
    
    def currentVal = 0.0
    try {
        currentVal = evt.value != null ? evt.value.toDouble() : 0.0
    } catch (e) {
        currentVal = evt.floatValue != null ? evt.floatValue.toDouble() : 0.0
    }
    
    def previousVal = state["last_kwh_" + devId] ?: currentVal
    def delta = Math.max(0.0, currentVal - previousVal)
    state["last_kwh_" + devId] = currentVal

    data.todayEnergy += delta
    data.weekEnergy += delta
    data.monthEnergy += delta
    
    updateVariableLinks(devId, data)
    state.cachedRowsHtml = generateTableRowsOnly()
}

def powerHandler(evt) {
    state.cachedRowsHtml = generateTableRowsOnly()
}

def voltageHandler(evt) {
    state.cachedRowsHtml = generateTableRowsOnly()
}
/**
 *  Energy Cost Calculator - Part 3 (Table Deletion Fixed)
 */

def buildTelemetryTable() {
    def html = "<div style='overflow-x:auto;'><table style='width:100%; border:1px solid #ddd; text-align:left; font-size:13px; font-family:sans-serif; border-collapse:collapse;'>"
    html += "<thead style='background-color:#f2f2f2; border-bottom:2px solid #aaa;'><tr>"
    html += "<th style='padding:8px; border:1px solid #ddd;'>Meter</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right; color:#22c55e;'>Live kW</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right; color:#ffa500;'>Volts</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right;'>Use Today</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right;'>Today's Cost</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right;'>Use Week</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right;'>Cost Week</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right;'>Use Month</th>"
    html += "<th style='padding:8px; border:1px solid #ddd; text-align:right;'>Cost Month</th></tr></thead>"
    
    html += "<tbody id='emporia-grid-body'>"
    state.cachedRowsHtml = generateTableRowsOnly()
    html += state.cachedRowsHtml
    html += "</tbody></table></div>"
    
    html += "<div style='font-size:10px; color:#999; font-family:monospace; text-align:right; margin-top:4px;'>Data updating live in background (Scroll-Safe Cache)...</div>"
    
    html += "<script>"
    html += "function fetchEmporiaData() {"
    html += "  fetch('tableData?access_token=" + state.accessToken + "')"
    html += "    .then(response => {"
    html += "       if (response.status === 200) return response.text();"
    html += "       throw new Error('Data placeholder drop protection...');"
    html += "    })"
    html += "    .then(htmlContent => {"
    html += "      var container = document.getElementById('emporia-grid-body');"
    html += "      if (container && htmlContent && htmlContent.trim().length > 0 && !htmlContent.includes('Protected from blank')) {"
    html += "         container.innerHTML = htmlContent;"
    html += "      }"
    html += "    }).catch(err => { /* Locks original numbers visually in place with zero modifications */ });"
    html += "}"
    html += "setInterval(fetchEmporiaData, 10000);"
    html += "</script>"
    
    return html
}

def generateTableRowsOnly() {
    def rate = state.rate ?: 0.11
    if(state.deviceData == null) state.deviceData = [:]
    if(state.stateDeviceNames == null) state.stateDeviceNames = [:]
    def html = ""
    
    def aggTEng = 0.0, aggTCst = 0.0
    def aggWEng = 0.0, aggWCst = 0.0
    def aggMEng = 0.0, aggMCst = 0.0
    def aggTotalKw = 0.0

    // FIX: If background loop context blinds the hardware array, instantly break operation and return raw text
    // This instructs the background fetch interpreter to leave your existing full rows untouched on the browser screen.
    if (!energyDevices && state.cachedRowsHtml != null && state.cachedRowsHtml != "") {
        return state.cachedRowsHtml
    }
    if (!energyDevices) return ""

    // Alphanumeric sorting lists: Main stays at index position 0
    def sortedEntries = state.deviceData.entrySet().toList().sort { a, b ->
        def nameA = state.stateDeviceNames[a.key] ?: ""
        def nameB = state.stateDeviceNames[b.key] ?: ""
        
        def lowA = nameA.toLowerCase(); def lowB = nameB.toLowerCase()
        if (lowA.contains("main") && !lowB.contains("main")) return -1
        if (!lowA.contains("main") && lowB.contains("main")) return 1
        
        def matcherA = nameA =~ /\d+/; def matcherB = nameB =~ /\d+/
        if (matcherA.find() && matcherB.find()) {
            def numA = matcherA.group().toInteger(); def numB = matcherB.group().toInteger()
            if (numA != numB) return numA <=> numB
        }
        return nameA.compareToIgnoreCase(nameB)
    }

    sortedEntries.each { entry ->
        def devIdStr = entry.key
        def data = entry.value
        def dev = energyDevices.find { it.id.toString() == devIdStr }
        def sanitizedName = state.stateDeviceNames[devIdStr] ?: ("Device ID " + devIdStr)
        
        def rawWatts = 0.0
        def liveVolts = 120.0
        if(dev) {
            rawWatts = dev.currentValue("power") != null ? dev.currentValue("power").toDouble() : 0.0
            liveVolts = dev.currentValue("voltage") != null ? dev.currentValue("voltage").toDouble() : 120.0
            sanitizedName = dev.displayName.replaceAll("'", "")
            state.stateDeviceNames[devIdStr] = sanitizedName
        }
        
        def liveKw = rawWatts / 1000.0
        def currentDeviceTotal = dev ? (dev.currentValue("energy") != null ? dev.currentValue("energy").toDouble() : 0.0) : 0.0
        
        def tEng = data ? (data.todayEnergy ?: 0.0) : 0.0
        def wEng = data ? (data.weekEnergy ?: 0.0) : 0.0
        def mEng = data ? (data.monthEnergy ?: 0.0) : 0.0
        
        if(tEng == 0.0 && wEng == 0.0 && mEng == 0.0 && currentDeviceTotal > 0.0) {
            tEng = currentDeviceTotal
            wEng = currentDeviceTotal
            mEng = currentDeviceTotal
        }

        def tCst = tEng * rate
        def wCst = wEng * rate
        def mCst = mEng * rate

        if(!sanitizedName.toLowerCase().contains("main")) {
            aggTotalKw += liveKw
            aggTEng += tEng; aggTCst += tCst
            aggWEng += wEng; aggWCst += wCst
            aggMEng += mEng; aggMCst += mCst
        }

        def rowStyle = "border-bottom:1px solid #ddd;"
        if(sanitizedName.toLowerCase().contains("main")) {
            rowStyle = "border-bottom:2px solid #00ffcc; background-color:#1c2d2d; font-weight:bold;"
        }

        html += "<tr style='" + rowStyle + "'><td style='padding:8px; border:1px solid #ddd; font-weight:bold; color:#0056b3;'>" + sanitizedName + "</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; font-weight:bold; color:#22c55e;'>${String.format('%.3f', liveKw)} kW</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#ffa500;'>${String.format('%.1f', liveVolts)}V</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace;'>${String.format('%.1f', tEng)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#85144b;'>\$${String.format('%.2f', tCst)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace;'>${String.format('%.1f', wEng)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#001f3f;'>\$${String.format('%.2f', wCst)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace;'>${String.format('%.1f', mEng)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#001f3f;'>\$${String.format('%.2f', mCst)}</td></tr>"
    }

    if(html != "") {
        html += "<tr style='background-color:#f9f9f9; font-weight:bold; border-top:2px solid #aaa;'> "
        html += "<td style='padding:8px; border:1px solid #ddd;'>Total Branches</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#22c55e;'>${String.format('%.3f', aggTotalKw)} kW</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#aaa;'>β€”</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace;'>${String.format('%.1f', aggTEng)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#85144b;'>\$${String.format('%.2f', aggTCst)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace;'>${String.format('%.1f', aggWEng)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#001f3f;'>\$${String.format('%.2f', aggWCst)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace;'>${String.format('%.1f', aggMEng)}</td>"
        html += "<td style='padding:8px; border:1px solid #ddd; text-align:right; font-family:monospace; color:#001f3f;'>\$${String.format('%.2f', aggMCst)}</td></tr>"
    }
    
    return html
}

def checkDateRollovers() {
    def currentDay = new Date().getDate()
    if (state.deviceData) {
        state.deviceData.each { devId, data ->
            if(data && data.lastResetDay != currentDay) {
                data.yesterdayEnergy = data.todayEnergy ?: 0.0
                data.todayEnergy = 0.0
                data.lastResetDay = currentDay
            }
        }
    }
}

def updateVariableLinks(devId, data) {
    def rate = state.rate ?: 0.11
    if (data) {
        if(settings["vToday_" + devId]) {
            setGlobalVar(settings["vToday_" + devId], Math.round(((data.todayEnergy ?: 0.0) * rate) * 100) / 100)
        }
    }
}

def appButtonHandler(btn) {
    if(btn == "btnRefresh") {
        state.cachedRowsHtml = generateTableRowsOnly()
    } else if(btn == "btnReset") {
        state.deviceData = [:]
        state.stateDeviceNames = [:]
        initialize()
    }
}

You might have better luck asking in the original Energy Cost Calculator thread β€” @thebearmay, the app author, is active there and knows the code. As a workaround, Hubitat's built-in Energy Usage app can track cost per device, so if each Emporia child shows up as its own device, that may give you the per-room cost without custom code. Posting the exact behavior and which community driver you're using there would help too.