diff --git a/node-red-node-wot/README.md b/node-red-node-wot/README.md index ea3aad6..4135a4e 100644 --- a/node-red-node-wot/README.md +++ b/node-red-node-wot/README.md @@ -12,13 +12,15 @@ After installation, the package adds 8 different nodes at the Node-RED palette, Those nodes are as follows and needed to interact with different interaction affordances of a Thing: 1) Read Property node; -2) Write Property node; -3) Invoke Action node; -4) Subscribe Event node; -5) Server-End node; -6) Server-property node; -7) Server-Action node; -8) Server-Event node. +1) Write Property node; +1) Invoke Action node; +1) Subscribe Event node; +1) Update TD node; +1) Server-End node; +1) Server-property node; +1) Server-Action node; +1) Server-Event node; +1) Server-TD node. ![WoT nodes](screenshots/nodes.png) @@ -100,6 +102,8 @@ If you create a new thing config on the properties screen, the following screen Specify the Thing title. The Thing title will be included in the Thing Description. +Description and Thing ID are optional; if Thing ID is not specified, Thing ID will be generated automatically. + Server config and Thing config can be shared across multiple Server-Property, Server-Action, and Server-Event nodes. By sharing configs, you can publish one or more properties, actions, and events to the client as a single Thing. In addition to server config and Thing config, there are necessary settings for each Server-Property, Server-Action, and Server-Event node. For the settings of each node, refer to the node help. Help can be viewed on the Node-RED editor's Help tab. @@ -116,6 +120,14 @@ If you want to check the Thing Description, open the Context Data tab of the Nod ![Reference TD](https://raw.githubusercontent.com/eclipse-thingweb/node-red/main/node-red-node-wot/screenshots/reference-td.png) +The Thing Description can also be obtained using the Server-TD node. + +Also, the client can use the Update TD node to replace the Thing Description. For example, if the server URL is changed, the destination server can be changed without restarting the flow. + +You can get the Example from the Import menu of the Node-RED editor for reference. + +![Import Example Flows](screenshots/import-example-flows.png) + Currently, the supported binding types are HTTP, CoAP, and MQTT. As shown in the table below, each of these types of bindings has its own available/unavailable functions. Available functions are denoted by `✓` and unavailable functions are denoted by `-`. | |http|coap|mqtt| @@ -129,3 +141,5 @@ Currently, the supported binding types are HTTP, CoAP, and MQTT. As shown in the *1: After the connection with the server times out after 1 hour, it is not reconnected Also, it is unclear how to define the flow when the data type is null. + + diff --git a/node-red-node-wot/examples/client-side-flows.json b/node-red-node-wot/examples/client-side-flows.json new file mode 100644 index 0000000..4eeeed6 --- /dev/null +++ b/node-red-node-wot/examples/client-side-flows.json @@ -0,0 +1,302 @@ +[ + { + "id": "db77b829167cd9b7", + "type": "tab", + "label": "client-side", + "disabled": false, + "info": "", + "env": [] + }, + { + "id": "61630a3c7b474222", + "type": "read-property", + "z": "db77b829167cd9b7", + "name": "", + "topic": "", + "thing": "a18841fe05744488", + "property": "count", + "uriVariables": "{}", + "observe": true, + "x": 460, + "y": 100, + "wires": [ + [ + "322c0bcf28580284" + ] + ] + }, + { + "id": "79fd49c9fbbeb1e7", + "type": "write-property", + "z": "db77b829167cd9b7", + "name": "", + "topic": "", + "thing": "a18841fe05744488", + "property": "count", + "uriVariables": "{}", + "x": 460, + "y": 220, + "wires": [] + }, + { + "id": "93ec4f561fdf3421", + "type": "invoke-action", + "z": "db77b829167cd9b7", + "name": "", + "topic": "", + "thing": "a18841fe05744488", + "action": "upper-case", + "uriVariables": "{}", + "x": 470, + "y": 340, + "wires": [ + [ + "fad6462e13d99af4" + ] + ] + }, + { + "id": "fa02f1dbac1161e9", + "type": "subscribe-event", + "z": "db77b829167cd9b7", + "name": "", + "topic": "", + "thing": "a18841fe05744488", + "event": "exampleEvnet", + "x": 160, + "y": 460, + "wires": [ + [ + "eac3b3f682579b5b" + ] + ] + }, + { + "id": "3f31b3723b36200a", + "type": "update-td", + "z": "db77b829167cd9b7", + "name": "", + "thing": "a18841fe05744488", + "tdSourceType": "msg", + "tdSource": "payload", + "x": 330, + "y": 600, + "wires": [] + }, + { + "id": "eac3b3f682579b5b", + "type": "debug", + "z": "db77b829167cd9b7", + "name": "received event", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 420, + "y": 460, + "wires": [] + }, + { + "id": "dc716499a751f93f", + "type": "inject", + "z": "db77b829167cd9b7", + "name": "new TD", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "{\"@context\":[\"https://www.w3.org/2019/wot/td/v1\",\"https://www.w3.org/2022/wot/td/v1.1\",{\"@language\":\"en\"}],\"@type\":\"Thing\",\"title\":\"example-thing\",\"securityDefinitions\":{\"nosec\":{\"scheme\":\"nosec\"}},\"security\":[\"nosec\"],\"properties\":{\"count\":{\"description\":\"count property of example-thing\",\"type\":\"integer\",\"readOnly\":false,\"observable\":true,\"forms\":[{\"href\":\"http://localhost:8080/example-thing/properties/count\",\"contentType\":\"application/json\",\"op\":[\"readproperty\",\"writeproperty\"]},{\"href\":\"http://localhost:8080/example-thing/properties/count/observable\",\"contentType\":\"application/json\",\"op\":[\"observeproperty\",\"unobserveproperty\"],\"subprotocol\":\"longpoll\"},{\"href\":\"http://localhost:8080/example-thing/properties/count\",\"contentType\":\"application/cbor\",\"op\":[\"readproperty\",\"writeproperty\"]},{\"href\":\"http://localhost:8080/example-thing/properties/count/observable\",\"contentType\":\"application/cbor\",\"op\":[\"observeproperty\",\"unobserveproperty\"],\"subprotocol\":\"longpoll\"}],\"writeOnly\":false}},\"actions\":{\"upper-case\":{\"description\":\"change text upper case\",\"input\":{\"type\":\"string\"},\"output\":{\"type\":\"string\"},\"forms\":[{\"href\":\"http://localhost:8080/example-thing/actions/upper-case\",\"contentType\":\"application/json\",\"op\":[\"i.168nvokeaction\"],\"htv:methodName\":\"POST\"},{\"href\":\"http://localhost:8080/example-thing/actions/upper-case\",\"contentType\":\"application/cbor\",\"op\":[\"invokeaction\"],\"htv:methodName\":\"POST\"}],\"idempotent\":false,\"safe\":false}},\"events\":{\"exampleEvnet\":{\"description\":\"event of example-thing\",\"data\":{\"type\":\"string\"},\"forms\":[{\"href\":\"http://localhost:8080/example-thing/events/exampleEvnet\",\"contentType\":\"application/json\",\"subprotocol\":\"longpoll\",\"op\":[\"subscribeevent\",\"unsubscribeevent\"]},{\"href\":\"http://localhost:8080/example-thing/events/exampleEvnet\",\"contentType\":\"application/cbor\",\"subprotocol\":\"longpoll\",\"op\":[\"subscribeevent\",\"unsubscribeevent\"]}]}},\"id\":\"urn:uuid:8c04b6e3-efa9-40eb-afc3-2b21adf2b2fa\",\"description\":\"thing for example\",\"forms\":[{\"href\":\"http://localhost:8080/example-thing/properties\",\"contentType\":\"application/json\",\"op\":[\"readallproperties\",\"readmultipleproperties\",\"writeallproperties\",\"writemultipleproperties\"]},{\"href\":\"http://localhost:8080/example-thing/properties\",\"contentType\":\"application/cbor\",\"op\":[\"readallproperties\",\"readmultipleproperties\",\"writeallproperties\",\"writemultipleproperties\"]}]}", + "payloadType": "json", + "x": 130, + "y": 600, + "wires": [ + [ + "3f31b3723b36200a" + ] + ] + }, + { + "id": "b8623621b379b0c1", + "type": "inject", + "z": "db77b829167cd9b7", + "name": "triggers count property retrieving", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "str", + "x": 210, + "y": 100, + "wires": [ + [ + "61630a3c7b474222" + ] + ] + }, + { + "id": "322c0bcf28580284", + "type": "debug", + "z": "db77b829167cd9b7", + "name": "value of count property", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 690, + "y": 100, + "wires": [] + }, + { + "id": "5113a72823dfb569", + "type": "inject", + "z": "db77b829167cd9b7", + "name": "triggers count property writing", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "999", + "payloadType": "num", + "x": 200, + "y": 220, + "wires": [ + [ + "79fd49c9fbbeb1e7" + ] + ] + }, + { + "id": "50ff215582317f8a", + "type": "inject", + "z": "db77b829167cd9b7", + "name": "triggers action with arguments", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Test Text", + "payloadType": "str", + "x": 200, + "y": 340, + "wires": [ + [ + "93ec4f561fdf3421" + ] + ] + }, + { + "id": "fad6462e13d99af4", + "type": "debug", + "z": "db77b829167cd9b7", + "name": "received event", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 700, + "y": 340, + "wires": [] + }, + { + "id": "7f3d6eb93c5502af", + "type": "comment", + "z": "db77b829167cd9b7", + "name": "Get value of count property", + "info": "", + "x": 180, + "y": 40, + "wires": [] + }, + { + "id": "b938daf466a9911a", + "type": "comment", + "z": "db77b829167cd9b7", + "name": "Write count property value", + "info": "", + "x": 170, + "y": 160, + "wires": [] + }, + { + "id": "9117817f4aacfbea", + "type": "comment", + "z": "db77b829167cd9b7", + "name": "Call to action", + "info": "", + "x": 130, + "y": 280, + "wires": [] + }, + { + "id": "7685f7bd971ff8dc", + "type": "comment", + "z": "db77b829167cd9b7", + "name": "Receives events generated by the server", + "info": "", + "x": 220, + "y": 400, + "wires": [] + }, + { + "id": "f354146ea4e9855b", + "type": "comment", + "z": "db77b829167cd9b7", + "name": "Update TD", + "info": "", + "x": 120, + "y": 540, + "wires": [] + }, + { + "id": "a18841fe05744488", + "type": "consumed-thing", + "tdLink": "", + "td": "{\"@context\":[\"https://www.w3.org/2019/wot/td/v1\",\"https://www.w3.org/2022/wot/td/v1.1\",{\"@language\":\"en\"}],\"@type\":\"Thing\",\"title\":\"example-thing\",\"securityDefinitions\":{\"nosec\":{\"scheme\":\"nosec\"}},\"security\":[\"nosec\"],\"properties\":{\"count\":{\"description\":\"count property of example-thing\",\"type\":\"integer\",\"readOnly\":false,\"observable\":true,\"forms\":[{\"href\":\"http://localhost:8080/example-thing/properties/count\",\"contentType\":\"application/json\",\"op\":[\"readproperty\",\"writeproperty\"]},{\"href\":\"http://localhost:8080/example-thing/properties/count/observable\",\"contentType\":\"application/json\",\"op\":[\"observeproperty\",\"unobserveproperty\"],\"subprotocol\":\"longpoll\"},{\"href\":\"http://localhost:8080/example-thing/properties/count\",\"contentType\":\"application/cbor\",\"op\":[\"readproperty\",\"writeproperty\"]},{\"href\":\"http://localhost:8080/example-thing/properties/count/observable\",\"contentType\":\"application/cbor\",\"op\":[\"observeproperty\",\"unobserveproperty\"],\"subprotocol\":\"longpoll\"}],\"writeOnly\":false}},\"actions\":{\"upper-case\":{\"description\":\"change text upper case\",\"input\":{\"type\":\"string\"},\"output\":{\"type\":\"string\"},\"forms\":[{\"href\":\"http://localhost:8080/example-thing/actions/upper-case\",\"contentType\":\"application/json\",\"op\":[\"i.168nvokeaction\"],\"htv:methodName\":\"POST\"},{\"href\":\"http://localhost:8080/example-thing/actions/upper-case\",\"contentType\":\"application/cbor\",\"op\":[\"invokeaction\"],\"htv:methodName\":\"POST\"}],\"idempotent\":false,\"safe\":false}},\"events\":{\"exampleEvnet\":{\"description\":\"event of example-thing\",\"data\":{\"type\":\"string\"},\"forms\":[{\"href\":\"http://localhost:8080/example-thing/events/exampleEvnet\",\"contentType\":\"application/json\",\"subprotocol\":\"longpoll\",\"op\":[\"subscribeevent\",\"unsubscribeevent\"]},{\"href\":\"http://localhost:8080/example-thing/events/exampleEvnet\",\"contentType\":\"application/cbor\",\"subprotocol\":\"longpoll\",\"op\":[\"subscribeevent\",\"unsubscribeevent\"]}]}},\"id\":\"urn:uuid:8c04b6e3-efa9-40eb-afc3-2b21adf2b2fa\",\"description\":\"thing for example\",\"forms\":[{\"href\":\"http://localhost:8080/example-thing/properties\",\"contentType\":\"application/json\",\"op\":[\"readallproperties\",\"readmultipleproperties\",\"writeallproperties\",\"writemultipleproperties\"]},{\"href\":\"http://localhost:8080/example-thing/properties\",\"contentType\":\"application/cbor\",\"op\":[\"readallproperties\",\"readmultipleproperties\",\"writeallproperties\",\"writemultipleproperties\"]}]}", + "http": true, + "ws": false, + "coap": false, + "mqtt": false, + "opcua": false, + "modbus": false, + "basicAuth": false, + "username": "", + "password": "" + } +] \ No newline at end of file diff --git a/node-red-node-wot/examples/server-side-flows.json b/node-red-node-wot/examples/server-side-flows.json new file mode 100644 index 0000000..3b7fb9d --- /dev/null +++ b/node-red-node-wot/examples/server-side-flows.json @@ -0,0 +1,395 @@ +[ + { + "id": "cf78d0f5c734a1c1", + "type": "tab", + "label": "server-side", + "disabled": false, + "info": "", + "env": [] + }, + { + "id": "202e63ba6adc8a34", + "type": "comment", + "z": "cf78d0f5c734a1c1", + "name": "Notification of server-side property changes", + "info": "", + "x": 250, + "y": 80, + "wires": [] + }, + { + "id": "ada66d68fe81a507", + "type": "comment", + "z": "cf78d0f5c734a1c1", + "name": "Processing in response to read requests", + "info": "", + "x": 840, + "y": 60, + "wires": [] + }, + { + "id": "7973116ae4dd5b38", + "type": "comment", + "z": "cf78d0f5c734a1c1", + "name": "Processing in response to write requests", + "info": "", + "x": 840, + "y": 140, + "wires": [] + }, + { + "id": "55dedaa6ded25815", + "type": "comment", + "z": "cf78d0f5c734a1c1", + "name": "Processing in response to a call to action", + "info": "", + "x": 240, + "y": 240, + "wires": [] + }, + { + "id": "1b9b4b89822a4505", + "type": "comment", + "z": "cf78d0f5c734a1c1", + "name": "Notification of events to clients", + "info": "", + "x": 210, + "y": 360, + "wires": [] + }, + { + "id": "d953be2f767a1189", + "type": "inject", + "z": "cf78d0f5c734a1c1", + "name": "notify property change", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "str", + "x": 200, + "y": 140, + "wires": [ + [ + "e8a4a494e8f5f37f" + ] + ] + }, + { + "id": "e8a4a494e8f5f37f", + "type": "wot-server-property", + "z": "cf78d0f5c734a1c1", + "name": "count property of example-thing", + "propertyName": "count", + "propertyDescription": "count property of example-thing", + "propertyDataType": "integer", + "propertyReadOnlyFlag": false, + "propertyObservableFlag": true, + "outParams2_writingValueType": "msg", + "outParams2_writingValueConstValue": "payload", + "woTServerConfig": "8ea1eb186841e85e", + "woTThingConfig": "1c0a9615452ce161", + "x": 490, + "y": 140, + "wires": [ + [ + "3cbfdf6405a53157" + ], + [ + "e969b903f974666a" + ] + ] + }, + { + "id": "3cbfdf6405a53157", + "type": "function", + "z": "cf78d0f5c734a1c1", + "name": "property reading process", + "func": "let count = global.get(\"example-thing::count\") || 0\nmsg.payload = count\nreturn msg", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 790, + "y": 100, + "wires": [ + [ + "8cb23c141bd2b38b" + ] + ] + }, + { + "id": "8cb23c141bd2b38b", + "type": "wot-server-end", + "z": "cf78d0f5c734a1c1", + "name": "return to client with count", + "inParams_returnValueType": "msg", + "inParams_returnValueConstValue": "payload", + "x": 1050, + "y": 100, + "wires": [] + }, + { + "id": "e969b903f974666a", + "type": "function", + "z": "cf78d0f5c734a1c1", + "name": "property writing process", + "func": "let count = msg.payload\nglobal.set(\"example-thing::count\", count)\nreturn msg", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 790, + "y": 180, + "wires": [ + [ + "028f6d05faa89b8b" + ] + ] + }, + { + "id": "028f6d05faa89b8b", + "type": "wot-server-end", + "z": "cf78d0f5c734a1c1", + "name": "return to client", + "inParams_returnValueType": "msg", + "inParams_returnValueConstValue": "payload", + "x": 1020, + "y": 180, + "wires": [] + }, + { + "id": "e7ad2187d86fc7c4", + "type": "catch", + "z": "cf78d0f5c734a1c1", + "name": "", + "scope": null, + "uncaught": false, + "x": 140, + "y": 660, + "wires": [ + [ + "01e1f11d37e0ea92" + ] + ] + }, + { + "id": "01e1f11d37e0ea92", + "type": "debug", + "z": "cf78d0f5c734a1c1", + "name": "error", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "statusVal": "", + "statusType": "auto", + "x": 330, + "y": 660, + "wires": [] + }, + { + "id": "ff3585318a4155cb", + "type": "wot-server-action", + "z": "cf78d0f5c734a1c1", + "name": "upper-case action of example-thing", + "actionName": "upper-case", + "actionDescription": "change text upper case", + "actionInputDataType": "string", + "actionOutputDataType": "string", + "outParams1_actionArgsType": "msg", + "outParams1_actionArgsConstValue": "payload", + "woTServerConfig": "8ea1eb186841e85e", + "woTThingConfig": "1c0a9615452ce161", + "x": 220, + "y": 300, + "wires": [ + [ + "718f49c507b46038" + ] + ] + }, + { + "id": "718f49c507b46038", + "type": "function", + "z": "cf78d0f5c734a1c1", + "name": "property writing process", + "func": "let text = msg.payload\nmsg.payload = text.toUpperCase()\nreturn msg", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 510, + "y": 300, + "wires": [ + [ + "e1433e51ad928ef6" + ] + ] + }, + { + "id": "e1433e51ad928ef6", + "type": "wot-server-end", + "z": "cf78d0f5c734a1c1", + "name": "return to client with return value", + "inParams_returnValueType": "msg", + "inParams_returnValueConstValue": "payload", + "x": 810, + "y": 300, + "wires": [] + }, + { + "id": "741b5cd6ab2cf4b9", + "type": "wot-server-event", + "z": "cf78d0f5c734a1c1", + "name": "send event to client", + "eventName": "exampleEvnet", + "eventDescription": "event of example-thing", + "eventDataType": "string", + "inParams_eventValueType": "msg", + "inParams_eventValueConstValue": "payload", + "woTServerConfig": "8ea1eb186841e85e", + "woTThingConfig": "1c0a9615452ce161", + "x": 410, + "y": 420, + "wires": [] + }, + { + "id": "6ceccdfd164ad23f", + "type": "inject", + "z": "cf78d0f5c734a1c1", + "name": "event occurrence", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "test-event", + "payloadType": "str", + "x": 180, + "y": 420, + "wires": [ + [ + "741b5cd6ab2cf4b9" + ] + ] + }, + { + "id": "89227d23772205ee", + "type": "comment", + "z": "cf78d0f5c734a1c1", + "name": "Catching exceptions", + "info": "", + "x": 170, + "y": 600, + "wires": [] + }, + { + "id": "b8f65f663573710d", + "type": "comment", + "z": "cf78d0f5c734a1c1", + "name": "Retrieve generated TD (TD can also be retrieved from the global context)", + "info": "", + "x": 340, + "y": 480, + "wires": [] + }, + { + "id": "f2e4e6998c4dfd70", + "type": "wot-server-td", + "z": "cf78d0f5c734a1c1", + "name": "Retrieve generated TD", + "outParams1_tdType": "msg", + "outParams1_tdConstValue": "payload", + "woTServerConfig": "8ea1eb186841e85e", + "woTThingConfig": "1c0a9615452ce161", + "outputTDAfterServerStartFlag": true, + "x": 440, + "y": 540, + "wires": [ + [ + "582194de977619e5" + ] + ] + }, + { + "id": "2971b78046383811", + "type": "inject", + "z": "cf78d0f5c734a1c1", + "name": "triggers TD retrieving", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "str", + "x": 200, + "y": 540, + "wires": [ + [ + "f2e4e6998c4dfd70" + ] + ] + }, + { + "id": "582194de977619e5", + "type": "debug", + "z": "cf78d0f5c734a1c1", + "name": "TD", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 650, + "y": 540, + "wires": [] + }, + { + "id": "8ea1eb186841e85e", + "type": "wot-server-config", + "name": "HttpServer", + "bindingType": "http", + "bindingConfigType": "json", + "bindingConfigConstValue": "{\"port\":8080,\"allowSelfSigned\":true}" + }, + { + "id": "1c0a9615452ce161", + "type": "wot-thing-config", + "name": "example-thing", + "description": "thing for example", + "thingId": "", + "basicAuth": false, + "basicAuthUsername": "", + "basicAuthPassword": "" + } +] \ No newline at end of file diff --git a/node-red-node-wot/package-lock.json b/node-red-node-wot/package-lock.json index 61d5608..25e41e5 100644 --- a/node-red-node-wot/package-lock.json +++ b/node-red-node-wot/package-lock.json @@ -1,12 +1,12 @@ { "name": "@thingweb/node-red-node-wot", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@thingweb/node-red-node-wot", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "@node-wot/binding-coap": "0.8.12", @@ -18,7 +18,7 @@ "@node-wot/core": "0.8.12" }, "devDependencies": { - "@types/chai": "^4.3.11", + "@types/chai": "^4.3.12", "@types/chai-as-promised": "^7.1.8", "@types/mocha": "^10.0.6", "@types/node-red": "^1.3.4", @@ -36,7 +36,7 @@ "typescript": "^4.9.5" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" } }, "node_modules/@babel/code-frame": { diff --git a/node-red-node-wot/package.json b/node-red-node-wot/package.json index febea88..bfc3784 100644 --- a/node-red-node-wot/package.json +++ b/node-red-node-wot/package.json @@ -1,6 +1,6 @@ { "name": "@thingweb/node-red-node-wot", - "version": "1.0.1", + "version": "1.1.0", "description": "Web of Things nodes for Node-RED using node-wot", "author": "Eclipse Thingweb (https://thingweb.io/)", "license": "MIT", @@ -12,7 +12,7 @@ "node-red" ], "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" }, "repository": { "type": "git", @@ -25,14 +25,16 @@ "wot-property": "dist/wot-property.js", "wot-action": "dist/wot-action.js", "wot-event": "dist/wot-event.js", + "wot-update-td": "dist/wot-update-td.js", "wot-server-config": "dist/wot-server-config.js", "wot-thing-config": "dist/wot-thing-config.js", "wot-server-end": "dist/wot-server-end.js", "wot-server-property": "dist/wot-server-property.js", "wot-server-action": "dist/wot-server-action.js", - "wot-server-event": "dist/wot-server-event.js" + "wot-server-event": "dist/wot-server-event.js", + "wot-server-td": "dist/wot-server-td.js" }, - "version":">=2.0.0" + "version": ">=1.3.7" }, "scripts": { "build": "npm run copy:src2dist && tsc", @@ -51,7 +53,7 @@ "@node-wot/core": "0.8.12" }, "devDependencies": { - "@types/chai": "^4.3.11", + "@types/chai": "^4.3.12", "@types/chai-as-promised": "^7.1.8", "@types/mocha": "^10.0.6", "@types/node-red": "^1.3.4", diff --git a/node-red-node-wot/screenshots/import-example-flows.png b/node-red-node-wot/screenshots/import-example-flows.png new file mode 100644 index 0000000..b29cceb Binary files /dev/null and b/node-red-node-wot/screenshots/import-example-flows.png differ diff --git a/node-red-node-wot/screenshots/nodes.png b/node-red-node-wot/screenshots/nodes.png old mode 100755 new mode 100644 index 2d4d360..cc67726 Binary files a/node-red-node-wot/screenshots/nodes.png and b/node-red-node-wot/screenshots/nodes.png differ diff --git a/node-red-node-wot/screenshots/thing-config-settings.png b/node-red-node-wot/screenshots/thing-config-settings.png index 02e0667..0abab08 100644 Binary files a/node-red-node-wot/screenshots/thing-config-settings.png and b/node-red-node-wot/screenshots/thing-config-settings.png differ diff --git a/node-red-node-wot/src/locales/en-US/wot-server-td.html b/node-red-node-wot/src/locales/en-US/wot-server-td.html new file mode 100644 index 0000000..de46681 --- /dev/null +++ b/node-red-node-wot/src/locales/en-US/wot-server-td.html @@ -0,0 +1,37 @@ + diff --git a/node-red-node-wot/src/locales/en-US/wot-server-td.json b/node-red-node-wot/src/locales/en-US/wot-server-td.json new file mode 100644 index 0000000..31c5baf --- /dev/null +++ b/node-red-node-wot/src/locales/en-US/wot-server-td.json @@ -0,0 +1,18 @@ +{ + "editor": { + "paletteLabel": "Server-TD", + "nameLabel": "Name", + "serverConfigLabel": "Server config", + "thingConfigLabel": "Thing config", + "outputTDAfterServerStartFlagLabel": "TD output after server start", + "outParams1": { + "tabLabel": "Output TD", + "outputLabel": "TD", + "td": { + "label": "TD destination", + "description": "TD destination.", + "placeholder": "TD destination" + } + } + } +} diff --git a/node-red-node-wot/src/locales/en-US/wot-thing-config.html b/node-red-node-wot/src/locales/en-US/wot-thing-config.html index 3f15dc0..2346340 100644 --- a/node-red-node-wot/src/locales/en-US/wot-thing-config.html +++ b/node-red-node-wot/src/locales/en-US/wot-thing-config.html @@ -10,6 +10,15 @@

Details

to this setting are published with the Thing title specified here.
  • Description: Specify the description of the Thing.
  • +
  • + Thing ID: Specify the Thing ID. The specified Thing ID is used as the ID for Thing Description. If omitted, + the ID for Thing Description is automatically generated.
    + Example of Thing ID: "urn:dev:ops:32473-WoTLamp-1234" +
  • +
  • + Use basic authentication: Specifies whether Basic authentication is applied to the Thing.
    + If you use Basic Authentication, please set your user name and password. +
  • The Thing Description is stored in the thingDescriptions variable of the global context with the key of diff --git a/node-red-node-wot/src/locales/en-US/wot-thing-config.json b/node-red-node-wot/src/locales/en-US/wot-thing-config.json index 20e987e..fa4dccd 100644 --- a/node-red-node-wot/src/locales/en-US/wot-thing-config.json +++ b/node-red-node-wot/src/locales/en-US/wot-thing-config.json @@ -1,6 +1,11 @@ { "editor": { "nameLabel": "Thing title", - "descriptionLabel": "Description" + "descriptionLabel": "Description", + "thingIdLabel": "Thing ID", + "securityLabel": "Security:", + "basicAuthLabel": "Use basic authentication", + "usernameLabel": "Username", + "passwordLabel": "Password" } } diff --git a/node-red-node-wot/src/locales/ja/wot-server-property.html b/node-red-node-wot/src/locales/ja/wot-server-property.html index 09720bc..b9c28c8 100644 --- a/node-red-node-wot/src/locales/ja/wot-server-property.html +++ b/node-red-node-wot/src/locales/ja/wot-server-property.html @@ -15,7 +15,7 @@

    Inputs

    プロパティ変更の通知 - any(プロパティの定義による) + -
    サーバ側でプロパティに変更があった場合に入力を行います。入力を行うことで観測しているクライアントに通知されます。入力に値を含める必要はありません。 diff --git a/node-red-node-wot/src/locales/ja/wot-server-td.html b/node-red-node-wot/src/locales/ja/wot-server-td.html new file mode 100644 index 0000000..03a3640 --- /dev/null +++ b/node-red-node-wot/src/locales/ja/wot-server-td.html @@ -0,0 +1,37 @@ + diff --git a/node-red-node-wot/src/locales/ja/wot-server-td.json b/node-red-node-wot/src/locales/ja/wot-server-td.json new file mode 100644 index 0000000..b0f3365 --- /dev/null +++ b/node-red-node-wot/src/locales/ja/wot-server-td.json @@ -0,0 +1,18 @@ +{ + "editor": { + "paletteLabel": "Server-TD", + "nameLabel": "名前", + "serverConfigLabel": "サーバ設定", + "thingConfigLabel": "Thing設定", + "outputTDAfterServerStartFlagLabel": "サーバ起動後のTD出力", + "outParams1": { + "tabLabel": "TD出力", + "outputLabel": "TD", + "td": { + "label": "TDの出力先", + "description": "取得したTDの出力先。", + "placeholder": "取得したTDの出力先" + } + } + } +} diff --git a/node-red-node-wot/src/locales/ja/wot-thing-config.html b/node-red-node-wot/src/locales/ja/wot-thing-config.html index d2b3d4a..28d3d81 100644 --- a/node-red-node-wot/src/locales/ja/wot-thing-config.html +++ b/node-red-node-wot/src/locales/ja/wot-thing-config.html @@ -10,6 +10,15 @@

    Details

    Descriptionでは、ここで指定したThing名で、本設定を参照するプロパティ、アクション、イベントを公開します。
  • 説明: Thingの説明を指定します。
  • +
  • + Thing ID: Thing IDを指定します。指定したThing IDは、Thing + DescriptionのIDとして利用します。省略した場合は、Thing DescriptionのIDは自動的に生成されます。
    + Thing IDの例: "urn:dev:ops:32473-WoTLamp-1234" +
  • +
  • + Basic認証利用: ThingにBasic認証をかけるかどうかを指定します。
    + Basic認証を利用する場合は、ユーザー名とパスワードを設定してください。 +
  • Thing diff --git a/node-red-node-wot/src/locales/ja/wot-thing-config.json b/node-red-node-wot/src/locales/ja/wot-thing-config.json index 044e695..128d9b0 100644 --- a/node-red-node-wot/src/locales/ja/wot-thing-config.json +++ b/node-red-node-wot/src/locales/ja/wot-thing-config.json @@ -1,6 +1,11 @@ { "editor": { "nameLabel": "Thing名", - "descriptionLabel": "説明" + "descriptionLabel": "説明", + "thingIdLabel": "Thing ID", + "securityLabel": "セキュリティ:", + "basicAuthLabel": "Basic認証利用", + "usernameLabel": "ユーザー名", + "passwordLabel": "パスワード" } } diff --git a/node-red-node-wot/src/servients/servient-wrapper.ts b/node-red-node-wot/src/servients/servient-wrapper.ts index c15efc1..00563d1 100644 --- a/node-red-node-wot/src/servients/servient-wrapper.ts +++ b/node-red-node-wot/src/servients/servient-wrapper.ts @@ -51,6 +51,12 @@ export default class ServientWrapper { return this.things[thingName] } + public addCredentials(title, credentials) { + const thing = this.things[title] + const td = thing.getThingDescription() + this.servient.addCredentials({ [td.id]: credentials }) + } + public async endServient() { if (this.server) { console.debug("[debug] endServient called.") diff --git a/node-red-node-wot/src/wot-action.js b/node-red-node-wot/src/wot-action.js index 0714161..69ed103 100644 --- a/node-red-node-wot/src/wot-action.js +++ b/node-red-node-wot/src/wot-action.js @@ -4,6 +4,9 @@ module.exports = function (RED) { function invokeActionNode(config) { RED.nodes.createNode(this, config) let node = this + let consumedThing + + this.status({}) if (!config.thing) { this.status({ fill: "red", shape: "dot", text: "Error: Thing undefined" }) @@ -17,15 +20,26 @@ module.exports = function (RED) { return } + const thingNode = RED.nodes.getNode(config.thing) + thingNode.addUpdateTDListener(async (_consumedThing) => { + consumedThing = _consumedThing + }) + this.on("input", function (msg, send, done) { - RED.nodes.getNode(config.thing).consumedThing.then((consumedThing) => { - const uriVariables = config.uriVariables ? JSON.parse(config.uriVariables) : undefined - consumedThing - .invokeAction(config.action, msg.payload, { - uriVariables: uriVariables, - }) - .then(async (resp) => { - const payload = resp ? await resp.value() : "" + if (!consumedThing) { + node.error("[error] consumedThing is not defined.") + done("consumedThing is not defined.") + return + } + const uriVariables = config.uriVariables ? JSON.parse(config.uriVariables) : undefined + consumedThing + .invokeAction(config.action, msg.payload, { + uriVariables: uriVariables, + }) + .then(async (resp) => { + let payload + try { + payload = await resp.value() node.send({ payload: payload, topic: config.topic }) node.status({ fill: "green", @@ -33,17 +47,20 @@ module.exports = function (RED) { text: "invoked", }) done() + } catch (err) { + console.error(`[error] failed to get return value. err: `, err) + done(`[error] failed to get return value. err: ${err.toString()}`) + } + }) + .catch((err) => { + node.warn(err) + node.status({ + fill: "red", + shape: "ring", + text: err.message, }) - .catch((err) => { - node.warn(err) - node.status({ - fill: "red", - shape: "ring", - text: err.message, - }) - done(err) - }) - }) + done(err) + }) }) this.on("close", function (removed, done) { diff --git a/node-red-node-wot/src/wot-event.js b/node-red-node-wot/src/wot-event.js index 89a0f53..ba7a639 100644 --- a/node-red-node-wot/src/wot-event.js +++ b/node-red-node-wot/src/wot-event.js @@ -4,8 +4,10 @@ module.exports = function (RED) { function subscribeEventNode(config) { RED.nodes.createNode(this, config) let node = this + let consumedThing + let subscription + let repeatId - this.subscription = undefined this.status({}) if (!config.thing) { @@ -16,95 +18,86 @@ module.exports = function (RED) { return } - RED.nodes - .getNode(config.thing) - .consumedThing.then(async (consumedThing) => { - let subscription - // Repeat until event subscription succeeds. - try { - while (true) { - subscription = await consumedThing - .subscribeEvent( - config.event, - async (resp) => { - if (resp) { - let payload - try { - payload = await resp.value() - } catch (err) { - node.error(`[error] failed to get event. err: ${err.toString()}`) - console.error(`[error] failed to get event. err: `, err) - } - node.send({ payload, topic: config.topic }) + const thingNode = RED.nodes.getNode(config.thing) + thingNode.addUpdateTDListener(async (_consumedThing) => { + if (repeatId) { + clearInterval(repeatId) + repeatId = undefined + } + if (subscription) { + // Stop if already subscribed + await subscription.stop() + } + subscription = undefined + consumedThing = _consumedThing + // Repeat until event subscription succeeds. + await new Promise((resolve) => { + repeatId = setInterval(() => { + consumedThing + .subscribeEvent( + config.event, + async (resp) => { + if (resp) { + let payload + try { + payload = await resp.value() + } catch (err) { + node.error(`[error] failed to get event. err: ${err.toString()}`) + console.error(`[error] failed to get event. err: `, err) } - node.status({ - fill: "green", - shape: "dot", - text: "Subscribed", - }) - }, - (err) => { - console.error("[error] subscribe events.", err) - node.error(`[error] subscribe events. err: ${err.toString()}`) - node.status({ - fill: "red", - shape: "ring", - text: "Subscription error", - }) - }, - () => { - console.error("[warn] Subscription ended.") - node.warn("[warn] Subscription ended.") - node.status({}) - node.subscription = undefined + node.send({ payload, topic: config.topic }) } - ) - .catch((err) => { - console.warn("[warn] event subscribe error. try again. error: " + err) - }) - if (subscription) { - break - } - await (() => { - return new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 500) - }) - })() - } - } catch (err) { - node.status({ - fill: "red", - shape: "ring", - text: "Subscription error", - }) - node.error(`[error] failed to subscribe events. error: ${err.toString()}`) - } - node.subscription = subscription - - if (node.subscription) { - node.status({ - fill: "green", - shape: "dot", - text: "Subscribed", - }) - } + node.status({ + fill: "green", + shape: "dot", + text: "Subscribed", + }) + }, + (err) => { + console.error("[error] subscribe events.", err) + node.error(`[error] subscribe events. err: ${err.toString()}`) + node.status({ + fill: "red", + shape: "ring", + text: "Subscription error", + }) + }, + () => { + console.error("[warn] Subscription ended.") + node.warn("[warn] Subscription ended.") + node.status({}) + subscription = undefined + } + ) + .then((sub) => { + subscription = sub + clearInterval(repeatId) + repeatId = undefined + resolve() + }) + .catch((err) => { + console.warn("[warn] event subscribe error. try again. error: " + err) + }) + }, 1000) }) - .catch((err) => { + + if (subscription) { node.status({ - fill: "red", - shape: "ring", - text: "Subscription error", + fill: "green", + shape: "dot", + text: "Subscribed", }) - node.error(`[error] Failed to create consumed thing for enents. err: ${err.toString()}`) - }) + } + }) - this.on("close", function (removed, done) { - if (removed) { - // This node has been deleted - } else { - // This node is being restarted + this.on("close", async function (removed, done) { + if (repeatId) { + clearInterval(repeatId) + repeatId = undefined + } + if (subscription) { + // Stop if already subscribed + await subscription.stop() } done() }) diff --git a/node-red-node-wot/src/wot-property.js b/node-red-node-wot/src/wot-property.js index e35097d..9d4e3ed 100644 --- a/node-red-node-wot/src/wot-property.js +++ b/node-red-node-wot/src/wot-property.js @@ -5,6 +5,8 @@ module.exports = function (RED) { RED.nodes.createNode(this, config) let node = this let consumedThing + let subscription + let repeatId this.status({}) @@ -20,60 +22,69 @@ module.exports = function (RED) { return } - RED.nodes.getNode(config.thing).consumedThing.then(async (thing) => { - consumedThing = thing + const thingNode = RED.nodes.getNode(config.thing) + thingNode.addUpdateTDListener(async (_consumedThing) => { + if (repeatId) { + clearInterval(repeatId) + repeatId = undefined + } + if (subscription) { + // Stop if already subscribed + await subscription.stop() + } + subscription = undefined + consumedThing = _consumedThing if (config.observe === false) { return } // Repeat until observeProperty succeeds. - let ob - while (true) { - try { - ob = await consumedThing.observeProperty( - config.property, - async (resp) => { - let payload - try { - payload = await resp.value() - } catch (err) { - node.error(`[error] failed to get property change. err: ${err.toString()}`) - console.error(`[error] failed to get property change. err:`, err) + await new Promise((resolve) => { + repeatId = setInterval(() => { + consumedThing + .observeProperty( + config.property, + async (resp) => { + let payload + try { + payload = await resp.value() + } catch (err) { + node.error(`[error] failed to get property change. err: ${err.toString()}`) + console.error(`[error] failed to get property change. err:`, err) + } + node.send({ payload, topic: config.topic }) + }, + (err) => { + node.error(`[error] property observe error. error: ${err.toString()}`) + console.error(`[error] property observe error. error: `, err) + node.status({ + fill: "red", + shape: "ring", + text: "Observe error", + }) } - node.send({ payload, topic: config.topic }) - }, - (err) => { - node.error(`[error] property observe error. error: ${err.toString()}`) - console.error(`[error] property observe error. error: `, err) + ) + .then((sub) => { + subscription = sub + clearInterval(repeatId) + repeatId = undefined + resolve() + }) + .catch((err) => { + console.warn("[warn] property observe error. try again. error: " + err) node.status({ fill: "red", shape: "ring", text: "Observe error", }) - } - ) - } catch (err) { - console.warn("[warn] property observe error. try again. error: " + err) - node.status({ - fill: "red", - shape: "ring", - text: "Observe error", - }) - } - if (ob) { - node.status({ - fill: "green", - shape: "dot", - text: "connected", - }) - break - } - await (() => { - return new Promise((resolve) => { - setTimeout(() => { - resolve() - }, 500) - }) - })() + }) + }, 1000) + }) + if (subscription) { + node.status({ + fill: "green", + shape: "dot", + text: "connected", + }) } }) @@ -113,11 +124,14 @@ module.exports = function (RED) { }) }) - node.on("close", function (removed, done) { - if (removed) { - // This node has been deleted - } else { - // This node is being restarted + node.on("close", async function (removed, done) { + if (repeatId) { + clearInterval(repeatId) + repeatId = undefined + } + if (subscription) { + // Stop if already subscribed + await subscription.stop() } done() }) @@ -127,6 +141,7 @@ module.exports = function (RED) { function writePropertyNode(config) { RED.nodes.createNode(this, config) let node = this + let consumedThing this.status({}) @@ -142,32 +157,49 @@ module.exports = function (RED) { return } - RED.nodes.getNode(config.thing).consumedThing.then((consumedThing) => { - node.on("input", function (msg, send, done) { - const uriVariables = config.uriVariables ? JSON.parse(config.uriVariables) : undefined - consumedThing - .writeProperty(config.property, msg.payload, { - uriVariables: uriVariables, - }) - .then((resp) => { - if (resp) node.send({ payload: resp, topic: config.topic }) - node.status({ - fill: "green", - shape: "dot", - text: "connected", - }) - done() + const thingNode = RED.nodes.getNode(config.thing) + thingNode.addUpdateTDListener(async (_consumedThing) => { + consumedThing = _consumedThing + }) + + node.on("input", function (msg, send, done) { + if (!consumedThing) { + node.error("[error] consumedThing is not defined.") + done("consumedThing is not defined.") + return + } + const uriVariables = config.uriVariables ? JSON.parse(config.uriVariables) : undefined + consumedThing + .writeProperty(config.property, msg.payload, { + uriVariables: uriVariables, + }) + .then((resp) => { + if (resp) node.send({ payload: resp, topic: config.topic }) + node.status({ + fill: "green", + shape: "dot", + text: "connected", }) - .catch((err) => { - node.warn(err) - node.status({ - fill: "red", - shape: "ring", - text: err.message, - }) - done(err) + done() + }) + .catch((err) => { + node.warn(err) + node.status({ + fill: "red", + shape: "ring", + text: err.message, }) - }) + done(err) + }) + }) + + this.on("close", function (removed, done) { + if (removed) { + // This node has been deleted + } else { + // This node is being restarted + } + done() }) } RED.nodes.registerType("write-property", writePropertyNode) diff --git a/node-red-node-wot/src/wot-server-action.html b/node-red-node-wot/src/wot-server-action.html index ae0c2b7..c3f20dd 100644 --- a/node-red-node-wot/src/wot-server-action.html +++ b/node-red-node-wot/src/wot-server-action.html @@ -5,16 +5,17 @@ color: "#a2dea0", defaults: { name: { value: "" }, - actionName: { value: "", required: true }, actionDescription: { value: "" }, actionInputDataType: { value: "string", required: true }, actionOutputDataType: { value: "string", required: true }, outParams1_actionArgsType: { value: "msg", + required: true, }, outParams1_actionArgsConstValue: { value: "payload", + required: true, }, woTServerConfig: { type: "wot-server-config", @@ -27,9 +28,6 @@ required: true, }, }, - credentials: { - outParams1_actionArgs: { type: "text" }, - }, inputs: 0, outputs: 1, outputLabels: function (index) { @@ -61,66 +59,14 @@ id: "tab-outParams1-settings", label: this._("editor.outParams1.tabLabel"), }) - prepareInOutParamSetting( - "outParams1", - { - name: "actionArgs", - types: ["msg"], - defaultType: "msg", - defaultValue: "payload", - required: true, - }, - this - ) - }, - oneditsave: function () { - saveInOutParamSetting( - "outParams1", - { - name: "actionArgs", - types: ["msg"], - defaultType: "msg", - defaultValue: "payload", - required: true, - }, - this - ) + $("#node-input-outParams1_actionArgsConstValue").typedInput({ + defaultType: "msg", + defaultValue: "payload", + types: ["msg"], + typeField: "#node-input-outParams1_actionArgsType", + }) }, }) - const prepareInOutParamSetting = (inOrOutParams, params, _this) => { - const { name, types, defaultType, defaultValue, required } = params - const varName = `${inOrOutParams}_${name}` - if (!_this[`${varName}Type`]) { - _this[`${varName}Type`] = defaultType - } - if (this[`${varName}Type`] == "str") { - $(`#node-input-${varName}ConstValue`).val("") - } else { - if (_this[`${varName}ConstValue`] == "") { - $(`#node-input-${varName}ConstValue`).val(_this.credentials[varName]) - } else { - _this.credentials[varName] = _this[`${varName}ConstValue`] - $(`#node-input-${varName}`).val(_this.credentials[`${varName}`]) - } - } - $(`#node-input-${varName}Type`).val(_this[`${varName}Type`]) - $(`#node-input-${varName}`).typedInput({ - default: defaultType, - typeField: $(`#node-input-${varName}Type`), - types: types, - }) - $(`#node-input-${varName}`).typedInput("type", _this[`${varName}Type`]) - } - const saveInOutParamSetting = (inOrOutParams, params, _this) => { - const varName = `${inOrOutParams}_${params.name}` - if ($(`#node-input-${varName}Type`).val() != "str") { - _this[`${varName}ConstValue`] = $(`#node-input-${varName}`).val() - $(`#node-input-${varName}ConstValue`).val(_this[`${varName}ConstValue`]) - } else { - $(`#node-input-${varName}ConstValue`).val("") - _this[`${varName}ConstValue`] = "" - } - } })() @@ -181,10 +127,9 @@

    - - + + -
    diff --git a/node-red-node-wot/src/wot-server-action.ts b/node-red-node-wot/src/wot-server-action.ts index 5da6736..0ed7118 100644 --- a/node-red-node-wot/src/wot-server-action.ts +++ b/node-red-node-wot/src/wot-server-action.ts @@ -31,9 +31,8 @@ module.exports = function (RED) { } // for wot-server-config - node.getThingProps = () => { - const woTThingConfig = RED.nodes.getNode(config.woTThingConfig) - return woTThingConfig.getProps() + node.getThingNode = () => { + return RED.nodes.getNode(config.woTThingConfig) } node.on("close", function (removed, done) { @@ -45,12 +44,8 @@ module.exports = function (RED) { done() }) - const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) //test + const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) woTServerConfig.addUserNode(node) } - RED.nodes.registerType("wot-server-action", WoTServerAction, { - credentials: { - inParams_actionName: { type: "text" }, - }, - }) + RED.nodes.registerType("wot-server-action", WoTServerAction) } diff --git a/node-red-node-wot/src/wot-server-config.html b/node-red-node-wot/src/wot-server-config.html index 04c5c08..e1d4a5c 100644 --- a/node-red-node-wot/src/wot-server-config.html +++ b/node-red-node-wot/src/wot-server-config.html @@ -22,12 +22,9 @@ category: "config", defaults: { name: { value: "", required: true }, - bindingType: { value: "http" }, - bindingConfigType: { value: "json" }, - bindingConfigConstValue: { value: "{}" }, - }, - credentials: { - bindingConfig: { type: "object" }, + bindingType: { value: "http", required: true }, + bindingConfigType: { value: "json", required: true }, + bindingConfigConstValue: { value: "{}", required: true }, }, label: function () { return this.name || this._("editor.configLabel") @@ -37,16 +34,12 @@ }, oneditprepare: function () { // 入力パラメータの指定項目作成 - createInOutParamSetting( - { - name: "bindingConfig", - types: ["json"], - defaultType: "json", - defaultValue: "{}", - required: true, - }, - this - ) + $("#node-config-input-bindingConfigConstValue").typedInput({ + defaultType: "json", + defaultValue: "{}", + types: ["json"], + typeField: "#node-config-input-bindingConfigType", + }) BINDING_TYPE_LIST.forEach((type) => { $("#node-config-input-bindingType").append(``) @@ -57,62 +50,19 @@ $("#node-config-input-bindingType").on("change", () => { const selectedBindingType = $("#node-config-input-bindingType").val() if (selectedBindingType === this.bindingType && this.bindingConfigConstValue !== "{}") { - $("#node-config-input-bindingConfig").typedInput("value", this.bindingConfigConstValue) + $("#node-config-input-bindingConfigConstValue").typedInput( + "value", + this.bindingConfigConstValue + ) } else { - $("#node-config-input-bindingConfig").typedInput( + $("#node-config-input-bindingConfigConstValue").typedInput( "value", JSON.stringify(BINDING_CONFIG_DEFAULT[selectedBindingType]) ) } }) }, - oneditsave: function () { - saveInOutParamSetting( - { - name: "bindingConfig", - types: ["json"], - defaultType: "json", - defaultValue: "{}", - required: true, - }, - this - ) - }, }) - - const createInOutParamSetting = (params, _this) => { - const { name, types, defaultType, defaultValue, required } = params - if (!_this[name + "Type"]) { - _this[name + "Type"] = defaultType - } - if (_this[name + "Type"] == "str") { - $(`#node-config-input-${name}ConstValue`).val("") - } else { - if (_this[name + "ConstValue"] == "") { - $(`#node-config-input-${name}ConstValue`).val(_this.credentials[name]) - } else { - _this.credentials[name] = _this[name + "ConstValue"] - $(`#node-config-input-${name}`).val(_this.credentials[name]) - } - } - $(`#node-config-input-${name}Type`).val(_this[name + "Type"]) - $(`#node-config-input-${name}`).typedInput({ - default: defaultType, - typeField: $(`#node-config-input-${name}Type`), - types: types, - }) - $(`#node-config-input-${name}`).typedInput("type", _this[name + "Type"]) - } - const saveInOutParamSetting = (params, _this) => { - const { name } = params - if ($(`#node-config-input-${name}Type`).val() != "str") { - _this[`${name}ConstValue`] = $(`#node-config-input-${name}`).val() - $(`#node-config-input-${name}ConstValue`).val(_this[`${name}ConstValue`]) - } else { - $(`#node-config-input-${name}ConstValue`).val("") - _this[`${name}ConstValue`] = "" - } - } })() @@ -126,14 +76,14 @@
    - + -
    diff --git a/node-red-node-wot/src/wot-server-config.ts b/node-red-node-wot/src/wot-server-config.ts index 1e04519..d830c81 100644 --- a/node-red-node-wot/src/wot-server-config.ts +++ b/node-red-node-wot/src/wot-server-config.ts @@ -21,6 +21,16 @@ module.exports = function (RED) { } } + function getSecurityDefinition(scheme) { + let params + if (scheme === "basic") { + params = { scheme, in: "header" } + } else { + params = { scheme } + } + return params + } + async function waitForFinishPrepareRelatedNodes(userNodes: any[], userNodeIds: string[]) { const MAX_CHECK_COUNT = 50 const WAIT_MILLI_SEC = 100 //ms @@ -62,12 +72,10 @@ module.exports = function (RED) { const finish = (payload) => { resolve(payload) } - userNode.send([ - { - _wot: { finish }, - }, - null, - ]) + let msg = { + _wot: { finish }, + } + userNode.send([msg, null]) }) }) if (!props.content.readOnly) { @@ -90,13 +98,10 @@ module.exports = function (RED) { resolve() } } - userNode.send([ - null, - { - _wot: { finish }, - [props.outputAttr]: v, - }, - ]) + let msg = {} + setOutput("msg", props.outputAttr, msg, node.context(), v) + msg["_wot"] = { finish } + userNode.send([null, msg]) }) }) } @@ -109,16 +114,22 @@ module.exports = function (RED) { const finish = (payload) => { resolve(payload) } - userNode.send({ - _wot: { finish }, - [props.outputArgs]: args, - }) + let msg = {} + setOutput("msg", props.outputArgs, msg, node.context(), args) + msg["_wot"] = { finish } + userNode.send(msg) }) }) } async function createWoTScriptAndExpose( - thingProps: { title: string; description: string }, + thingProps: { + title: string + description: string + id?: string + securityDefinitions?: any + security?: string[] + }, servientWrapper: ServientWrapper, userNodes: any[] ) { @@ -156,7 +167,6 @@ module.exports = function (RED) { } async function launchServient() { - node.bindingType = node.credentials.bindingType if (config.bindingConfigConstValue && config.bindingConfigType) { node.bindingConfig = RED.util.evaluateNodeProperty( config.bindingConfigConstValue, @@ -168,25 +178,60 @@ module.exports = function (RED) { // create thing const bindingType = config.bindingType const bindingConfig = node.bindingConfig - console.debug("[debug] createServient ", node.id, bindingType, bindingConfig) - const servientWrapper = servientManager.createServientWrapper(node.id, bindingType, bindingConfig) try { await waitForFinishPrepareRelatedNodes(userNodes, config._users) - await servientWrapper.startServient() - // make thing title list - const thingNamesObj = {} + // make thing title list and security definitions + const securityDefinitions = [] + const thingTitles = [] for (const userNode of userNodes) { - thingNamesObj[userNode.getThingProps().title] = true + if (userNode.type === "wot-server-td") { + continue + } + let thingNode = userNode.getThingNode() + if (!thingNode) { + continue + } + let title = thingNode.getProps()?.title + if (title && !thingTitles.includes(title)) { + thingTitles.push(title) + // make security definitions for server + let secDef = getSecurityDefinition(thingNode.getSecurityScheme()) + if (secDef.scheme !== "nosec") { + securityDefinitions.push(secDef) + } + } } - const thingNames = Object.keys(thingNamesObj) + // merge security params to bindingConfig + bindingConfig["security"] = securityDefinitions + console.debug("[debug] createServient ", node.id, bindingType, bindingConfig) + const servientWrapper = servientManager.createServientWrapper(node.id, bindingType, bindingConfig) + await servientWrapper.startServient() // Generate and Expose a Thing for each Thing title - for (const thingName of thingNames) { - const targetNodes = userNodes.filter((n) => n.getThingProps().title === thingName) - const thingProps = targetNodes[0]?.getThingProps() || {} - await createWoTScriptAndExpose(thingProps, servientWrapper, targetNodes) + for (const thingTitle of thingTitles) { + const targetNodes = userNodes.filter( + (n) => n.type !== "wot-server-td" && n.getThingNode().getProps().title === thingTitle + ) + if (targetNodes.length > 0) { + const thingNode = targetNodes[0].getThingNode() + const thingProps = thingNode.getProps() || {} + // add security definition to thingProps + const secScheme = thingNode.getSecurityScheme() + if (secScheme !== "nosec") { + thingProps["securityDefinitions"] = { + sc: getSecurityDefinition(secScheme), + } + thingProps["security"] = ["sc"] + } + await createWoTScriptAndExpose(thingProps, servientWrapper, targetNodes) + servientWrapper.addCredentials(thingProps.title, thingNode.getCredentials()) + } } node.running = true userNodes.forEach((n) => { + if (n.type === "wot-server-td" && n.getOutputTDAfterServerStartFlag() === true) { + // send trigger to wot-server-td node for getting TD + n.receive({}) + } n.setServientStatus(node.running) }) } catch (err) { @@ -237,9 +282,28 @@ module.exports = function (RED) { }) } - RED.nodes.registerType("wot-server-config", WoTServerConfig, { - credentials: { - bindingConfig: { type: "object" }, - }, - }) + RED.nodes.registerType("wot-server-config", WoTServerConfig) + + const setOutput = (type, valueName, msg, context, value) => { + if (type === "msg") { + const names = valueName.split(".") + let target = msg + for (let i = 0; i < names.length - 1; i++) { + let n = names[i] + if (target[n] && target[n] instanceof Object) { + target = target[n] + } else { + target[n] = {} + target = target[n] + } + } + target[names[names.length - 1]] = value + } else if (type === "node") { + context.set(valueName, value) + } else if (type === "flow") { + context.flow.set(valueName, value) + } else if (type === "global") { + context.global.set(valueName, value) + } + } } diff --git a/node-red-node-wot/src/wot-server-end.html b/node-red-node-wot/src/wot-server-end.html index 7ac3c6c..2288c8f 100644 --- a/node-red-node-wot/src/wot-server-end.html +++ b/node-red-node-wot/src/wot-server-end.html @@ -7,14 +7,13 @@ name: { value: "" }, inParams_returnValueType: { value: "msg", + required: true, }, inParams_returnValueConstValue: { value: "payload", + required: true, }, }, - credentials: { - inParams_returnValue: { type: "text" }, - }, inputs: 1, outputs: 0, icon: "arrow.png", @@ -45,66 +44,14 @@ label: this._("editor.inParams.tabLabel"), }) - prepareInOutParamSetting( - "inParams", - { - name: "returnValue", - types: ["msg"], - defaultType: "msg", - defaultValue: "payload", - required: true, - }, - this - ) - }, - oneditsave: function () { - saveInOutParamSetting( - "inParams", - { - name: "returnValue", - types: ["msg", "str", "num", "env"], - defaultType: "str", - defaultValue: "payload", - required: true, - }, - this - ) + $("#node-input-inParams_returnValueConstValue").typedInput({ + defaultType: "msg", + defaultValue: "payload", + types: ["msg"], + typeField: "#node-input-inParams_returnValueType", + }) }, }) - const prepareInOutParamSetting = (inOrOutParams, params, _this) => { - const { name, types, defaultType, defaultValue, required } = params - const varName = `${inOrOutParams}_${name}` - if (!_this[`${varName}Type`]) { - _this[`${varName}Type`] = defaultType - } - if (this[`${varName}Type`] == "str") { - $(`#node-input-${varName}ConstValue`).val("") - } else { - if (_this[`${varName}ConstValue`] == "") { - $(`#node-input-${varName}ConstValue`).val(_this.credentials[varName]) - } else { - _this.credentials[varName] = _this[`${varName}ConstValue`] - $(`#node-input-${varName}`).val(_this.credentials[`${varName}`]) - } - } - $(`#node-input-${varName}Type`).val(_this[`${varName}Type`]) - $(`#node-input-${varName}`).typedInput({ - default: defaultType, - typeField: $(`#node-input-${varName}Type`), - types: types, - }) - $(`#node-input-${varName}`).typedInput("type", _this[`${varName}Type`]) - } - const saveInOutParamSetting = (inOrOutParams, params, _this) => { - const varName = `${inOrOutParams}_${params.name}` - if ($(`#node-input-${varName}Type`).val() != "str") { - _this[`${varName}ConstValue`] = $(`#node-input-${varName}`).val() - $(`#node-input-${varName}ConstValue`).val(_this[`${varName}ConstValue`]) - } else { - $(`#node-input-${varName}ConstValue`).val("") - _this[`${varName}ConstValue`] = "" - } - } })() @@ -122,17 +69,15 @@
    - -
    diff --git a/node-red-node-wot/src/wot-server-end.ts b/node-red-node-wot/src/wot-server-end.ts index 6af98c1..42eb2bf 100644 --- a/node-red-node-wot/src/wot-server-end.ts +++ b/node-red-node-wot/src/wot-server-end.ts @@ -5,7 +5,6 @@ module.exports = function (RED) { node.on("input", async (msg, send, done) => { try { - node.inParams_returnValue = node.credentials.inParams_returnValue if (config.inParams_returnValueConstValue && config.inParams_returnValueType) { node.inParams_returnValue = RED.util.evaluateNodeProperty( config.inParams_returnValueConstValue, @@ -30,32 +29,5 @@ module.exports = function (RED) { done() }) } - RED.nodes.registerType("wot-server-end", WoTServerEnd, { - credentials: { - inParams_propertyName: { type: "text" }, - }, - }) - - const setOutput = (type, valueName, msg, context, value) => { - if (type === "msg") { - const names = valueName.split(".") - let target = msg - for (let i = 0; i < names.length - 1; i++) { - let n = names[i] - if (target[n] && target[n] instanceof Object) { - target = target[n] - } else { - target[n] = {} - target = target[n] - } - } - target[names[names.length - 1]] = value - } else if (type === "node") { - context.set(valueName, value) - } else if (type === "flow") { - context.flow.set(valueName, value) - } else if (type === "global") { - context.global.set(valueName, value) - } - } + RED.nodes.registerType("wot-server-end", WoTServerEnd) } diff --git a/node-red-node-wot/src/wot-server-event.html b/node-red-node-wot/src/wot-server-event.html index ffef77a..7087e3e 100644 --- a/node-red-node-wot/src/wot-server-event.html +++ b/node-red-node-wot/src/wot-server-event.html @@ -10,9 +10,11 @@ eventDataType: { value: "string", required: true }, inParams_eventValueType: { value: "msg", + required: true, }, inParams_eventValueConstValue: { value: "payload", + required: true, }, woTServerConfig: { type: "wot-server-config", @@ -25,9 +27,6 @@ required: true, }, }, - credentials: { - inParams_eventValue: { type: "text" }, - }, inputs: 1, outputs: 0, icon: "arrow.png", @@ -57,66 +56,14 @@ label: this._("editor.inParams.tabLabel"), }) - prepareInOutParamSetting( - "inParams", - { - name: "eventValue", - types: ["msg"], - defaultType: "msg", - defaultValue: "payload", - required: true, - }, - this - ) - }, - oneditsave: function () { - saveInOutParamSetting( - "inParams", - { - name: "eventValue", - types: ["msg"], - defaultType: "msg", - defaultValue: "payload", - required: true, - }, - this - ) + $("#node-input-inParams_eventValueConstValue").typedInput({ + defaultType: "msg", + defaultValue: "payload", + types: ["msg"], + typeField: "#node-input-inParams_eventValueType", + }) }, }) - const prepareInOutParamSetting = (inOrOutParams, params, _this) => { - const { name, types, defaultType, defaultValue, required } = params - const varName = `${inOrOutParams}_${name}` - if (!_this[`${varName}Type`]) { - _this[`${varName}Type`] = defaultType - } - if (this[`${varName}Type`] == "str") { - $(`#node-input-${varName}ConstValue`).val("") - } else { - if (_this[`${varName}ConstValue`] == "") { - $(`#node-input-${varName}ConstValue`).val(_this.credentials[varName]) - } else { - _this.credentials[varName] = _this[`${varName}ConstValue`] - $(`#node-input-${varName}`).val(_this.credentials[`${varName}`]) - } - } - $(`#node-input-${varName}Type`).val(_this[`${varName}Type`]) - $(`#node-input-${varName}`).typedInput({ - default: defaultType, - typeField: $(`#node-input-${varName}Type`), - types: types, - }) - $(`#node-input-${varName}`).typedInput("type", _this[`${varName}Type`]) - } - const saveInOutParamSetting = (inOrOutParams, params, _this) => { - const varName = `${inOrOutParams}_${params.name}` - if ($(`#node-input-${varName}Type`).val() != "str") { - _this[`${varName}ConstValue`] = $(`#node-input-${varName}`).val() - $(`#node-input-${varName}ConstValue`).val(_this[`${varName}ConstValue`]) - } else { - $(`#node-input-${varName}ConstValue`).val("") - _this[`${varName}ConstValue`] = "" - } - } })() @@ -165,10 +112,9 @@
    - - + + -
    diff --git a/node-red-node-wot/src/wot-server-event.ts b/node-red-node-wot/src/wot-server-event.ts index 8fbd57e..38d87b6 100644 --- a/node-red-node-wot/src/wot-server-event.ts +++ b/node-red-node-wot/src/wot-server-event.ts @@ -28,16 +28,14 @@ module.exports = function (RED) { } // for wot-server-config - node.getThingProps = () => { - const woTThingConfig = RED.nodes.getNode(config.woTThingConfig) - return woTThingConfig.getProps() + node.getThingNode = () => { + return RED.nodes.getNode(config.woTThingConfig) } node.on("input", async (msg, send, done) => { try { const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) - node.inParams_eventValue = node.credentials.inParams_eventValue if (config.inParams_eventValueConstValue && config.inParams_eventValueType) { node.inParams_eventValue = RED.util.evaluateNodeProperty( config.inParams_eventValueConstValue, @@ -47,7 +45,7 @@ module.exports = function (RED) { ) } await ServientManager.getInstance() - .getThing(woTServerConfig.id, node.getThingProps().title) + .getThing(woTServerConfig.id, node.getThingNode().getProps().title) .emitEvent(config.eventName, node.inParams_eventValue) console.debug("[debug] emitEvent finished. eventName: ", config.eventName) @@ -67,35 +65,8 @@ module.exports = function (RED) { done() }) - const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) //test + const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) woTServerConfig?.addUserNode(node) } - RED.nodes.registerType("wot-server-event", WoTServerEvent, { - credentials: { - inParams_propertyName: { type: "text" }, - }, - }) - - const setOutput = (type, valueName, msg, context, value) => { - if (type === "msg") { - const names = valueName.split(".") - let target = msg - for (let i = 0; i < names.length - 1; i++) { - let n = names[i] - if (target[n] && target[n] instanceof Object) { - target = target[n] - } else { - target[n] = {} - target = target[n] - } - } - target[names[names.length - 1]] = value - } else if (type === "node") { - context.set(valueName, value) - } else if (type === "flow") { - context.flow.set(valueName, value) - } else if (type === "global") { - context.global.set(valueName, value) - } - } + RED.nodes.registerType("wot-server-event", WoTServerEvent) } diff --git a/node-red-node-wot/src/wot-server-property.html b/node-red-node-wot/src/wot-server-property.html index bdf3617..716d9e0 100644 --- a/node-red-node-wot/src/wot-server-property.html +++ b/node-red-node-wot/src/wot-server-property.html @@ -12,9 +12,11 @@ propertyObservableFlag: { value: true, required: true }, outParams2_writingValueType: { value: "msg", + required: true, }, outParams2_writingValueConstValue: { value: "payload", + required: true, }, woTServerConfig: { type: "wot-server-config", @@ -27,9 +29,6 @@ required: true, }, }, - credentials: { - outParams2_writingValue: { type: "text" }, - }, inputs: 1, outputs: 2, outputLabels: function (index) { @@ -62,66 +61,14 @@ label: this._("editor.outParams2.tabLabel"), }) - prepareInOutParamSetting( - "outParams2", - { - name: "writingValue", - types: ["msg"], - defaultType: "msg", - defaultValue: "payload", - required: true, - }, - this - ) - }, - oneditsave: function () { - saveInOutParamSetting( - "outParams2", - { - name: "writingValue", - types: ["msg"], - defaultType: "msg", - defaultValue: "payload", - required: true, - }, - this - ) + $("#node-input-outParams2_writingValueConstValue").typedInput({ + defaultType: "msg", + defaultValue: "payload", + types: ["msg"], + typeField: "#node-input-outParams2_writingValueType", + }) }, }) - const prepareInOutParamSetting = (inOrOutParams, params, _this) => { - const { name, types, defaultType, defaultValue, required } = params - const varName = `${inOrOutParams}_${name}` - if (!_this[`${varName}Type`]) { - _this[`${varName}Type`] = defaultType - } - if (this[`${varName}Type`] == "str") { - $(`#node-input-${varName}ConstValue`).val("") - } else { - if (_this[`${varName}ConstValue`] == "") { - $(`#node-input-${varName}ConstValue`).val(_this.credentials[varName]) - } else { - _this.credentials[varName] = _this[`${varName}ConstValue`] - $(`#node-input-${varName}`).val(_this.credentials[`${varName}`]) - } - } - $(`#node-input-${varName}Type`).val(_this[`${varName}Type`]) - $(`#node-input-${varName}`).typedInput({ - default: defaultType, - typeField: $(`#node-input-${varName}Type`), - types: types, - }) - $(`#node-input-${varName}`).typedInput("type", _this[`${varName}Type`]) - } - const saveInOutParamSetting = (inOrOutParams, params, _this) => { - const varName = `${inOrOutParams}_${params.name}` - if ($(`#node-input-${varName}Type`).val() != "str") { - _this[`${varName}ConstValue`] = $(`#node-input-${varName}`).val() - $(`#node-input-${varName}ConstValue`).val(_this[`${varName}ConstValue`]) - } else { - $(`#node-input-${varName}ConstValue`).val("") - _this[`${varName}ConstValue`] = "" - } - } })() @@ -179,10 +126,9 @@
    - - + + -
    diff --git a/node-red-node-wot/src/wot-server-property.ts b/node-red-node-wot/src/wot-server-property.ts index f5f1736..c002f96 100644 --- a/node-red-node-wot/src/wot-server-property.ts +++ b/node-red-node-wot/src/wot-server-property.ts @@ -30,9 +30,8 @@ module.exports = function (RED) { } // for wot-server-config - node.getThingProps = () => { - const woTThingConfig = RED.nodes.getNode(config.woTThingConfig) - return woTThingConfig.getProps() + node.getThingNode = () => { + return RED.nodes.getNode(config.woTThingConfig) } node.on("input", async (msg, send, done) => { @@ -40,7 +39,7 @@ module.exports = function (RED) { const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) await ServientManager.getInstance() - .getThing(woTServerConfig.id, node.getThingProps().title) + .getThing(woTServerConfig.id, node.getThingNode().getProps().title) .emitPropertyChange(config.propertyName) console.debug("[debug] emitPropertyChange finished. propertyName: ", config.propertyName) @@ -60,35 +59,8 @@ module.exports = function (RED) { done() }) - const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) //test + const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) woTServerConfig?.addUserNode(node) } - RED.nodes.registerType("wot-server-property", WoTServerProperty, { - credentials: { - inParams_propertyName: { type: "text" }, - }, - }) - - const setOutput = (type, valueName, msg, context, value) => { - if (type === "msg") { - const names = valueName.split(".") - let target = msg - for (let i = 0; i < names.length - 1; i++) { - let n = names[i] - if (target[n] && target[n] instanceof Object) { - target = target[n] - } else { - target[n] = {} - target = target[n] - } - } - target[names[names.length - 1]] = value - } else if (type === "node") { - context.set(valueName, value) - } else if (type === "flow") { - context.flow.set(valueName, value) - } else if (type === "global") { - context.global.set(valueName, value) - } - } + RED.nodes.registerType("wot-server-property", WoTServerProperty) } diff --git a/node-red-node-wot/src/wot-server-td.html b/node-red-node-wot/src/wot-server-td.html new file mode 100644 index 0000000..55310fb --- /dev/null +++ b/node-red-node-wot/src/wot-server-td.html @@ -0,0 +1,104 @@ + + + + + diff --git a/node-red-node-wot/src/wot-server-td.ts b/node-red-node-wot/src/wot-server-td.ts new file mode 100644 index 0000000..959567c --- /dev/null +++ b/node-red-node-wot/src/wot-server-td.ts @@ -0,0 +1,50 @@ +import ServientManager from "./servients/servient-manager" + +module.exports = function (RED) { + function WoTServerTD(config) { + RED.nodes.createNode(this, config) + const node = this + this.status({ fill: "red", shape: "dot", text: "not prepared" }) + + node.setServientStatus = (running: boolean) => { + if (running) { + node.status({ fill: "green", shape: "dot", text: "running" }) + } else { + node.status({ fill: "red", shape: "dot", text: "not prepared" }) + } + } + + node.getOutputTDAfterServerStartFlag = () => { + return config.outputTDAfterServerStartFlag + } + + node.on("input", async (msg, send, done) => { + try { + const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) + const woTThingConfig = RED.nodes.getNode(config.woTThingConfig) + const thing = ServientManager.getInstance().getThing(woTServerConfig.id, woTThingConfig.name) + const tdOutputKey = config.outParams1_tdConstValue + const td = thing.getThingDescription() + msg[tdOutputKey] = td + console.debug("[debug] send td. td: ", td) + send(msg) + done() + } catch (err) { + done(err) + } + }) + + node.on("close", function (removed, done) { + if (removed) { + // This node has been disabled/deleted + } else { + // This node is being restarted + } + done() + }) + + const woTServerConfig = RED.nodes.getNode(config.woTServerConfig) + woTServerConfig?.addUserNode(node) + } + RED.nodes.registerType("wot-server-td", WoTServerTD) +} diff --git a/node-red-node-wot/src/wot-thing-config.html b/node-red-node-wot/src/wot-thing-config.html index bab0083..035591c 100644 --- a/node-red-node-wot/src/wot-thing-config.html +++ b/node-red-node-wot/src/wot-thing-config.html @@ -4,52 +4,32 @@ category: "config", defaults: { name: { value: "", required: true }, - description: { value: "" }, + description: { value: "", required: false }, + thingId: { value: "", required: false }, + basicAuth: { value: false }, + basicAuthUsername: { value: "", required: false }, + basicAuthPassword: { value: "", required: false }, }, - credentials: {}, label: function () { return this.name || this._("editor.configLabel") }, labelStyle: function () { return this.name ? "node_label_italic" : "" }, - oneditprepare: function () {}, - oneditsave: function () {}, - }) - - const createInOutParamSetting = (params, _this) => { - const { name, types, defaultType, defaultValue, required } = params - if (!_this[name + "Type"]) { - _this[name + "Type"] = defaultType - } - if (_this[name + "Type"] == "str") { - $(`#node-config-input-${name}ConstValue`).val("") - } else { - if (_this[name + "ConstValue"] == "") { - $(`#node-config-input-${name}ConstValue`).val(_this.credentials[name]) - } else { - _this.credentials[name] = _this[name + "ConstValue"] - $(`#node-config-input-${name}`).val(_this.credentials[name]) + oneditprepare: function () { + const $basicAuth = $("#node-config-input-basicAuth") + function toggleBasicAuthContainer() { + // Check if event is triggered by user + if ($basicAuth.prop("checked") === true) { + $(".basicAuthContainer").show() + } else { + $(".basicAuthContainer").hide() + } } - } - $(`#node-config-input-${name}Type`).val(_this[name + "Type"]) - $(`#node-config-input-${name}`).typedInput({ - default: defaultType, - typeField: $(`#node-config-input-${name}Type`), - types: types, - }) - $(`#node-config-input-${name}`).typedInput("type", _this[name + "Type"]) - } - const saveInOutParamSetting = (params, _this) => { - const { name } = params - if ($(`#node-config-input-${name}Type`).val() != "str") { - _this[`${name}ConstValue`] = $(`#node-config-input-${name}`).val() - $(`#node-config-input-${name}ConstValue`).val(_this[`${name}ConstValue`]) - } else { - $(`#node-config-input-${name}ConstValue`).val("") - _this[`${name}ConstValue`] = "" - } - } + // Toggle username and password when basic auth is enabled/disabled + $basicAuth.change(toggleBasicAuthContainer) + }, + }) })() @@ -62,4 +42,21 @@ +
    + + +
    +

    +
    + + +
    +
    + + +
    +
    + + +
    diff --git a/node-red-node-wot/src/wot-thing-config.ts b/node-red-node-wot/src/wot-thing-config.ts index f94a699..ce47e80 100644 --- a/node-red-node-wot/src/wot-thing-config.ts +++ b/node-red-node-wot/src/wot-thing-config.ts @@ -3,14 +3,29 @@ module.exports = function (RED) { RED.nodes.createNode(this, config) const node = this node.getProps = () => { - return { + let props = { title: config.name, description: config.description, } + if (config.thingId) { + props["id"] = config.thingId + } + return props + } + node.getCredentials = () => { + return { + username: config.basicAuthUsername, + password: config.basicAuthPassword, + } + } + node.getSecurityScheme = () => { + if (config.basicAuth) { + return "basic" + } else { + return "nosec" + } } } - RED.nodes.registerType("wot-thing-config", WoTThingConfig, { - credentials: {}, - }) + RED.nodes.registerType("wot-thing-config", WoTThingConfig) } diff --git a/node-red-node-wot/src/wot-thing.js b/node-red-node-wot/src/wot-thing.js index 3e759cb..7124a4e 100644 --- a/node-red-node-wot/src/wot-thing.js +++ b/node-red-node-wot/src/wot-thing.js @@ -13,16 +13,27 @@ module.exports = function (RED) { function consumedThingNode(config) { RED.nodes.createNode(this, config) const node = this + let consumedThing + let tdListeners = [] + let servient - this.tdLink = config.tdLink - this.td = JSON.parse(config.td) + this.addUpdateTDListener = (listener) => { + tdListeners.push(listener) + if (consumedThing) { + listener(consumedThing) + } + } - this.consumedThing = new Promise((resolve, reject) => { - let servient = new Servient() + this.createConsumedThing = async (td) => { + node.td = td //for debug + if (servient) { + servient.shutdown() + } + servient = new Servient() if (config.basicAuth) { servient.addCredentials({ - [this.td.id]: { username: config.username.trim(), password: config.password }, + [td.id]: { username: config.username.trim(), password: config.password }, }) } @@ -47,16 +58,21 @@ module.exports = function (RED) { servient.addClientFactory(new ModbusClientFactory()) } - servient - .start() - .then((thingFactory) => { - let consumedThing = thingFactory.consume(this.td) - resolve(consumedThing) - }) - .catch((err) => { - node.error(`[error] failed to start servient. err: ${err.toString()}`) - reject(err) - }) + const thingFactory = await servient.start() + consumedThing = await thingFactory.consume(td) + tdListeners.forEach((listener) => { + listener(consumedThing) + }) + } + + const td = JSON.parse(config.td) + this.createConsumedThing(td).catch((err) => { + node.error(`[error] failed to start servient. err: ${err.toString()}`) + }) + + this.on("close", function (removed, done) { + tdListeners = [] + done() }) } RED.nodes.registerType("consumed-thing", consumedThingNode) diff --git a/node-red-node-wot/src/wot-update-td.html b/node-red-node-wot/src/wot-update-td.html new file mode 100644 index 0000000..16c670b --- /dev/null +++ b/node-red-node-wot/src/wot-update-td.html @@ -0,0 +1,58 @@ + + + + + diff --git a/node-red-node-wot/src/wot-update-td.js b/node-red-node-wot/src/wot-update-td.js new file mode 100644 index 0000000..e978173 --- /dev/null +++ b/node-red-node-wot/src/wot-update-td.js @@ -0,0 +1,51 @@ +"use strict" + +module.exports = function (RED) { + function UpdateTDNode(config) { + RED.nodes.createNode(this, config) + let node = this + + this.status({}) + + if (!config.thing) { + this.status({ fill: "red", shape: "dot", text: "Error: Thing undefined" }) + return + } else if (!config.tdSource) { + this.status({ + fill: "red", + shape: "dot", + text: "Error: Choose a td source", + }) + return + } + + const thingNode = RED.nodes.getNode(config.thing) + + this.on("input", async function (msg, send, done) { + let td + if (config.tdSource && config.tdSourceType) { + try { + td = await RED.util.evaluateNodeProperty(config.tdSource, config.tdSourceType, node, msg) + } catch (err) { + return done("cannot evaluate td source") + } + } + try { + await thingNode.createConsumedThing(td) + done() + } catch (err) { + done(err) + } + }) + + this.on("close", function (removed, done) { + if (removed) { + // This node has been deleted + } else { + // This node is being restarted + } + done() + }) + } + RED.nodes.registerType("update-td", UpdateTDNode) +} diff --git a/node-red-node-wot/test/server-td-test.ts b/node-red-node-wot/test/server-td-test.ts new file mode 100644 index 0000000..5062d56 --- /dev/null +++ b/node-red-node-wot/test/server-td-test.ts @@ -0,0 +1,125 @@ +/** + * test for server-td + */ +import "mocha" +import * as chai from "chai" +import chaiAsPromised from "chai-as-promised" +import helper from "node-red-node-test-helper" +import { startFlow, endFlow, getNodeAfterStartFlow } from "./util" + +helper.init(require.resolve("node-red")) + +chai.use(chaiAsPromised) +const assert = chai.assert + +/* + Flow Summary + [Server-side] + 1a. wot-server-event:id.serverevent01 (id.serverconfig01, id.thingconfig01') + 1b. wot-server-td:id.servertd01 (id.serverconfig01, id.thingconfig01') + 2b. helper:id.gettdhelper01 + */ +const targetFlow = [ + // Server-side + { + id: "id.serverevent01", + type: "wot-server-event", + name: "", + eventName: "testEvent", + eventDescription: "test event", + eventDataType: "string", + inParams_eventValueType: "msg", + inParams_eventValueConstValue: "payload", + woTServerConfig: "id.serverconfig01", + woTThingConfig: "id.thingconfig01", + wires: [], + }, + { + id: "id.servertd01", + type: "wot-server-td", + name: "", + outParams1_tdType: "msg", + outParams1_tdConstValue: "payload", + woTServerConfig: "id.serverconfig01", + woTThingConfig: "id.thingconfig01", + outputTDAfterServerStartFlag: true, + wires: [["id.gettdhelper01"]], + }, + { + id: "id.serverconfig01", + type: "wot-server-config", + name: "httpserver", + bindingType: "http", + bindingConfigType: "json", + bindingConfigConstValue: '{"port":8383}', + }, + { + id: "id.thingconfig01", + type: "wot-thing-config", + name: "thing01", + description: "thing01 for test", + }, + { id: "id.gettdhelper01", type: "helper" }, +] + +let eventContent = "test-content" + +describe("Tests for Server TD", function () { + this.timeout(15 * 1000) + before(async function () { + await startFlow(targetFlow, helper, 0) + }) + + after(async function () { + await endFlow("id.serverconfig01", helper) + }) + + beforeEach(function (done) { + done() + }) + + afterEach(function (done) { + done() + }) + + it("get td after server start", function (done) { + getNodeAfterStartFlow("id.gettdhelper01", helper).then((helperNode) => { + helperNode.removeAllListeners("input") + helperNode.on("input", function (msg) { + try { + //@ts-ignore + assert.equal(msg.payload?.title, "thing01") + done() + } catch (err) { + done(err) + } + }) + }) + }) + + it("get td by input", function (done) { + // wait for servient start + new Promise((resolve, reject) => { + setTimeout(resolve, 2000) + }).then(() => { + const helperNode = helper.getNode("id.gettdhelper01") + let sentFlg = false + helperNode.removeAllListeners("input") + helperNode.on("input", function (msg) { + try { + // check after send trigger or not + if (sentFlg) { + //@ts-ignore + assert.equal(msg.payload?.title, "thing01") + done() + } + } catch (err) { + done(err) + } + }) + const serverTDNode = helper.getNode("id.servertd01") + serverTDNode.receive({}) + sentFlg = true + }) + }) +}) diff --git a/node-red-node-wot/test/update-td-test.ts b/node-red-node-wot/test/update-td-test.ts new file mode 100644 index 0000000..210eebf --- /dev/null +++ b/node-red-node-wot/test/update-td-test.ts @@ -0,0 +1,226 @@ +/** + * test for update-td + */ +import "mocha" +import * as chai from "chai" +import chaiAsPromised from "chai-as-promised" +import helper from "node-red-node-test-helper" +import { startFlow, endFlow } from "./util" + +helper.init(require.resolve("node-red")) + +chai.use(chaiAsPromised) +const assert = chai.assert + +/* + Flow Summary + [Server-side] + 1a. wot-server-event:id.serverevent01 (id.serverconfig01, id.thingconfig01') + 1b. wot-server-event:id.serverevent02 (id.serverconfig02, id.thingconfig01') + [Client-side] + 1a. subscribe-event:id.subscribeevent01 (id.consumedthing01) + 2a. helper:id.subscribeeventhelper01 + 1b. update-td:id.updatetd01 (id.consumedthing01) + */ +const targetFlow = [ + // Server-side + { + id: "id.serverevent01", + type: "wot-server-event", + name: "", + eventName: "testEvent", + eventDescription: "test event", + eventDataType: "string", + inParams_eventValueType: "msg", + inParams_eventValueConstValue: "payload", + woTServerConfig: "id.serverconfig01", + woTThingConfig: "id.thingconfig01", + wires: [], + }, + { + id: "id.serverconfig01", + type: "wot-server-config", + name: "httpserver", + bindingType: "http", + bindingConfigType: "json", + bindingConfigConstValue: '{"port":8181}', + }, + { + id: "id.thingconfig01", + type: "wot-thing-config", + name: "thing01", + description: "thing01 for test", + }, + { + id: "id.serverevent02", + type: "wot-server-event", + name: "", + eventName: "testEvent", + eventDescription: "test event", + eventDataType: "string", + inParams_eventValueType: "msg", + inParams_eventValueConstValue: "payload", + woTServerConfig: "id.serverconfig02", + woTThingConfig: "id.thingconfig01", + wires: [], + }, + { + id: "id.serverconfig02", + type: "wot-server-config", + name: "httpserver", + bindingType: "http", + bindingConfigType: "json", + bindingConfigConstValue: '{"port":8282}', + }, + // Client-side + { + id: "id.subscribeevent01", + type: "subscribe-event", + name: "", + topic: "", + thing: "id.consumedthing01", + event: "testEvent", + uriVariables: "", + wires: [["id.subscribeeventhelper01"]], + }, + { id: "id.subscribeeventhelper01", type: "helper" }, + { + id: "id.consumedthing01", + type: "consumed-thing", + tdLink: "", + td: JSON.stringify({ + "@context": [ + "https://www.w3.org/2019/wot/td/v1", + "https://www.w3.org/2022/wot/td/v1.1", + { "@language": "en" }, + ], + "@type": "Thing", + title: "thing01", + securityDefinitions: { nosec: { scheme: "nosec" } }, + security: ["nosec"], + events: { + testEvent: { + description: "", + data: { type: "string" }, + forms: [ + { + href: "http://localhost:8181/thing01/events/testEvent", + contentType: "application/json", + subprotocol: "longpoll", + op: ["subscribeevent", "unsubscribeevent"], + }, + { + href: "http://localhost:8181/thing01/events/testEvent", + contentType: "application/cbor", + subprotocol: "longpoll", + op: ["subscribeevent", "unsubscribeevent"], + }, + ], + }, + }, + id: "urn:uuid:cf950521-8eaf-4e1c-9277-758930e47246", + description: "", + }), + http: true, + ws: false, + coap: false, + mqtt: false, + opcua: false, + modbus: false, + basicAuth: false, + username: "", + password: "", + }, + { + id: "id.updatetd01", + type: "update-td", + name: "", + thing: "id.consumedthing01", + tdSourceType: "msg", + tdSource: "payload", + wires: [], + }, +] + +describe("Tests for Update TD", function () { + this.timeout(15 * 1000) + before(async function () { + await startFlow(targetFlow, helper) + }) + + after(async function () { + await endFlow("id.serverconfig01", helper) + }) + + beforeEach(function (done) { + done() + }) + + afterEach(function (done) { + done() + }) + + it("update td", function (done) { + const clientHelperNode = helper.getNode("id.subscribeeventhelper01") + clientHelperNode.removeAllListeners("input") + let expectedEvent + clientHelperNode.on("input", function (msg) { + try { + assert.equal(msg.payload, expectedEvent) + if (expectedEvent === "event from server02") { + done() + } + } catch (err) { + done(err) + } + }) + const serverEventNode01 = helper.getNode("id.serverevent01") + const serverEventNode02 = helper.getNode("id.serverevent02") + expectedEvent = "event from server01" + serverEventNode01.receive({ payload: "event from server01" }) + serverEventNode02.receive({ payload: "event from server02" }) + new Promise((resolve) => setTimeout(resolve, 500)).then(() => { + const updateTDNode = helper.getNode("id.updatetd01") + updateTDNode.receive({ + payload: { + "@context": [ + "https://www.w3.org/2019/wot/td/v1", + "https://www.w3.org/2022/wot/td/v1.1", + { "@language": "en" }, + ], + "@type": "Thing", + title: "thing01", + securityDefinitions: { nosec: { scheme: "nosec" } }, + security: ["nosec"], + events: { + testEvent: { + description: "", + data: { type: "string" }, + forms: [ + { + href: "http://localhost:8282/thing01/events/testEvent", + contentType: "application/json", + subprotocol: "longpoll", + op: ["subscribeevent", "unsubscribeevent"], + }, + { + href: "http://localhost:8282/thing01/events/testEvent", + contentType: "application/cbor", + subprotocol: "longpoll", + op: ["subscribeevent", "unsubscribeevent"], + }, + ], + }, + }, + id: "urn:uuid:cf950521-8eaf-4e1c-9277-758930e47246", + description: "", + }, + }) + new Promise((resolve) => setTimeout(resolve, 1500)).then(() => { + expectedEvent = "event from server02" + serverEventNode01.receive({ payload: "event from server01" }) + serverEventNode02.receive({ payload: "event from server02" }) + }) + }) + }) +}) diff --git a/node-red-node-wot/test/util.ts b/node-red-node-wot/test/util.ts index 09a40f6..0170b6c 100644 --- a/node-red-node-wot/test/util.ts +++ b/node-red-node-wot/test/util.ts @@ -6,10 +6,12 @@ const serverPropertyNode = require("../src/wot-server-property.ts") const serverActionNode = require("../src/wot-server-action.ts") const serverEventNode = require("../src/wot-server-event.ts") const serverEndNode = require("../src/wot-server-end.ts") +const serverTDNode = require("../src/wot-server-td.ts") const thingConfigNode = require("../src/wot-thing.js") const propertyNode = require("../src/wot-property.js") const actionNode = require("../src/wot-action.js") const eventNode = require("../src/wot-event.js") +const updateTDNode = require("../src/wot-update-td.js") const USE_NODES = [ serverConfigNode, @@ -18,28 +20,30 @@ const USE_NODES = [ serverActionNode, serverEventNode, serverEndNode, + serverTDNode, thingConfigNode, propertyNode, actionNode, eventNode, + updateTDNode, ] -const launchFlow = async (flow: any[], helper) => { +const launchFlow = async (flow: any[], helper, waitForServientStart = 2000) => { return new Promise((resolve, reject) => { helper.load(USE_NODES, flow, function () { // Wait for the servient to start. setTimeout(function () { resolve() - }, 2000) + }, waitForServientStart) }) }) } -export const startFlow = async (targetFlow, helper) => { +export const startFlow = async (targetFlow, helper, waitForServientStart = 2000) => { return new Promise((resolve, reject) => { helper.startServer(async function () { try { - await launchFlow(targetFlow, helper) + await launchFlow(targetFlow, helper, waitForServientStart) resolve() } catch (err) { reject(err) @@ -58,3 +62,17 @@ export const endFlow = async (id: string, helper) => { } }) } + +export const getNodeAfterStartFlow = async (id: string, helper, wait = 50, maxCount = 50) => { + for (let i = 0; i < maxCount; i++) { + let node = helper.getNode(id) + if (node) { + return node + } else { + await new Promise((resolve, reject) => { + setTimeout(resolve, wait) + }) + } + } + throw new Error("timeout for getting node") +}