node-red-contrib-homekit-bridged 2.0.0-dev.1 → 2.0.0-dev.10

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 +185 -146
  2. package/build/lib/HAPServiceNode.js +200 -177
  3. package/build/lib/HAPServiceNode2.js +208 -177
  4. package/build/lib/NRCHKBError.js +23 -2
  5. package/build/lib/PairingQRCode.js +62 -0
  6. package/build/lib/Storage.js +152 -90
  7. package/build/lib/api.js +654 -290
  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 -112
  39. package/build/lib/utils/BridgeUtils.js +95 -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 +433 -368
  45. package/build/lib/utils/ServiceUtils2.js +519 -304
  46. package/build/lib/utils/index.js +11 -12
  47. package/build/nodes/bridge.html +206 -168
  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 +1753 -117
  51. package/build/nodes/nrchkb.js +66 -88
  52. package/build/nodes/plugin-instance.html +509 -0
  53. package/build/nodes/plugin-instance.js +46 -0
  54. package/build/nodes/service.html +557 -306
  55. package/build/nodes/service.js +5 -6
  56. package/build/nodes/service2.html +1735 -455
  57. package/build/nodes/service2.js +5 -8
  58. package/build/nodes/standalone.html +208 -176
  59. package/build/nodes/standalone.js +27 -9
  60. package/build/nodes/status.html +51 -18
  61. package/build/nodes/status.js +47 -41
  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 -82
  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 hap_nodejs_1 = require("@homebridge/hap-nodejs");
40
- const logger_1 = require("@nrchkb/logger");
41
- const path = __importStar(require("path"));
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('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,509 @@
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 appendFieldHelp(row, input, fieldId, field) {
272
+ const helpText = field.description || field.placeholder || ''
273
+
274
+ if (!helpText) {
275
+ return
276
+ }
277
+
278
+ const helpId = fieldId + '-help'
279
+ input.attr('aria-describedby', helpId)
280
+ $('<div/>', {
281
+ id: helpId,
282
+ class: 'nrchkb-plugin-instance-help'
283
+ }).text(helpText).appendTo(row)
284
+ }
285
+
286
+ function renderPluginEditor(plugin, config) {
287
+ const editorContainer = $('#node-config-input-plugin-instance-editor').empty()
288
+
289
+ if (!plugin) {
290
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
291
+ .text('Plugin metadata is not available.')
292
+ .appendTo(editorContainer)
293
+ return
294
+ }
295
+
296
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
297
+ .text(plugin.displayName + ' v' + plugin.version + '. ' + plugin.description)
298
+ .appendTo(editorContainer)
299
+
300
+ const sections = plugin.editor && Array.isArray(plugin.editor.sections)
301
+ ? plugin.editor.sections
302
+ : []
303
+
304
+ if (!sections.length) {
305
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
306
+ .text('This plugin does not provide editor fields.')
307
+ .appendTo(editorContainer)
308
+ return
309
+ }
310
+
311
+ sections.forEach(function (section, index) {
312
+ const details = $('<details/>', {
313
+ class: 'nrchkb-section nrchkb-plugin-instance-section'
314
+ }).appendTo(editorContainer)
315
+
316
+ if (section.collapsed !== true) {
317
+ details.prop('open', true)
318
+ }
319
+
320
+ const summary = $('<summary/>').appendTo(details)
321
+ $('<i/>', {class: 'fa ' + (section.icon || 'fa-sliders')}).appendTo(summary)
322
+ $('<span/>', {class: 'nrchkb-plugin-instance-section-title'})
323
+ .text(section.title || 'Settings ' + (index + 1))
324
+ .appendTo(summary)
325
+
326
+ const body = $('<div/>', {class: 'nrchkb-section-body'}).appendTo(details)
327
+
328
+ if (section.description) {
329
+ $('<div/>', {class: 'nrchkb-plugin-instance-about'})
330
+ .text(section.description)
331
+ .appendTo(body)
332
+ }
333
+
334
+ const fields = $('<div/>', {class: 'nrchkb-plugin-instance-fields'}).appendTo(body)
335
+ ;(section.fields || []).forEach(function (field) {
336
+ renderPluginField(fields, field, config)
337
+ })
338
+ })
339
+
340
+ editorContainer.find('[data-plugin-dynamic-select="true"]').each(function () {
341
+ loadDynamicPluginSelect(editorContainer, $(this))
342
+ })
343
+
344
+ editorContainer.on('change', '[data-plugin-config-path]', function (event) {
345
+ const changedInput = $(event.target)
346
+ const changedPath = $(event.target).attr('data-plugin-config-path') || ''
347
+ const changedField = changedInput.data('pluginField') || {}
348
+
349
+ if (changedField.type === 'dynamic-select') {
350
+ changedInput.data('pluginDynamicValue', changedInput.val() || '')
351
+ }
352
+
353
+ editorContainer.find('[data-plugin-dynamic-select="true"]').each(function () {
354
+ const dynamicInput = $(this)
355
+ const dynamicField = dynamicInput.data('pluginField') || {}
356
+
357
+ if (dynamicField.optionsDependsOn === changedPath) {
358
+ dynamicInput.data(
359
+ 'pluginDynamicValue',
360
+ dynamicInput.val() || dynamicInput.data('pluginDynamicValue') || ''
361
+ )
362
+ loadDynamicPluginSelect(editorContainer, dynamicInput)
363
+ }
364
+ })
365
+ })
366
+ }
367
+
368
+ function renderPluginField(container, field, config) {
369
+ const fieldId = createFieldId(field)
370
+ const value = getConfigValue(config, field.path, field.default)
371
+ if (fieldId === 'node-config-input-controller') {
372
+ $('#' + fieldId).remove()
373
+ }
374
+ const row = $('<div/>', {
375
+ class: 'form-row' + (field.type === 'checkbox'
376
+ ? ' nrchkb-checkbox-row nrchkb-plugin-instance-check-row'
377
+ : field.type === 'textarea'
378
+ ? ' nrchkb-plugin-instance-textarea-row'
379
+ : '')
380
+ }).appendTo(container)
381
+
382
+ if (field.type === 'checkbox') {
383
+ const label = $('<label/>', {
384
+ class: 'nrchkb-checkbox-label nrchkb-plugin-instance-check',
385
+ for: fieldId
386
+ }).appendTo(row)
387
+ const input = $('<input/>', {type: 'checkbox', id: fieldId}).appendTo(label)
388
+ $('<span/>')
389
+ .append(field.icon ? $('<i/>', {class: 'fa ' + field.icon}) : $())
390
+ .append(field.icon ? ' ' : '')
391
+ .append(document.createTextNode(field.label || field.path))
392
+ .appendTo(label)
393
+ input.prop('checked', !!value)
394
+ attachFieldMetadata(input, field)
395
+ appendFieldHelp(row, input, fieldId, field)
396
+ return
397
+ }
398
+
399
+ const label = $('<label/>', {for: fieldId}).appendTo(row)
400
+ if (field.icon) {
401
+ $('<i/>', {class: 'fa ' + field.icon}).appendTo(label)
402
+ label.append(' ')
403
+ }
404
+ label.append(document.createTextNode(field.label || field.path))
405
+
406
+ let input
407
+ if (field.type === 'select' || field.type === 'dynamic-select') {
408
+ input = $('<select/>', {id: fieldId, class: 'nrchkb-plugin-instance-select'}).appendTo(row)
409
+ ;(field.options || []).forEach(function (option) {
410
+ $('<option/>').val(option.value).text(option.label).appendTo(input)
411
+ })
412
+ input.val(value === undefined ? '' : value)
413
+ } else if (field.type === 'textarea') {
414
+ input = $('<textarea/>', {
415
+ id: fieldId,
416
+ class: 'nrchkb-plugin-instance-textarea',
417
+ rows: 4
418
+ }).appendTo(row)
419
+ input.val(value === undefined ? '' : value)
420
+ } else {
421
+ input = $('<input/>', {
422
+ type: field.type === 'number' ? 'number' : field.type === 'password' ? 'password' : 'text',
423
+ id: fieldId,
424
+ placeholder: field.placeholder || ''
425
+ }).appendTo(row)
426
+ input.val(value === undefined ? '' : value)
427
+ }
428
+
429
+ attachFieldMetadata(input, field)
430
+
431
+ if (field.type === 'config-node' && field.configNodeType && RED.editor && RED.editor.prepareConfigNodeSelect) {
432
+ const property = fieldId.replace(/^node-config-input-/, '')
433
+ const configNode = {}
434
+ configNode[property] = value === undefined ? '' : value
435
+
436
+ RED.editor.prepareConfigNodeSelect(
437
+ configNode,
438
+ property,
439
+ field.configNodeType,
440
+ 'node-config-input'
441
+ )
442
+
443
+ attachFieldMetadata($('#' + fieldId), field)
444
+ }
445
+
446
+ if (field.type === 'dynamic-select') {
447
+ input.attr('data-plugin-dynamic-select', 'true')
448
+ input.data('pluginDynamicValue', value === undefined ? '' : value)
449
+ }
450
+
451
+ appendFieldHelp(row, input, fieldId, field)
452
+ }
453
+
454
+ function attachFieldMetadata(input, field) {
455
+ input
456
+ .attr('data-plugin-config-path', field.path)
457
+ .data('pluginField', field)
458
+ }
459
+
460
+ function loadDynamicPluginSelect(container, input) {
461
+ const field = input.data('pluginField') || {}
462
+ const selectedValue = input.val() || input.data('pluginDynamicValue') || ''
463
+
464
+ if (!field.optionsUrl) {
465
+ return
466
+ }
467
+
468
+ let url = field.optionsUrl
469
+ if (field.optionsDependsOn) {
470
+ const dependsInput = container.find('[data-plugin-config-path="' + field.optionsDependsOn + '"]')
471
+ const dependsValue = dependsInput.val()
472
+
473
+ if (!dependsValue) {
474
+ input.empty()
475
+ $('<option/>').val('').text('Choose a dependency first').appendTo(input)
476
+ input.val('')
477
+ return
478
+ }
479
+
480
+ url = url.replace('{' + field.optionsDependsOn + '}', encodeURIComponent(dependsValue))
481
+ }
482
+
483
+ input.empty()
484
+ $('<option/>').val('').text('Loading...').appendTo(input)
485
+
486
+ $.getJSON(url, function (options) {
487
+ input.empty()
488
+ $('<option/>').val('').text('Choose...').appendTo(input)
489
+ ;(options || []).forEach(function (option) {
490
+ $('<option/>').val(option.value).text(option.label).appendTo(input)
491
+ })
492
+ if (selectedValue && input.find('option').filter(function () {
493
+ return $(this).val() === selectedValue
494
+ }).length === 0) {
495
+ $('<option/>').val(selectedValue).text('Saved value (' + selectedValue + ')').appendTo(input)
496
+ }
497
+ input.val(selectedValue)
498
+ input.data('pluginDynamicValue', input.val() || selectedValue)
499
+ }).fail(function () {
500
+ input.empty()
501
+ $('<option/>').val('').text('Unable to load options').appendTo(input)
502
+ if (selectedValue) {
503
+ $('<option/>').val(selectedValue).text('Saved value (' + selectedValue + ')').appendTo(input)
504
+ input.val(selectedValue)
505
+ }
506
+ input.data('pluginDynamicValue', input.val() || selectedValue)
507
+ })
508
+ }
509
+ </script>