Integration of Energy-Chart Electricity Traffic Light (Stromampel) into OpenHab

The following shows how to integrate an electricity traffic light (German: Stromampel) into OpenHab. The purpose is to optimize use of renewable energy, by starting power-hungry devices (washing machines, etc.) only when the share of renewables in the grid is high.

The website energy-charts.info provides information on energy production and spot market prices and also makes the data available through an API. The Mastodon account stromampelbot@climatejustice.social uses the data to make nice charts like this:

The following shows how to access and use the same data in OpenHab. Note that data on renewable shares is currently only available for Germany, Austria and France.

.items

String iTrafficLight_raw "Raw JSON: [%s]" 
Number iCurrentPowerTrafficLight "Stromampel: [MAP(stromampel.map):%s]" <energy>
Number iNextPowerTrafficLight "Nächste Ampelphase: [MAP(stromampel.map):%s]" <energy>
DateTime iNextPowerPeriodStart "Neue Phase ab: [%1$tH:%1$tM]" <time>

.rules (don’t forget to adapt country in sendGetHttpGetRequest if you’re not in Germany)

// The following rules makes use of https://api.energy-charts.info to tell users when best to use power 

rule "Get new power traffic light data"
when 
	Time cron "0 0 2/6 ? * *"        // update every six hours, offset at 2AM (forecast data available around 19:00 give or take)
then
	var response = sendHttpGetRequest("https://api.energy-charts.info/traffic_signal?country=de")
	// some day introduce some checks on response here...
	iTrafficLight_raw.postUpdate(response)
end

rule "Update Power traffic light"
    when
		Time cron "0 */15 * ? * *"        // update at every 15th minute
    then
        var String input_string = (iTrafficLight_raw.state as StringType).toString

        val json_arr_length = Integer::parseInt(transform("JSONPATH", "$.[1].data.length()", input_string))

		// find index corresponding to current time
		val long now_epoch = now.toInstant.toEpochMilli
		var long data_epoch = 0
		var now_index = -1
        var i = 0
		var boolean flag = true
        while(flag) {
			data_epoch = Long::parseLong(transform("JSONPATH", "$.[0].xAxisValues[" + i + "]", input_string))
			if(Math::abs(data_epoch - now_epoch) < 450000) {
				now_index = i
				flag = false
			}
			if (i == (json_arr_length - 1)) {
				flag = false
			}
			i = i + 1
		}
		
		// get current status
		val current_signal = Integer::parseInt(transform("JSONPATH", "$.[1].data[" + now_index + "]", input_string))
		iCurrentPowerTrafficLight.postUpdate(current_signal)
				
		// stop here and return if we hit the end of time			
		if (i >= json_arr_length) {
				iNextPowerTrafficLight.postUpdate(3)
				iNextPowerPeriodStart.postUpdate(now)
				return
		}

		// now check for next status and time of next status - starting from index i above
		var data_signal = 0
		var next_period_index = 0
		flag = true
        while(flag) {
			data_signal = Integer::parseInt(transform("JSONPATH", "$.[1].data[" + i + "]", input_string))
			if(data_signal != current_signal) {
				next_period_index = i
				flag = false
			}
			if (i == (json_arr_length - 1)) {
				flag = false
				if(data_signal != current_signal) {
					// nothing changes until end of time, so we don't know next phase
					data_signal = 3 // set to unknown
					next_period_index = i // slightly misleading as this is not the next phase, but just the end of time
				}
			}
			i = i + 1
		}
		
		val next_signal = data_signal
		val next_period_epoch = Long::parseLong(transform("JSONPATH", "$.[0].xAxisValues[" + next_period_index + "]", input_string))
		val next_period_time = Instant.ofEpochMilli(next_period_epoch).atZone(ZoneId.systemDefault()) 

		iNextPowerTrafficLight.postUpdate(next_signal)
		iNextPowerPeriodStart.postUpdate(new DateTimeType(next_period_time))

end

stromampel.map (all states are displayed as full circle; color is added in sitemap)

0=\u2B24
1=\u2B24
2=\u2B24
3=\u2B24
NULL=unknown

.sitemap

Text item=iCurrentPowerTrafficLight valuecolor=[>2="white",==2="green",==1="yellow",<=1="red"]
Text item=iNextPowerPeriodStart 
Text item=iNextPowerTrafficLight valuecolor=[>2="white",==2="green",==1="yellow",<=1="red"]

The code has been running for a week now on my system and works well so far. Let me know if you bump into any issues. :slightly_smiling_face:

2 Likes

Thanks for posting!

I like to encourage all tutorials and solutions publishers to consider using a rule template for their rules. This allows end users to just install and configure the rule instead of needing to copy/paste/edit.

1 Like

Thanks for the hint - I wasn’t even aware of this.

I only scanned over your linked post, but do I understand correctly, that this only applies to UI-based rules? (" Only UI based rules are suitable for rule templates.") In that case, I would put this on the back-burner, because I will need to be forced at gun-point to switch to UI-based configuration. :wink:

Yes, rule templates are only supported through managed rules.

I’m not going to try to convince you one way or the other. Obviously to what works best for you. But I’ve only heard two rather edge use case reasons why people stick to file based configs that are not either missunderstanting/misinterpretation of how managed configs are actually implemented and work or based on completely false beliefs that come from struggles with UI tools that are not OH (including source controlling configs).

One reason is that I’ve started back in the days of OH 1.8 and my system is close to “peak functionality” of what I can currently imagine. I’m currently too lazy/busy to do the transition and too afraid of breaking important stuff in the process. Maybe one day, when the kids have all moved out and I get bored.

Me too! 1.6 actually.

Lots of people will spin up an experimental instance and mess around there. There’s no fear of breaking things there.

Over time, more and more new features will only be possible through managed configs. Already we have rule templates and custom MainUI widgets. I fear that too many die hard text file config users will be unnecessarily left behind and miss out on new features.

Even on one “production” instance of OH you can use the disable feature to disable the old, work on the new, disable the new and reenable the old when done until you know the new works like you need it to. You’d have to be particularly careless to break something that takes more than a couple mouse clicks to recover from and try again.

Again, I’m not trying to convince you one way or the other. But I am careful that a false impression of managed configs doesn’t persist on the forum. Far to many think managed configs cannot be saved to git, require lots and lots of clicking around, are hard to edit and adjust, and lots of other “can’ts” which are not true.

Fear the day, when I get bored and come back to haunt you with stupid or nasty questions… :joy:

In any case, thanks for nudging me.

While I have you on the line: thanks a lot for all your advice and contributions here on the forum. I can’t say how much I’ve learned from you and how often your posts have helped me out. Thanks!!

Bring them! It’s my favorite type of post to reply on.

When/if you do, be sure to check out the existing rule templates. It’s probably a bad example, but about 80% of all my rules are handled by rule templates now. It’s a bad example because most of them are templates I wrote.

But I try to make them low level and flexible to handle lots of different use cases. For example, I’ve a bunch of instances of my Threshold Alert rule to:

  • alert me when the humidity in a given room gets too low
  • turn on/off smart plugs that control dumb humidifiers
  • alert me when a door has been left open for too long
  • alert me when a sensor has not reported for too long
  • alert me when a service related to home automation is offline for too long
  • alert me if the motion detector at my dad’s house doesn’t detect motion for too long a time
  • alert me when a battery reading gets too low

That’s a lot of milage out of one rule template and that’s my goal. I want end users to be able to mostly just have to worry about what to do (alert, turn on/off a switch, etc) when a condition is met but let the rule template(s) handle the detection.

1 Like

I’m using a fully UI-based production system and have no experience whatsoever with file-based configuration and I have no sitemaps. But I’m very interested in this energy-chart and have no idea how to recreate this in the UI, so I whipped up a brand new openhabian-image to test this on a spare RPI4.

First of all the bindings JSONPATH transformation and MAP transformation need to be installed, this doesn"t come ‘out of the box’ on a fresh install.
After booting the machine, Openhab first complains about Configuration model ‘stromampel.sitemap’, Script execution of rule with UID ‘StromAmpel-2’ and Failed transformations:

14:13:30.193 [WARN ] [del.core.internal.ModelRepositoryImpl] - Configuration model 'stromampel.sitemap' is either empty or cannot be parsed correctly!
14:15:00.882 [ERROR] [.internal.handler.ScriptActionHandler] - Script execution of rule with UID 'StromAmpel-2' failed: Could not cast NULL to org.openhab.core.library.types.StringType; line 16, column 36, length 37 in StromAmpel
14:23:06.360 [WARN ] [.rest.core.item.EnrichedItemDTOMapper] - Failed transforming the state '1.0' on item 'iNextPowerTrafficLight' with pattern 'MAP(stromampel.map):%s': Target value not found in map for '1.0'
14:23:06.393 [WARN ] [.rest.core.item.EnrichedItemDTOMapper] - Failed transforming the state '0.0' on item 'iCurrentPowerTrafficLight' with pattern 'MAP(stromampel.map):%s': Target value not found in map for '0.0'
14:23:20.118 [WARN ] [.rest.core.item.EnrichedItemDTOMapper] - Failed transforming the state '1.0' on item 'iNextPowerTrafficLight' with pattern 'MAP(stromampel.map):%s': Target value not found in map for '1.0'
14:23:20.128 [WARN ] [.rest.core.item.EnrichedItemDTOMapper] - Failed transforming the state '0.0' on item 'iCurrentPowerTrafficLight' with pattern 'MAP(stromampel.map):%s': Target value not found in map for '0.0'

After manually triggering the rules ‘Get new power traffic light data’ and ‘Update Power traffic light’ it fills the items with data, and the errors and warnings go away:

14:23:29.142 [INFO ] [openhab.event.ItemStateChangedEvent  ] - Item 'iTrafficLight_raw' changed from NULL to [{"name":{"en":"Renewable Share of Load"},"data":[21.9,21.8,22.1,22.5,23.1,23.1,23.1,23.3,23.8,24.3,24.7,24.6,24.4,24.4,24.6,24.7,24.7,24.3,24.3,24.2,23.7,23.4,23.0,22.3,21.0,20.9,21.5,22.3,23.3,24.9,26.6,28.9,31.7,34.1,36.6,39.4,42.9,45.7,48.5,51.5,55.5,58.5,60.6,63.0,64.8,67.0,68.4,69.8,71.1,71.8,72.9,73.7,74.0,74.7,74.9,75.3,75.4,74.9,74.6,73.8,72.5,71.0,69.5,67.9,65.9,63.3,60.9,57.9,54.2,51.0,47.1,43.2,39.1,35.7,32.1,28.8,26.5,24.4,22.9,21.8,21.0,20.6,20.5,20.6,20.7,20.9,21.2,21.7,21.4,21.6,21.9,21.9,22.4,22.3,22.5,22.6],"xAxisValues":[1692655200000,1692656100000,1692657000000,1692657900000,1692658800000,1692659700000,1692660600000,1692661500000,1692662400000,1692663300000,1692664200000,1692665100000,1692666000000,1692666900000,1692667800000,1692668700000,1692669600000,1692670500000,1692671400000,1692672300000,1692673200000,1692674100000,1692675000000,1692675900000,1692676800000,1692677700000,1692678600000,1692679500000,1692680400000,1692681300000,1692682200000,1692683100000,1692684000000,1692684900000,1692685800000,1692686700000,1692687600000,1692688500000,1692689400000,1692690300000,1692691200000,1692692100000,1692693000000,1692693900000,1692694800000,1692695700000,1692696600000,1692697500000,1692698400000,1692699300000,1692700200000,1692701100000,1692702000000,1692702900000,1692703800000,1692704700000,1692705600000,1692706500000,1692707400000,1692708300000,1692709200000,1692710100000,1692711000000,1692711900000,1692712800000,1692713700000,1692714600000,1692715500000,1692716400000,1692717300000,1692718200000,1692719100000,1692720000000,1692720900000,1692721800000,1692722700000,1692723600000,1692724500000,1692725400000,1692726300000,1692727200000,1692728100000,1692729000000,1692729900000,1692730800000,1692731700000,1692732600000,1692733500000,1692734400000,1692735300000,1692736200000,1692737100000,1692738000000,1692738900000,1692739800000,1692740700000],"xAxisFormat":"unixTime","date":1692706085986},{"name":{"en":"Color"},"comment":"0: Red, 1: Yellow, 2: Green","data":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]
14:23:38.292 [INFO ] [openhab.event.ItemStateChangedEvent  ] - Item 'iCurrentPowerTrafficLight' changed from 0.0 to 2
14:23:38.338 [INFO ] [openhab.event.ItemStateChangedEvent  ] - Item 'iNextPowerPeriodStart' changed from NULL to 2023-08-22T17:00:00.000+0200

So far so good I would say, BUT:
How on earth can I display this in OH4? From the docs I understand that main web UI is not currently able to display sitemaps. From searching the forum I learned that it should be available at http://MyHost:8080/basicui/app?sitemap=MySitemapFile, so http://MyHost:8080/basicui/app?sitemap=stromampel. However, this page just tells me ‘It seems like you have not defined any sitemaps yet.’

Am I missing something here?

Hi Frans, from the log warning I understand that it doesn’t like your sitemap file. If you just copied the sitemap-lines into a file, you might be missing the “envelope”. Try this maybe and save as stromampel.sitemap

sitemap stromampel label="Stromampel"
{
	Frame label="Stromampel" {
		Text item=iCurrentPowerTrafficLight valuecolor=[>2="white",==2="green",==1="yellow",<=1="red"]
		Text item=iNextPowerPeriodStart 
		Text item=iNextPowerTrafficLight valuecolor=[>2="white",==2="green",==1="yellow",<=1="red"]
	}
}

If you go to the URL you stated above you should see something like this:

That did the trick, thank you so much! Now I can carefully integrate this into my productive system :slightly_smiling_face:

1 Like

This seems similar in spirit to using WattTime, which I think is US/Canada-centric.

Yes, absolutely the same in spirit. :+1: Just different coverage areas and, if I understand correctly, the Wattime free plan doesn’t provide forecasts, which I feel is quite important for decision making. (Do I turn on the washing machine now, or do I wait a couple of hours for the green light?)

Just wondering, would it be possible to extract a ‘next green light start time’ item from this data?

Yes and no. That was also my initial plan, but I dropped it, after looking at the daily charts of the StromAmpelBot: https://masto.ai/@stromampelbot@climatejustice.social

At least in the summer there is one green phase over the day. When you get the forecast in the evening you might already “see” the next green phase of the following day, but that’s still far away. In other words, most of the time the next green phase will be either unknown or will take a long time. That’s why I didn’t implement it.

You can however extend the update-rule by adding a third while-loop, with a copy of the second while-loop. Replace the check for the exit-conditon

			if(data_signal != current_signal) {
				next_period_index = i
				flag = false
			}

with

			if(data_signal != 2) {
				next_period_index = i
				flag = false
			}

A lot of checks and details missing here, just to show how this could be done.

Thanks. I guess you’re right. To have an overview and a prognosis, putting this page in a webframe card also does the trick: Energy-Charts

Here are the settings for oh-list-card

- component: oh-list-card
  config: {}
  slots:
	default:
	  - component: oh-label-item
		config:
		  title: Power Traffic Light
	  - component: oh-label-item
		config:
		  action: analyzer
		  actionAnalyzerItems:
			- iCurrentPowerTrafficLight
		  fallbackIconToInitial: true
		  icon: oh:energy_flash
		  item: iCurrentPowerTrafficLight
		  title: iCurrentPowerTrafficLight
		  style:
			--f7-list-item-after-text-color: '=Number.parseInt(items.iCurrentPowerTrafficLight.state)
			  > 2 ? "white" :
			  Number.parseInt(items.iCurrentPowerTrafficLight.state)
			  == 2 ? "green" :
			  Number.parseInt(items.iCurrentPowerTrafficLight.state)
			  == 1 ? "yellow" :
			  Number.parseInt(items.iCurrentPowerTrafficLight.state)  <
			  1 ? "red":"white"'
	  - component: oh-label-item
		config:
		  fallbackIconToInitial: true
		  icon: oh:time
		  item: iNextPowerTrafficLightPeriodStart
		  title: iNextPowerTrafficLightPeriodStart
	  - component: oh-label-item
		config:
		  action: analyzer
		  actionAnalyzerItems:
			- iNextPowerTrafficLight
		  fallbackIconToInitial: true
		  icon: oh:energy_flash
		  item: iNextPowerTrafficLight
		  title: iNextPowerTrafficLight
		  style:
			--f7-list-item-after-text-color: '=Number.parseInt(items.iNextPowerTrafficLight.state)
			  > 2 ? "white" :
			  Number.parseInt(items.iNextPowerTrafficLight.state)
			  == 2 ? "green" :
			  Number.parseInt(items.iNextPowerTrafficLight.state)
			  == 1 ? "yellow" :
			  Number.parseInt(items.iNextPowerTrafficLight.state)  <
			  1 ? "red":"white"'
2 Likes