Quick Start
Swiftly implement the backend portion of a custom integration with this code example.
Prerequisites
Before developing the backend part of the integration, you might need to be familiar with:
- The fundamental concepts and terminology of Beaver IoT
- Basic Java syntax
- Core knowledge of the Spring Framework
If you are acquainted with the above subjects, please proceed to follow along and create a simple demo step by step.
Environment Setup
Before starting development, ensure you have the following environment set up:
- Java IDE (IntelliJ IDEA recommended)
- Java Version 17 SDK
- Maven
- Git CLI
Once these are ready, execute the following git command to obtain the source code for the integration project beaver-iot-integrations
:
- SSH
- Https
git clone git@github.com:Milesight-IoT/beaver-iot-integrations.git
git clone https://github.com/Milesight-IoT/beaver-iot-integrations.git
beaver-iot-integrations/
├── integrations/ # integration directory
│ ├── sample-integrations/ # Sample integration directory
│ │ └── ... # Sample integrations
│ ├── msc-integration
│ └── ... # All other integrations
(Optional) Obtain the Beaver IoT backend project source code beaver-iot
for testing after integration development:
- SSH
- Https
git clone git@github.com:Milesight-IoT/beaver-iot.git
git clone https://github.com/Milesight-IoT/beaver-iot.git
Once you open these projects in your Java IDE, you can begin developing an integration.
Writing a Hello World
First of all, go to the beaver-iot-integrations
project
Create Integration Metadata
Create a new module under the integrations
module of the project and name it as the integration ID.
[integration-id]
Replace [integration-id]
with the ID you just generated in all the following example codes.
The pom.xml
file for the module should be as follows:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.milesight.beaveriot.integrations</groupId>
<artifactId>integrations</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>[integration-id]</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.milesight.beaveriot</groupId>
<artifactId>context</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- in case you have your own dependencies to be packaged -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Dependencies with scope
set to provided
will not be packaged into the integration; they are provided by Beaver IoT. The maven-shade-plugin
bundles dependencies into a single JAR. The context
module is the core module of Beaver IoT, offering essential functionalities for integration development.
Create a new resource file integration.yaml
:
integration:
[integration-id]: # integration identifier
name: My Integration Name # integration name
description: "My Demo Integration" # integration description
enabled: true # whether to enable this integration. Must be "true" for now
Create Bootstrap Class
Create a new package com.milesight.beaveriot.integrations.[integration-id]
containing a Java class file MyIntegrationBootstrap.java
:
package com.milesight.beaveriot.integrations.[integration-id];
import com.milesight.beaveriot.context.integration.bootstrap.IntegrationBootstrap;
import com.milesight.beaveriot.context.integration.model.Integration;
import org.springframework.stereotype.Component;
@Component
public class MyIntegrationBootstrap implements IntegrationBootstrap {
@Override
public void onPrepared(Integration integration) {
// do nothing
}
@Override
public void onStarted(Integration integrationConfig) {
System.out.println("Hello, world!");
}
@Override
public void onDestroy(Integration integration) {
// do nothing
}
}
Thus, you have completed your first and simplest integration, which will print
Hello, world!
when Beaver IoT initializes the integration at startup.
(Optional) Launch Your First Integration
In the beaver-iot-integrations
project, install your integration module.
Go to the beaver-iot
project and add your integration to the dependencies list of application/application-standard
.
<!-- ... -->
<artifactId>application-standard</artifactId>
<name>application-standard</name>
<!-- ... -->
<dependencies>
<!-- ... -->
<!-- default integrations -->
<!-- ... -->
<dependency>
<groupId>com.milesight.beaveriot.integrations</groupId>
<artifactId>[integration-id]</artifactId>
<version>${project.version}</version>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
</project>
Upon starting application-standard
, you can observe the console output:
Hello, world!
Implementing a Useful Integration
You have now created a basic integration that prints a message to the console. Next, let's develop a more functional integration.
This new integration will detect whether devices at specific IP addresses are online. It includes the following features:
- Support for triggering a check to see if all devices are online
- Sending a report event each time a check is completed
- Support for adding devices to be monitored
- Support for removing devices
- Returning the number of online devices via HTTP
Defining Entities
Based on the above requirements, the integration needs the following entities:
- A service entity
benchmark
to check if all devices are online - A property entity
detect_status
to indicate the detection status (detecting/pending) - An event entity
detect_report
to report the results of the check (including the number of devices checked and the time taken)
Additionally, adding and removing devices are also service entities:
- Add device service
add_device
- Remove device service
delete_device
If you have any questions about the definitions of these entities based on the above requirements, please refer to the Concepts Introduction.
First, go to the beaver-iot-integrations
project and find the module you just created
Create a new Java class file MyIntegrationEntities.java
to define the above five entities and their sub-entities using annotations:
package com.milesight.beaveriot.integrations.[integration-id].entity;
import com.milesight.beaveriot.context.integration.context.AddDeviceAware;
import com.milesight.beaveriot.context.integration.context.DeleteDeviceAware;
import com.milesight.beaveriot.context.integration.entity.annotation.Attribute;
import com.milesight.beaveriot.context.integration.entity.annotation.Entities;
import com.milesight.beaveriot.context.integration.entity.annotation.Entity;
import com.milesight.beaveriot.context.integration.entity.annotation.IntegrationEntities;
import com.milesight.beaveriot.context.integration.enums.AccessMod;
import com.milesight.beaveriot.context.integration.enums.EntityType;
import com.milesight.beaveriot.context.integration.model.ExchangePayload;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@IntegrationEntities
public class MyIntegrationEntities extends ExchangePayload {
@Entity(type = EntityType.SERVICE, name = "Device Connection Benchmark", identifier = "benchmark")
private Benchmark benchmark;
@Entity(type = EntityType.PROPERTY, name = "Detect Status", identifier = "detect_status", attributes = @Attribute(enumClass = DetectStatus.class), accessMod = AccessMod.R)
private Long detectStatus;
@Entity(type = EntityType.EVENT, name = "Detect Report", identifier = "detect_report")
private DetectReport detectReport;
@Entity(type = EntityType.SERVICE, identifier = "add_device", visible = false)
private AddDevice addDevice;
@Entity(type = EntityType.SERVICE, identifier = "delete_device", visible = false)
private DeleteDevice deleteDevice;
@Data
@EqualsAndHashCode(callSuper = true)
@Entities
public static class DetectReport extends ExchangePayload {
@Entity
private Long consumedTime;
@Entity
private Long onlineCount;
@Entity
private Long offlineCount;
}
@Data
@EqualsAndHashCode(callSuper = true)
@Entities
public static class AddDevice extends ExchangePayload implements AddDeviceAware {
@Entity
private String ip;
}
@Data
@EqualsAndHashCode(callSuper = true)
@Entities
public static class DeleteDevice extends ExchangePayload implements DeleteDeviceAware {
}
@Data
@EqualsAndHashCode(callSuper = true)
@Entities
public static class Benchmark extends ExchangePayload {
}
public enum DetectStatus {
STANDBY, DETECTING;
}
}
The delete device service entity cannot have sub-entities.
Adding and removing devices are common functionalities that represent internal interactions between the integration and Beaver IoT, and are not directly accessible to users. Therefore, in the MyIntegrationEntities
class, the visible
attribute for these entities is set to false
.
In this class, we define the entities for adding devices and removing devices. We need to sync their identifier
to the metadata to indicate to Beaver IoT that this integration supports adding and removing devices.
Update the resource file integration.yaml
:
integration:
[integration-id]: # integration identifier
# ...
entity-identifier-add-device: add_device
# the same for deleteDevice identifier
entity-identifier-delete-device: delete_device
Define the Device
Let's define the device template for this integration. Each device will include a single entity representing the device status.
Create a new Java class file MyDeviceEntities.java
to define the device MyDeviceEntities
and its entity status
using annotations:
package com.milesight.beaveriot.integrations.[integration-id].entity;
import com.milesight.beaveriot.context.integration.entity.annotation.*;
import com.milesight.beaveriot.context.integration.enums.AccessMod;
import com.milesight.beaveriot.context.integration.enums.EntityType;
import com.milesight.beaveriot.context.integration.model.ExchangePayload;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@DeviceTemplateEntities(name = "Ping Device")
public class MyDeviceEntities extends ExchangePayload {
@Entity(type = EntityType.PROPERTY, name = "Device Connection Status", accessMod = AccessMod.R, attributes = @Attribute(enumClass = DeviceStatus.class))
private Long status;
public enum DeviceStatus {
ONLINE, OFFLINE;
}
}
Listen to Events - Add Device / Remove Device
We have defined the add/remove device service entities. When a user calls these services, corresponding events are sent. We can listen to these events and implement the corresponding functionality.
The context of the add device event includes the device name specified by the user (in this example, we use the AddDeviceAware
interface to get the new device name). Since the identifier
cannot contain the dot character from the IP address, we perform a conversion.
The context of the remove device event includes the device instance (in this example, we use the DeleteDeviceAware
interface to get the device to be removed).
Create a new Java class file MyDeviceService.java
to implement the methods for adding and removing devices:
package com.milesight.beaveriot.integrations.[integration-id].service;
import com.milesight.beaveriot.context.api.DeviceServiceProvider;
import com.milesight.beaveriot.context.api.EntityValueServiceProvider;
import com.milesight.beaveriot.context.integration.model.*;
import com.milesight.beaveriot.context.integration.model.event.ExchangeEvent;
import com.milesight.beaveriot.context.integration.wrapper.AnnotatedEntityWrapper;
import com.milesight.beaveriot.context.integration.wrapper.AnnotatedTemplateEntityWrapper;
import com.milesight.beaveriot.eventbus.annotations.EventSubscribe;
import com.milesight.beaveriot.eventbus.api.Event;
import com.milesight.beaveriot.integrations.[integration-id].entity.MyDeviceEntities;
import com.milesight.beaveriot.integrations.[integration-id].entity.MyIntegrationEntities;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@Service
public class MyDeviceService {
@Autowired
private DeviceServiceProvider deviceServiceProvider;
@Autowired
private EntityValueServiceProvider entityValueServiceProvider;
public static final String INTEGRATION_ID = "[integration-id]";
@EventSubscribe(payloadKeyExpression = INTEGRATION_ID + ".integration.add_device.*", eventType = ExchangeEvent.EventType.CALL_SERVICE)
public void onAddDevice(Event<MyIntegrationEntities.AddDevice> event) {
MyIntegrationEntities.AddDevice addDevice = event.getPayload();
String deviceName = addDevice.getAddDeviceName();
String ip = event.getPayload().getIp();
Device device = new DeviceBuilder(INTEGRATION_ID)
.name(deviceName)
.identifier(ip.replace(".", "_"))
.additional(Map.of("ip", ip))
.entities(()-> new AnnotatedTemplateEntityBuilder(INTEGRATION_ID, ip.replace(".", "_")).build(MyDeviceEntities.class))
.build();
deviceServiceProvider.save(device);
}
@EventSubscribe(payloadKeyExpression = INTEGRATION_ID + ".integration.delete_device", eventType = ExchangeEvent.EventType.CALL_SERVICE)
public void onDeleteDevice(Event<MyIntegrationEntities.DeleteDevice> event) {
Device device = event.getPayload().getDeletedDevice();
deviceServiceProvider.deleteById(device.getId());
}
}
Listen to Events - Benchmark
Next, we create a method to listen to the Benchmark service entity and implement this method. Once all devices have been checked, a detect_report
event will be sent.
Update the MyDeviceService.java
class to add the implementation of the Benchmark service entity method:
@Service
public class MyDeviceService {
// ...
@EventSubscribe(payloadKeyExpression = INTEGRATION_ID + ".integration.benchmark", eventType = ExchangeEvent.EventType.CALL_SERVICE)
public void doBenchmark(Event<MyIntegrationEntities> event) {
// Mark benchmark starting
new AnnotatedEntityWrapper<MyIntegrationEntities>()
.saveValue(MyIntegrationEntities::getDetectStatus, (long) MyIntegrationEntities.DetectStatus.DETECTING.ordinal())
.publishSync();
// Start pinging
final int timeout = 5000;
List<Device> devices = deviceServiceProvider.findAll(INTEGRATION_ID);
AtomicReference<Long> activeCount = new AtomicReference<>(0L);
AtomicReference<Long> inactiveCount = new AtomicReference<>(0L);
Long startTimestamp = System.currentTimeMillis();
devices.forEach(device -> {
boolean isSuccess = false;
try {
String ip = (String) device.getAdditional().get("ip");
InetAddress inet = InetAddress.getByName(ip);
if (inet.isReachable(timeout)) {
isSuccess = true;
}
} catch (Exception e) {
e.printStackTrace();
}
int deviceStatus = MyDeviceEntities.DeviceStatus.OFFLINE.ordinal();
if (isSuccess) {
activeCount.updateAndGet(v -> v + 1);
deviceStatus = MyDeviceEntities.DeviceStatus.ONLINE.ordinal();
} else {
inactiveCount.updateAndGet(v -> v + 1);
}
// Device has only one entity
new AnnotatedTemplateEntityWrapper<MyDeviceEntities>(device.getIdentifier()).saveValue(MyDeviceEntities::getStatus, (long) deviceStatus);
});
Long endTimestamp = System.currentTimeMillis();
// Mark benchmark done
new AnnotatedEntityWrapper<MyIntegrationEntities>()
.saveValue(MyIntegrationEntities::getDetectStatus, (long) MyIntegrationEntities.DetectStatus.STANDBY.ordinal())
.publishSync();
// Send report event
new AnnotatedEntityWrapper<MyIntegrationEntities.DetectReport>().saveValues(Map.of(
MyIntegrationEntities.DetectReport::getConsumedTime, endTimestamp - startTimestamp,
MyIntegrationEntities.DetectReport::getOnlineCount, activeCount.get(),
MyIntegrationEntities.DetectReport::getOfflineCount, inactiveCount.get()
)).publishSync();
}
// ...
}
Listen to Events - Detect Report
We can listen to the report event sent after the detection is completed.
Update the MyDeviceService.java
class to add a method to listen to the report and print it:
@Service
public class MyDeviceService {
// ...
@EventSubscribe(payloadKeyExpression = INTEGRATION_ID + ".integration.detect_report.*", eventType = ExchangeEvent.EventType.REPORT_EVENT)
public void listenDetectReport(Event<MyIntegrationEntities.DetectReport> event) {
System.out.println("[Get-Report] " + event.getPayload()); // Do something with this report
}
// ...
}
Create HTTP API
We will allow the integration to set up its own HTTP routes for custom frontend calls or as a webhook entry point.
Here, we will implement an HTTP interface that returns the count of online devices with the endpoint GET /[integration-id]/active-count
.
To prevent route conflicts between different integrations and the system, the URL address of the integration should start with the integration name, such as:
- /[integration-id]/foo
- /[integration-id]/foo/bar
- /[integration-id]/bar
Create a Java class MyIntegrationController.java
to add a controller that handles the request.
package com.milesight.beaveriot.integrations.[integration-id].controller;
import com.milesight.beaveriot.base.response.ResponseBody;
import com.milesight.beaveriot.base.response.ResponseBuilder;
import com.milesight.beaveriot.context.api.DeviceServiceProvider;
import com.milesight.beaveriot.context.api.EntityValueServiceProvider;
import com.milesight.beaveriot.integrations.[integration-id].entity.MyDeviceEntities;
import com.milesight.beaveriot.integrations.[integration-id].service.MyDeviceService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/" + MyDeviceService.INTEGRATION_ID) // Should use integration identifier
public class MyIntegrationController {
@Autowired
private DeviceServiceProvider deviceServiceProvider;
@Autowired
private EntityValueServiceProvider entityValueServiceProvider;
@GetMapping("/active-count")
public ResponseBody<CountResponse> getActiveDeviceCount() {
List<String> statusEntityKeys = new ArrayList<>();
deviceServiceProvider.findAll(MyDeviceService.INTEGRATION_ID).forEach(device -> statusEntityKeys.add(device.getEntities().get(0).getKey()));
Long count = entityValueServiceProvider
.findValuesByKeys(statusEntityKeys)
.values()
.stream()
.map(n -> (long) n)
.filter(status -> status == MyDeviceEntities.DeviceStatus.ONLINE.ordinal())
.count();
CountResponse resp = new CountResponse();
resp.setCount(count);
return ResponseBuilder.success(resp);
}
@Data
public class CountResponse {
private Long count;
}
}
(Optional) Test Your Integration
In the beaver-iot-integrations
project, reinstall your integration module.
Go to the beaver-iot
project and make sure your integration has been added to the dependencies list of application/application-standard
and restart it.
Register User
curl --location 'http://localhost:9200/user/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "john.doe@example.com",
"nickname": "JohnDoe",
"password": "12#$qwER"
}'
Login User
curl --location 'http://192.168.43.46:9200/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username=john.doe@example.com' \
--data-urlencode 'password=12#$qwER' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=iab' \
--data-urlencode 'client_secret=milesight*iab'
The response data should look like this:
{
"data": {
"access_token":"***.****.***",
"refresh_token":"***",
"token_type":"Bearer",
"expires_in":86399
},
"status":"Success"
}
Record the access_token
:
access_token=***.****.***
Get Integration Information
curl --location --request GET 'http://localhost:9200/integration/[integration-id]' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $access_token" \
--data '{
}'
Add Device
For a device with IP 8.8.8.8
and name Test Device
:
curl --location 'http://localhost:9200/device' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $access_token" \
--data '{
"name": "Test Device",
"integration": "[integration-id]",
"param_entities": {
"[integration-id].integration.add_device.ip": "8.8.8.8"
}
}'
Search Devices
curl --location 'http://localhost:9200/device/search' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $access_token" \
--data '{
"name": ""
}'
Call Benchmark Service
curl --location 'http://localhost:9200/entity/service/call' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $access_token" \
--data '{
"exchange": {
"[integration-id].integration.benchmark": ""
}
}'
You should see logs in the console:
[Get-Report] {[integration-id].integration.detect_report.offline_count=1, [integration-id].integration.detect_report.consumed_time=5099, [integration-id].integration.detect_report.online_count=1}
Search Entities
curl --location 'http://localhost:9200/entity/search' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $access_token" \
--data '{
"keyword": "",
"page_size": 100
}'
Get Entity Value
For example, if the entity_key
for the entity [integration-id].device.8_8_8_8.status
has the ID 1879410769126817793
, get this entity's value:
curl --location --request GET 'http://localhost:9200/entity/1879410769126817793/status' \
--header "Authorization: Bearer $access_token" \
--header 'Content-Type: application/json'
Delete Device
For example, if the device ID is 1879410769026154498
, delete this device:
curl --location 'http://localhost:9200/device/batch-delete' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $access_token" \
--data '{
"device_id_list": ["1879410769026154498"]
}'
You can search for devices again to verify if the deletion was successful.