/*********************************************************************************************************************** * * This is a Hubitat device driver which will connect to the Solaredge monitoring API and retrieve inverter, battery * and site overview data. * * License: * This program falls under the GNU GENERAL PUBLIC LICENSE Version 3. * For full license visit https://github.com/funzie19/hubitat-solaredge/blob/master/LICENSE * * Name: Solaredge Monitoring Driver * * Changelog: https://github.com/funzie19/hubitat-solaredge/blob/master/CHANGELOG.md * ***********************************************************************************************************************/ private version() { return "1.2.0" } metadata { definition( name: "Solaredge Monitoring Driver", namespace: "funzie19.solaredge", author: "Joseph Kregloh", importUrl: "https://raw.githubusercontent.com/funzie19/hubitat-solaredge/master/solaredge-driver.groovy", description: "Driver to query the Solaredge API endpoint and return data related to solar production and consumption for a given site." ) { capability "Power Meter" capability "Battery" capability "Refresh" command "updateTiles" command "getEnergy" command "getOverview" command "getSitePowerFlow" command "clearData" attribute "production", "number" attribute "consumption", "number" attribute "self_consumption", "number" attribute "feed_in", "number" attribute "purchased", "number" attribute "last_day", "number" attribute "last_month", "number" attribute "last_year", "number" attribute "lifetime", "number" attribute "grid_power", "string" attribute "load_power", "string" attribute "pv_power", "string" // Battery Storage Attributes attribute "storage_status", "string" // Idle, Charging, Discharging attribute "storage_power", "string" // Power either coming in (charging) or going out (Discharging), value is positive regardless of direction attribute "storage_charge", "string" // 51.0 for 51% charge attribute "battery", "number" // Duplication of storage_charge attribute to align to Battery capability attribute "energy_tile", "string" attribute "overview_tile", "string" attribute "power_flow_tile", "string" } preferences { section("Configure Solaredge API connection parameters") { input("site_id", "number", title: "Site Id", description: "Site Id to monitor.", required: true) input("api_key", "text", title: "API Key", description: "API key for given site id. This key is generated using installer level permissions. Ask installer to generate key or setup as sub-account with the correct permissions. Or sign up as an installer and take ownership of the inverter.", required: true) input("url", "text", title: "Solaredge Monitor API URL", description: "Endpoint URL for Solaredge Monitoring API.", required: true, defaultValue: "https://monitoringapi.solaredge.com") input("energy_refresh_interval", "enum", title: "How often to refresh the energy data", options: [ 255:"Do NOT update", 1:"1 Minute", 2:"2 Minutes", 3:"3 Minutes", 4:"4 Minutes", 5:"5 Minutes", 6:"6 Minutes", 7:"7 Minutes", 8:"8 Minutes", 9:"9 Minutes", 10:"10 Minutes", 11:"11 Minutes", 12:"12 Minutes", 13:"13 Minutes", 14:"14 Minutes", 15:"15 Minutes", 16:"16 Minutes", 17:"17 Minutes", 18:"18 Minutes", 19:"19 Minutes", 20:"20 Minutes", 21:"21 Minutes", 22:"22 Minutes", 23:"23 Minutes", 24:"24 Minutes", 25:"25 Minutes", 26:"26 Minutes", 27:"27 Minutes", 28:"28 Minutes", 29:"29 Minutes", 30:"30 Minutes", 31:"31 Minutes", 32:"32 Minutes", 33:"33 Minutes", 34:"34 Minutes", 35:"35 Minutes", 36:"36 Minutes", 37:"37 Minutes", 38:"38 Minutes", 39:"39 Minutes", 40:"40 Minutes", 41:"41 Minutes", 42:"42 Minutes", 43:"43 Minutes", 44:"44 Minutes", 45:"45 Minutes", 46:"46 Minutes", 47:"47 Minutes", 48:"48 Minutes", 49:"49 Minutes", 50:"50 Minutes", 51:"51 Minutes", 52:"52 Minutes", 53:"53 Minutes", 54:"54 Minutes", 55:"55 Minutes", 56:"56 Minutes", 57:"57 Minutes", 58:"58 Minutes", 59:"59 Minutes" ], required: true) input("overview_refresh_interval", "enum", title: "How often to refresh the production overview data", options: [ 255:"Do NOT update", 1:"1 Minute", 2:"2 Minutes", 3:"3 Minutes", 4:"4 Minutes", 5:"5 Minutes", 6:"6 Minutes", 7:"7 Minutes", 8:"8 Minutes", 9:"9 Minutes", 10:"10 Minutes", 11:"11 Minutes", 12:"12 Minutes", 13:"13 Minutes", 14:"14 Minutes", 15:"15 Minutes", 16:"16 Minutes", 17:"17 Minutes", 18:"18 Minutes", 19:"19 Minutes", 20:"20 Minutes", 21:"21 Minutes", 22:"22 Minutes", 23:"23 Minutes", 24:"24 Minutes", 25:"25 Minutes", 26:"26 Minutes", 27:"27 Minutes", 28:"28 Minutes", 29:"29 Minutes", 30:"30 Minutes", 31:"31 Minutes", 32:"32 Minutes", 33:"33 Minutes", 34:"34 Minutes", 35:"35 Minutes", 36:"36 Minutes", 37:"37 Minutes", 38:"38 Minutes", 39:"39 Minutes", 40:"40 Minutes", 41:"41 Minutes", 42:"42 Minutes", 43:"43 Minutes", 44:"44 Minutes", 45:"45 Minutes", 46:"46 Minutes", 47:"47 Minutes", 48:"48 Minutes", 49:"49 Minutes", 50:"50 Minutes", 51:"51 Minutes", 52:"52 Minutes", 53:"53 Minutes", 54:"54 Minutes", 55:"55 Minutes", 56:"56 Minutes", 57:"57 Minutes", 58:"58 Minutes", 59:"59 Minutes" ], required: true) input("power_flow_refresh_interval", "enum", title: "How often to refresh power flow data", options: [ 255:"Do NOT update", 1:"1 Minute", 2:"2 Minutes", 3:"3 Minutes", 4:"4 Minutes", 5:"5 Minutes", 6:"6 Minutes", 7:"7 Minutes", 8:"8 Minutes", 9:"9 Minutes", 10:"10 Minutes", 11:"11 Minutes", 12:"12 Minutes", 13:"13 Minutes", 14:"14 Minutes", 15:"15 Minutes", 16:"16 Minutes", 17:"17 Minutes", 18:"18 Minutes", 19:"19 Minutes", 20:"20 Minutes", 21:"21 Minutes", 22:"22 Minutes", 23:"23 Minutes", 24:"24 Minutes", 25:"25 Minutes", 26:"26 Minutes", 27:"27 Minutes", 28:"28 Minutes", 29:"29 Minutes", 30:"30 Minutes", 31:"31 Minutes", 32:"32 Minutes", 33:"33 Minutes", 34:"34 Minutes", 35:"35 Minutes", 36:"36 Minutes", 37:"37 Minutes", 38:"38 Minutes", 39:"39 Minutes", 40:"40 Minutes", 41:"41 Minutes", 42:"42 Minutes", 43:"43 Minutes", 44:"44 Minutes", 45:"45 Minutes", 46:"46 Minutes", 47:"47 Minutes", 48:"48 Minutes", 49:"49 Minutes", 50:"50 Minutes", 51:"51 Minutes", 52:"52 Minutes", 53:"53 Minutes", 54:"54 Minutes", 55:"55 Minutes", 56:"56 Minutes", 57:"57 Minutes", 58:"58 Minutes", 59:"59 Minutes" ], required: true) input("energy_title", "text", title: "Daily Energy Details Title", description: "Title for energy details tile. Empty value removes title from tile.", required: false) input("overview_title", "text", title: "Production Overview Title", description: "Title for solar produtcion overview tile. Empty value removes title from tile.", required: false) input("power_flow_title", "text", title: "Power Flow Title", description: "Title for current flow overview(PV, load, and grid). Empty value removes title from tile.", required: false) input(name: "display_last_updated", type: "bool", title: "Display last updated timestamp?", defaultValue: false) input(name: "debug", type: "bool", title: "Enable debug logging", defaultValue: false) } } } def installed() { log.info "Solaredge Monitoring has been installed." clearData() state.version = version() } def uninstalled() { if (debug) log.info "Solaredge Monitoring has been uninstalled." unschedule(refresh) unschedule(queryEnergyEndpoint) unschedule(queryOverviewEndpoint) unschedule(querySitePowerFlowEndpoint) } def refresh() { if (debug) log.debug "Solaredge API is being queried." queryEnergyEndpoint() queryOverviewEndpoint() querySitePowerFlowEndpoint() updateTiles() } def updated() { if (debug) { log.debug "Settings updated." } unschedule(refresh) unschedule(queryEnergyEndpoint) unschedule(queryOverviewEndpoint) unschedule(querySitePowerFlowEndpoint) if( settings.energy_refresh_interval != "255") schedule("0 */${settings.energy_refresh_interval} * ? * * *", queryEnergyEndpoint) if( settings.overview_refresh_interval != "255") schedule("0 */${settings.overview_refresh_interval} * ? * * *", queryOverviewEndpoint) if( settings.power_flow_refresh_interval != "255") schedule("0 */${settings.power_flow_refresh_interval} * ? * * *", querySitePowerFlowEndpoint) updateTiles() state.version = version() } def getEnergy() { queryEnergyEndpoint() updateTiles() } def getOverview() { queryOverviewEndpoint() updateTiles() } def getSitePowerFlow() { querySitePowerFlowEndpoint() updateTiles() } def clearData() { if (debug) log.debug "Deleting all metrics." delayBetween([ sendEvent(name: "feed_in", value: 0), sendEvent(name: "self_consumption", value: 0), sendEvent(name: "purchased", value: 0), sendEvent(name: "consumption", value: 0), sendEvent(name: "production", value: 0), sendEvent(name: "power", value: 0), sendEvent(name: "last_day", value: 0), sendEvent(name: "last_month", value: 0), sendEvent(name: "last_year", value: 0), sendEvent(name: "grid_power", value: "0 kW"), sendEvent(name: "load_power", value: "0 kW"), sendEvent(name: "pv_power", value: "0 kW"), sendEvent(name: "energy_tile", value: ""), sendEvent(name: "overview_tile", value: ""), sendEvent(name: "power_flow_tile", value: ""), sendEvent(name: "lifetime", value: 0)]) updateTiles() } def queryEnergyEndpoint() { if (debug) log.debug "Gathering energy metrics." def today = timeToday("00:00").format("YYYY-MM-dd") def params = [ uri: "${settings.url}", path: "/site/${settings.site_id}/energyDetails", query: [timeUnit: "DAY", startTime: "${today} 00:00:00", endTime: "${today} 23:59:59", api_key: "${settings.api_key}"] ] try { httpGet(params) { r -> def meters = r.data.energyDetails.meters if (debug) { log.debug "Status: ${r.getStatus()}" log.debug "Headers: ${r.getAllHeaders()}" log.debug "Content Type: ${r.getContentType()}" log.debug "Response: ${r.data}" log.debug "meters: ${meters}" } meters.each { meter -> switch(meter.type) { case "FeedIn": if (meter.values[0].value == null) meter.values[0].value = 0.0 sendEvent(name: "feed_in", value: meter.values[0].value) if (debug) log.debug "feed_in: ${meter.values[0].value}" break case "SelfConsumption": if (meter.values[0].value == null) meter.values[0].value = 0.0 sendEvent(name: "self_consumption", value: meter.values[0].value) if (debug) log.debug "self_consumption: ${meter.values[0].value}" break case "Purchased": if (meter.values[0].value == null) meter.values[0].value = 0.0 sendEvent(name: "purchased", value: meter.values[0].value) if (debug) log.debug "purchased: ${meter.values[0].value}" break case "Consumption": if (meter.values[0].value == null) meter.values[0].value = 0.0 sendEvent(name: "consumption", value: meter.values[0].value) if (debug) log.debug "consumption: ${meter.values[0].value}" break case "Production": if (meter.values[0].value == null) meter.values[0].value = 0.0 sendEvent(name: "production", value: meter.values[0].value) if (debug) log.debug "production: ${meter.values[0].value}" break } } } } catch (Exception e) { log.error "Exception" if(e.getStatusCode()) { switch(e.getStatusCode()) { case 401: log.error "401 - Not authorized" break case 403: log.error "403 - Forbidden" break case 429: log.error "429 - Too many requests" break default: log.error "Unkown Error" log.error "httpGet status code: e.getStatusCode ${e.getStatusCode()}" log.error "httpGet message: e.message : ${e.message}" log.error "httpGet full stack: e : ${e}" break } } } updateTiles() } def queryOverviewEndpoint() { if (debug) log.debug "Gathering overview metrics." def params = [ uri: "${settings.url}", path: "/site/${settings.site_id}/overview", query: [api_key: "${settings.api_key}"] ] try { httpGet(params) { r -> if (debug) { log.debug "Status: ${r.getStatus()}" log.debug "Headers: ${r.getAllHeaders()}" log.debug "Content Type: ${r.getContentType()}" log.debug "Response: ${r.data}" } delayBetween([ sendEvent(name: "power", value: r.data.overview.currentPower.power), sendEvent(name: "last_day", value: r.data.overview.lastDayData.energy), sendEvent(name: "last_month", value: r.data.overview.lastMonthData.energy), sendEvent(name: "last_year", value: r.data.overview.lastYearData.energy), sendEvent(name: "lifetime", value: r.data.overview.lifeTimeData.energy) ]) } } catch (Exception e) { log.error "Exception" if(e.getStatusCode()) { switch(e.getStatusCode()) { case 401: log.error "401 - Not authorized" break case 403: log.error "403 - Forbidden" break case 429: log.error "429 - Too many requests" break default: log.error "Unkown Error" log.error "httpGet status code: e.getStatusCode ${e.getStatusCode()}" log.error "httpGet message: e.message : ${e.message}" log.error "httpGet full stack: e : ${e}" break } } } updateTiles() } def querySitePowerFlowEndpoint() { if (debug) log.debug "Gathering site power flow data." def params = [ uri: "${settings.url}", path: "/site/${settings.site_id}/currentPowerFlow", query: [api_key: "${settings.api_key}"] ] try { httpGet(params) { r -> if (debug) { log.debug "Status: ${r.getStatus()}" log.debug "Headers: ${r.getAllHeaders()}" log.debug "Content Type: ${r.getContentType()}" log.debug "Response: ${r.data}" } delayBetween([ sendEvent(name: "grid_power", value: r.data.siteCurrentPowerFlow.GRID.currentPower + " " + r.data.siteCurrentPowerFlow.unit), sendEvent(name: "load_power", value: r.data.siteCurrentPowerFlow.LOAD.currentPower + " " + r.data.siteCurrentPowerFlow.unit), sendEvent(name: "pv_power", value: r.data.siteCurrentPowerFlow.PV.currentPower + " " + r.data.siteCurrentPowerFlow.unit) ]) def storagePower = 0; if(r.data.siteCurrentPowerFlow.STORAGE) { delayBetween([ sendEvent(name: "storage_power", value: r.data.siteCurrentPowerFlow.STORAGE.currentPower + " " + r.data.siteCurrentPowerFlow.unit), sendEvent(name: "storage_charge", value: r.data.siteCurrentPowerFlow.STORAGE.chargeLevel + "%"), sendEvent(name: "battery", value: r.data.siteCurrentPowerFlow.STORAGE.chargeLevel + "%"), sendEvent(name: "storage_status", value: r.data.siteCurrentPowerFlow.STORAGE.status) ]); storagePower = r.data.siteCurrentPowerFlow.STORAGE.currentPower; } if (r.data.siteCurrentPowerFlow.PV.currentPower + storagePower - r.data.siteCurrentPowerFlow.LOAD.currentPower > 0) { state.flow_direction = "green" } else { state.flow_direction = "red" } } } catch (Exception e) { log.error "Exception" + e if(e.getStatusCode()) { switch(e.getStatusCode()) { case 401: log.error "401 - Not authorized" break case 403: log.error "403 - Forbidden" break case 429: log.error "429 - Too many requests" break default: log.error "Unkown Error" log.error "httpGet status code: e.getStatusCode ${e.getStatusCode()}" log.error "httpGet message: e.message : ${e.message}" log.error "httpGet full stack: e : ${e}" break } } } updateTiles() } def updateTiles() { if (debug) log.debug "Updating tile information." state.last_updated = new Date().format("YYYY-MM-dd HH:mm:ss") def production = device.currentValue("production") def production_status = "red" if (device.currentValue("production") < device.currentValue("consumption")) { production_status = "green" } def energy_tile = "
" if (settings.energy_title) energy_tile += "" energy_tile += "" energy_tile += "" energy_tile += "" energy_tile += "" energy_tile += "" if (settings.display_last_updated == true) energy_tile += "" energy_tile += "
" + settings.energy_title + "
" + "Production: " + formatEnergy(device.currentValue("production")) + "
" + "Consumption: " if (device.currentValue("production") < device.currentValue("consumption")) { energy_tile += "" } else { energy_tile += "" } energy_tile += formatEnergy(device.currentValue("consumption")) + "
" + "Self Consumed: " + formatEnergy(device.currentValue("self_consumption")) + "
" + "Import: " if (device.currentValue("feed_in") < device.currentValue("purchased")) { energy_tile += "" } else { energy_tile += "" } energy_tile += formatEnergy(device.currentValue("purchased")) + "
" + "Exported: " + formatEnergy(device.currentValue("feed_in")) + "

Last Updated: " + state.last_updated + "
" sendEvent(name: "energy_tile", value: energy_tile) def overview_tile = "
" if (settings.overview_title) overview_tile += "" overview_tile += "" overview_tile += "" overview_tile += "" overview_tile += "" if (settings.display_last_updated == true) overview_tile += "" overview_tile += "
" + settings.overview_title + "
" + "Today: " + formatEnergy(device.currentValue("last_day")) + "
" + "Monthly " + formatEnergy(device.currentValue("last_month")) + "
" + "Yearly " + formatEnergy(device.currentValue("last_year")) + "
" + "Lifetime " + formatEnergy(device.currentValue("lifetime")) + "

Last Updated: " + state.last_updated + "
" sendEvent(name: "overview_tile", value: overview_tile) def power_flow_tile = "
" if (settings.power_flow_title) power_flow_tile += "" if (device.currentValue("pv_power")) power_flow_tile += "" if (device.currentValue("grid_power")) power_flow_tile += "" if (device.currentValue("storage_power")) power_flow_tile += "" if (device.currentValue("load_power")) power_flow_tile += "" if (settings.display_last_updated == true) power_flow_tile += "" power_flow_tile += "
" + settings.power_flow_title + "
" + "Solar: " + device.currentValue("pv_power") + "
" + "Grid: " + device.currentValue("grid_power") + "
" + "Battery: " + device.currentValue("storage_power") + "
" + "Usage: " + device.currentValue("load_power") + "

Last Updated: " + state.last_updated + "
" sendEvent(name: "power_flow_tile", value: power_flow_tile) } private formatEnergy(energy) { if (energy < 1000) return energy + " Wh" if (energy < 1000000) return Math.round((double) (energy/1000) * 100) / 100 + " kWh" return Math.round((double) (energy/1000/1000) * 100) / 100 + " MWh" }