API:et är inspirerat/baserat på specifikationen för REST-API:et i RealEstateCore (även kallat REC).
POST /api/spaces
GET /api/spaces
POST /api/buildings
GET /api/buildings
POST /api/sensors
GET /api/sensors
POST /api/observations
GET /api/observations
root[id]
- id för root-objekt
root[type]
- typ för root-objekt
hasObservationTime[starting]
- starttidpunkt för tidsspann med observationer
hasObservationTime[ending]
- sluttidpunkt för tidsspann med observationer
page
aktuell sida att hämta
size
antal objekt (buildings, sensors, observations o.dyl.) i varje sida
api-rec
tar emot meddelanden och lagrar data i ett tidsserie format.
iot-events
konfigureras att POST ett cloudevent till api-rec endpoint /api/cloudevents
. Event som ska POST:as är message.accepted
och function.updated
.
subscribers:
- id: api-rec-devices
name: MessageAccepted
type: message.accepted
endpoint: http://api-rec:8080/api/cloudevents
source: github.com/diwise/iot-agent
eventType: message.accepted
tenants:
- default
entities:
- id: api-rec-functions
name: FunctionUpdated
type: function.updated
endpoint: http://api-rec:8080/api/cloudevents
source: github.com/diwise/iot-core
eventType: function.updated
tenants:
- default
entities:
Flödet blir då: sensor -> iot-agent -> iot-events -> cloudevents -> api-rec
api-rec
kommer tolka händelserna och skapa observations
från dem.
-> api-rec
En observation
kan också POST:as direkt till /api/observations
.
{
"format": "rec3.3",
"deviceId": "https://recref.com/device/64b65a99-a53c-47f5-b959-1c7a641d82d8",
"observations": [
{
"observationTime": "2019-05-27T20:07:44Z",
"value": 16.1,
"quantityKind": "https://w3id.org/rec/core/Temperature",
"sensorId" : "https://recref.com/sensor/e0d5120b-90f1-48d6-a47f-f8ccd7727b04"
}
]
}
Ett urval av typer som finns i spec för REC:
- Concentration
- Energy
- Force
- Illuminance
- Power
- Pressure
- RelativeHumidity
- Resistance
- Temperature
- Volume
Några extra har skapats
- diwise:AirQuality
- diwise:DigitalInput
- diwise:Level
- diwise:Lifebuoy
- diwise:Presence
- diwise:Timer
och fler eller andra kommer skapas vid behov.
https://github.com/RealEstateCore/rec/blob/main/API/Edge/edge_message.schema.json
Skillnden mellan deviceId
och sensorId
är att ett device
kan ha en eller flera sensor
er i samma "låda".
API för att stukturera fastigheter, byggnader, våningar, rum, m.m. Vi kan behöva fler/andra modeller från REC.
För närvarande finns endpoints för spaces
, buildings
och sensors
.
POST /spaces
{
"@context": "https://dev.realestatecore.io/contexts/Space.jsonld",
"@id": "00f67d60-d4d4-4bd5-af32-cf6c9b9310ec",
"@type": "dtmi:org:w3id:rec:Space;1"
}
POST /buildings
{
"@context": "https://dev.realestatecore.io/contexts/Building.jsonld",
"@id": "79b30db6-c5d3-4cd1-a438-6d8954b330ad",
"@type": "dtmi:org:w3id:rec:Building;1",
"isPartOf" : {
"@id": "00f67d60-d4d4-4bd5-af32-cf6c9b9310ec",
"@type": "dtmi:org:w3id:rec:Space;1"
}
}
POST /sensors
{
"@context": "https://dev.realestatecore.io/contexts/Sensor.jsonld",
"@id": "76bb4d31-1167-49e0-8766-768eb47c47e2",
"@type": "dtmi:org:brickschema:schema:Brick:Sensor;1",
"isPartOf" : {
"@id": "79b30db6-c5d3-4cd1-a438-6d8954b330ad",
"@type": "dtmi:org:w3id:rec:Building;1"
}
}
isPartOf
skapar relation mellan entiteter. Alla modeller har fler properties för metadata som inte finns med i spiken.
Exempel med /sensors
.
page=0
och size=10
är default om inget annat anges. Med dessa parametrar kan man hämta delar av dataset:et. hydra:totalItems
kommer att ha totalt antal objekt i det fullständiga svaret.
root[type]
och root[id]
finns inte i spec, men faller in under Advanced queries och är tänkt svara på frågor som "ge mig alla sensorer i byggnad X".
hydra:view
visas enbart om det finns en uppdelning av dataset:et.
Obs Används root[type]
och root[id]
så kommer inte page=0
och/eller size=10
att påverka något, utan då hämtas hela resultatet i samma fråga.
{
"@context": "http://www.w3.org/ns/hydra/context.jsonld",
"@id": "/api/sensors",
"@type": "hydra:Collection",
"hydra:totalItems": 32,
"hydra:member": [
{
"@context": "https://dev.realestatecore.io/contexts/Sensor.jsonld",
"@id": "76bb4d31-1167-49e0-8766-768eb47c47e2",
"@type": "dtmi:org:brickschema:schema:Brick:Sensor;1",
},
...
],
"hydra:view": {
"@id": "/api/sensors?page=3",
"@type": "hydra:PartialCollectionView",
"first": "/api/sensors?page=0&size=10",
"previous": "/api/sensors?page=2&size=10",
"last": "/api/sensors?page=3&size=10"
}
}
GET /sensors?root[type]=building&root[id]=79b30db6-c5d3-4cd1-a438-6d8954b330ad
Hämtar alla sensorer som finns i byggnaden med id 79b30db6-c5d3-4cd1-a438-6d8954b330ad
. type
måste anges då olika typer (spaces, buildings o.dyl.) kan ha samma ID.
{
"@context": "http://www.w3.org/ns/hydra/context.jsonld",
"@id": "/api/sensors",
"@type": "hydra:Collection",
"hydra:totalItems": 32,
"hydra:member": [
{
"@context": "https://dev.realestatecore.io/contexts/Sensor.jsonld",
"@id": "76bb4d31-1167-49e0-8766-768eb47c47e2",
"@type": "dtmi:org:brickschema:schema:Brick:Sensor;1",
},
...
]
}
Se Time interval queries för information.
För /observations
finns ?hasObservationTime[starting]
och hasObservationTime[ending]
för att få ut data för ett visst tidsintervall. Datum måste vara formaterade enl. RFC3339
page=0
och size=10
funkar för observations på samma sätt som för t.ex. /sensors
.
GET /observations?sensorId=76bb4d31-1167-49e0-8766-768eb47c47e2&hasObservationTime[starting]=2019-05-27T20:07:44Z&hasObservationTime[ending]=2019-06-27T20:07:44Z
{
"@context": "http://www.w3.org/ns/hydra/context.jsonld",
"@id": "/api/observations",
"@type": "hydra:Collection",
"hydra:totalItems": 78,
"hydra:member": [
{
"observationTime": "2020-04-27T10:18:12Z",
"value": 12380400000000,
"quantityKind": "Energy",
"sensorId": "vp1-em01"
},
...
],
"hydra:view": {
"@id": "/observations?sensorId=76bb4d31-1167-49e0-8766-768eb47c47e2&hasObservationTime[starting]=2019-05-27T20:07:44Z&hasObservationTime[ending]=2019-06-27T20:07:44Z",
"@type": "hydra:PartialCollectionView",
"first": "/observations?sensorId=76bb4d31-1167-49e0-8766-768eb47c47e2&hasObservationTime[starting]=2019-05-27T20:07:44Z&hasObservationTime[ending]=2019-06-27T20:07:44Z&page=0&size=10",
"previous": "/observations?sensorId=76bb4d31-1167-49e0-8766-768eb47c47e2&hasObservationTime[starting]=2019-05-27T20:07:44Z&hasObservationTime[ending]=2019-06-27T20:07:44Z&page=2&size=10",
"last": "/observations?sensorId=76bb4d31-1167-49e0-8766-768eb47c47e2&hasObservationTime[starting]=2019-05-27T20:07:44Z&hasObservationTime[ending]=2019-06-27T20:07:44Z&page=3&size=10"
}
}
Det finns logik som hindrar att samma värde lagras flera gånger inom en tidsperiod (nu 1 minut), dvs om sensor X skickar värdet 42
n gånger inom samma tidsperiod kommer enbart värdet lagras första gången, de andra gångerna kastas värdet. Om sensorn däremot skickar 42
, 43
, 42
inom samma tidsperiod kommer alla tre värden att lagras.
En graf skapas med två tabeller tills det behövs en riktig grafdatabashanterare.
CREATE TABLE IF NOT EXISTS entity (
node_id BIGSERIAL,
entity_id TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_context TEXT NOT NULL,
PRIMARY KEY (node_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS entity_entity_type_entity_id_unique_indx ON entity (entity_type, entity_id);
CREATE TABLE IF NOT EXISTS relation (
parent BIGINT NOT NULL,
child BIGINT NOT NULL,
PRIMARY KEY (parent, child)
);
CREATE INDEX IF NOT EXISTS relation_child_parent_indx ON relation(child, parent);
CREATE TABLE IF NOT EXISTS observations (
observation_id BIGSERIAL PRIMARY KEY,
device_id TEXT NOT NULL,
sensor_id TEXT NOT NULL,
observation_time TIMESTAMPTZ NOT NULL,
value NUMERIC NULL,
value_string TEXT NULL,
value_boolean BOOLEAN NULL,
quantity_kind TEXT NOT NULL
);
ALTER TABLE observations DROP CONSTRAINT IF EXISTS observations_device_id_sensor_id_observation_time_value_val_key;
CREATE UNIQUE INDEX IF NOT EXISTS observations_device_id_sensor_id_observation_time_quantity_kind_indx ON observations (device_id, sensor_id, observation_time, quantity_kind);
SQL för att svara på frågor som t.ex. "ge mig alla sensorer i byggnad X"
WITH RECURSIVE traverse(node_id, entity_type, entity_id) AS (
SELECT
node_id,
entity_type,
entity_id
FROM
entity
WHERE
entity.entity_id = $1 AND
entity.entity_type = $2
UNION ALL
SELECT
entity.node_id,
entity.entity_type,
entity.entity_id
FROM traverse JOIN
relation ON traverse.node_id = relation.parent JOIN
entity ON relation.child = entity.node_id
)
SELECT
traverse.entity_id
FROM traverse
WHERE traverse.entity_type = $3
GROUP BY traverse.entity_id
ORDER BY traverse.entity_id ASC
PK för en entitet är id
+ type
. $1
och $2
pekar ut root-entiteten och $3
den typ som ska hittas bland kopplade entiteter.
Möjligen måste type
vara dtmi:org:w3id:rec:Building;1
och inte bara building
. Men det finns en CSV med översättningar för endpoints.
Bättre säkerhet för magiska strängar om de läggs in i en ENUM?
CREATE TYPE entity_type AS ENUM (
'dtmi:org:w3id:rec:Space;1',
'dtmi:org:w3id:rec:Building;1',
'dtmi:org:brickschema:schema:Brick:Sensor;1',
'dtmi:org:w3id:rec:ObservationEvent;1'
);
CREATE TYPE entity_context AS ENUM (
'https://dev.realestatecore.io/contexts/Space.jsonld',
'https://dev.realestatecore.io/contexts/Building.jsonld',
'https://dev.realestatecore.io/contexts/Sensor.jsonld',
'https://dev.realestatecore.io/contexts/ObservationEvent.jsonld'
);
CREATE TYPE quantity_kind AS ENUM (
'diwise:AirQuality'
'diwise:DigitalInput',
'diwise:Presence',
'Acceleration',
'Angle',
'AngularAcceleration',
'AngularVelocity',
'Area',
'Capacitance',
'Concentration',
'Conductivity',
'DataRate',
'DataSize',
'Density',
'Distance',
'Efficiency',
'ElectricCharge',
'ElectricCurrent',
'Energy',
'Force',
'Frequency',
'Illuminance',
'Inductance',
'Irradiance',
'Length',
'Luminance',
'LuminousFlux',
'LuminousIntensity',
'MagneticFlux',
'MagneticFluxDensity',
'Mass',
'MassFlowRate',
'Power',
'PowerFactor',
'Pressure',
'RelativeHumidity',
'Resistance',
'SoundPressureLevel',
'Temperature',
'Thrust',
'Time',
'Torque',
'Velocity',
'Voltage',
'Volume',
'VolumeFlowRate'
);