@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,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Device class
|
|
3
|
+
* A device is a network endpoint that can have multiple features (capabilities)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as plugins from '../plugins.js';
|
|
7
|
+
import type {
|
|
8
|
+
TFeatureType,
|
|
9
|
+
IFeatureInfo,
|
|
10
|
+
IDiscoveredFeature,
|
|
11
|
+
} from '../interfaces/feature.interfaces.js';
|
|
12
|
+
import type { TDeviceStatus, IRetryOptions } from '../interfaces/index.js';
|
|
13
|
+
import { Feature, type TDeviceReference } from '../features/feature.abstract.js';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Device Info Interface
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Serializable device information
|
|
21
|
+
*/
|
|
22
|
+
export interface IUniversalDeviceInfo {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
address: string;
|
|
26
|
+
port: number;
|
|
27
|
+
status: TDeviceStatus;
|
|
28
|
+
manufacturer?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
serialNumber?: string;
|
|
31
|
+
firmwareVersion?: string;
|
|
32
|
+
features: IFeatureInfo[];
|
|
33
|
+
featureTypes: TFeatureType[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for creating a device
|
|
38
|
+
*/
|
|
39
|
+
export interface IDeviceCreateOptions {
|
|
40
|
+
name?: string;
|
|
41
|
+
manufacturer?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
serialNumber?: string;
|
|
44
|
+
firmwareVersion?: string;
|
|
45
|
+
retryOptions?: IRetryOptions;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Universal Device Class
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Universal Device - represents any network device with composable features
|
|
54
|
+
*
|
|
55
|
+
* Instead of having separate Scanner, Printer, Speaker classes, a Device can have
|
|
56
|
+
* any combination of features (scan, print, playback, volume, etc.)
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // A multifunction printer/scanner
|
|
61
|
+
* const mfp = new UniversalDevice('192.168.1.100', 80);
|
|
62
|
+
* mfp.addFeature(new ScanFeature(mfp, 80));
|
|
63
|
+
* mfp.addFeature(new PrintFeature(mfp, 631));
|
|
64
|
+
*
|
|
65
|
+
* // Check capabilities
|
|
66
|
+
* if (mfp.hasFeature('scan') && mfp.hasFeature('print')) {
|
|
67
|
+
* const scanFeature = mfp.getFeature<ScanFeature>('scan');
|
|
68
|
+
* await scanFeature.scan({ format: 'pdf' });
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export class UniversalDevice extends plugins.events.EventEmitter {
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Properties
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Unique device identifier
|
|
79
|
+
* Format: {address}:{port} or custom ID from discovery
|
|
80
|
+
*/
|
|
81
|
+
public readonly id: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Human-readable device name
|
|
85
|
+
*/
|
|
86
|
+
public name: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Device network address (IP or hostname)
|
|
90
|
+
*/
|
|
91
|
+
public readonly address: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Primary device port
|
|
95
|
+
*/
|
|
96
|
+
public readonly port: number;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Device manufacturer
|
|
100
|
+
*/
|
|
101
|
+
public manufacturer?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Device model
|
|
105
|
+
*/
|
|
106
|
+
public model?: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Device serial number
|
|
110
|
+
*/
|
|
111
|
+
public serialNumber?: string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Device firmware version
|
|
115
|
+
*/
|
|
116
|
+
public firmwareVersion?: string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Current device status
|
|
120
|
+
*/
|
|
121
|
+
protected _status: TDeviceStatus = 'unknown';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Map of features by type
|
|
125
|
+
*/
|
|
126
|
+
protected _features: Map<TFeatureType, Feature> = new Map();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Retry options for features
|
|
130
|
+
*/
|
|
131
|
+
protected _retryOptions: IRetryOptions;
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Constructor
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
constructor(
|
|
138
|
+
address: string,
|
|
139
|
+
port: number,
|
|
140
|
+
options?: IDeviceCreateOptions
|
|
141
|
+
) {
|
|
142
|
+
super();
|
|
143
|
+
this.address = address;
|
|
144
|
+
this.port = port;
|
|
145
|
+
this.id = `${address}:${port}`;
|
|
146
|
+
this.name = options?.name ?? `Device at ${address}`;
|
|
147
|
+
this.manufacturer = options?.manufacturer;
|
|
148
|
+
this.model = options?.model;
|
|
149
|
+
this.serialNumber = options?.serialNumber;
|
|
150
|
+
this.firmwareVersion = options?.firmwareVersion;
|
|
151
|
+
this._retryOptions = options?.retryOptions ?? {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Status Management
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get current device status
|
|
160
|
+
*/
|
|
161
|
+
public get status(): TDeviceStatus {
|
|
162
|
+
return this._status;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set device status
|
|
167
|
+
*/
|
|
168
|
+
public set status(value: TDeviceStatus) {
|
|
169
|
+
if (this._status !== value) {
|
|
170
|
+
const oldStatus = this._status;
|
|
171
|
+
this._status = value;
|
|
172
|
+
this.emit('status:changed', { oldStatus, newStatus: value });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if device is online
|
|
178
|
+
*/
|
|
179
|
+
public get isOnline(): boolean {
|
|
180
|
+
return this._status === 'online';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Feature Management
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Add a feature to the device
|
|
189
|
+
*/
|
|
190
|
+
public addFeature(feature: Feature): void {
|
|
191
|
+
if (this._features.has(feature.type)) {
|
|
192
|
+
throw new Error(`Feature '${feature.type}' already exists on device ${this.id}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this._features.set(feature.type, feature);
|
|
196
|
+
|
|
197
|
+
// Forward feature events
|
|
198
|
+
feature.on('connected', () => this.emit('feature:connected', feature.type));
|
|
199
|
+
feature.on('disconnected', () => this.emit('feature:disconnected', feature.type));
|
|
200
|
+
feature.on('error', (error) => this.emit('feature:error', { type: feature.type, error }));
|
|
201
|
+
feature.on('state:changed', (states) => this.emit('feature:state:changed', { type: feature.type, ...states }));
|
|
202
|
+
|
|
203
|
+
this.emit('feature:added', feature);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Remove a feature from the device
|
|
208
|
+
*/
|
|
209
|
+
public async removeFeature(type: TFeatureType): Promise<boolean> {
|
|
210
|
+
const feature = this._features.get(type);
|
|
211
|
+
if (!feature) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Disconnect the feature first
|
|
216
|
+
try {
|
|
217
|
+
await feature.disconnect();
|
|
218
|
+
} catch {
|
|
219
|
+
// Ignore disconnect errors
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this._features.delete(type);
|
|
223
|
+
this.emit('feature:removed', type);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check if device has a specific feature
|
|
229
|
+
*/
|
|
230
|
+
public hasFeature(type: TFeatureType): boolean {
|
|
231
|
+
return this._features.has(type);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if device has all specified features
|
|
236
|
+
*/
|
|
237
|
+
public hasFeatures(types: TFeatureType[]): boolean {
|
|
238
|
+
return types.every((type) => this._features.has(type));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if device has any of the specified features
|
|
243
|
+
*/
|
|
244
|
+
public hasAnyFeature(types: TFeatureType[]): boolean {
|
|
245
|
+
return types.some((type) => this._features.has(type));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get a feature by type (returns undefined if not available)
|
|
250
|
+
*/
|
|
251
|
+
public getFeature<T extends Feature>(type: TFeatureType): T | undefined {
|
|
252
|
+
return this._features.get(type) as T | undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Select a feature by type (throws if not available).
|
|
257
|
+
* Use this when you expect the device to have this feature and want fail-fast behavior.
|
|
258
|
+
*
|
|
259
|
+
* @param type The feature type to select
|
|
260
|
+
* @returns The feature instance
|
|
261
|
+
* @throws Error if the device does not have this feature
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* const scanFeature = device.selectFeature<ScanFeature>('scan');
|
|
266
|
+
* await scanFeature.connect();
|
|
267
|
+
* const result = await scanFeature.scan({ source: 'flatbed' });
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
public selectFeature<T extends Feature>(type: TFeatureType): T {
|
|
271
|
+
const feature = this._features.get(type) as T | undefined;
|
|
272
|
+
if (!feature) {
|
|
273
|
+
throw new Error(`Device '${this.name}' does not have feature '${type}'`);
|
|
274
|
+
}
|
|
275
|
+
return feature;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get all features
|
|
280
|
+
*/
|
|
281
|
+
public getFeatures(): Feature[] {
|
|
282
|
+
return Array.from(this._features.values());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get all feature types
|
|
287
|
+
*/
|
|
288
|
+
public getFeatureTypes(): TFeatureType[] {
|
|
289
|
+
return Array.from(this._features.keys());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get feature count
|
|
294
|
+
*/
|
|
295
|
+
public get featureCount(): number {
|
|
296
|
+
return this._features.size;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// Connection Management
|
|
301
|
+
// ============================================================================
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Connect all features
|
|
305
|
+
*/
|
|
306
|
+
public async connect(): Promise<void> {
|
|
307
|
+
const features = this.getFeatures();
|
|
308
|
+
const results = await Promise.allSettled(
|
|
309
|
+
features.map((f) => f.connect())
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Check if any connected successfully
|
|
313
|
+
const anyConnected = results.some((r) => r.status === 'fulfilled');
|
|
314
|
+
|
|
315
|
+
if (anyConnected) {
|
|
316
|
+
this.status = 'online';
|
|
317
|
+
} else if (results.length > 0) {
|
|
318
|
+
// All failed
|
|
319
|
+
this.status = 'error';
|
|
320
|
+
const errors = results
|
|
321
|
+
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
|
|
322
|
+
.map((r) => r.reason);
|
|
323
|
+
throw new AggregateError(errors, 'Failed to connect any features');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Connect a specific feature
|
|
329
|
+
*/
|
|
330
|
+
public async connectFeature(type: TFeatureType): Promise<void> {
|
|
331
|
+
const feature = this._features.get(type);
|
|
332
|
+
if (!feature) {
|
|
333
|
+
throw new Error(`Feature '${type}' not found on device ${this.id}`);
|
|
334
|
+
}
|
|
335
|
+
await feature.connect();
|
|
336
|
+
|
|
337
|
+
// Update device status if this is the first connected feature
|
|
338
|
+
if (this._status !== 'online') {
|
|
339
|
+
this.status = 'online';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Disconnect all features
|
|
345
|
+
*/
|
|
346
|
+
public async disconnect(): Promise<void> {
|
|
347
|
+
const features = this.getFeatures();
|
|
348
|
+
await Promise.allSettled(
|
|
349
|
+
features.map((f) => f.disconnect())
|
|
350
|
+
);
|
|
351
|
+
this.status = 'offline';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Disconnect a specific feature
|
|
356
|
+
*/
|
|
357
|
+
public async disconnectFeature(type: TFeatureType): Promise<void> {
|
|
358
|
+
const feature = this._features.get(type);
|
|
359
|
+
if (feature) {
|
|
360
|
+
await feature.disconnect();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Update device status if all features are disconnected
|
|
364
|
+
const anyConnected = this.getFeatures().some((f) => f.isConnected);
|
|
365
|
+
if (!anyConnected) {
|
|
366
|
+
this.status = 'offline';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================================
|
|
371
|
+
// Serialization
|
|
372
|
+
// ============================================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get device reference for features
|
|
376
|
+
*/
|
|
377
|
+
public getDeviceReference(): TDeviceReference {
|
|
378
|
+
return {
|
|
379
|
+
id: this.id,
|
|
380
|
+
name: this.name,
|
|
381
|
+
address: this.address,
|
|
382
|
+
port: this.port,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get serializable device info
|
|
388
|
+
*/
|
|
389
|
+
public getDeviceInfo(): IUniversalDeviceInfo {
|
|
390
|
+
return {
|
|
391
|
+
id: this.id,
|
|
392
|
+
name: this.name,
|
|
393
|
+
address: this.address,
|
|
394
|
+
port: this.port,
|
|
395
|
+
status: this._status,
|
|
396
|
+
manufacturer: this.manufacturer,
|
|
397
|
+
model: this.model,
|
|
398
|
+
serialNumber: this.serialNumber,
|
|
399
|
+
firmwareVersion: this.firmwareVersion,
|
|
400
|
+
features: this.getFeatures().map((f) => f.getFeatureInfo()),
|
|
401
|
+
featureTypes: this.getFeatureTypes(),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get retry options for features
|
|
407
|
+
*/
|
|
408
|
+
public get retryOptions(): IRetryOptions {
|
|
409
|
+
return this._retryOptions;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// Static Factory Methods
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Create a device from discovered features
|
|
418
|
+
*/
|
|
419
|
+
public static fromDiscovery(
|
|
420
|
+
id: string,
|
|
421
|
+
name: string,
|
|
422
|
+
address: string,
|
|
423
|
+
port: number,
|
|
424
|
+
discoveredFeatures: IDiscoveredFeature[],
|
|
425
|
+
options?: IDeviceCreateOptions
|
|
426
|
+
): UniversalDevice {
|
|
427
|
+
const device = new UniversalDevice(address, port, {
|
|
428
|
+
...options,
|
|
429
|
+
name,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Override the generated ID with the discovery ID
|
|
433
|
+
(device as { id: string }).id = id;
|
|
434
|
+
|
|
435
|
+
// Features will be added by the DeviceManager after creation
|
|
436
|
+
// This is because feature instances need protocol-specific initialization
|
|
437
|
+
|
|
438
|
+
return device;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// Type Guards
|
|
444
|
+
// ============================================================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Check if a device has a specific feature (type guard)
|
|
448
|
+
*/
|
|
449
|
+
export function deviceHasFeature<T extends Feature>(
|
|
450
|
+
device: UniversalDevice,
|
|
451
|
+
type: TFeatureType
|
|
452
|
+
): device is UniversalDevice & { getFeature(type: TFeatureType): T } {
|
|
453
|
+
return device.hasFeature(type);
|
|
454
|
+
}
|