Making dynamic icons work - 32-point wind rose example

Tags: #<Tag:0x00007f38920b9a38> #<Tag:0x00007f38920bff78>

I’m currently running a recent snapshot of OH2.5.

According to the docs the following should work:

windrose.items:

Number:Angle	Wx_OWM_Current_Wind_Direction	"Wind Direction [%d %unit%]"	<wind>	(gWeatherCurrent)	{channel="openweathermap:weather-and-forecast:<api_key>:local:current#wind-direction"}
String				Wx_OWM_Current_Wind_Direction_Simplified	"Wind Direction [%s]"	<wind>	(gWeatherCurrent)
Number:Dimensionless	Wx_OWM_Current_Wind_Direction_Windrose	"Wind Direction [SCALE(windrose-32.map):%s]"	<windrose>	(gWeatherCurrent)

The idea is to map the 32 wind rose points plus the “indeterminate wind direction” point to the [0, 100] range used in Number:Dimensionless items. Here 0 refers to “indeterminate wind direction” and all 32 wind rose points are evenly spaced (in steps of 3).

I also created a set of 32+2 wind rose icons for dynamic icon support. They’re located in /etc/openhab2/icons/classic/ and have the following names:

  • windrose.svg (catch-all for UNDEF state - represents unknown wind direction
  • windrose-N.svg (where N is 0 for variable wind direction, 3 for ‘North’, 6 for ‘North by East’, … 96 for ‘North by West’)

The idea is to transform a given wind direction (range 0°-360°) to a number in the range 0-32, and then to multiply this number by 3 and adding an offset of 3 so state 0 can be used for reporting indeterminate wind direction (different from unknown/undefined wind direction, as those correspond to an error states). In other words:

  1. windDirectionDegrees := (Wx_OWM_Current_Wind_Direction.state as Number).doubleValue
  2. windDirectionForWindrose := windDirectionDegrees / 360.0 * 32.0
  3. windDirectionWindroseDynamicIconState := 3 + (3 * Math.round( windDirectionForWindrose )
  4. postUpdate(Wx_OWM_Current_Wind_Direction_Windrose, windDirectionWindroseDynamicIconState)

(I’m using Math.round() here as .intValue truncates a floating point number (as with Math.floor()) rather than rounding it to the nearest integer.

windrose-32.map:

0=Variable
3=N
6=NbE
9=NNE
12=NEbN
15=NE
18=NEbE
21=ENE
24=EbN
27=E
30=EbS
33=ESE
36=SEbE
39=SEbE
42=SEbS
45=SSE
48=SbE
51=S
54=SbW
57=SSW
60=SWbS
63=SWbS
66=SWbW
69=WSW
72=WbS
75=W
78=WbN
81=WNW
84=NWbW
87=NWbW
90=NWbN
93=NNW
96=NbW

windrose-32.rule:

/* Provide the compass wind direction in a proxy Item (Wx_OWM_Current_Wind_Direction_Simplified) for displaying on the sitemap.
 * This is needed as the SCALE transform only accepts numeric values, whereas the OWM binding sometimes reports non-numeric values
 * in cases where the wind direction is undefined or hard to define.
 */
rule "Update simplified wind direction"
when
	Item Wx_OWM_Current_Wind_Direction changed
then
	val String ruleTitle = "UpdateOWMCurrentWindDirection"

	// Update Wx_OWM_Current_Wind_Direction_Simplified if Wx_OWM_Wind_Speed is numeric
	var String status = ""	// Will hold the textual representation of the wind direction (Wx_OWM_Current_Wind_Direction_Simplified)
	var boolean error = true
	var int windrose = 0 	// Will hold the integer state for using dynamic 32-point windrose icon representing the wind direction (can also be used in a MAP(windrose-32.map) transform)

	if (Wx_OWM_Current_Wind_Direction.state == NULL) {
		status = "(not set)"
	} else if (Wx_OWM_Current_Wind_Direction.state == UNDEF) {
		status = "(undefined)"
	} else if (Wx_OWM_Current_Wind_Direction.state.toString == "-") {
		status = "(-)"
	} else if (Wx_OWM_Current_Wind_Direction.state instanceof Number) {
		logInfo(ruleTitle, "Wx_OWM_Current_Wind_Direction has state '{}' - will be transformed", Wx_OWM_Current_Wind_Direction.state)
		// We can now safely use the SCALE() transform as the input is a Number
		status = transform("SCALE", "wind.scale", Wx_OWM_Current_Wind_Direction.state.toString())

		// Now we will compute the 32-point wind rose offset for use with dynamic icons
		// First convert the wind direction (degrees) to a scale of 0-32:
		val Number x =  (Wx_OWM_Current_Wind_Direction.state as Number).doubleValue / (360.0 / 32.0)
		logInfo(ruleTitle, "Wx_OWM_Current_Wind_Direction has state '{}' - transformed to '{}' (x = {})",
			Wx_OWM_Current_Wind_Direction.state, status, x)
		// Now we round this windrose value to the nearest integer
		val int y = Math.round(x.floatValue)
		logInfo(ruleTitle, "Wx_OWM_Current_Wind_Direction has state '{}' - transformed to '{}' (y = {})",
			Wx_OWM_Current_Wind_Direction.state, status, y)
		// Now we compute the dynamic state offset value (multiply by 3 and add start offset of 3). We also handle negative degrees by adding 32 to the windrose integer value (and use modulo calculus to get back to the range [0..32[)
		windrose = ((y + 32) % 32) * 3 + 3
		logInfo(ruleTitle, "Wx_OWM_Current_Wind_Direction has state '{}' - transformed to '{}' (x = {}) (y = {}) (windrose value {})",
			Wx_OWM_Current_Wind_Direction.state, status, x, y, windrose)
		error = false
	} else { // Unexpected state type
		status = "(invalid: " + Wx_OWM_Current_Wind_Direction.state.toString() + ")"
	}
	if (error) {
		logInfo(ruleTitle, "{} has state '{}', expecting Number", Wx_OWM_Current_Wind_Direction.name, Wx_OWM_Current_Wind_Direction.state.toString())
	} else {
		logDebug(ruleTitle, "{} has Number state '{}'", Wx_OWM_Current_Wind_Direction.name, Wx_OWM_Current_Wind_Direction.state.toString())
	}
	postUpdate(Wx_OWM_Current_Wind_Direction_Simplified, status)
	postUpdate(Wx_OWM_Current_Wind_Direction_Windrose, windrose.intValue.toString)
end

This, in theory, should work.

However, adding the following in the sitemap doesn’t yield the expected result:

// The followng doesn't properly handle 'UNDEF' state, hence replaced with Wx_OWM_Current_Wind_Direction_Simplified:
// Default item=Wx_OWM_Current_Wind_Direction
// Computed by means of a rule whenever Wx_OWM_Current_Wind_Direction changes;
Default item=Wx_OWM_Current_Wind_Direction_Simplified
Default item=Wx_OWM_Current_Wind_Direction_Windrose

Instead, I get an error:

2019-06-14 13:58:21.570 [WARN ] [rm.AbstractFileTransformationService] - Could not transform '51.0' with the file 'windrose.map' : Target value not found in map for '51.0'

This happens, irrespective of the type used in postUpdate(). I tried:

  1. postUpdate(Wx_OWM_Current_Wind_Direction_Windrose, windrose) → doesn’t work (yields float)
  2. postUpdate(Wx_OWM_Current_Wind_Direction_Windrose, windrose.intValue) → doesn’t work (yields float)
  3. postUpdate(Wx_OWM_Current_Wind_Direction_Windrose, windrose.intValue.toString) → doesn’t work (yields float)

I eventually generated a SCALE() transform so I would at least already get the wind directions mapped to the textual representations of the wind direction - here’s windrose-32.scale:

[0..1.5]=(var)
]1.5..4.5]=N
]4.5..7.5]=NbE
]7.5..10.5]=NNE
]10.5..13.5]=NEbN
]13.5..16.5]=NE
]16.5..19.5]=NEbE
]19.5..22.5]=ENE
]22.5..25.5]=EbN
]25.5..28.5]=E
]28.5..31.5]=EbS
]31.5..34.5]=ESE
]34.5..37.5]=SEbE
]37.5..40.5]=SEbE
]40.5..43.5]=SEbS
]43.5..46.5]=SSE
]46.5..49.5]=SbE
]49.5..52.5]=S
]52.5..55.5]=SbW
]55.5..58.5]=SSW
]58.5..61.5]=SWbS
]61.5..64.5]=SWbS
]64.5..67.5]=SWbW
]67.5..70.5]=WSW
]70.5..73.5]=WbS
]73.5..76.5]=W
]76.5..79.5]=WbN
]79.5..82.5]=WNW
]82.5..85.5]=NWbW
]85.5..88.5]=NWbW
]88.5..91.5]=NWbN
]91.5..94.5]=NNW
]94.5..97.5]=NbW

Now the 32-point wind rose names are correctly rendered, but the dynamic state icons still won’t work. In fact, none is ever shown.

What am I doing wrong?

It’s an avoidance, but if you have a String Item with values like “SSE” you can have icons windrose-sse.svg

If you get no dynamic icons check the default bitmap/vector setting in your PaperUI for BasicUI/ClassicUI

Ok, I now realize that it works in a regular web browser but not in the iPhone app.

Unsure how I could make it work in the iPhone app though.

I think thats an unsolved issue in the iphone app.

1 Like

It appears that the iOS app only supports PNG icon file format.

If I convert the SVG icons to PNG with ImageMagick (just using convert icon.svg icon.png), then I get 50 by 50 PNG icons, but they initially don’t show up in the app.

I then performed 2 rounds of the following routine before the PNG icons eventually started to appear:

  1. $ sudo openhab-cli clean-cache
  2. $ sudo systemctl restart openhab2
  3. $ sudo systemctl restart openhab2

FWIW, I noticed that openHAB 2.5.0~S1601-1 (Build #1601) which I am currently running requires a double restart after cleaning the cache as not all bundles are properly restored in one run:

2019-06-14 21:46:02.578 [ERROR] [core.karaf.internal.FeatureInstaller] - Failed installing ‘openhab-misc-openhabcloud, openhab-transformation-javascript, openhab-persistence-influxdb, openhab-ui-habpanel, openhab-transformation-regex, openhab-binding-buienradar, openhab-transformation-jsonpath, openhab-binding-zwave, openhab-ui-restdocs, openhab-transformation-xpath, openhab-binding-mqtt, openhab-ui-basic, openhab-transformation-map, openhab-ui-classic, openhab-binding-openweathermap, openhab-binding-tradfri, openhab-binding-astro, openhab-ui-habmin, openhab-transformation-xslt, openhab-binding-gpstracker, openhab-transformation-exec, openhab-ui-paper, openhab-transformation-scale’: Error restarting bundles:
Exception in org.eclipse.smarthome.io.rest.sse.internal.SseActivator.start() of bundle org.openhab.core.io.rest.sse.