Calculate periodic consumption of gas based on meter readings

Hi all,

The main purpose of this topic is to give back after reading and using numerous posts of knowledgeable people here on the OpenHAB forum and beyond.
After searching through the internet for clear examples to calculate gas usage per time unit, I decided to build my own rules script, as there is not a comprehensive guide to build your own rules.

The main problem I am solving here is to calculate the precise amount of gas that is consumed every minute, hour and day, based on periodic readings of the gasmeter. The binding that I am using is the excellent DSMR-binding (https://docs.openhab.org/addons/bindings/dsmr1/readme.html) that reads values every 5 minutes from my ISKRA AM550 smart meter.

About the script:
I have tried to document it as extensive as I can in order to explain what I am doing.
Also I have added the ability to control the amount of logmessages using an OpenHAB number item “loglevel”

I welcome your comments and suggestions to improve it further.

// SlimmeMeter.rules - Created by Harold Horsman - February 2018

// These 3 rules can calculate the gas usage  per minute, per hour and per day.
// Trigger for usage per minute is the change of a gas meter reading value. The DSMR gasmeter binding reads the gasmeter every 5 minutes.
// The hourly rule is triggered on the whole hour and the daily is triggered at midnight.
// Each calculation takes the value of the gasmeter reading and the previous reading. The difference is then calculated
// Then the time difference between the two readings is calculated.
// Finally the usage is calculated based on the 2 values.
// After the calculation is done, the current gasmeter reading and its timestamp are stored into OpenHAB items to use for the next calculation.

// Used variables and items:
// INPUTS
// M3Meter_DeliveryM3			OH measured item from DSMR	Contains gasmeter counter value
// M3Meter_Timestamp			OH measured item from DSMR	Contains timestamp of last measurement
// LogLevel						OH item		Loglevel determines the amount of logoutput

// USED BY CALCULATIONS:
// P1GasActualUsage				internal rule variable		Used to calculate the difference between 2 gasmeter readings
// TimePeriodInMins				internal rule variable		Used to calculate the exact time difference in minutes between 2 gasmeter readings
// P1GasActualUsage				internal rule variable		Used to calculate the actual gas usage over a certain period.
// P1GasActualUsagePerMinute	internal rule variable		Used to calculate the actual gas usage per minute over a certain period.
// P1GasActualUsagePerHour		internal rule variable		Used to calculate the actual gas usage per hour over a certain period.
// P1GasActualUsagePerDay		internal rule variable		Used to calculate the actual gas usage per day over a certain period.
// newDate						internal rule variable		Used to determine the timestamp of the most recent reading and make calculations with it
// oldDate						internal rule variable		Used to determine the timestamp of the previous calculation and make calculations with it

// OUTPUTS
// P1_Gas_Actual_UsagePerMinute	OH item		Calculated delivery over last period (M3/minute)
// P1_Gas_Actual_UsagePerHour	OH item		Calculated delivery over last hour (M3/hour)
// P1_Gas_Actual_UsagePerDay	OH item		Calculated delivery over last day (M3/day)
// P1_Gas_TS_Actual_Usage		OH item		Timestamp of Calculated delivery over last period (M3/minute)
// P1_Gas_TS_Actual_Hour		OH item		Timestamp of Calculated delivery over last hour (M3/hour)
// P1_Gas_TS_Actual_Day			OH item		Timestamp of Calculated delivery over last day (M3/day)

// Import the right Java classes
// import org.joda.time.*
// import java.text.SimpleDateFormat
// import java.util.Date
// import java.time
// 2018-01-08 Note: the import of the java libraries above is not required (anymore)
//
// ---------------------------------------------------------------------------------------


// **************************************************************************************
rule "P1 Calculate Gas Usage per minute after EVERY update"
when
	Item M3Meter_DeliveryM3 changed		// Every time there is a gasmeter reading, calculate the usage per minute
then

// Calculate the difference in readings and its timestamps and then calculate the usage over that period
//	val double daymsec				= 86400000
//	val double millisperhour		= 3600000
	val double millisperminute		= 60000
	var Number P1GasActualUsagePerMinute	= 0
	var oldDate 					= new DateTime((P1_Gas_TS_Last_Value_Actual.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli)
	var newDate 					= new DateTime((M3Meter_Timestamp.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli)
// Calculate the difference between the gasmeter readings
	var Number P1GasActualUsage		= (M3Meter_DeliveryM3.state as Number) - (P1_Gas_Last_Value_Actual.state as Number)
// Calculate the period between the two timestamps (in hours)
	var Number TimePeriodInMins		= ((newDate.millis - oldDate.millis) / millisperminute)
// Check that value of TimePeriodInMins is not zero and then calculate and update the items
	if (TimePeriodInMins == 0) {
		logInfo ("SlimmeMeterRules", "UPDATE As TimePeriodInMins = 0, not updates are done")
		}
	else {
// Calculate the actual usage over the determined period (in minutes)
		P1GasActualUsagePerMinute 		= (P1GasActualUsage  / TimePeriodInMins)
//Update the output values:
		P1_Gas_Actual_UsagePerMinute.postUpdate(P1GasActualUsagePerMinute)
		P1_Gas_Last_Value_Actual.postUpdate(M3Meter_DeliveryM3.state as Number)
		P1_Gas_TS_Last_Value_Actual.postUpdate(M3Meter_Timestamp.state as DateTimeType)
		logInfo ("SlimmeMeterRules", "UPDATE P1GasActualUsagePerMinute = {}", P1GasActualUsagePerMinute)
	} // End else
end
// ---------------------------------------------------------------------------------------

// **************************************************************************************
rule "P1 Calculate Gas Usage per HOUR"
when
// cron for every hour
// cron   "S M H D M Day [Y]"
Time cron "0 0 * * * ?"			// Set CRON to run this rule every hour at HH:00

then
	logInfo ("SlimmeMeterRules", "================= START CALCULATION HOURLY VALUE ===================")
//	val double daymsec= 86400000
	val double millisperhour= 3600000
//	val double millisperminute= 60000
	var Number P1GasActualUsagePerHour = 0
// Reset previous values to some reasonable values when they are NULL
	if (P1_Gas_TS_Last_Value_Hour.state == NULL) {
			P1_Gas_TS_Last_Value_Hour.postUpdate(OH_SystemDownTime.state as DateTimeType)
		}
	else {
		if (P1_Gas_Last_Value_Hour.state == NULL) {
			P1_Gas_Last_Value_Hour.postUpdate(M3Meter_DeliveryM3.state as Number)
			logInfo ("SlimmeMeterRules", "HOUR P1_Gas_Last_Value_Hour was reset from NULL to ={}", P1_Gas_Last_Value_Actual)
			}
		}
	val oldDate = new DateTime((P1_Gas_TS_Last_Value_Hour.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli)
	val newDate = new DateTime((M3Meter_Timestamp.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli)
// Calculate the difference between the gasmeter readings
	var Number P1GasActualUsage	= ((M3Meter_DeliveryM3.state as Number) - (P1_Gas_Last_Value_Hour.state as Number))
// Calculate the period between the two timestamps (in hours)
	var Number TimePeriodInHours	= ((newDate.millis - oldDate.millis) / millisperhour)

// Check that value of TimePeriodInMins is not zero and then calculate and update the items
	if (TimePeriodInHours == 0) {
		logInfo ("SlimmeMeterRules", "HOUR As TimePeriodInHours = 0, not updates are done")
		}
	else {
// Calculate the actual usage over the determined period (in hours)
		P1GasActualUsagePerHour = (P1GasActualUsage  / TimePeriodInHours)
//Update the output values:	
		P1_Gas_TS_Last_Value_Hour.postUpdate(M3Meter_Timestamp.state as DateTimeType)
		P1_Gas_Last_Value_Hour.postUpdate(M3Meter_DeliveryM3.state as Number)
		P1_Gas_Actual_UsagePerHour.postUpdate(P1GasActualUsagePerHour)
		logInfo ("SlimmeMeterRules", "HOUR P1_Gas_Actual_UsagePerHour ={}", P1_Gas_Actual_UsagePerHour)
		} // End else
	
	logInfo ("SlimmeMeterRules", "================= END CALCULATION HOURLY VALUE ===================")	
end
//---------------------------------------------------------------------------------------

// **************************************************************************************
rule "P1 Calculate Gas Usage per DAY"
when
// cron for every day
// cron   "S M H D M Day [Y]"
Time cron "0 0 0 * * ?"				// Set CRON to run this rule every day at 00:00:00
then
	logInfo ("SlimmeMeterRules", "================= START CALCULATION DAILY VALUE ===================")
	val double daymsec= 86400000
//	val double millisperhour= 3600000
//	val double millisperminute= 60000
	var Number P1GasActualUsagePerDay = 0

//	First reset previous values to some reasonable values when they are NULL
	if (P1_Gas_TS_Last_Value_Day.state == NULL) {
		P1_Gas_TS_Last_Value_Day.postUpdate(M3Meter_Timestamp.state as DateTimeType)
		logInfo ("SlimmeMeterRules", "DAILY P1_Gas_TS_Last_Value_Day was reset from NULL to ={}", P1_Gas_TS_Last_Value_Day)
		}
	if (P1_Gas_Last_Value_Day.state == NULL) {
		P1_Gas_Last_Value_Day.postUpdate(M3Meter_DeliveryM3.state as Number)
		logInfo ("SlimmeMeterRules", "DAILY P1_Gas_Last_Value_Day was reset from NULL to ={}", P1_Gas_Last_Value_Actual)
		}
	val oldDate = new DateTime((P1_Gas_TS_Last_Value_Day.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli)
	val newDate = new DateTime((M3Meter_Timestamp.state as DateTimeType).zonedDateTime.toInstant().toEpochMilli)
// Calculate the difference between the gasmeter readings
	var Number P1GasActualUsage	= ((M3Meter_DeliveryM3.state as Number) - (P1_Gas_Last_Value_Day.state as Number))
// Calculate the period between the two timestamps (in hours)
	var Number TimePeriodInDays	= ((newDate.millis - oldDate.millis) / daymsec)
// Check that value of TimePeriodInDays is not zero and then calculate and update the items
	if (TimePeriodInDays == 0) {
		logInfo ("SlimmeMeterRules", "DAILY As TimePeriodInDays = 0, not updates are done")
		}
	else {
// Calculate the actual usage over the determined period (in days)
		P1GasActualUsagePerDay = (P1GasActualUsage  / TimePeriodInDays)
//Update the output values:	
		P1_Gas_TS_Last_Value_Day.postUpdate(M3Meter_Timestamp.state as DateTimeType)
		P1_Gas_Last_Value_Day.postUpdate(M3Meter_DeliveryM3.state as Number)
		P1_Gas_Actual_UsagePerDay.postUpdate(P1GasActualUsagePerDay)
		logInfo ("SlimmeMeterRules", "DAILY P1_Gas_Actual_UsagePerDay ={}", P1_Gas_Actual_UsagePerDay)
		} // End else
	
if (LogLevel.state > 0)		logInfo ("SlimmeMeterRules", "================= END CALCULATION DAILY VALUE ===================")	

end
//---------------------------------------------------------------------------------------
2 Likes

Your post would be SO much easier to read with code fences! :disappointed:

1 Like

Thank you Scott for pointing this out. In my first edit, I did not see any reference to making the layout better readable, but now I found it! I hope ou can read it now :slight_smile:

Yes, much better! I also suggest that you remove all references to LogLevel, and instead use logInfo and logDebug for providing multiple levels of logging within the DSL. You can then adjust the level you see in the logs with…

log:set DEBUG org.eclipse.smarthome.model.script.Rules.
log:set INFO org.eclipse.smarthome.model.script.Rules.
1 Like

The reason I used this approach, using a separate loglevel item, is that I am now able to control the loglevel from a sitemap in stead of from a command prompt. As I am a big fan of the OpenhabianPi approach of @ThomDietrich, I wanted to stay away from the inside of OH as much as possible :slight_smile:

For readability purposes I should maybe even remove all those loginfo’s. But on the other hand, everybody who sees value in this code could do that themselves as well.

You can do the same using your existing switch… just make a rule to set the logging level or use a lambda. Just options for you! And yes, it makes your code very hard to read, especially because you used single lines instead of blocks.

You are touching new areas here for me. Do you have an example of this kind of code to set the the loglevel using a rule?

And yes, it makes your code very hard to read, especially because you used single lines instead of blocks.

OK, point taken :smile:, let me brush up the code and come back with the result, later.

Some quick comments:

  • Since you only ever use the milliseconds in your calculations anyway, don’t bother to create DateTimes for oldDate and newDate. Just keep the millis.

  • Since the calculations are pretty much all identical with the main difference being the time periods and Items you should use a lambda and avoid the duplicated code. Reusable Functions: A simple lambda example with copious notes

1 Like

There are plenty of examples of using lambdas in the forum (Rich just posted a good one), but setting the log level is IMO slightly slicker (and something I could see myself using too). I created a new thread to not muddy up yours even further. :wink:

1 Like

@hhors Thank you for posting this! Accactly what I was looking for.
Did you by any chance code an update? I found these error trying this rule:

The use of wildcard imports is deprecated.
The import 'java.util.Date' is never used.
The import 'java.text.SimpleDateFormat' is never used.

also the item P1_Gas_TS_Last_Value_Actual was not in the list of Items ‘to be set’. That was easily repared of course. But now I get this:

Rule 'P1 Calculate Gas Usage per minute after EVERY update': Could not cast NULL to org.eclipse.smarthome.core.library.types.DateTimeType;

Remove all those imports. They are but needed for OH2.

The error is because one of your DateTime Items’s state is uninitialised, I. e. NULL. You cannot cast a NULL to a DateTimeType. Check for NULL and UNDEF states befire trying to use the state of an Item in cases like this.

Ok, thanx Rich, I addes the following:

        if(P1_Gas_TS_Last_Value_Actual.state == null ){
            P1_Gas_TS_Last_Value_Actual.postUpdate(new DateTimeType())
        }

Now I have to wait till I spill some gas to see what happens :wink:.

UnDefType.NULL != null… you’ll need to use NULL instead of null for this to work.

I thought '=='would solve that since that is not the same as ‘===’. But the rule doesn’t work so I’ll give it a try!

Hi @Guido_van_Haasteren, sorry for my late reply. Work has been pushing me away from OH.
To answer your question, I have a STARTUP rule that initializes every item, preventing NULL’s. Since it isn’t mentioned in the (comments of the) rule itself, I should add that.
Meanwhile I’m hoping your rule is giving you nice graphs of your gas consumption :slight_smile:

Hi Harold,
I had given up before and try again after your new post. But I still have some troubles with it. Could you post your items-file too please?

Hi @hhors, Thanks for the good work! However, I have similar issues. The script can not cast NULL to a date and then stops executing it. I need to spend time to dive into it, but please keep me posted if there is a solution available. Thanks!