@ecobridge.xyz/devicemanager 3.0.2
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/.gitea/workflows/default_nottags.yaml +66 -0
- package/.gitea/workflows/default_tags.yaml +124 -0
- package/.vscode/launch.json +11 -0
- package/.vscode/settings.json +26 -0
- package/changelog.md +91 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/device/device.classes.device.d.ts +205 -0
- package/dist_ts/device/device.classes.device.js +344 -0
- package/dist_ts/devicemanager.classes.devicemanager.d.ts +210 -0
- package/dist_ts/devicemanager.classes.devicemanager.js +835 -0
- package/dist_ts/discovery/discovery.classes.homeassistant.d.ts +101 -0
- package/dist_ts/discovery/discovery.classes.homeassistant.js +313 -0
- package/dist_ts/discovery/discovery.classes.mdns.d.ts +90 -0
- package/dist_ts/discovery/discovery.classes.mdns.js +291 -0
- package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +64 -0
- package/dist_ts/discovery/discovery.classes.networkscanner.js +535 -0
- package/dist_ts/discovery/discovery.classes.ssdp.d.ts +112 -0
- package/dist_ts/discovery/discovery.classes.ssdp.js +255 -0
- package/dist_ts/factories/index.d.ts +190 -0
- package/dist_ts/factories/index.js +350 -0
- package/dist_ts/features/feature.abstract.d.ts +118 -0
- package/dist_ts/features/feature.abstract.js +172 -0
- package/dist_ts/features/feature.camera.d.ts +95 -0
- package/dist_ts/features/feature.camera.js +162 -0
- package/dist_ts/features/feature.climate.d.ts +174 -0
- package/dist_ts/features/feature.climate.js +328 -0
- package/dist_ts/features/feature.cover.d.ts +121 -0
- package/dist_ts/features/feature.cover.js +217 -0
- package/dist_ts/features/feature.fan.d.ts +127 -0
- package/dist_ts/features/feature.fan.js +235 -0
- package/dist_ts/features/feature.light.d.ts +156 -0
- package/dist_ts/features/feature.light.js +296 -0
- package/dist_ts/features/feature.lock.d.ts +103 -0
- package/dist_ts/features/feature.lock.js +170 -0
- package/dist_ts/features/feature.playback.d.ts +98 -0
- package/dist_ts/features/feature.playback.js +191 -0
- package/dist_ts/features/feature.power.d.ts +79 -0
- package/dist_ts/features/feature.power.js +173 -0
- package/dist_ts/features/feature.print.d.ts +78 -0
- package/dist_ts/features/feature.print.js +226 -0
- package/dist_ts/features/feature.scan.d.ts +79 -0
- package/dist_ts/features/feature.scan.js +315 -0
- package/dist_ts/features/feature.sensor.d.ts +95 -0
- package/dist_ts/features/feature.sensor.js +147 -0
- package/dist_ts/features/feature.snmp.d.ts +88 -0
- package/dist_ts/features/feature.snmp.js +175 -0
- package/dist_ts/features/feature.switch.d.ts +75 -0
- package/dist_ts/features/feature.switch.js +126 -0
- package/dist_ts/features/feature.volume.d.ts +104 -0
- package/dist_ts/features/feature.volume.js +186 -0
- package/dist_ts/features/index.d.ts +19 -0
- package/dist_ts/features/index.js +23 -0
- package/dist_ts/helpers/helpers.iprange.d.ts +35 -0
- package/dist_ts/helpers/helpers.iprange.js +151 -0
- package/dist_ts/helpers/helpers.retry.d.ts +25 -0
- package/dist_ts/helpers/helpers.retry.js +69 -0
- package/dist_ts/index.d.ts +15 -0
- package/dist_ts/index.js +40 -0
- package/dist_ts/interfaces/feature.interfaces.d.ts +255 -0
- package/dist_ts/interfaces/feature.interfaces.js +6 -0
- package/dist_ts/interfaces/homeassistant.interfaces.d.ts +550 -0
- package/dist_ts/interfaces/homeassistant.interfaces.js +111 -0
- package/dist_ts/interfaces/index.d.ts +330 -0
- package/dist_ts/interfaces/index.js +16 -0
- package/dist_ts/interfaces/smarthome.interfaces.d.ts +282 -0
- package/dist_ts/interfaces/smarthome.interfaces.js +7 -0
- package/dist_ts/paths.d.ts +1 -0
- package/dist_ts/paths.js +3 -0
- package/dist_ts/plugins.d.ts +23 -0
- package/dist_ts/plugins.js +30 -0
- package/dist_ts/protocols/index.d.ts +12 -0
- package/dist_ts/protocols/index.js +21 -0
- package/dist_ts/protocols/protocol.escl.d.ts +58 -0
- package/dist_ts/protocols/protocol.escl.js +364 -0
- package/dist_ts/protocols/protocol.homeassistant.d.ts +248 -0
- package/dist_ts/protocols/protocol.homeassistant.js +611 -0
- package/dist_ts/protocols/protocol.ipp.d.ts +45 -0
- package/dist_ts/protocols/protocol.ipp.js +284 -0
- package/dist_ts/protocols/protocol.nut.d.ts +160 -0
- package/dist_ts/protocols/protocol.nut.js +364 -0
- package/dist_ts/protocols/protocol.sane.d.ts +104 -0
- package/dist_ts/protocols/protocol.sane.js +597 -0
- package/dist_ts/protocols/protocol.snmp.d.ts +133 -0
- package/dist_ts/protocols/protocol.snmp.js +333 -0
- package/dist_ts/protocols/protocol.upnp.d.ts +207 -0
- package/dist_ts/protocols/protocol.upnp.js +442 -0
- package/dist_ts/protocols/protocol.upssnmp.d.ts +160 -0
- package/dist_ts/protocols/protocol.upssnmp.js +263 -0
- package/npmextra.json +24 -0
- package/package.json +39 -0
- package/readme.hints.md +115 -0
- package/readme.md +624 -0
- package/test/test.ts +261 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/device/device.classes.device.ts +454 -0
- package/ts/devicemanager.classes.devicemanager.ts +1219 -0
- package/ts/discovery/discovery.classes.homeassistant.ts +376 -0
- package/ts/discovery/discovery.classes.mdns.ts +347 -0
- package/ts/discovery/discovery.classes.networkscanner.ts +648 -0
- package/ts/discovery/discovery.classes.ssdp.ts +357 -0
- package/ts/factories/index.ts +696 -0
- package/ts/features/feature.abstract.ts +251 -0
- package/ts/features/feature.camera.ts +214 -0
- package/ts/features/feature.climate.ts +407 -0
- package/ts/features/feature.cover.ts +278 -0
- package/ts/features/feature.fan.ts +296 -0
- package/ts/features/feature.light.ts +369 -0
- package/ts/features/feature.lock.ts +223 -0
- package/ts/features/feature.playback.ts +246 -0
- package/ts/features/feature.power.ts +231 -0
- package/ts/features/feature.print.ts +297 -0
- package/ts/features/feature.scan.ts +370 -0
- package/ts/features/feature.sensor.ts +202 -0
- package/ts/features/feature.snmp.ts +235 -0
- package/ts/features/feature.switch.ts +170 -0
- package/ts/features/feature.volume.ts +256 -0
- package/ts/features/index.ts +25 -0
- package/ts/helpers/helpers.iprange.ts +172 -0
- package/ts/helpers/helpers.retry.ts +100 -0
- package/ts/index.ts +165 -0
- package/ts/interfaces/feature.interfaces.ts +359 -0
- package/ts/interfaces/homeassistant.interfaces.ts +666 -0
- package/ts/interfaces/index.ts +421 -0
- package/ts/interfaces/smarthome.interfaces.ts +421 -0
- package/ts/paths.ts +5 -0
- package/ts/plugins.ts +41 -0
- package/ts/protocols/index.ts +59 -0
- package/ts/protocols/protocol.escl.ts +439 -0
- package/ts/protocols/protocol.homeassistant.ts +737 -0
- package/ts/protocols/protocol.ipp.ts +329 -0
- package/ts/protocols/protocol.nut.ts +471 -0
- package/ts/protocols/protocol.sane.ts +694 -0
- package/ts/protocols/protocol.snmp.ts +439 -0
- package/ts/protocols/protocol.upnp.ts +627 -0
- package/ts/protocols/protocol.upssnmp.ts +377 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { EsclProtocol } from '../protocols/index.js';
|
|
3
|
+
import { cidrToIps, ipRangeToIps, isValidIp, } from '../helpers/helpers.iprange.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default ports to probe for device discovery
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_PORTS = [
|
|
8
|
+
631, // IPP printers
|
|
9
|
+
80, // eSCL scanners (HTTP)
|
|
10
|
+
443, // eSCL scanners (HTTPS)
|
|
11
|
+
6566, // SANE scanners
|
|
12
|
+
9100, // JetDirect printers
|
|
13
|
+
7000, // AirPlay speakers
|
|
14
|
+
5000, // AirPlay control / RTSP
|
|
15
|
+
3689, // DAAP (iTunes/AirPlay 2)
|
|
16
|
+
1400, // Sonos speakers
|
|
17
|
+
8009, // Chromecast devices
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Default scan options
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_OPTIONS = {
|
|
23
|
+
concurrency: 50,
|
|
24
|
+
timeout: 2000,
|
|
25
|
+
ports: DEFAULT_PORTS,
|
|
26
|
+
probeEscl: true,
|
|
27
|
+
probeIpp: true,
|
|
28
|
+
probeSane: true,
|
|
29
|
+
probeAirplay: true,
|
|
30
|
+
probeSonos: true,
|
|
31
|
+
probeChromecast: true,
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Simple concurrency limiter
|
|
35
|
+
*/
|
|
36
|
+
class ConcurrencyLimiter {
|
|
37
|
+
limit;
|
|
38
|
+
running = 0;
|
|
39
|
+
queue = [];
|
|
40
|
+
constructor(limit) {
|
|
41
|
+
this.limit = limit;
|
|
42
|
+
}
|
|
43
|
+
async run(fn) {
|
|
44
|
+
while (this.running >= this.limit) {
|
|
45
|
+
await new Promise((resolve) => this.queue.push(resolve));
|
|
46
|
+
}
|
|
47
|
+
this.running++;
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
this.running--;
|
|
53
|
+
const next = this.queue.shift();
|
|
54
|
+
if (next)
|
|
55
|
+
next();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Network Scanner - scans IP ranges for scanners and printers
|
|
61
|
+
*/
|
|
62
|
+
export class NetworkScanner extends plugins.events.EventEmitter {
|
|
63
|
+
cancelled = false;
|
|
64
|
+
scanning = false;
|
|
65
|
+
/**
|
|
66
|
+
* Check if a scan is currently in progress
|
|
67
|
+
*/
|
|
68
|
+
get isScanning() {
|
|
69
|
+
return this.scanning;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Scan a network range for devices
|
|
73
|
+
*/
|
|
74
|
+
async scan(options) {
|
|
75
|
+
if (this.scanning) {
|
|
76
|
+
throw new Error('A scan is already in progress');
|
|
77
|
+
}
|
|
78
|
+
this.cancelled = false;
|
|
79
|
+
this.scanning = true;
|
|
80
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
81
|
+
const results = [];
|
|
82
|
+
try {
|
|
83
|
+
// Get list of IPs to scan
|
|
84
|
+
const ips = this.resolveIps(options);
|
|
85
|
+
if (ips.length === 0) {
|
|
86
|
+
throw new Error('No IPs to scan. Provide ipRange, or startIp and endIp.');
|
|
87
|
+
}
|
|
88
|
+
const limiter = new ConcurrencyLimiter(opts.concurrency);
|
|
89
|
+
let scanned = 0;
|
|
90
|
+
// Progress tracking
|
|
91
|
+
const emitProgress = (currentIp) => {
|
|
92
|
+
const progress = {
|
|
93
|
+
total: ips.length,
|
|
94
|
+
scanned,
|
|
95
|
+
percentage: Math.round((scanned / ips.length) * 100),
|
|
96
|
+
currentIp,
|
|
97
|
+
devicesFound: results.reduce((sum, r) => sum + r.devices.length, 0),
|
|
98
|
+
};
|
|
99
|
+
this.emit('progress', progress);
|
|
100
|
+
};
|
|
101
|
+
emitProgress();
|
|
102
|
+
// Scan all IPs with concurrency limit
|
|
103
|
+
const scanPromises = ips.map((ip) => limiter.run(async () => {
|
|
104
|
+
if (this.cancelled)
|
|
105
|
+
return;
|
|
106
|
+
const devices = await this.probeIp(ip, opts);
|
|
107
|
+
scanned++;
|
|
108
|
+
if (devices.length > 0) {
|
|
109
|
+
const result = { address: ip, devices };
|
|
110
|
+
results.push(result);
|
|
111
|
+
this.emit('device:found', result);
|
|
112
|
+
}
|
|
113
|
+
emitProgress(ip);
|
|
114
|
+
}));
|
|
115
|
+
await Promise.all(scanPromises);
|
|
116
|
+
if (this.cancelled) {
|
|
117
|
+
this.emit('cancelled');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.emit('complete', results);
|
|
121
|
+
}
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
this.scanning = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Cancel an ongoing scan
|
|
130
|
+
*/
|
|
131
|
+
async cancel() {
|
|
132
|
+
if (this.scanning) {
|
|
133
|
+
this.cancelled = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve IPs from options (CIDR or start/end range)
|
|
138
|
+
*/
|
|
139
|
+
resolveIps(options) {
|
|
140
|
+
if (options.ipRange) {
|
|
141
|
+
return cidrToIps(options.ipRange);
|
|
142
|
+
}
|
|
143
|
+
if (options.startIp && options.endIp) {
|
|
144
|
+
return ipRangeToIps(options.startIp, options.endIp);
|
|
145
|
+
}
|
|
146
|
+
if (options.startIp && !options.endIp) {
|
|
147
|
+
// Single IP
|
|
148
|
+
if (isValidIp(options.startIp)) {
|
|
149
|
+
return [options.startIp];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Probe a single IP address for devices
|
|
156
|
+
*/
|
|
157
|
+
async probeIp(ip, opts) {
|
|
158
|
+
const devices = [];
|
|
159
|
+
const timeout = opts.timeout;
|
|
160
|
+
// First, do a quick port scan to see which ports are open
|
|
161
|
+
const openPorts = await this.scanPorts(ip, opts.ports, timeout / 2);
|
|
162
|
+
if (openPorts.length === 0 || this.cancelled) {
|
|
163
|
+
return devices;
|
|
164
|
+
}
|
|
165
|
+
// Probe each open port for specific protocols
|
|
166
|
+
const probePromises = [];
|
|
167
|
+
for (const port of openPorts) {
|
|
168
|
+
// IPP probe (port 631)
|
|
169
|
+
if (opts.probeIpp && port === 631) {
|
|
170
|
+
probePromises.push(this.probeIpp(ip, port, timeout).then((device) => {
|
|
171
|
+
if (device)
|
|
172
|
+
devices.push(device);
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
// eSCL probe (ports 80, 443)
|
|
176
|
+
if (opts.probeEscl && (port === 80 || port === 443)) {
|
|
177
|
+
probePromises.push(this.probeEscl(ip, port, timeout).then((device) => {
|
|
178
|
+
if (device)
|
|
179
|
+
devices.push(device);
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
// SANE probe (port 6566)
|
|
183
|
+
if (opts.probeSane && port === 6566) {
|
|
184
|
+
probePromises.push(this.probeSane(ip, port, timeout).then((device) => {
|
|
185
|
+
if (device)
|
|
186
|
+
devices.push(device);
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
// JetDirect probe (port 9100) - just mark as raw printer
|
|
190
|
+
if (port === 9100) {
|
|
191
|
+
devices.push({
|
|
192
|
+
type: 'printer',
|
|
193
|
+
protocol: 'jetdirect',
|
|
194
|
+
port: 9100,
|
|
195
|
+
name: `Raw Printer at ${ip}`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// AirPlay probe (port 7000) - try HTTP endpoints
|
|
199
|
+
if (opts.probeAirplay && port === 7000) {
|
|
200
|
+
probePromises.push(this.probeAirplay(ip, port, timeout).then((device) => {
|
|
201
|
+
if (device)
|
|
202
|
+
devices.push(device);
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
// AirPlay ports 5000 (RTSP) and 3689 (DAAP) - if open, it's likely an AirPlay device
|
|
206
|
+
if (opts.probeAirplay && (port === 5000 || port === 3689)) {
|
|
207
|
+
devices.push({
|
|
208
|
+
type: 'speaker',
|
|
209
|
+
protocol: 'airplay',
|
|
210
|
+
port,
|
|
211
|
+
name: `AirPlay Device at ${ip}`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Sonos probe (port 1400)
|
|
215
|
+
if (opts.probeSonos && port === 1400) {
|
|
216
|
+
probePromises.push(this.probeSonos(ip, port, timeout).then((device) => {
|
|
217
|
+
if (device)
|
|
218
|
+
devices.push(device);
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
// Chromecast probe (port 8009)
|
|
222
|
+
if (opts.probeChromecast && port === 8009) {
|
|
223
|
+
probePromises.push(this.probeChromecast(ip, port, timeout).then((device) => {
|
|
224
|
+
if (device)
|
|
225
|
+
devices.push(device);
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await Promise.all(probePromises);
|
|
230
|
+
return devices;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Quick TCP port scan
|
|
234
|
+
*/
|
|
235
|
+
async scanPorts(ip, ports, timeout) {
|
|
236
|
+
const openPorts = [];
|
|
237
|
+
const scanPromises = ports.map(async (port) => {
|
|
238
|
+
const isOpen = await this.isPortOpen(ip, port, timeout);
|
|
239
|
+
if (isOpen) {
|
|
240
|
+
openPorts.push(port);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
await Promise.all(scanPromises);
|
|
244
|
+
return openPorts;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check if a TCP port is open
|
|
248
|
+
*/
|
|
249
|
+
isPortOpen(ip, port, timeout) {
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
const socket = new plugins.net.Socket();
|
|
252
|
+
const timer = setTimeout(() => {
|
|
253
|
+
socket.destroy();
|
|
254
|
+
resolve(false);
|
|
255
|
+
}, timeout);
|
|
256
|
+
socket.on('connect', () => {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
socket.destroy();
|
|
259
|
+
resolve(true);
|
|
260
|
+
});
|
|
261
|
+
socket.on('error', () => {
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
socket.destroy();
|
|
264
|
+
resolve(false);
|
|
265
|
+
});
|
|
266
|
+
socket.connect(port, ip);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Probe for IPP printer using a simple HTTP check
|
|
271
|
+
* We avoid using the full ipp library for probing since it can hang and produce noisy output
|
|
272
|
+
*/
|
|
273
|
+
async probeIpp(ip, port, timeout) {
|
|
274
|
+
try {
|
|
275
|
+
// Use a simple HTTP OPTIONS or POST to check if IPP endpoint exists
|
|
276
|
+
// IPP uses HTTP POST to /ipp/print or /ipp/printer
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
279
|
+
try {
|
|
280
|
+
const response = await fetch(`http://${ip}:${port}/ipp/print`, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: {
|
|
283
|
+
'Content-Type': 'application/ipp',
|
|
284
|
+
},
|
|
285
|
+
body: Buffer.from([
|
|
286
|
+
// Minimal IPP Get-Printer-Attributes request
|
|
287
|
+
0x01, 0x01, // IPP version 1.1
|
|
288
|
+
0x00, 0x0b, // operation-id: Get-Printer-Attributes
|
|
289
|
+
0x00, 0x00, 0x00, 0x01, // request-id: 1
|
|
290
|
+
0x01, // operation-attributes-tag
|
|
291
|
+
0x47, // charset
|
|
292
|
+
0x00, 0x12, // name-length: 18
|
|
293
|
+
...Buffer.from('attributes-charset'),
|
|
294
|
+
0x00, 0x05, // value-length: 5
|
|
295
|
+
...Buffer.from('utf-8'),
|
|
296
|
+
0x48, // naturalLanguage
|
|
297
|
+
0x00, 0x1b, // name-length: 27
|
|
298
|
+
...Buffer.from('attributes-natural-language'),
|
|
299
|
+
0x00, 0x05, // value-length: 5
|
|
300
|
+
...Buffer.from('en-us'),
|
|
301
|
+
0x03, // end-of-attributes-tag
|
|
302
|
+
]),
|
|
303
|
+
signal: controller.signal,
|
|
304
|
+
});
|
|
305
|
+
clearTimeout(timeoutId);
|
|
306
|
+
// If we get a response with application/ipp content type, it's likely an IPP printer
|
|
307
|
+
const contentType = response.headers.get('content-type') || '';
|
|
308
|
+
if (response.ok || contentType.includes('application/ipp')) {
|
|
309
|
+
return {
|
|
310
|
+
type: 'printer',
|
|
311
|
+
protocol: 'ipp',
|
|
312
|
+
port,
|
|
313
|
+
name: `IPP Printer at ${ip}`,
|
|
314
|
+
model: undefined,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (fetchErr) {
|
|
319
|
+
clearTimeout(timeoutId);
|
|
320
|
+
// Fetch failed or was aborted
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Not an IPP printer
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Probe for eSCL scanner
|
|
330
|
+
*/
|
|
331
|
+
async probeEscl(ip, port, timeout) {
|
|
332
|
+
try {
|
|
333
|
+
const secure = port === 443;
|
|
334
|
+
const escl = new EsclProtocol(ip, port, secure);
|
|
335
|
+
const caps = await Promise.race([
|
|
336
|
+
escl.getCapabilities(),
|
|
337
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout)),
|
|
338
|
+
]);
|
|
339
|
+
if (caps) {
|
|
340
|
+
return {
|
|
341
|
+
type: 'scanner',
|
|
342
|
+
protocol: 'escl',
|
|
343
|
+
port,
|
|
344
|
+
name: caps.makeAndModel || `eSCL Scanner at ${ip}`,
|
|
345
|
+
model: caps.makeAndModel,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Not an eSCL scanner
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Probe for SANE scanner (quick check)
|
|
356
|
+
*/
|
|
357
|
+
async probeSane(ip, port, timeout) {
|
|
358
|
+
// SANE probing requires full protocol implementation
|
|
359
|
+
// For now, just check if port is open and assume it's SANE
|
|
360
|
+
// A more thorough implementation would send SANE_NET_INIT
|
|
361
|
+
try {
|
|
362
|
+
const isOpen = await this.isPortOpen(ip, port, timeout);
|
|
363
|
+
if (isOpen) {
|
|
364
|
+
return {
|
|
365
|
+
type: 'scanner',
|
|
366
|
+
protocol: 'sane',
|
|
367
|
+
port,
|
|
368
|
+
name: `SANE Scanner at ${ip}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Not a SANE scanner
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Probe for AirPlay speaker on port 7000
|
|
379
|
+
* Tries HTTP endpoints to identify the device. If no HTTP response,
|
|
380
|
+
* but port 7000 is open, it's still likely an AirPlay device.
|
|
381
|
+
*/
|
|
382
|
+
async probeAirplay(ip, port, timeout) {
|
|
383
|
+
try {
|
|
384
|
+
const controller = new AbortController();
|
|
385
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
386
|
+
// Try /server-info first (older AirPlay devices)
|
|
387
|
+
try {
|
|
388
|
+
const response = await fetch(`http://${ip}:${port}/server-info`, {
|
|
389
|
+
signal: controller.signal,
|
|
390
|
+
});
|
|
391
|
+
clearTimeout(timeoutId);
|
|
392
|
+
if (response.ok) {
|
|
393
|
+
const text = await response.text();
|
|
394
|
+
// Parse model from plist if available
|
|
395
|
+
const modelMatch = text.match(/<key>model<\/key>\s*<string>([^<]+)<\/string>/);
|
|
396
|
+
const model = modelMatch?.[1];
|
|
397
|
+
return {
|
|
398
|
+
type: 'speaker',
|
|
399
|
+
protocol: 'airplay',
|
|
400
|
+
port,
|
|
401
|
+
name: model || `AirPlay Speaker at ${ip}`,
|
|
402
|
+
model,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
clearTimeout(timeoutId);
|
|
408
|
+
}
|
|
409
|
+
// Try /info endpoint (some AirPlay 2 devices)
|
|
410
|
+
const controller2 = new AbortController();
|
|
411
|
+
const timeoutId2 = setTimeout(() => controller2.abort(), timeout);
|
|
412
|
+
try {
|
|
413
|
+
const response = await fetch(`http://${ip}:${port}/info`, {
|
|
414
|
+
signal: controller2.signal,
|
|
415
|
+
});
|
|
416
|
+
clearTimeout(timeoutId2);
|
|
417
|
+
if (response.ok) {
|
|
418
|
+
const text = await response.text();
|
|
419
|
+
// Try to parse model info
|
|
420
|
+
const modelMatch = text.match(/<key>model<\/key>\s*<string>([^<]+)<\/string>/);
|
|
421
|
+
const nameMatch = text.match(/<key>name<\/key>\s*<string>([^<]+)<\/string>/);
|
|
422
|
+
const model = modelMatch?.[1];
|
|
423
|
+
const name = nameMatch?.[1];
|
|
424
|
+
return {
|
|
425
|
+
type: 'speaker',
|
|
426
|
+
protocol: 'airplay',
|
|
427
|
+
port,
|
|
428
|
+
name: name || model || `AirPlay Speaker at ${ip}`,
|
|
429
|
+
model,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
clearTimeout(timeoutId2);
|
|
435
|
+
}
|
|
436
|
+
// Port 7000 is open but no HTTP endpoints responded
|
|
437
|
+
// Still likely an AirPlay device (AirPlay 2 / HomePod)
|
|
438
|
+
return {
|
|
439
|
+
type: 'speaker',
|
|
440
|
+
protocol: 'airplay',
|
|
441
|
+
port,
|
|
442
|
+
name: `AirPlay Device at ${ip}`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Not an AirPlay speaker
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Probe for Sonos speaker
|
|
452
|
+
*/
|
|
453
|
+
async probeSonos(ip, port, timeout) {
|
|
454
|
+
try {
|
|
455
|
+
const controller = new AbortController();
|
|
456
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
457
|
+
try {
|
|
458
|
+
// Sonos devices respond to device description requests
|
|
459
|
+
const response = await fetch(`http://${ip}:${port}/xml/device_description.xml`, {
|
|
460
|
+
signal: controller.signal,
|
|
461
|
+
});
|
|
462
|
+
clearTimeout(timeoutId);
|
|
463
|
+
if (response.ok) {
|
|
464
|
+
const text = await response.text();
|
|
465
|
+
// Check if it's actually a Sonos device
|
|
466
|
+
if (text.includes('Sonos') || text.includes('schemas-upnp-org')) {
|
|
467
|
+
// Parse friendly name and model
|
|
468
|
+
const nameMatch = text.match(/<friendlyName>([^<]+)<\/friendlyName>/);
|
|
469
|
+
const modelMatch = text.match(/<modelName>([^<]+)<\/modelName>/);
|
|
470
|
+
return {
|
|
471
|
+
type: 'speaker',
|
|
472
|
+
protocol: 'sonos',
|
|
473
|
+
port,
|
|
474
|
+
name: nameMatch?.[1] || `Sonos Speaker at ${ip}`,
|
|
475
|
+
model: modelMatch?.[1],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
clearTimeout(timeoutId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// Not a Sonos speaker
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Probe for Chromecast device
|
|
491
|
+
*/
|
|
492
|
+
async probeChromecast(ip, port, timeout) {
|
|
493
|
+
try {
|
|
494
|
+
const controller = new AbortController();
|
|
495
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
496
|
+
try {
|
|
497
|
+
// Chromecast devices have an info endpoint on port 8008 (HTTP)
|
|
498
|
+
// Port 8009 is the Cast protocol port (TLS)
|
|
499
|
+
// Try fetching the eureka_info endpoint
|
|
500
|
+
const response = await fetch(`http://${ip}:8008/setup/eureka_info`, {
|
|
501
|
+
signal: controller.signal,
|
|
502
|
+
});
|
|
503
|
+
clearTimeout(timeoutId);
|
|
504
|
+
if (response.ok) {
|
|
505
|
+
const data = await response.json();
|
|
506
|
+
return {
|
|
507
|
+
type: 'speaker',
|
|
508
|
+
protocol: 'chromecast',
|
|
509
|
+
port,
|
|
510
|
+
name: data.name || `Chromecast at ${ip}`,
|
|
511
|
+
model: data.cast_build_revision || data.model_name,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
clearTimeout(timeoutId);
|
|
517
|
+
}
|
|
518
|
+
// Alternative: just check if port 8009 is open (Cast protocol)
|
|
519
|
+
const isOpen = await this.isPortOpen(ip, port, timeout);
|
|
520
|
+
if (isOpen) {
|
|
521
|
+
return {
|
|
522
|
+
type: 'speaker',
|
|
523
|
+
protocol: 'chromecast',
|
|
524
|
+
port,
|
|
525
|
+
name: `Chromecast at ${ip}`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
// Not a Chromecast
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY292ZXJ5LmNsYXNzZXMubmV0d29ya3NjYW5uZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kaXNjb3ZlcnkvZGlzY292ZXJ5LmNsYXNzZXMubmV0d29ya3NjYW5uZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUM7QUFDekMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ3JELE9BQU8sRUFDTCxTQUFTLEVBQ1QsWUFBWSxFQUNaLFNBQVMsR0FDVixNQUFNLCtCQUErQixDQUFDO0FBUXZDOztHQUVHO0FBQ0gsTUFBTSxhQUFhLEdBQUc7SUFDcEIsR0FBRyxFQUFJLGVBQWU7SUFDdEIsRUFBRSxFQUFLLHVCQUF1QjtJQUM5QixHQUFHLEVBQUksd0JBQXdCO0lBQy9CLElBQUksRUFBRyxnQkFBZ0I7SUFDdkIsSUFBSSxFQUFHLHFCQUFxQjtJQUM1QixJQUFJLEVBQUcsbUJBQW1CO0lBQzFCLElBQUksRUFBRyx5QkFBeUI7SUFDaEMsSUFBSSxFQUFHLDBCQUEwQjtJQUNqQyxJQUFJLEVBQUcsaUJBQWlCO0lBQ3hCLElBQUksRUFBRyxxQkFBcUI7Q0FDN0IsQ0FBQztBQUVGOztHQUVHO0FBQ0gsTUFBTSxlQUFlLEdBQXlFO0lBQzVGLFdBQVcsRUFBRSxFQUFFO0lBQ2YsT0FBTyxFQUFFLElBQUk7SUFDYixLQUFLLEVBQUUsYUFBYTtJQUNwQixTQUFTLEVBQUUsSUFBSTtJQUNmLFFBQVEsRUFBRSxJQUFJO0lBQ2QsU0FBUyxFQUFFLElBQUk7SUFDZixZQUFZLEVBQUUsSUFBSTtJQUNsQixVQUFVLEVBQUUsSUFBSTtJQUNoQixlQUFlLEVBQUUsSUFBSTtDQUN0QixDQUFDO0FBRUY7O0dBRUc7QUFDSCxNQUFNLGtCQUFrQjtJQUlGO0lBSFosT0FBTyxHQUFHLENBQUMsQ0FBQztJQUNaLEtBQUssR0FBbUIsRUFBRSxDQUFDO0lBRW5DLFlBQW9C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
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
/**
|
|
3
|
+
* SSDP service types for device discovery
|
|
4
|
+
*/
|
|
5
|
+
export declare const SSDP_SERVICE_TYPES: {
|
|
6
|
+
ROOT: string;
|
|
7
|
+
MEDIA_RENDERER: string;
|
|
8
|
+
MEDIA_SERVER: string;
|
|
9
|
+
SONOS_ZONE_PLAYER: string;
|
|
10
|
+
BASIC_DEVICE: string;
|
|
11
|
+
INTERNET_GATEWAY: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* SSDP discovered device information
|
|
15
|
+
*/
|
|
16
|
+
export interface ISsdpDevice {
|
|
17
|
+
/** Unique service name (USN) */
|
|
18
|
+
usn: string;
|
|
19
|
+
/** Service type (ST) */
|
|
20
|
+
serviceType: string;
|
|
21
|
+
/** Location URL of device description */
|
|
22
|
+
location: string;
|
|
23
|
+
/** IP address */
|
|
24
|
+
address: string;
|
|
25
|
+
/** Port number */
|
|
26
|
+
port: number;
|
|
27
|
+
/** Device description (fetched from location) */
|
|
28
|
+
description?: ISsdpDeviceDescription;
|
|
29
|
+
/** Raw headers from SSDP response */
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* UPnP device description (from XML)
|
|
34
|
+
*/
|
|
35
|
+
export interface ISsdpDeviceDescription {
|
|
36
|
+
deviceType: string;
|
|
37
|
+
friendlyName: string;
|
|
38
|
+
manufacturer: string;
|
|
39
|
+
manufacturerURL?: string;
|
|
40
|
+
modelDescription?: string;
|
|
41
|
+
modelName: string;
|
|
42
|
+
modelNumber?: string;
|
|
43
|
+
modelURL?: string;
|
|
44
|
+
serialNumber?: string;
|
|
45
|
+
UDN: string;
|
|
46
|
+
services: ISsdpService[];
|
|
47
|
+
icons?: ISsdpIcon[];
|
|
48
|
+
}
|
|
49
|
+
export interface ISsdpService {
|
|
50
|
+
serviceType: string;
|
|
51
|
+
serviceId: string;
|
|
52
|
+
SCPDURL: string;
|
|
53
|
+
controlURL: string;
|
|
54
|
+
eventSubURL: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ISsdpIcon {
|
|
57
|
+
mimetype: string;
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
depth: number;
|
|
61
|
+
url: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* SSDP Discovery service using node-ssdp
|
|
65
|
+
*/
|
|
66
|
+
export declare class SsdpDiscovery extends plugins.events.EventEmitter {
|
|
67
|
+
private client;
|
|
68
|
+
private devices;
|
|
69
|
+
private running;
|
|
70
|
+
private searchInterval;
|
|
71
|
+
constructor();
|
|
72
|
+
/**
|
|
73
|
+
* Start SSDP discovery
|
|
74
|
+
*/
|
|
75
|
+
start(serviceTypes?: string[]): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Stop SSDP discovery
|
|
78
|
+
*/
|
|
79
|
+
stop(): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Check if discovery is running
|
|
82
|
+
*/
|
|
83
|
+
get isRunning(): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Get all discovered devices
|
|
86
|
+
*/
|
|
87
|
+
getDevices(): ISsdpDevice[];
|
|
88
|
+
/**
|
|
89
|
+
* Get devices by service type
|
|
90
|
+
*/
|
|
91
|
+
getDevicesByType(serviceType: string): ISsdpDevice[];
|
|
92
|
+
/**
|
|
93
|
+
* Search for a specific service type
|
|
94
|
+
*/
|
|
95
|
+
search(serviceType: string): void;
|
|
96
|
+
/**
|
|
97
|
+
* Handle SSDP response
|
|
98
|
+
*/
|
|
99
|
+
private handleSsdpResponse;
|
|
100
|
+
/**
|
|
101
|
+
* Fetch and parse device description XML
|
|
102
|
+
*/
|
|
103
|
+
private fetchDeviceDescription;
|
|
104
|
+
/**
|
|
105
|
+
* Parse UPnP device description XML
|
|
106
|
+
*/
|
|
107
|
+
private parseDeviceDescription;
|
|
108
|
+
/**
|
|
109
|
+
* Make a UPnP SOAP request
|
|
110
|
+
*/
|
|
111
|
+
soapRequest(controlUrl: string, serviceType: string, action: string, args?: Record<string, string | number>): Promise<string>;
|
|
112
|
+
}
|