iobroker.iot 5.0.13 → 6.0.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.
Files changed (54) hide show
  1. package/README.md +9 -14
  2. package/admin/assets/{index-DCr4hGKK.js → index-BdqghSg7.js} +40 -40
  3. package/admin/index_m.html +1 -1
  4. package/admin/rules/@mf-types/compiled-types/ActionVisu.d.ts +1 -1
  5. package/admin/rules/@mf-types.zip +0 -0
  6. package/admin/rules/assets/{ActionVisu-BN0Kjfo9.js → ActionVisu-BCZFwf_Y.js} +1 -1
  7. package/admin/rules/assets/{ActionVisu__loadShare__react__loadShare__.js-aa1tDx2C.js → ActionVisu__loadShare__react__loadShare__.js-B4USkKO5.js} +1 -1
  8. package/admin/rules/assets/{ActionVisu__loadShare__react__loadShare__.js_commonjs-proxy-DX6cx281.js → ActionVisu__loadShare__react__loadShare__.js_commonjs-proxy-CEUqE4IA.js} +1 -1
  9. package/admin/rules/assets/{ActionVisu__loadShare__react_mf_2_dom__loadShare__.js_commonjs-proxy-27bLZRGy.js → ActionVisu__loadShare__react_mf_2_dom__loadShare__.js_commonjs-proxy-MphP5XTI.js} +1 -1
  10. package/admin/rules/assets/{bootstrap-QjsDSZNA.js → bootstrap-BKoQjf4I.js} +1 -1
  11. package/admin/rules/assets/{index-DiSsXjpZ.js → index-BlSAfGU-.js} +4 -4
  12. package/admin/rules/assets/index-Dk5YUp3j.js +1187 -0
  13. package/admin/rules/assets/{index-C1ZEp-2N.js → index-pMP6L5OL.js} +2 -2
  14. package/admin/rules/assets/{jsx-runtime-CnsYFZsa.js → jsx-runtime-D-zp5RaI.js} +1 -1
  15. package/admin/rules/assets/{localSharedImportMap-8wECk2hR.js → localSharedImportMap-BsAeuDnA.js} +1 -1
  16. package/admin/rules/assets/{virtualExposes-b6CxLzZb.js → virtualExposes-B2aPQUg7.js} +1 -1
  17. package/admin/rules/customRuleBlocks.js +5 -5
  18. package/build/lib/AlexaSmartHomeV3/Alexa/Directives/Discovery.js +11 -1
  19. package/build/lib/AlexaSmartHomeV3/Alexa/Directives/Discovery.js.map +1 -1
  20. package/build/lib/AlexaSmartHomeV3/Alexa/Properties/TargetSetpoint.js +8 -1
  21. package/build/lib/AlexaSmartHomeV3/Alexa/Properties/TargetSetpoint.js.map +1 -1
  22. package/build/lib/AlexaSmartHomeV3/Alexa/Properties/Temperature.js +8 -1
  23. package/build/lib/AlexaSmartHomeV3/Alexa/Properties/Temperature.js.map +1 -1
  24. package/build/lib/AlexaSmartHomeV3/Controls/AirCondition.js +2 -2
  25. package/build/lib/AlexaSmartHomeV3/Controls/AirCondition.js.map +1 -1
  26. package/build/lib/AlexaSmartHomeV3/Controls/Control.js +7 -0
  27. package/build/lib/AlexaSmartHomeV3/Controls/Control.js.map +1 -1
  28. package/build/lib/AlexaSmartHomeV3/Controls/Ct.js +1 -1
  29. package/build/lib/AlexaSmartHomeV3/Controls/Ct.js.map +1 -1
  30. package/build/lib/AlexaSmartHomeV3/Controls/Rgb.js +1 -1
  31. package/build/lib/AlexaSmartHomeV3/Controls/Rgb.js.map +1 -1
  32. package/build/lib/AlexaSmartHomeV3/Controls/RgbSingle.js +1 -1
  33. package/build/lib/AlexaSmartHomeV3/Controls/RgbSingle.js.map +1 -1
  34. package/build/lib/AlexaSmartHomeV3/Controls/RgbwSingle.js +1 -1
  35. package/build/lib/AlexaSmartHomeV3/Controls/RgbwSingle.js.map +1 -1
  36. package/build/lib/AlexaSmartHomeV3/Controls/Thermostat.js +1 -1
  37. package/build/lib/AlexaSmartHomeV3/Controls/Thermostat.js.map +1 -1
  38. package/build/lib/AlexaSmartHomeV3/Controls/Volume.js +2 -2
  39. package/build/lib/AlexaSmartHomeV3/Controls/Volume.js.map +1 -1
  40. package/build/lib/AlexaSmartHomeV3/DeviceManager.js +18 -9
  41. package/build/lib/AlexaSmartHomeV3/DeviceManager.js.map +1 -1
  42. package/build/lib/AlexaSmartHomeV3/Helpers/DiscoveryValidator.js +283 -0
  43. package/build/lib/AlexaSmartHomeV3/Helpers/DiscoveryValidator.js.map +1 -0
  44. package/build/lib/AlexaSmartHomeV3/Helpers/Utils.js +4 -0
  45. package/build/lib/AlexaSmartHomeV3/Helpers/Utils.js.map +1 -1
  46. package/build/lib/alisa.js +15 -14
  47. package/build/lib/googleHome.js +151 -207
  48. package/build/lib/remote.js +1 -1
  49. package/build/lib/remote.js.map +1 -1
  50. package/io-package.json +27 -27
  51. package/package.json +5 -5
  52. package/admin/rules/assets/index-FrXvyqvW.js +0 -1187
  53. package/admin/rules/mf-stats.json +0 -1
  54. /package/admin/rules/assets/{index-Ccpql4vu.js → index-BPY9gF0q.js} +0 -0
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateDiscoveryResponse = validateDiscoveryResponse;
4
+ // Alexa limits: https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery.html
5
+ const MAX_ENDPOINTS = 300;
6
+ const MAX_ENDPOINT_ID_LENGTH = 256;
7
+ const MAX_FRIENDLY_NAME_LENGTH = 128;
8
+ const MAX_DESCRIPTION_LENGTH = 128;
9
+ const MAX_MANUFACTURER_NAME_LENGTH = 128;
10
+ const MAX_CAPABILITIES_PER_ENDPOINT = 100;
11
+ // AWS IoT MQTT message size limit (128 KB minus overhead)
12
+ const MAX_RESPONSE_SIZE_BYTES = 127 * 1024;
13
+ const RESPONSE_SIZE_WARNING_BYTES = 100 * 1024;
14
+ // https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery.html#display-categories
15
+ const VALID_DISPLAY_CATEGORIES = new Set([
16
+ 'ACTIVITY_TRIGGER',
17
+ 'AIR_CONDITIONER',
18
+ 'AIR_FRESHENER',
19
+ 'AIR_PURIFIER',
20
+ 'AIR_QUALITY_MONITOR',
21
+ 'ALEXA_VOICE_ENABLED',
22
+ 'AUTO_ACCESSORY',
23
+ 'BLUETOOTH_SPEAKER',
24
+ 'CAMERA',
25
+ 'CHRISTMAS_TREE',
26
+ 'COFFEE_MAKER',
27
+ 'COMPUTER',
28
+ 'CONTACT_SENSOR',
29
+ 'DISHWASHER',
30
+ 'DOOR',
31
+ 'DOORBELL',
32
+ 'DRYER',
33
+ 'EXTERIOR_BLIND',
34
+ 'FAN',
35
+ 'GAME_CONSOLE',
36
+ 'GARAGE_DOOR',
37
+ 'HEADPHONES',
38
+ 'HUB',
39
+ 'INTERIOR_BLIND',
40
+ 'LAPTOP',
41
+ 'LIGHT',
42
+ 'MICROWAVE',
43
+ 'MOBILE_PHONE',
44
+ 'MOTION_SENSOR',
45
+ 'MUSIC_SYSTEM',
46
+ 'NETWORK_HARDWARE',
47
+ 'OTHER',
48
+ 'OVEN',
49
+ 'PHONE',
50
+ 'PRINTER',
51
+ 'REMOTE',
52
+ 'ROUTER',
53
+ 'SCENE_TRIGGER',
54
+ 'SCREEN',
55
+ 'SECURITY_PANEL',
56
+ 'SECURITY_SYSTEM',
57
+ 'SLOW_COOKER',
58
+ 'SMARTLOCK',
59
+ 'SMARTPLUG',
60
+ 'SPEAKER',
61
+ 'STREAMING_DEVICE',
62
+ 'SWITCH',
63
+ 'TABLET',
64
+ 'TEMPERATURE_SENSOR',
65
+ 'THERMOSTAT',
66
+ 'TV',
67
+ 'VACUUM_CLEANER',
68
+ 'VACUUM',
69
+ 'VEHICLE',
70
+ 'WASHER',
71
+ 'WATER_HEATER',
72
+ 'WEARABLE',
73
+ ]);
74
+ const VALID_NAMESPACES = new Set([
75
+ 'Alexa',
76
+ 'Alexa.BrightnessController',
77
+ 'Alexa.ColorController',
78
+ 'Alexa.ColorTemperatureController',
79
+ 'Alexa.ContactSensor',
80
+ 'Alexa.EndpointHealth',
81
+ 'Alexa.HumiditySensor',
82
+ 'Alexa.LockController',
83
+ 'Alexa.ModeController',
84
+ 'Alexa.MotionSensor',
85
+ 'Alexa.PercentageController',
86
+ 'Alexa.PowerController',
87
+ 'Alexa.RangeController',
88
+ 'Alexa.SceneController',
89
+ 'Alexa.Speaker',
90
+ 'Alexa.TemperatureSensor',
91
+ 'Alexa.ThermostatController',
92
+ ]);
93
+ // Multi-instance capabilities that require an instance property
94
+ const REQUIRES_INSTANCE = new Set(['Alexa.ModeController', 'Alexa.RangeController', 'Alexa.ToggleController']);
95
+ // Alexa wake words — devices named like this can't be controlled
96
+ const WAKE_WORDS = ['alexa', 'echo', 'amazon', 'computer', 'ziggy'];
97
+ /**
98
+ * Validates the Alexa Discovery response and removes invalid endpoints.
99
+ * Returns the sanitized response.
100
+ */
101
+ function validateDiscoveryResponse(response, log) {
102
+ // Validate response header structure
103
+ const header = response?.event?.header;
104
+ if (!header) {
105
+ log.error('Discovery: response has no event.header');
106
+ return response;
107
+ }
108
+ // @ts-expect-error
109
+ if (header.namespace !== 'Alexa.Discovery') {
110
+ log.error(`Discovery: unexpected namespace "${header.namespace}", expected "Alexa.Discovery"`);
111
+ }
112
+ if (header.name !== 'Discover.Response') {
113
+ log.error(`Discovery: unexpected name "${header.name}", expected "Discover.Response"`);
114
+ }
115
+ if (header.payloadVersion !== '3') {
116
+ log.error(`Discovery: unexpected payloadVersion "${header.payloadVersion}", expected "3"`);
117
+ }
118
+ const endpoints = response?.event?.payload?.endpoints;
119
+ if (!endpoints || !Array.isArray(endpoints)) {
120
+ return response;
121
+ }
122
+ const errors = [];
123
+ const warnings = [];
124
+ const seenIds = new Set();
125
+ const seenNames = new Set();
126
+ // Validate and filter endpoints in place (backwards to safely splice)
127
+ for (let i = endpoints.length - 1; i >= 0; i--) {
128
+ const ep = endpoints[i];
129
+ const epIssues = validateEndpoint(ep, seenIds, seenNames);
130
+ const epErrors = epIssues.filter(e => e.severity === 'error');
131
+ const epWarnings = epIssues.filter(e => e.severity === 'warning');
132
+ warnings.push(...epWarnings);
133
+ if (epErrors.length) {
134
+ errors.push(...epErrors);
135
+ endpoints.splice(i, 1);
136
+ }
137
+ }
138
+ // Enforce 300 endpoint limit
139
+ if (endpoints.length > MAX_ENDPOINTS) {
140
+ log.warn(`Discovery: ${endpoints.length} endpoints exceeds Alexa limit of ${MAX_ENDPOINTS}, truncating`);
141
+ endpoints.length = MAX_ENDPOINTS;
142
+ }
143
+ // Check total response size against AWS IoT MQTT limit
144
+ const responseSize = JSON.stringify(response).length;
145
+ if (responseSize > MAX_RESPONSE_SIZE_BYTES) {
146
+ const originalCount = endpoints.length;
147
+ while (endpoints.length > 1 && JSON.stringify(response).length > MAX_RESPONSE_SIZE_BYTES) {
148
+ const removed = endpoints.pop();
149
+ log.warn(`Discovery: removed "${removed.friendlyName}" to fit AWS IoT message size limit`);
150
+ }
151
+ log.warn(`Discovery: response was ${Math.round(responseSize / 1024)} KB (limit: ${Math.round(MAX_RESPONSE_SIZE_BYTES / 1024)} KB), reduced from ${originalCount} to ${endpoints.length} endpoints`);
152
+ }
153
+ else if (responseSize > RESPONSE_SIZE_WARNING_BYTES) {
154
+ log.warn(`Discovery: response is ${Math.round(responseSize / 1024)} KB with ${endpoints.length} endpoints — approaching AWS IoT limit of ${Math.round(MAX_RESPONSE_SIZE_BYTES / 1024)} KB`);
155
+ }
156
+ // Log warnings (non-fatal)
157
+ for (const w of warnings) {
158
+ log.warn(`Discovery: "${w.friendlyName}" (${w.endpointId}): ${w.field} — ${w.message}`);
159
+ }
160
+ // Log errors (endpoint was removed)
161
+ for (const err of errors) {
162
+ log.warn(`Discovery: removed "${err.friendlyName}" (${err.endpointId}): ${err.field} — ${err.message}`);
163
+ }
164
+ if (errors.length || warnings.length) {
165
+ log.info(`Discovery: ${endpoints.length} endpoint(s) valid, ${errors.length} removed, ${warnings.length} warning(s)`);
166
+ }
167
+ return response;
168
+ }
169
+ function validateEndpoint(ep, seenIds, seenNames) {
170
+ const issues = [];
171
+ const id = ep.endpointId || '(empty)';
172
+ const name = ep.friendlyName || '(empty)';
173
+ const error = (field, message) => ({
174
+ endpointId: id,
175
+ friendlyName: name,
176
+ field,
177
+ message,
178
+ severity: 'error',
179
+ });
180
+ const warning = (field, message) => ({
181
+ endpointId: id,
182
+ friendlyName: name,
183
+ field,
184
+ message,
185
+ severity: 'warning',
186
+ });
187
+ // --- endpointId ---
188
+ if (!ep.endpointId) {
189
+ issues.push(error('endpointId', 'missing'));
190
+ }
191
+ else if (ep.endpointId.length > MAX_ENDPOINT_ID_LENGTH) {
192
+ issues.push(error('endpointId', `exceeds ${MAX_ENDPOINT_ID_LENGTH} chars`));
193
+ }
194
+ else if (!/^[\w#;:!@"$%&'()*+,\-./>=<?[\\\]^`{|}~ ]+$/.test(ep.endpointId)) {
195
+ issues.push(error('endpointId', 'contains invalid characters'));
196
+ }
197
+ else if (seenIds.has(ep.endpointId)) {
198
+ issues.push(error('endpointId', 'duplicate'));
199
+ }
200
+ seenIds.add(ep.endpointId);
201
+ // --- friendlyName ---
202
+ if (!ep.friendlyName || !ep.friendlyName.trim()) {
203
+ issues.push(error('friendlyName', 'missing or empty'));
204
+ }
205
+ else if (ep.friendlyName.length > MAX_FRIENDLY_NAME_LENGTH) {
206
+ issues.push(error('friendlyName', `exceeds ${MAX_FRIENDLY_NAME_LENGTH} chars`));
207
+ }
208
+ else {
209
+ // Alexa rejects names that are only digits
210
+ if (/^\d+$/.test(ep.friendlyName.trim())) {
211
+ issues.push(error('friendlyName', 'must not be only digits'));
212
+ }
213
+ // Wake words as device name make the device unusable
214
+ const lower = ep.friendlyName.toLowerCase().trim();
215
+ if (WAKE_WORDS.includes(lower)) {
216
+ issues.push(warning('friendlyName', `"${ep.friendlyName}" is an Alexa wake word — device may not be controllable`));
217
+ }
218
+ // Duplicate names confuse Alexa ("which one did you mean?")
219
+ if (seenNames.has(lower)) {
220
+ issues.push(warning('friendlyName', 'duplicate name — Alexa will ask "which one did you mean?"'));
221
+ }
222
+ seenNames.add(lower);
223
+ }
224
+ // --- description ---
225
+ if (ep.description && ep.description.length > MAX_DESCRIPTION_LENGTH) {
226
+ issues.push(warning('description', `exceeds ${MAX_DESCRIPTION_LENGTH} chars, will be truncated`));
227
+ }
228
+ // --- manufacturerName ---
229
+ if (ep.manufacturerName && ep.manufacturerName.length > MAX_MANUFACTURER_NAME_LENGTH) {
230
+ issues.push(warning('manufacturerName', `exceeds ${MAX_MANUFACTURER_NAME_LENGTH} chars`));
231
+ }
232
+ // --- displayCategories ---
233
+ if (!ep.displayCategories || !ep.displayCategories.length) {
234
+ issues.push(error('displayCategories', 'missing or empty'));
235
+ }
236
+ else {
237
+ for (const cat of ep.displayCategories) {
238
+ if (!VALID_DISPLAY_CATEGORIES.has(cat)) {
239
+ issues.push(error('displayCategories', `unknown category "${cat}"`));
240
+ }
241
+ }
242
+ }
243
+ // --- capabilities ---
244
+ if (!ep.capabilities || !ep.capabilities.length) {
245
+ issues.push(error('capabilities', 'missing or empty'));
246
+ }
247
+ else {
248
+ if (ep.capabilities.length > MAX_CAPABILITIES_PER_ENDPOINT) {
249
+ issues.push(error('capabilities', `exceeds ${MAX_CAPABILITIES_PER_ENDPOINT} capabilities`));
250
+ }
251
+ // Alexa interface is required on every endpoint
252
+ const hasAlexa = ep.capabilities.some(c => c.interface === 'Alexa');
253
+ if (!hasAlexa) {
254
+ issues.push(error('capabilities', 'missing required Alexa interface'));
255
+ }
256
+ // Check for duplicate capabilities (same interface+instance)
257
+ const capKeys = new Set();
258
+ for (const cap of ep.capabilities) {
259
+ const capKey = `${cap.interface || ''}::${cap.instance || ''}`;
260
+ if (cap.interface !== 'Alexa' && capKeys.has(capKey)) {
261
+ issues.push(error('capabilities', `duplicate capability ${cap.interface}${cap.instance ? ` (${cap.instance})` : ''}`));
262
+ }
263
+ capKeys.add(capKey);
264
+ // Validate individual capability
265
+ if (cap.interface && !VALID_NAMESPACES.has(cap.interface)) {
266
+ issues.push(warning('capabilities', `unknown interface "${cap.interface}"`));
267
+ }
268
+ if (cap.version && cap.version !== '3' && cap.version !== '3.2') {
269
+ issues.push(error('capabilities', `unexpected version "${cap.version}" for ${cap.interface}`));
270
+ }
271
+ // ModeController and RangeController require instance
272
+ if (cap.interface && REQUIRES_INSTANCE.has(cap.interface) && !cap.instance) {
273
+ issues.push(error('capabilities', `${cap.interface} requires an instance property`));
274
+ }
275
+ // properties.supported must not be empty if defined
276
+ if (cap.properties && Array.isArray(cap.properties.supported) && cap.properties.supported.length === 0) {
277
+ issues.push(warning('capabilities', `${cap.interface} has empty properties.supported array`));
278
+ }
279
+ }
280
+ }
281
+ return issues;
282
+ }
283
+ //# sourceMappingURL=DiscoveryValidator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscoveryValidator.js","sourceRoot":"","sources":["../../../../src/lib/AlexaSmartHomeV3/Helpers/DiscoveryValidator.ts"],"names":[],"mappings":"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","sourcesContent":["import type Logger from './Logger';\nimport type AlexaResponse from '../Alexa/AlexaResponse';\n\n// Alexa limits: https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery.html\nconst MAX_ENDPOINTS = 300;\nconst MAX_ENDPOINT_ID_LENGTH = 256;\nconst MAX_FRIENDLY_NAME_LENGTH = 128;\nconst MAX_DESCRIPTION_LENGTH = 128;\nconst MAX_MANUFACTURER_NAME_LENGTH = 128;\nconst MAX_CAPABILITIES_PER_ENDPOINT = 100;\n\n// AWS IoT MQTT message size limit (128 KB minus overhead)\nconst MAX_RESPONSE_SIZE_BYTES = 127 * 1024;\nconst RESPONSE_SIZE_WARNING_BYTES = 100 * 1024;\n\n// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery.html#display-categories\nconst VALID_DISPLAY_CATEGORIES = new Set([\n 'ACTIVITY_TRIGGER',\n 'AIR_CONDITIONER',\n 'AIR_FRESHENER',\n 'AIR_PURIFIER',\n 'AIR_QUALITY_MONITOR',\n 'ALEXA_VOICE_ENABLED',\n 'AUTO_ACCESSORY',\n 'BLUETOOTH_SPEAKER',\n 'CAMERA',\n 'CHRISTMAS_TREE',\n 'COFFEE_MAKER',\n 'COMPUTER',\n 'CONTACT_SENSOR',\n 'DISHWASHER',\n 'DOOR',\n 'DOORBELL',\n 'DRYER',\n 'EXTERIOR_BLIND',\n 'FAN',\n 'GAME_CONSOLE',\n 'GARAGE_DOOR',\n 'HEADPHONES',\n 'HUB',\n 'INTERIOR_BLIND',\n 'LAPTOP',\n 'LIGHT',\n 'MICROWAVE',\n 'MOBILE_PHONE',\n 'MOTION_SENSOR',\n 'MUSIC_SYSTEM',\n 'NETWORK_HARDWARE',\n 'OTHER',\n 'OVEN',\n 'PHONE',\n 'PRINTER',\n 'REMOTE',\n 'ROUTER',\n 'SCENE_TRIGGER',\n 'SCREEN',\n 'SECURITY_PANEL',\n 'SECURITY_SYSTEM',\n 'SLOW_COOKER',\n 'SMARTLOCK',\n 'SMARTPLUG',\n 'SPEAKER',\n 'STREAMING_DEVICE',\n 'SWITCH',\n 'TABLET',\n 'TEMPERATURE_SENSOR',\n 'THERMOSTAT',\n 'TV',\n 'VACUUM_CLEANER',\n 'VACUUM',\n 'VEHICLE',\n 'WASHER',\n 'WATER_HEATER',\n 'WEARABLE',\n]);\n\nconst VALID_NAMESPACES = new Set([\n 'Alexa',\n 'Alexa.BrightnessController',\n 'Alexa.ColorController',\n 'Alexa.ColorTemperatureController',\n 'Alexa.ContactSensor',\n 'Alexa.EndpointHealth',\n 'Alexa.HumiditySensor',\n 'Alexa.LockController',\n 'Alexa.ModeController',\n 'Alexa.MotionSensor',\n 'Alexa.PercentageController',\n 'Alexa.PowerController',\n 'Alexa.RangeController',\n 'Alexa.SceneController',\n 'Alexa.Speaker',\n 'Alexa.TemperatureSensor',\n 'Alexa.ThermostatController',\n]);\n\n// Multi-instance capabilities that require an instance property\nconst REQUIRES_INSTANCE = new Set(['Alexa.ModeController', 'Alexa.RangeController', 'Alexa.ToggleController']);\n\n// Alexa wake words — devices named like this can't be controlled\nconst WAKE_WORDS = ['alexa', 'echo', 'amazon', 'computer', 'ziggy'];\n\ninterface DiscoveryEndpoint {\n endpointId: string;\n friendlyName: string;\n description?: string;\n manufacturerName?: string;\n displayCategories?: string[];\n capabilities?: DiscoveryCapability[];\n}\n\ninterface DiscoveryCapability {\n type?: string;\n interface?: string;\n instance?: string;\n version?: string;\n properties?: {\n supported?: { name: string }[];\n };\n}\n\nexport interface ValidationError {\n endpointId: string;\n friendlyName: string;\n field: string;\n message: string;\n severity: 'error' | 'warning';\n}\n\n/**\n * Validates the Alexa Discovery response and removes invalid endpoints.\n * Returns the sanitized response.\n */\nexport function validateDiscoveryResponse(response: AlexaResponse, log: Logger): AlexaResponse {\n // Validate response header structure\n const header = response?.event?.header;\n if (!header) {\n log.error('Discovery: response has no event.header');\n return response;\n }\n // @ts-expect-error\n if (header.namespace !== 'Alexa.Discovery') {\n log.error(`Discovery: unexpected namespace \"${header.namespace}\", expected \"Alexa.Discovery\"`);\n }\n if (header.name !== 'Discover.Response') {\n log.error(`Discovery: unexpected name \"${header.name}\", expected \"Discover.Response\"`);\n }\n if (header.payloadVersion !== '3') {\n log.error(`Discovery: unexpected payloadVersion \"${header.payloadVersion as unknown as string}\", expected \"3\"`);\n }\n\n const endpoints: DiscoveryEndpoint[] | undefined = response?.event?.payload?.endpoints;\n if (!endpoints || !Array.isArray(endpoints)) {\n return response;\n }\n\n const errors: ValidationError[] = [];\n const warnings: ValidationError[] = [];\n const seenIds = new Set<string>();\n const seenNames = new Set<string>();\n\n // Validate and filter endpoints in place (backwards to safely splice)\n for (let i = endpoints.length - 1; i >= 0; i--) {\n const ep = endpoints[i];\n const epIssues = validateEndpoint(ep, seenIds, seenNames);\n const epErrors = epIssues.filter(e => e.severity === 'error');\n const epWarnings = epIssues.filter(e => e.severity === 'warning');\n warnings.push(...epWarnings);\n if (epErrors.length) {\n errors.push(...epErrors);\n endpoints.splice(i, 1);\n }\n }\n\n // Enforce 300 endpoint limit\n if (endpoints.length > MAX_ENDPOINTS) {\n log.warn(`Discovery: ${endpoints.length} endpoints exceeds Alexa limit of ${MAX_ENDPOINTS}, truncating`);\n endpoints.length = MAX_ENDPOINTS;\n }\n\n // Check total response size against AWS IoT MQTT limit\n const responseSize = JSON.stringify(response).length;\n if (responseSize > MAX_RESPONSE_SIZE_BYTES) {\n const originalCount = endpoints.length;\n while (endpoints.length > 1 && JSON.stringify(response).length > MAX_RESPONSE_SIZE_BYTES) {\n const removed = endpoints.pop()!;\n log.warn(`Discovery: removed \"${removed.friendlyName}\" to fit AWS IoT message size limit`);\n }\n log.warn(\n `Discovery: response was ${Math.round(responseSize / 1024)} KB (limit: ${Math.round(MAX_RESPONSE_SIZE_BYTES / 1024)} KB), reduced from ${originalCount} to ${endpoints.length} endpoints`,\n );\n } else if (responseSize > RESPONSE_SIZE_WARNING_BYTES) {\n log.warn(\n `Discovery: response is ${Math.round(responseSize / 1024)} KB with ${endpoints.length} endpoints — approaching AWS IoT limit of ${Math.round(MAX_RESPONSE_SIZE_BYTES / 1024)} KB`,\n );\n }\n\n // Log warnings (non-fatal)\n for (const w of warnings) {\n log.warn(`Discovery: \"${w.friendlyName}\" (${w.endpointId}): ${w.field} — ${w.message}`);\n }\n\n // Log errors (endpoint was removed)\n for (const err of errors) {\n log.warn(`Discovery: removed \"${err.friendlyName}\" (${err.endpointId}): ${err.field} — ${err.message}`);\n }\n\n if (errors.length || warnings.length) {\n log.info(\n `Discovery: ${endpoints.length} endpoint(s) valid, ${errors.length} removed, ${warnings.length} warning(s)`,\n );\n }\n\n return response;\n}\n\nfunction validateEndpoint(ep: DiscoveryEndpoint, seenIds: Set<string>, seenNames: Set<string>): ValidationError[] {\n const issues: ValidationError[] = [];\n const id = ep.endpointId || '(empty)';\n const name = ep.friendlyName || '(empty)';\n\n const error = (field: string, message: string): ValidationError => ({\n endpointId: id,\n friendlyName: name,\n field,\n message,\n severity: 'error',\n });\n const warning = (field: string, message: string): ValidationError => ({\n endpointId: id,\n friendlyName: name,\n field,\n message,\n severity: 'warning',\n });\n\n // --- endpointId ---\n if (!ep.endpointId) {\n issues.push(error('endpointId', 'missing'));\n } else if (ep.endpointId.length > MAX_ENDPOINT_ID_LENGTH) {\n issues.push(error('endpointId', `exceeds ${MAX_ENDPOINT_ID_LENGTH} chars`));\n } else if (!/^[\\w#;:!@\"$%&'()*+,\\-./>=<?[\\\\\\]^`{|}~ ]+$/.test(ep.endpointId)) {\n issues.push(error('endpointId', 'contains invalid characters'));\n } else if (seenIds.has(ep.endpointId)) {\n issues.push(error('endpointId', 'duplicate'));\n }\n seenIds.add(ep.endpointId);\n\n // --- friendlyName ---\n if (!ep.friendlyName || !ep.friendlyName.trim()) {\n issues.push(error('friendlyName', 'missing or empty'));\n } else if (ep.friendlyName.length > MAX_FRIENDLY_NAME_LENGTH) {\n issues.push(error('friendlyName', `exceeds ${MAX_FRIENDLY_NAME_LENGTH} chars`));\n } else {\n // Alexa rejects names that are only digits\n if (/^\\d+$/.test(ep.friendlyName.trim())) {\n issues.push(error('friendlyName', 'must not be only digits'));\n }\n\n // Wake words as device name make the device unusable\n const lower = ep.friendlyName.toLowerCase().trim();\n if (WAKE_WORDS.includes(lower)) {\n issues.push(\n warning('friendlyName', `\"${ep.friendlyName}\" is an Alexa wake word — device may not be controllable`),\n );\n }\n\n // Duplicate names confuse Alexa (\"which one did you mean?\")\n if (seenNames.has(lower)) {\n issues.push(warning('friendlyName', 'duplicate name — Alexa will ask \"which one did you mean?\"'));\n }\n seenNames.add(lower);\n }\n\n // --- description ---\n if (ep.description && ep.description.length > MAX_DESCRIPTION_LENGTH) {\n issues.push(warning('description', `exceeds ${MAX_DESCRIPTION_LENGTH} chars, will be truncated`));\n }\n\n // --- manufacturerName ---\n if (ep.manufacturerName && ep.manufacturerName.length > MAX_MANUFACTURER_NAME_LENGTH) {\n issues.push(warning('manufacturerName', `exceeds ${MAX_MANUFACTURER_NAME_LENGTH} chars`));\n }\n\n // --- displayCategories ---\n if (!ep.displayCategories || !ep.displayCategories.length) {\n issues.push(error('displayCategories', 'missing or empty'));\n } else {\n for (const cat of ep.displayCategories) {\n if (!VALID_DISPLAY_CATEGORIES.has(cat)) {\n issues.push(error('displayCategories', `unknown category \"${cat}\"`));\n }\n }\n }\n\n // --- capabilities ---\n if (!ep.capabilities || !ep.capabilities.length) {\n issues.push(error('capabilities', 'missing or empty'));\n } else {\n if (ep.capabilities.length > MAX_CAPABILITIES_PER_ENDPOINT) {\n issues.push(error('capabilities', `exceeds ${MAX_CAPABILITIES_PER_ENDPOINT} capabilities`));\n }\n\n // Alexa interface is required on every endpoint\n const hasAlexa = ep.capabilities.some(c => c.interface === 'Alexa');\n if (!hasAlexa) {\n issues.push(error('capabilities', 'missing required Alexa interface'));\n }\n\n // Check for duplicate capabilities (same interface+instance)\n const capKeys = new Set<string>();\n for (const cap of ep.capabilities) {\n const capKey = `${cap.interface || ''}::${cap.instance || ''}`;\n if (cap.interface !== 'Alexa' && capKeys.has(capKey)) {\n issues.push(\n error(\n 'capabilities',\n `duplicate capability ${cap.interface}${cap.instance ? ` (${cap.instance})` : ''}`,\n ),\n );\n }\n capKeys.add(capKey);\n\n // Validate individual capability\n if (cap.interface && !VALID_NAMESPACES.has(cap.interface)) {\n issues.push(warning('capabilities', `unknown interface \"${cap.interface}\"`));\n }\n\n if (cap.version && cap.version !== '3' && cap.version !== '3.2') {\n issues.push(error('capabilities', `unexpected version \"${cap.version}\" for ${cap.interface}`));\n }\n\n // ModeController and RangeController require instance\n if (cap.interface && REQUIRES_INSTANCE.has(cap.interface) && !cap.instance) {\n issues.push(error('capabilities', `${cap.interface} requires an instance property`));\n }\n\n // properties.supported must not be empty if defined\n if (cap.properties && Array.isArray(cap.properties.supported) && cap.properties.supported.length === 0) {\n issues.push(warning('capabilities', `${cap.interface} has empty properties.supported array`));\n }\n }\n }\n\n return issues;\n}\n"]}
@@ -991,6 +991,10 @@ function hal2rgbw(hal) {
991
991
  g = 0;
992
992
  b = 0;
993
993
  }
994
+ // Extract the common white component (min of R,G,B = p) into the dedicated white channel
995
+ // const w = p;
996
+ // return `#${toHex(to255(r - w))}${toHex(to255(g - w))}${toHex(to255(b - w))}${toHex(to255(w))}`;
997
+ // Simple calculations
994
998
  return `#${toHex(to255(r))}${toHex(to255(g))}${toHex(to255(b))}${toHex(to255(255))}`;
995
999
  }
996
1000
  /**