loupedeck-commander 1.2.7 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { readJSONFile, writeJSONFile } from './utils.mjs'
1
+ import { readJSONFile, writeJSONFile,syncParams } from './utils.mjs'
2
2
 
3
3
  export class ApplicationConfig {
4
4
  application = 'undefined'
@@ -42,6 +42,7 @@ class Profile {
42
42
  knobs = {}
43
43
  buttons = {}
44
44
  parameters = {}
45
+ default = {}
45
46
  #file = ''
46
47
  loaded = false
47
48
  #error = true
@@ -57,6 +58,7 @@ class Profile {
57
58
  this.buttons = new ButtonConfig().buttons
58
59
  this.knobs = new KnobsConfig().knobs
59
60
  this.parameters = new ParametersConfig().parameters
61
+ this.default = new DefaultConfig().default
60
62
 
61
63
  this.loadFromFile(this.#file)
62
64
  if (this.#error) { this.saveToFile(`profile-${this.name}-sav.json`) }
@@ -83,15 +85,16 @@ class Profile {
83
85
  }
84
86
  this.profile = config.profile
85
87
  this.description = config.description
88
+ // Load Parameters.parameters = config.parameters
89
+ this.parameters = new ParametersConfig(config.parameters).parameters
90
+ this.default = new DefaultConfig(config.default).default
86
91
 
87
92
  // Load the Configurations for Touch-Displays
88
- this.touch = new TouchConfig(config.touch)
93
+ this.touch = new TouchConfig(config.touch,this.default)
89
94
  // Load the Configurations for Button-Areas
90
95
 
91
96
  this.buttons = new ButtonConfig(config.buttons,this.#profileCount,this.#index).buttons
92
97
  this.knobs = new KnobsConfig(config.knobs).knobs
93
- // Load Parameters.parameters = config.parameters
94
- this.parameters = new ParametersConfig(config.parameters).parameters
95
98
 
96
99
  this.#error = false
97
100
  this.loaded = true
@@ -132,11 +135,11 @@ class TouchConfig {
132
135
  } // RIGHT Display Config - Available in CT & LIVE
133
136
  //knob = {} // KNOB Display Config - Available only in CT
134
137
 
135
- constructor (data) {
136
- this.loadFromJSON(data)
138
+ constructor (data,defaultState) {
139
+ this.loadFromJSON(data,defaultState)
137
140
  }
138
141
 
139
- loadFromJSON (data) {
142
+ loadFromJSON (data,defaultState) {
140
143
  if (!data)
141
144
  return
142
145
  if (!data.center)
@@ -150,7 +153,21 @@ class TouchConfig {
150
153
  this.center = data.center
151
154
  this.left = data.left
152
155
  this.right = data.right
153
- //this.knob = data.knob
156
+
157
+ /**
158
+ * Sychronize the states of the buttons with the default state - add missing options from default state
159
+ */
160
+ var buttons = Object.keys(this.center)
161
+ for (var buttonID=0;buttonID<buttons.length;buttonID++){
162
+ if (!this.center[buttonID])
163
+ continue
164
+ var states = Object.keys(this.center[buttonID].states)
165
+ for (var stateID=0;stateID<states.length;stateID++){
166
+ var stateKey = states[stateID]
167
+ var stateOld = this.center[buttonID].states[stateKey]
168
+ this.center[buttonID].states[stateKey] = syncParams(stateOld,defaultState)
169
+ }
170
+ }
154
171
  }
155
172
  }
156
173
 
@@ -221,8 +238,10 @@ class ParametersConfig {
221
238
  parameters = {
222
239
  "hostname": "127.0.0.1",
223
240
  "endpointurl": "opc.tcp://{hostname}:4840",
224
- "nodeid" : "ns=0;s=nodeID"
225
- } // KNOB Config - Available only in CT
241
+ "nodeid" : "ns=0;s=nodeID",
242
+ "min" : 0,
243
+ "max" : 100
244
+ }
226
245
 
227
246
  constructor (data) {
228
247
  this.loadFromJSON(data)
@@ -234,3 +253,20 @@ class ParametersConfig {
234
253
  this.parameters = data
235
254
  }
236
255
  }
256
+
257
+ class DefaultConfig {
258
+ default = {
259
+ "filter": "released",
260
+ "color": "#111111"
261
+ }
262
+
263
+ constructor (data) {
264
+ this.loadFromJSON(data)
265
+ }
266
+
267
+ loadFromJSON (data) {
268
+ if (!data)
269
+ return
270
+ this.default = data
271
+ }
272
+ }
@@ -166,6 +166,8 @@ export class BaseLoupeDeckHandler {
166
166
  await this.updateScreens()
167
167
 
168
168
  await this.buttons.draw(this.device)
169
+ // Initialize the Interfaces
170
+ await InitializeInterfaces(profile)
169
171
 
170
172
  }
171
173
 
@@ -186,8 +188,9 @@ export class BaseLoupeDeckHandler {
186
188
 
187
189
  await this.activateProfile(0)
188
190
 
189
- var profile = this.getCurrentProfile()
190
- await InitializeInterfaces(profile,this.buttons.setState)
191
+ // move into activate profile function to call init with every profile change
192
+ // var profile = this.getCurrentProfile()
193
+ //await InitializeInterfaces(profile)
191
194
 
192
195
  const self = this
193
196
 
@@ -333,6 +336,7 @@ export class BaseLoupeDeckHandler {
333
336
  this.screenUpdate["right"] = true
334
337
 
335
338
  ok = await this.screens.center.changed(buttonID,nodeid,val)
339
+ ok = await this.knobs.changed(buttonID,nodeid,val)
336
340
 
337
341
  await this.updateScreens()
338
342
  return ok
@@ -5,7 +5,7 @@ import * as shellif from '../interfaces/shellif.mjs'
5
5
  import * as httpif from '../interfaces/httpif.mjs'
6
6
  import * as opcuaif from '../interfaces/opcuaif.mjs'
7
7
  import format from 'string-template'
8
- import { calcDelta } from './utils.mjs'
8
+ import { calcDelta, invertColor } from './utils.mjs'
9
9
  import { EventEmitter } from 'node:events'
10
10
 
11
11
 
@@ -13,17 +13,18 @@ export var opcuainterface = undefined
13
13
  var httpinterface = undefined
14
14
  var shellinterface = undefined
15
15
  export var profileEmitter = undefined
16
- export async function InitializeInterfaces(appConfig,callbackFunction){
16
+ export async function InitializeInterfaces(appConfig){
17
17
  if (opcuainterface === undefined ){
18
18
  opcuainterface = new opcuaif.OPCUAIf()
19
- opcuainterface.init(appConfig.parameters,appConfig,callbackFunction)
20
19
  }
20
+ // the opcua interface needs the profile to register nodes with subscriptions:
21
+ opcuainterface.init(appConfig.parameters,appConfig)
21
22
  if (httpinterface === undefined)
22
23
  httpinterface = new httpif.HTTPif()
23
24
  if (shellinterface === undefined)
24
25
  shellinterface = new shellif.SHELLif()
25
-
26
- profileEmitter = new EventEmitter()
26
+ if (profileEmitter === undefined)
27
+ profileEmitter = new EventEmitter()
27
28
  }
28
29
 
29
30
  export async function StopInterfaces(){
@@ -81,7 +82,7 @@ export class ButtonField {
81
82
  const keys = Object.keys(data)
82
83
  for (let i = 0; i < keys.length; i++) {
83
84
  const key = keys[i]
84
- const tb = new Button(`${this.#type}-${key}`, width / columns, height / rows, data[key],key,this.#profile.parameters)
85
+ const tb = new Button(`${this.#type}-${key}`, width / columns, height / rows, data[key],key,this.#profile)
85
86
  this.#buttons[key] = tb
86
87
  }
87
88
 
@@ -191,6 +192,8 @@ export class ButtonField {
191
192
 
192
193
  export class Button {
193
194
  #profile
195
+ #params
196
+ #data
194
197
  width = 0
195
198
  height = 0
196
199
 
@@ -229,7 +232,7 @@ export class Button {
229
232
  minPressed = 25
230
233
  key = -1
231
234
 
232
- constructor (id, width, height, data,key,params) {
235
+ constructor (id, width, height, data,key,profile) {
233
236
  this.id = id
234
237
  this.key = key
235
238
  this.width = width
@@ -237,8 +240,10 @@ export class Button {
237
240
  this.#index = 0
238
241
 
239
242
  if (data && data.states) {
240
- this.group = data.group
243
+ this.#data = data
241
244
 
245
+ this.group = data.group
246
+
242
247
  this.#states = data.states
243
248
  this.#keys = Object.keys(this.#states)
244
249
  if (data.type) {
@@ -253,12 +258,23 @@ export class Button {
253
258
  this.text = data.text
254
259
  }
255
260
  }
261
+ if (profile === undefined){
262
+ this.#profile = {}
263
+ this.#params = {}
264
+ }else{
265
+ this.#profile = profile
266
+ this.#params = profile.parameters
267
+ if (profile.parameters.min !== undefined)
268
+ this.#min = profile.parameters.min
269
+ if (profile.parameters.max !== undefined)
270
+ this.#max = profile.parameters.max
271
+ }
256
272
  if (this.#states === undefined) {
257
273
  this.#states = {}
258
274
  this.#keys = []
259
275
  }
260
276
  if (data.nodeid){
261
- this.#nodeid = format(data.nodeid, params)
277
+ this.#nodeid = format(data.nodeid, this.#params)
262
278
  }
263
279
  if (data.default) {
264
280
  this.#index = this.#keys.indexOf(data.default)
@@ -308,8 +324,10 @@ export class Button {
308
324
  }
309
325
  }
310
326
  if (this.text){
311
- const lastElem = this.getLastElement()
312
- ctx.fillStyle = lastElem.color
327
+ //const lastElem = this.getLastElement()
328
+ // Only change the text color, if it differnce from the currently set color
329
+
330
+ ctx.fillStyle = invertColor(elem.color)
313
331
  ctx.font = '20px Verdana'
314
332
  ctx.textBaseline = 'top';
315
333
  ctx.textAlign = 'left';
@@ -408,7 +426,7 @@ export class Button {
408
426
  if (!this.getCurrentElement()) { return false }
409
427
 
410
428
  this.#event = "rotated"
411
- this.#value = calcDelta(this.#value, delta, this.#max)
429
+ this.#value = calcDelta(this.#value, delta, this.#min, this.#max)
412
430
  return this.runCommand()
413
431
  }
414
432
 
@@ -429,20 +447,21 @@ export class Button {
429
447
 
430
448
  // check if the nodeid is the same and the value is one of the states
431
449
  let state = this.#states[key]
432
- if (!state.value)
450
+ if (state.value === undefined)
433
451
  continue
434
452
 
435
453
  const params = {
436
454
  id: buttonID,
437
455
  key: buttonID,
456
+ state : key,
438
457
  ...state
439
458
  }
440
459
  let val1 = format(state.value,params)
441
- if (val1 === val){
460
+ if (val1 === val.toString()){
442
461
  this.#index = i;
443
462
  break;
444
463
  }
445
- break;
464
+ //break;
446
465
  }
447
466
  }
448
467
 
@@ -507,7 +526,7 @@ export class Button {
507
526
  y: (this.#y %100)
508
527
  }
509
528
 
510
- if (!params.value)
529
+ if (params.value === undefined)
511
530
  params.value = this.#value
512
531
 
513
532
 
@@ -529,6 +548,12 @@ export class Button {
529
548
  if ('opcua' in elem) {
530
549
  if (opcuainterface){
531
550
  res = await opcuainterface.call(elem.opcua, params)
551
+
552
+ if (this.#data.statenodeid){
553
+ let stateParams = params
554
+ params.value = params.state
555
+ res = await opcuainterface.call(this.#data.statenodeid, params)
556
+ }
532
557
  }else{
533
558
  console.warn("opcuainterface not started")
534
559
  }
package/common/utils.mjs CHANGED
@@ -22,9 +22,64 @@ export function writeJSONFile (fileName, jsonObj) {
22
22
  writeFileSync(fileName, data)
23
23
  }
24
24
 
25
- export function calcDelta (data, delta, max = 100) {
25
+ /**
26
+ * Calculate the delta of a value within a range between min and max with overflow
27
+ * @param {*} data
28
+ * @param {*} delta
29
+ * @param {*} min
30
+ * @param {*} max
31
+ * @returns
32
+ */
33
+ export function calcDelta (data, delta, min = 0, max = 100) {
26
34
  data = data + delta
27
- if (data > max) { data = max }
28
- if (data < 0) { data = 0 }
35
+ if (data > max) { data = min }
36
+ else if (data < min) { data = max }
29
37
  return data
30
38
  }
39
+
40
+ /**
41
+ * Invert a color in hex format (#RRGGBB) to its inverted color
42
+ * @param {*} colorAsHex
43
+ * @returns
44
+ */
45
+ export function invertColor(colorAsHex){
46
+ let rgb = colorToRGB(colorAsHex)
47
+ for (var i = 0; i < rgb.length; i++) {
48
+ rgb[i] = (i === 3 ? 1 : 255) - rgb[i];
49
+ }
50
+ let invertedColor = `#${rgb[0].toString(16)}${rgb[1].toString(16)}${rgb[2].toString(16)})`
51
+ //let invertedColor = `#${rgb[0].toString(16).padStart(2, '0')}${rgb[1].toString(16).padStart(2, '0')}${rgb[2].toString(16).padStart(2, '0')})`
52
+ //console.log("invert", colorAsHex,"=>",invertedColor)
53
+ return invertedColor
54
+ }
55
+
56
+ /**
57
+ * Convert a color in hex format (#RRGGBB) to RGB array
58
+ * @param {*} colorAsHex
59
+ * @returns
60
+ */
61
+ export function colorToRGB(colorAsHex){
62
+ const r = parseInt(colorAsHex.slice(1, 3), 16)
63
+ const g = parseInt(colorAsHex.slice(3, 5), 16)
64
+ const b = parseInt(colorAsHex.slice(5, 7), 16)
65
+ return [r,g,b]
66
+ }
67
+
68
+ /**
69
+ * Synchronize the parameters of a node with the default node
70
+ * @param {*} node
71
+ * @param {*} defaultnode
72
+ * @returns node
73
+ */
74
+ export function syncParams(node, defaultnode){
75
+ if (node === undefined || defaultnode === undefined)
76
+ return node
77
+ let keys = Object.keys(defaultnode)
78
+ for (var i=0;i<keys.length;i++){
79
+ let key = keys[i]
80
+ if (!(key in node))
81
+ node[key] = defaultnode[key]
82
+ }
83
+ return node
84
+ }
85
+
@@ -36,7 +36,6 @@ export class OPCUAIf extends BaseIf {
36
36
  monitoreditems
37
37
  types
38
38
  buttons
39
- #callback
40
39
  constructor() {
41
40
  super()
42
41
 
@@ -61,14 +60,13 @@ export class OPCUAIf extends BaseIf {
61
60
  * @param {*} config
62
61
  * @param {*} callbackFunction
63
62
  */
64
- async init( options = {},config = {},callbackFunction){
63
+ async init( options = {},config = {}){
65
64
  var res = this.Check(options)
66
65
  if (res<0){
67
66
  this.LogError(`OPCUAIf: Missing essential options in dictionary => Quitting $res $options\n`)
68
67
  }
69
68
  try{
70
69
  this.#endpointurl = this.formatString(options.endpointurl,options)
71
- this.#callback = callbackFunction
72
70
  this.monitoreditems = {}
73
71
  this.types = {}
74
72
  this.buttons = {}
@@ -84,11 +82,18 @@ export class OPCUAIf extends BaseIf {
84
82
  for (let i = 0; i < keys.length; i++) {
85
83
  const key = keys[i]
86
84
  const elem = field[key]
85
+ // groupnode
87
86
  if (elem.nodeid){
88
87
  let format = this.formatString(elem.nodeid,options)
89
88
  let monitoredItemId = await this.Subscribe(format)
90
89
  this.buttons[monitoredItemId] = i
91
90
  }
91
+ // statenode
92
+ if (elem.statenodeid){
93
+ let format = this.formatString(elem.statenodeid,options)
94
+ let monitoredItemId = await this.Subscribe(format)
95
+ this.buttons[monitoredItemId] = i
96
+ }
92
97
  await this.monitorStates(elem,options)
93
98
  }
94
99
  }
@@ -146,6 +151,15 @@ export class OPCUAIf extends BaseIf {
146
151
  if (typeof value == "number")
147
152
  return value.toString();
148
153
  return value
154
+ case DataType.Boolean:
155
+ if (typeof value == "number" && value === 1)
156
+ return true
157
+ if (typeof value == "string"){
158
+ if (["true","on"].includes(value)){
159
+ return true
160
+ }
161
+ }
162
+ return false
149
163
  default:
150
164
  return value
151
165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loupedeck-commander",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "A system to ease working with LoupeDeck devices using CMD-line, OPC/UA or HTTP-client interfaces",
5
5
  "main": "index.mjs",
6
6
  "scripts": {
package/numbers/README.md DELETED
@@ -1,5 +0,0 @@
1
- # Numbers Icons
2
-
3
- Downloaded from:
4
-
5
- - https://www.freepik.com/author/moniruldislam/icons/generic-color-lineal-color_10172?query=Number#from_element=families
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,373 +0,0 @@
1
- {
2
- "name": "profile-1",
3
- "profile": "example",
4
- "description": "",
5
- "touch": {
6
- "center": {
7
- "0": {
8
- "states": {
9
- "off": {
10
- "color": "#000099",
11
- "image": "icons/home.png",
12
- "cmd": "echo \"{id} {state}\""
13
- },
14
- "on": {
15
- "color": "#00ff00",
16
- "image": "icons/home.png",
17
- "cmd": "echo \"{id} {state}\""
18
- }
19
- },
20
- "group": ""
21
- },
22
- "1": {
23
- "states": {
24
- "off": {
25
- "color": "#000099",
26
- "image": "icons/home.png",
27
- "cmd": "echo \"{id} {state}\""
28
- },
29
- "on": {
30
- "color": "#00ff00",
31
- "image": "icons/home.png",
32
- "cmd": "echo \"{id} {state}\""
33
- }
34
- },
35
- "group": ""
36
- },
37
- "2": {
38
- "states": {
39
- "off": {
40
- "color": "#000099",
41
- "image": "icons/home.png",
42
- "cmd": "echo \"{id} {state}\""
43
- },
44
- "on": {
45
- "color": "#00ff00",
46
- "image": "icons/home.png",
47
- "cmd": "echo \"{id} {state}\""
48
- }
49
- },
50
- "group": ""
51
- },
52
- "3": {
53
- "states": {
54
- "off": {
55
- "color": "#000099",
56
- "image": "icons/home.png",
57
- "cmd": "echo \"{id} {state}\""
58
- },
59
- "on": {
60
- "color": "#00ff00",
61
- "image": "icons/home.png",
62
- "cmd": "echo \"{id} {state}\""
63
- }
64
- },
65
- "group": ""
66
- },
67
- "4": {
68
- "states": {
69
- "off": {
70
- "color": "#000099",
71
- "image": "icons/home.png",
72
- "cmd": "echo \"{id} {state}\""
73
- },
74
- "on": {
75
- "color": "#00ff00",
76
- "image": "icons/home.png",
77
- "cmd": "echo \"{id} {state}\""
78
- }
79
- },
80
- "group": ""
81
- },
82
- "5": {
83
- "states": {
84
- "off": {
85
- "color": "#000099",
86
- "image": "icons/home.png",
87
- "cmd": "echo \"{id} {state}\""
88
- },
89
- "on": {
90
- "color": "#00ff00",
91
- "image": "icons/home.png",
92
- "cmd": "echo \"{id} {state}\""
93
- }
94
- },
95
- "group": ""
96
- },
97
- "6": {
98
- "states": {
99
- "off": {
100
- "color": "#000099",
101
- "image": "icons/home.png",
102
- "cmd": "echo \"{id} {state}\""
103
- },
104
- "on": {
105
- "color": "#00ff00",
106
- "image": "icons/home.png",
107
- "cmd": "echo \"{id} {state}\""
108
- }
109
- },
110
- "group": ""
111
- },
112
- "7": {
113
- "states": {
114
- "off": {
115
- "color": "#000099",
116
- "image": "icons/home.png",
117
- "cmd": "echo \"{id} {state}\""
118
- },
119
- "on": {
120
- "color": "#00ff00",
121
- "image": "icons/home.png",
122
- "cmd": "echo \"{id} {state}\""
123
- }
124
- },
125
- "group": ""
126
- },
127
- "8": {
128
- "states": {
129
- "off": {
130
- "color": "#000099",
131
- "image": "icons/home.png",
132
- "cmd": "echo \"{id} {state}\""
133
- },
134
- "on": {
135
- "color": "#00ff00",
136
- "image": "icons/home.png",
137
- "cmd": "echo \"{id} {state}\""
138
- }
139
- },
140
- "group": ""
141
- },
142
- "9": {
143
- "states": {
144
- "off": {
145
- "color": "#000099",
146
- "image": "icons/home.png",
147
- "cmd": "echo \"{id} {state}\""
148
- },
149
- "on": {
150
- "color": "#00ff00",
151
- "image": "icons/home.png",
152
- "cmd": "echo \"{id} {state}\""
153
- }
154
- },
155
- "group": ""
156
- },
157
- "10": {
158
- "states": {
159
- "off": {
160
- "color": "#000099",
161
- "image": "icons/home.png",
162
- "cmd": "echo \"{id} {state}\""
163
- },
164
- "on": {
165
- "color": "#00ff00",
166
- "image": "icons/home.png",
167
- "cmd": "echo \"{id} {state}\""
168
- }
169
- },
170
- "group": ""
171
- },
172
- "11": {
173
- "states": {
174
- "off": {
175
- "color": "#000099",
176
- "image": "icons/home.png",
177
- "cmd": "echo \"{id} {state}\""
178
- },
179
- "on": {
180
- "color": "#00ff00",
181
- "image": "icons/home.png",
182
- "cmd": "echo \"{id} {state}\""
183
- }
184
- },
185
- "group": ""
186
- }
187
- },
188
- "left": {
189
- "0": {
190
- "states": {
191
- "on": {
192
- "color": "#000000",
193
- "cmd": "echo \"{id} {state}\""
194
- }
195
- },
196
- "group": ""
197
- }
198
- },
199
- "right": {
200
- "0": {
201
- "states": {
202
- "on": {
203
- "color": "#000000",
204
- "cmd": "echo \"{id} {state}\""
205
- }
206
- },
207
- "group": ""
208
- }
209
- }
210
- },
211
- "knobs": {
212
- "knobTL": {
213
- "states": {
214
- "on": {
215
- "cmd": "echo \"{id} {state}\""
216
- }
217
- },
218
- "group": ""
219
- },
220
- "knobCL": {
221
- "states": {
222
- "on": {
223
- "cmd": "echo \"{id} {state}\""
224
- }
225
- },
226
- "group": ""
227
- },
228
- "knobBL": {
229
- "states": {
230
- "on": {
231
- "cmd": "echo \"{id} {state}\""
232
- }
233
- },
234
- "group": ""
235
- },
236
- "knobTR": {
237
- "states": {
238
- "on": {
239
- "cmd": "echo \"{id} {state}\""
240
- }
241
- },
242
- "group": ""
243
- },
244
- "knobCR": {
245
- "states": {
246
- "on": {
247
- "cmd": "echo \"{id} {state}\""
248
- }
249
- },
250
- "group": ""
251
- },
252
- "knobBR": {
253
- "states": {
254
- "on": {
255
- "cmd": "echo \"{id} {state}\""
256
- }
257
- },
258
- "group": ""
259
- }
260
- },
261
- "buttons": {
262
- "0": {
263
- "states": {
264
- "off": {
265
- "color": "#000000",
266
- "cmd": "echo \"{id} {state}\""
267
- },
268
- "on": {
269
- "color": "#550000",
270
- "cmd": "echo \"{id} {state}\""
271
- }
272
- },
273
- "group": ""
274
- },
275
- "1": {
276
- "states": {
277
- "off": {
278
- "color": "#000000",
279
- "cmd": "echo \"{id} {state}\""
280
- },
281
- "on": {
282
- "color": "#ff0000",
283
- "cmd": "echo \"{id} {state}\""
284
- }
285
- },
286
- "group": ""
287
- },
288
- "2": {
289
- "states": {
290
- "off": {
291
- "color": "#000000",
292
- "cmd": "echo \"{id} {state}\""
293
- },
294
- "on": {
295
- "color": "#005500",
296
- "cmd": "echo \"{id} {state}\""
297
- }
298
- },
299
- "group": ""
300
- },
301
- "3": {
302
- "states": {
303
- "off": {
304
- "color": "#000000",
305
- "cmd": "echo \"{id} {state}\""
306
- },
307
- "on": {
308
- "color": "#00ff00",
309
- "cmd": "echo \"{id} {state}\""
310
- }
311
- },
312
- "group": ""
313
- },
314
- "4": {
315
- "states": {
316
- "off": {
317
- "color": "#000000",
318
- "cmd": "echo \"{id} {state}\""
319
- },
320
- "on": {
321
- "color": "#000055",
322
- "cmd": "echo \"{id} {state}\""
323
- }
324
- },
325
- "group": ""
326
- },
327
- "5": {
328
- "states": {
329
- "off": {
330
- "color": "#000000",
331
- "cmd": "echo \"{id} {state}\""
332
- },
333
- "on": {
334
- "color": "#0000ff",
335
- "cmd": "echo \"{id} {state}\""
336
- }
337
- },
338
- "group": ""
339
- },
340
- "6": {
341
- "states": {
342
- "off": {
343
- "color": "#000000",
344
- "cmd": "echo \"{id} {state}\""
345
- },
346
- "on": {
347
- "color": "#555500",
348
- "cmd": "echo \"{id} {state}\""
349
- }
350
- },
351
- "group": ""
352
- },
353
- "7": {
354
- "states": {
355
- "off": {
356
- "color": "#000000",
357
- "cmd": "echo \"{id} {state}\""
358
- },
359
- "on": {
360
- "color": "#ffff00",
361
- "cmd": "echo \"{id} {state}\""
362
- }
363
- },
364
- "group": ""
365
- }
366
- },
367
- "parameters": {
368
- "hostname": "127.0.0.1",
369
- "endpointurl": "opc.tcp://{hostname}:4840",
370
- "nodeid": "ns=0;s=nodeID"
371
- },
372
- "loaded": false
373
- }