homebridge-ratgdo 2.1.3 → 2.2.1
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.
- package/README.md +6 -4
- package/config.schema.json +1 -1
- package/dist/ratgdo-device.d.ts +3 -4
- package/dist/ratgdo-device.js +22 -48
- package/dist/ratgdo-device.js.map +1 -1
- package/dist/ratgdo-options.d.ts +4 -16
- package/dist/ratgdo-options.js +6 -97
- package/dist/ratgdo-options.js.map +1 -1
- package/dist/ratgdo-platform.d.ts +4 -4
- package/dist/ratgdo-platform.js +43 -54
- package/dist/ratgdo-platform.js.map +1 -1
- package/dist/ratgdo-types.d.ts +0 -6
- package/dist/ratgdo-types.js.map +1 -1
- package/homebridge-ui/public/index.html +20 -8
- package/homebridge-ui/public/lib/featureoptions.js +376 -0
- package/homebridge-ui/public/lib/featureoptions.js.map +1 -0
- package/homebridge-ui/public/lib/webUi-featureoptions.mjs +836 -0
- package/homebridge-ui/public/lib/webUi.mjs +184 -0
- package/homebridge-ui/public/ui.mjs +9 -136
- package/homebridge-ui/server.js +4 -39
- package/package.json +17 -15
- package/dist/ratgdo-mqtt.d.ts +0 -19
- package/dist/ratgdo-mqtt.js +0 -155
- package/dist/ratgdo-mqtt.js.map +0 -1
- package/homebridge-ui/public/lib/featureoptions.mjs +0 -200
- package/homebridge-ui/public/ratgdo-featureoptions.mjs +0 -638
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* webUi.mjs: Plugin webUI.
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { webUiFeatureOptions } from "./webUi-featureoptions.mjs";
|
|
8
|
+
|
|
9
|
+
export class webUi {
|
|
10
|
+
|
|
11
|
+
// Feature options class instance.
|
|
12
|
+
featureOptions;
|
|
13
|
+
|
|
14
|
+
// First run webUI callback endpoints for customization.
|
|
15
|
+
#firstRun;
|
|
16
|
+
|
|
17
|
+
// Plugin name.
|
|
18
|
+
#name;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* featureOptions - parameters to webUiFeatureOptions.
|
|
22
|
+
* firstRun - first run handlers:
|
|
23
|
+
* isRequired - do we need to run the first run UI workflow?
|
|
24
|
+
* onStart - initialization for the first run webUI to populate forms and other startup tasks.
|
|
25
|
+
* onSubmit - execute the first run workflow, typically a login or configuration validation of some sort.
|
|
26
|
+
* name - plugin name.
|
|
27
|
+
*/
|
|
28
|
+
constructor({ featureOptions, firstRun = {}, name } = {}) {
|
|
29
|
+
|
|
30
|
+
// Defaults for our first run handlers.
|
|
31
|
+
this.firstRun = { isRequired: () => false, onStart: () => true, onSubmit: () => true };
|
|
32
|
+
|
|
33
|
+
// Figure out the options passed in to us.
|
|
34
|
+
this.featureOptions = new webUiFeatureOptions(featureOptions);
|
|
35
|
+
this.firstRun = Object.assign({}, this.firstRun, firstRun);
|
|
36
|
+
this.name = name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Render the webUI.
|
|
41
|
+
*/
|
|
42
|
+
// Render the UI.
|
|
43
|
+
show() {
|
|
44
|
+
|
|
45
|
+
// Fire off our UI, catching errors along the way.
|
|
46
|
+
try {
|
|
47
|
+
|
|
48
|
+
this.#launchWebUI();
|
|
49
|
+
} catch(err) {
|
|
50
|
+
|
|
51
|
+
// If we had an error instantiating or updating the UI, notify the user.
|
|
52
|
+
homebridge.toast.error(err.message, "Error");
|
|
53
|
+
} finally {
|
|
54
|
+
|
|
55
|
+
// Always leave the UI in a usable place for the end user.
|
|
56
|
+
homebridge.hideSpinner();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Show the first run user experience if we don't have valid login credentials.
|
|
61
|
+
async #showFirstRun() {
|
|
62
|
+
|
|
63
|
+
const buttonFirstRun = document.getElementById("firstRun");
|
|
64
|
+
|
|
65
|
+
// Run a custom initialization handler the user may have provided.
|
|
66
|
+
if(!(await this.#processHandler(this.firstRun.onStart))) {
|
|
67
|
+
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// First run user experience.
|
|
72
|
+
buttonFirstRun.addEventListener("click", async () => {
|
|
73
|
+
|
|
74
|
+
// Show the beachball while we setup.
|
|
75
|
+
homebridge.showSpinner();
|
|
76
|
+
|
|
77
|
+
// Run a custom submit handler the user may have provided.
|
|
78
|
+
if(!(await this.#processHandler(this.firstRun.onSubmit))) {
|
|
79
|
+
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create our UI.
|
|
84
|
+
document.getElementById("pageFirstRun").style.display = "none";
|
|
85
|
+
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
86
|
+
this.featureOptions.show();
|
|
87
|
+
|
|
88
|
+
// All done. Let the user interact with us, although in practice, we shouldn't get here.
|
|
89
|
+
// homebridge.hideSpinner();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
document.getElementById("pageFirstRun").style.display = "block";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Show the main plugin configuration tab.
|
|
96
|
+
#showSettings() {
|
|
97
|
+
|
|
98
|
+
// Show the beachball while we setup.
|
|
99
|
+
homebridge.showSpinner();
|
|
100
|
+
|
|
101
|
+
// Highlight the tab in our UI.
|
|
102
|
+
this.#toggleClasses("menuHome", "btn-elegant", "btn-primary");
|
|
103
|
+
this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary");
|
|
104
|
+
this.#toggleClasses("menuSettings", "btn-primary", "btn-elegant");
|
|
105
|
+
|
|
106
|
+
document.getElementById("pageSupport").style.display = "none";
|
|
107
|
+
document.getElementById("pageFeatureOptions").style.display = "none";
|
|
108
|
+
|
|
109
|
+
homebridge.showSchemaForm();
|
|
110
|
+
|
|
111
|
+
// All done. Let the user interact with us.
|
|
112
|
+
homebridge.hideSpinner();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Show the support tab.
|
|
116
|
+
#showSupport() {
|
|
117
|
+
|
|
118
|
+
// Show the beachball while we setup.
|
|
119
|
+
homebridge.showSpinner();
|
|
120
|
+
homebridge.hideSchemaForm();
|
|
121
|
+
|
|
122
|
+
// Highlight the tab in our UI.
|
|
123
|
+
this.#toggleClasses("menuHome", "btn-primary", "btn-elegant");
|
|
124
|
+
this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary");
|
|
125
|
+
this.#toggleClasses("menuSettings", "btn-elegant", "btn-primary");
|
|
126
|
+
|
|
127
|
+
document.getElementById("pageSupport").style.display = "block";
|
|
128
|
+
document.getElementById("pageFeatureOptions").style.display = "none";
|
|
129
|
+
|
|
130
|
+
// All done. Let the user interact with us.
|
|
131
|
+
homebridge.hideSpinner();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Launch our webUI.
|
|
135
|
+
async #launchWebUI() {
|
|
136
|
+
|
|
137
|
+
// Retrieve the current plugin configuration.
|
|
138
|
+
this.featureOptions.currentConfig = await homebridge.getPluginConfig();
|
|
139
|
+
|
|
140
|
+
// Add our event listeners to animate the UI.
|
|
141
|
+
document.getElementById("menuHome").addEventListener("click", () => this.#showSupport());
|
|
142
|
+
document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.show());
|
|
143
|
+
document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings());
|
|
144
|
+
|
|
145
|
+
// Get the list of devices the plugin knows about.
|
|
146
|
+
const devices = await homebridge.getCachedAccessories();
|
|
147
|
+
|
|
148
|
+
// If we've got devices detected, we launch our feature option UI. Otherwise, we launch our first run UI.
|
|
149
|
+
if(this.featureOptions.currentConfig.length && devices?.length && !(await this.#processHandler(this.firstRun.isRequired))) {
|
|
150
|
+
|
|
151
|
+
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
152
|
+
this.featureOptions.show();
|
|
153
|
+
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If we have the name property set for the plugin configuration yet, let's do so now. If we don't have a configuration, let's initialize it as well.
|
|
158
|
+
(this.featureOptions.currentConfig[0] ??= { name: this.name }).name ??= this.name;
|
|
159
|
+
|
|
160
|
+
// Update the plugin configuration and launch the first run UI.
|
|
161
|
+
await homebridge.updatePluginConfig(this.featureOptions.currentConfig);
|
|
162
|
+
this.#showFirstRun();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Utility to process user-provided custom handlers that can handle both synchronous and asynchronous handlers.
|
|
166
|
+
async #processHandler(handler) {
|
|
167
|
+
|
|
168
|
+
if(((typeof handler === "function") && !(await handler())) || ((typeof handler !== "function") && !handler)) {
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Utility to toggle our classes.
|
|
177
|
+
#toggleClasses(id, removeClass, addClass) {
|
|
178
|
+
|
|
179
|
+
const element = document.getElementById(id);
|
|
180
|
+
|
|
181
|
+
element.classList.remove(removeClass);
|
|
182
|
+
element.classList.add(addClass);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -1,144 +1,17 @@
|
|
|
1
1
|
/* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
|
-
* ui.mjs: Ratgdo webUI.
|
|
3
|
+
* ui.mjs: Homebridge Ratgdo webUI.
|
|
4
4
|
*/
|
|
5
|
-
"use strict";
|
|
6
|
-
|
|
7
|
-
import { ratgdoFeatureOptions } from "./ratgdo-featureoptions.mjs";
|
|
8
|
-
|
|
9
|
-
// Keep a list of all the feature options and option groups.
|
|
10
|
-
const featureOptions = new ratgdoFeatureOptions();
|
|
11
|
-
|
|
12
|
-
// Show the first run user experience if we don't have valid login credentials.
|
|
13
|
-
async function showFirstRun () {
|
|
14
|
-
|
|
15
|
-
const buttonFirstRun = document.getElementById("firstRun");
|
|
16
|
-
const serverPortInfo = document.getElementById("serverPortInfo");
|
|
17
|
-
|
|
18
|
-
serverPortInfo.innerHTML = (featureOptions.currentConfig[0].port ?? "18830");
|
|
19
|
-
|
|
20
|
-
// First run user experience.
|
|
21
|
-
buttonFirstRun.addEventListener("click", async () => {
|
|
22
|
-
|
|
23
|
-
// Show the beachball while we setup.
|
|
24
|
-
homebridge.showSpinner();
|
|
25
|
-
|
|
26
|
-
// Get the list of devices the plugin knows about.
|
|
27
|
-
const ratgdoDevices = await homebridge.getCachedAccessories();
|
|
28
|
-
|
|
29
|
-
// Sort it for posterity.
|
|
30
|
-
ratgdoDevices?.sort((a, b) => {
|
|
31
|
-
|
|
32
|
-
const aCase = (a.displayName ?? "").toLowerCase();
|
|
33
|
-
const bCase = (b.displayName ?? "").toLowerCase();
|
|
34
|
-
|
|
35
|
-
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Create our UI.
|
|
39
|
-
document.getElementById("pageFirstRun").style.display = "none";
|
|
40
|
-
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
41
|
-
featureOptions.showUI();
|
|
42
|
-
|
|
43
|
-
// All done. Let the user interact with us, although in practice, we shouldn't get here.
|
|
44
|
-
// homebridge.hideSpinner();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
document.getElementById("pageFirstRun").style.display = "block";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Show the main plugin configuration tab.
|
|
51
|
-
function showSettings () {
|
|
52
|
-
|
|
53
|
-
// Show the beachball while we setup.
|
|
54
|
-
homebridge.showSpinner();
|
|
55
|
-
|
|
56
|
-
// Create our UI.
|
|
57
|
-
document.getElementById("menuHome").classList.remove("btn-elegant");
|
|
58
|
-
document.getElementById("menuHome").classList.add("btn-primary");
|
|
59
|
-
document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
|
|
60
|
-
document.getElementById("menuFeatureOptions").classList.add("btn-primary");
|
|
61
|
-
document.getElementById("menuSettings").classList.add("btn-elegant");
|
|
62
|
-
document.getElementById("menuSettings").classList.remove("btn-primary");
|
|
63
5
|
|
|
64
|
-
|
|
65
|
-
document.getElementById("pageFeatureOptions").style.display = "none";
|
|
66
|
-
|
|
67
|
-
homebridge.showSchemaForm();
|
|
68
|
-
|
|
69
|
-
// All done. Let the user interact with us.
|
|
70
|
-
homebridge.hideSpinner();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Show the support tab.
|
|
74
|
-
function showSupport() {
|
|
75
|
-
|
|
76
|
-
// Show the beachball while we setup.
|
|
77
|
-
homebridge.showSpinner();
|
|
78
|
-
homebridge.hideSchemaForm();
|
|
79
|
-
|
|
80
|
-
// Create our UI.
|
|
81
|
-
document.getElementById("menuHome").classList.add("btn-elegant");
|
|
82
|
-
document.getElementById("menuHome").classList.remove("btn-primary");
|
|
83
|
-
document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
|
|
84
|
-
document.getElementById("menuFeatureOptions").classList.add("btn-primary");
|
|
85
|
-
document.getElementById("menuSettings").classList.remove("btn-elegant");
|
|
86
|
-
document.getElementById("menuSettings").classList.add("btn-primary");
|
|
87
|
-
|
|
88
|
-
document.getElementById("pageSupport").style.display = "block";
|
|
89
|
-
document.getElementById("pageFeatureOptions").style.display = "none";
|
|
90
|
-
|
|
91
|
-
// All done. Let the user interact with us.
|
|
92
|
-
homebridge.hideSpinner();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Launch our webUI.
|
|
96
|
-
async function launchWebUI() {
|
|
97
|
-
|
|
98
|
-
// Retrieve the current plugin configuration.
|
|
99
|
-
featureOptions.currentConfig = await homebridge.getPluginConfig();
|
|
100
|
-
|
|
101
|
-
// Add our event listeners to animate the UI.
|
|
102
|
-
menuHome.addEventListener("click", () => showSupport());
|
|
103
|
-
menuFeatureOptions.addEventListener("click", () => featureOptions.showUI());
|
|
104
|
-
menuSettings.addEventListener("click", () => showSettings());
|
|
105
|
-
|
|
106
|
-
// Get the list of devices the plugin knows about.
|
|
107
|
-
const ratgdoDevices = await homebridge.getCachedAccessories();
|
|
108
|
-
|
|
109
|
-
// If we've got Ratgdo devices detected, we launch our feature option UI. Otherwise, we launch our first run UI.
|
|
110
|
-
if(featureOptions.currentConfig.length && ratgdoDevices?.length) {
|
|
111
|
-
|
|
112
|
-
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
113
|
-
featureOptions.showUI();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// If we have no configuration, let's create one.
|
|
118
|
-
if(!featureOptions.currentConfig.length) {
|
|
119
|
-
|
|
120
|
-
featureOptions.currentConfig.push({ name: "Ratgdo" });
|
|
121
|
-
} else if(!("name" in featureOptions.currentConfig[0])) {
|
|
122
|
-
|
|
123
|
-
// If we haven't set the name, let's do so now.
|
|
124
|
-
featureOptions.currentConfig[0].name = "Ratgdo";
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Update the plugin configuration and launch the first run UI.
|
|
128
|
-
await homebridge.updatePluginConfig(featureOptions.currentConfig);
|
|
129
|
-
showFirstRun();
|
|
130
|
-
}
|
|
6
|
+
"use strict";
|
|
131
7
|
|
|
132
|
-
|
|
133
|
-
try {
|
|
8
|
+
import { webUi } from "./lib/webUi.mjs";
|
|
134
9
|
|
|
135
|
-
|
|
136
|
-
|
|
10
|
+
// Parameters for our feature options webUI.
|
|
11
|
+
const featureOptionsParams = { hasControllers: false, sidebar: { deviceLabel: "Ratgdo Devices" } };
|
|
137
12
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} finally {
|
|
13
|
+
// Instantiate the webUI.
|
|
14
|
+
const ui = new webUi({ featureOptions: featureOptionsParams, name: "Ratgdo" });
|
|
141
15
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
16
|
+
// Display the webUI.
|
|
17
|
+
ui.show();
|
package/homebridge-ui/server.js
CHANGED
|
@@ -1,58 +1,23 @@
|
|
|
1
1
|
/* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
3
|
* server.js: homebridge-ratgdo webUI server API.
|
|
4
|
-
*
|
|
5
|
-
* This module is heavily inspired by the homebridge-config-ui-x source code and borrows from both.
|
|
6
|
-
* Thank you oznu for your contributions to the HomeKit world.
|
|
7
4
|
*/
|
|
8
5
|
"use strict";
|
|
9
6
|
|
|
10
|
-
import { featureOptionCategories, featureOptions
|
|
7
|
+
import { featureOptionCategories, featureOptions } from "../dist/ratgdo-options.js";
|
|
11
8
|
import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
|
|
12
|
-
import util from "node:util";
|
|
13
9
|
|
|
14
10
|
class PluginUiServer extends HomebridgePluginUiServer {
|
|
15
11
|
|
|
16
|
-
constructor
|
|
12
|
+
constructor() {
|
|
13
|
+
|
|
17
14
|
super();
|
|
18
15
|
|
|
19
16
|
// Register getOptions() with the Homebridge server API.
|
|
20
|
-
this
|
|
17
|
+
this.onRequest("/getOptions", () => ({ categories: featureOptionCategories, options: featureOptions }));
|
|
21
18
|
|
|
22
19
|
this.ready();
|
|
23
20
|
}
|
|
24
|
-
|
|
25
|
-
// Register the getOptions() webUI server API endpoint.
|
|
26
|
-
#registerGetOptions() {
|
|
27
|
-
|
|
28
|
-
// Return the list of options configured for a given Ratgdo device.
|
|
29
|
-
this.onRequest("/getOptions", async(request) => {
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
|
|
33
|
-
const optionSet = {};
|
|
34
|
-
|
|
35
|
-
// Loop through all the feature option categories.
|
|
36
|
-
for(const category of featureOptionCategories) {
|
|
37
|
-
|
|
38
|
-
optionSet[category.name] = [];
|
|
39
|
-
|
|
40
|
-
for(const options of featureOptions[category.name]) {
|
|
41
|
-
|
|
42
|
-
options.value = isOptionEnabled(request.configOptions, request.ratgdoDevice, category.name + "." + options.name, options.default);
|
|
43
|
-
optionSet[category.name].push(options);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return { categories: featureOptionCategories, options: optionSet };
|
|
48
|
-
|
|
49
|
-
} catch(err) {
|
|
50
|
-
|
|
51
|
-
// Return nothing if we error out for some reason.
|
|
52
|
-
return {};
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
21
|
}
|
|
57
22
|
|
|
58
23
|
(() => new PluginUiServer())();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-ratgdo",
|
|
3
3
|
"displayName": "Homebridge Ratgdo",
|
|
4
|
-
"version": "2.1
|
|
4
|
+
"version": "2.2.1",
|
|
5
5
|
"description": "HomeKit integration for LiftMaster and Chamberlain garage door openers, without requiring myQ.",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"repository": {
|
|
@@ -14,35 +14,37 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18.0",
|
|
17
|
-
"homebridge": "
|
|
17
|
+
"homebridge": ">=1.8.0"
|
|
18
18
|
},
|
|
19
19
|
"main": "dist/index.js",
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"build": "
|
|
21
|
+
"prebuild": "npm run clean && npm run build-ui",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"build-ui": "shx mkdir -p homebridge-ui/public/lib && shx cp \"node_modules/homebridge-plugin-utils/dist/ui/**/*.@(js|mjs){,.map}\" homebridge-ui/public/lib",
|
|
24
|
+
"clean": "shx rm -rf dist homebridge-ui/public/lib",
|
|
25
|
+
"prelint": "npm run build-ui",
|
|
26
|
+
"lint": "eslint eslint.config.mjs src/**.ts homebridge-ui/*.js homebridge-ui/public/**/*.mjs",
|
|
27
|
+
"postpublish": "npm run clean",
|
|
24
28
|
"prepublishOnly": "npm run lint && npm run build"
|
|
25
29
|
},
|
|
26
30
|
"keywords": [
|
|
27
31
|
"homebridge-plugin"
|
|
28
32
|
],
|
|
29
33
|
"devDependencies": {
|
|
30
|
-
"@stylistic/eslint-plugin": "^1.
|
|
34
|
+
"@stylistic/eslint-plugin": "^2.1.0",
|
|
31
35
|
"@types/eventsource": "^1.1.15",
|
|
32
|
-
"@types/node": "^20.
|
|
33
|
-
"eslint": "
|
|
34
|
-
"homebridge": "^1.8.
|
|
35
|
-
"
|
|
36
|
-
"rimraf": "^5.0.5",
|
|
37
|
-
"ts-node": "^10.9.2",
|
|
36
|
+
"@types/node": "^20.13.0",
|
|
37
|
+
"eslint": "8.57.0",
|
|
38
|
+
"homebridge": "^1.8.2",
|
|
39
|
+
"shx": "^0.3.4",
|
|
38
40
|
"typescript": "^5.4.5",
|
|
39
|
-
"typescript-eslint": "^7.
|
|
41
|
+
"typescript-eslint": "^7.11.0"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@adobe/fetch": "^4.1.
|
|
44
|
+
"@adobe/fetch": "^4.1.3",
|
|
43
45
|
"@homebridge/plugin-ui-utils": "^1.0.3",
|
|
44
46
|
"bonjour-service": "^1.2.1",
|
|
45
47
|
"eventsource": "^2.0.2",
|
|
46
|
-
"
|
|
48
|
+
"homebridge-plugin-utils": "^1.2.0"
|
|
47
49
|
}
|
|
48
50
|
}
|
package/dist/ratgdo-mqtt.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { RatgdoAccessory } from "./ratgdo-device.js";
|
|
3
|
-
import { RatgdoPlatform } from "./ratgdo-platform.js";
|
|
4
|
-
export declare class RatgdoMqtt {
|
|
5
|
-
private config;
|
|
6
|
-
private isConnected;
|
|
7
|
-
private log;
|
|
8
|
-
private mqtt;
|
|
9
|
-
private platform;
|
|
10
|
-
private subscriptions;
|
|
11
|
-
constructor(platform: RatgdoPlatform);
|
|
12
|
-
private configure;
|
|
13
|
-
publish(accessory: RatgdoAccessory, topic: string, message: string): void;
|
|
14
|
-
subscribe(accessory: RatgdoAccessory, topic: string, callback: (cbBuffer: Buffer) => void): void;
|
|
15
|
-
subscribeGet(accessory: RatgdoAccessory, topic: string, type: string, getValue: () => string): void;
|
|
16
|
-
subscribeSet(accessory: RatgdoAccessory, topic: string, type: string, setValue: (value: string) => void): void;
|
|
17
|
-
unsubscribe(accessory: RatgdoAccessory, topic: string): void;
|
|
18
|
-
private expandTopic;
|
|
19
|
-
}
|
package/dist/ratgdo-mqtt.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
-
*
|
|
3
|
-
* ratgdo-mqtt.ts: MQTT connectivity class for Ratgdo.
|
|
4
|
-
*/
|
|
5
|
-
import mqtt from "mqtt";
|
|
6
|
-
import { RATGDO_MQTT_RECONNECT_INTERVAL } from "./settings.js";
|
|
7
|
-
export class RatgdoMqtt {
|
|
8
|
-
config;
|
|
9
|
-
isConnected;
|
|
10
|
-
log;
|
|
11
|
-
mqtt;
|
|
12
|
-
platform;
|
|
13
|
-
subscriptions;
|
|
14
|
-
constructor(platform) {
|
|
15
|
-
this.config = platform.config;
|
|
16
|
-
this.isConnected = false;
|
|
17
|
-
this.log = platform.log;
|
|
18
|
-
this.mqtt = null;
|
|
19
|
-
this.platform = platform;
|
|
20
|
-
this.subscriptions = {};
|
|
21
|
-
if (!this.config.mqttUrl) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
this.configure();
|
|
25
|
-
}
|
|
26
|
-
// Connect to the MQTT broker.
|
|
27
|
-
configure() {
|
|
28
|
-
// Try to connect to the MQTT broker and make sure we catch any URL errors.
|
|
29
|
-
try {
|
|
30
|
-
this.mqtt = mqtt.connect(this.config.mqttUrl, { reconnectPeriod: RATGDO_MQTT_RECONNECT_INTERVAL * 1000, rejectUnauthorized: false });
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (error instanceof Error) {
|
|
34
|
-
switch (error.message) {
|
|
35
|
-
case "Missing protocol":
|
|
36
|
-
this.log.error("MQTT Broker: Invalid URL provided: %s.", this.config.mqttUrl);
|
|
37
|
-
break;
|
|
38
|
-
default:
|
|
39
|
-
this.log.error("MQTT Broker: Error: %s.", error.message);
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// We've been unable to even attempt to connect. It's likely we have a configuration issue - we're done here.
|
|
45
|
-
if (!this.mqtt) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
// Notify the user when we connect to the broker.
|
|
49
|
-
this.mqtt.on("connect", () => {
|
|
50
|
-
this.isConnected = true;
|
|
51
|
-
// Magic incantation to redact passwords.
|
|
52
|
-
const redact = /^(?<pre>.*:\/{0,2}.*:)(?<pass>.*)(?<post>@.*)/;
|
|
53
|
-
this.log.info("Connected to MQTT broker: %s (topic: %s).", this.config.mqttUrl.replace(redact, "$<pre>REDACTED$<post>"), this.config.mqttTopic);
|
|
54
|
-
});
|
|
55
|
-
// Notify the user when we've disconnected.
|
|
56
|
-
this.mqtt.on("close", () => {
|
|
57
|
-
if (this.isConnected) {
|
|
58
|
-
this.isConnected = false;
|
|
59
|
-
// Magic incantation to redact passwords.
|
|
60
|
-
const redact = /^(?<pre>.*:\/{0,2}.*:)(?<pass>.*)(?<post>@.*)/;
|
|
61
|
-
this.log.info("Disconnected from MQTT broker: %s.", this.config.mqttUrl.replace(redact, "$<pre>REDACTED$<post>"));
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
// Process inbound messages and pass it to the right message handler.
|
|
65
|
-
this.mqtt.on("message", (topic, message) => {
|
|
66
|
-
if (this.subscriptions[topic]) {
|
|
67
|
-
this.subscriptions[topic](message);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
// Notify the user when there's a connectivity error.
|
|
71
|
-
this.mqtt.on("error", (error) => {
|
|
72
|
-
switch (error.code) {
|
|
73
|
-
case "ECONNREFUSED":
|
|
74
|
-
this.log.error("MQTT Broker: Connection refused (url: %s). Will retry again in %s minute%s.", this.config.mqttUrl, RATGDO_MQTT_RECONNECT_INTERVAL / 60, RATGDO_MQTT_RECONNECT_INTERVAL / 60 > 1 ? "s" : "");
|
|
75
|
-
break;
|
|
76
|
-
case "ECONNRESET":
|
|
77
|
-
this.log.error("MQTT Broker: Connection reset (url: %s). Will retry again in %s minute%s.", this.config.mqttUrl, RATGDO_MQTT_RECONNECT_INTERVAL / 60, RATGDO_MQTT_RECONNECT_INTERVAL / 60 > 1 ? "s" : "");
|
|
78
|
-
break;
|
|
79
|
-
case "ENOTFOUND":
|
|
80
|
-
this.mqtt?.end(true);
|
|
81
|
-
this.log.error("MQTT Broker: Hostname or IP address not found. (url: %s).", this.config.mqttUrl);
|
|
82
|
-
break;
|
|
83
|
-
default:
|
|
84
|
-
this.log.error("MQTT Broker: %s (url: %s). Will retry again in %s minute%s.", error, this.config.mqttUrl, RATGDO_MQTT_RECONNECT_INTERVAL / 60, RATGDO_MQTT_RECONNECT_INTERVAL / 60 > 1 ? "s" : "");
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
// Publish an MQTT event to a broker.
|
|
90
|
-
publish(accessory, topic, message) {
|
|
91
|
-
const expandedTopic = this.expandTopic(accessory.device.mac, topic);
|
|
92
|
-
// No valid topic returned, we're done.
|
|
93
|
-
if (!expandedTopic) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
accessory.log.debug("MQTT publish: %s Message: %s.", expandedTopic, message);
|
|
97
|
-
// By default, we publish as: ratgdo/mac/event/name
|
|
98
|
-
this.mqtt?.publish(expandedTopic, message);
|
|
99
|
-
}
|
|
100
|
-
// Subscribe to an MQTT topic.
|
|
101
|
-
subscribe(accessory, topic, callback) {
|
|
102
|
-
const expandedTopic = this.expandTopic(accessory.device.mac, topic);
|
|
103
|
-
// No valid topic returned, we're done.
|
|
104
|
-
if (!expandedTopic) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
accessory.log.debug("MQTT subscribe: %s.", expandedTopic);
|
|
108
|
-
// Add to our callback list.
|
|
109
|
-
this.subscriptions[expandedTopic] = callback;
|
|
110
|
-
// Tell MQTT we're subscribing to this event. By default, we subscribe as: ratgdo/mac/event/name.
|
|
111
|
-
this.mqtt?.subscribe(expandedTopic);
|
|
112
|
-
}
|
|
113
|
-
// Subscribe to a specific MQTT topic and publish a value on a get request.
|
|
114
|
-
subscribeGet(accessory, topic, type, getValue) {
|
|
115
|
-
// Return the current status of a given sensor.
|
|
116
|
-
this.platform.mqtt?.subscribe(accessory, topic + "/get", (message) => {
|
|
117
|
-
const value = message.toString().toLowerCase();
|
|
118
|
-
// Only publish if we receive a true value.
|
|
119
|
-
if (value !== "true") {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
// Publish our value and inform the user.
|
|
123
|
-
this.platform.mqtt?.publish(accessory, topic, getValue());
|
|
124
|
-
accessory.log.info("MQTT: %s status published.", type);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
// Subscribe to a specific MQTT topic and set a value on a set request.
|
|
128
|
-
subscribeSet(accessory, topic, type, setValue) {
|
|
129
|
-
// Return the current status of a given sensor.
|
|
130
|
-
this.platform.mqtt?.subscribe(accessory, topic + "/set", (message) => {
|
|
131
|
-
const value = message.toString().toLowerCase();
|
|
132
|
-
// Set our value and inform the user.
|
|
133
|
-
setValue(value);
|
|
134
|
-
accessory.log.info("MQTT: set message received for %s: %s.", type, value);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
// Unsubscribe to an MQTT topic.
|
|
138
|
-
unsubscribe(accessory, topic) {
|
|
139
|
-
const expandedTopic = this.expandTopic(accessory.device.mac, topic);
|
|
140
|
-
// No valid topic returned, we're done.
|
|
141
|
-
if (!expandedTopic) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
delete this.subscriptions[expandedTopic];
|
|
145
|
-
}
|
|
146
|
-
// Expand a topic to a unique, fully formed one.
|
|
147
|
-
expandTopic(mac, topic) {
|
|
148
|
-
// No accessory, we're done.
|
|
149
|
-
if (!mac) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
return this.config.mqttTopic + "/" + mac + "/" + topic;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
//# sourceMappingURL=ratgdo-mqtt.js.map
|
package/dist/ratgdo-mqtt.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ratgdo-mqtt.js","sourceRoot":"","sources":["../src/ratgdo-mqtt.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,IAAoB,MAAM,MAAM,CAAC;AACxC,OAAO,EAAE,8BAA8B,EAAE,MAAM,eAAe,CAAC;AAM/D,MAAM,OAAO,UAAU;IAEb,MAAM,CAAgB;IACtB,WAAW,CAAU;IACrB,GAAG,CAAgB;IACnB,IAAI,CAAoB;IACxB,QAAQ,CAAiB;IACzB,aAAa,CAAkD;IAEvE,YAAY,QAAwB;QAElC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,IAAI,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;QAExB,IAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAExB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED,8BAA8B;IACtB,SAAS;QAEf,2EAA2E;QAC3E,IAAI,CAAC;YAEH,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,8BAA8B,GAAG,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAC,CAAC,CAAC;QAEtI,CAAC;QAAC,OAAM,KAAK,EAAE,CAAC;YAEd,IAAG,KAAK,YAAY,KAAK,EAAE,CAAC;gBAE1B,QAAO,KAAK,CAAC,OAAO,EAAE,CAAC;oBAErB,KAAK,kBAAkB;wBAErB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,wCAAwC,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;wBAC9E,MAAM;oBAER;wBAEE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;wBACzD,MAAM;gBACV,CAAC;YAEH,CAAC;QAEH,CAAC;QAED,6GAA6G;QAC7G,IAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAEd,OAAO;QACT,CAAC;QAED,iDAAiD;QACjD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;YAE3B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;YAExB,yCAAyC;YACzC,MAAM,MAAM,GAAG,+CAA+C,CAAC;YAE/D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,2CAA2C,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAClJ,CAAC,CAAC,CAAC;QAEH,2CAA2C;QAC3C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAEzB,IAAG,IAAI,CAAC,WAAW,EAAE,CAAC;gBAEpB,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;gBAEzB,yCAAyC;gBACzC,MAAM,MAAM,GAAG,+CAA+C,CAAC;gBAE/D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,oCAAoC,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC;YACpH,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,qEAAqE;QACrE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAa,EAAE,OAAe,EAAE,EAAE;YAEzD,IAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;gBAE7B,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC;YACrC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,qDAAqD;QACrD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;YACrC,QAAQ,KAA+B,CAAC,IAAI,EAAE,CAAC;gBAE7C,KAAK,cAAc;oBAEjB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,6EAA6E,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAC/G,8BAA8B,GAAG,EAAE,EAAE,8BAA8B,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA,CAAC,CAAC,EAAE,CAAC,CAAC;oBAC1F,MAAM;gBAER,KAAK,YAAY;oBAEf,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,2EAA2E,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAC7G,8BAA8B,GAAG,EAAE,EAAE,8BAA8B,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA,CAAC,CAAC,EAAE,CAAC,CAAC;oBAC1F,MAAM;gBAER,KAAK,WAAW;oBAEd,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;oBACrB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,2DAA2D,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;oBACjG,MAAM;gBAER;oBAEE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,6DAA6D,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EACtG,8BAA8B,GAAG,EAAE,EAAE,8BAA8B,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA,CAAC,CAAC,EAAE,CAAC,CAAC;oBAC1F,MAAM;YACV,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qCAAqC;IAC9B,OAAO,CAAC,SAA0B,EAAE,KAAa,EAAE,OAAe;QAEvE,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAEpE,uCAAuC;QACvC,IAAG,CAAC,aAAa,EAAE,CAAC;YAElB,OAAO;QACT,CAAC;QAED,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,+BAA+B,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;QAE7E,mDAAmD;QACnD,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC;IAED,8BAA8B;IACvB,SAAS,CAAC,SAA0B,EAAE,KAAa,EAAE,QAAoC;QAE9F,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAEpE,uCAAuC;QACvC,IAAG,CAAC,aAAa,EAAE,CAAC;YAElB,OAAO;QACT,CAAC;QAED,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,qBAAqB,EAAE,aAAa,CAAC,CAAC;QAE1D,4BAA4B;QAC5B,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ,CAAC;QAE7C,iGAAiG;QACjG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;IACtC,CAAC;IAED,2EAA2E;IACpE,YAAY,CAAC,SAA0B,EAAE,KAAa,EAAE,IAAY,EAAE,QAAsB;QAEjG,+CAA+C;QAC/C,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,EAAE,CAAC,OAAe,EAAE,EAAE;YAE3E,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC;YAE/C,2CAA2C;YAC3C,IAAG,KAAK,KAAK,MAAM,EAAE,CAAC;gBAEpB,OAAO;YACT,CAAC;YAED,yCAAyC;YACzC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC1D,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,4BAA4B,EAAE,IAAI,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,uEAAuE;IAChE,YAAY,CAAC,SAA0B,EAAE,KAAa,EAAE,IAAY,EAAE,QAAiC;QAE5G,+CAA+C;QAC/C,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,EAAE,CAAC,OAAe,EAAE,EAAE;YAE3E,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC;YAE/C,qCAAqC;YACrC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAChB,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,wCAAwC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;IACL,CAAC;IAED,gCAAgC;IACzB,WAAW,CAAC,SAA0B,EAAE,KAAa;QAE1D,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAEpE,uCAAuC;QACvC,IAAG,CAAC,aAAa,EAAE,CAAC;YAElB,OAAO;QACT,CAAC;QAED,OAAO,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IAC3C,CAAC;IAED,gDAAgD;IACxC,WAAW,CAAC,GAAW,EAAE,KAAa;QAE5C,4BAA4B;QAC5B,IAAG,CAAC,GAAG,EAAE,CAAC;YAER,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,KAAK,CAAC;IACzD,CAAC;CACF"}
|