/**
* Enlighten Solar System (Local)
*
* Modified from original by Andreas Amann
*
* 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://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
static String version() {
return "1.0.2"
}
preferences {
input("confIpAddr", "string", title:"Envoy Local IP Address",
required: true, displayDuringSetup: true)
input("confTcpPort", "number", title:"TCP Port",
defaultValue:"80", required: true, displayDuringSetup: true)
input("confNumInverters", "number", title:"Number of Inverters/Panels",
required: true, displayDuringSetup: true)
input("pollingInterval", "number", title:"Polling Interval (min)",
defaultValue:"15", range: "2..59", required: true, displayDuringSetup: true)
input("confInverterSize", "number", title:"Rated max power for each inverter", description: "Use '225' for M215 and '250' for M250",
required: true, displayDuringSetup: true)
input("confPanelSize", "number", title:"Panel size (W)", description: "Rated maximum power in Watts for each panel",
required: true, displayDuringSetup: true)
input("debugEnable", "bool", title: "Enable debug logging?", defaultValue: true)
}
metadata {
definition (name: "Enlighten Envoy (local)", namespace: "E_Sch", author: "Eric, Andreas Amann") {
capability "Sensor"
capability "Power Meter"
capability "Energy Meter"
capability "Refresh"
capability "Polling"
attribute "energy_yesterday", "number"
attribute "energy_last7days", "number"
attribute "energy_life", "number"
attribute "power_details", "string"
attribute "efficiency", "number"
attribute "efficiency_yesterday", "number"
attribute "efficiency_last7days", "number"
attribute "efficiency_lifetime", "number"
attribute "installationDate", "string"
attribute "numInverters", "number"
attribute "pollingInterval", "number"
attribute "inverterSize", "number"
attribute "panelSize", "number"
}
}
void poll() {
pullData()
}
void refresh() {
pullData()
}
void updated() {
if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {
state.updatedLastRanAt = now()
trace "updated() called with settings: ${settings.inspect()}".toString()
state.remove('api')
state.remove('installationDate')
state.maxPower = settings.confNumInverters * settings.confInverterSize
state.LastDetailsRanAt = null
sendEvent(name: "numInverters", value: confNumInverters, displayed:false)
sendEvent(name: "inverterSize", value: confInverterSize, displayed:false)
sendEvent(name: "panelSize", value: confPanelSize, displayed:false)
pullData()
startPoll()
if(debugEnable) runIn(1800,logsOff)
} else {
trace "updated() ran within the last 2 seconds - skipping"
}
}
void logsOff() {
debug "debug logging disabled..."
device.updateSetting("debugEnable",[value:"false",type:"bool"])
}
void ping() {
trace "checking device health…"
pullData()
}
void startPoll() {
unschedule()
// Schedule polling based on preference setting
def sec = Math.round(Math.floor(Math.random() * 60))
def min = Math.round(Math.floor(Math.random() * settings.pollingInterval.toInteger()))
String cron = "${sec} ${min}/${settings.pollingInterval.toInteger()} * * * ?" // every N min
trace "startPoll: schedule('$cron', pullData)".toString()
schedule(cron, pullData)
}
void updateDNI() {
if (!state.dni || state.dni != device.deviceNetworkId || (state.mac && state.mac != device.deviceNetworkId)) {
device.setDeviceNetworkId(createNetworkId(settings.confIpAddr, settings.confTcpPort))
state.dni = device.deviceNetworkId
}
}
private String createNetworkId(ipaddr, port) {
if (state.mac) {
return state.mac
}
def hexIp = ipaddr.tokenize('.').collect {
String.format('%02X', it.toInteger())
}.join()
def hexPort = String.format('%04X', port.toInteger())
return "${hexIp}:${hexPort}".toString()
}
private String getHostAddress() {
return "${settings.confIpAddr}:${settings.confTcpPort}"
}
void pullData() {
updateDNI()
if (!state.installationDate) {
debug "requesting installation date from Envoy…".toString()
sendHubCommand(new hubitat.device.HubAction([
method: "GET",
path: "/production?locale=en",
headers: [HOST:getHostAddress()]
],
state.dni,
[callback: installationDateCallback])
)
} else {
state.lastRequestType = (state.api == "HTML" ? "HTML" : "JSON API")
debug "requesting latest data from Envoy via ${state.lastRequestType}…".toString()
updateDNI()
sendHubCommand(new hubitat.device.HubAction([
method: "GET",
path: state.lastRequestType == "HTML" ? "/production?locale=en" : "/api/v1/production",
headers: [HOST:getHostAddress()]
],
state.dni,
[callback: dataCallback])
)
}
}
private static Integer retrieveProductionValue(String body, String heading) {
Integer val = 0
def patternString = "(?ms).*?${heading}.*?
\\s*([\\d\\.]+)\\s*([kM]?W)h?<.*"
if (body ==~ /${patternString}/) {
body.replaceFirst(/${patternString}/) {all, num, unit ->
val = Double.parseDouble(num)
if (unit == "kW") {
val *= 1000
}
else if (unit == "MW") {
val *= 1000000
}
return true
}
return val
}
return null
}
private static Map parseHTMLProductionData(String body) {
def data = [:]
data.wattHoursToday = retrieveProductionValue(body, "Today")
data.wattHoursSevenDays = retrieveProductionValue(body, "Past Week")
data.wattHoursLifetime = retrieveProductionValue(body, "Since Installation")
data.wattsNow = retrieveProductionValue(body, "Currently")
return data
}
void installationDateCallback(hubitat.device.HubResponse msg) {
if (!state.mac || state.mac != msg.mac) {
state.mac = msg.mac
}
if (!state.installationDate && !msg.json && msg.body) {
debug "trying to determine system installation date…"
def patternString = "(?ms).*?System has been live since.*?>(.*?)<.*"
if (msg.body ==~ /${patternString}/) {
msg.body.replaceFirst(/${patternString}/) {all, dateString ->
try {
state.installationDate = new Date().parse("E MMM dd, yyyy H:m a z", dateString).getTime()
debug "system has been live since ${dateString}".toString()
sendEvent(name: 'installationDate', value: "System live since " + new Date(state.installationDate).format("MMM dd, yyyy"), displayed: false)
}
catch (Exception ex) {
debug "unable to parse installation date '${dateString}' ('${ex}')".toString()
state.installationDate = -1
}
}
}
else {
debug "unable to find installation date on page"
state.installationDate = -1
}
}
pullData()
}
void dataCallback(hubitat.device.HubResponse msg) {
if (!state.mac || state.mac != msg.mac) {
state.mac = msg.mac
}
if (!state.api && state.lastRequestType != "HTML" && (msg.status != 200 || !msg.json)) {
debug "JSON API not available, falling back to HTML interface (Envoy responded with status code ${msg.status})".toString()
state.api = "HTML"
return
}
else if (!msg.body) {
log.error "${device.displayName} - no HTTP body found in '${message}'".toString()
return
}
def data = state.api == "HTML" ? parseHTMLProductionData(msg.body) : msg.json
if (state.lastData && (data.wattHoursToday == state.lastData.wattHoursToday) && (data.wattsNow == state.lastData.wattsNow)) {
debug "no new data"
//sendEvent(name: 'lastUpdate', value: new Date(), displayed: false) // dummy event for health check
return
}
state.lastData = data
debug "new data: ${data}".toString()
def energyToday = (data.wattHoursToday/1000).toFloat()
def energyLast7Days = (data.wattHoursSevenDays/1000).toFloat()
def energyLife = (data.wattHoursLifetime/1000000).toFloat()
def currentPower = data.wattsNow
def todayDay = new Date().format("dd",location.timeZone)
def powerTable = state?.powerTable
def energyTable = state?.energyTable
Boolean dayChg = false
if (!state.today || state.today != todayDay) {
dayChg = true
state.peakpower = currentPower
state.today = todayDay
state.powerTableYesterday = powerTable
state.energyTableYesterday = energyTable
powerTable = powerTable ? [] : null
energyTable = energyTable ? [] : null
state.lastPower = 0
state.lastPower1 = 0
sendEvent(name: 'energy_yesterday', value: device.currentState("energy")?.value, displayed: false)
sendEvent(name: 'efficiency_yesterday', value: device.currentState("efficiency")?.value, displayed: false)
}
def efficiencyToday = (1000*energyToday/(settings.confNumInverters * settings.confPanelSize)).toFloat()
if( dayChg || currentPower == 0 || (!state.LastDetailsRanAt || now() >= state.LastDetailsRanAt + (2*60*60*1000)) ) {
state.LastDetailsRanAt = now()
def previousPower = state.lastPower != null ? state.lastPower : currentPower
def powerChange = currentPower - previousPower
state.lastPower = currentPower
if (state.peakpower <= currentPower) {
state.peakpower = currentPower
state.peakpercentage = (100*state.peakpower/state.maxPower).toFloat()
}
sendEvent(name: 'power_details', value: ("(" + String.format("%+,d", powerChange) + "W) — Today's Peak: " + String.format("%,d", state.peakpower) + "W (" + String.format("%.1f", state.peakpercentage) + "%)"), displayed: false)
sendEvent(name: 'energy_last7days', value: energyLast7Days, displayed: false)
sendEvent(name: 'energy_life', value: energyLife, displayed: false)
def efficiencyLifetime = "NA"
if (state.installationDate && state.installationDate > 0) {
def systemAgeInDays = (new Date().getTime() - state.installationDate)/(1000*60*60*24)
efficiencyLifetime = (1000000/systemAgeInDays*energyLife/(settings.confNumInverters * settings.confPanelSize)).toFloat()
}
sendEvent(name: 'efficiency_lifetime', value: efficiencyLifetime, displayed: false)
sendEvent(name: 'efficiency', value: efficiencyToday, displayed: false)
def efficiencyLast7Days = (1000/7*energyLast7Days/(settings.confNumInverters * settings.confPanelSize)).toFloat()
sendEvent(name: 'efficiency_last7days', value: efficiencyLast7Days, displayed: false)
}
def previousPower = state.lastPower1 != null ? state.lastPower1 : currentPower
def powerChange = currentPower - previousPower
state.lastPower1 = currentPower
sendEvent(name: 'energy', value: energyToday, unit: "kWh", descriptionText: "Energy is " + String.format("%,#.3f", energyToday) + "kWh\n(Efficiency: " + String.format("%#.3f", efficiencyToday) + "kWh/kW)")
sendEvent(name: 'power', value: currentPower, unit: "W", descriptionText: "Power is " + String.format("%,d", currentPower) + "W (" + String.format("%#.1f", 100*currentPower/state.maxPower) + "%)\n(" + String.format("%+,d", powerChange) + "W since last reading)")
// get power data for yesterday and today so we can create a graph
if (state.powerTableYesterday == null || state.energyTableYesterday == null || powerTable == null || energyTable == null) {
Date startOfToday = timeToday("00:00", location.timeZone)
def newValues
if (state.powerTableYesterday == null || state.energyTableYesterday == null) {
//trace "Querying DB for yesterday's data…"
def dataTable = []
def powerData = [:] //device.statesBetween("power", startOfToday - 1, startOfToday, [max: 288]) // 24h in 5min intervals should be more than sufficient…
// work around a bug where the platform would return less than the requested number of events (as of June 2016, only 50 events are returned)
if (powerData.size()) {
/*
while ((newValues = [:] //device.statesBetween("power", startOfToday - 1, powerData.last().date, [max: 288])).size()) {
powerData += newValues
}
powerData.reverse().each() {
dataTable.add([it.date.format("H", location.timeZone),it.date.format("m", location.timeZone),it.integerValue])
}
*/
}
state.powerTableYesterday = dataTable
dataTable = []
def energyData = [:] //device.statesBetween("energy", startOfToday - 1, startOfToday, [max: 288])
if (energyData.size()) {
/*
while ((newValues = [:] //device.statesBetween("energy", startOfToday - 1, energyData.last().date, [max: 288])).size()) {
energyData += newValues
}
// we drop the first point after midnight (0 energy) in order to have the graph scale correctly
energyData.reverse().drop(1).each() {
dataTable.add([it.date.format("H", location.timeZone),it.date.format("m", location.timeZone),it.floatValue])
}
*/
}
state.energyTableYesterday = dataTable
}
if (powerTable == null || energyTable == null) {
//trace "Querying DB for today's data…"
powerTable = []
def powerData = [:] //device.statesSince("power", startOfToday, [max: 288])
if (powerData.size()) {
/*
while ((newValues = [:] //device.statesBetween("power", startOfToday, powerData.last().date, [max: 288])).size()) {
powerData += newValues
}
powerData.reverse().each() {
powerTable.add([it.date.format("H", location.timeZone),it.date.format("m", location.timeZone),it.integerValue])
}
*/
}
energyTable = []
def energyData = [:] //device.statesSince("energy", startOfToday, [max: 288])
if (energyData.size()) {
/*
while ((newValues = [:] //device.statesBetween("energy", startOfToday, energyData.last().date, [max: 288])).size()) {
energyData += newValues
}
energyData.reverse().drop(1).each() {
energyTable.add([it.date.format("H", location.timeZone),it.date.format("m", location.timeZone),it.floatValue])
}
*/
}
}
}
// add latest power & energy readings for the graph
if (currentPower > 0 || powerTable.size() != 0) {
def newDate = new Date()
powerTable.add([newDate.format("H", location.timeZone),newDate.format("m", location.timeZone),currentPower])
energyTable.add([newDate.format("H", location.timeZone),newDate.format("m", location.timeZone),energyToday])
}
state.powerTable = powerTable
state.energyTable = energyTable
}
void debug(String msg) {
if(debugEnable) log.debug device.displayName+' - '+msg
}
void trace(String msg) {
if(debugEnable) log.trace device.displayName+' - '+msg
}
|