@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,223 @@
1
+ /**
2
+ * Lock Feature
3
+ * Provides control for smart locks
4
+ */
5
+
6
+ import { Feature, type TDeviceReference } from './feature.abstract.js';
7
+ import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
8
+ import type {
9
+ TLockProtocol,
10
+ TLockState,
11
+ ILockCapabilities,
12
+ ILockStateInfo,
13
+ ILockFeatureInfo,
14
+ ILockProtocolClient,
15
+ } from '../interfaces/smarthome.interfaces.js';
16
+
17
+ /**
18
+ * Options for creating a LockFeature
19
+ */
20
+ export interface ILockFeatureOptions extends IFeatureOptions {
21
+ /** Protocol type */
22
+ protocol: TLockProtocol;
23
+ /** Entity ID (for Home Assistant) */
24
+ entityId: string;
25
+ /** Protocol client for controlling the lock */
26
+ protocolClient: ILockProtocolClient;
27
+ /** Whether the lock supports physical open */
28
+ supportsOpen?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Lock Feature - control for smart locks
33
+ *
34
+ * Protocol-agnostic: works with Home Assistant, MQTT, August, Yale, etc.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const lock = device.getFeature<LockFeature>('lock');
39
+ * if (lock) {
40
+ * await lock.lock();
41
+ * console.log(`Lock is ${lock.isLocked ? 'locked' : 'unlocked'}`);
42
+ * }
43
+ * ```
44
+ */
45
+ export class LockFeature extends Feature {
46
+ public readonly type = 'lock' as const;
47
+ public readonly protocol: TLockProtocol;
48
+
49
+ /** Entity ID (e.g., "lock.front_door") */
50
+ public readonly entityId: string;
51
+
52
+ /** Capabilities */
53
+ public readonly capabilities: ILockCapabilities;
54
+
55
+ /** Current state */
56
+ protected _lockState: TLockState = 'unknown';
57
+ protected _isLocked: boolean = false;
58
+
59
+ /** Protocol client for controlling the lock */
60
+ private protocolClient: ILockProtocolClient;
61
+
62
+ constructor(
63
+ device: TDeviceReference,
64
+ port: number,
65
+ options: ILockFeatureOptions
66
+ ) {
67
+ super(device, port, options);
68
+ this.protocol = options.protocol;
69
+ this.entityId = options.entityId;
70
+ this.protocolClient = options.protocolClient;
71
+
72
+ this.capabilities = {
73
+ supportsOpen: options.supportsOpen ?? false,
74
+ };
75
+ }
76
+
77
+ // ============================================================================
78
+ // Properties
79
+ // ============================================================================
80
+
81
+ /**
82
+ * Get current lock state (cached)
83
+ */
84
+ public get lockState(): TLockState {
85
+ return this._lockState;
86
+ }
87
+
88
+ /**
89
+ * Check if locked (cached)
90
+ */
91
+ public get isLocked(): boolean {
92
+ return this._isLocked;
93
+ }
94
+
95
+ /**
96
+ * Check if unlocked
97
+ */
98
+ public get isUnlocked(): boolean {
99
+ return this._lockState === 'unlocked';
100
+ }
101
+
102
+ /**
103
+ * Check if currently locking
104
+ */
105
+ public get isLocking(): boolean {
106
+ return this._lockState === 'locking';
107
+ }
108
+
109
+ /**
110
+ * Check if currently unlocking
111
+ */
112
+ public get isUnlocking(): boolean {
113
+ return this._lockState === 'unlocking';
114
+ }
115
+
116
+ /**
117
+ * Check if jammed
118
+ */
119
+ public get isJammed(): boolean {
120
+ return this._lockState === 'jammed';
121
+ }
122
+
123
+ // ============================================================================
124
+ // Connection
125
+ // ============================================================================
126
+
127
+ protected async doConnect(): Promise<void> {
128
+ try {
129
+ const state = await this.protocolClient.getState(this.entityId);
130
+ this.updateStateInternal(state);
131
+ } catch {
132
+ // Ignore errors fetching initial state
133
+ }
134
+ }
135
+
136
+ protected async doDisconnect(): Promise<void> {
137
+ // Nothing to disconnect
138
+ }
139
+
140
+ // ============================================================================
141
+ // Lock Control
142
+ // ============================================================================
143
+
144
+ /**
145
+ * Lock the lock
146
+ */
147
+ public async lock(): Promise<void> {
148
+ await this.protocolClient.lock(this.entityId);
149
+ this._lockState = 'locking';
150
+ this.emit('state:changed', this.getState());
151
+ }
152
+
153
+ /**
154
+ * Unlock the lock
155
+ */
156
+ public async unlock(): Promise<void> {
157
+ await this.protocolClient.unlock(this.entityId);
158
+ this._lockState = 'unlocking';
159
+ this.emit('state:changed', this.getState());
160
+ }
161
+
162
+ /**
163
+ * Open the lock (physically open the door if supported)
164
+ */
165
+ public async open(): Promise<void> {
166
+ if (!this.capabilities.supportsOpen) {
167
+ throw new Error('Lock does not support physical open');
168
+ }
169
+ await this.protocolClient.open(this.entityId);
170
+ this._lockState = 'unlocked';
171
+ this._isLocked = false;
172
+ this.emit('state:changed', this.getState());
173
+ }
174
+
175
+ /**
176
+ * Get current state as object
177
+ */
178
+ public getState(): ILockStateInfo {
179
+ return {
180
+ state: this._lockState,
181
+ isLocked: this._isLocked,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Refresh state from the device
187
+ */
188
+ public async refreshState(): Promise<ILockStateInfo> {
189
+ const state = await this.protocolClient.getState(this.entityId);
190
+ this.updateStateInternal(state);
191
+ return state;
192
+ }
193
+
194
+ /**
195
+ * Update state from external source
196
+ */
197
+ public updateState(state: ILockStateInfo): void {
198
+ this.updateStateInternal(state);
199
+ this.emit('state:changed', state);
200
+ }
201
+
202
+ /**
203
+ * Internal state update
204
+ */
205
+ private updateStateInternal(state: ILockStateInfo): void {
206
+ this._lockState = state.state;
207
+ this._isLocked = state.isLocked;
208
+ }
209
+
210
+ // ============================================================================
211
+ // Feature Info
212
+ // ============================================================================
213
+
214
+ public getFeatureInfo(): ILockFeatureInfo {
215
+ return {
216
+ ...this.getBaseFeatureInfo(),
217
+ type: 'lock',
218
+ protocol: this.protocol,
219
+ capabilities: this.capabilities,
220
+ currentState: this.getState(),
221
+ };
222
+ }
223
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Playback Feature
3
+ * Provides media playback control capability
4
+ */
5
+
6
+ import { Feature, type TDeviceReference } from './feature.abstract.js';
7
+ import type {
8
+ TPlaybackProtocol,
9
+ TPlaybackState,
10
+ ITrackInfo,
11
+ IPlaybackStatus,
12
+ IPlaybackFeatureInfo,
13
+ IFeatureOptions,
14
+ } from '../interfaces/feature.interfaces.js';
15
+
16
+ /**
17
+ * Options for creating a PlaybackFeature
18
+ */
19
+ export interface IPlaybackFeatureOptions extends IFeatureOptions {
20
+ protocol: TPlaybackProtocol;
21
+ supportsQueue?: boolean;
22
+ supportsSeek?: boolean;
23
+ /** Protocol-specific client instance (Sonos, AirPlay, etc.) */
24
+ protocolClient?: unknown;
25
+ }
26
+
27
+ /**
28
+ * Playback Feature - provides media playback control
29
+ *
30
+ * Abstract feature that can be backed by different protocols (Sonos, AirPlay, Chromecast, DLNA).
31
+ * Concrete implementations should extend this class for protocol-specific behavior.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const playback = device.getFeature<PlaybackFeature>('playback');
36
+ * if (playback) {
37
+ * await playback.play('spotify:track:123');
38
+ * const status = await playback.getPlaybackStatus();
39
+ * console.log(`Playing: ${status.track?.title}`);
40
+ * }
41
+ * ```
42
+ */
43
+ export class PlaybackFeature extends Feature {
44
+ public readonly type = 'playback' as const;
45
+ public readonly protocol: TPlaybackProtocol;
46
+
47
+ // Capabilities
48
+ public supportsQueue: boolean = true;
49
+ public supportsSeek: boolean = true;
50
+
51
+ // Current state
52
+ protected _playbackState: TPlaybackState = 'stopped';
53
+ protected _currentTrack: ITrackInfo | null = null;
54
+ protected _position: number = 0;
55
+ protected _duration: number = 0;
56
+
57
+ // Protocol client (set by subclass or passed in options)
58
+ protected protocolClient: unknown = null;
59
+
60
+ constructor(
61
+ device: TDeviceReference,
62
+ port: number,
63
+ options: IPlaybackFeatureOptions
64
+ ) {
65
+ super(device, port, options);
66
+ this.protocol = options.protocol;
67
+ if (options.supportsQueue !== undefined) this.supportsQueue = options.supportsQueue;
68
+ if (options.supportsSeek !== undefined) this.supportsSeek = options.supportsSeek;
69
+ if (options.protocolClient) this.protocolClient = options.protocolClient;
70
+ }
71
+
72
+ // ============================================================================
73
+ // Properties
74
+ // ============================================================================
75
+
76
+ public get playbackState(): TPlaybackState {
77
+ return this._playbackState;
78
+ }
79
+
80
+ public get currentTrack(): ITrackInfo | null {
81
+ return this._currentTrack;
82
+ }
83
+
84
+ public get position(): number {
85
+ return this._position;
86
+ }
87
+
88
+ public get duration(): number {
89
+ return this._duration;
90
+ }
91
+
92
+ public get isPlaying(): boolean {
93
+ return this._playbackState === 'playing';
94
+ }
95
+
96
+ // ============================================================================
97
+ // Connection (to be overridden by protocol-specific subclasses)
98
+ // ============================================================================
99
+
100
+ protected async doConnect(): Promise<void> {
101
+ // Base implementation - protocol-specific subclasses should override
102
+ // and establish their protocol connection
103
+ }
104
+
105
+ protected async doDisconnect(): Promise<void> {
106
+ // Base implementation - protocol-specific subclasses should override
107
+ this.protocolClient = null;
108
+ }
109
+
110
+ // ============================================================================
111
+ // Playback Control
112
+ // ============================================================================
113
+
114
+ /**
115
+ * Start playback
116
+ * @param uri Optional URI to play. If not provided, resumes current playback.
117
+ */
118
+ public async play(uri?: string): Promise<void> {
119
+ this.emit('playback:play', { uri });
120
+ // Protocol-specific implementation should be provided by subclass
121
+ this._playbackState = 'playing';
122
+ this.emit('playback:state:changed', this._playbackState);
123
+ }
124
+
125
+ /**
126
+ * Pause playback
127
+ */
128
+ public async pause(): Promise<void> {
129
+ this.emit('playback:pause');
130
+ this._playbackState = 'paused';
131
+ this.emit('playback:state:changed', this._playbackState);
132
+ }
133
+
134
+ /**
135
+ * Stop playback
136
+ */
137
+ public async stop(): Promise<void> {
138
+ this.emit('playback:stop');
139
+ this._playbackState = 'stopped';
140
+ this._position = 0;
141
+ this.emit('playback:state:changed', this._playbackState);
142
+ }
143
+
144
+ /**
145
+ * Skip to next track
146
+ */
147
+ public async next(): Promise<void> {
148
+ this.emit('playback:next');
149
+ }
150
+
151
+ /**
152
+ * Go to previous track
153
+ */
154
+ public async previous(): Promise<void> {
155
+ this.emit('playback:previous');
156
+ }
157
+
158
+ /**
159
+ * Seek to position
160
+ * @param seconds Position in seconds
161
+ */
162
+ public async seek(seconds: number): Promise<void> {
163
+ if (!this.supportsSeek) {
164
+ throw new Error('Seek not supported');
165
+ }
166
+ this._position = seconds;
167
+ this.emit('playback:seek', seconds);
168
+ }
169
+
170
+ // ============================================================================
171
+ // Status
172
+ // ============================================================================
173
+
174
+ /**
175
+ * Get current track info
176
+ */
177
+ public async getCurrentTrack(): Promise<ITrackInfo | null> {
178
+ return this._currentTrack;
179
+ }
180
+
181
+ /**
182
+ * Get playback status
183
+ */
184
+ public async getPlaybackStatus(): Promise<IPlaybackStatus> {
185
+ return {
186
+ state: this._playbackState,
187
+ position: this._position,
188
+ duration: this._duration,
189
+ track: this._currentTrack ?? undefined,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Update track info (called by protocol-specific implementations)
195
+ */
196
+ protected updateTrack(track: ITrackInfo | null): void {
197
+ const oldTrack = this._currentTrack;
198
+ this._currentTrack = track;
199
+ if (track?.title !== oldTrack?.title || track?.uri !== oldTrack?.uri) {
200
+ this.emit('playback:track:changed', track);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Update position (called by protocol-specific implementations)
206
+ */
207
+ protected updatePosition(position: number, duration: number): void {
208
+ this._position = position;
209
+ this._duration = duration;
210
+ this.emit('playback:position:changed', { position, duration });
211
+ }
212
+
213
+ // ============================================================================
214
+ // Serialization
215
+ // ============================================================================
216
+
217
+ public getFeatureInfo(): IPlaybackFeatureInfo {
218
+ return {
219
+ ...this.getBaseFeatureInfo(),
220
+ type: 'playback',
221
+ protocol: this.protocol,
222
+ supportsQueue: this.supportsQueue,
223
+ supportsSeek: this.supportsSeek,
224
+ };
225
+ }
226
+
227
+ // ============================================================================
228
+ // Static Factory
229
+ // ============================================================================
230
+
231
+ /**
232
+ * Create from discovery metadata
233
+ */
234
+ public static fromDiscovery(
235
+ device: TDeviceReference,
236
+ port: number,
237
+ protocol: TPlaybackProtocol,
238
+ metadata: Record<string, unknown>
239
+ ): PlaybackFeature {
240
+ return new PlaybackFeature(device, port, {
241
+ protocol,
242
+ supportsQueue: metadata.supportsQueue as boolean ?? true,
243
+ supportsSeek: metadata.supportsSeek as boolean ?? true,
244
+ });
245
+ }
246
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Power Feature
3
+ * Provides UPS/power monitoring and control capability
4
+ */
5
+
6
+ import { Feature, type TDeviceReference } from './feature.abstract.js';
7
+ import type {
8
+ TPowerProtocol,
9
+ TPowerStatus,
10
+ IBatteryInfo,
11
+ IPowerInfo,
12
+ IPowerFeatureInfo,
13
+ IFeatureOptions,
14
+ } from '../interfaces/feature.interfaces.js';
15
+
16
+ /**
17
+ * Options for creating a PowerFeature
18
+ */
19
+ export interface IPowerFeatureOptions extends IFeatureOptions {
20
+ protocol: TPowerProtocol;
21
+ hasBattery?: boolean;
22
+ supportsShutdown?: boolean;
23
+ supportsTest?: boolean;
24
+ /** For NUT protocol: UPS name */
25
+ upsName?: string;
26
+ /** For SNMP: community string */
27
+ community?: string;
28
+ }
29
+
30
+ /**
31
+ * Power Feature - provides UPS/power monitoring and control
32
+ *
33
+ * Supports NUT (Network UPS Tools) and SNMP protocols for monitoring
34
+ * UPS devices and smart power equipment.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const power = device.getFeature<PowerFeature>('power');
39
+ * if (power) {
40
+ * const status = await power.getStatus();
41
+ * const battery = await power.getBatteryInfo();
42
+ * console.log(`Status: ${status}, Battery: ${battery.charge}%`);
43
+ * }
44
+ * ```
45
+ */
46
+ export class PowerFeature extends Feature {
47
+ public readonly type = 'power' as const;
48
+ public readonly protocol: TPowerProtocol;
49
+
50
+ // Configuration
51
+ public readonly upsName: string;
52
+ public readonly community: string;
53
+
54
+ // Capabilities
55
+ public readonly hasBattery: boolean;
56
+ public readonly supportsShutdown: boolean;
57
+ public readonly supportsTest: boolean;
58
+
59
+ // Current state
60
+ protected _status: TPowerStatus = 'unknown';
61
+ protected _batteryCharge: number = 0;
62
+ protected _batteryRuntime: number = 0;
63
+ protected _load: number = 0;
64
+
65
+ constructor(
66
+ device: TDeviceReference,
67
+ port: number,
68
+ options: IPowerFeatureOptions
69
+ ) {
70
+ super(device, port, options);
71
+ this.protocol = options.protocol;
72
+ this.upsName = options.upsName ?? 'ups';
73
+ this.community = options.community ?? 'public';
74
+ this.hasBattery = options.hasBattery ?? true;
75
+ this.supportsShutdown = options.supportsShutdown ?? false;
76
+ this.supportsTest = options.supportsTest ?? false;
77
+ }
78
+
79
+ // ============================================================================
80
+ // Properties
81
+ // ============================================================================
82
+
83
+ public get powerStatus(): TPowerStatus {
84
+ return this._status;
85
+ }
86
+
87
+ public get batteryCharge(): number {
88
+ return this._batteryCharge;
89
+ }
90
+
91
+ public get batteryRuntime(): number {
92
+ return this._batteryRuntime;
93
+ }
94
+
95
+ public get load(): number {
96
+ return this._load;
97
+ }
98
+
99
+ // ============================================================================
100
+ // Connection
101
+ // ============================================================================
102
+
103
+ protected async doConnect(): Promise<void> {
104
+ // Protocol-specific connection would be implemented here
105
+ // For now, just verify the port is reachable
106
+ }
107
+
108
+ protected async doDisconnect(): Promise<void> {
109
+ // Protocol-specific disconnection
110
+ }
111
+
112
+ // ============================================================================
113
+ // Status Monitoring
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Get current power status
118
+ */
119
+ public async getStatus(): Promise<TPowerStatus> {
120
+ // Protocol-specific implementation would fetch status
121
+ return this._status;
122
+ }
123
+
124
+ /**
125
+ * Get battery information
126
+ */
127
+ public async getBatteryInfo(): Promise<IBatteryInfo> {
128
+ return {
129
+ charge: this._batteryCharge,
130
+ runtime: this._batteryRuntime,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Get power information
136
+ */
137
+ public async getPowerInfo(): Promise<IPowerInfo> {
138
+ return {
139
+ load: this._load,
140
+ };
141
+ }
142
+
143
+ // ============================================================================
144
+ // Control Commands
145
+ // ============================================================================
146
+
147
+ /**
148
+ * Initiate shutdown
149
+ */
150
+ public async shutdown(delay?: number): Promise<void> {
151
+ if (!this.supportsShutdown) {
152
+ throw new Error('Shutdown not supported');
153
+ }
154
+ this.emit('power:shutdown', { delay });
155
+ }
156
+
157
+ /**
158
+ * Start battery test
159
+ */
160
+ public async testBattery(): Promise<void> {
161
+ if (!this.supportsTest) {
162
+ throw new Error('Battery test not supported');
163
+ }
164
+ this.emit('power:test:started');
165
+ }
166
+
167
+ // ============================================================================
168
+ // State Updates
169
+ // ============================================================================
170
+
171
+ protected updateStatus(status: TPowerStatus): void {
172
+ const oldStatus = this._status;
173
+ this._status = status;
174
+ if (oldStatus !== status) {
175
+ this.emit('power:status:changed', { oldStatus, newStatus: status });
176
+
177
+ // Emit specific events
178
+ if (status === 'onbattery') {
179
+ this.emit('power:onbattery');
180
+ } else if (status === 'online' && oldStatus === 'onbattery') {
181
+ this.emit('power:restored');
182
+ } else if (status === 'lowbattery') {
183
+ this.emit('power:lowbattery');
184
+ }
185
+ }
186
+ }
187
+
188
+ protected updateBattery(charge: number, runtime: number): void {
189
+ const oldCharge = this._batteryCharge;
190
+ this._batteryCharge = charge;
191
+ this._batteryRuntime = runtime;
192
+
193
+ if (oldCharge !== charge) {
194
+ this.emit('battery:changed', { charge, runtime });
195
+ }
196
+ }
197
+
198
+ // ============================================================================
199
+ // Serialization
200
+ // ============================================================================
201
+
202
+ public getFeatureInfo(): IPowerFeatureInfo {
203
+ return {
204
+ ...this.getBaseFeatureInfo(),
205
+ type: 'power',
206
+ protocol: this.protocol,
207
+ hasBattery: this.hasBattery,
208
+ supportsShutdown: this.supportsShutdown,
209
+ supportsTest: this.supportsTest,
210
+ };
211
+ }
212
+
213
+ // ============================================================================
214
+ // Static Factory
215
+ // ============================================================================
216
+
217
+ public static fromDiscovery(
218
+ device: TDeviceReference,
219
+ port: number,
220
+ protocol: TPowerProtocol,
221
+ metadata: Record<string, unknown>
222
+ ): PowerFeature {
223
+ return new PowerFeature(device, port, {
224
+ protocol,
225
+ upsName: metadata.upsName as string,
226
+ hasBattery: metadata.hasBattery as boolean ?? true,
227
+ supportsShutdown: metadata.supportsShutdown as boolean ?? false,
228
+ supportsTest: metadata.supportsTest as boolean ?? false,
229
+ });
230
+ }
231
+ }