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:
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.
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:
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:
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.
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.
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.
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:
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).