node-red-contrib-alice 2.2.5 → 2.3.3

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.
@@ -0,0 +1,227 @@
1
+ import { NodeAPI, Node } from "node-red";
2
+ import { AliceColorConfig, AliceDeviceNode, CapabilityState, CapabilityRegistration } from "./types.js";
3
+
4
+ interface RGBColor {
5
+ r: number;
6
+ g: number;
7
+ b: number;
8
+ }
9
+
10
+ interface HSVColor {
11
+ h: number;
12
+ s: number;
13
+ v: number;
14
+ }
15
+
16
+ export = (RED: NodeAPI): void => {
17
+ function AliceColor(this: Node, config: AliceColorConfig): void {
18
+ RED.nodes.createNode(this, config);
19
+ const device = RED.nodes.getNode(config.device) as AliceDeviceNode;
20
+ device.setMaxListeners(device.getMaxListeners() + 1);
21
+
22
+ const ctype = 'devices.capabilities.color_setting';
23
+ let scheme = config.scheme;
24
+ const temperature_k = config.temperature_k;
25
+ const temperature_min = parseInt(config.temperature_min);
26
+ const temperature_max = parseInt(config.temperature_max);
27
+ const color_scene = config.color_scene || [];
28
+ let needConvert = false;
29
+ let response = config.response;
30
+ let color_support = config.color_support;
31
+ let lastValue: string | undefined;
32
+
33
+ if (scheme == "rgb_normal") {
34
+ scheme = "rgb";
35
+ needConvert = true;
36
+ }
37
+ if (config.response === undefined) {
38
+ response = true;
39
+ }
40
+ if (config.color_support === undefined) {
41
+ color_support = true;
42
+ }
43
+
44
+ const init = (): void => {
45
+ const parameters: Record<string, any> = {};
46
+ const capab: CapabilityRegistration = {
47
+ type: ctype,
48
+ retrievable: true,
49
+ reportable: true,
50
+ parameters: parameters
51
+ };
52
+
53
+ if (!color_support && !temperature_k && color_scene.length < 1) {
54
+ this.error("Error on create capability: At least one parameter must be enabled");
55
+ this.status({ fill: "red", shape: "dot", text: "error" });
56
+ return;
57
+ }
58
+
59
+ if (color_scene.length > 0) {
60
+ const scenes = color_scene.map(s => ({ id: s }));
61
+ parameters.color_scene = { scenes: scenes };
62
+ }
63
+
64
+ if (color_support) {
65
+ parameters.color_model = scheme;
66
+ }
67
+
68
+ if (temperature_k) {
69
+ parameters.temperature_k = {
70
+ min: temperature_min,
71
+ max: temperature_max
72
+ };
73
+ }
74
+
75
+ device.setCapability(this.id, capab)
76
+ .then(() => {
77
+ this.status({ fill: "green", shape: "dot", text: "online" });
78
+ })
79
+ .catch(err => {
80
+ this.error("Error on create capability: " + err.message);
81
+ this.status({ fill: "red", shape: "dot", text: "error" });
82
+ });
83
+ };
84
+
85
+ if (device.initState) init();
86
+
87
+ device.on("online", () => {
88
+ init();
89
+ });
90
+
91
+ device.on("offline", () => {
92
+ this.status({ fill: "red", shape: "dot", text: "offline" });
93
+ });
94
+
95
+ device.on(this.id, (val: any, newstate: { instance: string }) => {
96
+ // отправляем данные на выход
97
+ const outmsgs: Array<{ payload: any } | null> = [null, null, null];
98
+ switch (newstate.instance) {
99
+ case 'rgb': {
100
+ const value: RGBColor = {
101
+ r: val >> 16,
102
+ g: val >> 8 & 0xFF,
103
+ b: val & 0xFF
104
+ };
105
+ outmsgs[0] = { payload: value };
106
+ break;
107
+ }
108
+ case 'hsv':
109
+ outmsgs[0] = { payload: val };
110
+ break;
111
+ case 'temperature_k':
112
+ outmsgs[1] = { payload: val };
113
+ break;
114
+ case 'scene':
115
+ outmsgs[2] = { payload: val };
116
+ break;
117
+ }
118
+ this.send(outmsgs);
119
+
120
+ // возвращаем подтверждение в базу
121
+ const state: CapabilityState = {
122
+ type: ctype,
123
+ state: {
124
+ instance: newstate.instance,
125
+ value: val
126
+ }
127
+ };
128
+ if (response) {
129
+ device.updateCapabState(this.id, state)
130
+ .then(() => {
131
+ lastValue = JSON.stringify(val);
132
+ this.status({ fill: "green", shape: "dot", text: "online" });
133
+ })
134
+ .catch(err => {
135
+ this.error("Error on update capability state: " + err.message);
136
+ this.status({ fill: "red", shape: "dot", text: "Error" });
137
+ });
138
+ }
139
+ });
140
+
141
+ this.on('input', (msg, _send, done) => {
142
+ let value: any = msg.payload;
143
+ const state: { value?: any; instance?: string } = {};
144
+
145
+ switch (typeof value) {
146
+ case 'object': {
147
+ const obj = value as RGBColor | HSVColor;
148
+ if (('r' in obj && 'g' in obj && 'b' in obj) || ('h' in obj && 's' in obj && 'v' in obj)) {
149
+ if (scheme == 'rgb' && 'r' in obj) {
150
+ value = (obj as RGBColor).r << 16 | (obj as RGBColor).g << 8 | (obj as RGBColor).b;
151
+ }
152
+ state.value = value;
153
+ state.instance = scheme;
154
+ } else {
155
+ this.error("Wrong type! For Color, msg.payload must be RGB or HSV Object.");
156
+ if (done) { done(); }
157
+ return;
158
+ }
159
+ break;
160
+ }
161
+ case 'number': {
162
+ value = Math.round(value as number);
163
+ if (value >= temperature_min && value <= temperature_max) {
164
+ state.value = value;
165
+ state.instance = 'temperature_k';
166
+ } else {
167
+ this.error("Wrong type! For Temperature_k, msg.payload must be >=MIN and <=MAX.");
168
+ if (done) { done(); }
169
+ return;
170
+ }
171
+ break;
172
+ }
173
+ case 'string':
174
+ if (color_scene.includes(value as string)) {
175
+ state.value = value;
176
+ state.instance = 'scene';
177
+ } else {
178
+ this.error("Wrong type! For the Scene, the msg.payload must be set in the settings");
179
+ if (done) { done(); }
180
+ return;
181
+ }
182
+ break;
183
+ default:
184
+ this.error("Wrong type! Unsupported msg.payload type");
185
+ if (done) { done(); }
186
+ return;
187
+ }
188
+
189
+ if (JSON.stringify(value) === lastValue) {
190
+ this.debug("Value not changed. Cancel update");
191
+ if (done) { done(); }
192
+ return;
193
+ }
194
+
195
+ const upState: CapabilityState = {
196
+ type: ctype,
197
+ state: state as CapabilityState['state']
198
+ };
199
+ device.updateCapabState(this.id, upState)
200
+ .then(() => {
201
+ lastValue = JSON.stringify(value);
202
+ this.status({ fill: "green", shape: "dot", text: JSON.stringify(msg.payload) });
203
+ if (done) { done(); }
204
+ })
205
+ .catch(err => {
206
+ this.error("Error on update capability state: " + err.message);
207
+ this.status({ fill: "red", shape: "dot", text: "Error" });
208
+ if (done) { done(); }
209
+ });
210
+ });
211
+
212
+ this.on('close', (removed: boolean, done: () => void) => {
213
+ if (removed) {
214
+ device.delCapability(this.id)
215
+ .then(() => { done(); })
216
+ .catch(err => {
217
+ this.error("Error on delete capability: " + err.message);
218
+ done();
219
+ });
220
+ } else {
221
+ done();
222
+ }
223
+ });
224
+ }
225
+
226
+ RED.nodes.registerType("Color", AliceColor);
227
+ };
@@ -0,0 +1,94 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('alice-device',{
3
+ category: 'config',
4
+ defaults:{
5
+ service: {value:"", type:"alice-service"},
6
+ name: {value:null, required: true},
7
+ description: {value:null},
8
+ room: {value:"Комната"},
9
+ dtype: {value:"devices.types.light", required: true}
10
+ },
11
+ label: function(){
12
+ return this.room+":"+this.name || "Alice-Device";
13
+ },
14
+ oneditprepare: ()=>{
15
+ $("#node-config-input-dtype").typedInput({
16
+ types: [
17
+ {
18
+ options: [
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" },
22
+ { value: "devices.types.socket", label: "Socket" },
23
+ { value: "devices.types.switch", label: "Switch" },
24
+ { value: "devices.types.thermostat", label: "Thermostat" },
25
+ { value: "devices.types.thermostat.ac", label: "Air conditioning" },
26
+ { value: "devices.types.media_device", label: "Multimedia" },
27
+ { value: "devices.types.media_device.tv", label: "TV" },
28
+ { value: "devices.types.media_device.tv_box", label: "TV Box" },
29
+ { value: "devices.types.media_device.receiver", label: "AV Receiver" },
30
+ { value: "devices.types.camera", label: "Camera" },
31
+ { value: "devices.types.cooking", label: "Kitchen appliances" },
32
+ { value: "devices.types.cooking.coffee_maker", label: "Coffee machine" },
33
+ { value: "devices.types.cooking.kettle", label: "Smart kettle" },
34
+ { value: "devices.types.cooking.multicooker", label: "Multicooker" },
35
+ { value: "devices.types.openable", label: "Door, gate, window, shutters" },
36
+ { value: "devices.types.openable.curtain", label: "Curtains, blinds" },
37
+ { value: "devices.types.openable.valve", label: "Valve (ball valve)" },
38
+ { value: "devices.types.humidifier", label: "Humidifier" },
39
+ { value: "devices.types.purifier", label: "Air purifier" },
40
+ { value: "devices.types.ventilation", label: "Ventilation" },
41
+ { value: "devices.types.ventilation.fan", label: "Fan" },
42
+ { value: "devices.types.vacuum_cleaner", label: "Vacuum cleaner robot" },
43
+ { value: "devices.types.washing_machine", label: "Washing machine" },
44
+ { value: "devices.types.dishwasher", label: "Dishwasher" },
45
+ { value: "devices.types.iron", label: "Iron, steam generator" },
46
+ { value: "devices.types.sensor", label: "Sensor" },
47
+ { value: "devices.types.sensor.motion", label: "Sensor motion" },
48
+ { value: "devices.types.sensor.vibration", label: "Sensor vibration" },
49
+ { value: "devices.types.sensor.illumination", label: "Sensor illumination" },
50
+ { value: "devices.types.sensor.open", label: "Sensor open" },
51
+ { value: "devices.types.sensor.climate", label: "Sensor climate" },
52
+ { value: "devices.types.sensor.water_leak", label: "Sensor water_leak" },
53
+ { value: "devices.types.sensor.button", label: "Sensor button" },
54
+ { value: "devices.types.sensor.gas", label: "Sensor gas" },
55
+ { value: "devices.types.sensor.smoke", label: "Sensor smoke" },
56
+ { value: "devices.types.smart_meter", label: "Counter" },
57
+ { value: "devices.types.smart_meter.cold_water", label: "Cold Water counter" },
58
+ { value: "devices.types.smart_meter.hot_water", label: "Hot Water counter" },
59
+ { value: "devices.types.smart_meter.electricity", label: "Electricity counter" },
60
+ { value: "devices.types.smart_meter.gas", label: "Gas counter" },
61
+ { value: "devices.types.smart_meter.heat", label: "Heat Water counter" },
62
+ { value: "devices.types.pet_drinking_fountain", label: "Pet drinking fountain" },
63
+ { value: "devices.types.pet_feeder", label: "Pet feeder" },
64
+ { value: "devices.types.other", label: "Other" },
65
+ ]
66
+ }
67
+ ]
68
+ });
69
+ }
70
+ })
71
+ </script>
72
+
73
+ <script type="text/x-red" data-template-name="alice-device">
74
+ <div class="form-row">
75
+ <label for="node-config-input-service">Credentials</label>
76
+ <input id="node-config-input-service">
77
+ </div>
78
+ <div class="form-row">
79
+ <label for="node-config-input-name">Name</label>
80
+ <input type="text" id="node-config-input-name" placeholder="Name">
81
+ </div>
82
+ <div class="form-row">
83
+ <label for="node-config-input-description">Description</label>
84
+ <input type="text" id="node-config-input-description" placeholder="">
85
+ </div>
86
+ <div class="form-row">
87
+ <label for="node-config-input-room">Room</label>
88
+ <input type="text" id="node-config-input-room" placeholder="Room">
89
+ </div>
90
+ <div class="form-row">
91
+ <label for="node-config-input-dtype">Type</label>
92
+ <input type="text" id="node-config-input-dtype">
93
+ </div>
94
+ </script>
@@ -0,0 +1,301 @@
1
+ import { NodeAPI } from "node-red";
2
+ import axios from "axios";
3
+ import pjson from "../package.json";
4
+ import {
5
+ AliceDeviceConfig,
6
+ AliceDeviceNode,
7
+ AliceServiceNode,
8
+ CapabilityRegistration,
9
+ CapabilityState,
10
+ SensorRegistration,
11
+ SensorState,
12
+ DeviceConfig,
13
+ DeviceStates
14
+ } from "./types.js";
15
+
16
+ export = (RED: NodeAPI): void => {
17
+ function AliceDevice(this: AliceDeviceNode, config: AliceDeviceConfig): void {
18
+ RED.nodes.createNode(this, config);
19
+ const service = RED.nodes.getNode(config.service) as AliceServiceNode;
20
+ service.setMaxListeners(service.getMaxListeners() + 1);
21
+
22
+ this.initState = false;
23
+ let updating = false;
24
+ let needSendEvent = false;
25
+ const capabilites: Record<string, string> = {};
26
+ const sensors: Record<string, string> = {};
27
+
28
+ const deviceconfig: DeviceConfig = {
29
+ id: this.id,
30
+ name: config.name,
31
+ description: config.description,
32
+ room: config.room,
33
+ type: config.dtype,
34
+ device_info: {
35
+ manufacturer: "NodeRed Home",
36
+ model: "virtual device",
37
+ sw_version: pjson.version
38
+ },
39
+ capabilities: [],
40
+ properties: []
41
+ };
42
+
43
+ const states: DeviceStates = {
44
+ id: this.id,
45
+ capabilities: [],
46
+ properties: []
47
+ };
48
+
49
+ if (service.isOnline) {
50
+ this.emit("online");
51
+ this.initState = true;
52
+ }
53
+
54
+ // функция обновления информации об устройстве
55
+ const _updateDeviceInfo = (): void => {
56
+ if (deviceconfig.capabilities.length == 0 && deviceconfig.properties.length == 0) {
57
+ this.debug("DELETE Device config from gateway ...");
58
+ axios.request({
59
+ timeout: 5000,
60
+ method: 'POST',
61
+ url: 'https://api.nodered-home.ru/gtw/device/config',
62
+ headers: {
63
+ 'content-type': 'application/json',
64
+ 'Authorization': "Bearer " + service.getToken()
65
+ },
66
+ data: {
67
+ id: this.id,
68
+ config: deviceconfig
69
+ }
70
+ })
71
+ .then(() => {
72
+ this.trace("Device config deleted on gateway successfully");
73
+ })
74
+ .catch(error => {
75
+ this.debug("Error when delete device config on gateway: " + error.message);
76
+ });
77
+ return;
78
+ }
79
+
80
+ if (!updating) {
81
+ updating = true;
82
+ setTimeout(() => {
83
+ this.debug("Updating Device config ...");
84
+ updating = false;
85
+ axios.request({
86
+ timeout: 5000,
87
+ method: 'POST',
88
+ url: 'https://api.nodered-home.ru/gtw/device/config',
89
+ headers: {
90
+ 'content-type': 'application/json',
91
+ 'Authorization': "Bearer " + service.getToken()
92
+ },
93
+ data: {
94
+ id: this.id,
95
+ config: deviceconfig
96
+ }
97
+ })
98
+ .then(() => {
99
+ this.trace("Device config updated successfully");
100
+ })
101
+ .catch(error => {
102
+ this.debug("Error when update device config: " + error.message);
103
+ });
104
+ }, 1000);
105
+ }
106
+ };
107
+
108
+ // функция обновления состояния устройства (умений и сенсоров)
109
+ const _updateDeviceState = (event: Record<string, any> | null = null): void => {
110
+ axios.request({
111
+ timeout: 5000,
112
+ method: 'POST',
113
+ url: 'https://api.nodered-home.ru/gtw/device/state',
114
+ headers: {
115
+ 'content-type': 'application/json',
116
+ 'Authorization': "Bearer " + service.getToken()
117
+ },
118
+ data: {
119
+ id: this.id,
120
+ event: event,
121
+ state: states
122
+ }
123
+ })
124
+ .then(() => {
125
+ this.trace("Device state updated successfully");
126
+ })
127
+ .catch(error => {
128
+ this.debug("Error when update device state: " + error.message);
129
+ });
130
+ };
131
+
132
+ // отправка эвентов
133
+ const _sendEvent = (event: CapabilityState): void => {
134
+ const data = JSON.stringify(event);
135
+ service.send2gate('$me/device/events/' + this.id, data, false);
136
+ };
137
+
138
+ // Установка параметров умения
139
+ this.setCapability = (capId: string, capab: CapabilityRegistration): Promise<boolean> => {
140
+ return new Promise((resolve, reject) => {
141
+ const instance = capab.parameters.instance || '';
142
+ const capabIndex = capab.type + "." + instance;
143
+ if (capabilites[capabIndex] && capabilites[capabIndex] != capId) {
144
+ reject(new Error("Dublicated capability on same device!"));
145
+ return;
146
+ }
147
+ // проверям было ли такое умение раньше и удаляем перед обновлением
148
+ if (deviceconfig.capabilities.findIndex(a => a.id === capId) > -1) {
149
+ this.delCapability(capId);
150
+ }
151
+ capabilites[capabIndex] = capId;
152
+ capab.id = capId;
153
+ deviceconfig.capabilities.push(capab);
154
+ _updateDeviceInfo();
155
+ resolve(true);
156
+ });
157
+ };
158
+
159
+ // Установка параметров сенсора
160
+ this.setSensor = (sensId: string, sensor: SensorRegistration): Promise<boolean> => {
161
+ return new Promise((resolve, reject) => {
162
+ const sensorIndex = sensor.type + "." + sensor.parameters.instance;
163
+ if (sensors[sensorIndex] && sensors[sensorIndex] != sensId) {
164
+ reject(new Error("Dublicated sensor on same device!"));
165
+ return;
166
+ }
167
+ // проверяем было ли такой сенсор раньше и удаляем перед обновлением
168
+ if (deviceconfig.properties.findIndex(a => a.id === sensId) > -1) {
169
+ this.delSensor(sensId);
170
+ }
171
+ sensors[sensorIndex] = sensId;
172
+ sensor.id = sensId;
173
+ deviceconfig.properties.push(sensor);
174
+ _updateDeviceInfo();
175
+ resolve(true);
176
+ });
177
+ };
178
+
179
+ // обновление текущего state умения
180
+ this.updateCapabState = (capId: string, state: CapabilityState): Promise<boolean> => {
181
+ return new Promise((resolve) => {
182
+ state.id = capId;
183
+ if (needSendEvent) {
184
+ _sendEvent(state);
185
+ }
186
+ const index = states.capabilities.findIndex(a => a.id === capId);
187
+ if (index > -1) {
188
+ states.capabilities.splice(index, 1);
189
+ }
190
+ states.capabilities.push(state);
191
+ const currentevent = {
192
+ id: this.id,
193
+ capabilities: [state]
194
+ };
195
+ _updateDeviceState(currentevent);
196
+ resolve(true);
197
+ });
198
+ };
199
+
200
+ // обновление текущего state сенсора
201
+ this.updateSensorState = (sensID: string, state: SensorState): Promise<boolean> => {
202
+ return new Promise((resolve) => {
203
+ state.id = sensID;
204
+ const index = states.properties.findIndex(a => a.id === sensID);
205
+ if (index > -1) {
206
+ states.properties.splice(index, 1);
207
+ }
208
+ states.properties.push(state);
209
+ const currentevent = {
210
+ id: this.id,
211
+ properties: [state]
212
+ };
213
+ _updateDeviceState(currentevent);
214
+ resolve(true);
215
+ });
216
+ };
217
+
218
+ // удаление умения
219
+ this.delCapability = (capId: string): Promise<boolean> => {
220
+ return new Promise((resolve) => {
221
+ const index = deviceconfig.capabilities.findIndex(a => a.id === capId);
222
+ if (index > -1) {
223
+ deviceconfig.capabilities.splice(index, 1);
224
+ }
225
+ const capabIndex = Object.keys(capabilites).find(key => capabilites[key] === capId);
226
+ if (capabIndex) delete capabilites[capabIndex];
227
+ _updateDeviceInfo();
228
+ const stateindex = states.capabilities.findIndex(a => a.id === capId);
229
+ if (stateindex > -1) {
230
+ states.capabilities.splice(stateindex, 1);
231
+ }
232
+ _updateDeviceState();
233
+ resolve(true);
234
+ });
235
+ };
236
+
237
+ // удаление сенсора
238
+ this.delSensor = (sensID: string): Promise<boolean> => {
239
+ return new Promise((resolve) => {
240
+ const index = deviceconfig.properties.findIndex(a => a.id === sensID);
241
+ if (index > -1) {
242
+ deviceconfig.properties.splice(index, 1);
243
+ }
244
+ const sensorIndex = Object.keys(sensors).find(key => sensors[key] === sensID);
245
+ if (sensorIndex) delete sensors[sensorIndex];
246
+ _updateDeviceInfo();
247
+ const stateindex = states.properties.findIndex(a => a.id === sensID);
248
+ if (stateindex > -1) {
249
+ states.properties.splice(stateindex, 1);
250
+ }
251
+ _updateDeviceState();
252
+ resolve(true);
253
+ });
254
+ };
255
+
256
+ service.on("online", () => {
257
+ this.debug("Received a signal online from the service");
258
+ this.emit("online");
259
+ this.initState = true;
260
+ });
261
+
262
+ service.on("offline", () => {
263
+ this.debug("Received a signal offline from the service");
264
+ this.emit("offline");
265
+ this.initState = false;
266
+ this.status({ fill: "red", shape: "dot", text: "offline" });
267
+ });
268
+
269
+ service.on(this.id, (incomingStates: any[]) => {
270
+ setTimeout(() => {
271
+ needSendEvent = false;
272
+ }, 2000);
273
+ needSendEvent = true;
274
+ incomingStates.forEach(cap => {
275
+ let capabIndex = cap.type + "." + cap.state.instance;
276
+ if (cap.type === "devices.capabilities.color_setting") {
277
+ capabIndex = cap.type + ".";
278
+ }
279
+ const capId = capabilites[capabIndex];
280
+ this.emit(capId, cap.state.value, cap.state);
281
+ });
282
+ });
283
+
284
+ this.on('close', (removed: boolean, done: () => void) => {
285
+ this.emit('offline');
286
+ if (removed) {
287
+ deviceconfig.capabilities = [];
288
+ deviceconfig.properties = [];
289
+ states.capabilities = [];
290
+ states.properties = [];
291
+ _updateDeviceState();
292
+ _updateDeviceInfo();
293
+ }
294
+ setTimeout(() => {
295
+ done();
296
+ }, 500);
297
+ });
298
+ }
299
+
300
+ RED.nodes.registerType("alice-device", AliceDevice);
301
+ };