hivescript
Hivescript is a scripting language that is built into Hivekit. It is designed to be a simple and efficient way to run logic when things change in the hivekit system. Hivescript is very capable, but it is not intended to be a full replacement for programming languages like python or javascript. If you find that hivescript doesn't suit your use case, it may be easier to write some code using the client library to do what you need to do.
Concepts
hivescript instructions are loaded into the realm. They can consist of up to four parts, the every
, the when
, the then
and the until
. In an instruction, when present, they always follow that order, but I'll talk about when, and then first because they are the most commonly used parts, and until and every afterwards.
Here's an example of a simple hivescript instruction:
when
object(troublemaker="true")
then
delete()
This instruction will delete any object that has the attribute troublemaker
set to true
. The when
part of the instruction is a filter that selects the objects that the instruction is interested in. The then
part of the instruction is a list of actions that will be performed on the objects that match the when
filter.
Instructions don't just take effect when they are added, they remain active in the realm, so in this case if an object gets a troublemaker
attribute set to true
in the future, it will be deleted at that point.
When working with hivescript, you should think in terms of groups of things. We select a group of things out of the realm (when
), and then we can perform actions on objects as they come into the group (then
) or leave the group (until
).
The when
, then
and until
parts consist of function calls. Functions are defined in the realm and either filter the current group of things, or take actions on the current group. In this example, object
is a function that selects just objects that match a criteria out of the realm, and delete
is a function that deletes all the objects in the current group.
Installing an instruction into a realm
You can load instructions into your realm through the hivekit web interface, or by using the client library.
To add an instruction using the client library, you can use the realm.instruction.create
method. Here's an example:
import HivekitClient from "@hivekit/client-js";
const client = new HivekitClient();
await client.connect('wss://hivekit.io/ws');
await client.authenticate('my_api_token');
const realm = await client.realm.get('realmId');
await realm.instruction.create('operations/ins1', {
label: 'delete troublemakers',
instructionString: `
when
object(troublemaker="true")
then
delete()
`
})
You can read more about using the client library in the client library documentation.
When
Hivekit instructions operate on groups of things. The when
section of the instruction decides which things are part of the initial group. When the membership of the group changes, the then
and until
parts of the instruction will run depending on what has changed.
Think of the when
part of the instruction as a filter that selects the things that the instruction is interested in out of everything in the realm. Most commonly, you'll be writing instructions that operate on objects in the realm. In our example above, we are selecting objects that have the attribute troublemaker
set to true
, but the object()
function can select a group of objects in other ways too.
Function calls can be chained too.
when
object(type="bike").near(10, type="danger")
then
sendToUrl("https://webhook.site/my-webhook-id")
In this case, the when
part of the instruction selects all objects that are of type bike
. These are then filtered based on which of them are within 10 meters of an object of type danger
.
It's important to think about what is in the group here. an object(type="bike")
call will return all the objects in the realm that are of type bike
, so the result of that function call is a group of objects. The near(10, type="danger")
call doesn't just filter the object, it returns pairs of objects that are within 10 meters of each other. So the result of the near
function call is a group of pairs of objects. The first object in each pair is the bike and the second object is the danger that the bike is close to. When the group of pairs changes, then the when
part of the instruction will operate on any new pairs. This means that if a new danger comes close to a bike, even if that bike has already triggered for some other danger, the new danger will cause a new triggering for the pair (bike, danger)
.
Then
When things come into the group defined by the when
clause, we can perform actions on them. The then
part of the instruction is a list of actions that will be performed on objects that newly match the when
filter.
In the troublemaker example, we perform the delete
action on all objects as soon as they match the when
filter. The delete
action will delete the object from the realm. Just as before, the then
section is a list of function calls, and we can filter or process the group of objects we operate on before taking action.
You can include more than one action in the then
section. Actions are split by a semicolon. Here's an example:
when
object(type="bike")
then
debug("bikes");
sendToUrl("https://webhook.site/bikes")
Until
Until is just like then
, but instead of operating on things that are coming into the active group, they operate on things that are leaving the active group.
Here's an example:
when
object(type="sensor", value<100)
then
set("status", "low")
until
set("status", "normal")
In this example, we are selecting all objects that are of type sensor
and have a value less than 100. When we first select these objects, we set the status
attribute to low
. When the value of the sensor goes back above 100, the sensor will leave the active group and the until
part of the instruction will run, setting the status
attribute to normal
.
Every
Most instructions should be written so that they are triggered by changes occurring in the realm. Instructions then only need to run when the realm is changing. However, sometimes you want your active group to depend on the time, and time is not something that changes in the realm. For these cases, you can use the every
part of an instruction.
every 30 seconds
when
object(type="sensor", $lastUpdate<now().minus(minutes=1))
then
set("status","faulty")
until
set("status","ok")
In this example, we find sensors that haven't reported in the last minute. To do this, we needed to use a special attribute $lastUpdate
which is set to the last time that hivekit processed an update for the object. You can also see that some of the functions understand time periods too, so we can use minutes=1
to specify a time period of 1 minute instead of using the number of milliseconds.
now()
returns the current time, but if the whole instruction ran every time the output of now()
changed, it would need to be evaluated continually. To keep the load on the hivekit server low, we evaluate the now() function in accordance with the every
part of the instruction.
In this case, we'll check every 30 seconds to see if any objects newly match the when
filter.
The parameter to every
is a time period. It understands a range of type periods, including weeks, days, hours, minutes and seconds, and abbreviations of those measures, such as every 3 mins
or every 2s
.
Since every
forces the instruction to re-evaluate, for efficiency reasons, you should use it only when you need to (such as when you need to check the time) and you should set the period to as long as possible.
Recursion
Instructions trigger on changes, but they can also cause changes. This means that instructions can trigger themselves or other instructions. In order to avoid infinite loops, after a certain number of recursive calls, the instructions will stop triggering.