/*
* TeslaDriver
*
* Description:
* This Hubitat driver is designed for use with a virtual device to connect to Tesla devices.
* Right now the driver is geared towards obtaining data from a Tesla PowerWall.
*
* Preferences:
* DeviceIP = REQUIRED. This identifies the IP of the device to connect to
* Email = REQUIRED
* Password = REQUIRED
* RefreshRate = REQUIRED - DEFAULT = 5 minutes. The rate at which the device will be polled
* ReauthorizationRate = REQUIRED - DEFAULT = 1 hour. The rate at which a reauthorization (login) attempt will be performed
* Enable Child Devices = OPTIONAL - DEFAULT = false. Whether child devices will be generated by the driver, requires TeslaChild.groovy also be loaded
* LogType = OPTIONAL - DEFAULT = Info. Only basic information will be stored to the log
* ShowAllPreferences = OPTIONAL - DEFAULT = true. Whether the other preferences are to be displayed or not
*
* Features List:
* Processes data from local connection method (at this time: grid status, sitemaster, registration, powerwalls, & system status)
* Ability to check drdsnell.com for updates to the driver
*
* Licensing:
* Copyright 2025 David Snell
* 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.
*
* Version Control:
* 1.2.19 - Another battery value correction, addition of MaxCurrent attribute, handling of other new data.
* 1.2.18 - Correction to battery value formula, removal of no-longer used data methods, added "SolarCurtailFrequencyDroopConfigured" attribute
* 1.2.17 - Correction of accidental DataValue usage, fixed a logging mistake, & restored Aggregate data checking.
* 1.2.16 - General updates to Event and State handling functions, added fault data, and added SystemStatus & BlocksWithFaults attributes for faults
* 1.2.15 - Correction to routines sent to the wrong parse and handling for new data points
* 1.2.14 - Rework of data handling to simplify the code, default login reauthorization now 1 hour (from 24 hours), change to handling failed connections
* 1.2.13 - Update to ProcessEvent to match other drivers
* 1.2.12 - Correction to remove Device Status events, comment out customer registration info, change to support daily checks for powerwalls and site_info
* 1.2.11 - Correction to ProcessEvent function and removal of old driver-specific attributes when Preferences are saved
* 1.2.10 - Added actuator capability so commands can be run with Rule Machine and other apps plus change for future event posts
* 1.2.9 - Made it possible to "force" events to be published, some key events now forced. Added some pauses between API queries.
* 1.2.8 - Providing "Grid Connection Possible" as an event rather than just a state
* 1.2.7 - Additional data handling, disabled phase_detection and grid_codes queries due to API as they no longer appear to work
* 1.2.6 - Change to driver version checking method and addition of a reauthorization schedule
* 1.2.5 - Updated some areas of rounding when the number was already large and decimal places have little value
* 1.2.4 - Expanded the range of acceptable refresh rates as well as changes to the driver update check
* 1.2.3 - Added in some processing of SystemStatus attributes and Nominal Full Pack related attributes
* 1.2.2 - Added commands to GetSystemStatus and GetAggregates and pruned more of the data dumps
* 1.2.1 - Pruning some of the data from the DataDump command due to likely lack of relevance
* 1.2.0 - Allows getting a vast amount more information, on DataDump command, to send to debug log
* 1.1.8 - Corrected error when parsing aggregates to only be site data
* 1.1.7 - Update to more directly include @Darwin's code for login and authcodes
* 1.1.6 - Another edit to login method.
* 1.1.5 - Yet another tweak to try to get login method working again
* 1.1.4 - Correction to login method and data retrieval method to account for the two cookies Tesla now requires,
* cleanup to the resulting queries, & changes to driver version checking to make it easier to read
* 1.1.3 - Minor formatting changes to login attempt
* 1.1.2 - Corrected username to be "customer" and changed existing Username field to be for Email, did some cleanup on
* scheduling as well as when Login is called
* 1.1.1 - Set the Login to ignore the SSL Cert
* 1.1.0 - Username/password support attempt
* 1.0.1 - Replaced all instances of boolean attributes with strings because boolean attributes are not valid
* + changes to logging and update methods
* 1.0.0 - "Release" version
* 0.3.3 - Bug fix and nomenclature for RefreshRate
* 0.3.2 - Aggregate data should be fixed
* 0.3.1 - Additional logging for Aggregates due to odd behavior
* 0.3.0 - Rework of aggregate and general status messaging
* 0.2.5 - Work on the Device data parsing
* 0.2.4 - Fixed a check on a possibly null variable and removed attempts for data that were not good anymore
* 0.2.3 - Cleaned up a bunch of variable names, pulled in more data, and other fixes
* 0.2.2 - Changed way serial numbers are handled due to Tesla's odd use of them
* 0.2.1 - Handling powerwall data now, json was "broken" by quotes Tesla included in the message
* 0.2.0 - Changed name and started parsing additional API calls
* 0.1.7 - Added more variables and attempts to pull in more API calls just for raw data at this point
* 0.1.6 - Reworked parsing of returned data so it can be processed, since it was not actually json formatted
* 0.1.5 - Response coming back for local was not json... specifically requesting content type now
* 0.1.4 - Enabled some more trace logging for development purposes
* 0.1.3 - Enhanced error checking & ignores SSL errors from Tesla's self-signed certificates
* 0.1.2 - Correction to Preferences and handling of data method
* 0.1.1 - Changes to attempt local support
* 0.1.0 - Initial version
*
* Thank you(s):
* @Cobra for inspiration of how I perform driver version checking.
* @jared.zimmerman, @scottgu3, & @cfoos1 for all their samples and testing of the driver
* @darwin for collaboration on the two cookie problem required for newer login/data retrieval methods
* Based on information from https://github.com/vloschiavo/powerwall2
*/
// Returns the driver name
def DriverName(){
return "TeslaDriver"
}
// Returns the driver version
def DriverVersion(){
return "1.2.19"
}
metadata{
definition ( name: "TeslaDriver", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/TeslaDriver.groovy" ) {
// Capabilities the device should be capable of
capability "Sensor"
capability "Refresh"
capability "Battery"
capability "Actuator"
// Commands in the driver
//command "DoSomething" // Test command that should be disabled before publishing
command "Login" // Logs in to get the required authentication token
command "DataDump" // Runs a whole bunch of data requests and dumps all the information to the debug log
command "PartialDataDump" // Runs some data requests and dumps all the information to the debug log
command "GetSystemStatus" // Gets the system status
command "GetAggregates" // Dumps the aggregates to debug log
// Attributes of the driver itself
attribute "DriverName", "string" // Driver identifies the driver being used for update purposes
attribute "DriverVersion", "string" // Version number of the driver
attribute "DriverStatus", "string" // Status of the driver version compared to what is currently published
// Attributes for device data
attribute "SystemStatus", "string"
attribute "BlocksWithFaults", "string"
attribute "Grid Status", "string"
attribute "Device Type", "string"
attribute "Tesla Device Version", "string"
attribute "System Energy (kWh)", "number"
attribute "System Power (kW)", "number"
attribute "SystemUp", "string"
attribute "System Running", "string"
attribute "Connected To Tesla", "string"
attribute "Receiving Power From Grid", "string"
attribute "Privacy Notice Signed", "string"
attribute "Under Warranty", "string"
attribute "Grid Connection Possible", "string"
attribute "Receive Marketing", "string"
attribute "System Registered", "string"
attribute "Registration Period Over", "string"
attribute "Instant Power", "number"
attribute "Instant Reactive Power", "number"
attribute "Instant Apparent Power", "number"
attribute "Instant Total Current", "number"
attribute "Instant Average Current", "number"
attribute "Energy Exported", "number"
attribute "Energy Imported", "number"
attribute "Frequency (Hz)", "number"
attribute "Site Instant Power", "number"
attribute "Site Instant Reactive Power", "number"
attribute "Site Instant Apparent Power", "number"
attribute "Site Instant Total Current", "number"
attribute "Site Energy Exported", "number"
attribute "Site Energy Imported", "number"
attribute "Site Frequency (Hz)", "number"
attribute "Site Voltage", "number"
attribute "Solar Instant Power", "number"
attribute "Solar Instant Reactive Power", "number"
attribute "Solar Instant Apparent Power", "number"
attribute "Solar Instant Total Current", "number"
attribute "Solar Energy Exported", "number"
attribute "Solar Energy Imported", "number"
attribute "Solar Frequency (Hz)", "number"
attribute "Solar Voltage", "number"
attribute "Battery Instant Power", "number"
attribute "Battery Instant Reactive Power", "number"
attribute "Battery Instant Apparent Power", "number"
attribute "Battery Instant Total Current", "number"
attribute "Battery Energy Exported", "number"
attribute "Battery Energy Imported", "number"
attribute "Battery Frequency (Hz)", "number"
attribute "Battery Voltage", "number"
attribute "Load Instant Power", "number"
attribute "Load Instant Reactive Power", "number"
attribute "Load Instant Apparent Power", "number"
attribute "Load Instant Total Current", "number"
attribute "Load Energy Exported", "number"
attribute "Load Energy Imported", "number"
attribute "Load Frequency (Hz)", "number"
attribute "Load Voltage", "number"
attribute "Grid Being Used", "string"
attribute "Powerwall(s) Charging", "string"
attribute "Solar Generating Power", "string"
attribute "Status", "string"
attribute "Last Login", "string"
attribute "Number of Batteries", "number"
attribute "Nominal Full Pack Energy", "number"
attribute "Nominal Full Pack Percentage", "number"
attribute "SiteInstantAverageCurrent", "number"
attribute "SolarInstantAverageCurrent", "number"
attribute "BatteryInstantAverageCurrent", "number"
attribute "LoadInstantAverageCurrent", "number"
attribute "SolarCurtailFrequencyDroopConfigured", "number"
attribute "MaxCurrent", "number"
}
preferences{
section{
if( ShowAllPreferences || ShowAllPreferences == null ){ // Show the preferences options
input( type: "string", name: "DeviceIP", title: "Powerwall's IP/Hostname", required: true ) // String to retain the device's IP or hostname
input( type: "string", name: "Email", title: "Email", required: true ) // String to retain the customer's email
input( type: "password", name: "Password", title: "Password", required: true ) // String to retain the customer's login password
input( type: "enum", name: "RefreshRate", title: "Refresh Rate", required: false, multiple: false, options: [ "5 seconds", "10 seconds", "15 seconds", "30 seconds", "1 minute", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "3 hours", "Manual" ], defaultValue: "5 minutes" ) // Enum to allow selecting the refresh rate that the device will be checked
input( type: "enum", name: "ReauthorizationRate", title: "Reauthorization Rate", required: false, multiple: false, options: [ "1 hour", "3 hours", "6 hours", "12 hours", "24 hours" ], defaultValue: "1 hour" ) // Enum to allow selecting the reauthorization rate
input( type: "bool", name: "ChildrenEnabled", title: "Enable Child Devices?", description: "Once enabled, child devices will be made for added sensors.", required: false, defaultValue: false ) // Bool about whether child devices will be enabled
input( type: "enum", name: "LogType", title: "Enable Logging?", required: true, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) // Enum to set the level of logging that will be used
input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) // Bool to set whether the other preferences should be displayed or not
} else {
input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) // Preferences should be hidden so only show the preference to show them or not
}
}
}
}
// DoSomething is just a test/correction command that should be disabled before publishing
def DoSomething(){
}
// updated is called whenever device parameters are saved and sets driver data, basic settings, and schedule
def updated(){
// Set default logging if preference is null
if( LogType == null ){
LogType = "Info"
}
// Check if children are disabled and if any exist
if( !ChildrenEnabled && getChildDevices() != null ){
// Delete all children
Logging( "Child devices disabled, but had been enabled, deleting child devices", 4 )
getChildDevices().each{
deleteChildDevice( it.deviceNetworkId )
}
}
// Set RefreshRate at default if null
if( RefreshRate == null ){
RefreshRate = "5 minutes"
}
// Set the schedule for driver version check and refreshing for data
def Hour = ( new Date().format( "h" ) as int )
def Minute = ( new Date().format( "m" ) as int )
def Second = ( new Date().format( "s" ) as int )
Second = ( (Second + 5) % 60 )
Login()
// Check what the refresh rate is set for then run it
switch( RefreshRate ){
case "5 seconds": // Schedule the refresh check for every 5 seconds
schedule( "0/5 * * ? * *", "refresh" )
break
case "10 seconds": // Schedule the refresh check for every 10 seconds
schedule( "0/10 * * ? * *", "refresh" )
break
case "15 seconds": // Schedule the refresh check for every 15 seconds
schedule( "0/15 * * ? * *", "refresh" )
break
case "30 seconds": // Schedule the refresh check for every 30 seconds
schedule( "0/30 * * ? * *", "refresh" )
break
case "1 minute": // Schedule the refresh check for every minute
schedule( "${ Second } * * ? * *", "refresh" )
break
case "5 minutes": // Schedule the refresh check for every 5 minutes
schedule( "${ Second } 0/5 * ? * *", "refresh" )
break
case "10 minutes": // Schedule the refresh check for every 10 minutes
schedule( "${ Second } 0/10 * ? * *", "refresh" )
break
case "15 minutes": // Schedule the refresh check for every 15 minutes
schedule( "${ Second } 0/15 * ? * *", "refresh" )
break
case "30 minutes": // Schedule the refresh check for every 30 minutes
schedule( "${ Second } 0/30 * ? * *", "refresh" )
break
case "1 hour": // Schedule the refresh check for every hour
schedule( "${ Second } ${ Minute } * ? * *", "refresh" )
break
case "3 hours": // Schedule the refresh check for every 3 hours
schedule( "${ Second } ${ Minute } 0/3 ? * *", "refresh" )
break
default:
unschedule( "refresh" )
RefreshRate = "Manual"
break
}
Logging( "Refresh rate: ${ RefreshRate }", 4 )
// Set the ReautorizationRate to default if null
if( ReauthorizationRate == null ){
ReauthorizationRate = "1 hour"
}
// Check what the reauthorization rate is set for then run it
switch( ReauthorizationRate ){
case "1 hour": // Schedule the Login to reauthorize every 3 hours
schedule( "${ Second } ${ Minute } * ? * *", "Login" )
break
case "3 hours": // Schedule the Login to reauthorize every 3 hours
schedule( "${ Second } ${ Minute } 0/3 ? * *", "Login" )
break
case "6 hours": // Schedule the Login to reauthorize every 6 hours
schedule( "${ Second } ${ Minute } 0/6 ? * *", "Login" )
break
case "12 hours": // Schedule the Login to reauthorize every 12 hours
schedule( "${ Second } ${ Minute } 0/12 ? * *", "Login" )
break
case "24 hours": // Schedule the Login to reauthorize every 24 hours
schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "Login" )
break
default:
unschedule( "refresh" )
RefreshRate = "Manual"
break
}
// Set the driver name and version before update checking is scheduled
if( state."Driver Status" != null ){
state.remove( "Driver Name" )
state.remove( "Driver Version" )
state.remove( "Driver Status" )
device.deleteCurrentState( "Driver Status" )
device.deleteCurrentState( "Driver Name" )
device.deleteCurrentState( "Driver Version" )
}
ProcessEvent( "DriverName", "${ DriverName() }" )
ProcessEvent( "DriverStatus", null )
ProcessEvent( "DriverVersion", "${ DriverVersion() }" )
// Set up a schedule for daily checks
schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "DailyCheck" )
unschedule( "CheckForUpdate" )
Logging( "Updated", 2 )
}
// Basic check to make sure connection information is valid
String LocalOK(){
if( ( DeviceIP != null ) && ( Email != null ) && ( Password != null ) ){
if( ( state.AuthCookie != null ) && ( state.UserRecord != null ) ){
return "OK"
} else {
Logging( "AuthCookie and/or UserRecord missing, Login is required", 5 )
return "Login Required"
}
} else {
Logging( "DeviceIP, Email, and/or Password are missing, these are required for driver to connect.", 5 )
return "Missing Information"
}
}
// refresh runs the device polling
def refresh(){
switch( LocalOK() ){
case "Login Required":
Login()
pauseExecution( 2000 )
case "OK":
ConnectLocal()
break
default:
break
}
}
// Attempt to perform a login to the Powerwall
def Login(){
def Params
if( ( DeviceIP != null ) && ( Email != null ) && ( Password != null ) ){
Params = [uri: "https://${DeviceIP}/api/login/Basic", contentType: "application/json", ignoreSSLIssues: true, query: [username: "customer", password : "${Password}"]]
Logging( "Login Params: ${ Params }", 4 )
try{
httpPost( Params ){ resp ->
Logging( "Login response = ${ resp }", 4 )
switch( resp.getStatus() ){
case 200:
Logging( "Login Success = ${ resp.data }", 4 )
ProcessEvent( "Status", "Login successful." )
ProcessEvent( "Last Login", new Date() )
TempAuthCookie = null
TempUserRecord = null
resp.getHeaders().each{
if (it.name == "Set-Cookie"){
String str = it.value
if( str.substring( 0,10 ) == "UserRecord" ){
ProcessState( "UserRecord", str.substring( str.indexOf( "=" ) + 1, str.indexOf( ";" ) ) )
} else if (str.substring(0,10) == "AuthCookie") {
ProcessState( "AuthCookie", str.substring( str.indexOf( "=" ) + 1, str.indexOf( ";" ) ) )
}
}
}
break
case 408:
Logging( "Request Timeout", 3 )
break
default:
Logging( "Error logging in: ${ resp.status }", 4 )
break
}
}
} catch( Exception e ){
Logging( "Exception when performing Login: ${ e }", 5 )
}
} else {
Logging( "Required information such as DeviceIP, Username, and/or Password are missing.", 5 )
}
}
// DailyCheck is meant for things that only need to be checked once per day
def DailyCheck(){
CheckForUpdate()
switch( LocalOK() ){
case "Login Required":
Login()
pauseExecution( 2000 )
case "OK":
// Try to connect for general information
def AuthHeader = [ "Cookie" : "AuthCookie=${ state.AuthCookie }; UserRecord=${ state.UserRecord }" ]
def Params = []
Params = [ uri: "https://${ DeviceIP }/api/powerwalls", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // powerwall information
asynchttpGet( "ParseResponse", Params, [ Method: "Powerwalls" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/site_info", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Site info
asynchttpGet( "ParseResponse", Params, [ Method: "Site_Info" ] )
break
default:
break
}
}
// Connect to the local device for information
def ConnectLocal(){
switch( LocalOK() ){
case "Login Required":
Login()
pauseExecution( 2000 )
case "OK":
// Try to connect for status
def AuthHeader = [ "Cookie" : "AuthCookie=${ state.AuthCookie }; UserRecord=${ state.UserRecord }" ]
def Params = []
Params = [ uri: "https://${ DeviceIP }/api/meters/aggregates", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Aggregated info
asynchttpGet( "ParseResponse", Params, [ Method: "Aggregates" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/system_status/grid_status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // grid status
asynchttpGet( "ParseResponse", Params, [ Method: "Grid_Status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/sitemaster", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // limited information about whether system is running
asynchttpGet( "ParseResponse", Params, [ Method: "Sitemaster" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/system_status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System status
asynchttpGet( "ParseResponse", Params, [ Method: "System_Status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/system_status/soe", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System status
asynchttpGet( "ParseResponse", Params, [ Method: "SOE" ] )
break
default:
break
}
}
// Parse the data received before passing it for processing
def ParseResponse( resp, data ){
switch( resp.status ){
case 200:
switch( data.Method ){
case "Aggregates":
def TempData = parseJson( resp.data )
NumberDevices = TempData.size()
if( NumberDevices == 4 ){
ProcessData( TempData.site, "site" )
ProcessData( TempData.solar, "solar" )
ProcessData( TempData.battery, "battery" )
ProcessData( TempData.load, "load" )
}
break
case "Sitemaster":
case "Grid_Status":
case "System_Status":
case "SOE":
case "Site_Info":
Logging( "${ data.Method } Data = ${ resp.data }", 4 )
def TempData = parseJson( resp.data )
ProcessData( TempData, null )
break
case "Powerwalls":
Logging( "${ data.Method } Data = ${ resp.data }", 4 )
def TempData = "${ resp.data }"
// Deal with "offending" characters in data as they are discovered
if( TempData.contains( "\"STOP SYSTEM\"" ) ){
TempData = TempData.replaceAll( "\"STOP SYSTEM\"", "STOP SYSTEM" )
}
if( TempData.contains( "\"RUN WIZARD\"" ) ){
TempData = TempData.replaceAll( "\"RUN WIZARD\"", "RUN WIZARD" )
}
def Data = parseJson( TempData )
Logging( "# Powerwall(s) = ${ Data.powerwalls.size() }", 4 )
Data.powerwalls.each(){
ProcessData( it, "Powerwall ${ it.PackageSerialNumber }" )
}
break
default:
Logging( "${ data.Method } datadump: ${ resp.data }", 3 )
break
}
UpdateStatus()
Logging( "Connected to Tesla for ${ data.Method }.", 4 )
sendEvent( name: "Status", value: "Succeeded" )
break
case 408:
Logging( "Timeout connecting to Tesla for ${ data.Method }.", 5 )
sendEvent( name: "Status", value: "Failed - Timeout" )
break
case 401:
Logging( "Authorization Failure, wiping cookie and user record.", 5 )
sendEvent( name: "Status", value: "Failed - Authorization" )
ProcessState( "AuthCookie", null )
ProcessState( "UserRecord", null )
break
default:
Logging( "Error ${ resp.status } connecting to Tesla for ${ data.Method }.", 5 )
sendEvent( name: "Status", value: "Failed - ${ resp.status }" )
break
}
}
// Attempt to get the aggregates alone
def GetAggregates(){
// Try to connect locally for status information
switch( LocalOK() ){
case "Login Required":
Login()
pauseExecution( 2000 )
case "OK":
def AuthHeader = [ "Cookie" : "AuthCookie=${ state.AuthCookie }; UserRecord=${ state.UserRecord }" ]
def Params = []
Params = [ uri: "https://${ DeviceIP }/api/meters/aggregates", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Aggregates
asynchttpGet( "ParseResponse", Params, [ Method: "Aggregates" ] )
break
default:
break
}
}
// Attempt to get the system status alone
def GetSystemStatus(){
// Try to connect locally for status information
switch( LocalOK() ){
case "Login Required":
Login()
pauseExecution( 2000 )
case "OK":
def AuthHeader = [ "Cookie" : "AuthCookie=${ state.AuthCookie }; UserRecord=${ state.UserRecord }" ]
def Params = []
Params = [ uri: "https://${ DeviceIP }/api/system_status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System Status
asynchttpGet( "ParseResponse", Params, [ Method: "System_Status" ] )
break
default:
break
}
}
// Updates the overall System Status once data has all been requested and processed for Aggregates
def UpdateStatus(){
if( state.'Site Instant Power' > 0 ){
ProcessEvent( "Grid Being Used", "true" )
} else {
ProcessEvent( "Grid Being Used", "false" )
}
if( state.'Battery Instant Power' > 0 ){
ProcessEvent( "Powerwall(s) Charging", "false" )
} else {
ProcessEvent( "Powerwall(s) Charging", "true" )
}
if( state.'Solar Instant Power' > 0 ){
ProcessEvent( "Solar Generating Power", "true" )
} else {
ProcessEvent( "Solar Generating Power", "false" )
}
}
// Attempt to get some data not normally reported
def PartialDataDump(){
// Try to connect locally for status information
switch( LocalOK() ){
case "Login Required":
Login()
pauseExecution( 2000 )
case "OK":
def AuthHeader = [ "Cookie" : "AuthCookie=${ state.AuthCookie }; UserRecord=${ state.UserRecord }" ]
def Params = []
Params = [ uri: "https://${ DeviceIP }/api/meters/readings", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meter Readings
asynchttpGet( "ParseResponse", Params, [ Method: "meters/readings" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/meters/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meter Status
asynchttpGet( "ParseResponse", Params, [ Method: "meters/status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/operation", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Operation
asynchttpGet( "ParseResponse", Params, [ Method: "operation" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/powerwalls/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Powerwalls status
asynchttpGet( "ParseResponse", Params, [ Method: "powerwalls/status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/solars", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Solars
asynchttpGet( "ParseResponse", Params, [ Method: "solars" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Status
asynchttpGet( "ParseResponse", Params, [ Method: "status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/system_status/grid_status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System Grid Status
asynchttpGet( "ParseResponse", Params, [ Method: "system_status/grid_status" ] )
break
default:
break
}
}
// Attempt to get all data not normally reported
def DataDump(){
// Try to connect locally for status information
switch( LocalOK() ){
case "Login Required":
Login()
pauseExecution( 2000 )
case "OK":
def AuthHeader = [ "Cookie" : "AuthCookie=${ state.AuthCookie }; UserRecord=${ state.UserRecord }" ]
def Params = []
Params = [ uri: "https://${ DeviceIP }/api/config", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Config
asynchttpGet( "ParseResponse", Params, [ Method: "config" ] )
pauseExecution( 2000 )
//Params = [ uri: "https://${ DeviceIP }/api/customer", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Customer
//asynchttpGet( "ParseResponse", Params, [ Method: "customer" ] )
//pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/generators", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Generators
asynchttpGet( "ParseResponse", Params, [ Method: "generators" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/generators/disconnect_types", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Generator disconnect types
asynchttpGet( "ParseResponse", Params, [ Method: "generators/disconnect_types" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/installer", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Installer
asynchttpGet( "ParseResponse", Params, [ Method: "installer" ] )
pauseExecution( 2000 )
//Params = [ uri: "https://${ DeviceIP }/api/installer/companies", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Installer Companies List
//asynchttpGet( "ParseResponse", Params, [ Method: "installer/companies" ] )
//pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/meters", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meters
asynchttpGet( "ParseResponse", Params, [ Method: "meters" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/meters/readings", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meter Readings
asynchttpGet( "ParseResponse", Params, [ Method: "meters/readings" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/meters/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meter Status
asynchttpGet( "ParseResponse", Params, [ Method: "meters/status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/networks", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Networks
asynchttpGet( "ParseResponse", Params, [ Method: "networks" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/networks/client_protocols", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Networks Client Protocols
asynchttpGet( "ParseResponse", Params, [ Method: "networks/client_protocols" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/operation", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Operation
asynchttpGet( "ParseResponse", Params, [ Method: "operation" ] )
pauseExecution( 2000 )
//Params = [ uri: "https://${ DeviceIP }/api/powerwalls/phase_usages", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Powerwalls phase usage
//asynchttpGet( "ParseResponse", Params, [ Method: "powerwalls/phase_usages" ] )
//pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/powerwalls/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Powerwalls status
asynchttpGet( "ParseResponse", Params, [ Method: "powerwalls/status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/site_info/grid_regions", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Site Info Grid Regions
asynchttpGet( "ParseResponse", Params, [ Method: "site_info/grid_regions" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/site_info/site_name", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Site Info Site Name
asynchttpGet( "ParseResponse", Params, [ Method: "site_info/site_name" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/solars", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Solars
asynchttpGet( "ParseResponse", Params, [ Method: "solars" ] )
pauseExecution( 2000 )
//Params = [ uri: "https://${ DeviceIP }/api/solars/brands", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Solars Brands
//asynchttpGet( "ParseResponse", Params, [ Method: "solars/brands" ] )
//pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Status
asynchttpGet( "ParseResponse", Params, [ Method: "status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/system/update/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System Update Status
asynchttpGet( "ParseResponse", Params, [ Method: "system/update/status" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/system_status/grid_faults", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System Grid Faults
asynchttpGet( "ParseResponse", Params, [ Method: "system_status/grid_faults" ] )
pauseExecution( 2000 )
Params = [ uri: "https://${ DeviceIP }/api/system_status/grid_status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System Grid Status
asynchttpGet( "ParseResponse", Params, [ Method: "system_status/grid_status" ] )
break
default:
break
}
}
// Process received data
def ProcessData( Data, Device = null ){
if( Device != null ){
if( Device.contains( "Powerwall" ) ){
Logging( "Processing Powerwall size = ${ Data.size() }", 4 )
} else {
Logging( "Processing ${ Device } size = ${ Data.size() }", 4 )
}
} else {
Logging( "Processing other size = ${ Data.size() }", 4 )
}
Data.each{
switch( it.key ){
// Aggregates
case "last_communication_time":
case "i_a_current":
case "i_b_current":
case "i_c_current":
case "timeout":
case "a":
// Powerwall
case "PackagePartNumber":
case "grid_state":
case "grid_reconnection_time_seconds":
case "under_phase_detection":
case "updating":
case "Type":
case "bc_type":
// Site Info
case "max_site_meter_power_kW":
case "min_site_meter_power_kW":
case "max_system_power_kW":
case "max_system_energy_kWh":
case "site_name":
case "timezone":
case "grid_voltage_setting":
case "grid_freq_setting":
case "grid_phase_setting":
case "country":
case "state":
case "distributor":
case "utility":
case "retailer":
case "region":
// Device specific data
case "ip_address":
case "port":
case "short_id":
case "neurio_connected":
case "frequency":
case "v_l1n":
if( Device == "site" || Device == null ){
ProcessState( "${ it.key }", it.value )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "${ it.key }", it.value )
}
break
case "grid_code":
ProcessState( "${ it.key }", it.value )
if( it.value.grid_code_overrides != null ){
it.value.grid_code_overrides.each(){
if( it.name == "soc_freq_droop_config_df_max" ){
ProcessEvent( "SolarCurtailFrequencyDroopConfigured", it.value )
}
}
}
break
// System Status
case "max_charge_power": // Value?
case "max_discharge_power": // Value?
case "max_apparent_power": // Value?
case "instantaneous_max_discharge_power": // Value?
case "instantaneous_max_charge_power": // Value?
case "instantaneous_max_apparent_power": // Value?
case "grid_services_power": // Value?
case "ffr_power_availability_high": // Value?
case "ffr_power_availability_low": // Value?
case "nominal_energy_remaining": // Redundant with other values
ProcessState( "${ it.key }", it.value )
break
case "percentage":
def Temp = it.value as float
ProcessState( "${ it.key }", ( Math.round( Temp * 100 ) / 100 ) )
ProcessEvent( "battery", Math.round( Temp ), "%" )
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "${ it.key }", ( Math.round( Temp * 100 ) / 100 ) )
PostEventToChild( Device, "battery", Math.round( Temp ), "%" )
}
break
// Things to make more "friendly"
case "type":
if( Device == "site" || Device == null ){
ProcessState( "Site Type", it.value )
}
if( ChildrenEnabled && Device.contains( "Powerwall" ) ){
if( it.value == "acpw" ){
PostStateToChild( Device, "Device Type", "AC Powerwall" )
} else {
PostStateToChild( Device, "Device Type", it.value )
Logging( "Powerwall's Device Type was ${ it.value }", 3 )
}
} else if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Device Type", it.value )
}
break
case "version":
if( Device == "site" || Device == null ){
ProcessState( "Tesla Device Version", it.value )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Tesla Device Version", it.value )
}
break
case "nominal_system_energy_kWh":
def Temp = it.value as float
ProcessEvent( "System Energy (kWh)", Math.round( Temp ), "kWh" )
if( ChildrenEnabled && Device != null ){
PostEventToChild( Device, "System Energy (kWh)", Math.round( Temp ), "kWh" )
}
break
case "nominal_system_power_kW":
def Temp = it.value as float
ProcessEvent( "System Power (kW)", Math.round( Temp ), "kW" )
if( ChildrenEnabled && Device != null ){
PostEventToChild( Device, "System Power (kW)", Math.round( Temp ), "kW" )
}
break
case "status":
if( it.value == "StatusUp" ){
ProcessEvent( "SystemUp", "Up" )
} else {
ProcessEvent( "SystemUp", "Not Up" )
Logging( "SystemUp identified as Not Up because value was ${ it.value }", 2 )
}
if( ChildrenEnabled && Device != null ){
if( it.value == "StatusUp" ){
PostEventToChild( Device, "SystemUp", "Up" )
} else {
PostEventToChild( Device, "SystemUp", "Not Up" )
Logging( "SystemUp identified as Not Up because value was ${ it.value }", 2 )
}
}
break
case "running":
ProcessEvent( "System Running", "${ it.value }" )
if( ChildrenEnabled && Device != null ){
PostEventToChild( Device, "System Running", "${ it.value }" )
}
break
case "connected_to_tesla":
if( Device == "site" || Device == null ){
ProcessState( "Connected To Tesla", "${ it.value }" )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Connected To Tesla", "${ it.value }" )
}
break
case "grid_services_active":
ProcessEvent( "Receiving Power From Grid", "${ it.value }" )
if( ChildrenEnabled && Device != null ){
PostEventToChild( Device, "Receiving Power From Grid", "${ it.value }" )
}
break
case "privacy_notice":
if( Device == "site" || Device == null ){
ProcessState( "Privacy Notice Signed", "${ it.value }" )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Privacy Notice Signed", "${ it.value }" )
}
break
case "limited_warranty":
if( Device == "site" || Device == null ){
ProcessState( "Under Warranty", "${ it.value }" )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Under Warranty", "${ it.value }" )
}
break
case "grid_services":
if( Device == "site" || Device == null ){
ProcessEvent( "Grid Connection Possible", "${ it.value }" )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Grid Connection Possible", "${ it.value }" )
}
break
case "marketing":
if( Device == "site" || Device == null ){
ProcessState( "Receive Marketing", "${ it.value }" )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Receive Marketing", "${ it.value }" )
}
break
case "registered":
if( Device == "site" || Device == null ){
ProcessState( "System Registered", "${ it.value }" )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "System Registered", "${ it.value }" )
}
break
case "timed_out_registration":
if( Device == "site" || Device == null ){
ProcessState( "Registration Period Over", "${ it.value }" )
}
if( ChildrenEnabled && Device != null ){
PostStateToChild( Device, "Registration Period Over", "${ it.value }" )
}
break
case "instant_power":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Instant Power", Math.round( Temp ) )
break
case "solar":
ProcessEvent( "Solar Instant Power", Math.round( Temp ) )
break
case "battery":
ProcessEvent( "Battery Instant Power", Math.round( Temp ) )
break
case "load":
ProcessEvent( "Load Instant Power", Math.round( Temp ) )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "Instant Power", Math.round( Temp ) )
}
}
break
case "instant_reactive_power":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Instant Reactive Power", Math.round( Temp ) )
break
case "solar":
ProcessEvent( "Solar Instant Reactive Power", Math.round( Temp ) )
break
case "battery":
ProcessEvent( "Battery Instant Reactive Power", Math.round( Temp ) )
break
case "load":
ProcessEvent( "Load Instant Reactive Power", Math.round( Temp ) )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "Instant Reactive Power", Math.round( Temp ) )
}
}
break
case "instant_average_current":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "SiteInstantAverageCurrent", Math.round( Temp ) )
break
case "solar":
ProcessEvent( "SolarInstantAverageCurrent", Math.round( Temp ) )
break
case "battery":
ProcessEvent( "BatteryInstantAverageCurrent", Math.round( Temp ) )
break
case "load":
ProcessEvent( "LoadInstantAverageCurrent", Math.round( Temp ) )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "InstantAverageCurrent", Math.round( Temp ) )
}
}
break
case "instant_apparent_power":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Instant Apparent Power", Math.round( Temp ) )
break
case "solar":
ProcessEvent( "Solar Instant Apparent Power", Math.round( Temp ) )
break
case "battery":
ProcessEvent( "Battery Instant Apparent Power", Math.round( Temp ) )
break
case "load":
ProcessEvent( "Load Instant Apparent Power", Math.round( Temp ) )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "Instant Apparent Power", Math.round( Temp ) )
}
}
break
case "instant_total_current":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Total Current", Math.round( Temp ) )
break
case "solar":
ProcessEvent( "Solar Total Current", Math.round( Temp ) )
break
case "battery":
ProcessEvent( "Battery Total Current", Math.round( Temp ) )
break
case "load":
ProcessEvent( "Load Total Current", Math.round( Temp ) )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "Instant Total Current", Math.round( Temp ) )
}
}
break
case "energy_exported":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Energy Exported", Math.round( Temp ) )
break
case "solar":
ProcessEvent( "Solar Energy Exported", Math.round( Temp ) )
break
case "battery":
ProcessEvent( "Battery Energy Exported", Math.round( Temp ) )
break
case "load":
ProcessEvent( "Load Energy Exported", Math.round( Temp ) )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "Energy Exported", Math.round( Temp ) )
}
}
break
case "energy_imported":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Energy Imported", Math.round( Temp ) )
break
case "solar":
ProcessEvent( "Solar Energy Imported", Math.round( Temp ) )
break
case "battery":
ProcessEvent( "Battery Energy Imported", Math.round( Temp ) )
break
case "load":
ProcessEvent( "Load Energy Imported", Math.round( Temp ) )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "Energy Imported", Math.round( Temp ) )
}
}
break
case "frequency":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Frequency (Hz)", ( Math.round( Temp * 100 ) / 100 ), "Hz" )
break
case "solar":
ProcessEvent( "Solar Frequency (Hz)", ( Math.round( Temp * 100 ) / 100 ), "Hz" )
break
case "battery":
ProcessEvent( "Battery Frequency (Hz)", ( Math.round( Temp * 100 ) / 100 ), "Hz" )
break
case "load":
ProcessEvent( "Load Frequency (Hz)", ( Math.round( Temp * 100 ) / 100 ), "Hz" )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "Frequency (Hz)", ( Math.round( Temp * 100 ) / 100 ), "Hz" )
}
}
break
case "instant_average_voltage":
if( Device != null ){
def Temp = it.value as float
switch( Device ){
case "site":
ProcessEvent( "Site Voltage", ( Math.round( Temp * 100 ) / 100 ), "v" )
break
case "solar":
ProcessEvent( "Solar Voltage", ( Math.round( Temp * 100 ) / 100 ), "v" )
break
case "battery":
ProcessEvent( "Battery Voltage", ( Math.round( Temp * 100 ) / 100 ), "v" )
break
case "load":
ProcessEvent( "Load Voltage", ( Math.round( Temp * 100 ) / 100 ), "v" )
break
}
if( ChildrenEnabled ){
PostEventToChild( Device, "voltage", ( Math.round( Temp * 100 ) / 100 ), "v" )
}
}
break
case "grid_status":
switch( it.value ){
case "SystemGridConnected":
ProcessEvent( "Grid Status", "Grid up" )
if( ChildrenEnabled && Device != null ){
PostEventToChild( Device, "Grid Status", "Grid up" )
}
break
case "SystemIslandedActive":
ProcessEvent( "Grid Status", "Grid down" )
if( ChildrenEnabled && Device != null ){
PostEventToChild( Device, "Grid Status", "Grid down" )
}
break
case "SystemTransitionToGrid":
ProcessEvent( "Grid Status", "Grid up still syncing" )
if( ChildrenEnabled && Device != null ){
PostEventToChild( Device, "Grid Status", "Grid up still syncing" )
}
break
}
break
case "nominal_full_pack_energy":
ProcessState( "Nominal Full Pack Energy", it.value )
if( state.'Number of Batteries' != null ){
ProcessEvent( "Nominal Full Pack Percentage", ( ( it.value / ( 13500 * ( state.'Number of Batteries' as int ) ) ) * 100 ), "%" )
} else {
ProcessEvent( "Nominal Full Pack Percentage", ( ( it.value / ( 13500 * ( Data.available_blocks as int ) ) ) * 100 ), "%" )
}
break
case "available_blocks":
ProcessState( "BatteriesAvailable", it.value )
if( it.value != state."BatteriesControlled" ){
ProcessEvent( "SystemStatus", "Fault, batteries available does not match batteries controlled." )
} else {
if( state.SystemStatus != "Fault, batteries available does not match batteries controlled." ){
ProcessEvent( "SystemStatus", "No Fault" )
}
}
break
case "blocks_controlled":
ProcessState( "BatteriesControlled", it.value )
if( it.value != state."BatteriesAvailable" ){
ProcessEvent( "SystemStatus", "Fault, batteries available does not match batteries controlled." )
} else {
if( state.SystemStatus != "Fault, batteries available does not match batteries controlled." ){
ProcessEvent( "SystemStatus", "No Fault" )
}
}
break
case "system_discharge_power_capability_unity_pf":
ProcessState( "SystemDischargePowerCapabilityUnityPF", it.value )
break
case "system_charge_power_capability_unity_pf":
ProcessState( "SystemChargePowerCapabilityUnityPF", it.value )
break
case "system_discharge_power_capability_design_pf":
ProcessState( "SystemDischargePowerCapabilityDesignPF", it.value )
break
case "system_charge_power_capability_design_pf":
ProcessState( "SystemChargePowerCapabilityDesignPF", it.value )
break
case "system_available_charge_power_design_pf":
ProcessState( "SystemAvailableChargePowerDesignPF", it.value )
break
case "system_available_discharge_power_design_pf":
ProcessState( "SystemAvailableDischargePowerDesignPF", it.value )
break
case "system_available_charge_power_unity_pf":
ProcessState( "SystemAvailableChargePowerUnityPF", it.value )
break
case "system_available_discharge_power_unity_pf":
ProcessState( "SystemAvailableDischargePowerUnityPF", it.value )
break
case "available_charger_blocks":
ProcessState( "AvailableChargerBlocks", it.value )
break
case "hardware_capability_discharge_power":
ProcessState( "HardwareCapabilityDischargePower", it.value )
break
case "hardware_capability_charge_power":
ProcessState( "HardwareCapabilityChargePower", it.value )
break
case "battery_blocks":
def Block = 1
def FaultedBlocks = ""
it.value.each(){
def BatteryState = Math.round( ( ( it.nominal_energy_remaining / it.nominal_full_pack_energy ) * 100 ) )
if( ChildrenEnabled ){
PostEventToChild( "Powerwall ${ it.PackageSerialNumber }", "battery", BatteryState, "%" )
}
ProcessState( "BlockMap_${ Block }", [ SerialNumber: "${ it.PackageSerialNumber }", DisabledReasons: "${ it.disabled_reasons }", EnergyRemaining: "${ it.nominal_energy_remaining }", FullEnergy: "${ it.nominal_full_pack_energy }", EnergyRemainingPercent: BatteryState ] )
if( it.OpSeqState == "Faulted" ){
if( FaultedBlocks != "" ){
FaultedBlocks += ",${ Block }"
} else {
FaultedBlocks += "${ Block }"
}
if( ChildrenEnabled ){
PostEventToChild( "Powerwall ${ it.PackageSerialNumber }", "PowerwallStatus", "Faulted due to ${ it.disabled_reasons }" )
if( it.pinv_state != null ){
PostEventToChild( "Powerwall ${ it.PackageSerialNumber }", "PINV_State", it.pinv_state )
}
}
}
Block = ( Block + 1 )
if( FaultedBlocks != "" ){
ProcessEvent( "BlocksWithFaults", "${ FaultedBlocks }" )
}
}
break
case "panel_max_current":
ProcessEvent( "MaxCurrent", it.value, "A" )
break
// Things to ignore at this time
case "PackageSerialNumber": // Serial# for individual Powerwall(s) but already have from other data
case "battery_target_power": // Samples only show 0
case "battery_target_reactive_power": // Samples only show 0
case "max_power_energy_remaining": // Samples only show 0
case "max_power_energy_to_be_charged": // Samples only show 0
case "system_island_state": // Appears to be redundant
case "battery_block": // Appears to be redundant with battery-specific responses
case "load_charge_constraint": // Samples only show 0
case "max_sustained_ramp_rate": // Value?
case "grid_faults": // Redundant?
case "can_reboot": // Relevance
case "smart_inv_delta_p": // Value?
case "smart_inv_delta_q": // Value?
case "last_toggle_timestamp": // Redundant
case "solar_real_power_limit": // Value?
case "score": // Value?
case "primary": // Value?
case "auxiliary_load": // Samples only show 0
case "all_enable_lines_high": // Value?
case "inverter_nominal_usable_power": // Value?
case "expected_energy_remaining": // Samples only show 0
case "command_source":
case "commissioning_diagnostic":
case "update_diagnostic":
case "device_serial":
case "https_conf":
case "last_phase_voltage_communication_time":
case "last_phase_power_communication_time":
case "last_phase_energy_communication_time":
case "power_supply_mode":
case "phase":
case "num_meters_aggregated":
case "in_config":
case "system_adjusted_charge_power_capability_unity_pf":
case "hardware_capability_adjusted_charge_power":
case "system_adjusted_charge_power_capability_design_pf":
Logging( "Ignoring ${ it.key }", 4 )
break
// Anything not handled gets to here and gets logged as unhandled so people can sent it to me
default:
Logging( "Unhandled data: ${ it.key } = ${ it.value }", 3 )
break
}
}
}
// installed is called when the device is installed, all it really does is run updated
def installed(){
Logging( "Installed", 2 )
updated()
}
// initialize is called when the device is initialized, all it really does is run updated
def initialize(){
Logging( "Initialized", 2 )
updated()
}
// uninstalling device so make sure to clean up children
void uninstalled() {
// Delete all children
getChildDevices().each{
deleteChildDevice( it.deviceNetworkId )
}
Logging( "Uninstalled", 2 )
}
// Process data to check against current state value and then send an event if it has changed
def ProcessEvent( Variable, Value, Unit = null, Description = null ){
sendEvent( name: Variable, value: Value, unit: Unit, descriptionText: Description )
Logging( "Event: ${ Variable } = ${ Value } Unit = ${ Unit } Description = ${ Description }", 4 )
ProcessState( Variable, Value )
//UpdateTile( "${ Value }" )
}
// Set a state variable to a value
def ProcessState( Variable, Value ){
Logging( "State: ${ Variable } = ${ Value }", 4 )
state."${ Variable }" = Value
//UpdateTile( "${ Value }" )
}
// Post data to child device
def PostEventToChild( Child, Variable, Value, Unit = null, Description = null ){
if( ChildrenEnabled ){
if( Child != null ){
if( getChildDevice( Child ) == null ){
addChild( Child )
}
if( getChildDevice( Child ) != null ){
getChildDevice( Child ).sendEvent( name: Variable, value: Value, unit: Unit, descriptionText: Description )
PostStateToChild( "${ Child }", "${ Variable }", Value )
} else {
Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }${ Unit }", 5 )
}
} else {
Logging( "Failure to add child because child name was null", 5 )
}
}
}
// Post data to child device
def PostStateToChild( Child, Variable, Value ){
if( ChildrenEnabled ){
if( Child != null ){
if( getChildDevice( "${ Child }" ) == null ){
addSensor( "${ Child }" )
}
if( getChildDevice( "${ Child }" ) != null ){
Logging( "Child State: ${ Variable } = ${ Value }", 4 )
getChildDevice( "${ Child }" ).ProcessState( "${ Variable }", Value )
} else {
Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }", 5 )
}
} else {
Logging( "Failure to add child because child name was null", 5 )
}
}
}
// Adds a child device
// Based on @mircolino's method for child sensors
def addChild( String DNI ){
if( ChildrenEnabled ){
try{
addChildDevice( "TeslaChild", DNI, [ name: "${ DNI }" ] )
}
catch( Exception e ){
def Temp = e as String
if( Temp.contains( "not found" ) ){
Logging( "TeslaChild driver is not loaded, this is required for child devices.\n Disabling children for rest of refresh.", 5 )
ChildrenEnabled = false
} else {
Logging( "Exception in addSensor: ${ Temp }", 5 )
}
}
}
}
// Handles whether logging is enabled and thus what to put there.
def Logging( LogMessage, LogLevel ){
// Add all messages as info logging
if( ( LogLevel == 2 ) && ( LogType != "None" ) ){
log.info( "${ device.displayName } - ${ LogMessage }" )
} else if( ( LogLevel == 3 ) && ( ( LogType == "Debug" ) || ( LogType == "Trace" ) ) ){
log.debug( "${ device.displayName } - ${ LogMessage }" )
} else if( ( LogLevel == 4 ) && ( LogType == "Trace" ) ){
log.trace( "${ device.displayName } - ${ LogMessage }" )
} else if( LogLevel == 5 ){
log.error( "${ device.displayName } - ${ LogMessage }" )
}
}
// Checks drdsnell.com for the latest version of the driver
// Original inspiration from @cobra's version checking
def CheckForUpdate(){
ProcessEvent( "DriverName", DriverName() )
ProcessEvent( "DriverVersion", DriverVersion() )
httpGet( uri: "https://www.drdsnell.com/projects/hubitat/drivers/versions.json", contentType: "application/json" ){ resp ->
switch( resp.status ){
case 200:
if( resp.data."${ DriverName() }" ){
CurrentVersion = DriverVersion().split( /\./ )
if( resp.data."${ DriverName() }".version == "REPLACED" ){
ProcessEvent( "DriverStatus", "Driver replaced, please use ${ resp.data."${ state.DriverName }".file }" )
} else if( resp.data."${ DriverName() }".version == "REMOVED" ){
ProcessEvent( "DriverStatus", "Driver removed and no longer supported." )
} else {
SiteVersion = resp.data."${ DriverName() }".version.split( /\./ )
if( CurrentVersion == SiteVersion ){
Logging( "Driver version up to date", 3 )
ProcessEvent( "DriverStatus", "Up to date" )
} else if( ( CurrentVersion[ 0 ] as int ) > ( SiteVersion [ 0 ] as int ) ){
Logging( "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 )
ProcessEvent( "DriverStatus", "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( CurrentVersion[ 1 ] as int ) > ( SiteVersion [ 1 ] as int ) ){
Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 )
ProcessEvent( "DriverStatus", "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( CurrentVersion[ 2 ] as int ) > ( SiteVersion [ 2 ] as int ) ){
Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 )
ProcessEvent( "DriverStatus", "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" )
} else if( ( SiteVersion[ 0 ] as int ) > ( CurrentVersion[ 0 ] as int ) ){
Logging( "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
} else if( ( SiteVersion[ 1 ] as int ) > ( CurrentVersion[ 1 ] as int ) ){
Logging( "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
} else if( ( SiteVersion[ 2 ] as int ) > ( CurrentVersion[ 2 ] as int ) ){
Logging( "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 )
ProcessEvent( "DriverStatus", "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" )
}
}
} else {
Logging( "${ DriverName() } is not published on drdsnell.com", 2 )
ProcessEvent( "DriverStatus", "${ DriverName() } is not published on drdsnell.com" )
}
break
default:
Logging( "Unable to check drdsnell.com for ${ DriverName() } driver updates.", 2 )
break
}
}
}