/* * 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 as of Tesla's version 20.49.0. * Password = REQUIRED as of Tesla's version 20.49.0. * RefreshRate = REQUIRED - DEFAULT = 5 minutes. The rate at which the device will be polled * ReauthorizationRate = REQUIRED - DEFAULT = 24 hours. 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: aggregate, grid status, sitemaster, registration, powerwalls, & system status) * Ability to check drdsnell.com for updates to the driver * * Licensing: * Copyright 2024 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.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.11" } metadata{ definition ( name: "TeslaDriver", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/TeslaDriver.groovy" ) { // Attempting to indicate what 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 a 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 in the driver // Driver identifies the driver being used for update purposes 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 the device data attribute "Grid Status", "string" attribute "Device Type", "string" attribute "Tesla Device Version", "string" attribute "System Energy (kWh)", "number" attribute "System Power (kW)", "number" attribute "System Status", "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" } preferences{ section{ if( ShowAllPreferences || ShowAllPreferences == null ){ // Show the preferences options // String to retain the device's IP or hostname input( type: "string", name: "DeviceIP", title: "Powerwall's IP/Hostname", required: true ) // String to retain the customer's email input( type: "string", name: "Email", title: "Email", required: true ) // String to retain the customer's login password input( type: "password", name: "Password", title: "Password", required: true ) // Enum to allow selecting the refresh rate that the device will be checked 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 reauthorization rate input( type: "enum", name: "ReauthorizationRate", title: "Reauthorization Rate", required: false, multiple: false, options: [ "3 hours", "6 hours", "12 hours", "24 hours" ], defaultValue: "24 hours" ) // Bool about whether child devices will be enabled input( type: "bool", name: "ChildrenEnabled", title: "Enable Child Devices?", description: "Once enabled, child devices will be made for added sensors.", required: false, defaultValue: false ) // Enum to set the level of logging that will be used input( type: "enum", name: "LogType", title: "Enable Logging?", required: true, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) // Bool to set whether the other preferences should be displayed or not input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } else { // Preferences should be hidden so only show the preference to show them or not input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } } } } // DoSomething is just a test/correction command that should be disabled before publishing def DoSomething(){ state.'Grid Status' = null } // updated is called whenever device parameters are saved // Sets the current version of the driver, basic settings, and schedules def updated(){ // Set basic info logging if for some reason the 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 ) } } // Check if the refresh rate is not set for some reason and putting it at the default 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 ) // Check if the reauthorization rate is not set for some reason and putting it at the default if( ReauthorizationRate == null ){ ReauthorizationRate = "24 hours" } // Check what the reauthorization rate is set for then run it switch( ReauthorizationRate ){ 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 Name" != null ){ state.remove( "Driver Name" ) state.remove( "Driver Version" ) device.deleteCurrentState( "Driver Name" ) device.deleteCurrentState( "Driver Version" ) } ProcessState( "DriverName", "${ DriverName() }" ) ProcessState( "DriverVersion", "${ DriverVersion() }" ) schedule( "${ Second } ${ Minute } ${ Hour } ? * *", "CheckForUpdate" ) Logging( "Updated", 2 ) } // refresh runs the device polling def refresh(){ if( DeviceIP != null ){ if( state.AuthCookie != null && state.UserRecord != null ){ ConnectLocal() } else { Logging( "AuthCookie and/or UserRecord missing, will attempt Login again.", 5 ) Login() } } else { ProcessEvent( "Status", "Unsuccessful: Lacking device IP address/hostname" ) Logging( "Cannot use local method without device IP/hostname", 5 ) } } // Attempt to perform a login to the Powerwall def Login(){ def Params if( Email != null && Password != null ){ //Params = [ uri: "https://${ DeviceIP }/api/login/Basic", requestContentType: "application/json", contentType: "application/json", ignoreSSLIssues: true, body: "{\"username\":\"customer\",\"password\":\"${ Password }\",\"email\":\"${ Email }\",\"force_sm_off\":\"false\"}" ] //Params = [ uri: "https://${ DeviceIP }/api/login/Basic", requestContentType: "application/json", contentType: "application/json", ignoreSSLIssues: true, query: "[\"username\":\"customer\",\"password\":\"${ Password }\",\"email\":\"${ Email }\",\"force_sm_off\":\"false\"]" ] Params = [uri: "https://${DeviceIP}/api/login/Basic", 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() ) //ProcessState( "Token", resp.getHeaders().token ) TempAuthCookie = null TempUserRecord = null /* resp.getHeaders().each{ TempCookie = resp.getHeaders().'Set-Cookie' if( it.value.split( '=' )[ 0 ].toString() == "AuthCookie" ){ TempAuthCookie = TempCookie.split( ";" )[ 0 ] + ";" } else if( it.value.split( '=' )[ 0 ].toString() == "UserRecord" ){ TempAuthCookie = TempCookie.split( ";" )[ 0 ] } } */ 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( "Username and Password are required for Login.", 5 ) } } // Connect to the local device for information def ConnectLocal(){ // Try to connect locally for status information 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( "ParseAggregates", Params ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/system_status/grid_status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // grid status asynchttpGet( "ParseGridStatus", Params ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/customer/registration", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // customer registration information asynchttpGet( "ParseRegistration", Params ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/powerwalls", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // powerwall information asynchttpGet( "ParsePowerwalls", Params ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/sitemaster", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // limited information about whether system is running asynchttpGet( "ParseSitemaster", Params ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/system_status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System status asynchttpGet( "ParseSystemStatus", Params ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/system_status/soe", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System status asynchttpGet( "ParseSystemStatusSOE", Params ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/site_info", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Site info asynchttpGet( "ParseSiteInfo", Params ) } // Parse the aggregate data received before passing it for processing def ParseAggregates( resp, data ){ switch( resp.status ){ case 200: def TempData 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" ) } UpdateStatus() Logging( "Connected to Tesla for Aggregate.", 4 ) sendEvent( name: "Status", value: "Local Connection Succeeded" ) break case 408: Logging( "Timeout connecting to Tesla for Aggregate.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for Aggregate. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Parse the data received before passing it for processing def ParseDevice( resp, data ){ switch( resp.status ){ case 200: Logging( "Device data = ${ resp.data }", 4 ) Data = parseJson( resp.data ) ProcessData( Data.connection, "${ Data.location }" ) ProcessData( Data.Cached_readings, "${ Data.location }" ) Logging( "Connected to Tesla for Device: ${ Data.location }.", 4 ) break case 408: Logging( "Timeout connecting to Tesla for Specific Device.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for Specific Device. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Parse the data received before passing it for processing def ParseGridStatus( resp, data ){ switch( resp.status ){ case 200: Logging( "GridStatus data = ${ resp.data }", 4 ) Data = parseJson( resp.data ) ProcessData( Data, null ) Logging( "Connected to Tesla for Grid Status.", 4 ) break case 408: Logging( "Timeout connecting to Tesla for Grid Status.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for Grid Status. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Parse the data received before passing it for processing def ParseRegistration( resp, data ){ switch( resp.status ){ case 200: Logging( "Registration data = ${ resp.data }", 4 ) Data = parseJson( resp.data ) ProcessData( Data, null ) Logging( "Connected to Tesla for Registration.", 4 ) break case 408: Logging( "Timeout connecting to Tesla for Registration.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for Registration. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Parse the data received before passing it for processing def ParsePowerwalls( resp, data ){ switch( resp.status ){ case 200: Logging( "Powerwall(s) 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" ) } Data = parseJson( TempData ) Logging( "# Powerwall(s) = ${ Data.powerwalls.size() }", 4 ) Data.powerwalls.each(){ ProcessData( it, "Powerwall ${ it.PackageSerialNumber }" ) } Logging( "Connected to Tesla for Powerwall(s).", 4 ) break case 408: Logging( "Timeout connecting to Tesla for Powerwall(s).", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for Powerwall(s). ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Parse the data received before passing it for processing def ParseSitemaster( resp, data ){ switch( resp.status ){ case 200: Logging( "Sitemaster data = ${ resp.data }", 4 ) Data = parseJson( resp.data ) ProcessData( Data, null ) Logging( "Connected to Tesla for Sitemaster.", 4 ) break case 408: Logging( "Timeout connecting to Tesla for Sitemaster.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for Sitemaster. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Attempt to get the system status alone def GetSystemStatus(){ // Try to connect locally for status information 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( "ParseSystemStatus", Params, [ method: "system_status" ] ) } // Parse the data received before passing it for processing def ParseSystemStatus( resp, data ){ switch( resp.status ){ case 200: Logging( "SystemStatus data = ${ resp.data }", 4 ) Data = parseJson( resp.data ) ProcessData( Data, null ) Logging( "Connected to Tesla for System Status.", 4 ) break case 408: Logging( "Timeout connecting to Tesla for System Status.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for System Status. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Parse the data received before passing it for processing def ParseSystemStatusSOE( resp, data ){ switch( resp.status ){ case 200: Logging( "SystemStatus SOE data = ${ resp.data }", 4 ) Data = parseJson( resp.data ) ProcessData( Data, null ) Logging( "Connected to Tesla for System Status SOE.", 4 ) break case 408: Logging( "Timeout connecting to Tesla for System Status SOE.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for System Status SOE. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) break } } // Parse the data received before passing it for processing def ParseSiteInfo( resp, data ){ switch( resp.status ){ case 200: Logging( "Site Info data = ${ resp.data }", 4 ) Data = parseJson( resp.data ) ProcessData( Data, null ) Logging( "Connected to Tesla for Site Info.", 4 ) break case 408: Logging( "Timeout connecting to Tesla for Site Info.", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for Site Info. ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) 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 the aggregates alone def GetAggregates(){ // Try to connect locally for status information 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( "ParseDataDump", Params, [ method: "aggregates" ] ) } // Attempt to get some data not normally reported def PartialDataDump(){ // Try to connect locally for status information 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( "ParseDataDump", Params, [ method: "meters/readings" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/meters/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meter Status asynchttpGet( "ParseDataDump", Params, [ method: "meters/status" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/operation", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Operation asynchttpGet( "ParseDataDump", Params, [ method: "operation" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/powerwalls/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Powerwalls status asynchttpGet( "ParseDataDump", Params, [ method: "powerwalls/status" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/solars", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Solars asynchttpGet( "ParseDataDump", Params, [ method: "solars" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Status asynchttpGet( "ParseDataDump", 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( "ParseDataDump", Params, [ method: "system_status/grid_status" ] ) } // Attempt to get all data not normally reported def DataDump(){ // Try to connect locally for status information 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( "ParseDataDump", Params, [ method: "config" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/customer", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Customer asynchttpGet( "ParseDataDump", Params, [ method: "customer" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/generators", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Generators asynchttpGet( "ParseDataDump", Params, [ method: "generators" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/generators/disconnect_types", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Generator disconnect types asynchttpGet( "ParseDataDump", Params, [ method: "generators/disconnect_types" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/installer", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Installer asynchttpGet( "ParseDataDump", Params, [ method: "installer" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/installer/companies", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Installer Companies List asynchttpGet( "ParseDataDump", Params, [ method: "installer/companies" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/meters", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meters asynchttpGet( "ParseDataDump", Params, [ method: "meters" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/meters/readings", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meter Readings asynchttpGet( "ParseDataDump", Params, [ method: "meters/readings" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/meters/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Meter Status asynchttpGet( "ParseDataDump", Params, [ method: "meters/status" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/networks", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Networks asynchttpGet( "ParseDataDump", Params, [ method: "networks" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/networks/client_protocols", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Networks Client Protocols asynchttpGet( "ParseDataDump", Params, [ method: "networks/client_protocols" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/operation", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Operation asynchttpGet( "ParseDataDump", Params, [ method: "operation" ] ) pauseExecution( 2000 ) //Params = [ uri: "https://${ DeviceIP }/api/powerwalls/phase_detection", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Powerwalls phase detection //asynchttpGet( "ParseDataDump", Params, [ method: "powerwalls/phase_detection" ] ) //pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/powerwalls/phase_usages", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Powerwalls phase usage asynchttpGet( "ParseDataDump", Params, [ method: "powerwalls/phase_usages" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/powerwalls/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Powerwalls status asynchttpGet( "ParseDataDump", Params, [ method: "powerwalls/status" ] ) pauseExecution( 2000 ) //Params = [ uri: "https://${ DeviceIP }/api/site_info/grid_codes", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Site Info Grid Codes //asynchttpGet( "ParseDataDump", Params, [ method: "site_info/grid_codes" ] ) //pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/site_info/grid_regions", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Site Info Grid Regions asynchttpGet( "ParseDataDump", 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( "ParseDataDump", Params, [ method: "site_info/site_name" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/solars", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Solars asynchttpGet( "ParseDataDump", Params, [ method: "solars" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/solars/brands", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Solars Brands asynchttpGet( "ParseDataDump", Params, [ method: "solars/brands" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // Status asynchttpGet( "ParseDataDump", Params, [ method: "status" ] ) pauseExecution( 2000 ) Params = [ uri: "https://${ DeviceIP }/api/system/update/status", contentType: "application/json", ignoreSSLIssues: true, headers: AuthHeader ] // System Update Status asynchttpGet( "ParseDataDump", 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( "ParseDataDump", 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( "ParseDataDump", Params, [ method: "system_status/grid_status" ] ) } // Parse the data received def ParseDataDump( resp, data ){ switch( resp.status ){ case 200: Logging( "${ data.method } datadump response ${ resp.data }", 3 ) break case 408: Logging( "Timeout connecting to Tesla for ${ data.method }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed - Timeout" ) break default: Logging( "Error connecting to Tesla for ${ data.method } ${ resp.status }", 5 ) sendEvent( name: "Status", value: "Local Connection Failed" ) 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_code": 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 // 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 ), "%", true ) 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( "System Status", "Up", null, true ) } else { ProcessEvent( "System Status", "Not Up", null, true ) Logging( "System Status identified as Not Up because value was ${ it.value }", 3 ) } if( ChildrenEnabled && Device != null ){ if( it.value == "StatusUp" ){ PostEventToChild( Device, "System Status", "Up", null, true ) } else { PostEventToChild( Device, "System Status", "Not Up", null, true ) Logging( "System Status identified as Not Up because value was ${ it.value }", 3 ) } } break case "running": ProcessEvent( "System Running", "${ it.value }", null, true ) 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 }", null, true ) 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 }", null, true ) } 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_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", null, true ) if( ChildrenEnabled && Device != null ){ PostEventToChild( Device, "Grid Status", "Grid up" ) } break case "SystemIslandedActive": ProcessEvent( "Grid Status", "Grid down", null, true ) if( ChildrenEnabled && Device != null ){ PostEventToChild( Device, "Grid Status", "Grid down" ) } break case "SystemTransitionToGrid": ProcessEvent( "Grid Status", "Grid up still syncing", null, true ) 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( "Number of Batteries", it.value ) break // Things to ignore at this time case "PackageSerialNumber": // Moved to ignore so it is not recorded anywhere 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 "blocks_controlled": // Redundant 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 "battery_blocks": 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": 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, ForceEvent = false ){ if( ( state."${ Variable }" != Value ) || ( ForceEvent == true ) ){ state."${ Variable }" = Value if( Unit != null ){ Logging( "Event: ${ Variable } = ${ Value }${ Unit }", 4 ) sendEvent( name: "${ Variable }", value: Value, unit: Unit, isStateChange: true ) } else { Logging( "Event: ${ Variable } = ${ Value }", 4 ) sendEvent( name: "${ Variable }", value: Value, isStateChange: true ) } } } // Process data to check against current state value def ProcessState( Variable, Value ){ if( state."${ Variable }" != Value ){ Logging( "State: ${ Variable } = ${ Value }", 4 ) state."${ Variable }" = Value } } // Post data to child device def PostEventToChild( Child, Variable, Value, Unit = null, ForceEvent = false ){ if( ChildrenEnabled ){ def ChildParent = "${ Child }" if( ChildParent != null ){ if( getChildDevice( "${ ChildParent }" ) == null ){ addSensor( "${ ChildParent }" ) } if( getChildDevice( "${ ChildParent }" ) != null ){ if( Unit != null ){ if( ForceEvent != null ){ getChildDevice( "${ ChildParent }" ).ProcessEvent( "${ Variable }", Value, "${ Unit }", ForceEvent ) Logging( "Child Event: ${ Variable } = ${ Value }${ Unit }", 4 ) } else { getChildDevice( "${ ChildParent }" ).ProcessEvent( "${ Variable }", Value, "${ Unit }" ) Logging( "Child Event: ${ Variable } = ${ Value }", 4 ) } } else { if( ForceEvent != null ){ getChildDevice( "${ ChildParent }" ).ProcessEvent( "${ Variable }", Value, null, ForceEvent ) Logging( "Child Event: ${ Variable } = ${ Value }${ Unit }", 4 ) } else { getChildDevice( "${ ChildParent }" ).ProcessEvent( "${ Variable }", Value ) Logging( "Child Event: ${ Variable } = ${ Value }", 4 ) } } } else { if( Unit != null ){ Logging( "Failure to add ${ ChildParent } and post ${ Variable }=${ Value }${ Unit }", 5 ) } else { Logging( "Failure to add ${ ChildParent } and post ${ Variable }=${ Value }", 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 ){ //def ChildParent = "${ Child } ${ device.deviceNetworkId }" def ChildParent = "${ Child }" if( ChildParent != null ){ if( getChildDevice( "${ ChildParent }" ) == null ){ addSensor( "${ ChildParent }" ) } if( getChildDevice( "${ ChildParent }" ) != null ){ Logging( "Child State: ${ Variable } = ${ Value }", 4 ) getChildDevice( "${ ChildParent }" ).ProcessState( "${ Variable }", Value ) } else { Logging( "Failure to add ${ ChildParent } and post ${ Variable }=${ Value }", 5 ) } } else { Logging( "Failure to add child because child name was null", 5 ) } } } // Adds a TeslaChild child device // Based on @mircolino's method for child sensors def addSensor( String DNI ){ try{ Logging( "addSensor(${ DNI })", 3 ) 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 } } }