Another Things and Items file Generator

I wrote an items and things file generator in Python last year, because I just learned Python back then. This tool has been extremely useful to me, and I have been using it up until today.

I have rewritten this tool in Ruby, keeping only the main idea and the templates, which had to be modified to match the new templating engine. Note to use this, you just need the ruby interpreter (usually already installed on MacOS and Linux). I believe it will work with any ruby version without any external dependencies, but I’ve only tested it with 2.6 and 3.0.

Features

  • Things channels and Items fields output are nicely aligned
  • Use the power of Ruby inside the erb template
  • Includes helper methods to make template writing easier
  • Easily add default icon, groups, tags, and metadata
  • Be as flexible or as simple as desired
  • It can be used from the command line or from another ruby script, e.g. within a openhab-jruby rule

How do I use it? I have one BIG yaml file called devices.yaml. This one file contains the list of all the things/devices to be generated by their corresponding templates, so it contains multiple things one after another. They all get generated into ‘generated.things’ and ‘generated.items’.

I have other custom / hand edited things and items files too for unique one off items like the central air conditioning system.

Usage

Command Line Usage

Execute itemsgen.rb from the command line

$ chmod 755 itemsgen.rb
$ ./itemsgen.rb devices.yaml -f

The command line options will be printed out with -h:

Usage: ./itemsgen.rb [options] yamlfile
    -t, --things THINGS_FILE         The path to things file output
    -i, --items  ITEMS_FILE          The path to items file output
    -v, --verbose                    Print details
    -n, --dry-run                    Run process but do not write to output files
    -f, --force                      Overwrite output files
    -d, --template-dir PATH          Path to look for the template files
    -h, --help                       Print this help

When the output files are specified inside the yaml file, the -t and -i options are not required. When provided, they will override the yaml, however.

When template-dir is not specified, it will look in the templates/ subdirectory relative to the yaml file.

Template Example

Simple:

YAML data:

MasterBathRoom_Thermometer:
  template: aqara-thermometer
  groups:
    - gMasterBathRoom

Template:

Thing mqtt:topic:mosquitto:<%= thingid %> (mqtt:broker:mosquitto)  @ "Environment" {
    Channels:
        Type number : temperature  [ stateTopic="zigbee/<%= thingid %>/temperature", unit="°C" ]
        Type number : humidity     [ stateTopic="zigbee/<%= thingid %>/humidity", unit="%" ]
        Type number : pressure     [ stateTopic="zigbee/<%= thingid %>/pressure", unit="mbar" ]
        Type number : linkquality  [ stateTopic="zigbee/<%= thingid %>/linkquality" ]
        Type contact: availability [ stateTopic="zigbee/<%= thingid %>/availability", on="online", off="offline" ]
        Type number : battery      [ stateTopic="zigbee/<%= thingid %>/battery", unit="%" ]
}

// Template: <%= template_name %>

Group <%= name %> "<%= label %>" <%= make_groups groups %> <%= make_tags 'Sensor' %>
<%# Must add one extra line after the closing tag %>

Number:Temperature <%= name_parts[0] %>_Temperature "<%= room %> Temperature" <temperature> <%= make_groups name, temperature['groups'], 'gTemperature' %> <%= make_tags %w[Measurement Temperature], temperature['tags'] %> { channel="mqtt:topic:mosquitto:<%= thingid %>:temperature", ga="TemperatureSensor", alexa="TemperatureSensor.temperature"<%= add_metadata temperature['metadata'] %> }
Number             <%= name_parts[0] %>_Humidity "<%= room %> Humidity [%.1f%%]" <humidity> <%= make_groups name, humidity['groups'], 'gHumidity' %> <%= make_tags %w[Measurement Humidity], humidity['tags'] %> { channel="mqtt:topic:mosquitto:<%= thingid %>:humidity", alexa="CurrentHumidity"<%= add_metadata humidity['metadata'] %> }
Number:Pressure    <%= name_parts[0] %>_Pressure "<%= room %> Pressure" <%= make_groups name, pressure['groups'] %> <%= make_tags %w[Measurement Pressure], pressure['tags'] %> { channel="mqtt:topic:mosquitto:<%= thingid %>:pressure"<%= add_metadata pressure['metadata'] %> }
Number             <%= name %>_Link    "<%= label %> Link" <network>  <%= make_groups name, 'gSignalStrength' %> { channel="mqtt:topic:mosquitto:<%= thingid %>:linkquality" }
Number             <%= name %>_Battery "<%= label %> Battery [%d%%]" <battery> <%= make_groups name, 'gBatteries' %> { channel="mqtt:topic:mosquitto:<%= thingid %>:battery" }
Contact            <%= name %>_Availability "<%= label %> Availability [MAP(availability.map):%s]" <%= make_groups name, 'gAvailability' %> { channel="mqtt:topic:mosquitto:<%= thingid %>:availability" }

More complex:

YAML:

MasterBedRoom_Light:
  template: tasmota-light
  groups: # Common group that applies to the light
    - gMasterBedRoom
  power:
    groups: # Groups that apply only to the "Power" item
      - gInsideLights
      - gPresenceSimulators
  color: # This lightbulb supports color feature
  ct: # This lightbulb supports color temperature

Template:

Thing mqtt:topic:mosquitto:<%= thingid %> "<%= label %>" (mqtt:broker:mosquitto) {
    Channels:
        Type switch : power	     [ stateTopic="stat/<%= thingid %>/RESULT", transformationPattern="REGEX:(.*POWER.*)∩JSONPATH:$.POWER", commandTopic="cmnd/<%= thingid %>/POWER" ]
        Type dimmer : dimmer     [ stateTopic="stat/<%= thingid %>/RESULT", transformationPattern="REGEX:(.*Dimmer.*)∩JSONPATH:$.Dimmer", commandTopic="cmnd/<%= thingid %>/Dimmer" ]
<% if key? 'ct' %>
        Type dimmer : ct         [ stateTopic="stat/<%= thingid %>/RESULT", transformationPattern="REGEX:(.*CT.*)∩JSONPATH:$.CT", commandTopic="cmnd/<%= thingid %>/CT", min=153, max=500, step=1 ]
<% end %>

<% if key? 'color' %>
        Type string : color      [ stateTopic="stat/<%= thingid %>/RESULT", transformationPattern="REGEX:(.*Color.*)∩JSONPATH:$.Color", commandTopic="cmnd/<%= thingid %>/Color" ]
        Type colorHSB : colorhsb [ stateTopic="stat/<%= thingid %>/RESULT", transformationPattern="REGEX:(.*HSBColor.*)∩JSONPATH:$.HSBColor", commandTopic="cmnd/<%= thingid %>/HSBColor" ]
<% end %>
        Type number : rssi	                [ stateTopic="tele/<%= thingid %>/STATE", transformationPattern="JSONPATH:$.Wifi.RSSI" ]
        Type string : state      [ stateTopic="tele/<%= thingid %>/dummy", commandTopic="cmnd/<%= thingid %>/STATE" ]
        Type string : ipaddress [ stateTopic="stat/<%= thingid %>/STATUS5", transformationPattern="JSONPATH:$..StatusNET.IPAddress", commandTopic="cmnd/<%= thingid %>/status" ]
        Type contact: availability [ stateTopic="tele/<%= thingid %>/LWT", on="Online", off="Offline" ]
}

<%

# Based on `omit_assistant_metadata` (from yaml), conditionally return
# the given array or an empty array
assistant = {
  light: %w[ga="Light" alexa="Endpoint.Light"],
  brightness: %w[ga="lightBrightness" alexa="BrightnessController.brightness"],
  power: %w[ga="lightPower" alexa="Powercontroller.powerState"],
  ct: %w{ga="lightColorTemperature" alexa="ColorTemperatureController.colorTemperatureInKelvin" [increment="10"]},
  color: %w[ga="lightColor" alexa="ColorController.color"]
}.transform_values { |metadata| omit_assistant_metadata ? [] : metadata }
%>
// Template: <%= template_name %>

Group <%= name %> "<%= label %>" <light> <%= make_groups groups %> <%= make_tags 'Lightbulb', tags %> <%= make_metadata assistant[:light], metadata %> 

Switch <%= name %>_Power  "<%= label %> Power" <light> <%= make_groups name, power['groups'] %> <%= make_tags %w[Control Power], power['tags'] %> { channel="mqtt:topic:mosquitto:<%= thingid %>:power", autoupdate="false"<%= add_metadata assistant[:power], power['metadata'] %> }
Dimmer <%= name %>_Dimmer "<%= label %>" <%= make_groups name, dimmer['groups'] %> <%= make_tags %w[Control Light], dimmer['tags'] %> { channel="mqtt:topic:mosquitto:<%= thingid %>:dimmer"<%= add_metadata assistant[:brightness], dimmer['metadata'] %> }

<% if key? 'ct' %>
Dimmer <%= name %>_CT     "<%= label %> CT" <%= make_groups name, ct['groups'] %> <%= make_tags %w[Control ColorTemperature], ct['tags'] %> { channel="mqtt:topic:mosquitto:<%= thingid %>:ct"<%= add_metadata assistant[:ct], ct['metadata'] %> }
<% end %>

<% if key? 'color' %>
Color <%= name %>_Color  "<%= label %> Color" <colorwheel> <%= make_groups name, color['groups'] %> <%= make_tags %w[Control Color], color['tags'] %>  { channel="mqtt:topic:mosquitto:<%= thingid %>:colorhsb"<%= add_metadata assistant[:color], color['metadata'] %> }
<% end %>

Number <%= name %>_RSSI   "<%= label %> RSSI [%d%%]" <network> <%= make_groups name, 'gSignalStrength' %>  { channel="mqtt:topic:mosquitto:<%= thingid %>:rssi" }
String <%= name %>_State <%= make_groups name, 'gTasmotaState' %>     { channel="mqtt:topic:mosquitto:<%= thingid %>:state", id="<%= thingid %>" }
String <%= name %>_IPAddress <%= make_groups name, 'gTasmotaIPs' %>     { channel="mqtt:topic:mosquitto:<%= thingid %>:ipaddress", autoupdate="false" }
Contact <%= name %>_Availability "<%= label %> Availability [MAP(availability.map):%s]" <%= make_groups name, 'gAvailability' %> { channel="mqtt:topic:mosquitto:<%= thingid %>:availability" }


Note: I’m a Ruby newbie, and the code isn’t as polished as it could be. I especially suck at choosing good class / variable names.

I’d appreciate any criticisms, feedback and suggestions.

I am planning to integrate this into my automation rules so that it would automatically regenerate the .items and .things files when the yaml file is modified. Currently waiting for the directory watch feature from @broconne

1 Like

The items/things file can be generated automatically when devices.yaml file is updated with the help of this rule:

require 'openhab'
require 'personal/itemsgen'

rule 'Autogen Items' do
  watch OpenHAB.conf_root / 'scripts/devices.yaml', :for => :modified
  run do |event|
    logger.info("#{event.path} #{event.type}. Regenerating things/items file.")
    yaml = YAML.load_file(event.path)
    gen = OpenhabGenerator::Devices.new(yaml)
    gen.template_dir = OpenHAB.conf_root / 'scripts/templates'
    output = gen.generate
    File.write(OpenHAB.conf_root / 'things/z_generated.things', output['things'])
    File.write(OpenHAB.conf_root / 'items/z_generated.items', output['items'])
  end
end

Note the devices.yaml and templates/ directory are placed inside conf/scripts directory. Due to a current bug in openhab-core AbstractWatchService can't handle two listeners for same directory · Issue #2466 · openhab/openhab-core · GitHub - we shouldn’t add a watcher on the conf/items directory.

1 Like