Skip to content

Commit f65d0f9

Browse files
authored
2.0.6 - Allows to implicitly link asset to device without specifying every measure names (#258)
1 parent 16fd8b6 commit f65d0f9

File tree

7 files changed

+179
-20
lines changed

7 files changed

+179
-20
lines changed

features/Device/Controller/LinkAsset.feature

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Feature: LinkAsset
1818
| linkedDevices[0].measureNames[0].asset | "temperatureExt" |
1919
| linkedDevices[0].measureNames[0].device | "temperature" |
2020
| _kuzzle_info.updater | "-1" |
21+
# Link second device
2122
When I successfully execute the action "device-manager/devices":"linkAsset" with args:
2223
| _id | "DummyTemp-unlinked2" |
2324
| assetId | "Container-unlinked1" |
@@ -31,6 +32,34 @@ Feature: LinkAsset
3132
| linkedDevices[1]._id | "DummyTemp-unlinked2" |
3233
| linkedDevices[1].measureNames[0].asset | "temperatureInt" |
3334

35+
Scenario: Link device with default measure names
36+
Given I successfully execute the action "device-manager/devices":"linkAsset" with args:
37+
| _id | "DummyTemp-unlinked1" |
38+
| assetId | "Container-unlinked1" |
39+
| engineId | "engine-ayse" |
40+
| body.measureNames[0].device | "temperature" |
41+
| body.measureNames[0].asset | "temperatureExt" |
42+
When I successfully execute the action "device-manager/devices":"linkAsset" with args:
43+
| _id | "DummyTempPosition-unlinked3" |
44+
| assetId | "Container-unlinked1" |
45+
| engineId | "engine-ayse" |
46+
| implicitMeasuresLinking | true |
47+
| body.measureNames[0].device | "temperature" |
48+
| body.measureNames[0].asset | "temperatureInt" |
49+
And The document "engine-ayse":"assets":"Container-unlinked1" content match:
50+
# Keep the previously linked measure
51+
| linkedDevices[0]._id | "DummyTemp-unlinked1" |
52+
| linkedDevices[0].measureNames[0].asset | "temperatureExt" |
53+
| linkedDevices[0].measureNames[0].device | "temperature" |
54+
# Explicitly linked measure
55+
| linkedDevices[1]._id | "DummyTempPosition-unlinked3" |
56+
| linkedDevices[1].measureNames.length | 2 |
57+
| linkedDevices[1].measureNames[0].asset | "temperatureInt" |
58+
| linkedDevices[1].measureNames[0].device | "temperature" |
59+
# Implicitly linked measure
60+
| linkedDevices[1].measureNames[1].asset | "position" |
61+
| linkedDevices[1].measureNames[1].device | "position" |
62+
3463
Scenario: Error when device is already linked
3564
When I execute the action "device-manager/devices":"linkAsset" with args:
3665
| _id | "DummyTemp-linked1" |

features/fixtures/fixtures.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ const deviceAyseUnlinked2 = {
4646
};
4747
const deviceAyseUnlinked2Id = `${deviceAyseUnlinked2.model}-${deviceAyseUnlinked2.reference}`;
4848

49+
const deviceAyseUnlinked3 = {
50+
model: "DummyTempPosition",
51+
reference: "unlinked3",
52+
metadata: {},
53+
measures: {},
54+
engineId: "engine-ayse",
55+
assetId: null,
56+
};
57+
const deviceAyseUnlinked3Id = `${deviceAyseUnlinked3.model}-${deviceAyseUnlinked3.reference}`;
58+
4959
const assetAyseLinked1 = {
5060
model: "Container",
5161
reference: "linked1",
@@ -97,14 +107,21 @@ module.exports = {
97107
devices: [
98108
{ index: { _id: deviceAyseLinked1Id } },
99109
deviceAyseLinked1,
110+
100111
{ index: { _id: deviceAyseLinked2Id } },
101112
deviceAyseLinked2,
113+
102114
{ index: { _id: deviceDetached1Id } },
103115
deviceDetached1,
116+
104117
{ index: { _id: deviceAyseUnlinked1Id } },
105118
deviceAyseUnlinked1,
119+
106120
{ index: { _id: deviceAyseUnlinked2Id } },
107121
deviceAyseUnlinked2,
122+
123+
{ index: { _id: deviceAyseUnlinked3Id } },
124+
deviceAyseUnlinked3,
108125
],
109126
},
110127

@@ -113,18 +130,26 @@ module.exports = {
113130
devices: [
114131
{ index: { _id: deviceAyseLinked1Id } },
115132
deviceAyseLinked1,
133+
116134
{ index: { _id: deviceAyseLinked2Id } },
117135
deviceAyseLinked2,
136+
118137
{ index: { _id: deviceAyseUnlinked1Id } },
119138
deviceAyseUnlinked1,
139+
120140
{ index: { _id: deviceAyseUnlinked2Id } },
121141
deviceAyseUnlinked2,
142+
143+
{ index: { _id: deviceAyseUnlinked3Id } },
144+
deviceAyseUnlinked3,
122145
],
123146
assets: [
124147
{ index: { _id: assetAyseLinked1Id } },
125148
assetAyseLinked1,
149+
126150
{ index: { _id: assetAyseLinked2Id } },
127151
assetAyseLinked2,
152+
128153
{ index: { _id: assetAyseUnlinkedId } },
129154
assetAyseUnlinked,
130155
],

lib/modules/device/DeviceService.ts

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ import {
1818
} from "./../asset";
1919
import { InternalCollection, DeviceManagerConfiguration } from "../../core";
2020
import { Metadata, lock, ask, onAsk } from "../shared";
21-
import { AskModelDeviceGet } from "../model";
21+
import {
22+
AskModelAssetGet,
23+
AskModelDeviceGet,
24+
AssetModelContent,
25+
DeviceModelContent,
26+
} from "../model";
2227
import { MeasureContent } from "../measure";
2328

2429
import { DeviceContent } from "./types/DeviceContent";
@@ -417,13 +422,17 @@ export class DeviceService {
417422
deviceId: string,
418423
assetId: string,
419424
measureNames: ApiDeviceLinkAssetRequest["body"]["measureNames"],
420-
{ refresh }: { refresh?: any } = {}
425+
{
426+
refresh,
427+
implicitMeasuresLinking,
428+
}: { refresh?: any; implicitMeasuresLinking?: boolean } = {}
421429
): Promise<{
422430
asset: KDocument<AssetContent>;
423431
device: KDocument<DeviceContent>;
424432
}> {
425433
return lock(`device:linkAsset:${deviceId}`, async () => {
426434
const device = await this.get(this.config.adminIndex, deviceId);
435+
const engine = await this.getEngine(engineId);
427436

428437
this.checkAttachedToEngine(device);
429438

@@ -445,7 +454,26 @@ export class DeviceService {
445454
assetId
446455
);
447456

448-
this.checkAssetMeasureNamesAvailability(asset, measureNames);
457+
const [assetModel, deviceModel] = await Promise.all([
458+
ask<AskModelAssetGet>("ask:device-manager:model:asset:get", {
459+
engineGroup: engine.group,
460+
model: asset._source.model,
461+
}),
462+
ask<AskModelDeviceGet>("ask:device-manager:model:device:get", {
463+
model: device._source.model,
464+
}),
465+
]);
466+
467+
this.checkAlreadyProvidedMeasures(asset, measureNames);
468+
469+
if (implicitMeasuresLinking) {
470+
this.generateMissingAssetMeasureNames(
471+
asset,
472+
assetModel,
473+
deviceModel,
474+
measureNames
475+
);
476+
}
449477

450478
device._source.assetId = assetId;
451479
asset._source.linkedDevices.push({
@@ -514,22 +542,68 @@ export class DeviceService {
514542
* Checks if the asset does not already have a linked device using one of the
515543
* requested measure names.
516544
*/
517-
private checkAssetMeasureNamesAvailability(
545+
private checkAlreadyProvidedMeasures(
518546
asset: KDocument<AssetContent>,
519-
measureNames: ApiDeviceLinkAssetRequest["body"]["measureNames"]
547+
requestedMeasureNames: ApiDeviceLinkAssetRequest["body"]["measureNames"]
520548
) {
521-
const requestedMeasuresNames = measureNames.map((m) => m.asset);
549+
const measureAlreadyProvided = (assetMeasureName: string): boolean => {
550+
return asset._source.linkedDevices.some((link) =>
551+
link.measureNames.some((names) => names.asset === assetMeasureName)
552+
);
553+
};
522554

523-
for (const link of asset._source.linkedDevices) {
524-
const existingMeasureNames = link.measureNames.map((m) => m.asset);
555+
for (const name of requestedMeasureNames) {
556+
if (measureAlreadyProvided(name.asset)) {
557+
throw new BadRequestError(
558+
`Measure name "${name.asset}" is already used by another device on this asset.`
559+
);
560+
}
561+
}
562+
}
525563

526-
for (const requestedMeasuresName of requestedMeasuresNames) {
527-
if (existingMeasureNames.includes(requestedMeasuresName)) {
528-
throw new BadRequestError(
529-
`Measure name "${requestedMeasuresName}" is already used by another device on this asset.`
530-
);
531-
}
564+
/**
565+
* Goes through the device available measures and add them into the link if:
566+
* - they are not already provided by another device
567+
* - they are not already present in the link request
568+
* - they are declared in the asset model
569+
*/
570+
private generateMissingAssetMeasureNames(
571+
asset: KDocument<AssetContent>,
572+
assetModel: AssetModelContent,
573+
deviceModel: DeviceModelContent,
574+
requestedMeasureNames: ApiDeviceLinkAssetRequest["body"]["measureNames"]
575+
) {
576+
const measureAlreadyProvided = (deviceMeasureName: string): boolean => {
577+
return asset._source.linkedDevices.some((link) =>
578+
link.measureNames.some((names) => names.device === deviceMeasureName)
579+
);
580+
};
581+
582+
const measureAlreadyRequested = (deviceMeasureName: string): boolean => {
583+
return requestedMeasureNames.some(
584+
(names) => names.device === deviceMeasureName
585+
);
586+
};
587+
588+
const measureUndeclared = (deviceMeasureName: string): boolean => {
589+
return !assetModel.asset.measures.some(
590+
(measure) => measure.name === deviceMeasureName
591+
);
592+
};
593+
594+
for (const deviceMeasure of deviceModel.device.measures) {
595+
if (
596+
measureAlreadyRequested(deviceMeasure.name) ||
597+
measureAlreadyProvided(deviceMeasure.name) ||
598+
measureUndeclared(deviceMeasure.name)
599+
) {
600+
continue;
532601
}
602+
603+
requestedMeasureNames.push({
604+
asset: deviceMeasure.name,
605+
device: deviceMeasure.name,
606+
});
533607
}
534608
}
535609

@@ -703,4 +777,14 @@ export class DeviceService {
703777
);
704778
}
705779
}
780+
781+
private async getEngine(engineId: string): Promise<JSONObject> {
782+
const engine = await this.sdk.document.get(
783+
this.config.adminIndex,
784+
InternalCollection.CONFIG,
785+
`engine-device-manager--${engineId}`
786+
);
787+
788+
return engine._source.engine;
789+
}
706790
}

lib/modules/device/DevicesController.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ export class DevicesController {
237237
const measureNames = request.getBodyArray(
238238
"measureNames"
239239
) as ApiDeviceLinkAssetRequest["body"]["measureNames"];
240+
const implicitMeasuresLinking = request.getBoolean(
241+
"implicitMeasuresLinking"
242+
);
240243
const refresh = request.getRefresh();
241244

242245
const { asset, device } = await this.deviceService.linkAsset(
@@ -245,7 +248,7 @@ export class DevicesController {
245248
deviceId,
246249
assetId,
247250
measureNames,
248-
{ refresh }
251+
{ implicitMeasuresLinking, refresh }
249252
);
250253

251254
return {

lib/modules/device/types/DeviceApi.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,27 @@ export interface ApiDeviceLinkAssetRequest extends DevicesControllerRequest {
112112

113113
assetId: string;
114114

115+
/**
116+
* This option allows to not specify the names of all the measures that should
117+
* be linked to the asset.
118+
*
119+
* The algorithm will go through all the measures names provided by the device
120+
* and add the one who are present with the same name in the asset.
121+
*
122+
* It will not add the measure if:
123+
* - it has been specified in the link request
124+
* - it was already present in the asset
125+
*
126+
* @example
127+
* if the device provide a measure of type "temperature" with the name "temp"
128+
* if the asset has declared a measure of type "temperature" with the name "temp"
129+
* then the measure will be automatically added in the link and will later be propagated to the asset
130+
*/
131+
implicitMeasuresLinking?: boolean;
132+
115133
body?: {
116134
/**
117-
* Names of the linked measures
135+
* Names of the linked measures.
118136
*
119137
* Array<{ asset: string, device: string }>
120138
*
@@ -124,7 +142,7 @@ export interface ApiDeviceLinkAssetRequest extends DevicesControllerRequest {
124142
* { asset: "externalTemperature", device: "temperature" }
125143
* ]
126144
*/
127-
measureNames: Array<{ asset: string; device: string }>;
145+
measureNames?: Array<{ asset: string; device: string }>;
128146
};
129147
}
130148
export type ApiDeviceLinkAssetResult = {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kuzzle-device-manager",
3-
"version": "2.0.5",
3+
"version": "2.0.6",
44
"description": "Manage your IoT devices and assets. Choose a provisioning strategy, receive and decode payload, handle your IoT business logic.",
55
"author": "The Kuzzle Team (support@kuzzle.io)",
66
"repository": {

0 commit comments

Comments
 (0)