Skip to content

Commit 6901b5a

Browse files
authored
Merge pull request #1214 from JKRhb/coap-observe
fix(coap-server): add missing cov:observe subprotocol
2 parents 8adf840 + 5a58d39 commit 6901b5a

3 files changed

Lines changed: 217 additions & 18 deletions

File tree

packages/binding-coap/src/coap-server.ts

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,17 @@ import { Server, createServer, registerFormat, IncomingMessage, OutgoingMessage
3232
import slugify from "slugify";
3333
import { Readable } from "stream";
3434
import { MdnsIntroducer } from "./mdns-introducer";
35-
import { PropertyElement, DataSchema } from "wot-thing-description-types";
35+
import { PropertyElement, DataSchema, ActionElement, EventElement } from "wot-thing-description-types";
3636
import { CoapServerConfig } from "./coap";
3737
import { DataSchemaValue } from "wot-typescript-definitions";
38+
import { filterPropertyObserveOperations, getPropertyOpValues } from "./util";
3839

3940
const { debug, warn, info, error } = createLoggers("binding-coap", "coap-server");
4041

4142
type CoreLinkFormatParameters = Map<string, string[] | number[]>;
4243

44+
type AffordanceElement = PropertyElement | ActionElement | EventElement;
45+
4346
// TODO: Move to core?
4447
type AugmentedInteractionOptions = WoT.InteractionOptions & { formIndex: number };
4548

@@ -228,21 +231,45 @@ export default class CoapServer implements ProtocolServer {
228231
return opValues;
229232
}
230233

234+
private addFormToAffordance(form: TD.Form, affordance: AffordanceElement): void {
235+
const affordanceForms = affordance.forms;
236+
if (affordanceForms == null) {
237+
affordance.forms = [form];
238+
} else {
239+
affordanceForms.push(form);
240+
}
241+
}
242+
231243
private fillInPropertyBindingData(thing: ExposedThing, base: string, offeredMediaType: string) {
232244
for (const [propertyName, property] of Object.entries(thing.properties)) {
233-
const opValues = ProtocolHelpers.getPropertyOpValues(property);
234-
const form = this.createAffordanceForm(
235-
base,
236-
this.PROPERTY_DIR,
237-
offeredMediaType,
238-
opValues,
239-
thing.uriVariables,
240-
propertyName,
241-
property.uriVariables
242-
);
245+
const [readWriteOpValues, observeOpValues] = getPropertyOpValues(property);
246+
for (const formOpValues of [observeOpValues, readWriteOpValues]) {
247+
if (formOpValues.length === 0) {
248+
continue;
249+
}
250+
251+
let subprotocol: string | undefined;
252+
253+
const observeOpValues = filterPropertyObserveOperations(formOpValues);
254+
255+
if (observeOpValues.length > 0) {
256+
subprotocol = "cov:observe";
257+
}
258+
259+
const form = this.createAffordanceForm(
260+
base,
261+
this.PROPERTY_DIR,
262+
offeredMediaType,
263+
formOpValues,
264+
thing.uriVariables,
265+
propertyName,
266+
property.uriVariables,
267+
subprotocol
268+
);
243269

244-
property.forms.push(form);
245-
this.logHrefAssignment(form, "Property", propertyName);
270+
this.addFormToAffordance(form, property);
271+
this.logHrefAssignment(form, "Property", propertyName);
272+
}
246273
}
247274
}
248275

@@ -258,7 +285,7 @@ export default class CoapServer implements ProtocolServer {
258285
action.uriVariables
259286
);
260287

261-
action.forms.push(form);
288+
this.addFormToAffordance(form, action);
262289
this.logHrefAssignment(form, "Action", actionName);
263290
}
264291
}
@@ -272,10 +299,11 @@ export default class CoapServer implements ProtocolServer {
272299
["subscribeevent", "unsubscribeevent"],
273300
thing.uriVariables,
274301
eventName,
275-
event.uriVariables
302+
event.uriVariables,
303+
"cov:observe"
276304
);
277305

278-
event.forms.push(form);
306+
this.addFormToAffordance(form, event);
279307
this.logHrefAssignment(form, "Event", eventName);
280308
}
281309
}
@@ -287,7 +315,8 @@ export default class CoapServer implements ProtocolServer {
287315
opValues: string | string[],
288316
thingUriVariables: PropertyElement["uriVariables"],
289317
affordanceName?: string,
290-
affordanceUriVariables?: PropertyElement["uriVariables"]
318+
affordanceUriVariables?: PropertyElement["uriVariables"],
319+
subprotocol?: string
291320
): TD.Form {
292321
const affordanceNamePattern = Helpers.updateInteractionNameWithUriVariablePattern(
293322
affordanceName ?? "",
@@ -303,6 +332,7 @@ export default class CoapServer implements ProtocolServer {
303332

304333
const form = new TD.Form(href, offeredMediaType);
305334
form.op = opValues;
335+
form.subprotocol = subprotocol;
306336

307337
return form;
308338
}

packages/binding-coap/src/util.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/********************************************************************************
2+
* Copyright (c) 2018 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
* Document License (2015-05-13) which is available at
11+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
14+
********************************************************************************/
15+
16+
import { ProtocolHelpers } from "@node-wot/core";
17+
import { PropertyElement } from "wot-thing-description-types";
18+
19+
const observeOpFilter = ["observeproperty", "unobserveproperty"];
20+
const readWriteOpFilter = ["readproperty", "writeproperty"];
21+
const eventOpFilter = ["subscribeevent", "unsubscribeevent"];
22+
23+
function filterOpValues(opValues: string[], filterValues: string[]) {
24+
return opValues.filter((opValue) => filterValues.includes(opValue));
25+
}
26+
27+
/**
28+
* Convenience function to filter out the `op` values "observeproperty" and
29+
* "unobserveproperty" from a string array.
30+
*
31+
* @param opValues The `op` values to be filtered.
32+
* @returns A filtered array that might be empty.
33+
*/
34+
export function filterPropertyObserveOperations(opValues: string[]) {
35+
return filterOpValues(opValues, observeOpFilter);
36+
}
37+
38+
/**
39+
* Convenience function to filter out the `op` values "readproperty" and
40+
* "writeproperty" from a string array.
41+
*
42+
* @param opValues The `op` values to be filtered.
43+
* @returns A filtered array that might be empty.
44+
*/
45+
export function filterPropertyReadWriteOperations(opValues: string[]) {
46+
return filterOpValues(opValues, readWriteOpFilter);
47+
}
48+
49+
/**
50+
* Convenience function to filter out the `op` values "subscribeevent" and
51+
* "unsubscribeevent" from a string array.
52+
*
53+
* @param opValues The `op` values to be filtered.
54+
* @returns A filtered array that might be empty.
55+
*/
56+
export function filterEventOperations(opValues: string[]) {
57+
return filterOpValues(opValues, eventOpFilter);
58+
}
59+
60+
/**
61+
* Function to (potentially) generate two arrays of `op` values: One with the
62+
* values "readproperty" and "writeproperty", and one with
63+
* "observerproperty" and "unobserveproperty".
64+
*
65+
* This CoAP-specific distinction is made to be able to generate
66+
* separate forms for the observe-related operations, where the addition
67+
* of a `subprotocol` field with a value of `cov:observe` has to be added.
68+
*
69+
* @param property The property for which the forms are going to be
70+
* generated.
71+
* @returns A tuple consisting of two op value arrays (one for read and
72+
* write operations, one for observe-related operations).
73+
*/
74+
export function getPropertyOpValues(property: PropertyElement) {
75+
const opValues = ProtocolHelpers.getPropertyOpValues(property);
76+
77+
const readWriteOpValues = filterPropertyReadWriteOperations(opValues);
78+
const observeOpValues = filterPropertyObserveOperations(opValues);
79+
80+
return [readWriteOpValues, observeOpValues];
81+
}

packages/binding-coap/test/coap-server-test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ import Servient, { ExposedThing, Content } from "@node-wot/core";
2020

2121
import { suite, test } from "@testdeck/mocha";
2222
import { expect, should } from "chai";
23-
import { DataSchemaValue, InteractionInput, InteractionOptions } from "wot-typescript-definitions";
23+
import { DataSchemaValue, InteractionInput, InteractionOptions, ThingDescription } from "wot-typescript-definitions";
2424
import * as TD from "@node-wot/td-tools";
2525
import CoapServer from "../src/coap-server";
2626
import { CoapClient } from "../src/coap";
2727
import { Readable } from "stream";
2828
import { IncomingMessage, registerFormat, request } from "coap";
29+
import { filterEventOperations, filterPropertyObserveOperations, filterPropertyReadWriteOperations } from "../src/util";
2930

3031
// should must be called to augment all variables
3132
should();
@@ -634,4 +635,91 @@ class CoapServerTest {
634635

635636
await coapServer.stop();
636637
}
638+
639+
@test async "should add the cov:observe subprotocol value to obervable properties and events "() {
640+
const coapServer = new CoapServer({ port: 5683 });
641+
const servient = new Servient();
642+
servient.addServer(coapServer);
643+
644+
await coapServer.start(servient);
645+
646+
const covObserveThing = new ExposedThing(servient, {
647+
title: "Test",
648+
properties: {
649+
observableProperty: {
650+
observable: true,
651+
},
652+
nonObservableProperty: {},
653+
},
654+
events: {
655+
testEvent: {},
656+
},
657+
});
658+
659+
await coapServer.expose(covObserveThing);
660+
661+
await new Promise<void>((resolve) => {
662+
const req = request({
663+
host: "localhost",
664+
pathname: "test",
665+
port: 5683,
666+
method: "GET",
667+
});
668+
req.on("response", (res: IncomingMessage) => {
669+
const payload = res.payload.toString();
670+
const td = JSON.parse(payload) as ThingDescription;
671+
672+
for (const property of Object.values(td.properties!)) {
673+
let observeOpValueFormCount = 0;
674+
for (const form of property.forms) {
675+
const opValues = form.op!;
676+
expect(opValues.length).to.be.greaterThan(0);
677+
678+
const observeOpValueCount = filterPropertyObserveOperations(opValues as Array<string>).length;
679+
const observeOpValuePresent = observeOpValueCount > 0;
680+
681+
if (observeOpValuePresent) {
682+
observeOpValueFormCount++;
683+
expect(form.subprotocol).to.eql("cov:observe");
684+
}
685+
686+
const readWriteOpValueCount = filterPropertyReadWriteOperations(
687+
opValues as Array<string>
688+
).length;
689+
const readWriteOpValuePresent = readWriteOpValueCount > 0;
690+
691+
// eslint-disable-next-line no-unused-expressions
692+
expect(observeOpValuePresent && readWriteOpValuePresent).to.not.be.true;
693+
694+
if (property.observable !== true) {
695+
expect(observeOpValueCount).to.eql(0);
696+
}
697+
}
698+
699+
if (property.observable === true) {
700+
expect(observeOpValueFormCount).to.be.greaterThan(0);
701+
}
702+
}
703+
704+
for (const event of Object.values(td.events!)) {
705+
for (const form of event.forms) {
706+
const opValues = form.op!;
707+
expect(opValues.length > 0);
708+
709+
const eventOpValueCount = filterEventOperations(opValues as Array<string>).length;
710+
const eventOpValueCountPresent = eventOpValueCount > 0;
711+
712+
expect(eventOpValueCountPresent);
713+
714+
expect(form.subprotocol === "cov:observe");
715+
}
716+
}
717+
718+
resolve();
719+
});
720+
req.end();
721+
});
722+
723+
await coapServer.stop();
724+
}
637725
}

0 commit comments

Comments
 (0)