Core
IoT Backend v2.x
2

Enrich Assets with Geofencing #

In this tutorial you will learn how to use the Rules Engine to enrich your assets with geofencing information.

We will use both the Device Manager and the Rules Engine modules.

The following concepts are expected to be known:

In order to enrich measurements, we will use the event triggered before persisting the measures ingestion pipeline: engine:{engine-index}:device-manager:measures:persist:before

Events respecting the format engine:{engine-index}:... are events confined to a particular tenant. A tenant user cannot save a workflow on an event that does not respect this format.

Our business rule comes in the form of a workflow that will listen to this event.

Assets Concepts #

In order to enable geofencing, we need two particular types of assets:

  • A geofence asset that will represent a geofence area
  • A geofencing asset that will represent an asset that will be geofenced

Those particular properties are activated by creating a special metadata on an existing asset.

Geofence #

The geofence asset is a special asset that will represent a geofence area. It will be used to define the areas where the geofencing asset will be geofenced.

You can use the makeGeofence method on an existing asset definition to create a geofence asset.

Copied to clipboard!
import { makeGeofence } from "@kuzzleio/iot-backend";

// The object argument is the truck asset definition
export const warehouseAssetDefinition = makeGeofence({
  measures: [],
  metadataMappings: {
    surface: { type: "float" },
    loadingBays: { type: "float" },
  },
});

The following metadata will be created:

  • name: the name of the geofence. this value will be used to enrich asset inside the geofence
  • group: name of the geofence group
  • disabled: boolean to disable the geofence
  • perimeter: perimeter of the geofence
    • type : type of the geofence. Can be circle or polygon
    • coordinates : coordinates of the geofence. For a circle [lat, lon], for a polygon [[[lat, lon], [lat, lon], ...]]]
    • radius: circle radius

You can also define corresponding types for your Warehouse asset:

Copied to clipboard!
import { AssetContent } from "kuzzle-device-manager";
import { GeofencePolygonMetadata } from "@kuzzleio/iot-backend";

export interface WarehouseMetadata extends GeofencePolygonMetadata {
  surface: number;
  loadingBays: number;
}

export type WarehouseMeasurements = {};

export interface WarehouseAssetContent
  extends AssetContent<WarehouseMeasurements, WarehouseMetadata> {
  model: "Warehouse";
}

In our example, we will have a Warehouse asset that will be a geofence defined by a polygon.

Geofencing #

The geofencing asset is a special asset that will represent an asset that will be geofenced. It will be enriched with the name of the geofence it is currently inside.

You can use the makeGeofencing method on an existing asset definition to create a geofencing asset.

Copied to clipboard!
import { makeGeofencing } from "@kuzzleio/iot-backend";

// The object argument is the truck asset definition
export const truckAssetDefinition = makeGeofencing({
  defaultMetadata: {
    capacity: 38,
  },
  measures: [
    {
      name: "position",
      type: "position",
    },
  ],
  metadataMappings: {
    capacity: { type: "integer" },
    numberPlate: { type: "keyword" },
  },
});

The following metadata will be created:

  • state: name of the geofence the asset is currently inside. If the asset is not inside any geofence, the value will be null
  • disabled: boolean to disable the geofencing

Also, if it does not already exists, a position measure will be added.

You can also define corresponding types for your Truck asset:

Copied to clipboard!
import { AssetContent } from "kuzzle-device-manager";
import {
  GeofencingMeasurements,
  GeofencingMetadata,
} from "@kuzzleio/iot-backend";

export interface TruckMetadata extends GeofencingMetadata {
  numberPlate: string;
  capacity: number;
}

export interface TruckMeasurements extends GeofencingMeasurements {}

export interface TruckAssetContent
  extends AssetContent<TruckMeasurements, TruckMetadata> {
  model: "Truck";
}

Register our assets #

We need to register our assets in the Device Manager:

Copied to clipboard!
import { DeviceManagerPlugin } from "kuzzle-device-manager";
import { truckAssetDefinition, warehouseAssetDefinition } from "./assets";

const deviceManager = app.plugin.get < DeviceManagerPlugin > "device-manager";

deviceManager.models.registerAsset(
  "asset_tracking",
  "Truck",
  truckAssetDefinition
);

deviceManager.models.registerAsset(
  "asset_tracking",
  "Warehouse",
  warehouseAssetDefinition
);

Define geofencing workflow #

Our workflow will listen to the engine:{engine-index}:device-manager:measures:persist:before event and will have one rule as action.

This rule will only be executed on the Truck asset and will be responsible of executing the geofencing.

First, we need to define a new workflow:

Copied to clipboard!
import { Workflow, WorkflowContent } from "@kuzzleio/plugin-workflows";

const enrichmentWorkflowContent: WorkflowContent = {
  name: "Enrichment Workflow",
  description:
    "Workflow to enrich devices, assets and measures in the ingestion pipeline",
  payloadPath: ".",
  trigger: {
    type: "event",
    event: "engine:{engine-index}:device-manager:measures:persist:before",
  },
  actions: [
    {
      type: "rule",
      name: "asset-truck-geofencing",
    },
  ],
};

export const enrichmentWorkflow = new Workflow(enrichmentWorkflowContent);

Then, we need to define the rule that will be executed by the workflow.

This rule will have two actions:

  • a task action that will prepare the geofencing
    • check if the asset can be geofenced
    • set the previous geofence in context.props.previousGeofence
    • clean the state metadata to prepare the geofencing
  • a rule-group action that will execute the geofencing
Copied to clipboard!
import { Rule, RuleContent } from "@kuzzleio/plugin-workflows";

const assetTruckGeofencingRuleContent: RuleContent = {
  name: "asset-truck-geofencing",
  description:
    "Rule to enrich geofencing Truck assets by using the Warehouse geofence",
  filters: {
    and: [
      // Only if the asset is geofencing enabled
      {
        not: {
          equals: { "asset._source.metadata.geofencing.disabled": true },
        },
      },
      // Only if the asset model is Truck
      {
        equals: { "asset._source.model": "Truck" },
      },
    ],
  },
  actions: [
    {
      // Mandatory task to prepare the asset for the geofencing
      type: "task",
      name: "prepare-geofencing",
    },
    {
      type: "rule-group",
      // Use geofence of the group "warehouse"
      name: "warehouse",
    },
  ],
};

export const assetTruckGeofencingRule = new Rule(
  assetTruckGeofencingRuleContent
);

Then we need to register the workflow and the rule in the Workflows plugin:

Copied to clipboard!
import { WorkflowsPlugin } from "@kuzzleio/plugin-workflows";
import { enrichmentWorkflow, assetTruckGeofencingRule } from "./geofencing";

const workflowsPlugin = this.app.plugin.get < WorkflowsPlugin > "workflows";
workflowsPlugin.registerDefaultRule(assetTruckGeofencingRule, {
  group: "asset_tracking",
});
workflowsPlugin.registerDefaultWorkflow(enrichmentWorkflow, {
  group: "asset_tracking",
});

How it works #

For each geofence asset, a corresponding rule is created. This rule contains a geofencing filter corresponding to the asset geofence and an action to set the name of the geofence in the state metadata of the asset being geofenced.

Example of a geofence asset and it's generated rule:

Copied to clipboard!
// Geofence asset
{
  "model": "Warehouse",
  "reference": "IDFNord",
  "linkedDevices": [],
  "measures": {},
  "metadata": {
    "surface": 15000,
    "loadingBays": 12,
    "geofence": {
      "name": "IDFNord",
      "group": "warehouse",
      "disabled": false,
      "perimeter": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              49.00952957325299,
              2.4838636253124946
            ],
            [
              49.00600868479984,
              2.483106516509082
            ],
            [
              49.00582336798439,
              2.4856264458095723
            ],
            [
              49.00907001878247,
              2.48728756512466
            ],
            [
              49.00952957325299,
              2.4838636253124946
            ]
          ]
        ]
      }
    }
  },
}

// Generated rule
{
  "type": "rule",
  "rule": {
    "name": "Rule geofence for asset \"Warehouse-IDFNord\"",
    "description": "Autogenerated rule for asset geofence Warehouse-IDFNord",
    "group": "warehouse",
    "filters": {
      "geoPolygon": {
        "asset._source.measures.position.values.position": {
          "points": [
            [
              49.00952957325299,
              2.4838636253124946
            ],
            [
              49.00600868479984,
              2.483106516509082
            ],
            [
              49.00582336798439,
              2.4856264458095723
            ],
            [
              49.00907001878247,
              2.48728756512466
            ],
            [
              49.00952957325299,
              2.4838636253124946
            ]
          ]
        }
      }
    },
    "actions": [
      {
        "type": "task",
        "name": "enrich-payload",
        "args": {
          "asset._source.metadata.geofencing.state": "IDFNord"
        }
      }
    ]
  }
}

Then we are taking avantage of the action of type rule-group to execute the geofencing.

This action will try to match the asset with every rule of the group. If a rule matches, the action will be executed and thus the asset enriched.

Bonus: keep track of asset movement in geofences (enter/exit) #

It's possible to define an additional task that will create a new measure each time the asset enter or exit a geofence.

This task will be executed after the geofencing task and will be using the context.props.previousGeofence property.

First, we need to define a new measures:

Copied to clipboard!
import { MeasureDefinition } from "@kuzzleio/iot-backend";

export type MovementRecordMeasurement = {
  in: string | null,
  out: string | null,
};

export const movementRecordMeasureDefinition: MeasureDefinition = {
  valuesMappings: {
    in: { type: "keyword" },
    out: { type: "keyword" },
  },
};

Then update the Truck asset definition:

Copied to clipboard!
export interface TruckMetadata extends GeofencingMetadata {
  numberPlate: string;
  capacity: number;
}

export interface TruckMeasurements extends GeofencingMeasurements {
  movementRecord: MovementRecordMeasurement;
}

export interface TruckAssetContent
  extends AssetContent<TruckMeasurements, TruckMetadata> {
  model: "Truck";
}

export const truckAssetDefinition = makeGeofencing({
  defaultMetadata: {
    capacity: 38,
  },
  measures: [
    {
      name: "position",
      type: "position",
    },
    {
      name: "movementRecord",
      type: "movementRecord",
    },
  ],
  metadataMappings: {
    capacity: { type: "integer" },
    numberPlate: { type: "keyword" },
  },
});

Create the task responsible of creating a new measure each time the asset change zone:

Copied to clipboard!
import { Task, WorkflowContext } from "@kuzzleio/plugin-workflows";
import { MeasureContent } from "kuzzle-device-manager";
import { KDocument } from "kuzzle-sdk";

import { TruckAssetContent } from "../assets/Truck";
import { MovementRecordMeasurement } from "../measures";

export class CreateMovementRecordTask extends Task {
  constructor() {
    super("create-movement-record");

    this.description = `This task will create a new "movementRecord" measure for the asset if the asset entered or exited a geofence`;
  }

  async run(context: WorkflowContext) {
    const measures = context.payload.measures as MeasureContent[];
    const asset = context.payload.asset as KDocument<TruckAssetContent>;

    if (
      context.props.previousGeofence === asset._source.metadata.geofencing.state
    ) {
      return context;
    }

    const movementRecord: MeasureContent<MovementRecordMeasurement> = {
      type: "movementRecord",
      values: {
        out: context.props.previousGeofence,
        in: asset._source.metadata.geofencing.state,
      },
      measuredAt: asset._source.measures.position.measuredAt,
      asset: {
        _id: asset._id,
        model: asset._source.model,
        reference: asset._source.reference,
        measureName: "movementRecord",
        metadata: asset._source.metadata,
      },
      origin: {
        _id: asset._source.measures.position.originId,
        payloadUuids: asset._source.measures.position.payloadUuids,
        type: "computed",
        measureName: "movementRecord",
      },
    };

    measures.push(movementRecord);

    asset._source.measures.movementRecord = {
      name: "movementRecord",
      type: "computed",
      measuredAt: movementRecord.measuredAt,
      values: movementRecord.values,
      originId: movementRecord.origin._id,
      payloadUuids: movementRecord.origin.payloadUuids,
    };

    return context;
  }
}

Update the geofencing rule:

Copied to clipboard!
import { Rule, RuleContent } from "@kuzzleio/plugin-workflows";

const assetTruckGeofencingRuleContent: RuleContent = {
  name: "asset-truck-geofencing",
  description:
    "Rule to enrich geofencing Truck assets by using the Warehouse geofence",
  filters: {
    and: [
      // Only if the asset is geofencing enabled
      {
        not: {
          equals: { "asset._source.metadata.geofencing.disabled": true },
        },
      },
      // Only if the asset model is Truck
      {
        equals: { "asset._source.model": "Truck" },
      },
    ],
  },
  actions: [
    {
      type: "task",
      name: "prepare-geofencing",
    },
    {
      type: "rule-group",
      name: "warehouse",
    },
    {
      type: "task",
      name: "create-movement-record",
    },
  ],
};

export const assetTruckGeofencingRule = new Rule(
  assetTruckGeofencingRuleContent
);