node-red-contrib-alice 2.2.3 → 2.2.5

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.
package/.eslintrc.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "es2021": true
5
+ },
6
+ "extends": [
7
+ "eslint:recommended",
8
+ "plugin:@typescript-eslint/recommended"
9
+ ],
10
+ "parser": "@typescript-eslint/parser",
11
+ "parserOptions": {
12
+ "ecmaVersion": "latest",
13
+ "sourceType": "module"
14
+ },
15
+ "plugins": [
16
+ "@typescript-eslint"
17
+ ],
18
+ "rules": {
19
+ }
20
+ }
@@ -16,7 +16,9 @@
16
16
  types: [
17
17
  {
18
18
  options: [
19
- { value: "devices.types.light", label: "Light" },
19
+ { value: "devices.types.light", label: "Light" },
20
+ { value: "devices.types.light.ceiling", label: "Light Сeiling" },
21
+ { value: "devices.types.light.strip", label: "Light Strip" },
20
22
  { value: "devices.types.socket", label: "Socket" },
21
23
  { value: "devices.types.switch", label: "Switch" },
22
24
  { value: "devices.types.thermostat", label: "Thermostat" },
@@ -32,8 +34,11 @@
32
34
  { value: "devices.types.cooking.multicooker", label: "Multicooker" },
33
35
  { value: "devices.types.openable", label: "Door, gate, window, shutters" },
34
36
  { value: "devices.types.openable.curtain", label: "Curtains, blinds" },
37
+ { value: "devices.types.openable.valve", label: "Valve (ball valve)" },
35
38
  { value: "devices.types.humidifier", label: "Humidifier" },
36
39
  { value: "devices.types.purifier", label: "Air purifier" },
40
+ { value: "devices.types.ventilation", label: "Ventilation" },
41
+ { value: "devices.types.ventilation.fan", label: "Fan" },
37
42
  { value: "devices.types.vacuum_cleaner", label: "Vacuum cleaner robot" },
38
43
  { value: "devices.types.washing_machine", label: "Washing machine" },
39
44
  { value: "devices.types.dishwasher", label: "Dishwasher" },
@@ -86,50 +91,4 @@
86
91
  <label for="node-config-input-dtype">Type</label>
87
92
  <input type="text" id="node-config-input-dtype">
88
93
  </div>
89
- <!-- <div class="form-row">
90
- <label for="node-config-input-dtype">Type</label>
91
- <select id="node-config-input-dtype" style="width: 70%;">
92
- <option value="devices.types.light">Light</option>
93
- <option value="devices.types.socket">Socket</option>
94
- <option value="devices.types.switch" >Switch</option>
95
- <option value="devices.types.thermostat">Thermostat</option>
96
- <option value="devices.types.thermostat.ac">Air conditioning</option>
97
- <option value="devices.types.media_device">Multimedia</option>
98
- <option value="devices.types.media_device.tv">TV</option>
99
- <option value="devices.types.media_device.tv_box">TV Box</option>
100
- <option value="devices.types.media_device.receiver">AV Receiver</option>
101
- <option value="devices.types.camera">Camera</option>
102
- <option value="devices.types.cooking">Kitchen appliances</option>
103
- <option value="devices.types.cooking.coffee_maker">Coffee machine</option>
104
- <option value="devices.types.cooking.kettle">Smart kettle</option>
105
- <option value="devices.types.cooking.multicooker">Multicooker</option>
106
- <option value="devices.types.openable">Door, gate, window, shutters</option>
107
- <option value="devices.types.openable.curtain">Curtains, blinds</option>
108
- <option value="devices.types.humidifier">Humidifier</option>
109
- <option value="devices.types.purifier">Air purifier</option>
110
- <option value="devices.types.vacuum_cleaner">Vacuum cleaner robot</option>
111
- <option value="devices.types.washing_machine">Washing machine</option>
112
- <option value="devices.types.dishwasher">Dishwasher</option>
113
- <option value="devices.types.iron">Iron, steam generator</option>
114
- <option value="devices.types.sensor">Sensor</option>
115
- <option value="devices.types.sensor.motion">Sensor motion</option>
116
- <option value="devices.types.sensor.vibration">Sensor vibration</option>
117
- <option value="devices.types.sensor.illumination">Sensor illumination</option>
118
- <option value="devices.types.sensor.open">Sensor open</option>
119
- <option value="devices.types.sensor.climate">Sensor climate</option>
120
- <option value="devices.types.sensor.water_leak">Sensor water_leak</option>
121
- <option value="devices.types.sensor.button">Sensor button</option>
122
- <option value="devices.types.sensor.gas">Sensor gas</option>
123
- <option value="devices.types.sensor.smoke">Sensor smoke</option>
124
- <option value="devices.types.smart_meter">Counter</option>
125
- <option value="devices.types.smart_meter.cold_water">Cold Water counter</option>
126
- <option value="devices.types.smart_meter.hot_water">Hot Water counter</option>
127
- <option value="devices.types.smart_meter.electricity">Electricity counter</option>
128
- <option value="devices.types.smart_meter.gas">Gas counter</option>
129
- <option value="devices.types.smart_meter.heat">Heat Water counter</option>
130
- <option value="devices.types.pet_drinking_fountain">Pet drinking fountain</option>
131
- <option value="devices.types.pet_feeder">Pet feeder</option>
132
- <option value="devices.types.other">Other</option>
133
- </select>
134
- </div> -->
135
94
  </script>
@@ -0,0 +1,91 @@
1
+ <script type="text/javascript">
2
+ let udyaconfig={};
3
+ RED.nodes.registerType('Alice-Get',{
4
+ category: 'alice',
5
+ inputs:1,
6
+ outputs:1,
7
+ icon: "alice.png",
8
+ color: "#D8BFD8",
9
+ defaults:{
10
+ service: {value:"", type:"alice-service"},
11
+ device: {value:undefined}
12
+ },
13
+ label: function(){
14
+ return this.name || "Alice-Get";
15
+ },
16
+ oneditprepare: ()=>{
17
+ $("#node-input-service").change(function(e){
18
+ if (this.value &&this.value!='_ADD_'){
19
+ $.ajax({
20
+ url: "/noderedhome/"+this.value+"/getfullconfig",
21
+ type:"GET"
22
+ })
23
+ .done(result=>{
24
+ // RED.notify("Full Alice SmartHome config successfully retrieved", {type:"success"});
25
+ console.log(result);
26
+ udyaconfig=result;
27
+ updateHouse(result.households);
28
+ })
29
+ .fail(error=>{
30
+ RED.notify("Error when retrieve Alice SmartHome config", {type:"error"});
31
+ });
32
+ }
33
+ });
34
+ $('#node-input-home')
35
+ .prop('disabled', 'disabled')
36
+ .change((e)=>{
37
+ let val = $('#node-input-home').find(":selected").val();
38
+ console.log(val);
39
+ });
40
+
41
+ $('#node-input-room').prop('disabled', 'disabled');
42
+ $('#node-input-device').prop('disabled', 'disabled');
43
+
44
+ }
45
+ });
46
+ function updateHouse(data){
47
+ $('#node-input-home')
48
+ .find('option')
49
+ .remove()
50
+ .end();
51
+ udyaconfig.households.forEach(h => {
52
+ $('#node-input-home')
53
+ .append('<option value="'+h.id+'">'+h.name+'</option>');
54
+ });
55
+ $('#node-input-home')
56
+ .prop('disabled', false);
57
+ };
58
+ function updaterooms(house){
59
+ $('#node-input-room')
60
+ .find('option')
61
+ .remove()
62
+ .end();
63
+ udyaconfig.households.forEach(h => {
64
+ if (h.household_id==house){
65
+ $('#node-input-room')
66
+ .append('<option value="'+h.id+'">'+h.name+'</option>');
67
+ }
68
+ });
69
+ $('#node-input-room').prop('disabled', false);
70
+ };
71
+
72
+ </script>
73
+
74
+ <script type="text/x-red" data-template-name="Alice-Get">
75
+ <div class="form-row">
76
+ <label for="node-input-service">Account</label>
77
+ <input id="node-input-service">
78
+ </div>
79
+ <div class="form-row">
80
+ <label for="node-input-home">Home</label>
81
+ <select id="node-input-home" style="width: 70%;"></select>
82
+ </div>
83
+ <div class="form-row">
84
+ <label for="node-input-room">Room</label>
85
+ <select id="node-input-room" style="width: 70%;"></select>
86
+ </div>
87
+ <div class="form-row">
88
+ <label for="node-input-device">Room</label>
89
+ <select id="node-input-device" style="width: 70%;"></select>
90
+ </div>
91
+ </script>
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ ;
3
+ module.exports = (RED) => {
4
+ function AliceGet(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const service = RED.nodes.getNode(config.service);
7
+ }
8
+ ;
9
+ RED.nodes.registerType("Alice-Get", AliceGet);
10
+ };
@@ -4,7 +4,7 @@
4
4
  defaults:{
5
5
  device: {value:"", type:"alice-device"},
6
6
  name: {value:""},
7
- retrievable: {value:true},
7
+ // retrievable: {value:true},
8
8
  response:{value:true},
9
9
  split: {value:false}
10
10
  },
@@ -5,7 +5,6 @@ function AliceOnOff(config){
5
5
  const device = RED.nodes.getNode(config.device);
6
6
  device.setMaxListeners(device.getMaxListeners() + 1); // увеличиваем лимит для event
7
7
  const id =JSON.parse(JSON.stringify(this.id));
8
- const name = config.name;
9
8
  const ctype = 'devices.capabilities.on_off';
10
9
  const instance = 'on';
11
10
  let response = config.response;
@@ -22,8 +21,8 @@ function AliceOnOff(config){
22
21
  if (config.response === undefined){
23
22
  response = true;
24
23
  };
25
- if (!config.retrievable){
26
- split = true;
24
+ if (config.split === undefined){
25
+ split = false;
27
26
  };
28
27
 
29
28
  this.status({fill:"red",shape:"dot",text:"offline"});
@@ -214,8 +214,6 @@
214
214
  .find('option')
215
215
  .remove()
216
216
  .end()
217
- // .append('<option value="unit.gigacalorie">Gcal</option>')
218
- // .val('unit.gigacalorie')
219
217
  .prop('disabled', 'disabled');
220
218
  break;
221
219
  default:
@@ -64,7 +64,7 @@ function AliceSensor(config){
64
64
  if (done) {done();}
65
65
  return;
66
66
  };
67
- if (unit == 'unit.temperature.celsius'){
67
+ if (unit == 'unit.temperature.celsius' || unit == 'unit.ampere'){
68
68
  msg.payload = +msg.payload.toFixed(1);
69
69
  }else {
70
70
  msg.payload = +msg.payload.toFixed(0);
package/nodes/alice.js CHANGED
@@ -1,125 +1,137 @@
1
- const mqtt = require('mqtt');
2
- const axios = require('axios');
3
-
4
- module.exports = function(RED) {
5
- //Sevice node, Alice-Service (credential)
6
- function AliceService(config) {
7
- RED.nodes.createNode(this,config);
8
- this.debug("Starting Alice service...");
9
-
10
- const email = this.credentials.email;
11
- const login = this.credentials.id;
12
- const password = this.credentials.password;
13
- const token = this.credentials.token;
14
-
15
- const suburl = Buffer.from(email).toString('base64');
16
- RED.httpAdmin.get("/noderedhome/"+suburl+"/clearalldevice",(req,res)=>{
17
- const option = {
18
- method: 'POST',
19
- url: 'https://api.nodered-home.ru/gtw/device/clearallconfigs',
20
- headers: {
21
- 'content-type': 'application/json',
22
- 'Authorization': "Bearer "+this.getToken()
23
- },
24
- data: {}
25
- };
26
- axios.request(option)
27
- .then(result=>{
28
- this.trace("All devices configs deleted on gateway successfully");
29
- // console.log(result)
30
- res.sendStatus(200);
31
- })
32
- .catch(error=>{
33
- this.debug("Error when delete All devices configs deleted on gateway: "+error.message);
34
- res.sendStatus(500);
35
- });
36
- });
37
-
38
- this.isOnline = false;
39
- if (!token){
40
- this.error("Authentication is required!!!");
41
- return;
42
- };
43
- const mqttClient = mqtt.connect("mqtts://mqtt.cloud.yandex.net",{
44
- port: 8883,
45
- clientId: login,
46
- rejectUnauthorized: false,
47
- username: login,
48
- password: password,
49
- reconnectPeriod: 10000
50
- });
51
- mqttClient.on("message",(topic, payload)=>{
52
- const arrTopic = topic.split('/');
53
- const data = JSON.parse(payload);
54
- this.trace("Incoming:" + topic +" timestamp:"+new Date().getTime());
55
- if (payload.length && typeof data === 'object'){
56
- if (arrTopic[3]=='message'){
57
- this.warn(data.text);
58
- }else{
59
- this.emit(arrTopic[3],data);
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ const axios_1 = __importDefault(require("axios"));
6
+ const mqtt_1 = __importDefault(require("mqtt"));
7
+ ;
8
+ ;
9
+ ;
10
+ module.exports = (RED) => {
11
+ function AliceService(config) {
12
+ RED.nodes.createNode(this, config);
13
+ this.debug("Starting Alice service... ID: " + this.id);
14
+ const email = this.credentials.email;
15
+ const login = this.credentials.id;
16
+ const password = this.credentials.password;
17
+ const token = this.credentials.token;
18
+ const suburl = Buffer.from(email).toString('base64');
19
+ RED.httpAdmin.get("/noderedhome/" + suburl + "/clearalldevice", (req, res) => {
20
+ const option = {
21
+ method: 'POST',
22
+ url: 'https://api.nodered-home.ru/gtw/device/clearallconfigs',
23
+ headers: {
24
+ 'content-type': 'application/json',
25
+ 'Authorization': "Bearer " + this.getToken()
26
+ },
27
+ data: {}
28
+ };
29
+ axios_1.default.request(option)
30
+ .then(result => {
31
+ this.trace("All devices configs deleted on gateway successfully");
32
+ res.sendStatus(200);
33
+ })
34
+ .catch(error => {
35
+ this.debug("Error when delete All devices configs deleted on gateway: " + error.message);
36
+ res.sendStatus(500);
37
+ });
38
+ });
39
+ RED.httpAdmin.get("/noderedhome/" + this.id + "/getfullconfig", (req, res) => {
40
+ const option = {
41
+ method: 'GET',
42
+ url: 'https://api.iot.yandex.net/v1.0/user/info',
43
+ headers: {
44
+ 'content-type': 'application/json',
45
+ 'Authorization': "Bearer " + this.getToken()
46
+ }
47
+ };
48
+ axios_1.default.request(option)
49
+ .then(result => {
50
+ this.trace("Full Alice SmartHome config successfully retrieved");
51
+ res.json(result.data);
52
+ })
53
+ .catch(error => {
54
+ this.debug("Error when retrieve Alice SmartHome config: " + error.message);
55
+ res.sendStatus(500);
56
+ });
57
+ });
58
+ this.isOnline = false;
59
+ if (!token) {
60
+ this.error("Authentication is required!!!");
61
+ return;
62
+ }
63
+ ;
64
+ const mqttClient = mqtt_1.default.connect("mqtts://mqtt.cloud.yandex.net", {
65
+ port: 8883,
66
+ clientId: login,
67
+ rejectUnauthorized: false,
68
+ username: login,
69
+ password: password,
70
+ reconnectPeriod: 10000
71
+ });
72
+ mqttClient.on("message", (topic, payload) => {
73
+ const arrTopic = topic.split('/');
74
+ const data = JSON.parse(payload);
75
+ this.trace("Incoming:" + topic + " timestamp:" + new Date().getTime());
76
+ if (payload.length && typeof data === 'object') {
77
+ if (arrTopic[3] == 'message') {
78
+ this.warn(data.text);
79
+ }
80
+ else {
81
+ this.emit(arrTopic[3], data);
82
+ }
83
+ ;
84
+ }
85
+ });
86
+ mqttClient.on("connect", () => {
87
+ this.debug("Yandex IOT client connected. ");
88
+ this.emit('online');
89
+ mqttClient.subscribe("$me/device/commands/+", _ => {
90
+ this.debug("Yandex IOT client subscribed to the command");
91
+ });
92
+ });
93
+ mqttClient.on("offline", () => {
94
+ this.debug("Yandex IOT client offline. ");
95
+ this.emit('offline');
96
+ });
97
+ mqttClient.on("disconnect", () => {
98
+ this.debug("Yandex IOT client disconnect.");
99
+ this.emit('offline');
100
+ });
101
+ mqttClient.on("reconnect", () => {
102
+ this.debug("Yandex IOT client reconnecting ...");
103
+ });
104
+ mqttClient.on("error", (err) => {
105
+ this.error("Yandex IOT client Error: " + err.message);
106
+ this.emit('offline');
107
+ });
108
+ this.on('offline', () => {
109
+ this.isOnline = false;
110
+ });
111
+ this.on('online', () => {
112
+ this.isOnline = true;
113
+ });
114
+ this.on('close', (done) => {
115
+ this.emit('offline');
116
+ setTimeout(() => {
117
+ mqttClient.end(false, done);
118
+ }, 500);
119
+ });
120
+ this.send2gate = (path, data, retain) => {
121
+ this.trace("Outgoing: " + path);
122
+ mqttClient.publish(path, data, { qos: 0, retain: retain });
123
+ };
124
+ this.getToken = () => {
125
+ return JSON.parse(token).access_token;
60
126
  };
61
- }
62
- });
63
- mqttClient.on("connect",()=>{
64
- this.debug("Yandex IOT client connected. ");
65
- this.emit('online');
66
- // Подписываемся на получение комманд
67
- mqttClient.subscribe("$me/device/commands/+",_=>{
68
- this.debug("Yandex IOT client subscribed to the command");
69
- });
70
- });
71
- mqttClient.on("offline",()=>{
72
- this.debug("Yandex IOT client offline. ");
73
- this.emit('offline');
74
- });
75
- mqttClient.on("disconnect",()=>{
76
- this.debug("Yandex IOT client disconnect.");
77
- this.emit('offline');
78
- });
79
- mqttClient.on("reconnect",(err)=>{
80
- this.debug("Yandex IOT client reconnecting ...");
81
- });
82
- mqttClient.on("error",(err)=>{
83
- this.error("Yandex IOT client Error: "+ err.message);
84
- this.emit('offline');
85
- });
86
-
87
- this.on('offline', ()=>{
88
- this.isOnline = false;
89
- })
90
-
91
- this.on('online', ()=>{
92
- this.isOnline = true;
93
- })
94
-
95
- this.on('close',(done)=>{
96
- this.emit('offline');
97
- setTimeout(()=>{
98
- mqttClient.end(false,done);
99
- },500)
100
- });
101
-
102
- this.send2gate= (path,data,retain)=>{
103
- // this.debug(path);
104
- // this.debug(data);
105
- this.trace("Outgoing: "+path);
106
- mqttClient.publish(path, data ,{ qos: 0, retain: retain });
107
- }
108
-
109
- this.getToken = ()=>{
110
- return JSON.parse(token).access_token;
111
- }
112
-
113
- };
114
- RED.nodes.registerType("alice-service",AliceService,{
115
- credentials: {
116
- email: {type: "text"},
117
- password: {type: "password"},
118
- token: {type: "password"},
119
- id:{type:"text"}
120
127
  }
121
- });
128
+ ;
129
+ RED.nodes.registerType("alice-service", AliceService, {
130
+ credentials: {
131
+ email: { type: "text" },
132
+ password: { type: "password" },
133
+ token: { type: "password" },
134
+ id: { type: "text" }
135
+ }
136
+ });
122
137
  };
123
-
124
-
125
-
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "node-red-contrib-alice",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "",
5
- "main": "alice.js",
6
5
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
6
+ "start": "npm run build && node-red",
7
+ "build": "tsc && npm run copy-html",
8
+ "copy-html": "cp ./src/*.html ./nodes/"
8
9
  },
9
10
  "repository": {
10
11
  "type": "git",
@@ -44,5 +45,35 @@
44
45
  "dependencies": {
45
46
  "axios": "^1.4.0",
46
47
  "mqtt": "^4.3.8"
48
+ },
49
+ "devDependencies": {
50
+ "@types/axios": "^0.14.0",
51
+ "@types/mqtt": "^2.5.0",
52
+ "@types/node": "^20.11.16",
53
+ "@types/node-red": "^1.3.4",
54
+ "@typescript-eslint/eslint-plugin": "^6.20.0",
55
+ "@typescript-eslint/parser": "^6.20.0",
56
+ "eslint": "^8.56.0",
57
+ "nodemon": "^3.0.3",
58
+ "typescript": "^5.3.3"
59
+ },
60
+ "nodemonConfig": {
61
+ "ignoreRoot" : [".git", "test"],
62
+ "restartable": "rs",
63
+ "ignore": [
64
+ "node_modules/**/node_modules",
65
+ "node_modules/**/test",
66
+ "*.log"
67
+ ],
68
+ "verbose": true,
69
+ "delay": "1000",
70
+ "events": {
71
+ "restart": "echo ------ restarted due to: $FILENAME ------"
72
+ },
73
+ "watch": [
74
+ "src/",
75
+ "node_modules/node-red-*"
76
+ ],
77
+ "ext": "js json htm html css"
47
78
  }
48
79
  }
@@ -0,0 +1,91 @@
1
+ <script type="text/javascript">
2
+ let udyaconfig={};
3
+ RED.nodes.registerType('Alice-Get',{
4
+ category: 'alice',
5
+ inputs:1,
6
+ outputs:1,
7
+ icon: "alice.png",
8
+ color: "#D8BFD8",
9
+ defaults:{
10
+ service: {value:"", type:"alice-service"},
11
+ device: {value:undefined}
12
+ },
13
+ label: function(){
14
+ return this.name || "Alice-Get";
15
+ },
16
+ oneditprepare: ()=>{
17
+ $("#node-input-service").change(function(e){
18
+ if (this.value &&this.value!='_ADD_'){
19
+ $.ajax({
20
+ url: "/noderedhome/"+this.value+"/getfullconfig",
21
+ type:"GET"
22
+ })
23
+ .done(result=>{
24
+ // RED.notify("Full Alice SmartHome config successfully retrieved", {type:"success"});
25
+ console.log(result);
26
+ udyaconfig=result;
27
+ updateHouse(result.households);
28
+ })
29
+ .fail(error=>{
30
+ RED.notify("Error when retrieve Alice SmartHome config", {type:"error"});
31
+ });
32
+ }
33
+ });
34
+ $('#node-input-home')
35
+ .prop('disabled', 'disabled')
36
+ .change((e)=>{
37
+ let val = $('#node-input-home').find(":selected").val();
38
+ console.log(val);
39
+ });
40
+
41
+ $('#node-input-room').prop('disabled', 'disabled');
42
+ $('#node-input-device').prop('disabled', 'disabled');
43
+
44
+ }
45
+ });
46
+ function updateHouse(data){
47
+ $('#node-input-home')
48
+ .find('option')
49
+ .remove()
50
+ .end();
51
+ udyaconfig.households.forEach(h => {
52
+ $('#node-input-home')
53
+ .append('<option value="'+h.id+'">'+h.name+'</option>');
54
+ });
55
+ $('#node-input-home')
56
+ .prop('disabled', false);
57
+ };
58
+ function updaterooms(house){
59
+ $('#node-input-room')
60
+ .find('option')
61
+ .remove()
62
+ .end();
63
+ udyaconfig.households.forEach(h => {
64
+ if (h.household_id==house){
65
+ $('#node-input-room')
66
+ .append('<option value="'+h.id+'">'+h.name+'</option>');
67
+ }
68
+ });
69
+ $('#node-input-room').prop('disabled', false);
70
+ };
71
+
72
+ </script>
73
+
74
+ <script type="text/x-red" data-template-name="Alice-Get">
75
+ <div class="form-row">
76
+ <label for="node-input-service">Account</label>
77
+ <input id="node-input-service">
78
+ </div>
79
+ <div class="form-row">
80
+ <label for="node-input-home">Home</label>
81
+ <select id="node-input-home" style="width: 70%;"></select>
82
+ </div>
83
+ <div class="form-row">
84
+ <label for="node-input-room">Room</label>
85
+ <select id="node-input-room" style="width: 70%;"></select>
86
+ </div>
87
+ <div class="form-row">
88
+ <label for="node-input-device">Room</label>
89
+ <select id="node-input-device" style="width: 70%;"></select>
90
+ </div>
91
+ </script>