Tutorial: How To Write Block Libraries

This tutorial is a deep dive into Block Libraries, a new concept introduced in openHAB 3.2 to assist you in writing your rules.

Block Libraries can be seen as rule templates, but for making Blockly scripts instead of creating entire rules. They adopt the UI component structure, also used notably for UI widgets & pages (with components, config and slots). They allow you to provide additional custom blocks and block assemblies in the Blockly toolbox that you can simply drag to your script, configure and use right away.

Users of every skill level can benefit from those and find a new appeal to using Blockly for automation scripting, both from a practical and educational viewpoint; experienced users can show off code and techniques that the less experienced can learn from… or one can simply see it as a way to quickly perform a task that would normally involve gathering some reference code, like how to retrieve a framework service instance.

Using Block Libraries

Block libraries are authored in the Developer Tools > Block Libraries section of the UI as YAML files. They can also be imported from the Community Marketplace from the Automation section. Once imported or created, the block libraries appear in the Blockly editor under Libraries:

image

Every block library is in its own category in the toolbox. You can then drag and drop blocks from the block libraries as you would any other block.
When writing a Blockly script, you can use the Ctrl-B shortcut to switch between the editor and the generated code (that you can’t edit directly).

Creating a Block Library

In this tutorial we’ll go over this block library: Tutorial and explain how it’s been made.

The Block Library editor

Go to Developer Tools > Block Libraries and click the blue “+” button to create a new one.
The first thing you’ll want to change right away is the uid on the first line, as you won’t be able to after it’s created. You can also choose the name of the library on line 5, which will be the category label in the toolbox.

As you can see, a semi-complete example skeleton is already provided, let’s try some things right away - change a few things like the color on line 29 to 180 or the second option to stuff on like 16. You’ll notice the block preview on the right is updated automatically. If you encounter situations where it doesn’t happen anymore, hit the Refresh link in the bottom toolbar or Ctrl-R to force a refresh.

image

The other important keyboard shortcuts to know are Ctrl-P, and Ctrl-B. The former will toggle the Preview popup, which contain a Blockly script editor including the current state of your library in the toolbox. The latter will toggle the preview between the Blockly editor and the generated code (and open the preview if it is closed). You can also close the preview with Esc. Usually you’ll want to add the block you’re currently working on to the preview, as well as potential other blocks from the standard ones, then use Ctrl-B and Esc as you write the code generation template.

Anatomy of a Block Library

As mentioned before, block libraries are written in YAML with components, config and slots, like a UI widget.
The root UI component is named BlockLibrary and can contain the following sub-components in 2 separate slots:

  • blocks slot:
    • BlockType component
      • code slot:
        • BlockCodeTemplate component
      • toolbox slot:
        • PresetInput component
        • PresetField component
    • Separator component
    • BlockAssembly component
  • utilities slot:
    • UtilityFunction component
    • UtilityJavaType component
    • UtilityFrameworkService component

The block element defines what appears in the toolbox in your library’s category, and the utilities slot defines some shared code that your blocks’ code generation templates can use.

Defining Block Types

To start off, let’s define a simple block that returns the circumference of a circle.
As a math refresher, here is how to compute the circumference of a circle given its radius:

We’ll reuse that site to make sure the results are correct.

Designing the Block Type

Use the first BlockType component and try to change the message0 to:

circumference of circle with radius

and observe how it changes in the preview.
The config part of the BlockType component is what Blockly would accept as JSON configuration for the block types, only in YAML, with the following caveat: where normally you would put null as the value of the previousStatement, nextStatement or output property, here you should use empty strings "" instead.
To determine this config, you have a tool that will become your best friend when designing new complex blocks as you get familiar with the syntax: Blockly Developer Tools  |  Google Developers

Direct link:
https://blockly-demo.appspot.com/static/demos/blockfactory/index.html

Using this tool you can design Blockly block types… in Blockly.
The JSON result of the configuration of the block types that you can simply rewrite in YAML (or even copy directly as JSON is a subset of YAML - it will be converted if you save and refresh the page).
Define Blocks  |  Blockly  |  Google Developers is another resource you can read to get familiar with the concepts used in Blockly development, like:

  • fields
  • inputs
  • the output type
  • statements

Here for our circle circumference block, the configuration will be something like this:

    - component: BlockType
      config:
        args0:
          - check: Number
            name: RADIUS
            type: input_value
        colour: 70
        message0: circumference of circle with radius %1
        output: Number
        tooltip: Returns the circumference of a circle given its radius
        type: radius

When a configurable parameter is needed, you often have to choose between an input and a field. As a rule of thumb, use an input when the parameter might be something that the user can compute with other blocks, or a variable. Use fields only for those parameters that are normally hardcoded at design time and there’s no need to change at runtime.

Generating Code

To define code for a block type, insert a BlockCodeTemplate component in the code slot of the BlockType component. The skeleton already includes one, so let’s reuse that.

The formula to compute the circumference of a circle being 2·π·R this translates in JavaScript to:
2 * Math.PI * r
…but we have to reuse the RADIUS input we defined in the block type config.
To do that we have to introduce our first placeholder in the code template:
2 * Math.PI * {{input:RADIUS:ORDER_MULTIPLICATION}}

There are a few types of placeholders, the first part before the first colon determines its nature:

  • {{input:inputName[:order]}} - replace with the code from a value input (tex
  • {{field:fieldName}} - replace with the value of a field (text, numeric, dropdown)
  • {{statements:statementsName}} replace with the code from a statement input
  • {{utility:utilityName}} - inject an utility (we’ll get to them) if needed and replace with its actual name
  • {{temp_name:desiredName[:realm]}} - defines a collision-free unique name and replace with its actual name

The optional “order” featured in both the “input” placeholder and as a parameter of the BlockCodeTemplate component (if the block type has an output) refers to Operator Precedence  |  Blockly  |  Google Developers. Basically it will influence when Blockly will add parentheses around the code.

    - component: BlockType
      config:
        args0:
          - check: Number
            name: RADIUS
            type: input_value
        colour: 70
        message0: circumference of circle with radius %1
        output: Number
        tooltip: Returns the circumference of a circle given its radius
        type: radius
      slots:
        code:
          - component: BlockCodeTemplate
            config:
              order: ORDER_MULTIPLICATION
              template: 2 * Math.PI * {{input:RADIUS:ORDER_MULTIPLICATION}}          

Save the library, go create a script in Settings > Scripts and try your new block type:
connect it to a number block from Math and a log (or print) block from openHAB > Logging & Output:

image

Hit Ctrl-B to make sure the code is correct:

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);


logger.info((2 * Math.PI * 123));

Then hit Ctrl-R, watch your logs and surely enough:

[INFO ] [org.openhab.rule.blocklib_tutorial   ] - 772.8317927830891

And check that the result is correct:

image

Utilities

The circumference example was simple enough, but now let’s try to write something a little more involved: compute the length of an arc given the angle and the radius:

Copy the first BlockType component and change its type in the config to arc_length to distinguish it from the radius; on the preview pane on the right you can use the black button to switch the block type that you wish to preview. Let’s change the message and also add a way to enter the angle to our block type. Modify the config as follows:

        args0:
          - angle: 45
            name: ANGLE
            type: field_angle
          - check: Number
            name: RADIUS
            type: input_value
        colour: 50
        message0: arc length with angle %1 and radius %2
        output: ""
        tooltip: Returns the length of a arc given its radius and angle
        type: arc_length

Blockly conveniently offers a field to choose angles (Angle fields  |  Blockly  |  Google Developers) so let’s use that instead of an input which might have been preferable.

image

Now for the code. The formula to compute the arc length is L = r * θ where r is the radius and θ the angle, except the angle field gives us the angle in degrees and θ is supposed to be in radians.
To we need to perform this conversion. Here is how:

We could extract the code from this function so that it is part of our formula, but it’s far nicer to define the function as provided above and simply use it. We also need to make sure it is defined only once per script and that its name doesn’t collide with another name that might have been chosen for instance for an user variable.

To do that, we’ll add an utility function to our script.
Define a new utilities slot directly under the root BlockLibrary component and add a new UtilityFunction component. Set its name to degress_to_radians and copy the function’s code in the code parameter. Writing multi-line parameters in YAML is actually so complex that there’s a whole website dedicated to it:
https://yaml-multiline.info

Lastly, replace the actual function name in the code with {{name}}. We’ll see why later.

The utility component should now resemble the following:

  utilities:
    - component: UtilityFunction
      config:
        code: >-
          function {{name}}(degrees) {
            var pi = Math.PI;
            return degrees * (pi/180);
          }
        name: degrees_to_radians

Now back to the arc_length block type, we can define its code generation template like so:

          - component: BlockCodeTemplate
            config:
              order: ORDER_MULTIPLICATION
              template: >-
                {{utility:degrees_to_radians}}({{field:ANGLE}}) * {{input:RADIUS:ORDER_MULTIPLICATION}}

Use the preview to assemble your block with a literal number from Math and check out the generated code to make it is correct.

image

function degrees_to_radians(degrees) {
  var pi = Math.PI;
  return degrees * (pi/180);
}


print((degrees_to_radians(45) * 123));
print((degrees_to_radians(90) * 234));
print((degrees_to_radians(270) * 345));

Here you can see that the utility function was only defined once and invoked multiple times.
Notice also how the {{name}} placeholder has been changed back. Why have it in the first place then? Here’s why: in the Blockly editor, go to variables in the toolbox and click the Create variable… button. Name your variable “degrees_to_radians”, like the function. Use the “set” block to initialize the variable.

image

Now look at the generated code again:

var degrees_to_radians;

function degrees_to_radians2(degrees) {
  var pi = Math.PI;
  return degrees * (pi/180);
}


degrees_to_radians = 'collision!';
print((degrees_to_radians2(45) * 123));
print((degrees_to_radians2(90) * 234));
print((degrees_to_radians2(270) * 345));

Blockly has detected a name collision and therefore your utility function is now defined as degrees_to_radians2 and invoked as such. So the name of the utility function is actually the “desired” name - Blockly can decide to call it something else! Therefore it’s important to use {{name}} placeholders in your function names; if you didn’t, the function would still be defined as function degrees_to_radians(degrees) {} and then redefined to a string, so the calls wouldn’t work anymore. When using common identifiers, it can become a problem.

Other types of utilities

UtilityFunction is not the only component you can define under the utilities slot. There are 2 others, UtilityJavaType and UtilityFrameworkService. Let’s check them out. Suppose you want to write a block to get the metadata value of a certain openHAB item. You would need to get it from the MetadataRegistry using a MetadataKey and then get its value if it is defined.
So let’s declare them as utilities like so:

    - component: UtilityFrameworkService
      config:
        name: metadataService
        serviceClass: org.openhab.core.items.MetadataRegistry
    - component: UtilityJavaType
      config:
        javaClass: org.openhab.core.items.MetadataKey
        name: MetadataKey

Then also add an utility function:

    - component: UtilityFunction
      config:
        code: >-
          function {{name}}(metadata, itemName) {
            var result = {{metadataService}}.get(new {{MetadataKey}}(namespace, itemName));
            return (result) ? result.getValue() : null;
          }
        name: get_metadata_value

Note how you can reference other utilities inside the code of the utility function with the {{otherUtility}} syntax - not referring to them for the same reasons as explained above. Moreover, only by referencing other utilities you can make sure that they are all injected as needed.

Here’s the BlockType component to get the metadata value:

    - component: BlockType
      config:
        args0:
          - name: NAMESPACE
            text: namespace
            type: field_input
          - check: String
            name: ITEMNAME
            type: input_value
        colour: 0
        helpUrl: ""
        message0: get item %1 metadata value for %2
        output: ""
        tooltip: ""
        type: metadata_value
      slots:
        code:
          - component: BlockCodeTemplate
            config:
              template: >-
                {{utility:get_metadata_value}}('{{field:NAMESPACE}}', {{input:ITEMNAME}}))

Try it out in a Blockly script:

image

generates the following code:

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);

function addFrameworkService (serviceClass) {
  var bundleContext = Java.type('org.osgi.framework.FrameworkUtil').getBundle(scriptExtension.class).getBundleContext();
  var serviceReference = bundleContext.getServiceReference(serviceClass);
  return bundleContext.getService(serviceReference);
}

var metadataService = addFrameworkService('org.openhab.core.items.MetadataRegistry');

var MetadataKey = Java.type('org.openhab.core.items.MetadataKey');

function get_metadata_value(metadata, itemName) {
  var result = metadataService.get(new MetadataKey(namespace, itemName));
  return (result) ? result.getValue() : null;
}


logger.info((get_metadata_value('alexa', 'MyItem'))));
logger.info((get_metadata_value('homekit', 'MyItem'))));

All the utilities were declared first, in the proper order, and the actual block code simply reuses them!

Customize the Toolbox

Preset Input and Fields

You might have noticed that the inputs of your blocks in the toolbox lack pre-made example connections like many of those in the standard categories do. This can become problematic as an unconnected input can lead to invalid code. You can add so-called shadow blocks to your custom block types, with the PresetField and PresetInput components in the toolbox slot.

Example:

    - component: BlockType
      config:
        type: metadata
        message0: get item %1 metadata object for %2
        ...
      slots:
        code:
          ...
        toolbox:
          - component: PresetField
            config:
              name: ANGLE
              value: 270
          - component: PresetInput
            config:
              fields:
                NUM: 123
              name: RADIUS
              shadow: true
              type: math_number

Note that for PresetInput you have to figure out the type of the shadow block, its own field names, and put values in the fields map. You can reuse openHAB block types as well, for instance, if an input needs an item or a thing, use the oh_item or oh_thing block types respectively:

    - component: BlockType
      config:
        type: metadata
        message0: get item %1 metadata object for %2
        ...
      slots:
        code:
          ...
        toolbox:
          - component: PresetInput
            config:
              fields:
                itemName: item1
              name: ITEMNAME
              shadow: true
              type: oh_item

Separators

You can insert a separator between two blocks, with a configurable gap:

    - component: Separator
      config:
        gap: 100

Block Assemblies

An advanced component, this allows to define, instead of new block types, pre-made assemblies of existing types for instance to implement some algorithm, that the users can drag into their scripts but still modify.

You have to study Blockly’s XML format and put it in the blockXml property.

Examples taken from Blockly Demo: Toolbox

    - component: BlockAssembly
      config:
        blockXml: >
          <block type="procedures_defnoreturn">
            <mutation>
              <arg name="list"></arg>
            </mutation>
            <field name="NAME">randomize</field>
            <statement name="STACK">
              <block type="controls_for" inline="true">
                <field name="VAR">x</field>
                <value name="FROM">
                  <block type="math_number">
                    <field name="NUM">1</field>
                  </block>
                </value>
                <value name="TO">
                  <block type="lists_length" inline="false">
                    <value name="VALUE">
                      <block type="variables_get">
                        <field name="VAR">list</field>
                      </block>
                    </value>
                  </block>
                </value>
                <value name="BY">
                  <block type="math_number">
                    <field name="NUM">1</field>
                  </block>
                </value>
                <statement name="DO">
                  <block type="variables_set" inline="false">
                    <field name="VAR">y</field>
                    <value name="VALUE">
                      <block type="math_random_int" inline="true">
                        <value name="FROM">
                          <block type="math_number">
                            <field name="NUM">1</field>
                          </block>
                        </value>
                        <value name="TO">
                          <block type="lists_length" inline="false">
                            <value name="VALUE">
                              <block type="variables_get">
                                <field name="VAR">list</field>
                              </block>
                            </value>
                          </block>
                        </value>
                      </block>
                    </value>
                    <next>
                      <block type="variables_set" inline="false">
                        <field name="VAR">temp</field>
                        <value name="VALUE">
                          <block type="lists_getIndex" inline="true">
                            <mutation statement="false" at="true"></mutation>
                            <field name="MODE">GET</field>
                            <field name="WHERE">FROM_START</field>
                            <value name="AT">
                              <block type="variables_get">
                                <field name="VAR">y</field>
                              </block>
                            </value>
                            <value name="VALUE">
                              <block type="variables_get">
                                <field name="VAR">list</field>
                              </block>
                            </value>
                          </block>
                        </value>
                        <next>
                          <block type="lists_setIndex" inline="false">
                            <value name="AT">
                              <block type="variables_get">
                                <field name="VAR">y</field>
                              </block>
                            </value>
                            <value name="LIST">
                              <block type="variables_get">
                                <field name="VAR">list</field>
                              </block>
                            </value>
                            <value name="TO">
                              <block type="lists_getIndex" inline="true">
                                <mutation statement="false" at="true"></mutation>
                                <field name="MODE">GET</field>
                                <field name="WHERE">FROM_START</field>
                                <value name="AT">
                                  <block type="variables_get">
                                    <field name="VAR">x</field>
                                  </block>
                                </value>
                                <value name="VALUE">
                                  <block type="variables_get">
                                    <field name="VAR">list</field>
                                  </block>
                                </value>
                              </block>
                            </value>
                            <next>
                              <block type="lists_setIndex" inline="false">
                                <value name="AT">
                                  <block type="variables_get">
                                    <field name="VAR">x</field>
                                  </block>
                                </value>
                                <value name="LIST">
                                  <block type="variables_get">
                                    <field name="VAR">list</field>
                                  </block>
                                </value>
                                <value name="TO">
                                  <block type="variables_get">
                                    <field name="VAR">temp</field>
                                  </block>
                                </value>
                              </block>
                            </next>
                          </block>
                        </next>
                      </block>
                    </next>
                  </block>
                </statement>
              </block>
            </statement>
          </block>
    - component: BlockAssembly
      config:
        blockXml: >
          <block type="oh_print">
            <value name="message">
              <block type="text">
                <field name="TEXT">'Twas brillig, and the slithy toves</field>
              </block>
            </value>
            <next>
              <block type="oh_print">
                <value name="message">
                  <block type="text">
                    <field name="TEXT">  Did gyre and gimble in the wabe:</field>
                  </block>
                </value>
                <next>
                  <block type="oh_print">
                    <value name="message">
                      <block type="text">
                        <field name="TEXT">All mimsy were the borogroves,</field>
                      </block>
                    </value>
                    <next>
                      <block type="oh_print">
                        <value name="message">
                          <block type="text">
                            <field name="TEXT">  And the mome raths outgrabe.</field>
                          </block>
                        </value>
                      </block>
                    </next>
                  </block>
                </next>
              </block>
            </next>
          </block>

Tip: to create the structure you can also use Advanced Blockly Playground (use the result from the XML but strip the IDs) and for openHAB blocks you’ll have to figure out on your own (for instance, with the help of the source of the standard toolbox).

11 Likes

“Beware the Jabberwock, my son!”

Great tutorial and even greater new capability! I’ve been watching this development with anticipation. I like how some very complicated code can be hidden inside a single block.

One minor thing I would add is when choosing a uid, especially if one intends to publish this to the marketplace, choose something really unique to avoid conflicts with what others might have posted.

That can be achieved by coming up with your own “namespace”. I assume “:” is allowed like in rule templates so one approach could be to use your name, or forum username, or the like, a colon, and then the name of the block.

rlkoshak:snicker_snack

If you are managing a larger library and want to maintain some branding, use that instead of your name.

vorpal_blade:snicker_snack
3 Likes

Is it possible to create/use

.stream().filter(item -> item.state.contains("ON"))

especially for existing “get member of group” -blockly it would be very nice. But I did not get it right now.

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.