diff --git a/README.md b/README.md index c42c075..c68582e 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ The Test Thing is a total toy device that users can try different types of prope ### Smart Home Mashup -See the mashup's [readme](./mashups//smart-home/README.md). +See the mashup's [readme](./mashups/smart-home/README.md). ## How to Run @@ -139,10 +139,10 @@ For Node.js-based devices, we use npm workspaces and running `npm install` at th ### Saving Grafana Dashboards -Grafana dashboard json files are stored in [./conf/grafana/dashboards](./conf//grafana//dashboards/). +Grafana dashboard json files are stored in [./conf/grafana/dashboards](./conf/grafana/dashboards/). To save your newly created dashboard locally and push it into the remote repository: - Export the dashboard as JSON file using Share > Export. - - Save the exported JSON file to [./conf/grafana/dashboards](./conf//grafana//dashboards/). + - Save the exported JSON file to [./conf/grafana/dashboards](./conf/grafana/dashboards/). If your dashboard uses another datasource than our default `prometheus-datasource`, new datasource also must be provisioned in [./conf/grafana/datasources](./conf/grafana/provisioning/datasources/). For more information check Grafana's provisioning [documentation](https://grafana.com/docs/grafana/latest/administration/provisioning/). \ No newline at end of file diff --git a/mashups/smart-home/things/presence-sensor.ts b/mashups/smart-home/things/presence-sensor.ts index 119ba9a..197942f 100644 --- a/mashups/smart-home/things/presence-sensor.ts +++ b/mashups/smart-home/things/presence-sensor.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/mashups/smart-home/things/simple-coffee-machine.ts b/mashups/smart-home/things/simple-coffee-machine.ts index 1180e34..fa34be8 100644 --- a/mashups/smart-home/things/simple-coffee-machine.ts +++ b/mashups/smart-home/things/simple-coffee-machine.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/mashups/smart-home/things/smart-clock.ts b/mashups/smart-home/things/smart-clock.ts index 6b11e08..be31ae1 100644 --- a/mashups/smart-home/things/smart-clock.ts +++ b/mashups/smart-home/things/smart-clock.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/package-lock.json b/package-lock.json index 041a605..d519327 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,6 +121,17 @@ "slugify": "^1.4.5" } }, + "node_modules/@node-wot/binding-modbus": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/@node-wot/binding-modbus/-/binding-modbus-0.8.16.tgz", + "integrity": "sha512-7OAX4SduuE8Sm/XrXdv7kaSyykJ/xtRpmg5+EcXl9YrAORHT3SdFZPsvoGz/UGsAE7R95VW55r19Yx9h3rOOOw==", + "dependencies": { + "@node-wot/core": "0.8.16", + "modbus-serial": "^8.0.17", + "rxjs": "5.5.11", + "wot-typescript-definitions": "0.8.0-SNAPSHOT.29" + } + }, "node_modules/@node-wot/binding-mqtt": { "version": "0.8.16", "license": "EPL-2.0 OR W3C-20150513", @@ -4354,6 +4365,8 @@ "version": "1.0.0", "license": "EPL-2.0 OR W3C-20150513", "dependencies": { + "@node-wot/binding-modbus": "^0.8.16", + "@node-wot/core": "^0.8.16", "dotenv": "^16.3.1", "json-placeholder-replacer": "^1.0.35", "modbus": "^1.1.1", diff --git a/scripts/runTests.sh b/scripts/runTests.sh index b87c768..ff8fdd8 100755 --- a/scripts/runTests.sh +++ b/scripts/runTests.sh @@ -37,17 +37,18 @@ for tmd in things/* ; do npm run build fi - td_result="$(../../../../node_modules/mocha/bin/mocha.js --exit --timeout 5000)" + td_result=$(../../../../node_modules/mocha/bin/mocha.js) td_exit_code=$? cd $current_path if [ $td_exit_code -ne 0 ]; then echo -e "\033[0;31m** TD test failed for the thing $tdd.\033[0m" - echo $td_result + echo "$td_result" return_value=1 continue else echo -e "\033[0;32m** TD test successful for $tdd.\033[0m" + echo "$td_result" fi done echo "-----" diff --git a/things/advanced-coffee-machine/http/ts/test/client.test.ts b/things/advanced-coffee-machine/http/ts/test/client.test.ts index 77057bb..642e36e 100644 --- a/things/advanced-coffee-machine/http/ts/test/client.test.ts +++ b/things/advanced-coffee-machine/http/ts/test/client.test.ts @@ -1,30 +1,19 @@ import chai from 'chai' -import chaiAsPromised from 'chai-as-promised'; +import chaiAsPromised from 'chai-as-promised' -import { Servient } from "@node-wot/core"; +import { Servient } from "@node-wot/core" import { HttpClientFactory } from "@node-wot/binding-http" +import { port } from './fixtures' chai.use(chaiAsPromised) const expect = chai.expect let servient = new Servient() servient.addClientFactory(new HttpClientFactory()) -const port = 3000 let thing: WoT.ConsumedThing -const readProperty = async (thing: WoT.ConsumedThing, name: string): Promise => { - try { - const res = await thing.readProperty(name) - const value = await res.value() - return value - } - catch (error) { - console.error(`Error: ${error}`) - } -} - describe("Client Tests", () => { before(async () => { try { @@ -35,6 +24,10 @@ describe("Client Tests", () => { console.error(error) } }) + + after(async () => { + await servient.shutdown() + }) it("should read allAvailableResources property", async () => { const response = await thing.readProperty("allAvailableResources") diff --git a/things/advanced-coffee-machine/http/ts/test/fixtures.ts b/things/advanced-coffee-machine/http/ts/test/fixtures.ts index af7659c..2c66558 100644 --- a/things/advanced-coffee-machine/http/ts/test/fixtures.ts +++ b/things/advanced-coffee-machine/http/ts/test/fixtures.ts @@ -4,11 +4,11 @@ import path from "path" let thingProcess: ChildProcess | undefined let response: ThingStartResponse -const port = 3000 +export const port = 3000 export async function mochaGlobalSetup() { try { - response = await getInitiateMain(path.join(__dirname, '..', 'dist', 'main.js'), port) + response = await getInitiateMain('node', [path.join(__dirname, '..', 'dist', 'main.js'), '-p', `${port}`]) thingProcess = response.process } catch(error: any) { diff --git a/things/advanced-coffee-machine/http/ts/test/td.test.ts b/things/advanced-coffee-machine/http/ts/test/td.test.ts index a651471..ae31885 100644 --- a/things/advanced-coffee-machine/http/ts/test/td.test.ts +++ b/things/advanced-coffee-machine/http/ts/test/td.test.ts @@ -2,10 +2,10 @@ import * as chai from 'chai' import * as http from 'http' import { getTDValidate } from '../../../../../util/util' import { ValidateFunction } from 'ajv' +import { port } from './fixtures' const expect = chai.expect -const port = 3000 let validate: ValidateFunction | undefined describe("TD Test", () => { diff --git a/things/calculator/coap/js/.mocharc.json b/things/calculator/coap/js/.mocharc.json new file mode 100644 index 0000000..a665287 --- /dev/null +++ b/things/calculator/coap/js/.mocharc.json @@ -0,0 +1,5 @@ +{ + "exit": true, + "spec": "./test/**.test.js", + "require": ["./test/fixtures.js"] +} \ No newline at end of file diff --git a/things/calculator/coap/js/test/client.test.js b/things/calculator/coap/js/test/client.test.js new file mode 100644 index 0000000..d5d2e21 --- /dev/null +++ b/things/calculator/coap/js/test/client.test.js @@ -0,0 +1,218 @@ +const chai = require("chai") +const chaiAsPromised = require("chai-as-promised") + +const { Servient } = require("@node-wot/core") +const { CoapClientFactory } = require("@node-wot/binding-coap") +const { simplePort, contentNegotiationPort } = require('./fixtures') + +chai.use(chaiAsPromised) +const expect = chai.expect + +const servient = new Servient() +servient.addClientFactory(new CoapClientFactory()) +let WoT + +const readProperty = async (thing, propertyName) => { + try { + const response = await thing.readProperty(propertyName) + return await response.value() + } catch(error) { + console.error(`Error: ${error}`) + } +} + +describe("Client Tests", () => { + before(async () => { + try { + WoT = await servient.start() + } catch(error) { + console.error(error) + } + }) + after(async () => { + await servient.shutdown() + }) + + describe("Simple Calculator", () => { + let thing + + before(async () => { + try { + const td = await WoT.requestThingDescription(`coap://localhost:${simplePort}/coap-calculator-simple`) + thing = await WoT.consume(td) + } catch(error) { + console.error(error) + } + }) + + describe("result property", () => { + it("should return initial value", async () => { + const value = await readProperty(thing, "result") + expect(value).to.be.equal(0) + }) + + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + await thing.invokeAction("add", valueToAdd) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue + valueToAdd) + }) + + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + await thing.invokeAction("subtract", valueToSubtract) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("lastChange property", () => { + it("should observe a change when the result is changed", async () => { + setTimeout(async () => { + await thing.invokeAction('add', 1) + }, 200) + + let value + const subscription = thing.observeProperty('lastChange', async (response) => { + value = await response.value() + }) + + setTimeout(async () => { + expect(value).to.be.not.undefined + await subscription.stop() + }) + }) + }) + + describe("add action", () => { + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + const response = await thing.invokeAction("add", valueToAdd) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue + valueToAdd) + }) + }) + + describe("subtract action", () => { + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + const response = await thing.invokeAction("subtract", valueToSubtract) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("update event", () => { + it("should return the update message when subscribed", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 13 + + const actionTimeout = setInterval(async () => { + await thing.invokeAction('add', valueToAdd) + }, 200) + + const subscription = await thing.subscribeEvent("update", async (response) => { + await expect(response.value).to.have.eventually.be.equal(resultValue + valueToAdd) + }) + + await subscription.stop() + }) + }) + }) + + describe("Content Negotiation Calculator", () => { + let thing + + before(async () => { + try { + const td = await WoT.requestThingDescription(`coap://localhost:${contentNegotiationPort}/coap-calculator-content-negotiation`) + thing = await WoT.consume(td) + } catch(error) { + console.error(error) + } + }) + + describe("result property", () => { + it("should return initial value", async () => { + const value = await readProperty(thing, "result") + expect(value).to.be.equal(0) + }) + + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + await thing.invokeAction("add", valueToAdd) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue + valueToAdd) + }) + + it("should return sum when subtracting value from the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + await thing.invokeAction("subtract", valueToSubtract) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("lastChange property", () => { + it("should observe a change when the result is changed", async () => { + setTimeout(async () => { + await thing.invokeAction('add', 1) + }, 200) + + let value + const subscription = thing.observeProperty('lastChange', async (response) => { + value = await response.value() + }) + + setTimeout(async () => { + expect(value).to.be.not.undefined + await subscription.stop() + }) + }) + }) + + describe("add action", () => { + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + const response = await thing.invokeAction("add", valueToAdd) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue + valueToAdd) + }) + }) + + describe("subtract action", () => { + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + const response = await thing.invokeAction("subtract", valueToSubtract) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("update event", () => { + it("should return the update message when subscribed", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 13 + + const actionTimeout = setInterval(async () => { + await thing.invokeAction('add', valueToAdd) + }, 200) + + const subscription = await thing.subscribeEvent("update", async (response) => { + await expect(response.value).to.have.eventually.be.equal(resultValue + valueToAdd) + }) + + await subscription.stop() + }) + }) + }) +}) + diff --git a/things/calculator/coap/js/test/fixtures.js b/things/calculator/coap/js/test/fixtures.js new file mode 100644 index 0000000..1574569 --- /dev/null +++ b/things/calculator/coap/js/test/fixtures.js @@ -0,0 +1,45 @@ +const { getInitiateMain } = require("../../../../../util/dist/util") +const path = require("node:path") + +let simpleThingProcess +let contentNegotiationThingProcess +let response +const simplePort = 5683 +const contentNegotiationPort = 5684 + +const mochaGlobalSetup = async function() { + try { + response = await getInitiateMain('node', [path.join(__dirname, '..', 'coap-simple-calculator.js'), '-p', `${simplePort}`]) + simpleThingProcess = response.process + } + catch(error) { + console.error(error) + simpleThingProcess = error.process + } + + try { + response = await getInitiateMain('node', [path.join(__dirname, '..', 'coap-content-negotiation-calculator.js'), '-p', `${contentNegotiationPort}`]) + contentNegotiationThingProcess = response.process + } + catch (error) { + console.error(error) + contentNegotiationThingProcess = error.process + } +} + +const mochaGlobalTeardown = function() { + if(simpleThingProcess) { + simpleThingProcess.kill() + } + + if(contentNegotiationThingProcess) { + contentNegotiationThingProcess.kill() + } +} + +module.exports = { + simplePort, + contentNegotiationPort, + mochaGlobalSetup, + mochaGlobalTeardown +} \ No newline at end of file diff --git a/things/calculator/coap/js/test/td.test.js b/things/calculator/coap/js/test/td.test.js index 54bbd3c..9823a30 100644 --- a/things/calculator/coap/js/test/td.test.js +++ b/things/calculator/coap/js/test/td.test.js @@ -1,79 +1,83 @@ -const Ajv = require('ajv') const chai = require('chai') -const https = require('https') const coap = require('coap') -const path = require('path') - -const spawn = require('child_process').spawn - -const ajv = new Ajv({ strict: false, allErrors: true, validateFormats: false }) +const cbor = require('cbor') +const { getTDValidate } = require('../../../../../util/dist/util') +const { simplePort, contentNegotiationPort } = require('./fixtures') const expect = chai.expect -const port = 5683 -let thingProcess describe('Calculator CoAP JS', () => { let validate before(async () => { - const initiateMain = new Promise(async (resolve, reject) => { - thingProcess = spawn( - 'node', - ['coap-simple-calculator.js', '-p', `${port}`], - { cwd: path.join(__dirname, '..') } - ) - thingProcess.stdout.on('data', (data) => { - if (data.toString().trim() === 'ThingIsReady') { - resolve('Success') - } - }) - thingProcess.stderr.on('data', (data) => { - reject(`Error: ${data}`) - }) - thingProcess.on('error', (error) => { - reject(`Error: ${error}`) - }) - thingProcess.on('close', () => { - reject('Failed to initiate the main script.') + const tdValidate = getTDValidate() + + try { + const response = await Promise.all([tdValidate]) + validate = response[0].validate + } + catch (error) { + console.log(error) + } + }) + + describe('Calculator Simple', () => { + it('should have a valid TD', (done) => { + const req = coap.request(`coap://localhost:${simplePort}/coap-calculator-simple`) + + req.on('response', (res) => { + const valid = validate(JSON.parse(res.payload.toString())) + expect(valid).to.be.true + done() }) + + req.end() }) + }) - const getJSONSchema = new Promise((resolve, reject) => { - https.get('https://raw.githubusercontent.com/w3c/wot-thing-description/main/validation/td-json-schema-validation.json', function (response) { - const body = [] - response.on('data', (chunk) => { - body.push(chunk) - }) - - response.on('end', () => { - const tdSchema = JSON.parse(Buffer.concat(body).toString()) - validate = ajv.compile(tdSchema) - resolve('Success') - }) + describe('Calculator Content Negotiation', () => { + it('should have a valid application/json TD', (done) => { + const req = coap.request({ + method: 'GET', + observe: false, + host: 'localhost', + port: contentNegotiationPort, + pathname: 'coap-calculator-content-negotiation', + headers: { + "Accept": 'application/json' + } + }) + + req.on('response', (res) => { + const valid = validate(JSON.parse(res.payload.toString())) + expect(valid).to.be.true + done() }) - }) - await Promise.all([initiateMain, getJSONSchema]).then(data => { - if (data[0] !== 'Success' || data[1] !== 'Success') { - console.log(`initiateMain: ${data[0]}`) - console.log(`getJSONSchema: ${data[1]}`) - } + req.end() }) - }) - - after(() => { - thingProcess.kill() - }) - it('should have a valid TD', (done) => { - const req = coap.request(`coap://localhost:${port}/coap-calculator-simple`) + it('should have a valid application/cbor TD', (done) => { + const req = coap.request({ + method: 'GET', + observe: false, + host: 'localhost', + port: contentNegotiationPort, + pathname: 'coap-calculator-content-negotiation', + headers: { + "Accept": 'application/cbor' + } + }) + + req.on('response', (res) => { + // console.log(res.payload.toString()) + const decodedPayload = cbor.decode(res.payload) + const valid = validate(JSON.parse(decodedPayload)) + expect(valid).to.be.true + done() + }) - req.on('response', (res) => { - const valid = validate(JSON.parse(res.payload.toString())) - expect(valid).to.be.true - done() + req.end() }) - - req.end() }) }) diff --git a/things/calculator/http/express/.mocharc.json b/things/calculator/http/express/.mocharc.json new file mode 100644 index 0000000..a665287 --- /dev/null +++ b/things/calculator/http/express/.mocharc.json @@ -0,0 +1,5 @@ +{ + "exit": true, + "spec": "./test/**.test.js", + "require": ["./test/fixtures.js"] +} \ No newline at end of file diff --git a/things/calculator/http/express/http-content-negotiation-calculator-thing.td.jsonld b/things/calculator/http/express/http-content-negotiation-calculator-thing.td.jsonld index 4b0f14b..6096347 100644 --- a/things/calculator/http/express/http-content-negotiation-calculator-thing.td.jsonld +++ b/things/calculator/http/express/http-content-negotiation-calculator-thing.td.jsonld @@ -36,8 +36,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ] }, @@ -55,8 +55,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ], "subprotocol": "sse" @@ -72,8 +72,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ] }, @@ -91,8 +91,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ], "subprotocol": "sse" @@ -117,8 +117,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ] }, @@ -136,8 +136,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ], "subprotocol": "sse" @@ -153,8 +153,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ] }, @@ -172,8 +172,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ], "subprotocol": "sse" @@ -203,8 +203,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ] }, @@ -219,8 +219,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ] }, @@ -235,8 +235,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ] }, @@ -251,8 +251,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ] } @@ -279,8 +279,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ] }, @@ -295,8 +295,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ] }, @@ -311,8 +311,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ] }, @@ -327,8 +327,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ] } @@ -352,8 +352,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/json", - "fieldName": "Accept" + "htv:fieldValue": "application/json", + "htv:fieldName": "Accept" } ], "subprotocol": "sse" @@ -369,8 +369,8 @@ "htv:headers": [ { "@type": "htv:RequestHeader", - "fieldValue": "application/cbor", - "fieldName": "Accept" + "htv:fieldValue": "application/cbor", + "htv:fieldName": "Accept" } ], "subprotocol": "sse" diff --git a/things/calculator/http/express/http-content-negotiation-calculator.js b/things/calculator/http/express/http-content-negotiation-calculator.js index e51644d..578cb2f 100644 --- a/things/calculator/http/express/http-content-negotiation-calculator.js +++ b/things/calculator/http/express/http-content-negotiation-calculator.js @@ -71,8 +71,8 @@ const defaultForm = 'htv:headers': [ { '@type': 'htv:RequestHeader', - 'fieldValue': 'application/json', - 'fieldName': 'Accept' + 'htv:fieldValue': 'application/json', + 'htv:fieldName': 'Accept' } ] } @@ -106,7 +106,7 @@ for (const key in thingDescription['properties']) { const newFormRead = JSON.parse(JSON.stringify(originalForm)) newFormRead['contentType'] = type newFormRead['response'].contentType = type - newFormRead['htv:headers'][0]['fieldValue'] = type + newFormRead['htv:headers'][0]['htv:fieldValue'] = type thingDescription['properties'][key]['forms'].push(newFormRead) const newFormObs = JSON.parse(JSON.stringify(newFormRead)) @@ -142,7 +142,7 @@ for (const key in thingDescription['actions']) { if (!thingDescription['actions'][key]['forms'][0]['response'].contentType.includes(type)) { const newFormAccept = JSON.parse(JSON.stringify(newForm)) newFormAccept['response'].contentType = type; - newFormAccept['htv:headers'][0]['fieldValue'] = type + newFormAccept['htv:headers'][0]['htv:fieldValue'] = type thingDescription['actions'][key]['forms'].push(newFormAccept) } }) @@ -151,7 +151,7 @@ for (const key in thingDescription['actions']) { if (!originalForm['response'].contentType.includes(type)) { const newForm = JSON.parse(JSON.stringify(originalForm)); newForm['response'].contentType = type; - newForm['htv:headers'][0]['fieldValue'] = type; + newForm['htv:headers'][0]['htv:fieldValue'] = type; thingDescription['actions'][key]['forms'].push(newForm); } }) @@ -182,7 +182,7 @@ for (const key in thingDescription['events']) { const newForm = JSON.parse(JSON.stringify(originalForm)) newForm['contentType'] = type newForm['response'].contentType = type; - newForm['htv:headers'][0]['fieldValue'] = type + newForm['htv:headers'][0]['htv:fieldValue'] = type thingDescription['events'][key]['forms'].push(newForm) } }) diff --git a/things/calculator/http/express/test/client.test.js b/things/calculator/http/express/test/client.test.js new file mode 100644 index 0000000..13fb724 --- /dev/null +++ b/things/calculator/http/express/test/client.test.js @@ -0,0 +1,219 @@ +const chai = require("chai") +const chaiAsPromised = require("chai-as-promised") + +const { Servient } = require("@node-wot/core") +const { HttpClientFactory } = require("@node-wot/binding-http") +const { simplePort, contentNegotiationPort } = require('./fixtures') + +chai.use(chaiAsPromised) +const expect = chai.expect + +const servient = new Servient() +servient.addClientFactory(new HttpClientFactory()) +let WoT + +const readProperty = async (thing, propertyName) => { + try { + const response = await thing.readProperty(propertyName) + return await response.value() + } catch(error) { + console.error(`Error: ${error}`) + } +} + +describe("Client Tests", () => { + before(async () => { + try { + WoT = await servient.start() + } catch(error) { + console.error(error) + } + }) + + after(async () => { + await servient.shutdown() + }) + + describe("Simple Calculator", () => { + let thing + + before(async () => { + try { + const td = await WoT.requestThingDescription(`http://localhost:${simplePort}/http-express-calculator-simple`) + thing = await WoT.consume(td) + } catch(error) { + console.error(error) + } + }) + + describe("result property", () => { + it("should return initial value", async () => { + const value = await readProperty(thing, "result") + expect(value).to.be.equal(0) + }) + + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + await thing.invokeAction("add", valueToAdd) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue + valueToAdd) + }) + + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + await thing.invokeAction("subtract", valueToSubtract) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("lastChange property", () => { + it("should observe a change when the result is changed", async () => { + setTimeout(async () => { + await thing.invokeAction('add', 1) + }, 200) + + let value + const subscription = thing.observeProperty('lastChange', async (response) => { + value = await response.value() + }) + + setTimeout(async () => { + expect(value).to.be.not.undefined + await subscription.stop() + }) + }) + }) + + describe("add action", () => { + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + const response = await thing.invokeAction("add", valueToAdd) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue + valueToAdd) + }) + }) + + describe("subtract action", () => { + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + const response = await thing.invokeAction("subtract", valueToSubtract) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("update event", () => { + it("should return the update message when subscribed", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 13 + + const actionTimeout = setInterval(async () => { + await thing.invokeAction('add', valueToAdd) + }, 200) + + const subscription = await thing.subscribeEvent("update", async (response) => { + await expect(response.value).to.have.eventually.be.equal(resultValue + valueToAdd) + }) + + await subscription.stop() + }) + }) + }) + + describe("Content Negotiation Calculator", () => { + let thing + + before(async () => { + try { + const td = await WoT.requestThingDescription(`http://localhost:${contentNegotiationPort}/http-express-calculator-content-negotiation`) + thing = await WoT.consume(td) + } catch(error) { + console.error(error) + } + }) + + describe("result property", () => { + it("should return initial value", async () => { + const value = await readProperty(thing, "result") + expect(value).to.be.equal(0) + }) + + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + await thing.invokeAction("add", valueToAdd) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue + valueToAdd) + }) + + it("should return sum when subtracting value from the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + await thing.invokeAction("subtract", valueToSubtract) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("lastChange property", () => { + it("should observe a change when the result is changed", async () => { + setTimeout(async () => { + await thing.invokeAction('add', 1) + }, 200) + + let value + const subscription = thing.observeProperty('lastChange', async (response) => { + value = await response.value() + }) + + setTimeout(async () => { + expect(value).to.be.not.undefined + await subscription.stop() + }) + }) + }) + + describe("add action", () => { + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + const response = await thing.invokeAction("add", valueToAdd) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue + valueToAdd) + }) + }) + + describe("subtract action", () => { + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + const response = await thing.invokeAction("subtract", valueToSubtract) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("update event", () => { + it("should return the update message when subscribed", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 13 + + const actionTimeout = setInterval(async () => { + await thing.invokeAction('add', valueToAdd) + }, 200) + + const subscription = await thing.subscribeEvent("update", async (response) => { + await expect(response.value).to.have.eventually.be.equal(resultValue + valueToAdd) + }) + + await subscription.stop() + }) + }) + }) +}) + diff --git a/things/calculator/http/express/test/fixtures.js b/things/calculator/http/express/test/fixtures.js new file mode 100644 index 0000000..f865377 --- /dev/null +++ b/things/calculator/http/express/test/fixtures.js @@ -0,0 +1,45 @@ +const { getInitiateMain } = require("../../../../../util/dist/util") +const path = require("node:path") + +let simpleThingProcess +let contentNegotiationThingProcess +let response +const simplePort = 3000 +const contentNegotiationPort = 3001 + +const mochaGlobalSetup = async function() { + try { + response = await getInitiateMain('node', [path.join(__dirname, '..', 'http-simple-calculator.js'), '-p', `${simplePort}`]) + simpleThingProcess = response.process + } + catch(error) { + console.error(error) + simpleThingProcess = error.process + } + + try { + response = await getInitiateMain('node', [path.join(__dirname, '..', 'http-content-negotiation-calculator.js'), '-p', `${contentNegotiationPort}`]) + contentNegotiationThingProcess = response.process + } + catch (error) { + console.error(error) + contentNegotiationThingProcess = error.process + } +} + +const mochaGlobalTeardown = function() { + if(simpleThingProcess) { + simpleThingProcess.kill() + } + + if(contentNegotiationThingProcess) { + contentNegotiationThingProcess.kill() + } +} + +module.exports = { + simplePort, + contentNegotiationPort, + mochaGlobalSetup, + mochaGlobalTeardown +} \ No newline at end of file diff --git a/things/calculator/http/express/test/td.test.js b/things/calculator/http/express/test/td.test.js index 009cf04..5e2cdad 100644 --- a/things/calculator/http/express/test/td.test.js +++ b/things/calculator/http/express/test/td.test.js @@ -1,86 +1,73 @@ -const Ajv = require('ajv') const chai = require('chai') const http = require('http') -const https = require('https') -const path = require('path') - -const spawn = require('child_process').spawn - -const ajv = new Ajv({ strict: false, allErrors: true, validateFormats: false }) +const { getTDValidate } = require('../../../../../util/dist/util') +const { simplePort, contentNegotiationPort } = require('./fixtures') const expect = chai.expect -const port = 3000 -let thingProcess describe('Calculator HTTP JS', () => { let validate before(async () => { - const initiateMain = new Promise(async (resolve, reject) => { - thingProcess = spawn( - 'node', - ['http-simple-calculator.js', '-p', `${port}`], - { cwd: path.join(__dirname, '..') } - ) - thingProcess.stdout.on('data', (data) => { - if (data.toString().includes('ThingIsReady')) { - resolve('Success') - } - }) - thingProcess.stderr.on('data', (data) => { - reject(`Error: ${data}`) - }) - thingProcess.on('error', (error) => { - reject(`Error: ${error}`) - }) - thingProcess.on('close', () => { - reject('Failed to initiate the main script.') - }) - }) + const tdValidate = getTDValidate() + + try { + const response = await Promise.all([tdValidate]) + validate = response[0].validate + } + catch (error) { + console.log(error) + } + }) - const getJSONSchema = new Promise((resolve, reject) => { - https.get('https://raw.githubusercontent.com/w3c/wot-thing-description/main/validation/td-json-schema-validation.json', function (response) { + describe('Calculator Simple', () => { + it('should have a valid TD', (done) => { + http.get(`http://localhost:${simplePort}/http-express-calculator-simple`, function (response) { const body = [] response.on('data', (chunk) => { body.push(chunk) }) - + response.on('end', () => { - const tdSchema = JSON.parse(Buffer.concat(body).toString()) - validate = ajv.compile(tdSchema) - resolve('Success') + try { + const result = JSON.parse(Buffer.concat(body).toString()) + const valid = validate(result) + expect(valid).to.be.true + done() + } catch (error) { + console.log(error) + } }) }) }) - - await Promise.all([initiateMain, getJSONSchema]).then(data => { - if (data[0] !== 'Success' || data[1] !== 'Success') { - console.log(`initiateMain: ${data[0]}`) - console.log(`getJSONSchema: ${data[1]}`) - } - }) }) - - after(() => { - thingProcess.kill() - }) - - it('should have a valid TD', (done) => { - http.get(`http://localhost:${port}/http-express-calculator-simple`, function (response) { - const body = [] - response.on('data', (chunk) => { - body.push(chunk) - }) - - response.on('end', () => { - try { - const result = JSON.parse(Buffer.concat(body).toString()) - const valid = validate(result) - expect(valid).to.be.true - done() - } catch (error) { - console.log(error) + + describe('Calculator Content Negotiation', () => { + it('should have a valid TD', (done) => { + http.get({ + hostname: 'localhost', + port: contentNegotiationPort, + path: '/http-express-calculator-content-negotiation', + method: 'GET', + headers: { + accept: 'application/json' } + }, function (response) { + const body = [] + response.on('data', (chunk) => { + body.push(chunk) + }) + + response.on('end', () => { + try { + const result = JSON.parse(Buffer.concat(body).toString()) + const valid = validate(result) + expect(valid).to.be.true + done() + } catch (error) { + console.log(error) + } + }) }) }) }) diff --git a/things/calculator/mqtt/js/.mocharc.json b/things/calculator/mqtt/js/.mocharc.json new file mode 100644 index 0000000..9d68ca0 --- /dev/null +++ b/things/calculator/mqtt/js/.mocharc.json @@ -0,0 +1,4 @@ +{ + "spec": "./test/**.test.js", + "require": ["./test/fixtures.js"] +} \ No newline at end of file diff --git a/things/calculator/mqtt/js/main.js b/things/calculator/mqtt/js/main.js index 9d86cd0..e81e811 100644 --- a/things/calculator/mqtt/js/main.js +++ b/things/calculator/mqtt/js/main.js @@ -95,9 +95,15 @@ for (const key in thingDescription['events']) { thingDescription['events'][key]['forms'].push(newForm) } +fs.writeFile(`${thingName}.td.json`, JSON.stringify(thingDescription, 4, 4), 'utf-8', function(){}) broker.on('connect', () => { console.log(`Connected to broker via port ${portNumber}`) + broker.subscribe(`${thingName}/${PROPERTIES}/result`) + broker.subscribe(`${thingName}/${PROPERTIES}/lastChange`) + broker.subscribe(`${thingName}/${ACTIONS}/add`) + broker.subscribe(`${thingName}/${ACTIONS}/subtract`) + broker.subscribe(`${thingName}/${EVENTS}/update`) }) let result = 0 @@ -130,6 +136,8 @@ broker.on('message', (topic, payload, packet) => { } else { result += parsedValue lastChange = (new Date()).toLocaleTimeString() + broker.publish(`${thingName}/${PROPERTIES}/result`, `${result}`, { retain: true }) + broker.publish(`${thingName}/${PROPERTIES}/lastChange`, lastChange, { retain: true }) } } @@ -141,6 +149,8 @@ broker.on('message', (topic, payload, packet) => { } else { result -= parsedValue lastChange = (new Date()).toLocaleTimeString() + broker.publish(`${thingName}/${PROPERTIES}/result`, `${result}`, { retain: true }) + broker.publish(`${thingName}/${PROPERTIES}/lastChange`, lastChange, { retain: true }) } } } @@ -150,9 +160,6 @@ setInterval(() => { broker.publish(`${thingName}/${EVENTS}/update`, 'Updated the thing!') }, 500) -broker.subscribe(`${thingName}/${PROPERTIES}/result`) -broker.subscribe(`${thingName}/${PROPERTIES}/lastChange`) -broker.subscribe(`${thingName}/${ACTIONS}/add`) -broker.subscribe(`${thingName}/${ACTIONS}/subtract`) +// broker.publish(`${thingName}/${PROPERTIES}/result`, `${result}`, { retain: true }) broker.publish(`${thingName}`, JSON.stringify(thingDescription), { retain: true }) console.log('ThingIsReady') diff --git a/things/calculator/mqtt/js/mqtt-calculator.td.json b/things/calculator/mqtt/js/mqtt-calculator.td.json new file mode 100644 index 0000000..c863bd3 --- /dev/null +++ b/things/calculator/mqtt/js/mqtt-calculator.td.json @@ -0,0 +1,111 @@ +{ + "@context": [ + "https://www.w3.org/2019/wot/td/v1", + "https://www.w3.org/2022/wot/td/v1.1", + { + "@language": "en" + } + ], + "@type": "Thing", + "title": "mqtt-calculator", + "description": "Calculator Thing", + "securityDefinitions": { + "nosec_sc": { + "scheme": "nosec" + } + }, + "security": [ + "nosec_sc" + ], + "base": "mqtt://test.mosquitto.org:1883/mqtt-calculator/", + "properties": { + "result": { + "type": "number", + "readOnly": true, + "writeOnly": false, + "observable": true, + "forms": [ + { + "href": "properties/result", + "contentType": "application/json", + "op": [ + "readproperty" + ] + } + ] + }, + "lastChange": { + "type": "string", + "format": "date-time", + "readOnly": true, + "writeOnly": false, + "observable": true, + "forms": [ + { + "href": "properties/lastChange", + "contentType": "application/json", + "op": [ + "readproperty" + ] + } + ] + } + }, + "actions": { + "add": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "idempotent": false, + "safe": false, + "forms": [ + { + "href": "actions/add", + "contentType": "application/json", + "op": [ + "invokeaction" + ] + } + ] + }, + "subtract": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "idempotent": false, + "safe": false, + "forms": [ + { + "href": "actions/subtract", + "contentType": "application/json", + "op": [ + "invokeaction" + ] + } + ] + } + }, + "events": { + "update": { + "data": { + "type": "string" + }, + "forms": [ + { + "href": "events/update", + "contentType": "application/json", + "op": [ + "subscribeevent", + "unsubscribeevent" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/things/calculator/mqtt/js/test/client.test.js b/things/calculator/mqtt/js/test/client.test.js new file mode 100644 index 0000000..d651c55 --- /dev/null +++ b/things/calculator/mqtt/js/test/client.test.js @@ -0,0 +1,99 @@ +const chai = require("chai") +const chaiAsPromised = require("chai-as-promised") + +const { Servient } = require("@node-wot/core") +const { MqttClientFactory } = require("@node-wot/binding-mqtt") +const mqttTd = require('../mqtt-calculator.td.json') + +chai.use(chaiAsPromised) +const expect = chai.expect + +const servient = new Servient() +servient.addClientFactory(new MqttClientFactory()) +let thing + + +const readProperty = async (thing, propertyName) => { + try { + const response = await thing.readProperty(propertyName) + return await response.value() + } catch(error) { + console.error(`Error: ${error}`) + } +} + +/** + * FIXME: To be able to test readProperty, issues https://github.com/eclipse-thingweb/node-wot/issues/980 + * and https://github.com/eclipse-thingweb/node-wot/issues/1241 must be resolved. + * Until then we can use subscriptions. + */ + +describe.skip("Client Tests", () => { + before(async () => { + try { + const WoT = await servient.start() + thing = await WoT.consume(mqttTd) + } catch(error) { + console.error(error) + } + }) + + after(async () => { + await servient.shutdown() + }) + + describe("result property", () => { + it("should return initial value", async () => { + const value = await readProperty(thing, "result") + expect(value).to.be.equal(0) + }) + + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + await thing.invokeAction("add", valueToAdd) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue + valueToAdd) + }) + + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + await thing.invokeAction("subtract", valueToSubtract) + const newResultValue = await readProperty(thing, "result") + expect(newResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("add action", () => { + it("should return sum when adding value to the existing result", async () => { + const resultValue = await readProperty(thing, "result") + const valueToAdd = 12 + const response = await thing.invokeAction("add", valueToAdd) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue + valueToAdd) + }) + }) + + describe("subtract action", () => { + it("should return sum when subtracting value from the existing result", async() => { + const resultValue = await readProperty(thing, "result") + const valueToSubtract = 3 + const response = await thing.invokeAction("subtract", valueToSubtract) + const actionResultValue = await response.value() + expect(actionResultValue).to.be.equal(resultValue - valueToSubtract) + }) + }) + + describe("update event", () => { + it("should return the update message when subscribed", async () => { + const subscription = await thing.subscribeEvent("update", async (response) => { + const value = await response.value() + console.log(value) + expect(value).to.be.equal("Updated the thing!") + subscription.stop() + }) + }) + }) +}) + diff --git a/things/calculator/mqtt/js/test/fixtures.js b/things/calculator/mqtt/js/test/fixtures.js new file mode 100644 index 0000000..eb72af6 --- /dev/null +++ b/things/calculator/mqtt/js/test/fixtures.js @@ -0,0 +1,23 @@ +const { getInitiateMain } = require("../../../../../util/dist/util") +const path = require("node:path") + +let thingProcess +let response +const port = 1883 + +exports.mochaGlobalSetup = async function() { + try { + response = await getInitiateMain('node', [path.join(__dirname, '..', 'main.js'), '-p', `${port}`]) + thingProcess = response.process + } + catch(error) { + console.log(error) + thingProcess = error.process + } +} + +exports.mochaGlobalTeardown = function() { + if (thingProcess) { + thingProcess.kill() + } +} \ No newline at end of file diff --git a/things/calculator/mqtt/js/test/td.test.js b/things/calculator/mqtt/js/test/td.test.js index b69deb8..7ecfc95 100644 --- a/things/calculator/mqtt/js/test/td.test.js +++ b/things/calculator/mqtt/js/test/td.test.js @@ -1,69 +1,25 @@ -const Ajv = require('ajv') const chai = require('chai') -const https = require('https') const mqtt = require('mqtt') -const path = require('path') +const { getTDValidate } = require("../../../../../util/dist/util") +const { port } = require('./fixtures') -const spawn = require('child_process').spawn - -const ajv = new Ajv({ strict: false, allErrors: true, validateFormats: false }) const expect = chai.expect const hostname = 'test.mosquitto.org' -const port = 1883 -let thingProcess describe('Calculator MQTT JS', () => { let validate before(async () => { - const initiateMain = new Promise(async (resolve, reject) => { - thingProcess = spawn( - 'node', - ['main.js', '-p', `${port}`], - { cwd: path.join(__dirname, '..') } - ) - thingProcess.stdout.on('data', (data) => { - if (data.toString().trim() === 'ThingIsReady') { - resolve('Success') - } - }) - thingProcess.stderr.on('data', (data) => { - reject(`Error: ${data}`) - }) - thingProcess.on('error', (error) => { - reject(`Error: ${error}`) - }) - thingProcess.on('close', () => { - reject('Failed to initiate the main script.') - }) - }) - - const getJSONSchema = new Promise((resolve, reject) => { - https.get('https://raw.githubusercontent.com/w3c/wot-thing-description/main/validation/td-json-schema-validation.json', function (response) { - const body = [] - response.on('data', (chunk) => { - body.push(chunk) - }) - - response.on('end', () => { - const tdSchema = JSON.parse(Buffer.concat(body).toString()) - validate = ajv.compile(tdSchema) - resolve('Success') - }) - }) - }) - - await Promise.all([initiateMain, getJSONSchema]).then(data => { - if (data[0] !== 'Success' || data[1] !== 'Success') { - console.log(`initiateMain: ${data[0]}`) - console.log(`getJSONSchema: ${data[1]}`) - } - }) - }) - - after(() => { - thingProcess.kill() + const tdValidate = getTDValidate() + + try { + const response = await Promise.all([tdValidate]) + validate = response[0].validate + } + catch (error) { + console.log(error) + } }) it('should have a valid TD', (done) => { diff --git a/things/data-schema-thing/http/ts/test/client.test.ts b/things/data-schema-thing/http/ts/test/client.test.ts index 63d6a0f..5b72d9e 100644 --- a/things/data-schema-thing/http/ts/test/client.test.ts +++ b/things/data-schema-thing/http/ts/test/client.test.ts @@ -4,13 +4,13 @@ import chaiAsPromised from 'chai-as-promised'; import { Servient } from "@node-wot/core"; import { HttpClientFactory } from "@node-wot/binding-http" +import { port } from './fixtures' chai.use(chaiAsPromised) const expect = chai.expect let servient = new Servient() -servient.addClientFactory(new HttpClientFactory({ baseUri: 'localhost:3000' })) -const port = 3000 +servient.addClientFactory(new HttpClientFactory()) let thing: WoT.ConsumedThing @@ -35,6 +35,10 @@ describe("Client Tests", () => { console.error(error) } }) + + after(async () => { + await servient.shutdown() + }) describe("bool property", () => { it("should read property bool", async () => { diff --git a/things/data-schema-thing/http/ts/test/fixtures.ts b/things/data-schema-thing/http/ts/test/fixtures.ts index 796f067..e304662 100644 --- a/things/data-schema-thing/http/ts/test/fixtures.ts +++ b/things/data-schema-thing/http/ts/test/fixtures.ts @@ -4,11 +4,11 @@ import path from "path" let thingProcess: ChildProcess | undefined let response: ThingStartResponse -const port = 3000 +export const port = 3000 export async function mochaGlobalSetup() { try { - response = await getInitiateMain(path.join(__dirname, '..', 'dist', 'main.js'), port) + response = await getInitiateMain('node', [path.join(__dirname, '..', 'dist', 'main.js'), '-p', `${port}`]) } catch(error) { console.log(error) diff --git a/things/data-schema-thing/http/ts/test/td.test.ts b/things/data-schema-thing/http/ts/test/td.test.ts index bf7e44f..cc5267f 100644 --- a/things/data-schema-thing/http/ts/test/td.test.ts +++ b/things/data-schema-thing/http/ts/test/td.test.ts @@ -2,10 +2,10 @@ import * as chai from 'chai' import * as http from 'http' import { getTDValidate } from '../../../../../util/util' import { ValidateFunction } from 'ajv' +import { port } from './fixtures' const expect = chai.expect -const port = 3000 let validate: ValidateFunction | undefined describe("TD Test", () => { diff --git a/things/elevator/modbus/js/.mocharc.json b/things/elevator/modbus/js/.mocharc.json new file mode 100644 index 0000000..9d68ca0 --- /dev/null +++ b/things/elevator/modbus/js/.mocharc.json @@ -0,0 +1,4 @@ +{ + "spec": "./test/**.test.js", + "require": ["./test/fixtures.js"] +} \ No newline at end of file diff --git a/things/elevator/modbus/js/main.js b/things/elevator/modbus/js/main.js index c3f8647..5b350d9 100644 --- a/things/elevator/modbus/js/main.js +++ b/things/elevator/modbus/js/main.js @@ -3,6 +3,7 @@ const fs = require('fs') const path = require('path') const { JsonPlaceholderReplacer } = require('json-placeholder-replacer') const { parseArgs } = require('node:util') +const { get } = require("http") require('dotenv').config() const thingName = "modbus-elevator" @@ -13,11 +14,16 @@ const hostname = process.env.HOSTNAME let portNumber = process.env.PORT ?? "8502" const thingUnitID = 1 -const { values: { port } } = parseArgs({ +const { values: { port, isTestRun } } = parseArgs({ options: { port: { type: 'string', short: 'p' + }, + isTestRun: { + type: 'boolean', + short: 't', + default: false } } }) @@ -49,34 +55,36 @@ const coils = new Array(9999) const discreteInputs = new Array(9999) const holdingRegisters = new Array(9999) +coils[0] = 0 + const lightSwitchForms = [{ - "href": `?address=1&quantity=1`, + "href": `modbus+tcp://0.0.0.0:8502/1/1?quantity=1`, "op": "readproperty", - "modbus:entity": "Coil", - "modbus:function": "readCoil", + "modv:entity": "Coil", + "modv:function": "readCoil", "contentType": "application/octet-stream" }, { - "href": `?address=1&quantity=1`, + "href": `modbus+tcp://0.0.0.0:8502/1/1?quantity=1`, "op": "writeproperty", - "modbus:entity": "Coil", - "modbus:function": "writeSingleCoil", + "modv:entity": "Coil", + "modv:function": "writeSingleCoil", "contentType": "application/octet-stream" }] - thingDescription['properties']['lightSwitch']['forms'] = lightSwitchForms +const onTheMoveAddress = 0 const onTheMovePollingTime = 1000 const onTheMoveForms = [{ - "href": `?address=1&quantity=1`, + "href": `modbus+tcp://0.0.0.0:8502/1/10001?quantity=1`, "op": [ "readproperty", "observeproperty" ], - "modbus:entity": "DiscreteInput", - "modbus:function": "readDiscreteInput", - "modbus:pollingTime": onTheMovePollingTime, + "modv:entity": "DiscreteInput", + "modv:function": "readDiscreteInput", + "modv:pollingTime": onTheMovePollingTime, "contentType": "application/octet-stream" }] @@ -85,88 +93,152 @@ let onTheMoveIsPolled = false thingDescription['properties']['onTheMove']['forms'] = onTheMoveForms const floorNumberForms = [{ - "href": `?address=1&quantity=1`, + "href": `modbus+tcp://0.0.0.0:8502/1/40001?quantity=2`, "op": "readproperty", - "modbus:entity": "HoldingRegister", - "modbus:function": "readHoldingRegister", + "modv:entity": "HoldingRegister", + "modv:function": "readHoldingRegisters", "contentType": "application/octet-stream" }, { - "href": `?address=1&quantity=1`, + "href": `modbus+tcp://0.0.0.0:8502/1/40001?quantity=2`, "op": "writeproperty", - "modbus:entity": "HoldingRegister", - "modbus:function": "writeSingleHoldingRegister", + "modv:entity": "HoldingRegister", + "modv:function": "writeSingleHoldingRegister", "contentType": "application/octet-stream" }] +const floorNumberAddress = 0 +const floorNumberQuantity = 2 + +const getFloorNumberValue = () => { + return holdingRegisters + .slice(floorNumberAddress, floorNumberAddress + floorNumberQuantity) + .reduce((sum, e) => sum + e) +} + +holdingRegisters[0] = 0 const minFloorNumber = 0 const maxFloorNumber = 15 thingDescription['properties']['floorNumber']['forms'] = floorNumberForms fs.writeFile(`${thingName}.td.json`, JSON.stringify(thingDescription, 4, 4), 'utf-8', function(){}) + +const coilMemoryRange = [1, 9999] +const discreteInputMemoryRange = [10001, 19999] +const inputRegisterMemoryRange = [30001, 39999] +const holdingRegisterMemoryRange = [40001, 49999] + +const isAddressInRange = (address, range) => { + return address >= range[0] && address <= range[1] +} + +const getNormalizedAddress = (address, range) => { + return address - range[0] +} + const vector = { getDiscreteInput: function(addr, unitID) { if (thingUnitID === unitID) { + if (!isAddressInRange(addr, discreteInputMemoryRange)) { + console.log(`Address is out of discrete input memory range.`) + return + } + console.log(`Reading discrete input @${addr}`) - if (addr === 1) { + const normalizedAddress = getNormalizedAddress(addr, discreteInputMemoryRange) + + if (normalizedAddress === onTheMoveAddress) { if (onTheMoveIsPolled) { console.log(`Polling onTheMove too frequently. You should poll it every ${onTheMovePollingTime} ms.`) return } onTheMoveIsPolled = true - setTimeout(function() { + let returnValue + + if (isTestRun) { onTheMoveIsPolled = false - }, onTheMovePollingTime) + returnValue = discreteInputs[normalizedAddress] + discreteInputs[normalizedAddress] = 0 + } else { + setTimeout(function() { + onTheMoveIsPolled = false + }, onTheMovePollingTime) - return discreteInputs[addr - 1]; + returnValue = discreteInputs[normalizedAddress] + } + + return returnValue } } }, getHoldingRegister: function(addr, unitID, callback) { if (thingUnitID === unitID) { + if (!isAddressInRange(addr, holdingRegisterMemoryRange)) { + console.log(`Address is out of holding register memory range.`) + return + } + + const normalizedAddress = getNormalizedAddress(addr, holdingRegisterMemoryRange) + setTimeout(function() { - console.log(`Reading holding register @${addr}`) - callback(null, holdingRegisters[addr - 1]) + callback(null, holdingRegisters[normalizedAddress]) }, 10) } }, getCoil: function(addr, unitID) { if (thingUnitID === unitID) { + if (!isAddressInRange(addr, coilMemoryRange)) { + console.log(`Address is out of coil memory range.`) + return + } + return new Promise(function(resolve) { console.log(`Reading coil @${addr}`) - resolve(coils[addr - 1]) + const normalizedAddress = getNormalizedAddress(addr, coilMemoryRange) + resolve(coils[normalizedAddress]) }) } }, setRegister: function(addr, value, unitID) { if (thingUnitID === unitID) { + if (!isAddressInRange(addr, holdingRegisterMemoryRange)) { + console.log(`Address is out of holding register memory range.`) + return + } + console.log(`Setting register @${addr} to ${value}`) + const normalizedAddress = getNormalizedAddress(addr, holdingRegisterMemoryRange) // trying to change floor number - if (addr === 1) { + holdingRegisters[normalizedAddress] = value + + // writing last part of the value and running the thing logic + if (normalizedAddress === floorNumberAddress + floorNumberQuantity - 1) { // elevator is on the move - if (discreteInputs[0]) { + if (discreteInputs[onTheMoveAddress] && !isTestRun) { console.log("Elevator is on the move, cannot change the floor number") } else { - if (value < minFloorNumber) { + const floorNumberValue = getFloorNumberValue() + if (floorNumberValue < minFloorNumber) { console.log(`Floor number should not be under ${minFloorNumber}`) return -1 } - if (value > maxFloorNumber) { + if (floorNumberValue > maxFloorNumber) { console.log(`Floor number should not be above ${maxFloorNumber}`) return -1 } console.log(`Changing the floor number to ${value}`) - holdingRegisters[addr - 1] = value // simulating elevator movement - discreteInputs[0] = 1 + discreteInputs[onTheMoveAddress] = 1 // elevator completes its movement in 5 seconds - setTimeout(() => { - discreteInputs[0] = 0 - }, 5000) + if (!isTestRun) { + setTimeout(() => { + discreteInputs[onTheMoveAddress] = 0 + }, 5000) + } } } } @@ -175,8 +247,15 @@ const vector = { }, setCoil: function(addr, value, unitID) { if (thingUnitID === unitID) { + if (!isAddressInRange(addr, coilMemoryRange)) { + console.log(`Address is out of coil memory range.`) + return + } + + const normalizedAddress = getNormalizedAddress(addr, coilMemoryRange) + console.log(`Setting coil @${addr} to ${value}`) - coils[addr - 1] = value + coils[normalizedAddress] = value } return diff --git a/things/elevator/modbus/js/modbus-elevator.td.json b/things/elevator/modbus/js/modbus-elevator.td.json index f0b420b..e3bd3c6 100644 --- a/things/elevator/modbus/js/modbus-elevator.td.json +++ b/things/elevator/modbus/js/modbus-elevator.td.json @@ -26,17 +26,17 @@ "observable": false, "forms": [ { - "href": "?address=1&quantity=1", + "href": "modbus+tcp://0.0.0.0:8502/1/1?quantity=1", "op": "readproperty", - "modbus:entity": "Coil", - "modbus:function": "readCoil", + "modv:entity": "Coil", + "modv:function": "readCoil", "contentType": "application/octet-stream" }, { - "href": "?address=1&quantity=1", + "href": "modbus+tcp://0.0.0.0:8502/1/1?quantity=1", "op": "writeproperty", - "modbus:entity": "Coil", - "modbus:function": "writeSingleCoil", + "modv:entity": "Coil", + "modv:function": "writeSingleCoil", "contentType": "application/octet-stream" } ] @@ -48,14 +48,14 @@ "observable": true, "forms": [ { - "href": "?address=1&quantity=1", + "href": "modbus+tcp://0.0.0.0:8502/1/10001?quantity=1", "op": [ "readproperty", "observeproperty" ], - "modbus:entity": "DiscreteInput", - "modbus:function": "readDiscreteInput", - "modbus:pollingTime": 1000, + "modv:entity": "DiscreteInput", + "modv:function": "readDiscreteInput", + "modv:pollingTime": 1000, "contentType": "application/octet-stream" } ] @@ -69,17 +69,17 @@ "observable": false, "forms": [ { - "href": "?address=1&quantity=1", + "href": "modbus+tcp://0.0.0.0:8502/1/40001?quantity=2", "op": "readproperty", - "modbus:entity": "HoldingRegister", - "modbus:function": "readHoldingRegister", + "modv:entity": "HoldingRegister", + "modv:function": "readHoldingRegisters", "contentType": "application/octet-stream" }, { - "href": "?address=1&quantity=1", + "href": "modbus+tcp://0.0.0.0:8502/1/40001?quantity=2", "op": "writeproperty", - "modbus:entity": "HoldingRegister", - "modbus:function": "writeSingleHoldingRegister", + "modv:entity": "HoldingRegister", + "modv:function": "writeSingleHoldingRegister", "contentType": "application/octet-stream" } ] diff --git a/things/elevator/modbus/js/package.json b/things/elevator/modbus/js/package.json index a8caf99..50ffe3b 100644 --- a/things/elevator/modbus/js/package.json +++ b/things/elevator/modbus/js/package.json @@ -12,10 +12,12 @@ "author": "Eclipse Thingweb (https://thingweb.io/)", "license": "EPL-2.0 OR W3C-20150513", "dependencies": { + "@node-wot/binding-modbus": "^0.8.16", + "@node-wot/core": "^0.8.16", + "dotenv": "^16.3.1", "json-placeholder-replacer": "^1.0.35", "modbus": "^1.1.1", "modbus-serial": "^8.0.11", - "serialport": "^11.0.0", - "dotenv": "^16.3.1" + "serialport": "^11.0.0" } } diff --git a/things/elevator/modbus/js/test/client.test.js b/things/elevator/modbus/js/test/client.test.js new file mode 100644 index 0000000..2f1fe29 --- /dev/null +++ b/things/elevator/modbus/js/test/client.test.js @@ -0,0 +1,96 @@ +const chai = require('chai') +const chaiAsPromised = require('chai-as-promised') + +const { Servient } = require('@node-wot/core') +const { ModbusClientFactory } = require('@node-wot/binding-modbus') +const modbusTd = require('../modbus-elevator.td.json') + +chai.use(chaiAsPromised) +const expect = chai.expect + +let servient = new Servient() +servient.addClientFactory(new ModbusClientFactory()) + +let thing + +const readProperty = async (thing, propertyName) => { + try { + const response = await thing.readProperty(propertyName) + return await response.value() + } catch(error) { + console.error(`Error: ${error}`) + } +} + +const writeProperty = async (thing, propertyName, propertyValue) => { + try { + await thing.writeProperty(propertyName, propertyValue) + } catch (error) { + console.error(`Error: ${error}`) + } +} + +describe("Client Tests", () => { + before(async () => { + try { + const WoT = await servient.start() + thing = await WoT.consume(modbusTd) + } catch(error) { + console.error(error) + } + }) + + after(async () => { + await servient.shutdown() + }) + + describe("lightSwitch property", () => { + it("should return false when it is not turned on", async () => { + const value = await readProperty(thing, "lightSwitch") + expect(value).to.be.false + }) + + it("should return true when it is turned on", async () => { + await writeProperty(thing, "lightSwitch", true) + const value = await readProperty(thing, "lightSwitch") + expect(value).to.be.true + }) + }) + + describe("onTheMove property", () => { + it("should return false when floorNumber is not changed recently", async () => { + const value = await readProperty(thing, "onTheMove") + expect(value).to.be.false + }) + + it("should return true when floorNumber is changed recently", async () => { + const floorNumberValue = await readProperty(thing, "floorNumber") + await writeProperty(thing, "floorNumber", 12) + const onTheMoveValue = await readProperty(thing, "onTheMove") + await writeProperty(thing, "floorNumber", floorNumberValue) + expect(onTheMoveValue).to.be.true + }) + }) + + describe("floorNumber property", () => { + it("should return 0 when the thing is newly started", async () => { + const value = await readProperty(thing, "floorNumber") + expect(value).to.be.equal(0) + }) + + it("should return the recently set value when a new value is set", async () => { + const newValue = 12 + await writeProperty(thing, "floorNumber", newValue) + const value = await readProperty(thing, "floorNumber") + expect(value).to.be.equal(newValue) + }) + + it.skip("should not write a value when the value is below 0", async () => { + await expect(writeProperty(thing, "floorNumber", -1)).to.be.rejected + }) + + it.skip("should not write a value when the value is above 15", async() => { + await expect(writeProperty(thing, "floorNumber", 16)).to.be.rejected + }) + }) +}) \ No newline at end of file diff --git a/things/elevator/modbus/js/test/fixtures.js b/things/elevator/modbus/js/test/fixtures.js new file mode 100644 index 0000000..28b2cdf --- /dev/null +++ b/things/elevator/modbus/js/test/fixtures.js @@ -0,0 +1,23 @@ +const { getInitiateMain } = require("../../../../../util/dist/util") +const path = require("node:path") + +let thingProcess +let response +const port = 8502 + +exports.mochaGlobalSetup = async function() { + try { + response = await getInitiateMain('node', [path.join(__dirname, '..', 'main.js'), '-p', `${port}`, '-t']) + thingProcess = response.process + } + catch(error) { + console.log(error) + thingProcess = error.process + } +} + +exports.mochaGlobalTeardown = function() { + if (thingProcess) { + thingProcess.kill() + } +} \ No newline at end of file diff --git a/things/elevator/modbus/js/test/td.test.js b/things/elevator/modbus/js/test/td.test.js index 9a8cec9..12684f2 100644 --- a/things/elevator/modbus/js/test/td.test.js +++ b/things/elevator/modbus/js/test/td.test.js @@ -1,69 +1,23 @@ -const Ajv = require('ajv') const chai = require('chai') -const http = require('http') -const https = require('https') const fs = require('fs') const path = require('path') - -const spawn = require('child_process').spawn - -const ajv = new Ajv({ strict: false, allErrors: true, validateFormats: false }) +const { getTDValidate } = require("../../../../../util/dist/util") const expect = chai.expect -const port = "8502" -let thingProcess describe('Elevator Modbus JS', () => { let validate before(async () => { - const initiateMain = new Promise(async (resolve, reject) => { - thingProcess = spawn( - 'node', - ['main.js', '-p', `${port}`], - { cwd: path.join(__dirname, '..') } - ) - thingProcess.stdout.on('data', (data) => { - if (data.toString().includes('ThingIsReady')) { - resolve('Success') - } - }) - thingProcess.stderr.on('data', (data) => { - reject(`Error: ${data}`) - }) - thingProcess.on('error', (error) => { - reject(`Error: ${error}`) - }) - thingProcess.on('close', () => { - reject('Failed to initiate the main script.') - }) - }) - - const getJSONSchema = new Promise((resolve, reject) => { - https.get('https://raw.githubusercontent.com/w3c/wot-thing-description/main/validation/td-json-schema-validation.json', function (response) { - const body = [] - response.on('data', (chunk) => { - body.push(chunk) - }) - - response.on('end', () => { - const tdSchema = JSON.parse(Buffer.concat(body).toString()) - validate = ajv.compile(tdSchema) - resolve('Success') - }) - }) - }) - - await Promise.all([initiateMain, getJSONSchema]).then(data => { - if (data[0] !== 'Success' || data[1] !== 'Success') { - console.log(`initiateMain: ${data[0]}`) - console.log(`getJSONSchema: ${data[1]}`) - } - }) - }) - - after(() => { - thingProcess.kill() + const tdValidate = getTDValidate() + + try { + const response = await Promise.all([tdValidate]) + validate = response[0].validate + } + catch (error) { + console.log(error) + } }) it('should have a valid TD', (done) => { diff --git a/util/dist/util.js b/util/dist/util.js new file mode 100644 index 0000000..0e640cd --- /dev/null +++ b/util/dist/util.js @@ -0,0 +1,93 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getTDValidate = exports.getInitiateMain = void 0; +const ajv_1 = __importDefault(require("ajv")); +const https = __importStar(require("https")); +const spawn = require('node:child_process').spawn; +const getInitiateMain = (mainCmd, cmdArgs) => { + return new Promise((resolve, reject) => { + const thingProcess = spawn(mainCmd, cmdArgs); + // Avoids unsettled promise in case the promise is not settled in a second. + const timeout = setTimeout(() => { + reject({ + process: thingProcess, + message: 'Thing did not start as expected.' + }); + }, 1000); + thingProcess.stdout.on('data', (data) => { + if (data.toString().includes('ThingIsReady')) { + clearTimeout(timeout); + resolve({ + process: thingProcess, + message: 'Success' + }); + } + }); + thingProcess.stderr.on('data', (data) => { + reject({ + process: thingProcess, + message: `Error: ${data}` + }); + }); + thingProcess.on('error', (error) => { + reject({ + process: thingProcess, + message: `Error: ${error}` + }); + }); + thingProcess.on('close', () => { + reject({ + process: thingProcess, + message: 'Failed to initiate the main script.' + }); + }); + }); +}; +exports.getInitiateMain = getInitiateMain; +const ajv = new ajv_1.default({ strict: false, allErrors: true, validateFormats: false }); +const getTDValidate = async () => { + const tdSchema = await getTDJSONSchema; + return Promise.resolve({ + validate: ajv.compile(tdSchema), + message: 'Success' + }); +}; +exports.getTDValidate = getTDValidate; +const getTDJSONSchema = new Promise((resolve, reject) => { + https.get('https://raw.githubusercontent.com/w3c/wot-thing-description/main/validation/td-json-schema-validation.json', function (response) { + const body = []; + response.on('data', (chunk) => { + body.push(chunk); + }); + response.on('end', () => { + const tdSchema = JSON.parse(Buffer.concat(body).toString()); + resolve(tdSchema); + }); + }); +}); diff --git a/util/tsconfig.json b/util/tsconfig.json new file mode 100644 index 0000000..31dc724 --- /dev/null +++ b/util/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "ES2018", + "module": "commonjs", + "skipLibCheck": false, + "strict": true, + "sourceMap": false, + "esModuleInterop": true, + "removeComments": false + }, +} \ No newline at end of file diff --git a/util/util.ts b/util/util.ts index 8592a2f..affeb93 100644 --- a/util/util.ts +++ b/util/util.ts @@ -14,11 +14,11 @@ export type ValidateResponse = { const spawn = require('node:child_process').spawn -export const getInitiateMain = (thingPath: string, port: number): Promise => { +export const getInitiateMain = (mainCmd: string, cmdArgs: string[]): Promise => { return new Promise((resolve, reject) => { const thingProcess = spawn( - 'node', - [thingPath, '-p', `${port}`], + mainCmd, + cmdArgs, ) // Avoids unsettled promise in case the promise is not settled in a second.