@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.
Files changed (137) hide show
  1. package/.gitea/workflows/default_nottags.yaml +66 -0
  2. package/.gitea/workflows/default_tags.yaml +124 -0
  3. package/.vscode/launch.json +11 -0
  4. package/.vscode/settings.json +26 -0
  5. package/changelog.md +91 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/device/device.classes.device.d.ts +205 -0
  9. package/dist_ts/device/device.classes.device.js +344 -0
  10. package/dist_ts/devicemanager.classes.devicemanager.d.ts +210 -0
  11. package/dist_ts/devicemanager.classes.devicemanager.js +835 -0
  12. package/dist_ts/discovery/discovery.classes.homeassistant.d.ts +101 -0
  13. package/dist_ts/discovery/discovery.classes.homeassistant.js +313 -0
  14. package/dist_ts/discovery/discovery.classes.mdns.d.ts +90 -0
  15. package/dist_ts/discovery/discovery.classes.mdns.js +291 -0
  16. package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +64 -0
  17. package/dist_ts/discovery/discovery.classes.networkscanner.js +535 -0
  18. package/dist_ts/discovery/discovery.classes.ssdp.d.ts +112 -0
  19. package/dist_ts/discovery/discovery.classes.ssdp.js +255 -0
  20. package/dist_ts/factories/index.d.ts +190 -0
  21. package/dist_ts/factories/index.js +350 -0
  22. package/dist_ts/features/feature.abstract.d.ts +118 -0
  23. package/dist_ts/features/feature.abstract.js +172 -0
  24. package/dist_ts/features/feature.camera.d.ts +95 -0
  25. package/dist_ts/features/feature.camera.js +162 -0
  26. package/dist_ts/features/feature.climate.d.ts +174 -0
  27. package/dist_ts/features/feature.climate.js +328 -0
  28. package/dist_ts/features/feature.cover.d.ts +121 -0
  29. package/dist_ts/features/feature.cover.js +217 -0
  30. package/dist_ts/features/feature.fan.d.ts +127 -0
  31. package/dist_ts/features/feature.fan.js +235 -0
  32. package/dist_ts/features/feature.light.d.ts +156 -0
  33. package/dist_ts/features/feature.light.js +296 -0
  34. package/dist_ts/features/feature.lock.d.ts +103 -0
  35. package/dist_ts/features/feature.lock.js +170 -0
  36. package/dist_ts/features/feature.playback.d.ts +98 -0
  37. package/dist_ts/features/feature.playback.js +191 -0
  38. package/dist_ts/features/feature.power.d.ts +79 -0
  39. package/dist_ts/features/feature.power.js +173 -0
  40. package/dist_ts/features/feature.print.d.ts +78 -0
  41. package/dist_ts/features/feature.print.js +226 -0
  42. package/dist_ts/features/feature.scan.d.ts +79 -0
  43. package/dist_ts/features/feature.scan.js +315 -0
  44. package/dist_ts/features/feature.sensor.d.ts +95 -0
  45. package/dist_ts/features/feature.sensor.js +147 -0
  46. package/dist_ts/features/feature.snmp.d.ts +88 -0
  47. package/dist_ts/features/feature.snmp.js +175 -0
  48. package/dist_ts/features/feature.switch.d.ts +75 -0
  49. package/dist_ts/features/feature.switch.js +126 -0
  50. package/dist_ts/features/feature.volume.d.ts +104 -0
  51. package/dist_ts/features/feature.volume.js +186 -0
  52. package/dist_ts/features/index.d.ts +19 -0
  53. package/dist_ts/features/index.js +23 -0
  54. package/dist_ts/helpers/helpers.iprange.d.ts +35 -0
  55. package/dist_ts/helpers/helpers.iprange.js +151 -0
  56. package/dist_ts/helpers/helpers.retry.d.ts +25 -0
  57. package/dist_ts/helpers/helpers.retry.js +69 -0
  58. package/dist_ts/index.d.ts +15 -0
  59. package/dist_ts/index.js +40 -0
  60. package/dist_ts/interfaces/feature.interfaces.d.ts +255 -0
  61. package/dist_ts/interfaces/feature.interfaces.js +6 -0
  62. package/dist_ts/interfaces/homeassistant.interfaces.d.ts +550 -0
  63. package/dist_ts/interfaces/homeassistant.interfaces.js +111 -0
  64. package/dist_ts/interfaces/index.d.ts +330 -0
  65. package/dist_ts/interfaces/index.js +16 -0
  66. package/dist_ts/interfaces/smarthome.interfaces.d.ts +282 -0
  67. package/dist_ts/interfaces/smarthome.interfaces.js +7 -0
  68. package/dist_ts/paths.d.ts +1 -0
  69. package/dist_ts/paths.js +3 -0
  70. package/dist_ts/plugins.d.ts +23 -0
  71. package/dist_ts/plugins.js +30 -0
  72. package/dist_ts/protocols/index.d.ts +12 -0
  73. package/dist_ts/protocols/index.js +21 -0
  74. package/dist_ts/protocols/protocol.escl.d.ts +58 -0
  75. package/dist_ts/protocols/protocol.escl.js +364 -0
  76. package/dist_ts/protocols/protocol.homeassistant.d.ts +248 -0
  77. package/dist_ts/protocols/protocol.homeassistant.js +611 -0
  78. package/dist_ts/protocols/protocol.ipp.d.ts +45 -0
  79. package/dist_ts/protocols/protocol.ipp.js +284 -0
  80. package/dist_ts/protocols/protocol.nut.d.ts +160 -0
  81. package/dist_ts/protocols/protocol.nut.js +364 -0
  82. package/dist_ts/protocols/protocol.sane.d.ts +104 -0
  83. package/dist_ts/protocols/protocol.sane.js +597 -0
  84. package/dist_ts/protocols/protocol.snmp.d.ts +133 -0
  85. package/dist_ts/protocols/protocol.snmp.js +333 -0
  86. package/dist_ts/protocols/protocol.upnp.d.ts +207 -0
  87. package/dist_ts/protocols/protocol.upnp.js +442 -0
  88. package/dist_ts/protocols/protocol.upssnmp.d.ts +160 -0
  89. package/dist_ts/protocols/protocol.upssnmp.js +263 -0
  90. package/npmextra.json +24 -0
  91. package/package.json +39 -0
  92. package/readme.hints.md +115 -0
  93. package/readme.md +624 -0
  94. package/test/test.ts +261 -0
  95. package/ts/00_commitinfo_data.ts +8 -0
  96. package/ts/device/device.classes.device.ts +454 -0
  97. package/ts/devicemanager.classes.devicemanager.ts +1219 -0
  98. package/ts/discovery/discovery.classes.homeassistant.ts +376 -0
  99. package/ts/discovery/discovery.classes.mdns.ts +347 -0
  100. package/ts/discovery/discovery.classes.networkscanner.ts +648 -0
  101. package/ts/discovery/discovery.classes.ssdp.ts +357 -0
  102. package/ts/factories/index.ts +696 -0
  103. package/ts/features/feature.abstract.ts +251 -0
  104. package/ts/features/feature.camera.ts +214 -0
  105. package/ts/features/feature.climate.ts +407 -0
  106. package/ts/features/feature.cover.ts +278 -0
  107. package/ts/features/feature.fan.ts +296 -0
  108. package/ts/features/feature.light.ts +369 -0
  109. package/ts/features/feature.lock.ts +223 -0
  110. package/ts/features/feature.playback.ts +246 -0
  111. package/ts/features/feature.power.ts +231 -0
  112. package/ts/features/feature.print.ts +297 -0
  113. package/ts/features/feature.scan.ts +370 -0
  114. package/ts/features/feature.sensor.ts +202 -0
  115. package/ts/features/feature.snmp.ts +235 -0
  116. package/ts/features/feature.switch.ts +170 -0
  117. package/ts/features/feature.volume.ts +256 -0
  118. package/ts/features/index.ts +25 -0
  119. package/ts/helpers/helpers.iprange.ts +172 -0
  120. package/ts/helpers/helpers.retry.ts +100 -0
  121. package/ts/index.ts +165 -0
  122. package/ts/interfaces/feature.interfaces.ts +359 -0
  123. package/ts/interfaces/homeassistant.interfaces.ts +666 -0
  124. package/ts/interfaces/index.ts +421 -0
  125. package/ts/interfaces/smarthome.interfaces.ts +421 -0
  126. package/ts/paths.ts +5 -0
  127. package/ts/plugins.ts +41 -0
  128. package/ts/protocols/index.ts +59 -0
  129. package/ts/protocols/protocol.escl.ts +439 -0
  130. package/ts/protocols/protocol.homeassistant.ts +737 -0
  131. package/ts/protocols/protocol.ipp.ts +329 -0
  132. package/ts/protocols/protocol.nut.ts +471 -0
  133. package/ts/protocols/protocol.sane.ts +694 -0
  134. package/ts/protocols/protocol.snmp.ts +439 -0
  135. package/ts/protocols/protocol.upnp.ts +627 -0
  136. package/ts/protocols/protocol.upssnmp.ts +377 -0
  137. package/tsconfig.json +12 -0
@@ -0,0 +1,357 @@
1
+ import * as plugins from '../plugins.js';
2
+
3
+ /**
4
+ * SSDP service types for device discovery
5
+ */
6
+ export const SSDP_SERVICE_TYPES = {
7
+ // Root device
8
+ ROOT: 'upnp:rootdevice',
9
+
10
+ // DLNA/UPnP AV
11
+ MEDIA_RENDERER: 'urn:schemas-upnp-org:device:MediaRenderer:1',
12
+ MEDIA_SERVER: 'urn:schemas-upnp-org:device:MediaServer:1',
13
+
14
+ // Sonos
15
+ SONOS_ZONE_PLAYER: 'urn:schemas-upnp-org:device:ZonePlayer:1',
16
+
17
+ // Generic
18
+ BASIC_DEVICE: 'urn:schemas-upnp-org:device:Basic:1',
19
+ INTERNET_GATEWAY: 'urn:schemas-upnp-org:device:InternetGatewayDevice:1',
20
+ };
21
+
22
+ /**
23
+ * SSDP discovered device information
24
+ */
25
+ export interface ISsdpDevice {
26
+ /** Unique service name (USN) */
27
+ usn: string;
28
+ /** Service type (ST) */
29
+ serviceType: string;
30
+ /** Location URL of device description */
31
+ location: string;
32
+ /** IP address */
33
+ address: string;
34
+ /** Port number */
35
+ port: number;
36
+ /** Device description (fetched from location) */
37
+ description?: ISsdpDeviceDescription;
38
+ /** Raw headers from SSDP response */
39
+ headers: Record<string, string>;
40
+ }
41
+
42
+ /**
43
+ * UPnP device description (from XML)
44
+ */
45
+ export interface ISsdpDeviceDescription {
46
+ deviceType: string;
47
+ friendlyName: string;
48
+ manufacturer: string;
49
+ manufacturerURL?: string;
50
+ modelDescription?: string;
51
+ modelName: string;
52
+ modelNumber?: string;
53
+ modelURL?: string;
54
+ serialNumber?: string;
55
+ UDN: string;
56
+ services: ISsdpService[];
57
+ icons?: ISsdpIcon[];
58
+ }
59
+
60
+ export interface ISsdpService {
61
+ serviceType: string;
62
+ serviceId: string;
63
+ SCPDURL: string;
64
+ controlURL: string;
65
+ eventSubURL: string;
66
+ }
67
+
68
+ export interface ISsdpIcon {
69
+ mimetype: string;
70
+ width: number;
71
+ height: number;
72
+ depth: number;
73
+ url: string;
74
+ }
75
+
76
+ /**
77
+ * SSDP Discovery service using node-ssdp
78
+ */
79
+ export class SsdpDiscovery extends plugins.events.EventEmitter {
80
+ private client: InstanceType<typeof plugins.nodeSsdp.Client> | null = null;
81
+ private devices: Map<string, ISsdpDevice> = new Map();
82
+ private running = false;
83
+ private searchInterval: NodeJS.Timeout | null = null;
84
+
85
+ constructor() {
86
+ super();
87
+ }
88
+
89
+ /**
90
+ * Start SSDP discovery
91
+ */
92
+ public async start(serviceTypes?: string[]): Promise<void> {
93
+ if (this.running) {
94
+ return;
95
+ }
96
+
97
+ this.running = true;
98
+ this.client = new plugins.nodeSsdp.Client();
99
+
100
+ // Handle SSDP responses
101
+ this.client.on('response', (headers: Record<string, string>, statusCode: number, rinfo: { address: string; port: number }) => {
102
+ this.handleSsdpResponse(headers, rinfo);
103
+ });
104
+
105
+ // Search for devices
106
+ const typesToSearch = serviceTypes ?? [
107
+ SSDP_SERVICE_TYPES.ROOT,
108
+ SSDP_SERVICE_TYPES.MEDIA_RENDERER,
109
+ SSDP_SERVICE_TYPES.MEDIA_SERVER,
110
+ SSDP_SERVICE_TYPES.SONOS_ZONE_PLAYER,
111
+ ];
112
+
113
+ // Initial search
114
+ for (const st of typesToSearch) {
115
+ this.client.search(st);
116
+ }
117
+
118
+ // Periodic re-search (every 30 seconds)
119
+ this.searchInterval = setInterval(() => {
120
+ if (this.client) {
121
+ for (const st of typesToSearch) {
122
+ this.client.search(st);
123
+ }
124
+ }
125
+ }, 30000);
126
+
127
+ this.emit('started');
128
+ }
129
+
130
+ /**
131
+ * Stop SSDP discovery
132
+ */
133
+ public async stop(): Promise<void> {
134
+ if (!this.running) {
135
+ return;
136
+ }
137
+
138
+ this.running = false;
139
+
140
+ if (this.searchInterval) {
141
+ clearInterval(this.searchInterval);
142
+ this.searchInterval = null;
143
+ }
144
+
145
+ if (this.client) {
146
+ this.client.stop();
147
+ this.client = null;
148
+ }
149
+
150
+ this.emit('stopped');
151
+ }
152
+
153
+ /**
154
+ * Check if discovery is running
155
+ */
156
+ public get isRunning(): boolean {
157
+ return this.running;
158
+ }
159
+
160
+ /**
161
+ * Get all discovered devices
162
+ */
163
+ public getDevices(): ISsdpDevice[] {
164
+ return Array.from(this.devices.values());
165
+ }
166
+
167
+ /**
168
+ * Get devices by service type
169
+ */
170
+ public getDevicesByType(serviceType: string): ISsdpDevice[] {
171
+ return this.getDevices().filter((d) => d.serviceType === serviceType);
172
+ }
173
+
174
+ /**
175
+ * Search for a specific service type
176
+ */
177
+ public search(serviceType: string): void {
178
+ if (this.client && this.running) {
179
+ this.client.search(serviceType);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handle SSDP response
185
+ */
186
+ private handleSsdpResponse(
187
+ headers: Record<string, string>,
188
+ rinfo: { address: string; port: number }
189
+ ): void {
190
+ const usn = headers['USN'] || headers['usn'];
191
+ const location = headers['LOCATION'] || headers['location'];
192
+ const st = headers['ST'] || headers['st'];
193
+
194
+ if (!usn || !location) {
195
+ return;
196
+ }
197
+
198
+ // Parse location URL
199
+ let address = rinfo.address;
200
+ let port = 80;
201
+
202
+ try {
203
+ const url = new URL(location);
204
+ address = url.hostname;
205
+ port = parseInt(url.port) || (url.protocol === 'https:' ? 443 : 80);
206
+ } catch {
207
+ // Keep rinfo address
208
+ }
209
+
210
+ const device: ISsdpDevice = {
211
+ usn,
212
+ serviceType: st || 'unknown',
213
+ location,
214
+ address,
215
+ port,
216
+ headers: { ...headers },
217
+ };
218
+
219
+ const isNew = !this.devices.has(usn);
220
+ this.devices.set(usn, device);
221
+
222
+ if (isNew) {
223
+ // Fetch device description
224
+ this.fetchDeviceDescription(device).then(() => {
225
+ this.emit('device:found', device);
226
+ }).catch(() => {
227
+ // Still emit even without description
228
+ this.emit('device:found', device);
229
+ });
230
+ } else {
231
+ this.emit('device:updated', device);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Fetch and parse device description XML
237
+ */
238
+ private async fetchDeviceDescription(device: ISsdpDevice): Promise<void> {
239
+ try {
240
+ const response = await fetch(device.location, {
241
+ signal: AbortSignal.timeout(5000),
242
+ });
243
+
244
+ if (!response.ok) {
245
+ return;
246
+ }
247
+
248
+ const xml = await response.text();
249
+ device.description = this.parseDeviceDescription(xml);
250
+ } catch {
251
+ // Ignore fetch errors
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Parse UPnP device description XML
257
+ */
258
+ private parseDeviceDescription(xml: string): ISsdpDeviceDescription {
259
+ const getTagContent = (tag: string, source: string = xml): string => {
260
+ const regex = new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, 'i');
261
+ const match = source.match(regex);
262
+ return match?.[1]?.trim() ?? '';
263
+ };
264
+
265
+ const getTagBlock = (tag: string, source: string = xml): string => {
266
+ const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'i');
267
+ const match = source.match(regex);
268
+ return match?.[1] ?? '';
269
+ };
270
+
271
+ // Parse services
272
+ const services: ISsdpService[] = [];
273
+ const serviceListBlock = getTagBlock('serviceList');
274
+ const serviceMatches = serviceListBlock.match(/<service>[\s\S]*?<\/service>/gi) || [];
275
+
276
+ for (const serviceXml of serviceMatches) {
277
+ services.push({
278
+ serviceType: getTagContent('serviceType', serviceXml),
279
+ serviceId: getTagContent('serviceId', serviceXml),
280
+ SCPDURL: getTagContent('SCPDURL', serviceXml),
281
+ controlURL: getTagContent('controlURL', serviceXml),
282
+ eventSubURL: getTagContent('eventSubURL', serviceXml),
283
+ });
284
+ }
285
+
286
+ // Parse icons
287
+ const icons: ISsdpIcon[] = [];
288
+ const iconListBlock = getTagBlock('iconList');
289
+ const iconMatches = iconListBlock.match(/<icon>[\s\S]*?<\/icon>/gi) || [];
290
+
291
+ for (const iconXml of iconMatches) {
292
+ icons.push({
293
+ mimetype: getTagContent('mimetype', iconXml),
294
+ width: parseInt(getTagContent('width', iconXml)) || 0,
295
+ height: parseInt(getTagContent('height', iconXml)) || 0,
296
+ depth: parseInt(getTagContent('depth', iconXml)) || 0,
297
+ url: getTagContent('url', iconXml),
298
+ });
299
+ }
300
+
301
+ return {
302
+ deviceType: getTagContent('deviceType'),
303
+ friendlyName: getTagContent('friendlyName'),
304
+ manufacturer: getTagContent('manufacturer'),
305
+ manufacturerURL: getTagContent('manufacturerURL') || undefined,
306
+ modelDescription: getTagContent('modelDescription') || undefined,
307
+ modelName: getTagContent('modelName'),
308
+ modelNumber: getTagContent('modelNumber') || undefined,
309
+ modelURL: getTagContent('modelURL') || undefined,
310
+ serialNumber: getTagContent('serialNumber') || undefined,
311
+ UDN: getTagContent('UDN'),
312
+ services,
313
+ icons: icons.length > 0 ? icons : undefined,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Make a UPnP SOAP request
319
+ */
320
+ public async soapRequest(
321
+ controlUrl: string,
322
+ serviceType: string,
323
+ action: string,
324
+ args: Record<string, string | number> = {}
325
+ ): Promise<string> {
326
+ // Build SOAP body
327
+ let argsXml = '';
328
+ for (const [key, value] of Object.entries(args)) {
329
+ argsXml += `<${key}>${value}</${key}>`;
330
+ }
331
+
332
+ const soapBody = `<?xml version="1.0" encoding="utf-8"?>
333
+ <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
334
+ <s:Body>
335
+ <u:${action} xmlns:u="${serviceType}">
336
+ ${argsXml}
337
+ </u:${action}>
338
+ </s:Body>
339
+ </s:Envelope>`;
340
+
341
+ const response = await fetch(controlUrl, {
342
+ method: 'POST',
343
+ headers: {
344
+ 'Content-Type': 'text/xml; charset=utf-8',
345
+ 'SOAPACTION': `"${serviceType}#${action}"`,
346
+ },
347
+ body: soapBody,
348
+ signal: AbortSignal.timeout(10000),
349
+ });
350
+
351
+ if (!response.ok) {
352
+ throw new Error(`SOAP request failed: ${response.status}`);
353
+ }
354
+
355
+ return response.text();
356
+ }
357
+ }