homebridge-smartsystem 6.8.13 → 7.0.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/.vscode/launch.json +15 -4
- package/MIXINS-GUIDE.md +210 -0
- package/README.md +32 -0
- package/SETTINGS-UPDATE.md +195 -0
- package/TESTING-GUIDE.md +220 -0
- package/WEBSOCKET-API.md +303 -0
- package/accessories/accessory.js +20 -4
- package/accessories/accessory.ts +19 -4
- package/accessories/bulb.js +0 -5
- package/accessories/bulb.ts +1 -5
- package/accessories/dimmer.js +11 -3
- package/accessories/dimmer.ts +11 -4
- package/accessories/garagedoor.js +19 -9
- package/accessories/garagedoor.ts +21 -9
- package/accessories/lock.js +11 -3
- package/accessories/lock.ts +10 -3
- package/accessories/temperature.js +10 -2
- package/accessories/temperature.ts +9 -2
- package/accessories/windowcovering.js +30 -14
- package/accessories/windowcovering.ts +29 -15
- package/config.json +57 -19
- package/duotecno/types.js +48 -2
- package/duotecno/types.ts +75 -1
- package/package.json +12 -4
- package/raspberry-pi-setup.sh +80 -0
- package/server/HA-API.js +4 -1
- package/server/HA-API.ts +4 -1
- package/server/HB.js +6 -1
- package/server/HB.ts +5 -1
- package/server/mDNS.js +168 -0
- package/server/mDNS.ts +198 -0
- package/server/platform.js +7 -0
- package/server/platform.ts +8 -0
- package/server/proxy.js +58 -19
- package/server/proxy.ts +31 -3
- package/server/smartapp.js +397 -18
- package/server/smartapp.ts +432 -20
- package/server/socapp.js +2 -1
- package/server/socapp.ts +2 -1
- package/server/views/assets/Duotecno_logo_black.svg +10 -0
- package/server/views/assets/Duotecno_logo_white.svg +10 -0
- package/server/views/device-details.ejs +139 -0
- package/server/views/device-list.ejs +44 -0
- package/server/views/footer.ejs +0 -1
- package/server/views/head.ejs +9 -0
- package/server/views/homekit.ejs +2 -9
- package/server/views/link-details.ejs +2 -15
- package/server/views/link-list.ejs +2 -9
- package/server/views/login.ejs +8 -9
- package/server/views/master-detail.ejs +3 -9
- package/server/views/master-list.ejs +3 -10
- package/server/views/nav.ejs +44 -11
- package/server/views/node-details.ejs +1 -8
- package/server/views/power-binding.ejs +3 -10
- package/server/views/power-rule.ejs +3 -10
- package/server/views/power.ejs +8 -15
- package/server/views/proxy.ejs +1 -8
- package/server/views/scripts.ejs +10 -0
- package/server/views/service-list.ejs +2 -9
- package/server/views/settings.ejs +61 -22
- package/server/views/style.ejs +49 -5
- package/server/views/switch-details.ejs +2 -9
- package/server/views/switch-list.ejs +2 -9
- package/server/webapp.js +150 -11
- package/server/webapp.ts +166 -14
- package/{index.spec.js → testHB.js} +1 -1
- package/{run.js → testProxy.js} +2 -2
- package/{run.ts → testProxy.ts} +1 -1
- package/testWS.js +137 -0
- package/testWS.ts +174 -0
- package/run9998.js +0 -7
- package/run9998.ts +0 -6
- package/run9999.js +0 -7
- package/run9999.ts +0 -6
- /package/{index.spec.ts → testHB.ts} +0 -0
package/server/mDNS.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// mDNS Service Manager
|
|
3
|
+
// Purpose: Handle mDNS service discovery and hostname publishing
|
|
4
|
+
// Similar to ESP32's MDNS.begin() functionality
|
|
5
|
+
//
|
|
6
|
+
// Johan Coppieters, Nov 2025
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MDNSService = void 0;
|
|
9
|
+
const Bonjour = require("bonjour-service");
|
|
10
|
+
const mdns = require("multicast-dns");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
const logger_1 = require("../duotecno/logger");
|
|
13
|
+
class MDNSService {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Initialize mDNS service (call this before registering)
|
|
19
|
+
*/
|
|
20
|
+
init() {
|
|
21
|
+
try {
|
|
22
|
+
this.bonjour = new Bonjour.Bonjour();
|
|
23
|
+
(0, logger_1.log)("mDNS", "Service initialized");
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
(0, logger_1.err)("mDNS", `Failed to initialize: ${e.message}`);
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register both HTTP service and hostname (like ESP32 MDNS.begin())
|
|
32
|
+
*/
|
|
33
|
+
register() {
|
|
34
|
+
if (!this.bonjour) {
|
|
35
|
+
(0, logger_1.err)("mDNS", "Service not initialized, call init() first");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const { name, port, version } = this.config;
|
|
40
|
+
const ports = Array.isArray(port) ? port : [port];
|
|
41
|
+
const primaryPort = ports[0];
|
|
42
|
+
// Register the HTTP service using bonjour
|
|
43
|
+
const service = this.bonjour.publish({
|
|
44
|
+
name,
|
|
45
|
+
type: 'http',
|
|
46
|
+
port: primaryPort,
|
|
47
|
+
txt: {
|
|
48
|
+
version: version || '1.0.0',
|
|
49
|
+
ports: ports.join(','),
|
|
50
|
+
path: '/'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
(0, logger_1.log)("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
|
|
54
|
+
// Handle service errors
|
|
55
|
+
service.on('error', (error) => {
|
|
56
|
+
(0, logger_1.err)("mDNS", `Service error: ${error.message}`);
|
|
57
|
+
});
|
|
58
|
+
// Publish the hostname A record (like ESP32 MDNS.begin())
|
|
59
|
+
this.publishHostname(name);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
(0, logger_1.err)("mDNS", `Failed to register service: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Publish hostname as A/AAAA records (ESP32-style hostname publishing)
|
|
67
|
+
*/
|
|
68
|
+
publishHostname(hostname) {
|
|
69
|
+
try {
|
|
70
|
+
// Create multicast DNS instance
|
|
71
|
+
this.mdns = mdns();
|
|
72
|
+
// Get local IP addresses
|
|
73
|
+
const addresses = this.getLocalIPAddresses();
|
|
74
|
+
if (addresses.length === 0) {
|
|
75
|
+
(0, logger_1.err)("mDNS", "No network interfaces found, cannot publish hostname");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
(0, logger_1.log)("mDNS", `Publishing hostname ${hostname}.local with IPs: ${addresses.join(', ')}`);
|
|
79
|
+
// Respond to mDNS queries for our hostname (like ESP32 MDNS.begin())
|
|
80
|
+
this.mdns.on('query', (query) => {
|
|
81
|
+
// Check if someone is asking for our hostname
|
|
82
|
+
const hostnameQuery = query.questions.find(q => q.name === `${hostname}.local` && (q.type === 'A' || q.type === 'AAAA' || q.type === 'ANY'));
|
|
83
|
+
if (hostnameQuery) {
|
|
84
|
+
// Respond with A records for all our IP addresses
|
|
85
|
+
const answers = addresses.map(ip => {
|
|
86
|
+
const isIPv6 = ip.includes(':');
|
|
87
|
+
return {
|
|
88
|
+
name: `${hostname}.local`,
|
|
89
|
+
type: isIPv6 ? 'AAAA' : 'A',
|
|
90
|
+
ttl: 120,
|
|
91
|
+
data: ip
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
this.mdns.respond(answers, () => {
|
|
95
|
+
(0, logger_1.debug)("mDNS", `Responded to query for ${hostname}.local`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// Send announcement (like ESP32 does on startup)
|
|
100
|
+
this.announceHostname(hostname, addresses);
|
|
101
|
+
(0, logger_1.log)("mDNS", `Hostname responder started for ${hostname}.local`);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
(0, logger_1.err)("mDNS", `Failed to publish hostname: ${e.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Send unsolicited announcement (like ESP32 mDNS does on startup)
|
|
109
|
+
*/
|
|
110
|
+
announceHostname(hostname, addresses) {
|
|
111
|
+
const answers = addresses.map(ip => {
|
|
112
|
+
const isIPv6 = ip.includes(':');
|
|
113
|
+
return {
|
|
114
|
+
name: `${hostname}.local`,
|
|
115
|
+
type: isIPv6 ? 'AAAA' : 'A',
|
|
116
|
+
ttl: 120,
|
|
117
|
+
data: ip
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
this.mdns.respond(answers, () => {
|
|
121
|
+
(0, logger_1.log)("mDNS", `Announced ${hostname}.local on the network`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get local IPv4 addresses (excluding loopback and virtual interfaces)
|
|
126
|
+
*/
|
|
127
|
+
getLocalIPAddresses() {
|
|
128
|
+
const addresses = [];
|
|
129
|
+
const interfaces = os.networkInterfaces();
|
|
130
|
+
for (const name in interfaces) {
|
|
131
|
+
const iface = interfaces[name];
|
|
132
|
+
if (!iface)
|
|
133
|
+
continue;
|
|
134
|
+
for (const addr of iface) {
|
|
135
|
+
// Skip internal (loopback) and non-IPv4 addresses
|
|
136
|
+
if (addr.internal || addr.family !== 'IPv4')
|
|
137
|
+
continue;
|
|
138
|
+
// Skip docker and virtual interfaces
|
|
139
|
+
if (name.startsWith('docker') || name.startsWith('veth'))
|
|
140
|
+
continue;
|
|
141
|
+
addresses.push(addr.address);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return addresses;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Stop and cleanup mDNS services
|
|
148
|
+
*/
|
|
149
|
+
destroy() {
|
|
150
|
+
try {
|
|
151
|
+
if (this.mdns) {
|
|
152
|
+
this.mdns.destroy();
|
|
153
|
+
this.mdns = null;
|
|
154
|
+
(0, logger_1.log)("mDNS", "Hostname responder stopped");
|
|
155
|
+
}
|
|
156
|
+
if (this.bonjour) {
|
|
157
|
+
this.bonjour.destroy();
|
|
158
|
+
this.bonjour = null;
|
|
159
|
+
(0, logger_1.log)("mDNS", "Service discovery stopped");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
(0, logger_1.err)("mDNS", `Error during cleanup: ${e.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.MDNSService = MDNSService;
|
|
168
|
+
//# sourceMappingURL=mDNS.js.map
|
package/server/mDNS.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// mDNS Service Manager
|
|
2
|
+
// Purpose: Handle mDNS service discovery and hostname publishing
|
|
3
|
+
// Similar to ESP32's MDNS.begin() functionality
|
|
4
|
+
//
|
|
5
|
+
// Johan Coppieters, Nov 2025
|
|
6
|
+
|
|
7
|
+
import * as Bonjour from 'bonjour-service';
|
|
8
|
+
import * as mdns from 'multicast-dns';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { log, debug, err } from '../duotecno/logger';
|
|
11
|
+
|
|
12
|
+
export interface MDNSConfig {
|
|
13
|
+
name: string;
|
|
14
|
+
port: number | number[];
|
|
15
|
+
version?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MDNSService {
|
|
19
|
+
private bonjour: any;
|
|
20
|
+
private mdns: any;
|
|
21
|
+
private config: MDNSConfig;
|
|
22
|
+
|
|
23
|
+
constructor(config: MDNSConfig) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize mDNS service (call this before registering)
|
|
29
|
+
*/
|
|
30
|
+
init() {
|
|
31
|
+
try {
|
|
32
|
+
this.bonjour = new Bonjour.Bonjour();
|
|
33
|
+
log("mDNS", "Service initialized");
|
|
34
|
+
} catch (e) {
|
|
35
|
+
err("mDNS", `Failed to initialize: ${e.message}`);
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register both HTTP service and hostname (like ESP32 MDNS.begin())
|
|
42
|
+
*/
|
|
43
|
+
register() {
|
|
44
|
+
if (!this.bonjour) {
|
|
45
|
+
err("mDNS", "Service not initialized, call init() first");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const { name, port, version } = this.config;
|
|
51
|
+
const ports = Array.isArray(port) ? port : [port];
|
|
52
|
+
const primaryPort = ports[0];
|
|
53
|
+
|
|
54
|
+
// Register the HTTP service using bonjour
|
|
55
|
+
const service = this.bonjour.publish({
|
|
56
|
+
name,
|
|
57
|
+
type: 'http',
|
|
58
|
+
port: primaryPort,
|
|
59
|
+
txt: {
|
|
60
|
+
version: version || '1.0.0',
|
|
61
|
+
ports: ports.join(','),
|
|
62
|
+
path: '/'
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
log("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
|
|
67
|
+
|
|
68
|
+
// Handle service errors
|
|
69
|
+
service.on('error', (error) => {
|
|
70
|
+
err("mDNS", `Service error: ${error.message}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Publish the hostname A record (like ESP32 MDNS.begin())
|
|
74
|
+
this.publishHostname(name);
|
|
75
|
+
|
|
76
|
+
} catch (e) {
|
|
77
|
+
err("mDNS", `Failed to register service: ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Publish hostname as A/AAAA records (ESP32-style hostname publishing)
|
|
83
|
+
*/
|
|
84
|
+
private publishHostname(hostname: string) {
|
|
85
|
+
try {
|
|
86
|
+
// Create multicast DNS instance
|
|
87
|
+
this.mdns = mdns();
|
|
88
|
+
|
|
89
|
+
// Get local IP addresses
|
|
90
|
+
const addresses = this.getLocalIPAddresses();
|
|
91
|
+
|
|
92
|
+
if (addresses.length === 0) {
|
|
93
|
+
err("mDNS", "No network interfaces found, cannot publish hostname");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
log("mDNS", `Publishing hostname ${hostname}.local with IPs: ${addresses.join(', ')}`);
|
|
98
|
+
|
|
99
|
+
// Respond to mDNS queries for our hostname (like ESP32 MDNS.begin())
|
|
100
|
+
this.mdns.on('query', (query) => {
|
|
101
|
+
// Check if someone is asking for our hostname
|
|
102
|
+
const hostnameQuery = query.questions.find(q =>
|
|
103
|
+
q.name === `${hostname}.local` && (q.type === 'A' || q.type === 'AAAA' || q.type === 'ANY')
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (hostnameQuery) {
|
|
107
|
+
// Respond with A records for all our IP addresses
|
|
108
|
+
const answers = addresses.map(ip => {
|
|
109
|
+
const isIPv6 = ip.includes(':');
|
|
110
|
+
return {
|
|
111
|
+
name: `${hostname}.local`,
|
|
112
|
+
type: isIPv6 ? 'AAAA' : 'A',
|
|
113
|
+
ttl: 120,
|
|
114
|
+
data: ip
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.mdns.respond(answers, () => {
|
|
119
|
+
debug("mDNS", `Responded to query for ${hostname}.local`);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Send announcement (like ESP32 does on startup)
|
|
125
|
+
this.announceHostname(hostname, addresses);
|
|
126
|
+
|
|
127
|
+
log("mDNS", `Hostname responder started for ${hostname}.local`);
|
|
128
|
+
|
|
129
|
+
} catch (e) {
|
|
130
|
+
err("mDNS", `Failed to publish hostname: ${e.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Send unsolicited announcement (like ESP32 mDNS does on startup)
|
|
136
|
+
*/
|
|
137
|
+
private announceHostname(hostname: string, addresses: string[]) {
|
|
138
|
+
const answers = addresses.map(ip => {
|
|
139
|
+
const isIPv6 = ip.includes(':');
|
|
140
|
+
return {
|
|
141
|
+
name: `${hostname}.local`,
|
|
142
|
+
type: isIPv6 ? 'AAAA' : 'A',
|
|
143
|
+
ttl: 120,
|
|
144
|
+
data: ip
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.mdns.respond(answers, () => {
|
|
149
|
+
log("mDNS", `Announced ${hostname}.local on the network`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get local IPv4 addresses (excluding loopback and virtual interfaces)
|
|
155
|
+
*/
|
|
156
|
+
private getLocalIPAddresses(): string[] {
|
|
157
|
+
const addresses: string[] = [];
|
|
158
|
+
const interfaces = os.networkInterfaces();
|
|
159
|
+
|
|
160
|
+
for (const name in interfaces) {
|
|
161
|
+
const iface = interfaces[name];
|
|
162
|
+
if (!iface) continue;
|
|
163
|
+
|
|
164
|
+
for (const addr of iface) {
|
|
165
|
+
// Skip internal (loopback) and non-IPv4 addresses
|
|
166
|
+
if (addr.internal || addr.family !== 'IPv4') continue;
|
|
167
|
+
|
|
168
|
+
// Skip docker and virtual interfaces
|
|
169
|
+
if (name.startsWith('docker') || name.startsWith('veth')) continue;
|
|
170
|
+
|
|
171
|
+
addresses.push(addr.address);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return addresses;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Stop and cleanup mDNS services
|
|
180
|
+
*/
|
|
181
|
+
destroy() {
|
|
182
|
+
try {
|
|
183
|
+
if (this.mdns) {
|
|
184
|
+
this.mdns.destroy();
|
|
185
|
+
this.mdns = null;
|
|
186
|
+
log("mDNS", "Hostname responder stopped");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (this.bonjour) {
|
|
190
|
+
this.bonjour.destroy();
|
|
191
|
+
this.bonjour = null;
|
|
192
|
+
log("mDNS", "Service discovery stopped");
|
|
193
|
+
}
|
|
194
|
+
} catch (e) {
|
|
195
|
+
err("mDNS", `Error during cleanup: ${e.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
package/server/platform.js
CHANGED
|
@@ -83,6 +83,9 @@ class Platform extends base_1.Base {
|
|
|
83
83
|
try {
|
|
84
84
|
this.smartapp = new smartapp_1.SmartApp(this.system, this.power, this);
|
|
85
85
|
this.smartapp.serve();
|
|
86
|
+
// Register webapp with proxy for graceful shutdown
|
|
87
|
+
const { setWebApp } = require('./proxy');
|
|
88
|
+
setWebApp(this.smartapp);
|
|
86
89
|
}
|
|
87
90
|
catch (err) {
|
|
88
91
|
(0, logger_1.log)("homebridge", err);
|
|
@@ -111,6 +114,8 @@ class Platform extends base_1.Base {
|
|
|
111
114
|
else {
|
|
112
115
|
(0, logger_1.log)("homebridge", "platform - No SocApp configured.");
|
|
113
116
|
}
|
|
117
|
+
// Always set proxy kind to "gw" when running under Platform/HomeBridge
|
|
118
|
+
// This prevents auto-restart on errors even if proxy is not configured
|
|
114
119
|
if (this.config.proxy && ((_a = this.config.proxy) === null || _a === void 0 ? void 0 : _a.uniqueId)) {
|
|
115
120
|
this.config.proxy.kind = "gw";
|
|
116
121
|
(0, proxy_1.setProxyConfig)(this.config.proxy);
|
|
@@ -119,6 +124,8 @@ class Platform extends base_1.Base {
|
|
|
119
124
|
}
|
|
120
125
|
else {
|
|
121
126
|
(0, logger_1.log)("homebridge", "platform - No Proxy configured.");
|
|
127
|
+
// Still set kind to "gw" to prevent auto-restart on web server errors
|
|
128
|
+
(0, proxy_1.setProxyConfig)(Object.assign(Object.assign({}, this.config.proxy), { kind: "gw" }));
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
startPower(type, PowerMgr) {
|
package/server/platform.ts
CHANGED
|
@@ -89,6 +89,10 @@ export class Platform extends Base {
|
|
|
89
89
|
try {
|
|
90
90
|
this.smartapp = new SmartApp(this.system, this.power, this);
|
|
91
91
|
this.smartapp.serve();
|
|
92
|
+
|
|
93
|
+
// Register webapp with proxy for graceful shutdown
|
|
94
|
+
const { setWebApp } = require('./proxy');
|
|
95
|
+
setWebApp(this.smartapp);
|
|
92
96
|
|
|
93
97
|
} catch(err) {
|
|
94
98
|
log("homebridge", err);
|
|
@@ -117,6 +121,8 @@ export class Platform extends Base {
|
|
|
117
121
|
log("homebridge", "platform - No SocApp configured.");
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
// Always set proxy kind to "gw" when running under Platform/HomeBridge
|
|
125
|
+
// This prevents auto-restart on errors even if proxy is not configured
|
|
120
126
|
if (this.config.proxy && this.config.proxy?.uniqueId) {
|
|
121
127
|
this.config.proxy.kind = "gw";
|
|
122
128
|
setProxyConfig(this.config.proxy);
|
|
@@ -124,6 +130,8 @@ export class Platform extends Base {
|
|
|
124
130
|
log("homebridge", "platform - Proxy configured: " + JSON.stringify(this.config.proxy, null, 2));
|
|
125
131
|
} else {
|
|
126
132
|
log("homebridge", "platform - No Proxy configured.");
|
|
133
|
+
// Still set kind to "gw" to prevent auto-restart on web server errors
|
|
134
|
+
setProxyConfig({ ...this.config.proxy, kind: "gw" });
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
|
package/server/proxy.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.kEmptyProxy = void 0;
|
|
12
|
+
exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.setWebApp = exports.kEmptyProxy = void 0;
|
|
4
13
|
const net = require("net");
|
|
5
14
|
const child_process_1 = require("child_process");
|
|
6
15
|
const process_1 = require("process");
|
|
@@ -14,6 +23,12 @@ exports.kEmptyProxy = {
|
|
|
14
23
|
kind: "solo" // default running under PM2 or other process manager -> don't restart yourself
|
|
15
24
|
};
|
|
16
25
|
const config = Object.assign({}, exports.kEmptyProxy);
|
|
26
|
+
// Store reference to the webapp so we can close servers before restarting
|
|
27
|
+
let gWebApp = null;
|
|
28
|
+
function setWebApp(webapp) {
|
|
29
|
+
gWebApp = webapp;
|
|
30
|
+
}
|
|
31
|
+
exports.setWebApp = setWebApp;
|
|
17
32
|
function setProxyConfig(c) {
|
|
18
33
|
// set the proxy config
|
|
19
34
|
if (c) {
|
|
@@ -60,7 +75,18 @@ function error(str) {
|
|
|
60
75
|
///////////////////////////////
|
|
61
76
|
// Start up the proxy server //
|
|
62
77
|
///////////////////////////////
|
|
63
|
-
|
|
78
|
+
// Only auto-start if we have a config-proxy.json file (standalone mode)
|
|
79
|
+
// When running under Platform/HomeBridge, let Platform control the startup
|
|
80
|
+
try {
|
|
81
|
+
require.resolve('../config-proxy.json');
|
|
82
|
+
// config-proxy.json exists, we're running standalone -> start the proxy
|
|
83
|
+
log("[PROXY] Running in standalone mode (config-proxy.json found), starting proxy...");
|
|
84
|
+
cleanStart(true);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
// No config-proxy.json, we're probably running under Platform/HomeBridge
|
|
88
|
+
log("[PROXY] No config-proxy.json found, waiting for Platform to start proxy...");
|
|
89
|
+
}
|
|
64
90
|
function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count = 1) {
|
|
65
91
|
// retry making a cloud connection
|
|
66
92
|
function reconnect(err) {
|
|
@@ -312,23 +338,36 @@ function restart() {
|
|
|
312
338
|
}
|
|
313
339
|
// Graceful shutdown handler
|
|
314
340
|
function handleShutdown(signal) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
341
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
342
|
+
log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
|
|
343
|
+
if (connectionChecker) {
|
|
344
|
+
clearInterval(connectionChecker);
|
|
345
|
+
connectionChecker = null;
|
|
346
|
+
}
|
|
347
|
+
// Close all cloud connections
|
|
348
|
+
gCloudConnections.forEach(ctx => ctx.cleanupSockets());
|
|
349
|
+
gCloudConnections.splice(0, gCloudConnections.length);
|
|
350
|
+
// Close HTTP servers if available
|
|
351
|
+
if (gWebApp && typeof gWebApp.closeServers === 'function') {
|
|
352
|
+
log(`[PROXY] → [SYSTEM] Closing HTTP servers...`);
|
|
353
|
+
try {
|
|
354
|
+
yield gWebApp.closeServers();
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
error(`[PROXY] → [SYSTEM] Error closing servers: ${e.message}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
|
|
361
|
+
if (config.kind === "solo") {
|
|
362
|
+
log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
|
|
363
|
+
restart();
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
log(`[PROXY] → [SYSTEM] Exiting without restart.`);
|
|
367
|
+
// Give OS time to release ports before exit (helps prevent EADDRINUSE on restart)
|
|
368
|
+
setTimeout(() => process.exit(0), 500);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
332
371
|
}
|
|
333
372
|
// Listen for termination signals
|
|
334
373
|
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
package/server/proxy.ts
CHANGED
|
@@ -28,6 +28,13 @@ export const kEmptyProxy: ProxyConfig = {
|
|
|
28
28
|
|
|
29
29
|
const config = {...kEmptyProxy};
|
|
30
30
|
|
|
31
|
+
// Store reference to the webapp so we can close servers before restarting
|
|
32
|
+
let gWebApp: any = null;
|
|
33
|
+
|
|
34
|
+
export function setWebApp(webapp: any) {
|
|
35
|
+
gWebApp = webapp;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
export function setProxyConfig(c?: ProxyConfig): ProxyConfig {
|
|
32
39
|
// set the proxy config
|
|
33
40
|
if (c) {
|
|
@@ -81,7 +88,17 @@ function error(str: string) {
|
|
|
81
88
|
///////////////////////////////
|
|
82
89
|
// Start up the proxy server //
|
|
83
90
|
///////////////////////////////
|
|
84
|
-
|
|
91
|
+
// Only auto-start if we have a config-proxy.json file (standalone mode)
|
|
92
|
+
// When running under Platform/HomeBridge, let Platform control the startup
|
|
93
|
+
try {
|
|
94
|
+
require.resolve('../config-proxy.json');
|
|
95
|
+
// config-proxy.json exists, we're running standalone -> start the proxy
|
|
96
|
+
log("[PROXY] Running in standalone mode (config-proxy.json found), starting proxy...");
|
|
97
|
+
cleanStart(true);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// No config-proxy.json, we're probably running under Platform/HomeBridge
|
|
100
|
+
log("[PROXY] No config-proxy.json found, waiting for Platform to start proxy...");
|
|
101
|
+
}
|
|
85
102
|
|
|
86
103
|
|
|
87
104
|
export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string, count = 1) {
|
|
@@ -389,7 +406,7 @@ function restart() {
|
|
|
389
406
|
|
|
390
407
|
|
|
391
408
|
// Graceful shutdown handler
|
|
392
|
-
function handleShutdown(signal: string) {
|
|
409
|
+
async function handleShutdown(signal: string) {
|
|
393
410
|
log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
|
|
394
411
|
|
|
395
412
|
if (connectionChecker) {
|
|
@@ -401,6 +418,16 @@ function handleShutdown(signal: string) {
|
|
|
401
418
|
gCloudConnections.forEach(ctx => ctx.cleanupSockets());
|
|
402
419
|
gCloudConnections.splice(0, gCloudConnections.length);
|
|
403
420
|
|
|
421
|
+
// Close HTTP servers if available
|
|
422
|
+
if (gWebApp && typeof gWebApp.closeServers === 'function') {
|
|
423
|
+
log(`[PROXY] → [SYSTEM] Closing HTTP servers...`);
|
|
424
|
+
try {
|
|
425
|
+
await gWebApp.closeServers();
|
|
426
|
+
} catch (e) {
|
|
427
|
+
error(`[PROXY] → [SYSTEM] Error closing servers: ${e.message}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
404
431
|
log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
|
|
405
432
|
|
|
406
433
|
if (config.kind === "solo") {
|
|
@@ -408,7 +435,8 @@ function handleShutdown(signal: string) {
|
|
|
408
435
|
restart();
|
|
409
436
|
} else {
|
|
410
437
|
log(`[PROXY] → [SYSTEM] Exiting without restart.`);
|
|
411
|
-
|
|
438
|
+
// Give OS time to release ports before exit (helps prevent EADDRINUSE on restart)
|
|
439
|
+
setTimeout(() => process.exit(0), 500);
|
|
412
440
|
}
|
|
413
441
|
}
|
|
414
442
|
|