@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,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
|
+
}
|