forked from cakebake/node-red-contrib-alexa-remote-cakebaked
-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathalexa-remote-smarthome.js
296 lines (254 loc) · 10.5 KB
/
alexa-remote-smarthome.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
const tools = require('../lib/common.js');
const util = require('util');
const convert = require('../lib/color-convert.js');
module.exports = function (RED) {
function AlexaRemoteSmarthome(input) {
RED.nodes.createNode(this, input);
tools.assign(this, ['config', 'outputs'], input);
tools.assignNode(RED, this, ['account'], input);
if(!tools.nodeSetup(this, input, true)) return;
this.on('input', function (msg) {
const send = tools.nodeGetSendCb(this, msg);
const error = tools.nodeGetErrorCb(this);
if(this.account.state.code !== 'READY') return error('Account not initialised!');
this.status({ shape: 'dot', fill: 'grey', text: 'sending' });
const alexa = this.account.alexa;
const option = this.config.option;
let value = this.config.value;
const invalid = (what) => error(`invalid input: "${JSON.stringify(what || this.config)}"`);
switch(option) {
case 'get': {
switch(value) {
case 'devices': return alexa.getSmarthomeDevicesPromise().then(send).catch(error);
case 'groups': return alexa.getSmarthomeGroupsPromise().then(send).catch(error);
case 'entities': return alexa.getSmarthomeEntitiesPromise().then(send).catch(error);
case 'definitions': return alexa.getSmarthomeBehaviourActionDefinitionsPromise().then(send).catch(error);
case 'simplified': return alexa.smarthomeSimplifiedByEntityIdExt ? send(alexa.smarthomeSimplifiedByEntityIdExt) : error('Not available?!');
default: return invalid();
}
}
case 'query': {
if(!Array.isArray(value)) return invalid();
// i don't know either (that's what amazon expects...)
const getIdForQuery = (entity) => entity.type === 'APPLIANCE' ? entity.applianceId : entity.entityId;
const queries = value.length === 0 ? msg.payload : value;
if(!tools.matches(queries, [{ entity: '' }])) {
return error(`query: input must be an array of objects with the string property "entity" and optionally "property"`);
}
const entities = queries.map(query => {
const entity = alexa.findSmarthomeEntityExt(query.entity);
if(!entity) error(`smarthome entity not found: "${query.entity}"`, query.entity);
return entity;
});
if(entities.includes(undefined)) return;
const requests = Array.from(new Set(entities)).map(entity => ({entityType: entity.type, entityId: getIdForQuery(entity)}));
return alexa.querySmarthomeDevicesExt(requests).then(response => {
if(tools.matches(response, {message: ''})) return error(`error response: ${response.message}`);
if(!tools.matches(response, {
deviceStates: [{ entity: { entityId: '', entityType: ''}, capabilityStates: ['']}],
errors: [{ entity: { entityId: '', entityType: '' }}]
})) {
return error(`unexpected response layout: "${JSON.stringify(response)}"`);
}
const stateById = new Map();
const errorById = new Map();
for(const state of response.deviceStates) {
const simplified = {};
simplified.id = state.entity.entityId;
simplified.type = state.entity.type;
if(state.error) simplified.error = state.error;
simplified.properties = state.capabilityStates
.map(json => tools.tryParseJson(json))
.filter(cap => tools.isObject(cap))
.reduce((o,cap) => (o[cap.name] = cap.value, o), {});
stateById.set(simplified.id, simplified);
}
for(const error of response.errors) {
errorById.set(error.entity.entityId, error);
}
// tools.log({states: stateById, errors: errorById});
const mapQueryToMsg = (entity, query, reportErrors = true) => {
if(!entity) {
return null;
}
if(entity.type === 'GROUP') {
const msg = {};
msg.id = entity.entityId;
msg.type = 'GROUP';
msg.topic = entity.name;
msg.payload = entity.children
.filter(e => e.type !== 'GROUP')
.map(e => mapQueryToMsg(e, query, false))
.filter(m => m);
return msg;
}
const id = getIdForQuery(entity);
const state = stateById.get(id);
if(!state) {
if(reportErrors) {
const errorObj = errorById.get(id) || { message: `no response for smarthome entity "${entity.name}" (${id})!`};
error(errorObj.message || errorObj.code || JSON.stringify(errorObj), errorObj);
}
return null;
}
if(query.property && query.property !== 'all') {
const msg = tools.clone(state);
msg.payload = msg.properties[query.property];
switch(query.property) {
case 'color': {
const native = msg.payload;
if(!tools.isObject(native)) break;
const hsv = [native.hue, native.saturation, native.brightness];
switch(query.format) {
case 'hex': msg.payload = convert.hsv2hex(hsv); break;
case 'rgb': msg.payload = convert.hsv2rgb(hsv); break;
case 'hsv': msg.payload = hsv; break;
}
break;
}
}
msg.topic = entity.name;
delete msg.properties;
return msg;
}
else {
const msg = tools.clone(state);
msg.payload = msg.properties;
msg.topic = entity.name;
delete msg.properties;
return msg;
}
}
const msgs = queries.map((query,i) => mapQueryToMsg(entities[i], query));
tools.nodeSendMultiple(RED, this, msg, msgs, this.outputs);
}).catch(error);
}
case 'action': {
if(!Array.isArray(value)) return invalid();
const inputs = value.length === 0 ? msg.payload : value.map(input => {
const result = {};
result.entity = input.entity;
result.action = input.action;
for(const key of ['value', 'scale']) {
const param = input[key];
if(!tools.isObject(param)) continue;
result[key] = RED.util.evaluateNodeProperty(param.value, param.type, this, msg);
}
return result;
});
if(!tools.matches(inputs, [{ entity: '', action: '' }])) {
return error(`action: input must be an array of objects with the string properties "entity" and "action"`);
}
const entities = inputs.map(input => {
const entity = alexa.findSmarthomeEntityExt(input.entity);
if(!entity) error(`smarthome entity not found: "${input.entity}"`, input.entity);
return entity;
});
if(entities.includes(undefined)) return;
let requests = new Array(inputs.length);
for(let i = 0; i < inputs.length; i++) {
const input = inputs[i];
const entity = entities[i];
if(!entity) return;
const native = requests[i] = {};
native.entityId = entity.entityId;
native.entityType = entity.entityType;
native.parameters = { action: input.action };
switch(input.action) {
case 'setColor': {
const name = alexa.findSmarthomeColorNameExt(input.value);
if(!name) return error(`could not find closest color name of "${input.value}"`);
native.parameters['colorName'] = name;
break;
}
case 'setColorTemperature': {
const name = alexa.findSmarthomeColorTemperatureNameExt(input.value);
if(!name) return error(`could not find closest color name of "${input.value}"`);
native.parameters['colorTemperatureName'] = name;
break;
}
case 'setBrightness': {
native.parameters['brightness'] = Number(input.value);
break;
}
case 'setPercentage': {
native.parameters['percentage'] = Number(input.value);
break;
}
case 'setLockState':
case 'lockAction': {
native.parameters['targetLockState.value'] = String(input.value).trim().toUpperCase();
break;
}
case 'setTargetTemperature': {
native.parameters['targetTemperature.value'] = Number(input.value);
native.parameters['targetTemperature.scale'] = String(input.scale || 'celsius').trim().toUpperCase();
break;
}
}
}
requests = requests.filter(o => o);
return alexa.executeSmarthomeDeviceActionExt(requests).then(response => {
if(tools.matches(response, {message: ''})) return error(`error response: ${response.message}`);
if(!tools.matches(response, {
controlResponses: [{ entityId: '' }],
errors: [{ entity: { entityId: '', entityType: '' }}]
})) {
return error(`unexpected response layout: "${JSON.stringify(response)}"`);
}
const controlResponseById = new Map();
const errorById = new Map();
for(const controlResponse of response.controlResponses) {
controlResponseById.set(controlResponse.entityId, controlResponse);
}
for(const error of response.errors) {
errorById.set(error.entity.entityId, error);
}
const msgs = inputs.map((input, i) => {
const entity = entities[i];
if(!entity) return null;
const id = entity.entityId;
const controlResponse = controlResponseById.get(id);
if(!controlResponse) {
const errorObj = errorById.get(id) || {message: `no response for smarthome entity: "${entity.name}" (${id})!`};
error(errorObj.message, errorObj);
}
return controlResponse;
});
tools.nodeSendMultiple(RED, this, msg, msgs, this.outputs);
}).catch(error);
}
case 'discover': {
return alexa.discoverSmarthomeDevicePromise().then(send).catch(error);
}
case 'forget': {
if(!tools.matches(value, { what: '' })) {
return error(`invalid input: "${JSON.stringify(this.config)}"`);
}
switch(value.what) {
case 'device': {
if(!tools.matches(value, { entity: {type: '', value: ''} })) return error(`invalid input: "${JSON.stringify(this.config)}"`);
const id = RED.util.evaluateNodeProperty(value.entity.value, value.entity.type, this, msg);
return alexa.deleteSmarthomeDeviceExt(id).then(send).catch(error);
}
case 'group': {
if(!tools.matches(value, { entity: {type: '', value: ''} })) return error(`invalid input: "${JSON.stringify(this.config)}"`);
const id = RED.util.evaluateNodeProperty(value.entity.value, value.entity.type, this, msg);
return alexa.deleteSmarthomeGroupExt(id).then(send).catch(error);
}
case 'allDevices': {
return alexa.deleteAllSmarthomeDevicesExt().then(send).catch(error);
}
default: {
return error(`invalid input: "${JSON.stringify(this.config)}"`);
}
}
}
default: {
return invalid();
}
}
});
}
RED.nodes.registerType("alexa-remote-smarthome", AlexaRemoteSmarthome);
};