From c6cf41ca87e361c7c6ddecb9149848cc823b5194 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 22 Mar 2024 13:30:13 -0700 Subject: [PATCH] Dev iteration: UI improvements, bug fixes, cleanup --- README.md | 10 +- .../hiveoview/src/session/ClientSession.go | 26 +++-- bindings/hiveoview/src/static/hiveot.png | Bin 0 -> 6048 bytes .../src/views/directory/directory.go | 8 +- .../src/views/directory/directory.gohtml | 6 +- .../hiveoview/src/views/thing/editConfig.go | 8 -- .../src/views/thing/thingActions.gohtml | 11 +- .../src/views/thing/thingAttr.gohtml | 7 +- .../src/views/thing/thingConfig.gohtml | 8 +- .../src/views/thing/thingDetails.gohtml | 1 - .../src/views/thing/thingEvents.gohtml | 2 +- .../src/views/thing/thingEventsRow.gohtml | 25 ----- bindings/hiveoview/test/Hiveov_test.go | 2 +- bindings/isy99x/service/IsyGatewayThing.go | 10 +- bindings/owserver/service/OWServerBinding.go | 6 +- bindings/zwavejs/README.md | 22 +--- bindings/zwavejs/package.json | 3 +- bindings/zwavejs/src/ParseValues.ts | 19 ++-- bindings/zwavejs/src/ZWaveJSBinding.ts | 5 + bindings/zwavejs/src/parseController.ts | 26 ++--- bindings/zwavejs/src/parseNode.ts | 65 ++++++------ bindings/zwavejs/yarn.lock | 96 +++++++++++++++++- cmd/hubcli/authcli/AuthCommands.go | 4 +- cmd/hubcli/directorycli/DirectoryCommands.go | 2 +- cmd/hubcli/historycli/HistoryCommands.go | 11 +- cmd/hubcli/pubsubcli/SubCommands.go | 2 +- core/auth/authstore/AuthnFileStore.go | 17 ---- core/digitwin/README.md | 59 +++++++++++ core/history/service/LatestPropertiesStore.go | 5 +- docs/challenges.md | 12 ++- lib/buckets/BucketStore_test.go | 4 +- lib/hubclient/HubClient.go | 11 +- .../transports/mqtttransport/MqttTransport.go | 6 +- lib/things/TD.go | 40 ++++---- lib/things/TD_test.go | 5 +- lib/things/ThingValue.go | 17 ++-- lib/things/ThingValueMap.go | 34 +++---- libjs/src/things/ThingTD.ts | 17 +++- libjs/src/things/dataSchema.ts | 2 + 39 files changed, 388 insertions(+), 226 deletions(-) create mode 100644 bindings/hiveoview/src/static/hiveot.png delete mode 100644 bindings/hiveoview/src/views/thing/thingEventsRow.gohtml create mode 100644 core/digitwin/README.md diff --git a/README.md b/README.md index 6b098269..5358d4a8 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ HiveOT stands for the "Hive of Things". It provides a framework and services to collect and share IoT data with users. -The Hub for the -*Hive-of-Things* provides a secure core and plugins to view and operate IoT devices. The Hub securely mediates between IoT device 'Things', services, and users using a hub-and-spokes architecture. Users interact with Things via the Hub without connecting directly to the IoT devices or services. The Hub is based on the [W3C WoT TD 1.1 specification](https://www.w3.org/TR/wot-thing-description11/) and uses a NATS or MQTT message bus for secure communication. +The Hub for the *Hive-of-Things* provides a secure core and plugins to view and operate IoT devices. The Hub securely mediates between IoT device 'Things', services, and users using a hub-and-spokes architecture. Users interact with Things via the Hub without connecting directly to the IoT devices or services. The Hub is based on the [W3C WoT TD 1.1 specification](https://www.w3.org/TR/wot-thing-description11/) and uses a NATS or MQTT message bus for secure communication. ## Project Status @@ -22,9 +21,16 @@ Completed core services: Bindings * 1-wire protocol binding using owserver-v2 gateway (bindings/owserver) +* insteon binding using isy99x gateway (bindings/isy99x) * zwave protocol binding using zwavejs (bindings/zwavejs) * web client using html/htmx and go templates (bindings/hiveoview) - in progress +Integrations + +It is a bit early to look at integrations, but some interesting candidates are: +* plc4go (https://plc4x.apache.org/users/getting-started/plc4go.html) +* home assistant (https://www.home-assistant.io/) + ## Audience This project is aimed at software developers and system implementors that are working on secure IoT solutions. HiveOT users subscribe to the security mandate that IoT devices should be isolated from the internet and end-users should not have direct access to IoT devices. Instead, all access operates via the Hub. diff --git a/bindings/hiveoview/src/session/ClientSession.go b/bindings/hiveoview/src/session/ClientSession.go index 0d33239f..0b0334c1 100644 --- a/bindings/hiveoview/src/session/ClientSession.go +++ b/bindings/hiveoview/src/session/ClientSession.go @@ -1,6 +1,7 @@ package session import ( + "encoding/json" "fmt" "github.com/hiveot/hub/core/state/stateclient" "github.com/hiveot/hub/lib/hubclient" @@ -146,13 +147,20 @@ func (cs *ClientSession) onEvent(msg *things.ThingValue) { thingAddr := fmt.Sprintf("%s/%s", msg.AgentID, msg.ThingID) _ = cs.SendSSE(thingAddr, "") } else if msg.Name == transports.EventNameProps { - // Publish sse event indicating one or more properties of a Thing has changed. - // The UI that displays this event can use this as a trigger to reload the - // fragment that displays this event: - // hx-trigger="sse:{{.Thing.AgentID}}/{{.Thing.ThingID}}/$properties" - // where $properties is transports.EventNameProps (can the ui use JS to get this constant?) - thingAddr := fmt.Sprintf("%s/%s/%s", msg.AgentID, msg.ThingID, msg.Name) - _ = cs.SendSSE(thingAddr, string(msg.Data)) + // Publish an sse event for each of the properties + // The UI that displays this event can use this as a trigger to load the + // property value: + // hx-trigger="sse:{{.Thing.AgentID}}/{{.Thing.ThingID}}/{{k}}" + props := make(map[string]string) + err := json.Unmarshal(msg.Data, &props) + if err == nil { + for k, v := range props { + thingAddr := fmt.Sprintf("%s/%s/%s", msg.AgentID, msg.ThingID, k) + _ = cs.SendSSE(thingAddr, v) + thingAddr = fmt.Sprintf("%s/%s/%s/updated", msg.AgentID, msg.ThingID, k) + _ = cs.SendSSE(thingAddr, msg.GetUpdated()) + } + } } else { // Publish sse event indicating the event affordance or value has changed. // The UI that displays this event can use this as a trigger to reload the @@ -162,8 +170,10 @@ func (cs *ClientSession) onEvent(msg *things.ThingValue) { thingAddr := fmt.Sprintf("%s/%s/%s", msg.AgentID, msg.ThingID, msg.Name) _ = cs.SendSSE(thingAddr, string(msg.Data)) // TODO: improve on this crude way to update the 'updated' field + // Can the value contain an object with a value and updated field instead? + // htmx sse-swap does allow cherry picking the content unfortunately. thingAddr = fmt.Sprintf("%s/%s/%s/updated", msg.AgentID, msg.ThingID, msg.Name) - _ = cs.SendSSE(thingAddr, msg.Updated()) + _ = cs.SendSSE(thingAddr, msg.GetUpdated()) } } diff --git a/bindings/hiveoview/src/static/hiveot.png b/bindings/hiveoview/src/static/hiveot.png new file mode 100644 index 0000000000000000000000000000000000000000..77a6829dc5c8e271b041ecac1da736feb8ba7c61 GIT binary patch literal 6048 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 a#s34s>ta}}tKDS)0000 Publisher: - {{.Publisher}} + {{.AgentID}} - {{len .Things}} things @@ -55,7 +55,7 @@ - {{$agentID := .Publisher}} + {{$agentID := .AgentID}} {{range .Things}}
  • @@ -77,7 +77,7 @@
    {{.GetAtTypeVocab}}
    {{len .Events}} outputs
    {{len .Actions}} actions
    -
    {{.GetAge}} ago
    +
    {{.GetUpdated}}
  • {{end}} diff --git a/bindings/hiveoview/src/views/thing/editConfig.go b/bindings/hiveoview/src/views/thing/editConfig.go index de3bdfe5..9a4d7045 100644 --- a/bindings/hiveoview/src/views/thing/editConfig.go +++ b/bindings/hiveoview/src/views/thing/editConfig.go @@ -88,14 +88,6 @@ func PostThingConfig(w http.ResponseWriter, r *http.Request) { } _ = mySession.SendSSE("notify", "success: Configuration '"+propKey+"' updated") - // - //// read the updated config value and update the UI fragments - //cl := historyclient.NewReadHistoryClient(hc) - //tvs, err := cl.GetLatest(agentID, thingID, []string{propKey}) - //propAddr := fmt.Sprintf("%s/%s/%s", agentID, thingID, propKey) - //propVal := fmt.Sprintf("%.100s", tvs[propKey].Data) - //slog.Info("Updated value of prop", "propAddr", propAddr, "propVal", propVal) - //mySession.SendSSE(propAddr, propVal) w.WriteHeader(http.StatusOK) diff --git a/bindings/hiveoview/src/views/thing/thingActions.gohtml b/bindings/hiveoview/src/views/thing/thingActions.gohtml index 23b91eda..b769d295 100644 --- a/bindings/hiveoview/src/views/thing/thingActions.gohtml +++ b/bindings/hiveoview/src/views/thing/thingActions.gohtml @@ -23,11 +23,16 @@ {{$v.Title}}
    {{$v.ActionType}}
    -
    {{$.Values.ToString $k}}
    +
    + {{$.Values.ToString $k}} +
    {{$v.Description}}
    - {{$.Values.Updated $k}}
    + sse-swap="{{$.AgentID}}/{{$.ThingID}}/{{$k}}/updated" hx-swap="innerHTML" + title="Updated: {{$.Values.GetUpdated $k}} by {{($.Values.SenderID $k)}}"> + {{$.Values.GetUpdated $k}} + {{end}} {{if not .TD.Actions}} diff --git a/bindings/hiveoview/src/views/thing/thingAttr.gohtml b/bindings/hiveoview/src/views/thing/thingAttr.gohtml index a8a3041b..dc36504d 100644 --- a/bindings/hiveoview/src/views/thing/thingAttr.gohtml +++ b/bindings/hiveoview/src/views/thing/thingAttr.gohtml @@ -21,15 +21,16 @@ {{$v.Title}}
    - + {{$.Values.ToString $k}} {{$v.UnitSymbol}}
    {{$v.Description}}
    - {{$.Values.Updated $k}}
    + sse-swap="{{$.AgentID}}/{{$.ThingID}}/{{$k}}/updated" hx-swap="innerHTML" + title="Updated: {{$.Values.GetUpdated $k}} by {{($.Values.SenderID $k)}}"> + {{$.Values.GetUpdated $k}} {{end}} diff --git a/bindings/hiveoview/src/views/thing/thingConfig.gohtml b/bindings/hiveoview/src/views/thing/thingConfig.gohtml index 17131788..1f3c1d71 100644 --- a/bindings/hiveoview/src/views/thing/thingConfig.gohtml +++ b/bindings/hiveoview/src/views/thing/thingConfig.gohtml @@ -33,8 +33,7 @@ > {{/*replace show value on sse event*/}} - + {{$.Values.ToString $k}} {{$v.UnitSymbol}} @@ -43,9 +42,10 @@
    {{$v.Default}}
    {{$v.Description}}
    - {{$.Values.Updated $k}} + {{$.Values.GetUpdated $k}}
    diff --git a/bindings/hiveoview/src/views/thing/thingDetails.gohtml b/bindings/hiveoview/src/views/thing/thingDetails.gohtml index 8fa37ecc..22de55a9 100644 --- a/bindings/hiveoview/src/views/thing/thingDetails.gohtml +++ b/bindings/hiveoview/src/views/thing/thingDetails.gohtml @@ -22,7 +22,6 @@ diff --git a/bindings/hiveoview/src/views/thing/thingEvents.gohtml b/bindings/hiveoview/src/views/thing/thingEvents.gohtml index a24ccaaa..439330a1 100644 --- a/bindings/hiveoview/src/views/thing/thingEvents.gohtml +++ b/bindings/hiveoview/src/views/thing/thingEvents.gohtml @@ -34,7 +34,7 @@ {{/*TODO: improve on this rather crude way to refresh the derived 'updated' field when value changes?*/}}
    {{$.Values.Updated $k}}
    + >{{$.Values.GetUpdated $k}} {{end}} {{if not .TD.Events}} diff --git a/bindings/hiveoview/src/views/thing/thingEventsRow.gohtml b/bindings/hiveoview/src/views/thing/thingEventsRow.gohtml deleted file mode 100644 index dcf471e6..00000000 --- a/bindings/hiveoview/src/views/thing/thingEventsRow.gohtml +++ /dev/null @@ -1,25 +0,0 @@ - - - - {{- /*auto reload row on event changes*/ -}} -
  • -
    {{/*icon*/}}
    -
    - {{.EventID}} - {{.EventAffordance.Title}} -
    -
    - - {{$.Values.ToString .EventID}} - - {{.EventAffordance.Data.UnitSymbol}} -
    -
    {{.EventAffordance.Description}}
    -
    - {{$.Values.Updated .EventID}}
    -
  • diff --git a/bindings/hiveoview/test/Hiveov_test.go b/bindings/hiveoview/test/Hiveov_test.go index da2dbc6d..f69cca2f 100644 --- a/bindings/hiveoview/test/Hiveov_test.go +++ b/bindings/hiveoview/test/Hiveov_test.go @@ -42,7 +42,7 @@ func TestMain(m *testing.M) { func TestStartStop(t *testing.T) { t.Log("--- TestStartStop ---") - svc := service.NewHiveovService(8080, true, nil, "") + svc := service.NewHiveovService(9999, true, nil, "") hc1, err := testServer.AddConnectClient( serviceID, authapi.ClientTypeService, authapi.ClientRoleService) require.NoError(t, err) diff --git a/bindings/isy99x/service/IsyGatewayThing.go b/bindings/isy99x/service/IsyGatewayThing.go index 99bab7c1..e604d6cc 100644 --- a/bindings/isy99x/service/IsyGatewayThing.go +++ b/bindings/isy99x/service/IsyGatewayThing.go @@ -31,8 +31,8 @@ type IsyGatewayThing struct { // REST/SOAP/WS connection to the ISY hub ic *IsyAPI - // The gateway thing ID - id string + // The gateway thingID + thingID string // map of ISY product ID's prodMap map[string]InsteonProduct @@ -238,7 +238,7 @@ func (igw *IsyGatewayThing) GetIsyThingByNodeID(nodeID string) IIsyThing { // GetID return the gateway thingID func (igw *IsyGatewayThing) GetID() string { - return igw.id + return igw.thingID } // GetIsyThings returns a list of ISY devices for publishing TD or values as updated in @@ -269,7 +269,7 @@ func (igw *IsyGatewayThing) GetTD() *things.TD { return nil } - td := things.NewTD(igw.id, igw.Configuration.DeviceSpecs.Model, vocab.ThingNetGateway) + td := things.NewTD(igw.thingID, igw.Configuration.DeviceSpecs.Model, vocab.ThingNetGateway) td.Description = igw.Configuration.DeviceSpecs.Make + "-" + igw.Configuration.DeviceSpecs.Model //--- device read-only attributes @@ -330,7 +330,7 @@ func (igw *IsyGatewayThing) GetTD() *things.TD { // This removes prior use nodes for a fresh start. func (igw *IsyGatewayThing) Init(ic *IsyAPI) { igw.ic = ic - igw.id = ic.GetID() + igw.thingID = ic.GetID() igw.things = make(map[string]IIsyThing) igw.propValues = things.NewPropertyValues() diff --git a/bindings/owserver/service/OWServerBinding.go b/bindings/owserver/service/OWServerBinding.go index 9f9a61df..c321c2fd 100644 --- a/bindings/owserver/service/OWServerBinding.go +++ b/bindings/owserver/service/OWServerBinding.go @@ -22,6 +22,8 @@ const bindingMake = "make" // OWServerBinding is the hub protocol binding plugin for capturing 1-wire OWServer V2 Data type OWServerBinding struct { + thingID string + // Configuration of this protocol binding config *config.OWServerConfig @@ -48,8 +50,7 @@ type OWServerBinding struct { // CreateBindingTD generates a TD document for this binding func (svc *OWServerBinding) CreateBindingTD() *things.TD { - thingID := svc.hc.ClientID() - td := things.NewTD(thingID, "OWServer binding", vocab.ThingServiceAdapter) + td := things.NewTD(svc.thingID, "OWServer binding", vocab.ThingServiceAdapter) td.Description = "Driver for the OWServer V2 Gateway 1-wire interface" prop := td.AddProperty(bindingMake, vocab.PropDeviceMake, @@ -94,6 +95,7 @@ func (svc *OWServerBinding) Start(hc *hubclient.HubClient) (err error) { logging.SetLogging(svc.config.LogLevel, "") } svc.hc = hc + svc.thingID = hc.ClientID() // Create the adapter for the OWServer 1-wire gateway svc.edsAPI = eds.NewEdsAPI( svc.config.OWServerURL, svc.config.OWServerLogin, svc.config.OWServerPassword) diff --git a/bindings/zwavejs/README.md b/bindings/zwavejs/README.md index 9dd09248..02ec02d0 100644 --- a/bindings/zwavejs/README.md +++ b/bindings/zwavejs/README.md @@ -17,25 +17,6 @@ TODO: 1. Include DataSchema in controller configurations for properties that aren't in the zwave-js vids. 1. Dimming duration is currently not supported -## ~~Build with pkg using tsc~~ - -!!! WARNING: this no longer works as it isn't compatible with ESM. - -> Error [ERR_REQUIRE_ESM]: require() of ES Module node_modules/tslog/dist/cjs/index.js - -old: -This needs: - -* yarn -* nodejs v20+ -* typescript compiler v4.9+ (tsc, included in snap's node) -* make tools -* pkg, tsc, tsc-alias, etc - -Run 'yarn dist' and watch the magic unfold (...hopefully, this is a javascript build environment after all). - -If all goes according to plan, a single binary is produced in the dist folder: dist/zwavejs - ## Building with esbuild This first step is just for testing the build process using esbuild. If this already fails then no use using pkg or the 'postject' node20+ injector. Note: one reason to take this step is to allow packages with es modules (axios) to work. pkg seems to not build correctly with code generated with tsc && tsc-alias. @@ -54,7 +35,7 @@ Then run from the project root with: * note1: --clientID is the client this runs under, eg 'testsvc' during testing. Default will be the binding name zwavejs. -## build a single executable using pkg and esbuild from zwave-js-ui +## build a single executable using pkg and esbuild.js from zwave-js-ui NOTE: This uses the esbuild.js script from zwave-js-ui, which does some filename mangling to get the externals to work. Not sure how it works but I also really don't care. @@ -73,6 +54,7 @@ Installation needs the executable, an authentication token, and the CA certifica The binding executable should be copied to the hiveot plugins folder, for example: ~/bin/hiveot/bin/plugins. The authentication token is generated by the launcher, or can be generated manually using the hubcli. The token file has the same name as the executable with the .token extension. The CA certificate is generated by the hubcli on startup and placed in certs/caCert.pem. + ## Run Before running the binding make sure the hub gateway is running. 'hubcli ls' lists the running processes. diff --git a/bindings/zwavejs/package.json b/bindings/zwavejs/package.json index 14773759..a80ac30f 100644 --- a/bindings/zwavejs/package.json +++ b/bindings/zwavejs/package.json @@ -11,7 +11,7 @@ "pkg": "./build.sh", "esbuildold": "esbuild src/main.ts --bundle --platform=node --target=node20 --preserve-symlinks --external:\"prebuilds/*\" --external:./node_modules/zwave-js/package.json --external:./node_modules/@zwave-js/config/package.json --outfile=build/main.js", "devold": "tsc && tsc-alias && ZWAVEJS_EXTERNAL_CONFIG=dist/cache node --preserve-symlinks build/src/main.js --clientID testsvc --home ~/bin/hiveot", - "devtsxold": "ZWAVEJS_EXTERNAL_CONFIG=dist/cache tsx --preserve-symlinks src/main.ts --clientID testsvc --home ~/bin/hiveot", + "debugtsx": "ZWAVEJS_EXTERNAL_CONFIG=dist/cache tsx --preserve-symlinks src/main.ts --clientID testsvc --home ~/bin/hiveot", "distinstall": "cp dist/zwavejs ~/bin/hiveot/plugins", "test": "tsc && tsc-alias && node --preserve-symlinks build/src/tests/hubconnect_test.js", "testtsx": "tsx --preserve-symlinks src/tests/hubconnect_test.ts", @@ -52,6 +52,7 @@ "nkeys.js": "^1.0.5", "process": "^0.11.10", "serialport": "^12.0.0", + "ts-node": "^10.9.2", "tslog": "^4.9.2", "ws": "^8.14.2", "yaml": "^2.3.4", diff --git a/bindings/zwavejs/src/ParseValues.ts b/bindings/zwavejs/src/ParseValues.ts index 3bb73f66..f9aad356 100644 --- a/bindings/zwavejs/src/ParseValues.ts +++ b/bindings/zwavejs/src/ParseValues.ts @@ -2,6 +2,7 @@ import {getEnumMemberName, NodeStatus, ZWaveNode, ZWavePlusNodeType, ZWavePlusRo import {InterviewStage, SecurityClass} from '@zwave-js/core'; import * as vocab from "@hivelib/api/ht-vocab"; import {getPropID} from "./getPropID"; +import {PropDeviceDescription, PropDeviceSoftwareVersion} from "@hivelib/api/ht-vocab"; // Value map for node values @@ -44,7 +45,7 @@ export class ParseValues { //--- Node read-only attributes that are common to many nodes this.setIf("associationCount", node.deviceConfig?.associations?.size); this.setIf("canSleep", node.canSleep); - this.setIf("description", node.deviceConfig?.description); + this.setIf(PropDeviceDescription, node.deviceConfig?.description); if (node.deviceClass) { this.setIf("deviceClassBasic", node.deviceClass.basic.label); @@ -54,7 +55,7 @@ export class ParseValues { } this.setIf("endpointCount", node.getEndpointCount().toString()); // this.setIf("dc.firmwareVersion", node.deviceConfig?.firmwareVersion); - this.setIf("firmwareVersion", node.firmwareVersion?.toString()); + this.setIf(vocab.PropDeviceFirmwareVersion, node.firmwareVersion?.toString()); if (node.getHighestSecurityClass()) { let classID = node.getHighestSecurityClass() as number @@ -69,7 +70,7 @@ export class ParseValues { this.setIf("isRouting", node.isRouting); this.setIf("isControllerNode", node.isControllerNode) this.setIf("keepAwake", node.keepAwake); - this.setIf("label", node.deviceConfig?.label) + this.setIf(vocab.PropDeviceTitle, node.deviceConfig?.label) this.setIf("manufacturerId", node.manufacturerId); this.setIf(vocab.PropDeviceMake, node.deviceConfig?.manufacturer); @@ -79,17 +80,17 @@ export class ParseValues { this.setIf("nodeTypName", getEnumMemberName(ZWavePlusNodeType, node.nodeType)); } this.setIf("paramCount", node.deviceConfig?.paramInformation?.size); - this.setIf("productID", node.productId); + this.setIf("productId", node.productId); this.setIf("productType", node.productType); this.setIf("protocolVersion", node.protocolVersion); - this.setIf("sdkVersion", node.sdkVersion); - this.setIf("status", node.status); - this.setIf("statusName", getEnumMemberName(NodeStatus, node.status)); + this.setIf(vocab.PropDeviceSoftwareVersion, node.sdkVersion); + this.setIf(vocab.PropDeviceStatus, getEnumMemberName(NodeStatus, node.status)); this.setIf("supportedDataRates", node.supportedDataRates); + this.setIf("userIcon", node.userIcon); if (node.zwavePlusNodeType) { - this.setIf("zwPlusType", node.zwavePlusNodeType); - this.setIf("zwPlusTypeName", getEnumMemberName(ZWavePlusNodeType, node.zwavePlusNodeType)); + this.setIf("zwavePlusNodeType", node.zwavePlusNodeType); + this.setIf("zwavePlusNodeTypeName", getEnumMemberName(ZWavePlusNodeType, node.zwavePlusNodeType)); } if (node.zwavePlusRoleType) { this.setIf("zwavePlusRoleType", node.zwavePlusRoleType); diff --git a/bindings/zwavejs/src/ZWaveJSBinding.ts b/bindings/zwavejs/src/ZWaveJSBinding.ts index 08ee0251..3caa42cf 100644 --- a/bindings/zwavejs/src/ZWaveJSBinding.ts +++ b/bindings/zwavejs/src/ZWaveJSBinding.ts @@ -208,7 +208,12 @@ export class ZwaveJSBinding { valueMap.values[propID] = newValue // let serValue = JSON.stringify(newValue) + log.info("handleValueUpdate: publish event for deviceID="+deviceID+", propID="+propID+"" ) this.hc.pubEvent(deviceID, propID, serValue) + } else { + // for debugging + log.info("handleValueUpdate: unchanged value deviceID="+deviceID+", propID="+propID+" (ignored)" ) + } } diff --git a/bindings/zwavejs/src/parseController.ts b/bindings/zwavejs/src/parseController.ts index 477a42d6..298a57bb 100644 --- a/bindings/zwavejs/src/parseController.ts +++ b/bindings/zwavejs/src/parseController.ts @@ -17,35 +17,35 @@ export function parseController(td: ThingTD, ctl: ZWaveController) { } // controller events. Note these must match the controller event handler - td.AddEvent("healNetworkState", "healNetworkState", "Heal Network Progress", undefined, + td.AddEvent("healNetworkState", "", "Heal Network Progress", undefined, new DataSchema({title: "Heal State", type: WoTDataTypeString})) - td.AddEvent("inclusionState", "inclusionState", "Node Inclusion Progress", undefined, + td.AddEvent("inclusionState", "", "Node Inclusion Progress", undefined, new DataSchema({title: "Inclusion State", type: WoTDataTypeString})) - td.AddEvent("nodeAdded", "nodeAdded", "Node Added", undefined, + td.AddEvent("nodeAdded", "", "Node Added", undefined, new DataSchema({title: "ThingID", type: WoTDataTypeString})) - td.AddEvent("nodeRemoved", "nodeRemoved", "Node Removed", undefined, + td.AddEvent("nodeRemoved", "", "Node Removed", undefined, new DataSchema({title: "ThingID", type: WoTDataTypeString})) // controller network actions - td.AddAction("beginInclusion", "beginInclusion", "Start add node process", + td.AddAction("beginInclusion", "", "Start add node process", "Start the inclusion process for new nodes. Prefer S2 security if supported") - td.AddAction("stopInclusion", "stopInclusion", "Stop add node process") - td.AddAction("beginExclusion", "beginExclusion", "Start node removal process") - td.AddAction("stopExclusion", "stopExclusion", "Stop node removal process") - td.AddAction("beginHealingNetwork", "beginHealingNetwork", "Start heal network process", + td.AddAction("stopInclusion", "", "Stop add node process") + td.AddAction("beginExclusion", "", "Start node removal process") + td.AddAction("stopExclusion", "", "Stop node removal process") + td.AddAction("beginHealingNetwork", "", "Start heal network process", "Start healing the network routes. This can take a long time and slow things down.") - td.AddAction("stopHealingNetwork", "stopHealingNetwork", "Stop the ongoing healing process") + td.AddAction("stopHealingNetwork", "", "Stop the ongoing healing process") // controller node actions - td.AddAction("getNodeNeighbors", "getNodeNeighbors", "Update Neighbors", + td.AddAction("getNodeNeighbors", "", "Update Neighbors", "Request update to a node's neighbor list", new DataSchema({title: "ThingID", type: WoTDataTypeString}) ) - td.AddAction("healNode", "healNode", "Heal the node", + td.AddAction("healNode", "", "Heal the node", "Heal the node and update its neighbor list", new DataSchema({title: "ThingID", type: WoTDataTypeString}) ) - td.AddAction("removeFailedNode", "removeFailedNode", "Remove failed node", + td.AddAction("removeFailedNode", "", "Remove failed node", "Remove a failed node from the network", new DataSchema({title: "ThingID", type: WoTDataTypeString}) ) diff --git a/bindings/zwavejs/src/parseNode.ts b/bindings/zwavejs/src/parseNode.ts index 82293454..660752c1 100644 --- a/bindings/zwavejs/src/parseNode.ts +++ b/bindings/zwavejs/src/parseNode.ts @@ -25,6 +25,7 @@ import { WoTDataTypeNumber, WoTDataTypeString } from "@hivelib/api/wot-vocab"; +import {PropDeviceSoftwareVersion} from "@hivelib/api/ht-vocab"; // Add the ZWave value data to the TD as an action @@ -116,16 +117,20 @@ export function parseNode(zwapi: ZWAPI, node: ZWaveNode, vidLogFD: number | unde //--- Step 2: Add read-only attributes that are common to many nodes // since none of these have standard property names, use the ZWave name instead. // these names must match those used in parseNodeValues() + td.AddProperty("associationCount", "", "Association Count", + WoTDataTypeNumber); td.AddPropertyIf(node.canSleep, "canSleep", "", "Device sleeps to conserve battery", WoTDataTypeBool); + td.AddProperty("",vocab.PropDeviceDescription, + "Description", WoTDataTypeString); td.AddProperty("endpointCount", "", "Number of endpoints", WoTDataTypeNumber); - td.AddPropertyIf(node.firmwareVersion, "firmwareVersion", vocab.PropDeviceFirmwareVersion, + td.AddPropertyIf(node.firmwareVersion, "", vocab.PropDeviceFirmwareVersion, "Device firmware version", WoTDataTypeString); td.AddPropertyIf(node.getHighestSecurityClass(), "highestSecurityClass", "", "", WoTDataTypeString); - td.AddPropertyIf(node.interviewAttempts, "interviewAttempts", "Nr interview attempts", - "", WoTDataTypeNumber); + td.AddPropertyIf(node.interviewAttempts, "interviewAttempts", "", + "Nr interview attempts", WoTDataTypeNumber); if (node.interviewStage) { td.AddProperty("interviewStage", "", "Device Interview Stage", WoTDataTypeString).SetAsEnum(InterviewStage) @@ -140,10 +145,10 @@ export function parseNode(zwapi: ZWAPI, node: ZWaveNode, vidLogFD: number | unde "Device is a ZWave controller", WoTDataTypeBool); td.AddPropertyIf(node.keepAwake, "keepAwake", "", "Device stays awake a bit longer before sending it to sleep", WoTDataTypeBool); - td.AddPropertyIf(node.label, "label", "", "", WoTDataTypeString); + td.AddPropertyIf(node.label, "", vocab.PropDeviceTitle, "", WoTDataTypeString); td.AddPropertyIf(node.manufacturerId, "manufacturerId", "", "Manufacturer ID", WoTDataTypeString); - td.AddPropertyIf(node.deviceConfig?.manufacturer, vocab.PropDeviceMake, vocab.PropDeviceMake, + td.AddPropertyIf(node.deviceConfig?.manufacturer, "", vocab.PropDeviceMake, "Manufacturer", WoTDataTypeString); td.AddPropertyIf(node.maxDataRate, "maxDataRate", "", "Device maximum communication data rate", WoTDataTypeNumber); @@ -152,25 +157,26 @@ export function parseNode(zwapi: ZWAPI, node: ZWaveNode, vidLogFD: number | unde } td.AddPropertyIf(node.productId, "productId", "", "", WoTDataTypeNumber); - td.AddPropertyIf(node.productType, "productType", vocab.PropDeviceDescription, - "", WoTDataTypeNumber); - td.AddPropertyIf(node.protocolVersion, "protocolVersion", "", "ZWave protocol version", WoTDataTypeString); - td.AddPropertyIf(node.sdkVersion, "sdkVersion", "", - "", WoTDataTypeString); + + td.AddPropertyIf(node.sdkVersion, "", PropDeviceSoftwareVersion, + "SDK version", WoTDataTypeString); if (node.status) { - td.AddProperty(vocab.PropDeviceStatus, vocab.PropDeviceStatus, + td.AddProperty("", vocab.PropDeviceStatus, "Node status", WoTDataTypeNumber).SetAsEnum(NodeStatus) } td.AddPropertyIf(node.supportedDataRates, "supportedDataRates", "", "ZWave Data Speed", WoTDataTypeString); + td.AddPropertyIf(node.userIcon, "userIcon", "", "", WoTDataTypeString); // always show whether this is ZWave+ - let prop = td.AddProperty("zwavePlusNodeType", "", - "Type of ZWave+", WoTDataTypeNumber) + td.AddProperty("zwavePlusNodeType", "", + "ZWave+ Node Type", WoTDataTypeNumber) + let prop = td.AddProperty("zwavePlusNodeTypeName", "", + "ZWave+ Node Type Name", WoTDataTypeString) if (node.zwavePlusNodeType != undefined) { prop.SetAsEnum(ZWavePlusNodeType) } else { @@ -179,39 +185,36 @@ export function parseNode(zwapi: ZWAPI, node: ZWaveNode, vidLogFD: number | unde if (node.zwavePlusRoleType) { td.AddProperty("zwavePlusRoleType", "", - "Type of Z-Wave+ role of this device", WoTDataTypeNumber) + "ZWave+ Role Type", WoTDataTypeNumber) + td.AddProperty("zwavePlusRoleTypeName", "", + "ZWave+ Role Type Name", WoTDataTypeString) .SetAsEnum(ZWavePlusRoleType) } td.AddPropertyIf(node.zwavePlusVersion, "zwavePlusVersion", "", "Z-Wave+ Version", WoTDataTypeNumber); // writable configuration properties that are not VIDs - prop = td.AddProperty("name", vocab.PropDeviceTitle, + prop = td.AddProperty("", vocab.PropDeviceTitle, "Device Name", WoTDataTypeString) prop.readOnly = false - prop = td.AddProperty("checkLifelineHealth", "", - "Check connection health", WoTDataTypeBool) - prop.description = "Initiates tests to check the health of the connection between the controller and this node and returns the results. " + + let action = td.AddAction("checkLifelineHealth", "", + "Check connection health", WoTDataTypeNone) + action.description = "Initiates tests to check the health of the connection between the controller and this node and returns the results. " + "This should NOT be done while there is a lot of traffic on the network because it will negatively impact the test results" - prop.readOnly = false - prop.writeOnly = true - prop = td.AddProperty("ping", "", "Ping the device", WoTDataTypeNone) - prop.readOnly = false - prop.writeOnly = true - prop = td.AddProperty("refreshInfo", "", "Refresh Device Info", WoTDataTypeNone) - prop.description = "Resets (almost) all information about this node and forces a fresh interview. " + + action = td.AddAction("ping", "", "Ping", WoTDataTypeNone) + action.description = "Ping the device" + + action = td.AddAction("refreshInfo", "", "Refresh Device Info", WoTDataTypeNone) + action.description = "Resets (almost) all information about this node and forces a fresh interview. " + "Ignored when interview is in progress. After this action, the node will no longer be ready. This can take a long time." - prop.readOnly = false - prop.writeOnly = true - prop = td.AddProperty("refreshValues", "", "Refresh Device Values", WoTDataTypeBool) - prop.description = "Refresh all non-static sensor and actuator values. " + + + action = td.AddAction("refreshValues", "", "Refresh Device Values", WoTDataTypeNone) + action.description = "Refresh all non-static sensor and actuator values. " + "Use sparingly. This can take a long time and generate a lot of traffic." - prop.readOnly = false - prop.writeOnly = true //--- Step 4: add properties, events, and actions from the ValueIDs diff --git a/bindings/zwavejs/yarn.lock b/bindings/zwavejs/yarn.lock index 52e31b3f..d67f4a1e 100644 --- a/bindings/zwavejs/yarn.lock +++ b/bindings/zwavejs/yarn.lock @@ -86,6 +86,13 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@dabh/diagnostics@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" @@ -334,7 +341,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== @@ -349,6 +356,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.23" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz#afc96847f3f07841477f303eed687707a5aacd80" @@ -491,6 +506,26 @@ dependencies: defer-to-connect "^2.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/http-cache-semantics@^4.0.2": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" @@ -678,6 +713,16 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +acorn-walk@^8.1.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^8.4.1: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -717,6 +762,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" @@ -965,6 +1015,11 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1018,6 +1073,11 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1579,6 +1639,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + md5@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" @@ -2297,6 +2362,25 @@ triple-beam@*, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsc-alias@^1.8.8: version "1.8.8" resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.8.tgz#48696af442b7656dd7905e37ae0bc332d80be3fe" @@ -2371,6 +2455,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -2530,6 +2619,11 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + zwave-js@^12.3.1: version "12.4.4" resolved "https://registry.yarnpkg.com/zwave-js/-/zwave-js-12.4.4.tgz#c02f9e0fc9a8276ba51428ba0207145460c93d65" diff --git a/cmd/hubcli/authcli/AuthCommands.go b/cmd/hubcli/authcli/AuthCommands.go index 1c9c1013..3adf50dc 100644 --- a/cmd/hubcli/authcli/AuthCommands.go +++ b/cmd/hubcli/authcli/AuthCommands.go @@ -246,7 +246,7 @@ func HandleListClients(hc *hubclient.HubClient) (err error) { profileList, err := authn.GetProfiles() fmt.Println("Users") - fmt.Println("Login ID Display Name Role Updated") + fmt.Println("Login ID Display Name Role GetUpdated") fmt.Println("-------- ------------ ---- -------") for _, profile := range profileList { if profile.ClientType == authapi.ClientTypeUser { @@ -260,7 +260,7 @@ func HandleListClients(hc *hubclient.HubClient) (err error) { } fmt.Println() fmt.Println("Devices/Services") - fmt.Println("SenderID Type Updated") + fmt.Println("SenderID Type GetUpdated") fmt.Println("-------- ---- -------") for _, profile := range profileList { if profile.ClientType != authapi.ClientTypeUser { diff --git a/cmd/hubcli/directorycli/DirectoryCommands.go b/cmd/hubcli/directorycli/DirectoryCommands.go index 4439e824..50044adb 100644 --- a/cmd/hubcli/directorycli/DirectoryCommands.go +++ b/cmd/hubcli/directorycli/DirectoryCommands.go @@ -56,7 +56,7 @@ func HandleListDirectory(hc *hubclient.HubClient) (err error) { if err != nil { return err } - fmt.Printf("Agent ID / Thing ID @type Title #props #events #actions Updated \n") + fmt.Printf("Agent ID / Thing ID @type Title #props #events #actions GetUpdated \n") fmt.Printf("---------------------------------- ---------------------------------- ----------------------------------- ------ ------- -------- -----------------------------\n") i := 0 tv, valid, err := cursor.First() diff --git a/cmd/hubcli/historycli/HistoryCommands.go b/cmd/hubcli/historycli/HistoryCommands.go index c1e8d662..e33d8eb4 100644 --- a/cmd/hubcli/historycli/HistoryCommands.go +++ b/cmd/hubcli/historycli/HistoryCommands.go @@ -1,8 +1,10 @@ package historycli import ( + "encoding/json" "fmt" "github.com/hiveot/hub/core/history/historyclient" + "github.com/hiveot/hub/lib/hubclient/transports" "github.com/hiveot/hub/lib/utils" "github.com/urfave/cli/v2" @@ -121,13 +123,20 @@ func HandleListEvents(hc *hubclient.HubClient, agentID, thingID string, name str count := 0 for tv, valid, err := cursor.First(); err == nil && valid && count < limit; tv, valid, err = cursor.Next() { count++ + value := string(tv.Data) + // show number of properties + if tv.Name == transports.EventNameProps { + props := make(map[string]string) + _ = json.Unmarshal(tv.Data, &props) + value = fmt.Sprintf("(%d properties)", len(props)) + } fmt.Printf("%-14s %-18s %-30s %-20.20s %-30.30s\n", tv.AgentID, tv.ThingID, utils.FormatMSE(tv.CreatedMSec, false), tv.Name, - tv.Data, + value, ) } cursor.Release() diff --git a/cmd/hubcli/pubsubcli/SubCommands.go b/cmd/hubcli/pubsubcli/SubCommands.go index 838523ab..243e2a4f 100644 --- a/cmd/hubcli/pubsubcli/SubCommands.go +++ b/cmd/hubcli/pubsubcli/SubCommands.go @@ -74,7 +74,7 @@ func HandleSubTD(hc *hubclient.HubClient) error { msg.AgentID, msg.ThingID, td.Title, td.AtType, timeStr) } }) - fmt.Printf("Agent ID Thing ID Title @type Updated \n") + fmt.Printf("Agent ID Thing ID Title @type GetUpdated \n") fmt.Printf("------------------- ------------------------ ----------------------------- ------------------- --------------------\n") time.Sleep(time.Hour * 24) diff --git a/core/auth/authstore/AuthnFileStore.go b/core/auth/authstore/AuthnFileStore.go index d6eb2ee1..b1655f2a 100644 --- a/core/auth/authstore/AuthnFileStore.go +++ b/core/auth/authstore/AuthnFileStore.go @@ -249,23 +249,6 @@ func (authnStore *AuthnFileStore) SetPassword(loginID string, password string) ( return authnStore.SetPasswordHash(loginID, hash) } -// SetRole updates the client's role -//func (authnStore *AuthnFileStore) SetRole(clientID string, role string) (err error) { -// authnStore.mutex.Lock() -// defer authnStore.mutex.Unlock() -// -// entry, found := authnStore.entries[clientID] -// if !found { -// return fmt.Errorf("Client '%s' not found", clientID) -// } -// entry.Role = role -// entry.Updated = time.Now().Format(vocab.ISO8601Format) -// authnStore.entries[clientID] = entry -// -// err = authnStore.save() -// return err -//} - // SetPasswordHash adds/updates the password hash for the given login ID // Intended for use by administrators to add a new user or clients to update their password func (authnStore *AuthnFileStore) SetPasswordHash(loginID string, hash string) (err error) { diff --git a/core/digitwin/README.md b/core/digitwin/README.md new file mode 100644 index 00000000..edf035f7 --- /dev/null +++ b/core/digitwin/README.md @@ -0,0 +1,59 @@ +# DigiTwin Service + +## Status + +The DigiTwin service is in the planning phase. When completed, it replaces the directory service and the 'latest' capability (API) of the history service. + +## Introduction + +The IoT Digital Twin collection is the hearth of the HiveOT Hub. The DigiTwin service keeps a digital representation of all IoT devices that are registered with it. + +Each digital twin consists of a device's TD document and a state object holding the properties, latest event values and the queued actions that have not yet been delivered. + +The objectives of this service are: +1. Keep a digital twin instance for each registered IoT device. +2. Persist the digital twin collection between restarts. +3. Allow IoT devices to register their digital twin with their values. +4. Allow clients to query the twins by Thing ID, Agent, and device type. +5. Notify clients of changes to property values and events. +6. Support various communication protocols through protocol bindings. +7. Hold device configuration if needed. + +IoT devices update the TD document and state of their digital counterpart by communicating on their separate communication channel with the DigiTwin service. They retrieve action requests that are provided by the DigiTwin service. + +Users of IoT devices communicate with the DigiTwin service to receive information and send action requests. They do not connect or communicate directly with the IoT devices themselves. Instead they operate on the digital twin of the devices. + +This approach provides near complete isolation between IoT devices and their users, which greatly enhances security. At no time is there a direct connection between user and IoT device. + +Action requests from users are sent to the DigiTwin until they can be handed off to the IoT device or expire. This allows intermittently connected devices to receive action requests that were sent while they were offline. + +## IoT Device Connections + +IoT devices can connect directly to the Hub DigiTwin service through one of the protocols, and register their TD document, upload their property values, send events and receive action requests. +For devices that cannot persist their own configuration the service can also provide the last known configuration values when a device starts up. + +## Protocols + +The HiveOT Digital Twin service supports multiple communication protocols. + +The primary protocol is a connection based message bus, either MQTT or NATS. This supports pushing messages from the Digital Twin to the client once connection is established. + +The second protocol is Websocket based, which utilizes a permanent connection and also supports pushing messages from the Digital Twin to the client. + +The third protocol is REST over HTTPS. This connectionless interface supports pushing data to the digital twin and querying for data. It does not however, support data push from the Hub to the device or user client. + +The fourth protocol is server-side-events (SSE) which complements REST over HTTPS with server push. + +## Persistance + +This service persists the TD documents it receives from IoT devices and services, aka 'Things'. On shutdown and startup it restores the TD documents and creates a digital twin for each of the 'active' Things. Active Things are Things that have send data to the service in recent time. Inactive Things can be unloaded to free up memory. + +A digital twin also includes the state of the IoT device and constantly synchronizes this state with the actual IoT device. Synchronization takes place using event and action messages via one of the supported protocols. + +Changes to this state is periodically persisted, and stored on shutdown. The storage engine uses the 'bucket-service' API which is implemented using one of the embedded databases: pebble, kvbtree, or bolts. On startup the state of active things is loaded from the database. + +When querying the state of a Thing, the state of the digital twin is returned. If the Thing isn't yet active it will be activated and its state is loaded into memory. + +## Authentication and Authorization + +The authentication mechanism depends on the protocol used. \ No newline at end of file diff --git a/core/history/service/LatestPropertiesStore.go b/core/history/service/LatestPropertiesStore.go index 7a5129ff..4fd793e8 100644 --- a/core/history/service/LatestPropertiesStore.go +++ b/core/history/service/LatestPropertiesStore.go @@ -106,15 +106,16 @@ func (srv *LatestPropertiesStore) HandleAddValue(addtv *things.ThingValue) { if addtv.Name == transports.EventNameProps { // the value holds a map of property name:value pairs, add each one individually // in order to retain the sender and created timestamp. - props := make(map[string]string) + props := make(map[string]any) err := json.Unmarshal(addtv.Data, &props) if err != nil { return // data is not used } // turn each value into a ThingValue object for propName, propValue := range props { + propValueString := fmt.Sprint(propValue) tv := things.NewThingValue(transports.MessageTypeEvent, - addtv.AgentID, addtv.ThingID, propName, []byte(propValue), addtv.SenderID) + addtv.AgentID, addtv.ThingID, propName, []byte(propValueString), addtv.SenderID) tv.CreatedMSec = addtv.CreatedMSec // in case events arrive out of order, only update if the addtv is newer diff --git a/docs/challenges.md b/docs/challenges.md index a96fcaab..bd74e01d 100644 --- a/docs/challenges.md +++ b/docs/challenges.md @@ -98,4 +98,14 @@ While the last iteration using capnp RPC was fast, secure, reliable, and easy to ## Lack of Audio And Video -Support for audio and video is desirable but is not supported by IoT protocols. \ No newline at end of file +Support for audio and video is desirable but is not supported by IoT protocols. + + +## WoT TD +* no titles for enum values, workaround use oneOf +* no best practices with examples; per use-case? +* no IoT vocabulary, everyone has to roll their own +* affordances need inheritance which isn't always support (eg golang) +* multiple types for same property (single item vs list) +* forms/protocols unclear (2023) +* thingID uniqueness; using agentID in address \ No newline at end of file diff --git a/lib/buckets/BucketStore_test.go b/lib/buckets/BucketStore_test.go index 13adf1c5..2cce68c8 100644 --- a/lib/buckets/BucketStore_test.go +++ b/lib/buckets/BucketStore_test.go @@ -65,11 +65,11 @@ func createTD(id string) *things.TD { td := &things.TD{ ID: id, Title: fmt.Sprintf("test TD %s", id), - DeviceType: vocab.ThingSensor, + AtType: vocab.ThingSensor, Properties: make(map[string]*things.PropertyAffordance), Events: make(map[string]*things.EventAffordance), } - td.Properties[vocab.PropDeviceName] = &things.PropertyAffordance{ + td.Properties[vocab.PropDeviceTitle] = &things.PropertyAffordance{ DataSchema: things.DataSchema{ Title: "Sensor title", Description: "This is a smart sensor", diff --git a/lib/hubclient/HubClient.go b/lib/hubclient/HubClient.go index e1c6da4d..4c9b98cb 100644 --- a/lib/hubclient/HubClient.go +++ b/lib/hubclient/HubClient.go @@ -36,7 +36,10 @@ const PubKeyFileExt = ".pub" type HubClient struct { //serverURL string //caCert *x509.Certificate - clientID string + + // login ID + clientID string + // transport transports.IHubTransport // key set after connecting with token kp keys.IHiveKey @@ -65,8 +68,8 @@ type HubClient struct { // Where "+" is the wildcard for MQTT or "*" for Nats // // msgType is the message type: "event", "action", "config" or "rpc". -// agentID is the device or service being addressed. Use "" for wildcard -// thingID is the ID of the things managed by the publisher. Use "" for wildcard +// agentID is the thingID of the device or service being addressed. Use "" for wildcard +// thingID is the ID of the thing including the urn: prefix. Use "" for wildcard // name is the event or action name. Use "" for wildcard. // clientID is the login ID of the sender. Use "" for subscribe. func (hc *HubClient) MakeAddress(msgType, agentID, thingID, name string, clientID string) string { @@ -198,7 +201,7 @@ func (hc *HubClient) GetStatus() transports.HubTransportStatus { // // The key-pair is named {clientID}.key, the public key {clientID}.pub // -// clientID is the clientID to use, or "" to use the connecting ID +// clientID is the login ID to use, or "" to use the connecting ID // keysDir is the location where the keys are stored. // // This returns the serialized private and pub keypair, or an error. diff --git a/lib/hubclient/transports/mqtttransport/MqttTransport.go b/lib/hubclient/transports/mqtttransport/MqttTransport.go index 384ed507..f36e0884 100644 --- a/lib/hubclient/transports/mqtttransport/MqttTransport.go +++ b/lib/hubclient/transports/mqtttransport/MqttTransport.go @@ -20,8 +20,8 @@ import ( "time" ) -// InboxPrefix is the INBOX subscription topic used by the client and RPC calls -// _INBOX/{clientID} +// InboxTopicFormat is the INBOX subscription topic used by the client and RPC calls +// _INBOX/{clientID} (clientID is the unique session clientID, not per-se the loginID) const InboxTopicFormat = transports.MessageTypeINBOX + "/%s" const keepAliveInterval = 30 // seconds @@ -153,7 +153,9 @@ func (tp *MqttHubTransport) Connect(credentials string) error { cancelFn() if err != nil { // provide a more meaningful error, the actual error is not returned by paho + tp.mux.RLock() err = tp._status.LastError + tp.mux.RUnlock() } return err } diff --git a/lib/things/TD.go b/lib/things/TD.go index 9a441e37..33ab74a6 100644 --- a/lib/things/TD.go +++ b/lib/things/TD.go @@ -4,7 +4,6 @@ import ( "github.com/araddon/dateparse" "github.com/hiveot/hub/api/go" "github.com/hiveot/hub/lib/ser" - "github.com/hiveot/hub/lib/utils" "sync" "time" ) @@ -281,16 +280,16 @@ func (tdoc *TD) GetAction(name string) *ActionAffordance { return actionAffordance } -// GetAge returns the age of the document since last modified in a human-readable format. -// this is just an experiment to see if this is useful. -// Might be better to do on the UI client side to reduce cpu. -func (tdoc *TD) GetAge() string { - t, err := dateparse.ParseAny(tdoc.Modified) - if err != nil { - return tdoc.Modified - } - return utils.Age(t) -} +//// GetAge returns the age of the document since last modified in a human-readable format. +//// this is just an experiment to see if this is useful. +//// Might be better to do on the UI client side to reduce cpu. +//func (tdoc *TD) GetAge() string { +// t, err := dateparse.ParseAny(tdoc.Modified) +// if err != nil { +// return tdoc.Modified +// } +// return utils.Age(t) +//} // GetAtTypeVocab return the vocab map of the @type func (tdoc *TD) GetAtTypeVocab() string { @@ -340,8 +339,13 @@ func (tdoc *TD) GetPropertyOfType(atType string) (string, *PropertyAffordance) { // GetUpdated is a helper function to return the formatted time the thing was last updated. // This uses the time format RFC822 ("02 Jan 06 15:04 MST") func (tdoc *TD) GetUpdated() string { - created, _ := dateparse.ParseAny(tdoc.Modified) - return created.Format(time.RFC1123) + created, err := dateparse.ParseAny(tdoc.Modified) + if err != nil { + return tdoc.Modified + } + created = created.Local() + return created.Format(time.RFC822) + } // GetID returns the ID of the things TD @@ -420,18 +424,20 @@ func (tdoc *TD) UpdateTitleDescription(title string, description string) { // properties: {name: TDProperty, ...} // } func NewTD(thingID string, title string, deviceType string) *TD { + td := TD{ AtContext: []any{ WoTTDContext, map[string]string{"ht": HiveOTContext}, }, - AtType: deviceType, - Actions: map[string]*ActionAffordance{}, - Created: time.Now().Format(utils.ISO8601Format), + AtType: deviceType, + Actions: map[string]*ActionAffordance{}, + + Created: time.Now().Format(time.RFC3339), Events: map[string]*EventAffordance{}, Forms: nil, ID: thingID, - Modified: time.Now().Format(utils.ISO8601Format), + Modified: time.Now().Format(time.RFC3339), Properties: map[string]*PropertyAffordance{}, // security schemas don't apply to HiveOT devices, except services exposed by the hub itself Security: vocab.WoTNoSecurityScheme, diff --git a/lib/things/TD_test.go b/lib/things/TD_test.go index 4dca2e2d..260aff2b 100644 --- a/lib/things/TD_test.go +++ b/lib/things/TD_test.go @@ -3,7 +3,6 @@ package things_test import ( vocab "github.com/hiveot/hub/api/go" "github.com/hiveot/hub/lib/things" - "github.com/hiveot/hub/lib/utils" "testing" "time" @@ -34,9 +33,9 @@ func TestCreateTD(t *testing.T) { }, } - // created time must be set to ISO8601 + // created time must be set to RFC3339 assert.NotEmpty(t, tdoc.Created) - t1, err := time.Parse(utils.ISO8601Format, tdoc.Created) + t1, err := time.Parse(time.RFC3339, tdoc.Created) assert.NoError(t, err) assert.NotNil(t, t1) diff --git a/lib/things/ThingValue.go b/lib/things/ThingValue.go index bfbcb91b..4c643a1b 100644 --- a/lib/things/ThingValue.go +++ b/lib/things/ThingValue.go @@ -1,7 +1,6 @@ package things import ( - "github.com/hiveot/hub/lib/utils" "time" ) @@ -47,18 +46,18 @@ type ThingValue struct { ValueType string `json:"valueType"` } -// Updated is a helper function to return the formatted time the data was last updated. +// GetUpdated is a helper function to return the formatted time the data was last updated. // This uses the time format RFC822 ("02 Jan 06 15:04 MST") -func (tv *ThingValue) Updated() string { - created := time.Unix(tv.CreatedMSec/1000, 0) +func (tv *ThingValue) GetUpdated() string { + created := time.Unix(tv.CreatedMSec/1000, 0).Local() return created.Format(time.RFC822) } -// Age is a helper function to return the age of the data for use in templates -func (tv *ThingValue) Age() string { - t := time.Unix(tv.CreatedMSec/1000, 0) - return utils.Age(t) -} +//// Age is a helper function to return the age of the data for use in templates +//func (tv *ThingValue) Age() string { +// t := time.Unix(tv.CreatedMSec/1000, 0) +// return utils.Age(t) +//} // NewThingValue creates a new ThingValue object with the address of the things, the action or event id and the serialized value data // This copies the value buffer. diff --git a/lib/things/ThingValueMap.go b/lib/things/ThingValueMap.go index 53b13d63..e2b481ff 100644 --- a/lib/things/ThingValueMap.go +++ b/lib/things/ThingValueMap.go @@ -4,13 +4,13 @@ type ThingValueMap map[string]*ThingValue // Age returns the age of a property, or "" if it doesn't exist // intended for use in template as .Values.Age $key -func (vm ThingValueMap) Age(key string) string { - tv := vm.Get(key) - if tv == nil { - return "" - } - return tv.Age() -} +//func (vm ThingValueMap) Age(key string) string { +// tv := vm.Get(key) +// if tv == nil { +// return "" +// } +// return tv.Age() +//} // Get returns the value of a property key, or nil if it doesn't exist func (vm ThingValueMap) Get(key string) *ThingValue { @@ -21,6 +21,16 @@ func (vm ThingValueMap) Get(key string) *ThingValue { return tv } +// GetUpdated returns the timestamp of a property, or "" if it doesn't exist +// intended for use in template as .Values.GetUpdated $key +func (vm *ThingValueMap) GetUpdated(key string) string { + tv := vm.Get(key) + if tv == nil { + return "" + } + return tv.GetUpdated() +} + // ToString returns the value of a property as text, or "" if it doesn't exist // intended for use in template as .Values.ToString $key func (vm ThingValueMap) ToString(key string) string { @@ -47,16 +57,6 @@ func (vm ThingValueMap) Set(key string, tv *ThingValue) { vm[key] = tv } -// Updated returns the timestamp of a property, or "" if it doesn't exist -// intended for use in template as .Values.Updated $key -func (vm *ThingValueMap) Updated(key string) string { - tv := vm.Get(key) - if tv == nil { - return "" - } - return tv.Updated() -} - func NewThingValueMap() ThingValueMap { vm := make(ThingValueMap) return vm diff --git a/libjs/src/things/ThingTD.ts b/libjs/src/things/ThingTD.ts index b44834e0..8d388943 100644 --- a/libjs/src/things/ThingTD.ts +++ b/libjs/src/things/ThingTD.ts @@ -3,6 +3,8 @@ import {DataSchema} from "./dataSchema"; +const TD_CONTEXT = "https://www.w3.org/2022/wot/td/v1.1" +const HT_CONTEXT = "https://www.hiveot.net/vocab/v0.1" export class InteractionAffordance extends Object { // Unique name of the affordance, eg: property, event or action name @@ -97,12 +99,21 @@ export class ThingTD extends Object { constructor(deviceID: string, deviceType: string, title: string, description: string) { super(); this.id = deviceID; + this["@context"] = [ + TD_CONTEXT, + { "ht": HT_CONTEXT } + ] this["@type"] = deviceType; this.title = title; this.description = description; this.created = new Date().toISOString(); this.modified = this.created; } + /** JSON-LD context */ + public "@context": any[] = []; + + /** Type of thing defined in the vocabulary */ + public "@type": string = ""; /** Unique thing ID */ public readonly id: string = ""; @@ -119,8 +130,6 @@ export class ThingTD extends Object { /** Human-readable title for ui representation */ public title: string = ""; - /** Type of thing defined in the vocabulary */ - public "@type": string = ""; /** * Collection of properties of a thing @@ -186,12 +195,16 @@ export class ThingTD extends Object { // By default this property is read-only. (eg an attribute) // // @param id is the instance ID under which it is stored in the property affordance map. + // if not provided then propType is used. // @param propType is one of the PropertyTypes in the vocabulary or "" if not known // @param title is the title used in the property. // @param dataType is the type of data the property holds, DataTypeNumber, ..Object, ..Array, ..String, ..Integer, ..Boolean or null // @param initialValue the value at time of creation, for testing and debugging AddProperty(id: string, propType: string, title: string, dataType: string): PropertyAffordance { let prop = new PropertyAffordance() + if (id == "") { + id = propType + } prop.id = id; if (propType) { prop["@type"] = propType diff --git a/libjs/src/things/dataSchema.ts b/libjs/src/things/dataSchema.ts index c67146b6..7911ac6d 100644 --- a/libjs/src/things/dataSchema.ts +++ b/libjs/src/things/dataSchema.ts @@ -60,6 +60,7 @@ export class DataSchema extends Object { public writeOnly: boolean = false // Enumeration table to lookup the value or key + // FIXME: use oneOf object private enumTable: Object | undefined = undefined // Change the property into a writable configuration @@ -75,6 +76,7 @@ export class DataSchema extends Object { // @param enumeration is a map from enum values to names and vice-versa // @param initialValue is converted to name and stored in the schema as initialValue (for testing/debugging) SetAsEnum(enumeration: Object): DataSchema { + // FIXME: use oneOf object - match golang this.enumTable = enumeration let keys = Object.values(enumeration) this.enum = keys.filter((key: any) => {