/* * Neurio * * Description: * This Hubitat driver polls a Neurio home energy meter. * * Features List: * Up to 6 channels * Basic Authentication mode for Neurio * Provides Power & Voltage for channels reported by the Neurio * Capability to have a Tile Template for displaying multiple variables together * * Instructions for using Tile Template method (originally based on @mircolino's HTML Templates): * 1) In "Hubitat -> Devices" select the child/sensor (not the parent) you would like to "templetize" * 2) In "Preferences -> Tile Template" enter your template (example below) and click "Save Preferences" * Ex: "[font size='2'][b]Temperature:[/b] ${ temperature }¡Æ${ location.getTemperatureScale() }[/br][/font]" * 3) In a Hubitat dashboard, add a new tile, and select the child/sensor, in the center select "Attribute", and on the right select the "Tile" attribute * 4) Select the Add Tile button and the tile should appear * NOTE: Should accept most HTML formatting commands with [] instead of <> * * 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: * 0.7.1 - Correction to ProcessEvent function and removal of old driver-specific attributes when Preferences are saved * 0.7.0 - Major rework of various areas to match my current style of coding and data returned * 0.6.5 - DataAsOf reformatted with 2 digit year * 0.6.4 - Added "DataAsOf" attribute to know when data was last updated * 0.6.3 - Updated method for processing state and event data, update to driver version checking methods * 0.6.2 - Added HTML Template capability for dashboard use * 0.6.1 - Hide password field * 0.6.0 - Refresh of methods, newer style of code, correction to Refresh * 0.5.0 - Corrected some copy/paste errors and updated to new refresh/version/logging methods to match my other drivers * 0.4 - Update to driver version checking. * 0.3 - Corrected how attributes are listed to reduce duplication in sent events * 0.2 - Adding authentication method to handle Neurio Basic Authentication mode. Also added additional channel support. * 0.1 - Based on my AmbientWeather driver. * * Thank you(s): * @Cobra's contributions to the community, the driver update notice is based on his original method * @mircolino for working out the HTML Template method for dashboard use */ // Returns the driver name def DriverName(){ return "Neurio" } // Returns the driver version def DriverVersion(){ return "0.7.1" } metadata{ definition ( name: "Neurio", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/Neurio.groovy" ) { // Attempting to indicate what capabilities the device should be capable of capability "Sensor" capability "Refresh" capability "PowerMeter" capability "CurrentMeter" // Attributes being built into the device // 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 attribute "Firmware", "string" // Firmware keeps track of the current version of the Neurio firmware attribute "power", "number" // power is the CONSUMPTION channel's p_W value attribute "amperage", "number" // amperage is the total of all the CTS i_A values reported attribute "timestamp", "string" // timestamp is when the last reading was polled from the Neurio attribute "DataAsOf", "string" // Last time Hubitat updated the data from the Neurio // Direct matches for names attribute "PHASE_A_CONSUMPTION_Voltage", "number" attribute "PHASE_B_CONSUMPTION_Voltage", "number" attribute "PHASE_A_CONSUMPTION_Power", "number" attribute "PHASE_B_CONSUMPTION_Power", "number" attribute "CONSUMPTION_Voltage", "number" attribute "CONSUMPTION_Power", "number" // Special ones based on what a Neurio can typically do attribute "Channel1Power", "number" attribute "Channel1Voltage", "number" attribute "Channel1Type", "string" attribute "Channel2Power", "number" attribute "Channel2Voltage", "number" attribute "Channel2Type", "string" attribute "Channel3Power", "number" attribute "Channel3Voltage", "number" attribute "Channel3Type", "string" // Special ones based on what a Neurio can sometimes do with additional sub-meters attribute "Channel4Power", "number" attribute "Channel4Voltage", "number" attribute "Channel4Type", "string" attribute "Channel5Power", "number" attribute "Channel5Voltage", "number" attribute "Channel5Type", "string" attribute "Channel6Power", "number" attribute "Channel6Voltage", "number" attribute "Channel6Type", "string" // Tile Template attribute attribute "Tile", "string"; // Ex: "Voltage: ${ CONSUMPTION_Voltage }[/br]Power: ${ CONSUMPTION_Power }[/br]" } preferences{ section{ input( type: "enum", name: "RefreshRate", title: "Refresh Rate", required: false, multiple: false, options: [ "1 minute", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "3 hours", "Manual" ], defaultValue: "5 minutes" ) input( type: "string", name: "NeurioUsername", title: "Local Username for Neurio not cloud login", required: false ) input( type: "password", name: "NeurioPassword", title: "Local Password for Neurio not cloud login", required: false ) input( type: "string", name: "NeurioIP", title: "Neurio IPv4 Address", required: true ) input( type: "string", name: "TileTemplate", title: "Tile Template", description: "Ex: Voltage: \${ CONSUMPTION_Voltage }[/br]Power: \${ CONSUMPTION_Power }[/br]", defaultValue: "") input( type: "enum", name: "LogType", title: "Enable Logging?", required: true, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) } } } // updated is called whenever device parameters are saved // It sets the current version of the driver and sets some basic settings def updated(){ Logging( "Updated", 2 ) // 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() }" ) // Check what the refresh rate is set for then run it switch( RefreshRate ){ case "1 minute": runEvery1Minute( "refresh" ) break case "5 minutes": runEvery5Minutes( "refresh" ) break case "10 minutes": runEvery10Minutes( "refresh" ) break case "15 minutes": runEvery15Minutes( "refresh" ) break case "30 minutes": runEvery30Minutes( "refresh" ) break case "1 hour": runEvery1Hour( "refresh" ) break case "3 hours": runEvery3Hours( "refresh" ) break case "Manual": unschedule( "refresh" ) break } Logging( "Refresh rate: ${ RefreshRate }", 3 ) schedule( new Date(), CheckForUpdate ) } // refresh triggers a poll for data def refresh(){ PollNeurio() } //Poll Neurio for data def PollNeurio() { Logging( "Attempting to get data from Neurio", 4 ) def JSONParams = null if( NeurioUsername != null && NeurioPassword != null ){ JSONParams = [ uri: "http://${ NeurioUsername }:${ NeurioPassword }@${ NeurioIP }/current-sample", contentType: "application/json" ] } else { JSONParams = [ uri: "http://${ NeurioIP }/current-sample", contentType: "application/json" ] } asynchttpGet( "GetNeurio", JSONParams ) } // Handles the response from Neurio def GetNeurio( resp, data ) { if( resp.getStatus() == 200 ) { Logging( "Neurio Raw Response = ${ resp.data }", 4 ) Json = parseJson( resp.data ) ParseJSON( Json ) } else if( resp.getStatus() == 401 ) { Logging( "Error connecting to Neurio: ${ resp.getStatus() }, this may be caused by the Neurio using Basic Authentication so you will need to enter you login credentials as Preferences.", 5 ) } else { Logging( "Error connecting to Neurio: ${ resp.status }", 5 ) } } // Parse the data returned, output events and populate attributes as needed def ParseJSON( Json ){ // If there are any values returned, process them if( Json != null ){ def NumberOfValuesAll = Json.size() as Integer ProcessEvent( "timestamp", Json.timestamp ) def Count = 1 Json.channels.each{ ProcessState( "Channel${ Count }Type", it.type ) ProcessEvent( "Channel${ Count }Voltage", it.v_V, "V" ) ProcessEvent( "Channel${ Count }Power", it.p_W, "W" ) switch( it.type ){ case "PHASE_A_CONSUMPTION": ProcessEvent( "PHASE_A_CONSUMPTION_Voltage", it.v_V, "V" ) ProcessEvent( "PHASE_A_CONSUMPTION_Power", it.p_W, "W" ) break case "PHASE_B_CONSUMPTION": ProcessEvent( "PHASE_B_CONSUMPTION_Voltage", it.v_V, "V" ) ProcessEvent( "PHASE_B_CONSUMPTION_Power", it.p_W, "W" ) break case "CONSUMPTION": ProcessEvent( "CONSUMPTION_Voltage", it.v_V, "V" ) ProcessEvent( "CONSUMPTION_Power", it.p_W, "W" ) ProcessEvent( "power", it.p_W, "W" ) break case "SUBMETER": break } if( it.label != null ){ ProcessState( "Channel${ Count }Label", it.label ) } Count ++ } def TempAmps = 0 Json.cts.each{ TempAmps = ( TempAmps + it.i_A ) } ProcessEvent( "amperage", TempAmps, "A" ) ProcessEvent( "DataAsOf", "${ new Date().format( "MM/dd/yy HH:mm" ) }" ) Logging( "Updated as of: ${ new Date().format( "MM/dd/yy HH:mm" ) }", 4 ) } else { Logging( "No data reported", 3 ) } } // Tile Template method based on @mircolino's HTML Template method private void UpdateTile( String val ){ if( settings.TileTemplate ){ // Create special compound/html tile val = settings.TileTemplate.toString().replaceAll( "\\[", "<" ) val = val.replaceAll( "\\]", ">" ) val = val.replaceAll( ~/\$\{\s*([A-Za-z][A-Za-z0-9_]*)\s*\}/ ) { java.util.ArrayList m -> device.currentValue("${ m [ 1 ] }").toString() } if( device.currentValue( "Tile" ).toString() != val ){ sendEvent( name: "Tile", value: val ) } } } // 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 ) } UpdateTile( "${ Value }" ) } } // Process data to check against current state value and update if it has changed def ProcessState( Variable, Value ){ if( state."${ Variable }" != Value ){ Logging( "State: ${ Variable } = ${ Value }", 4 ) state."${ Variable }" = Value UpdateTile( "${ Value }" ) } } // installed is called when the device is installed, all it really does is run updated def installed(){ Logging( "Installed", 2 ) updated() } // 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 } } }