APC UPS Binding

, ,

Thanks for posting the method to access apcupsd - it worked right out of the box for me. :grinning:

For my setup, I simplified it a bit and created a single Thing with an associated Item for each of the parameters I’m interested in for the UPS (example for STATUS below). I monitor several parameters and each one can be done at different time intervals (or triggered by rules? but not there yet).

But clearly parsing the full output of apcaccess is what’s needed for a solid binding so thanks for sharing. :+1:

ups.things:
Thing exec:command:UPSstatus [command="/sbin/apcaccess -p STATUS", interval=60, timeout=2]

ups.items:
String UPS_Status “UPS Status: [%s]” (gUPS) {channel=“exec:command:UPSstatus:output”}

Hi @Roi,

I’m a newbie to OpenHAB 2 and am very slowly working with understanding how it functions and would you guess that getting my APC Back-UPS XS 1500 up and running on it was the first ‘thing’ I decided to add to my system.

Well the good news is that I can get the ‘Basic UI’ to display the data coming from the UPS, but I have yet to find a really nice interface that might even offer a way to set all of the variables used in the ‘rules’…

Also trying to learn about the various interface options for OpenHAB as I envision a button i press that takes me to a panel that maybe has some cool graphics that shows percentage of load, battery life expectancy based on load, changes color when on battery power and shows time left. Maybe a way to silence the alarm if possible…

Some, ok maybe all, of what I’m hoping for may not be possible, but I’m learning and still hunting…

Hi Roi, i used the instructions on github and finally got it work - thank you.
Can you help me understand why i get this messy representation of data on paperUI?

Hi Karl.
Is it possibel to get the temperature of the UPS? When i run apcaccess int the console, i can’t find this value?
Thanks and greetings,
Markus

Hi Markus,

Sorry I also don’t know. Maybe someone else knows?
Best regards,
Karl

Hi

Thanks for all the great ideas and scripts in this post.

I couldn’t get my head around how the python script worked, but I did find a solution using the Exec binding.

I created a Thing in paperUI using the Exec Binding with the simple command of

apcaccess -u

I then linked the output to a String Item - in my case I called it “APCUPSQuery_Output”

this rule then splits the string into separate variables

(testtrigger is just a virtual switch that I use to trigger rules when I’m developing)

rule "APC UPS String spliting"
when
Item APCUPSQuery_Output received update // Change this Item to your "apcaccess -u" exec query output
// or Item testtrigger changed	// Only a test switch to force the rule to run for testing

then 


	logInfo("APC Info", "APC UPS Query update = "+APCUPSQuery_Output.state)	// Change this Item to your "apcaccess -u" exec query output // Comment out this line to stop it populating your openhab.log file

	var String APCQUERY = APCUPSQuery_Output.state.toString	// Change this Item to your "apcaccess -u" exec query output

	
	
	
	var APCname 		= (APCQUERY.split(':').get(14)).split('\n').get(0) // The (\n) split removes the line feed and wrapped text from the result.
	var APCOnline		= (APCQUERY.split(':').get(15)).split('\n').get(0)
	var linev			= (APCQUERY.split(':').get(16)).split('\n').get(0)
	var LOAD 			= (APCQUERY.split(':').get(17)).split('\n').get(0)
	var loadNo 			= Float::parseFloat(String::format("%s",LOAD)) 			// turns the string into a usable number
	var batterych		= (APCQUERY.split(':').get(18)).split('\n').get(0)
	var batterychNo 	= Float::parseFloat(String::format("%s",batterych)) 	// turns the string into a usable number
	var timeleft		= (APCQUERY.split(':').get(19)).split('\n').get(0)
	var timeleftNo 		= Float::parseFloat(String::format("%s",timeleft)) 		// turns the string into a usable number
	var outputv			= (APCQUERY.split(':').get(23)).split('\n').get(0)
	var outputvNo 		= Float::parseFloat(String::format("%s",outputv	)) 		// turns the string into a usable number
	var itemp			= (APCQUERY.split(':').get(30)).split('\n').get(0)
	var itempNo 		= Float::parseFloat(String::format("%s",itemp	)) 		// turns the string into a usable number
	var battv			= (APCQUERY.split(':').get(32)).split('\n').get(0)
	var battvNo 		= Float::parseFloat(String::format("%s",battv	)) 		// turns the string into a usable number


	logInfo("APC Info","APC UPS Named "+APCName+" is "+APCOnline+", with an internal temperature of "+itempNo+"°C,  Load = "+loadNo+" Percent, Current Line Voltage is "+linev+" Battery is at "+batterychNo+" percent, giving "+timeleftNo+" minutes at current load, at "+outputvNo+" Vac. Current battery voltage "+battvNo+" Vdc")    // Comment out this line once you're happy the rule is working for you.

//	You could use the values to populate Items for use elsewhere, for example

//	APCname.postUpdate(APCName)
//	APCOnline.postUpdate(APCOnline)
//	APClinev.postUpdate(linev)
//	APCLoad.postUpdate(loadNo)
//	APCbatterycharge.postUpdate(batterychNo)
//	APCtimeleft.postUpdate(timeleftNo)
// 	APCoutputv.postUpdate(outputvNo)
//	APCbatteryvoltage.postUpdate(battvNo)

end

This gives me a loginfo output of :-

2019-02-21 10:53:25.870 [INFO ] [ipse.smarthome.model.script.APC Info] - APC UPS Named Smart-UPS 2200 XL is ONLINE , with an internal temperature of 11.2°C, Load = 7.1 Percent, Current Line Voltage is 239.0 Battery is at 100.0 percent, giving 1020.0 minutes at current load, at 239.0 Vac. Current battery voltage 55.4 Vdc

Thanks to @rlkoshak for his help yesterday with splitting strings, I’m still not entirely sure how to implement his parts idea, but I’m working on it :slight_smile:

Hi

I think you need the ITEMP value :slight_smile:

using the rule I created, it would be split no 30

	var itemp			= (APCQUERY.split(':').get(30)).split('\n').get(0)
	var itempNo 		= Float::parseFloat(String::format("%s",itemp	)) 		// turns the string into a usable number

FYI

	logInfo("APC Info","APC UPS Named "+APCName+" is "+APCOnline+", with an internal temperature of "+itempNo+"°C,  Load = "+loadNo+" Percent, Current Line Voltage is "+linev+" Battery is at "+batterychNo+" percent, giving "+timeleftNo+" minutes at current load, at "+outputvNo+" Vac. Current battery voltage "+battvNo+" Vdc")

To apply some of the lessons on that other thread here.

Since we know that calling split doesn’t actually change the original String, there really is no need to split the String over and over. Instead split the String only once and store that in a variable (I call it parts) are reuse the variable.

Next, to remove white space from the beginning and end of your String, there is a handy method called trim.

Finally, the String::format really isn’t doing anything for you in the Float::parseFloat calls. Since you are using %s this is the same as calling Float::parseFloat(LOAD.toString) and since LOAD is already a String this is basically a noop.

There are some other minor concerns about using parseFloat here as well. parseFloat returns a primitive float. Unfortunately, especially on an RPi, the use of primitives can greatly increase the amount of time it takes to load and parse .rules files. Because you are not actually doing anything with these variables that requires them to be a primitive float, it would be better to let it be a Number. By default the Rules DSL uses BigDecimal to represent numbers so let’s just use that.

So the above code would become:

	logInfo("APC Info", "APC UPS Query update = "+APCUPSQuery_Output.state)	// Change this Item to your "apcaccess -u" exec query output // Comment out this line to stop it populating your openhab.log file

	var APCQUERY = APCUPSQuery_Output.state.toString.split(':')	// Change this Item to your "apcaccess -u" exec query output

	var APCname 		= APCQUERY.get(14).trim
	var APCOnline		= APCQUERY.get(15).trim
	var linev			= APCQUERY.get(16).trim
	var LOAD 			= APCQUERY.get(17).trim
	var loadNo 			= new BigDecimal(LOAD) 			// turns the string into a usable number
	var batterych		= APCQUERY.get(18).trim
	var batterychNo 	= new BigDecimal(batterych) 	// turns the string into a usable number
	var timeleft		= APCQUERY.get(19).trim
	var timeleftNo 		= new BigDecimal(timeleft) 		// turns the string into a usable number
	var outputv			= APCQUERY.get(23).trim
	var outputvNo 		= new BigDecimal(outputv) 		// turns the string into a usable number
	var itemp			= APCQUERY.get(30).trim
	var itempNo 		= new BigDecimal(itemp) 		// turns the string into a usable number
	var battv			= APCQUERY.get(32).trim
	var battvNo 		= new BigDecimal(battv) 		// turns the string into a usable number

Not only is the above more efficient, which isn’t really that much of a concern here, but it avoids duplicated code which is a bigger deal. When ever you see a bunch of lines that look the same or groups of lines that look the same with only minor difference, it is usually worth while to try to centralize the duplicated code into one place. In this case, we centralized the split.

Theoretically you could also centralize the trims with something like

	var APCQUERY = APCUPSQuery_Output.state.toString.replace("\n", "").split(':')	// Change this Item to your "apcaccess -u" exec query output

But I don’t know the String that is being parsed well enough to know if that would cause problems.

The part referencing parseFloat is not something I would expect the vast majority of OH users to know and understand before now. The only reason I know it is because of my extensive experience helping other users.

1 Like

yet again your wisdom shines through.

BigDecimal is a new feature, I’ll try that and see what happens.

Update

I’ve just tried replacing the float command with the BigDecimal command and got this error :frowning_face:

2019-02-21 17:07:53.165 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'APC UPS String spliting': An error occurred during the script execution: null

As for .trim Vs. the (’\n’) split that I had, there is / was a really good reason for using it. :slight_smile:

without the .split(’\n’).get(0) part, the string returns the value after the ‘:’ AND the label for the next item.

so I was trying to get the value from a string like this :

linev : 237.5
battv : 55.1
itemp : 10.1

Which I concluded was actually…

linev : 237.5{NewLine}
battv : 55.1{NewLine}
itemp : 10.1{NewLine}

without the (’\n’) split, the value battv appears as

55.1
itemp

So it’s not just empty space I’m looking to get rid of, but the next bit of text too :smile:

Regarding the parts technique you suggested, I really couldn’t get that to work, either with this rule or the BBC Weather one.

I’ll give the exact rule you’ve written a go as you’ve taken the time to create it.

Mainly because I’m really interested to see how it works.

Makes sense. The trim won’t work then. You should update your comment though because it reads like you are just removing the last \n from the string.

There might be a more self documenting approach to this. What if you put the label and value into a Map. Then you can pull the values out by label instead of the arcane get(15) which only makes sense when you know what the 16th line is.

It would look something like:

import java.util.Map

rule "APC UPS String spliting"
when
    Item APCUPSQuery_Output received update // Change this Item to your "apcaccess -u" exec query output
// or Item testtrigger changed	// Only a test switch to force the rule to run for testing
then 

    logInfo("APC Info", "APC UPS Query update = "+APCUPSQuery_Output.state)	// Change this Item to your "apcaccess -u" exec query output // Comment out this line to stop it populating your openhab.log file

    val Map<String, String> results = newHashMap
    var APCQUERY = APCUPSQuery_Output.state.toString.split('\n')
    APCQUERY.forEach[ line | 
        val split = line.split(':') // this will not work if you need a field that has ':' as part of the data, like a date/time. There is a way to split the String on just the first ':'.
        results.put(split.get(0), split.get(1))
    ]

    APCname.postUpdate(results.get("NAME")) // whatever the label for Name happens to be
    ...
    APCLoad.postUpdate(results.get("LOAD")) // you don't have to convert the number values to BigDecimal to send them to a Number Item.
    ...
end

I’ll need to see the errors because I use this approach routinely.

1 Like

Hi

I’ve just pasted your new rule in with only a slight edit to the last line, and this is what the openhab.log shows…

2019-02-21 17:23:21.544 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'APC_UPS-split.rules'
2019-02-21 17:23:31.731 [INFO ] [ipse.smarthome.model.script.APC Info] - APC UPS Query update =
APC      : 001,043,1022
DATE     : 2019-02-21 17:03:11 +0000
HOSTNAME : ODroid
VERSION  : 3.14.12 (29 March 2014) debian
UPSNAME  : APC_XL3k
CABLE    : USB Cable
DRIVER   : USB UPS Driver
UPSMODE  : Stand Alone
STARTTIME: 2019-02-20 09:03:34 +0000
MODEL    : Smart-UPS 2200 XL
STATUS   : ONLINE
LINEV    : 240.4
LOADPCT  : 7.1
BCHARGE  : 100.0
TIMELEFT : 1020.0
MBATTCHG : 10
MINTIMEL : 3
MAXTIME  : 0
OUTPUTV  : 239.0
SENSE    : High
DWAKE    : -1
DSHUTD   : 180
LOTRANS  : 208.0
HITRANS  : 253.0
RETPCT   : 15.0
ITEMP    : 16.2
ALARMDEL : 30
BATTV    : 55.4
LINEFREQ : 50.0
LASTXFER : Automatic or explicit self test
NUMXFERS : 0
TONBATT  : 0
CUMONBATT: 0
XOFFBATT : N/A
SELFTEST : NO
STESTI   : 14 days
STATFLAG : 0x05000008
MANDATE  : 2008-06-21
SERIALNO : JS0825020309
BATTDATE : 2016-12-01
NOMOUTV  : 240
NOMBATTV : 48.0
FIRMWARE : 690.18.I USB FW:7.3
END APC  : 2019-02-21 17:03:59 +0000
2019-02-21 17:23:31.894 [INFO ] [ipse.smarthome.model.script.APC Info] - APC UPS Named null is null, with an internal temperature of null°C,  Load = null Percent, Current Line Voltage is null Battery is at null percent, giving null minutes at current load, at null Vac. Current battery voltage null Vdc

The exact rule I’m loading is this…

	import java.util.Map
	
	rule "APC UPS String spliting"
	when
		Item APCUPSQuery_Output received update // Change this Item to your "apcaccess -u" exec query output
	 or Item testtrigger changed	// Only a test switch to force the rule to run for testing
	then 
	
		logInfo("APC Info", "APC UPS Query update = \n"+APCUPSQuery_Output.state)	// Change this Item to your "apcaccess -u" exec query output // Comment out this line to stop it populating your openhab.log file
	
		val Map<String, String> results = newHashMap
		var APCQUERY = APCUPSQuery_Output.state.toString.split('\n')
		APCQUERY.forEach[ line | 
			val split = line.split(':') // this will not work if you need a field that has ':' as part of the data, like a date/time. There is a way to split the String on just the first ':'.
			results.put(split.get(0), split.get(1))
		]
	
	//	APCname.postUpdate(results.get("NAME")) // whatever the label for Name happens to be
	//	...
	//	APCLoad.postUpdate(results.get("LOAD")) // you don't have to convert the number values to BigDecimal to send them to a Number Item.
	//	...
	
	logInfo("APC Info","APC UPS Named "+results.get("MODEL")+" is "+results.get("STATUS")+", with an internal temperature of "+results.get("ITEMP")+"°C,  Load = "+results.get("LOADPCT")+" Percent, Current Line Voltage is "+results.get("LINEV")+" Battery is at "+results.get("BCHARGE")+" percent, giving "+results.get("TIMELEFT")+" minutes at current load, at "+results.get("OUTPUTV")+" Vac. Current battery voltage "+results.get("BATTV")+" Vdc")
	
	end

OK, there is white space after the label. Use

results.put(split.get(0).trim, split.get(1).trim)

to strip out the white spaces after the label names. You need to split the white space before the value as well or else it can’t be parsed into a number.

1 Like

Indeed :smile:

This rule works perfectly :smile:

	import java.util.Map
	
	rule "APC UPS String splitting"
	when
		Item APCUPSQuery_Output received update // Change this Item to your "apcaccess -u" exec query output
//	 or Item testtrigger changed	// Only a test switch to force the rule to run for testing
	then 
	
		logInfo("APC Info", "APC UPS Query update = \n"+APCUPSQuery_Output.state)	// Change this Item to your "apcaccess -u" exec query output // Comment out this line to stop it populating your openhab.log file
	
		val Map<String, String> results = newHashMap

		var APCQUERY = APCUPSQuery_Output.state.toString.split('\n')
		APCQUERY.forEach[ line | 
			val split = line.split(':') // this will not work if you need a field that has ':' as part of the data, like a date/time. There is a way to split the String on just the first ':'.
			results.put(split.get(0).trim, split.get(1).trim)
		]
	
	//	APCname.postUpdate(results.get("NAME")) // whatever the label for Name happens to be
	//	...
	//	APCLoad.postUpdate(results.get("LOADPCT")) // you don't have to convert the number values to BigDecimal to send them to a Number Item.
	//	...
	//	APCBatteryCharge.postUpdate(results.get("BCHARGE"))
	//	APCLoad.postUpdate(results.get("LOADPCT"))
	//	APCTimeLeft.postUpdate(results.get("TIMELEFT"))
		 
    // APC_Status.postUpdate(results.get("STATUS").replace("ONLINE", "ON").replace("OFFLINE", "OFF")) // The replace section means that APC_Status can be a Switch Item
	
	logInfo("APC Info","APC UPS information.\nUPS Named "+results.get("MODEL")+" is "+results.get("STATUS")+", with an internal temperature of "+results.get("ITEMP")+"°C.\nLoad = "+results.get("LOADPCT")+" Percent, Current Line Voltage is "+results.get("LINEV")+"\nBattery is at "+results.get("BCHARGE")+" percent, giving "+results.get("TIMELEFT")+" minutes at current load, at "+results.get("OUTPUTV")+" Vac.\nCurrent battery voltage "+results.get("BATTV")+" Vdc")
	
	end 

giving this result

2019-02-22 10:10:01.757 [INFO ] [ipse.smarthome.model.script.APC Info] - APC UPS information.
UPS Named Smart-UPS 2200 XL is ONLINE, with an internal temperature of 9.4°C.
Load = 7.1 Percent, Current Line Voltage is 239.0
Battery is at 100.0 percent, giving 1020.0 minutes at current load, at 239.0 Vac.
Current battery voltage 55.4 Vdc
1 Like

I’m glad it works.

One thing to pay attention to here is the progression we took to get to the final Rule. Rarely does anyone, even very experienced programmers, build Rules like this the first time around. As with writing essays or books, the first version is rarely any good. But through iterations of refinement we go from long Rules with repeated code to shorter and more concise Rules that are easier to maintain.

The process should always be:

  1. write something and make it work
  2. refine the Rule, trying to eliminate repetitious code; don’t be afraid to change your over all approach
  3. repeat 2 until satisfied.

And, just to show another iteration of improvements, it occurs to me that the text can be processed by a Properties Object.

You would replace the part where we parse the String with a Java class that was created for processing Strings and files in this format.

	import java.io.StringReader
    import java.utils.Properties
	
	rule "APC UPS String spliting"
	when
		Item APCUPSQuery_Output received update // Change this Item to your "apcaccess -u" exec query output
//	 or Item testtrigger changed	// Only a test switch to force the rule to run for testing
	then 
	
		logInfo("APC Info", "APC UPS Query update = \n"+APCUPSQuery_Output.state)	// Change this Item to your "apcaccess -u" exec query output // Comment out this line to stop it populating your openhab.log file
	
        val results = new Properties().load(new StringReader(APCUPSQuery_Output.state.toString))
	
    	logInfo("APC Info","APC UPS information.\nUPS Named "+results.get("MODEL")+" is "+results.get("STATUS")+", with an internal temperature of "+results.get("ITEMP")+"°C.\nLoad = "+results.get("LOADPCT")+" Percent, Current Line Voltage is "+results.get("LINEV")+"\nBattery is at "+results.get("BCHARGE")+" percent, giving "+results.get("TIMELEFT")+" minutes at current load, at "+results.get("OUTPUTV")+" Vac.\nCurrent battery voltage "+results.get("BATTV")+" Vdc")
	
	end 

This is one of those places were you would have to be a Java programmer to know that these utilities are available to you, which brings up another point. It is often valuable to revisit Rules you’ve written in the past because as you gain experience and learn things, you may discover new ways to accomplish the same functionality in a better way. Steps 2 and 3 are ongoing and never stop. I can’t tell you how many times I’ve written some of my Rules. I think my presence Rules were rewritten at least six times.

1 Like

@rlkoshak, I tried to set this up today, but the Properties are not working out for me:

2019-05-14 16:53:01.950 [INFO ] [ipse.smarthome.model.script.APC Info] - APC UPS Query update =
APC      : 001,038,0982
DATE     : 2019-05-14 16:52:23 +0200
HOSTNAME : spifffilez
VERSION  : 3.14.14 (31 May 2016) debian
UPSNAME  : spifffilez
CABLE    : USB Cable
DRIVER   : USB UPS Driver
UPSMODE  : Stand Alone
STARTTIME: 2019-01-27 16:13:18 +0100
MODEL    : Back-UPS RS 900G
STATUS   : ONLINE
LINEV    : 226.0
LOADPCT  : 14.0
BCHARGE  : 100.0
TIMELEFT : 80.4
MBATTCHG : 5
MINTIMEL : 3
MAXTIME  : 0
SENSE    : Medium
LOTRANS  : 176.0
HITRANS  : 294.0
ALARMDEL : 30
BATTV    : 27.3
LASTXFER : Automatic or explicit self test
NUMXFERS : 10
XONBATT  : 2019-05-09 02:35:33 +0200
TONBATT  : 0
CUMONBATT: 65
XOFFBATT : 2019-05-09 02:35:43 +0200
LASTSTEST: 2019-05-09 02:35:33 +0200
SELFTEST : NO
STATFLAG : 0x05000008
SERIALNO : 4B1829P23945
BATTDATE : 2018-07-19
NOMINV   : 230
NOMBATTV : 24.0
NOMPOWER : 540
FIRMWARE : 879.L4 .I USB FW:L4
END APC  : 2019-05-14 16:53:01 +0200
2019-05-14 16:53:01.951 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'APC UPS String splitting': An error occurred during the script execution: null

Don’t really know what the problem could be, but perhaps the Properties().load reguires a format with “=” as a separator, instead of “:” (at least that seemed to be what they were using in an example I found).

Anyways, I stepped back to the rule posted by @MDAR, changed split(’:’) to split(’:’, 2), so it works for strings containing colon as well.

Properties should accept either. From the Javadocs:

The key contains all of the characters in the line starting with the first non-white space character and up to, but not including, the first unescaped '=' , ':'

You’ve provided absolutely no information about how it’s not working for you or if there are errors so about all I can say is “bummer.”

Is “absolutely” relative :wink:? I know it is not much, but the last line in the log (in the previous post) reads:
2019-05-14 16:53:01.951 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'APC UPS String splitting': An error occurred during the script execution: null

I tried commenting out the line assigning “results”, and the error disappeared. Of course the next log statement did not work then. Commenting out the last log statement still shows the error, so it is not the log statement that is at fault (although my output from the exec-binding is missing a few fields referenced by the log statement).

I’m on my phone and apparently it’s truncating that last line because I still don’t see that line.

That error is almost always caused by a type error.

Nothing in the line that appears to generate the error shows anything that might be a type error.

Split the line up (i.e. build the StringReader first and then create the new Properties and then call load).

Thanks @rlkoshak,

Did a little more digging. Turns out the error was caused by an incorrect import statement. Should be import java.util.Properties, not import java.utils.properties

But I cannot query the results:
[ntime.internal.engine.RuleEngineImpl] - Rule 'APC UPS String splitting': 'get' is not a member of 'Object'; line 19, column 29, length 20

in my script line 19 reads:
APC_LineVoltage.postUpdate(results.get("LINEV"))

I also tried replacing get with getProperty, but no luck. I think I will just go with the previous solution using java.util.Map.

It looks like a type problem. It should be. Try forcing the type.

val Properties results = ...