node-red-contrib-homekit-bridged 2.0.0-dev.4 → 2.0.0-dev.7

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 (125) hide show
  1. package/build/lib/HAPHostNode.js +183 -141
  2. package/build/lib/HAPServiceNode.js +199 -172
  3. package/build/lib/HAPServiceNode2.js +207 -172
  4. package/build/lib/NRCHKBError.js +23 -2
  5. package/build/lib/PairingQRCode.js +62 -0
  6. package/build/lib/Storage.js +157 -92
  7. package/build/lib/api.js +654 -288
  8. package/build/lib/camera/CameraControl.js +125 -0
  9. package/build/lib/camera/CameraDelegate.js +507 -0
  10. package/build/lib/camera/MP4StreamingServer.js +159 -0
  11. package/build/lib/hap/HAPCharacteristic.js +25 -4
  12. package/build/lib/hap/HAPService.js +25 -4
  13. package/build/lib/hap/eve-app/EveCharacteristics.js +124 -81
  14. package/build/lib/hap/eve-app/EveServices.js +50 -17
  15. package/build/lib/hap/hap-nodejs.js +32 -0
  16. package/build/lib/migration/HomeKitService2Migration.js +34 -0
  17. package/build/lib/migration/NodeMigration.js +75 -0
  18. package/build/lib/types/AccessoryInformationType.js +15 -1
  19. package/build/lib/types/CameraConfigType.js +15 -1
  20. package/build/lib/types/CustomCharacteristicType.js +15 -1
  21. package/build/lib/types/HAPHostConfigType.js +15 -1
  22. package/build/lib/types/HAPHostNodeType.js +15 -1
  23. package/build/lib/types/HAPService2ConfigType.js +15 -1
  24. package/build/lib/types/HAPService2NodeType.js +15 -1
  25. package/build/lib/types/HAPServiceConfigType.js +15 -1
  26. package/build/lib/types/HAPServiceNodeType.js +15 -1
  27. package/build/lib/types/HAPStatusConfigType.js +15 -1
  28. package/build/lib/types/HAPStatusNodeType.js +15 -1
  29. package/build/lib/types/HostType.js +28 -7
  30. package/build/lib/types/NodeType.js +15 -1
  31. package/build/lib/types/PublishTimersType.js +15 -1
  32. package/build/lib/types/UniFiControllerConfigType.js +16 -0
  33. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.js +28 -7
  34. package/build/lib/types/hap-nodejs/HapCategories.js +64 -43
  35. package/build/lib/types/storage/SerializedHostType.js +15 -1
  36. package/build/lib/types/storage/StorageType.js +34 -10
  37. package/build/lib/unifi/ProtectDiscovery.js +80 -0
  38. package/build/lib/utils/AccessoryUtils.js +152 -110
  39. package/build/lib/utils/BridgeUtils.js +82 -39
  40. package/build/lib/utils/CharacteristicUtils.js +5 -49
  41. package/build/lib/utils/CharacteristicUtils2.js +5 -49
  42. package/build/lib/utils/CharacteristicUtilsBase.js +81 -0
  43. package/build/lib/utils/NodeStatusUtils.js +89 -40
  44. package/build/lib/utils/ServiceUtils.js +434 -373
  45. package/build/lib/utils/ServiceUtils2.js +514 -307
  46. package/build/lib/utils/index.js +10 -11
  47. package/build/nodes/bridge.html +184 -166
  48. package/build/nodes/bridge.js +27 -9
  49. package/build/nodes/locales/en-US/node-red-contrib-homekit-bridged.json +22 -0
  50. package/build/nodes/nrchkb.html +1601 -88
  51. package/build/nodes/nrchkb.js +66 -88
  52. package/build/nodes/plugin-instance.html +499 -0
  53. package/build/nodes/plugin-instance.js +46 -0
  54. package/build/nodes/service.html +517 -299
  55. package/build/nodes/service.js +5 -6
  56. package/build/nodes/service2.html +1683 -460
  57. package/build/nodes/service2.js +5 -8
  58. package/build/nodes/standalone.html +187 -174
  59. package/build/nodes/standalone.js +27 -9
  60. package/build/nodes/status.html +51 -18
  61. package/build/nodes/status.js +47 -40
  62. package/build/nodes/unifi-controller.html +92 -0
  63. package/build/nodes/unifi-controller.js +20 -0
  64. package/build/plugins/embedded/homebridge-camera-ffmpeg/index.js +479 -0
  65. package/build/plugins/embedded/homebridge-unifi-protect/index.js +521 -0
  66. package/build/plugins/embedded/index.js +58 -0
  67. package/build/plugins/nrchkb-homekit-plugins.js +17 -0
  68. package/build/plugins/registry/index.js +203 -0
  69. package/build/plugins/registry/types.js +16 -0
  70. package/build/scripts/migrate-homekit-service-flows.js +47 -0
  71. package/examples/demo/01 - ALL Demos single import.json +1885 -1885
  72. package/examples/demo/02 - Air Purifier.json +279 -279
  73. package/examples/demo/03 - Air Quality sensor with Battery.json +254 -254
  74. package/examples/demo/04 - Dimmable Bulb.json +172 -172
  75. package/examples/demo/05 - Color Bulb (HSV).json +195 -195
  76. package/examples/demo/06 - Fan (simple, 3 speeds).json +240 -240
  77. package/examples/demo/07 - Fan (with speed, oscillate, rotation direction).json +175 -175
  78. package/examples/demo/08 - CO2 detector.json +224 -224
  79. package/examples/demo/09 - CO (carbon monoxide) example.json +255 -255
  80. package/examples/demo/10 - Door window contact sensor.json +234 -234
  81. package/examples/demos (advanced)/01 - Television with inputs and speaker.json +541 -541
  82. package/examples/switch/01 - Plain Switch.json +178 -178
  83. package/package.json +95 -84
  84. package/build/lib/HAPHostNode.d.ts +0 -1
  85. package/build/lib/HAPServiceNode.d.ts +0 -1
  86. package/build/lib/HAPServiceNode2.d.ts +0 -1
  87. package/build/lib/NRCHKBError.d.ts +0 -3
  88. package/build/lib/Storage.d.ts +0 -30
  89. package/build/lib/api.d.ts +0 -1
  90. package/build/lib/hap/HAPCharacteristic.d.ts +0 -9
  91. package/build/lib/hap/HAPService.d.ts +0 -6
  92. package/build/lib/hap/eve-app/EveCharacteristics.d.ts +0 -20
  93. package/build/lib/hap/eve-app/EveServices.d.ts +0 -5
  94. package/build/lib/types/AccessoryInformationType.d.ts +0 -11
  95. package/build/lib/types/CameraConfigType.d.ts +0 -24
  96. package/build/lib/types/CustomCharacteristicType.d.ts +0 -6
  97. package/build/lib/types/HAPHostConfigType.d.ts +0 -22
  98. package/build/lib/types/HAPHostNodeType.d.ts +0 -14
  99. package/build/lib/types/HAPService2ConfigType.d.ts +0 -6
  100. package/build/lib/types/HAPService2NodeType.d.ts +0 -7
  101. package/build/lib/types/HAPServiceConfigType.d.ts +0 -26
  102. package/build/lib/types/HAPServiceNodeType.d.ts +0 -38
  103. package/build/lib/types/HAPStatusConfigType.d.ts +0 -5
  104. package/build/lib/types/HAPStatusNodeType.d.ts +0 -12
  105. package/build/lib/types/HostType.d.ts +0 -5
  106. package/build/lib/types/NodeType.d.ts +0 -3
  107. package/build/lib/types/PublishTimersType.d.ts +0 -4
  108. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.d.ts +0 -5
  109. package/build/lib/types/hap-nodejs/HapCategories.d.ts +0 -41
  110. package/build/lib/types/storage/SerializedHostType.d.ts +0 -5
  111. package/build/lib/types/storage/StorageType.d.ts +0 -8
  112. package/build/lib/utils/AccessoryUtils.d.ts +0 -1
  113. package/build/lib/utils/BridgeUtils.d.ts +0 -1
  114. package/build/lib/utils/CharacteristicUtils.d.ts +0 -1
  115. package/build/lib/utils/CharacteristicUtils2.d.ts +0 -1
  116. package/build/lib/utils/NodeStatusUtils.d.ts +0 -17
  117. package/build/lib/utils/ServiceUtils.d.ts +0 -1
  118. package/build/lib/utils/ServiceUtils2.d.ts +0 -1
  119. package/build/lib/utils/index.d.ts +0 -1
  120. package/build/nodes/bridge.d.ts +0 -1
  121. package/build/nodes/nrchkb.d.ts +0 -1
  122. package/build/nodes/service.d.ts +0 -1
  123. package/build/nodes/service2.d.ts +0 -1
  124. package/build/nodes/standalone.d.ts +0 -1
  125. package/build/nodes/status.d.ts +0 -1
@@ -1,93 +1,71 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
37
15
  };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- const path = __importStar(require("node:path"));
40
- const hap_nodejs_1 = require("@homebridge/hap-nodejs");
41
- const logger_1 = require("@nrchkb/logger");
42
- const semver_1 = __importDefault(require("semver"));
43
- const Storage_1 = require("../lib/Storage");
44
- (0, logger_1.loggerSetup)({ timestampEnabled: 'NRCHKB' });
45
- const log = (0, logger_1.logger)('NRCHKB');
46
- if (process.env.NRCHKB_EXPERIMENTAL === 'true') {
47
- log.error('Experimental features enabled');
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ var path = __toESM(require("node:path"));
25
+ var import_hap_nodejs = require("@homebridge/hap-nodejs");
26
+ var import_logger = require("@nrchkb/logger");
27
+ var import_semver = __toESM(require("semver"));
28
+ var import_Storage = require("../lib/Storage");
29
+ (0, import_logger.loggerSetup)({ timestampEnabled: "NRCHKB" });
30
+ const log = (0, import_logger.logger)("NRCHKB");
31
+ if (process.env.NRCHKB_EXPERIMENTAL === "true") {
32
+ log.error("Experimental features enabled");
48
33
  }
49
34
  module.exports = (RED) => {
50
- const deprecatedMinimalNodeVersion = '10.22.1';
51
- const minimalNodeVersion = '12.0.0';
52
- const nodeVersion = process.version;
53
- if (semver_1.default.gte(nodeVersion, deprecatedMinimalNodeVersion)) {
54
- log.debug(`Node.js version requirement met. Required >=${deprecatedMinimalNodeVersion}. Installed ${nodeVersion}`);
55
- if (semver_1.default.lt(nodeVersion, minimalNodeVersion)) {
56
- log.error('Node.js version requirement met but will be deprecated in Node-RED 2.0.0');
57
- log.error(`Recommended >=${minimalNodeVersion}. Installed ${nodeVersion}. Consider upgrading.`);
58
- }
59
- }
60
- else {
61
- throw RangeError(`Node.js version requirement not met. Required >=${deprecatedMinimalNodeVersion}. Installed ${nodeVersion}`);
62
- }
63
- const API = require('../lib/api')(RED);
64
- let rootFolder;
65
- if (RED.settings.available() && RED.settings.userDir) {
66
- log.debug('RED settings available');
67
- rootFolder = RED.settings.userDir;
68
- }
69
- else {
70
- log.error('RED settings not available');
71
- rootFolder = path.join(require('node:os').homedir(), '.node-red');
72
- }
73
- Storage_1.Storage.init(rootFolder, 'nrchkb').then(() => {
74
- log.debug(`nrchkb storage path set to ${Storage_1.Storage.storagePath()}`);
75
- API.init();
76
- const hapStoragePath = path.resolve(rootFolder, 'homekit-persist');
77
- try {
78
- hap_nodejs_1.HAPStorage.setCustomStoragePath(hapStoragePath);
79
- log.debug(`HAPStorage path set to ${hapStoragePath}`);
80
- }
81
- catch (error) {
82
- log.debug('HAPStorage already initialized');
83
- log.error('node-red restart highly recommended');
84
- log.trace(error);
85
- }
86
- if (process.env.NRCHKB_EXPERIMENTAL === 'true') {
87
- log.debug('Registering nrchkb type');
88
- RED.nodes.registerType('nrchkb', function (config) {
89
- RED.nodes.createNode(this, config);
90
- });
91
- }
92
- });
35
+ const minimalNodeVersion = "22.9.0";
36
+ const nodeVersion = process.version;
37
+ if (!import_semver.default.gte(nodeVersion, minimalNodeVersion)) {
38
+ throw RangeError(
39
+ `Node.js version requirement not met. Required >=${minimalNodeVersion}. Installed ${nodeVersion}`
40
+ );
41
+ }
42
+ log.debug(
43
+ `Node.js version requirement met. Required >=${minimalNodeVersion}. Installed ${nodeVersion}`
44
+ );
45
+ let rootFolder;
46
+ if (RED.settings.available() && RED.settings.userDir) {
47
+ log.debug("RED settings available");
48
+ rootFolder = RED.settings.userDir;
49
+ } else {
50
+ log.error("RED settings not available");
51
+ rootFolder = path.join(require("node:os").homedir(), ".node-red");
52
+ }
53
+ const hapStoragePath = path.resolve(rootFolder, "homekit-persist");
54
+ try {
55
+ import_hap_nodejs.HAPStorage.setCustomStoragePath(hapStoragePath);
56
+ log.debug(`HAPStorage path set to ${hapStoragePath}`);
57
+ } catch (error) {
58
+ log.debug("HAPStorage already initialized");
59
+ log.error("node-red restart highly recommended");
60
+ log.trace(error);
61
+ }
62
+ const API = require("../lib/api")(RED);
63
+ import_Storage.Storage.init(rootFolder, "nrchkb").then(() => {
64
+ log.debug(`nrchkb storage path set to ${import_Storage.Storage.storagePath()}`);
65
+ API.init();
66
+ });
67
+ log.debug("Registering nrchkb type");
68
+ RED.nodes.registerType("nrchkb", function(config) {
69
+ RED.nodes.createNode(this, config);
70
+ });
93
71
  };
@@ -0,0 +1,499 @@
1
+ <style>
2
+ .nrchkb-editor .nrchkb-plugin-instance-about {
3
+ margin-bottom: 10px;
4
+ padding: 7px 9px;
5
+ border: 1px solid var(--red-ui-secondary-border-color, #d8d8d8);
6
+ border-radius: 6px;
7
+ background: var(--red-ui-tertiary-background, #f7f7f7);
8
+ color: var(--red-ui-secondary-text-color, #666);
9
+ font-size: 12px;
10
+ line-height: 1.35;
11
+ }
12
+
13
+ .nrchkb-editor .nrchkb-plugin-instance-fields {
14
+ display: grid;
15
+ gap: 4px;
16
+ }
17
+
18
+ .nrchkb-editor .nrchkb-plugin-instance-section {
19
+ margin-top: 8px;
20
+ }
21
+
22
+ .nrchkb-editor .nrchkb-plugin-instance-section:first-of-type {
23
+ margin-top: 0;
24
+ }
25
+
26
+ .nrchkb-editor .nrchkb-plugin-instance-section summary {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 6px;
30
+ min-width: 0;
31
+ }
32
+
33
+ .nrchkb-editor .nrchkb-plugin-instance-section-title {
34
+ min-width: 0;
35
+ overflow: hidden;
36
+ text-overflow: ellipsis;
37
+ white-space: nowrap;
38
+ }
39
+
40
+ .nrchkb-editor .nrchkb-plugin-instance-check {
41
+ min-width: 0;
42
+ max-width: 100%;
43
+ color: var(--red-ui-primary-text-color, #333);
44
+ cursor: pointer;
45
+ }
46
+
47
+ .nrchkb-editor .nrchkb-plugin-instance-check input {
48
+ flex: 0 0 auto;
49
+ }
50
+
51
+ .nrchkb-editor .nrchkb-plugin-instance-check span {
52
+ min-width: 0;
53
+ overflow: hidden;
54
+ text-overflow: ellipsis;
55
+ white-space: nowrap;
56
+ }
57
+
58
+ .nrchkb-editor .nrchkb-plugin-instance-check-row {
59
+ margin-bottom: 5px;
60
+ min-height: 24px;
61
+ }
62
+
63
+ .nrchkb-editor .nrchkb-plugin-instance-textarea,
64
+ .nrchkb-editor .nrchkb-plugin-instance-select {
65
+ width: 100%;
66
+ box-sizing: border-box;
67
+ }
68
+
69
+ .nrchkb-editor .form-row > .nrchkb-plugin-instance-textarea {
70
+ flex: 1 1 auto;
71
+ width: auto !important;
72
+ }
73
+
74
+ .nrchkb-editor .nrchkb-plugin-instance-textarea-row {
75
+ align-items: flex-start;
76
+ flex-wrap: wrap;
77
+ }
78
+
79
+ .nrchkb-editor .nrchkb-plugin-instance-textarea {
80
+ min-height: 92px;
81
+ padding: 6px 8px;
82
+ resize: vertical;
83
+ font-family: var(--red-ui-monospace-font, monospace);
84
+ line-height: 1.35;
85
+ }
86
+
87
+ .nrchkb-editor .nrchkb-plugin-instance-help {
88
+ flex: 1 1 100%;
89
+ margin: -3px 0 3px 160px;
90
+ color: var(--red-ui-secondary-text-color, #666);
91
+ font-size: 12px;
92
+ line-height: 1.35;
93
+ }
94
+
95
+ @media (max-width: 420px) {
96
+ .nrchkb-editor .nrchkb-plugin-instance-help {
97
+ margin-left: 0;
98
+ }
99
+ }
100
+ </style>
101
+
102
+ <script data-template-name="homekit-plugin-instance" type="text/x-red">
103
+ <div class="nrchkb-editor">
104
+ <input type="hidden" id="node-config-input-pluginId">
105
+ <input type="hidden" id="node-config-input-pluginConfig">
106
+
107
+ <div id="node-config-input-plugin-instance-editor"></div>
108
+ </div>
109
+ </script>
110
+
111
+ <script data-help-name="homekit-plugin-instance" type="text/markdown">
112
+ # HomeKit Plugin Instance
113
+
114
+ Generic NRCHKB plugin instance used by service nodes to track plugin dependencies.
115
+
116
+ > [!NOTE]
117
+ > Plugin instances are created and managed from plugin-aware service editors.
118
+ </script>
119
+
120
+ <script type="text/javascript">
121
+ RED.nodes.registerType('homekit-plugin-instance', {
122
+ category: 'config',
123
+ defaults: {
124
+ pluginId: {value: ''},
125
+ pluginConfig: {value: '{}'},
126
+ controller: {
127
+ value: '',
128
+ type: 'homekit-unifi-controller',
129
+ required: false,
130
+ validate: function () { return true }
131
+ },
132
+ },
133
+ label: function () {
134
+ const plugin = (window.NRCHKBAvailablePlugins || []).find(function (entry) {
135
+ return entry.id === this.pluginId
136
+ }, this)
137
+ return plugin ? plugin.displayName : 'HomeKit Plugin Instance'
138
+ },
139
+ onadd: function () {
140
+ applyDefaultNodeDocumentation(this, 'homekit-plugin-instance')
141
+ },
142
+ oneditprepare: function () {
143
+ const node = this
144
+ const draft = window.NRCHKBPluginInstanceDraft || {}
145
+ const pluginId = node.pluginId || draft.pluginId || ''
146
+ const pluginConfig = $.extend(true, {}, draft.pluginConfig || {}, parsePluginConfig(node.pluginConfig))
147
+ if (node.controller && !pluginConfig.controller) {
148
+ pluginConfig.controller = node.controller
149
+ }
150
+
151
+ $('#node-config-input-pluginId').val(pluginId)
152
+ $('#node-config-input-pluginConfig').val(JSON.stringify(pluginConfig))
153
+
154
+ $.getJSON('nrchkb/plugins', function (plugins) {
155
+ window.NRCHKBAvailablePlugins = plugins || []
156
+ const plugin = (plugins || []).find(function (entry) {
157
+ return entry.id === pluginId
158
+ })
159
+ renderPluginEditor(plugin, pluginConfig)
160
+ }).fail(function () {
161
+ RED.notify('Unable to load NRCHKB plugin metadata.', 'warning')
162
+ renderPluginEditor(undefined, pluginConfig)
163
+ })
164
+ },
165
+ oneditsave: function () {
166
+ const config = {}
167
+
168
+ $('[data-plugin-config-path]').each(function () {
169
+ const input = $(this)
170
+ const field = input.data('pluginField') || {}
171
+ const path = input.attr('data-plugin-config-path')
172
+
173
+ if (!path) {
174
+ return
175
+ }
176
+
177
+ if (field.type === 'checkbox') {
178
+ setConfigValue(config, path, input.is(':checked'))
179
+ } else if (field.type === 'number') {
180
+ const value = input.val()
181
+ setConfigValue(config, path, value === '' || value === undefined ? undefined : Number(value))
182
+ } else if (field.type === 'dynamic-select') {
183
+ setConfigValue(config, path, input.val() || input.data('pluginDynamicValue') || '')
184
+ } else {
185
+ setConfigValue(config, path, input.val())
186
+ }
187
+ })
188
+
189
+ const pluginId = $('#node-config-input-pluginId').val()
190
+ const controller = config.controller || $('#node-config-input-controller').val() || ''
191
+ if (controller) {
192
+ setConfigValue(config, 'controller', controller)
193
+ }
194
+ const pluginConfig = JSON.stringify(config)
195
+
196
+ $('#node-config-input-pluginId').val(pluginId)
197
+ $('#node-config-input-pluginConfig').val(pluginConfig)
198
+ $('#node-config-input-controller').val(controller)
199
+
200
+ this.pluginId = pluginId
201
+ this.pluginConfig = pluginConfig
202
+ this.controller = controller
203
+ delete window.NRCHKBPluginInstanceDraft
204
+ },
205
+ oneditcancel: function () {
206
+ delete window.NRCHKBPluginInstanceDraft
207
+ },
208
+ })
209
+
210
+ function parsePluginConfig(value) {
211
+ if (!value) {
212
+ return {}
213
+ }
214
+
215
+ try {
216
+ return JSON.parse(value)
217
+ } catch (_) {
218
+ return {}
219
+ }
220
+ }
221
+
222
+ function getPathParts(path) {
223
+ return typeof path === 'string' && path.length
224
+ ? path.split('.').filter(function (part) {
225
+ return part.length > 0
226
+ })
227
+ : []
228
+ }
229
+
230
+ function getConfigValue(config, path, defaultValue) {
231
+ const parts = getPathParts(path)
232
+ let current = config
233
+
234
+ for (let index = 0; index < parts.length; index += 1) {
235
+ if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, parts[index])) {
236
+ return defaultValue
237
+ }
238
+
239
+ current = current[parts[index]]
240
+ }
241
+
242
+ return current === undefined ? defaultValue : current
243
+ }
244
+
245
+ function setConfigValue(config, path, value) {
246
+ const parts = getPathParts(path)
247
+ let current = config
248
+
249
+ parts.forEach(function (part, index) {
250
+ if (index === parts.length - 1) {
251
+ current[part] = value
252
+ return
253
+ }
254
+
255
+ if (!current[part] || typeof current[part] !== 'object') {
256
+ current[part] = {}
257
+ }
258
+
259
+ current = current[part]
260
+ })
261
+ }
262
+
263
+ function createFieldId(field) {
264
+ if (field.type === 'config-node' && field.path === 'controller' && field.configNodeType === 'homekit-unifi-controller') {
265
+ return 'node-config-input-controller'
266
+ }
267
+
268
+ return 'node-config-input-plugin-' + (field.path || 'field').replace(/[^a-zA-Z0-9_-]/g, '-')
269
+ }
270
+
271
+ function renderPluginEditor(plugin, config) {
272
+ const editorContainer = $('#node-config-input-plugin-instance-editor').empty()
273
+
274
+ if (!plugin) {
275
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
276
+ .text('Plugin metadata is not available.')
277
+ .appendTo(editorContainer)
278
+ return
279
+ }
280
+
281
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
282
+ .text(plugin.displayName + ' v' + plugin.version + '. ' + plugin.description)
283
+ .appendTo(editorContainer)
284
+
285
+ const sections = plugin.editor && Array.isArray(plugin.editor.sections)
286
+ ? plugin.editor.sections
287
+ : []
288
+
289
+ if (!sections.length) {
290
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
291
+ .text('This plugin does not provide editor fields.')
292
+ .appendTo(editorContainer)
293
+ return
294
+ }
295
+
296
+ sections.forEach(function (section, index) {
297
+ const details = $('<details/>', {
298
+ class: 'nrchkb-section nrchkb-plugin-instance-section'
299
+ }).appendTo(editorContainer)
300
+
301
+ if (section.collapsed !== true) {
302
+ details.prop('open', true)
303
+ }
304
+
305
+ const summary = $('<summary/>').appendTo(details)
306
+ $('<i/>', {class: 'fa ' + (section.icon || 'fa-sliders')}).appendTo(summary)
307
+ $('<span/>', {class: 'nrchkb-plugin-instance-section-title'})
308
+ .text(section.title || 'Settings ' + (index + 1))
309
+ .appendTo(summary)
310
+
311
+ const body = $('<div/>', {class: 'nrchkb-section-body'}).appendTo(details)
312
+
313
+ if (section.description) {
314
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
315
+ .text(section.description)
316
+ .appendTo(body)
317
+ }
318
+
319
+ const fields = $('<div/>', {class: 'nrchkb-plugin-instance-fields'}).appendTo(body)
320
+ ;(section.fields || []).forEach(function (field) {
321
+ renderPluginField(fields, field, config)
322
+ })
323
+ })
324
+
325
+ editorContainer.find('[data-plugin-dynamic-select="true"]').each(function () {
326
+ loadDynamicPluginSelect(editorContainer, $(this))
327
+ })
328
+
329
+ editorContainer.on('change', '[data-plugin-config-path]', function (event) {
330
+ const changedInput = $(event.target)
331
+ const changedPath = $(event.target).attr('data-plugin-config-path') || ''
332
+ const changedField = changedInput.data('pluginField') || {}
333
+
334
+ if (changedField.type === 'dynamic-select') {
335
+ changedInput.data('pluginDynamicValue', changedInput.val() || '')
336
+ }
337
+
338
+ editorContainer.find('[data-plugin-dynamic-select="true"]').each(function () {
339
+ const dynamicInput = $(this)
340
+ const dynamicField = dynamicInput.data('pluginField') || {}
341
+
342
+ if (dynamicField.optionsDependsOn === changedPath) {
343
+ dynamicInput.data(
344
+ 'pluginDynamicValue',
345
+ dynamicInput.val() || dynamicInput.data('pluginDynamicValue') || ''
346
+ )
347
+ loadDynamicPluginSelect(editorContainer, dynamicInput)
348
+ }
349
+ })
350
+ })
351
+ }
352
+
353
+ function renderPluginField(container, field, config) {
354
+ const fieldId = createFieldId(field)
355
+ const value = getConfigValue(config, field.path, field.default)
356
+ if (fieldId === 'node-config-input-controller') {
357
+ $('#' + fieldId).remove()
358
+ }
359
+ const row = $('<div/>', {
360
+ class: 'form-row' + (field.type === 'checkbox'
361
+ ? ' nrchkb-checkbox-row nrchkb-plugin-instance-check-row'
362
+ : field.type === 'textarea'
363
+ ? ' nrchkb-plugin-instance-textarea-row'
364
+ : '')
365
+ }).appendTo(container)
366
+
367
+ if (field.type === 'checkbox') {
368
+ const label = $('<label/>', {
369
+ class: 'nrchkb-checkbox-label nrchkb-plugin-instance-check',
370
+ for: fieldId
371
+ }).appendTo(row)
372
+ const input = $('<input/>', {type: 'checkbox', id: fieldId}).appendTo(label)
373
+ $('<span/>')
374
+ .append(field.icon ? $('<i/>', {class: 'fa ' + field.icon}) : $())
375
+ .append(field.icon ? ' ' : '')
376
+ .append(document.createTextNode(field.label || field.path))
377
+ .appendTo(label)
378
+ input.prop('checked', !!value)
379
+ attachFieldMetadata(input, field)
380
+ return
381
+ }
382
+
383
+ const label = $('<label/>', {for: fieldId}).appendTo(row)
384
+ if (field.icon) {
385
+ $('<i/>', {class: 'fa ' + field.icon}).appendTo(label)
386
+ label.append(' ')
387
+ }
388
+ label.append(document.createTextNode(field.label || field.path))
389
+
390
+ let input
391
+ if (field.type === 'select' || field.type === 'dynamic-select') {
392
+ input = $('<select/>', {id: fieldId, class: 'nrchkb-plugin-instance-select'}).appendTo(row)
393
+ ;(field.options || []).forEach(function (option) {
394
+ $('<option/>').val(option.value).text(option.label).appendTo(input)
395
+ })
396
+ input.val(value === undefined ? '' : value)
397
+ } else if (field.type === 'textarea') {
398
+ input = $('<textarea/>', {
399
+ id: fieldId,
400
+ class: 'nrchkb-plugin-instance-textarea',
401
+ rows: 4
402
+ }).appendTo(row)
403
+ input.val(value === undefined ? '' : value)
404
+ if (field.placeholder) {
405
+ const helpId = fieldId + '-help'
406
+ input.attr('aria-describedby', helpId)
407
+ $('<div/>', {
408
+ id: helpId,
409
+ class: 'nrchkb-plugin-instance-help'
410
+ }).text(field.placeholder).appendTo(row)
411
+ }
412
+ } else {
413
+ input = $('<input/>', {
414
+ type: field.type === 'number' ? 'number' : field.type === 'password' ? 'password' : 'text',
415
+ id: fieldId,
416
+ placeholder: field.placeholder || ''
417
+ }).appendTo(row)
418
+ input.val(value === undefined ? '' : value)
419
+ }
420
+
421
+ attachFieldMetadata(input, field)
422
+
423
+ if (field.type === 'config-node' && field.configNodeType && RED.editor && RED.editor.prepareConfigNodeSelect) {
424
+ const property = fieldId.replace(/^node-config-input-/, '')
425
+ const configNode = {}
426
+ configNode[property] = value === undefined ? '' : value
427
+
428
+ RED.editor.prepareConfigNodeSelect(
429
+ configNode,
430
+ property,
431
+ field.configNodeType,
432
+ 'node-config-input'
433
+ )
434
+
435
+ attachFieldMetadata($('#' + fieldId), field)
436
+ }
437
+
438
+ if (field.type === 'dynamic-select') {
439
+ input.attr('data-plugin-dynamic-select', 'true')
440
+ input.data('pluginDynamicValue', value === undefined ? '' : value)
441
+ }
442
+ }
443
+
444
+ function attachFieldMetadata(input, field) {
445
+ input
446
+ .attr('data-plugin-config-path', field.path)
447
+ .data('pluginField', field)
448
+ }
449
+
450
+ function loadDynamicPluginSelect(container, input) {
451
+ const field = input.data('pluginField') || {}
452
+ const selectedValue = input.val() || input.data('pluginDynamicValue') || ''
453
+
454
+ if (!field.optionsUrl) {
455
+ return
456
+ }
457
+
458
+ let url = field.optionsUrl
459
+ if (field.optionsDependsOn) {
460
+ const dependsInput = container.find('[data-plugin-config-path="' + field.optionsDependsOn + '"]')
461
+ const dependsValue = dependsInput.val()
462
+
463
+ if (!dependsValue) {
464
+ input.empty()
465
+ $('<option/>').val('').text('Choose a dependency first').appendTo(input)
466
+ input.val('')
467
+ return
468
+ }
469
+
470
+ url = url.replace('{' + field.optionsDependsOn + '}', encodeURIComponent(dependsValue))
471
+ }
472
+
473
+ input.empty()
474
+ $('<option/>').val('').text('Loading...').appendTo(input)
475
+
476
+ $.getJSON(url, function (options) {
477
+ input.empty()
478
+ $('<option/>').val('').text('Choose...').appendTo(input)
479
+ ;(options || []).forEach(function (option) {
480
+ $('<option/>').val(option.value).text(option.label).appendTo(input)
481
+ })
482
+ if (selectedValue && input.find('option').filter(function () {
483
+ return $(this).val() === selectedValue
484
+ }).length === 0) {
485
+ $('<option/>').val(selectedValue).text('Saved value (' + selectedValue + ')').appendTo(input)
486
+ }
487
+ input.val(selectedValue)
488
+ input.data('pluginDynamicValue', input.val() || selectedValue)
489
+ }).fail(function () {
490
+ input.empty()
491
+ $('<option/>').val('').text('Unable to load options').appendTo(input)
492
+ if (selectedValue) {
493
+ $('<option/>').val(selectedValue).text('Saved value (' + selectedValue + ')').appendTo(input)
494
+ input.val(selectedValue)
495
+ }
496
+ input.data('pluginDynamicValue', input.val() || selectedValue)
497
+ })
498
+ }
499
+ </script>