Following up on my post on Dynamic sitemaps with JRuby - this post will show how to template and generate items files with JRuby.
I will use my Nuvo Home Audio system items file as the example:
Before:
Switch NuvoFavorite { channel="exec:command:nuvo_playlist:run"}
String Music_Port_Source_A_URL
String Music_Port_Source_B_URL
String Music_Port_Source_C_URL
String Music_Port_Source_D_URL
Switch RestartMPS4
Group:Switch:OR(ON, OFF) ZonePowers
Group ZoneSources
// system
Switch Nuvo_System_Alloff "All Zones Off" { channel="nuvo:amplifier:nuvo:system#alloff" }
Switch Nuvo_System_Allmute "All Zones Mute" { channel="nuvo:amplifier:nuvo:system#allmute" }
Switch Nuvo_System_Page "Page All Zones" { channel="nuvo:amplifier:nuvo:system#page" }
// zones
Switch Nuvo_Z1_Power "Power" (ZonePowers) { channel="nuvo:amplifier:nuvo:zone1#power" }
Number Nuvo_Z1_Source "Source Input [%s]" (ZoneSources) { channel="nuvo:amplifier:nuvo:zone1#source" }
Dimmer Nuvo_Z1_Volume "Volume [%d %%]" { channel="nuvo:amplifier:nuvo:zone1#volume" }
Switch Nuvo_Z1_Mute "Mute" { channel="nuvo:amplifier:nuvo:zone1#mute" }
Number Nuvo_Z1_Favorite "Favorite" { channel="nuvo:amplifier:nuvo:zone1#favorite" }
Player Nuvo_Z1_Control "Control" { channel="nuvo:amplifier:nuvo:zone2#control" }
Number Nuvo_Z1_Treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone1#treble" }
Number Nuvo_Z1_Bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone1#bass" }
Number Nuvo_Z1_Balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone1#balance" }
Switch Nuvo_Z1_Loudness "Loudness" { channel="nuvo:amplifier:nuvo:zone1#loudness" }
Switch Nuvo_Z1_Dnd "Do Not Disturb" { channel="nuvo:amplifier:nuvo:zone1#dnd" }
Switch Nuvo_Z1_Lock "Zone Locked [%s]" { channel="nuvo:amplifier:nuvo:zone1#lock" }
Switch Nuvo_Z1_Party "Party Mode" { channel="nuvo:amplifier:nuvo:zone1#party" }
Switch Nuvo_Z2_Power "Power" (ZonePowers) { channel="nuvo:amplifier:nuvo:zone2#power" }
Number Nuvo_Z2_Source "Source Input [%s]" (ZoneSources) { channel="nuvo:amplifier:nuvo:zone2#source" }
Dimmer Nuvo_Z2_Volume "Volume [%d %%]" { channel="nuvo:amplifier:nuvo:zone2#volume" }
Switch Nuvo_Z2_Mute "Mute" { channel="nuvo:amplifier:nuvo:zone2#mute" }
Number Nuvo_Z2_Favorite "Favorite" { channel="nuvo:amplifier:nuvo:zone2#favorite" }
Player Nuvo_Z2_Control "Control" { channel="nuvo:amplifier:nuvo:zone2#control" }
Number Nuvo_Z2_Treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone2#treble" }
Number Nuvo_Z2_Bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone2#bass" }
Number Nuvo_Z2_Balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone2#balance" }
Switch Nuvo_Z2_Loudness "Loudness" { channel="nuvo:amplifier:nuvo:zone2#loudness" }
Switch Nuvo_Z2_Dnd "Do Not Disturb" { channel="nuvo:amplifier:nuvo:zone2#dnd" }
Switch Nuvo_Z2_Lock "Zone Locked [%s]" { channel="nuvo:amplifier:nuvo:zone2#lock" }
Switch Nuvo_Z2_Party "Party Mode" { channel="nuvo:amplifier:nuvo:zone2#party" }
Switch Nuvo_Z3_Power "Power" (ZonePowers) { channel="nuvo:amplifier:nuvo:zone3#power" }
Number Nuvo_Z3_Source "Source Input [%s]" (ZoneSources) { channel="nuvo:amplifier:nuvo:zone3#source" }
Dimmer Nuvo_Z3_Volume "Volume [%d %%]" { channel="nuvo:amplifier:nuvo:zone3#volume" }
Switch Nuvo_Z3_Mute "Mute" { channel="nuvo:amplifier:nuvo:zone3#mute" }
Number Nuvo_Z3_Favorite "Favorite" { channel="nuvo:amplifier:nuvo:zone3#favorite" }
Player Nuvo_Z3_Control "Control" { channel="nuvo:amplifier:nuvo:zone3#control" }
Number Nuvo_Z3_Treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone3#treble" }
Number Nuvo_Z3_Bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone3#bass" }
Number Nuvo_Z3_Balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone3#balance" }
Switch Nuvo_Z3_Loudness "Loudness" { channel="nuvo:amplifier:nuvo:zone3#loudness" }
Switch Nuvo_Z3_Dnd "Do Not Disturb" { channel="nuvo:amplifier:nuvo:zone3#dnd" }
Switch Nuvo_Z3_Lock "Zone Locked [%s]" { channel="nuvo:amplifier:nuvo:zone3#lock" }
Switch Nuvo_Z3_Party "Party Mode" { channel="nuvo:amplifier:nuvo:zone3#party" }
Switch Nuvo_Z4_Power "Power" (ZonePowers) { channel="nuvo:amplifier:nuvo:zone4#power" }
Number Nuvo_Z4_Source "Source Input [%s]" (ZoneSources) { channel="nuvo:amplifier:nuvo:zone4#source" }
Dimmer Nuvo_Z4_Volume "Volume [%d %%]" { channel="nuvo:amplifier:nuvo:zone4#volume" }
Switch Nuvo_Z4_Mute "Mute" { channel="nuvo:amplifier:nuvo:zone4#mute" }
Number Nuvo_Z4_Favorite "Favorite" { channel="nuvo:amplifier:nuvo:zone4#favorite" }
Player Nuvo_Z4_Control "Control" { channel="nuvo:amplifier:nuvo:zone4#control" }
Number Nuvo_Z4_Treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone4#treble" }
Number Nuvo_Z4_Bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone4#bass" }
Number Nuvo_Z4_Balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone4#balance" }
Switch Nuvo_Z4_Loudness "Loudness" { channel="nuvo:amplifier:nuvo:zone4#loudness" }
Switch Nuvo_Z4_Dnd "Do Not Disturb" { channel="nuvo:amplifier:nuvo:zone4#dnd" }
Switch Nuvo_Z4_Lock "Zone Locked [%s]" { channel="nuvo:amplifier:nuvo:zone4#lock" }
Switch Nuvo_Z4_Party "Party Mode" { channel="nuvo:amplifier:nuvo:zone4#party" }
Switch Nuvo_Z5_Power "Power" (ZonePowers) { channel="nuvo:amplifier:nuvo:zone5#power" }
Number Nuvo_Z5_Source "Source Input [%s]" (ZoneSources) { channel="nuvo:amplifier:nuvo:zone5#source" }
Dimmer Nuvo_Z5_Volume "Volume [%d %%]" { channel="nuvo:amplifier:nuvo:zone5#volume" }
Switch Nuvo_Z5_Mute "Mute" { channel="nuvo:amplifier:nuvo:zone5#mute" }
Number Nuvo_Z5_Favorite "Favorite" { channel="nuvo:amplifier:nuvo:zone5#favorite" }
Player Nuvo_Z5_Control "Control" { channel="nuvo:amplifier:nuvo:zone5#control" }
Number Nuvo_Z5_Treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone5#treble" }
Number Nuvo_Z5_Bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone5#bass" }
Number Nuvo_Z5_Balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone5#balance" }
Switch Nuvo_Z5_Loudness "Loudness" { channel="nuvo:amplifier:nuvo:zone5#loudness" }
Switch Nuvo_Z5_Dnd "Do Not Disturb" { channel="nuvo:amplifier:nuvo:zone5#dnd" }
Switch Nuvo_Z5_Lock "Zone Locked [%s]" { channel="nuvo:amplifier:nuvo:zone5#lock" }
Switch Nuvo_Z5_Party "Party Mode" { channel="nuvo:amplifier:nuvo:zone5#party" }
Switch Nuvo_Z6_Power "Power" (ZonePowers) { channel="nuvo:amplifier:nuvo:zone6#power" }
Number Nuvo_Z6_Source "Source Input [%s]" (ZoneSources) { channel="nuvo:amplifier:nuvo:zone6#source" }
Dimmer Nuvo_Z6_Volume "Volume [%d %%]" { channel="nuvo:amplifier:nuvo:zone6#volume" }
Switch Nuvo_Z6_Mute "Mute" { channel="nuvo:amplifier:nuvo:zone6#mute" }
Number Nuvo_Z6_Favorite "Favorite" { channel="nuvo:amplifier:nuvo:zone6#favorite" }
Player Nuvo_Z6_Control "Control" { channel="nuvo:amplifier:nuvo:zone6#control" }
Number Nuvo_Z6_Treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone6#treble" }
Number Nuvo_Z6_Bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone6#bass" }
Number Nuvo_Z6_Balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone6#balance" }
Switch Nuvo_Z6_Loudness "Loudness" { channel="nuvo:amplifier:nuvo:zone6#loudness" }
Switch Nuvo_Z6_Dnd "Do Not Disturb" { channel="nuvo:amplifier:nuvo:zone6#dnd" }
Switch Nuvo_Z6_Lock "Zone Locked [%s]" { channel="nuvo:amplifier:nuvo:zone6#lock" }
Switch Nuvo_Z6_Party "Party Mode" { channel="nuvo:amplifier:nuvo:zone6#party" }
Switch Nuvo_Z7_Power "Power" (ZonePowers) { channel="nuvo:amplifier:nuvo:zone7#power" }
Number Nuvo_Z7_Source "Source Input [%s]" (ZoneSources) { channel="nuvo:amplifier:nuvo:zone7#source" }
Dimmer Nuvo_Z7_Volume "Volume [%d %%]" { channel="nuvo:amplifier:nuvo:zone7#volume" }
Switch Nuvo_Z7_Mute "Mute" { channel="nuvo:amplifier:nuvo:zone7#mute" }
Number Nuvo_Z7_Favorite "Favorite" { channel="nuvo:amplifier:nuvo:zone7#favorite" }
Player Nuvo_Z7_Control "Control" { channel="nuvo:amplifier:nuvo:zone7#control" }
Number Nuvo_Z7_Treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone7#treble" }
Number Nuvo_Z7_Bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone7#bass" }
Number Nuvo_Z7_Balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:nuvo:zone7#balance" }
Switch Nuvo_Z7_Loudness "Loudness" { channel="nuvo:amplifier:nuvo:zone7#loudness" }
Switch Nuvo_Z7_Dnd "Do Not Disturb" { channel="nuvo:amplifier:nuvo:zone7#dnd" }
Switch Nuvo_Z7_Lock "Zone Locked [%s]" { channel="nuvo:amplifier:nuvo:zone7#lock" }
Switch Nuvo_Z7_Party "Party Mode" { channel="nuvo:amplifier:nuvo:zone7#party" }
String Nuvo_S5_Display_Line1 "Track: [%s]" { channel="nuvo:amplifier:nuvo:source5#display_line1" }
String Nuvo_S5_Display_Line2 "Album: [%s]" { channel="nuvo:amplifier:nuvo:source5#display_line2" }
String Nuvo_S5_Display_Line3 "Artist: [%s]" { channel="nuvo:amplifier:nuvo:source5#display_line3" }
String Nuvo_S5_Display_Line4 "Title: [%s]" { channel="nuvo:amplifier:nuvo:source5#display_line4" }
String Nuvo_S5_Play_Mode "Play Mode: [%s]" { channel="nuvo:amplifier:nuvo:source5#play_mode" }
//Number:Time Nuvo_S5_Track_Length "Track Length: [%s s]" { channel="nuvo:amplifier:nuvo:source5#track_length" }
//Number:Time Nuvo_S5_Track_Position "Track Position: [%s s]" { channel="nuvo:amplifier:nuvo:source5#track_position" }
String Nuvo_S5_Button_Press "Button: [%s]" { channel="nuvo:amplifier:nuvo:source5#button_press" }
String Nuvo_S6_Display_Line1 "Track: [%s]" { channel="nuvo:amplifier:nuvo:source6#display_line1" }
String Nuvo_S6_Display_Line2 "Album: [%s]" { channel="nuvo:amplifier:nuvo:source6#display_line2" }
String Nuvo_S6_Display_Line3 "Artist: [%s]" { channel="nuvo:amplifier:nuvo:source6#display_line3" }
String Nuvo_S6_Display_Line4 "Title: [%s]" { channel="nuvo:amplifier:nuvo:source6#display_line4" }
String Nuvo_S6_Play_Mode "Play Mode: [%s]" { channel="nuvo:amplifier:nuvo:source6#play_mode" }
//Number:Time Nuvo_S6_Track_Length "Track Length: [%s s]" { channel="nuvo:amplifier:nuvo:source6#track_length" }
//Number:Time Nuvo_S6_Track_Position "Track Position: [%s s]" { channel="nuvo:amplifier:nuvo:source6#track_position" }
String Nuvo_S6_Button_Press "Button: [%s]" { channel="nuvo:amplifier:nuvo:source6#button_press" }
Using templates
Switch NuvoFavorite { channel="exec:command:nuvo_playlist:run"}
<% ('A'..'D').each do |source| -%>
<%= "String Music_Port_Source_#{source}_URL" %>
<% end -%>
Switch RestartMPS4
Group:Switch:OR(ON, OFF) ZonePowers
Group ZoneSources
// system
Switch Nuvo_System_Alloff "All Zones Off" { channel="nuvo:amplifier:nuvo:system#alloff" }
Switch Nuvo_System_Allmute "All Zones Mute" { channel="nuvo:amplifier:nuvo:system#allmute" }
Switch Nuvo_System_Page "Page All Zones" { channel="nuvo:amplifier:nuvo:system#page" }
// zones
<% 1.upto(7).each do |zone| -%>
<% zone_channel = "nuvo:amplifier:nuvo:zone#{zone}" -%>
<%= %Q|Switch Nuvo_Z#{zone}_Power "Power" (ZonePowers) { channel="#{zone_channel}#power" } |%>
<%= %Q|Number Nuvo_Z#{zone}_Source "Source Input [%s]" (ZoneSources) { channel="#{zone_channel}#source" } |%>
<%= %Q|Dimmer Nuvo_Z#{zone}_Volume "Volume [%d %%]" { channel="#{zone_channel}#volume" } |%>
<%= %Q|Switch Nuvo_Z#{zone}_Mute "Mute" { channel="#{zone_channel}#mute" } |%>
<%= %Q|Number Nuvo_Z#{zone}_Favorite "Favorite" { channel="#{zone_channel}#favorite" } |%>
<%= %Q|Player Nuvo_Z#{zone}_Control "Control" { channel="#{zone_channel}#control" } |%>
<%= %Q|Number Nuvo_Z#{zone}_Treble "Treble Adjustment [%s]" { channel="#{zone_channel}#treble" } |%>
<%= %Q|Number Nuvo_Z#{zone}_Bass "Bass Adjustment [%s]" { channel="#{zone_channel}#bass" } |%>
<%= %Q|Number Nuvo_Z#{zone}_Balance "Balance Adjustment [%s]" { channel="#{zone_channel}#balance" } |%>
<%= %Q|Switch Nuvo_Z#{zone}_Loudness "Loudness" { channel="#{zone_channel}#loudness" } |%>
<%= %Q|Switch Nuvo_Z#{zone}_Dnd "Do Not Disturb" { channel="#{zone_channel}#dnd" } |%>
<%= %Q|Switch Nuvo_Z#{zone}_Lock "Zone Locked [%s]" { channel="#{zone_channel}#lock" } |%>
<%= %Q|Switch Nuvo_Z#{zone}_Party "Party Mode" { channel="#{zone_channel}#party" } |%>
<%= %>
<% end %>
// zones
<% [5,6].each do |source| -%>
<% source_channel = "nuvo:amplifier:nuvo:source#{source}" -%>
<%= %Q| String Nuvo_S#{source}_Display_Line1 "Track: [%s]" { channel="#{source_channel}#display_line1" } |%>
<%= %Q| String Nuvo_S#{source}_Display_Line2 "Album: [%s]" { channel="#{source_channel}#display_line2" } |%>
<%= %Q| String Nuvo_S#{source}_Display_Line3 "Artist: [%s]" { channel="#{source_channel}#display_line3" } |%>
<%= %Q| String Nuvo_S#{source}_Display_Line4 "Title: [%s]" { channel="#{source_channel}#display_line4" } |%>
<%= %Q| String Nuvo_S#{source}_Play_Mode "Play Mode: [%s]" { channel="#{source_channel}#play_mode" } |%>
<%= %Q| //Number:Time Nuvo_S#{source}_Track_Length "Track Length: [%s s]" { channel="#{source_channel}#track_length" } |%>
<%= %Q| //Number:Time Nuvo_S#{source}_Track_Position "Track Position: [%s s]" { channel="#{source_channel}#track_position" } |%>
<%= %Q| String Nuvo_S#{source}_Button_Press "Button: [%s]" { channel="#{source_channel}#button_press" } |%>
<%= %>
<% end %>
You may noticed the funny %Q|
above. Rather than forcing you to escape double quotes with \
everywhere like other languages, ruby has an alternative syntax you can use where you can do %Q
and then choose any character you want to start and end the string, so I selected ‘|’ since I don’t have a pipe anywhere in my item definitions.
Item template rendering rule
require 'openhab'
require 'erb'
ITEMS_DIR = __conf__/'items'
# Capture everything between and including the start/ending
def capture_between(name, start, ending = start)
/(?<#{name}>#{Regexp.quote(start)}[^#{Regexp.quote(ending)}]+#{Regexp.quote(ending)})/
# Capture name, find start, anything but the ending, then ending.
end
def regex
@regex ||= begin
#Group[:itemtype[:function]] groupname ["labeltext"] [<iconname>] [(group1, group2, ...)]
group_item = /Group(?::\w+(?::\w+(?:\(.*\))?)?)?/ # Find 'Group', maybe colon followed by word, maybe colon followed by word
# maybe open parens followed by anything and a closed parens
item_type = /\S+/ # Any non whitepsace
type = /(?<type>(?:#{group_item.source})|(?:#{item_type.source}))/
name = /(?<name>[\S]+)/ # one or more non whitespace chars
label = capture_between('label', '"')
icon = capture_between('icon', '<', '>')
groups = capture_between('groups', '(', ')')
tags = capture_between('tags', '[', ']')
binding_config = capture_between('binding_config', '{', '}')
# itemtype itemname "labeltext [stateformat]" <iconname> (group1, group2, ...) ["tag1", "tag2", ...] {bindingconfig}
optionals = [label, icon, groups, tags, binding_config].map do |optional|
# zero or more whitespace
# maybe the optional
/\s*#{optional.source}?/
end.map(&:source).join('')
# start of line
# zero or more whitespace
# do not match comment lines
# type or group
# one or more whitespace
# all of the optionals
# zero or more whitespace
# end of line
/^\s*#{type.source}\s+#{name.source}#{optionals}\s*$/
end
end
def parse_item_line(line, maxes)
return line.strip if line.strip.start_with? '//' #Ignore comment lines
if (matches = regex.match(line))
matches.named_captures.compact.each do |capture, value|
maxes[capture.to_sym] = [maxes[capture.to_sym], value.length].max
end
matches
else
line.strip
end
end
def format_line(line, maxes)
return line unless line.is_a? MatchData
# itemtype itemname "labeltext [stateformat]" <iconname> (group1, group2, ...) ["tag1", "tag2", ...] {bindingconfig}
%i[type name label icon groups tags binding_config].map { |field| (line[field] || '').ljust(maxes[field] || 0) }.join(' ')
end
def format(string)
maxes = Hash.new(0)
formatted = string.lines
.map { |line| parse_item_line(line, maxes) }
.map { |line| format_line(line, maxes) }
.join("\n")
end
# Render a template
# @param source template file in sitemap dir without the .erb extension
# @param variables optional list of bindings for template, if supplied template will not have access to openhab variables
def render(source, **variables)
template = ERB.new(File.read(source), trim_mode: '-')
variables ? template.result_with_hash(variables) : template.result
end
rule 'Template Items' do
watch __conf__/'items/templates/*.erb', for: [:created, :modified]
run do |event|
file = event.path
logger.debug("Templating #{file}")
items_content = format( render(file) )
items_file = ITEMS_DIR/file.basename(file.extname)
File.write(items_file, items_content)
logger.debug("Wrote #{items_file}")
end
end
The majority of the code is regular expressions for parsing a rendered items file and producing a file that will have all the columns line up. There is probably a better way to do that, but I suck at regular expressions… so suggestions welcome.
Going method by method:
capture_between / regex
regex generates the regular expression that can parse an item line, capture between is a regex that is reused to capture between a start and ending.
parse_item_line
This parses a line in an items file using the regex, ignoring comment lines, it also updates a hash that tracks the max length of all item fields.
format_line
If the line matched the regex, it generates a new line where each column is padded so the columns line up in the items file.
format
Ties together the line parsing with the formatting to generate the final output
render
This is the same as in the sitemap example and is the code that expands the templates into the rendered version.
‘Template Items’ rule
This is executed via the watch trigger whenever any file in the templates subdirectory of items is created or modified and then renders, formats and writes the output to a file in the items directory.