Skip to content

Commit edac17a

Browse files
authored
Merge pull request #968 from jason-fox/feature/linked-data
Add linked data entities.
2 parents 1dcc863 + 8d2439d commit edac17a

File tree

7 files changed

+219
-2
lines changed

7 files changed

+219
-2
lines changed

CHANGES_NEXT_RELEASE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Add: basic NGSI-LD support as experimental feature (#842)
1212
- Lazy Attributes
1313
- Commands
1414
- Mixed mode (based in ngsiVersion field in the provisioning API)
15+
- Support to linked data entities to enable the traversal of the nodes of the knowledge graph
1516
Update codebase to use ES6
1617
- Remove JSHint and jshint overrides
1718
- Add esLint using standard tamia presets

doc/advanced-topics.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,58 @@ Other unrecognised `type` attributes will be passed as NGSI-LD data using the fo
185185
}
186186
```
187187

188+
189+
### NGSI-LD Linked Data support
190+
191+
`static_attributes` may be supplied with an additional `link` data element when provisioning an IoT Agent to ensure that active attributes from the provisioned IoT Device may be maintained in parallel with a linked data entity . Take for example a temperature gauge placed within a building.
192+
The **Device** data model literally represents the IoT device itself, but the `temperature` attribute also needs to be shared with the **Building** entity
193+
194+
A `link` between them can be provisioned as shown:
195+
196+
e.g.:
197+
198+
```json
199+
{
200+
"entity_type": "Device",
201+
"resource": "/iot/d",
202+
"protocol": "PDI-IoTA-UltraLight",
203+
..etc
204+
"attributes": [
205+
{"object_id": "l", "name": "temperature", "type":"Float",
206+
"metadata":{
207+
"unitCode":{"type": "Text", "value" :"CEL"}
208+
}
209+
}
210+
],
211+
"static_attributes": [
212+
{
213+
"name": "controlledAsset",
214+
"type": "Relationship",
215+
"value": "urn:ngsi-ld:Building:001",
216+
"link": {
217+
"attributes": ["temperature"],
218+
"name": "providedBy",
219+
"type": "Building"
220+
}
221+
}
222+
]
223+
}
224+
```
225+
226+
Whenever a `temperature` measure is received **Device** is updated, and entity `urn:ngsi-ld:Building:001` is also updated as shown:
227+
228+
```json
229+
"temperature": {
230+
"type": "Property",
231+
"value": 27.6,
232+
"unitCode": "CEL",
233+
"providedBy": {
234+
"type": "Relationship",
235+
"object": "urn:ngsi-ld:Device:thermometer1"
236+
}
237+
}
238+
```
239+
188240
### Data mapping plugins
189241

190242
The IoT Agent Library provides a plugin mechanism in order to facilitate reusing code that makes small transformations

lib/services/devices/registrationUtils.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,11 @@ function sendRegistrationsNgsiLD(unregister, deviceData, callback) {
464464
}
465465

466466
let id = String(deviceData.name);
467-
id = id.startsWith(NGSI_LD_URN) ? id : NGSI_LD_URN + deviceData.type + ':' + id;
467+
if (!id.startsWith(NGSI_LD_URN)) {
468+
id = NGSI_LD_URN + deviceData.type + ':' + id;
469+
logger.debug(context, 'Amending id to a valid URN: %s', id);
470+
}
471+
468472

469473
const options = {
470474
url: cbHost + '/ngsi-ld/v1/csourceRegistrations/',

lib/services/ngsi/entities-NGSI-LD.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,11 @@ function formatAsNGSILD(json) {
200200
switch (key) {
201201
case 'id':
202202
id = json[key];
203-
obj[key] = id.startsWith(NGSI_LD_URN) ? id : NGSI_LD_URN + json.type + ':' + id;
203+
obj[key] = id;
204+
if (!id.startsWith(NGSI_LD_URN)){
205+
obj[key] = NGSI_LD_URN + json.type + ':' + id;
206+
logger.debug(context, 'Amending id to a valid URN: %s', obj[key]);
207+
}
204208
break;
205209
case 'type':
206210
obj[key] = json[key];
@@ -350,6 +354,57 @@ function sendQueryValueNgsiLD(entityName, attributes, typeInformation, token, ca
350354
);
351355
}
352356

357+
/**
358+
* Adds any linked data items to the NGSI-LD payload for the context broker.
359+
* For example a Building entity linked to a temperature gauge Device will
360+
* also be updated as measures are received to update the Device Entity.
361+
*
362+
* @param {Object} typeInformation Configuration information for the device.
363+
* @param {Object} json Payload to send to the context broker
364+
*/
365+
function addLinkedEntities(typeInformation, json) {
366+
// use the supplied type if possible, but fallback to reading the
367+
// type from the id of the entity.
368+
function getEntityType(id, target) {
369+
if (target.type) {
370+
return target.type;
371+
}
372+
let calculatedType = id.replace(NGSI_LD_URN, '');
373+
const n = calculatedType.indexOf(':');
374+
calculatedType = calculatedType.substring(0, n !== -1 ? n : calculatedType.length);
375+
logger.debug(context, 'Guessing linked type from %s as no type attribute found', id);
376+
return calculatedType;
377+
}
378+
const linkedTypeInfo = {};
379+
if(typeInformation.staticAttributes){
380+
typeInformation.staticAttributes.forEach((item) => {
381+
if (item.link) {
382+
linkedTypeInfo[item.name] = item.link;
383+
}
384+
});
385+
}
386+
387+
Object.keys(linkedTypeInfo).forEach((sourceAttr) => {
388+
const sourceAttribute = json[0][sourceAttr];
389+
if (sourceAttribute) {
390+
const linkInfo = linkedTypeInfo[sourceAttr];
391+
const linkName = linkInfo.name || 'providedBy';
392+
const linkedEntity = {
393+
'@context': config.getConfig().contextBroker.jsonLdContext,
394+
id: sourceAttribute.object || sourceAttribute.value
395+
};
396+
linkedEntity.type = getEntityType(linkedEntity.id, linkInfo);
397+
linkInfo.attributes.forEach((attr) => {
398+
if (json[0][attr]) {
399+
linkedEntity[attr] = json[0][attr];
400+
linkedEntity[attr][linkName] = { type: 'Relationship', object: json[0].id };
401+
}
402+
});
403+
json.push(linkedEntity);
404+
}
405+
});
406+
}
407+
353408
/**
354409
* Makes an update in the Device's entity in the context broker, with the values given in the 'attributes' array.
355410
* This array should comply to the NGSI-LD's attribute format.
@@ -495,6 +550,10 @@ function sendUpdateValueNgsiLD(entityName, attributes, typeInformation, token, c
495550
return callback(new errors.BadGeocoordinates(JSON.stringify(payload)));
496551
}
497552

553+
if (typeInformation.active) {
554+
addLinkedEntities(typeInformation, options.json);
555+
}
556+
498557
logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url);
499558
logger.debug(
500559
context,

test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributesMetadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
"type": "Property",
1010
"value": "StaticValue"
1111
},
12+
"controlledAsset": {
13+
"type": "Relationship",
14+
"object": "urn:ngsi-ld:Building:001"
15+
},
1216
"id": "urn:ngsi-ld:Lamp:lamp1",
1317
"luminosity": {
1418
"type": "Property",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[
2+
{
3+
"@context": "http://context.json-ld",
4+
"luminosity": {
5+
"type": "Property",
6+
"value": 87,
7+
"providedBy": {
8+
"type": "Relationship",
9+
"object": "urn:ngsi-ld:Lamp:lamp1"
10+
}
11+
},
12+
"controlledAsset": {
13+
"type": "Relationship",
14+
"object": "urn:ngsi-ld:Building:001"
15+
},
16+
"controlledProperty": {
17+
"type": "Property",
18+
"value": "StaticValue",
19+
"includes": {
20+
"type": "Property",
21+
"value": "bell"
22+
}
23+
},
24+
"id": "urn:ngsi-ld:Lamp:lamp1",
25+
"type": "Lamp"
26+
},
27+
{
28+
"@context": "http://context.json-ld",
29+
"id": "urn:ngsi-ld:Building:001",
30+
"type": "Building",
31+
"luminosity": {
32+
"type": "Property",
33+
"value": 87,
34+
"providedBy": {
35+
"type": "Relationship",
36+
"object": "urn:ngsi-ld:Lamp:lamp1"
37+
}
38+
}
39+
}
40+
]

test/unit/ngsi-ld/ngsiService/active-devices-test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ const iotAgentConfig = {
120120
commands: [],
121121
lazy: [],
122122
staticAttributes: [
123+
{
124+
name: 'controlledAsset',
125+
type: 'Relationship',
126+
value: 'urn:ngsi-ld:Building:001'
127+
},
123128
{
124129
name: 'controlledProperty',
125130
type: 'Property',
@@ -145,6 +150,58 @@ const iotAgentConfig = {
145150
providerUrl: 'http://smartGondor.com'
146151
};
147152

153+
describe('NGSI-LD - Linked Data Active attributes test', function () {
154+
const values = [
155+
{
156+
name: 'luminosity',
157+
type: 'Number',
158+
value: 87
159+
}
160+
];
161+
162+
beforeEach(function () {
163+
logger.setLevel('FATAL');
164+
});
165+
166+
afterEach(function (done) {
167+
iotAgentLib.deactivate(done);
168+
});
169+
170+
describe('When the IoT Agent receives new information from a device with an active link', function () {
171+
beforeEach(function (done) {
172+
nock.cleanAll();
173+
174+
contextBrokerMock = nock('http://192.168.1.1:1026')
175+
.matchHeader('fiware-service', 'smartGondor')
176+
.post(
177+
'/ngsi-ld/v1/entityOperations/upsert/?options=update',
178+
utils.readExampleFile(
179+
'./test/unit/ngsi-ld/examples/contextRequests/updateContextStaticLinkedAttributes.json'
180+
)
181+
)
182+
183+
.reply(204);
184+
185+
const deepClonedObject = JSON.parse(JSON.stringify(iotAgentConfig));
186+
deepClonedObject.types.Lamp.staticAttributes[0].link = {
187+
attributes: ['luminosity'],
188+
name: 'providedBy',
189+
type: 'Building'
190+
};
191+
192+
iotAgentLib.activate(deepClonedObject, done);
193+
});
194+
195+
it('should change the value of the corresponding attribute in the context broker and the linked entity', function (done) {
196+
iotAgentLib.update('lamp1', 'Lamp', '', values, function (error) {
197+
should.not.exist(error);
198+
contextBrokerMock.done();
199+
done();
200+
});
201+
});
202+
});
203+
});
204+
148205
describe('NGSI-LD - Active attributes test', function () {
149206
const values = [
150207
{

0 commit comments

Comments
 (0)