@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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fan Feature
|
|
3
|
+
* Provides control for fans (speed, oscillation, direction)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Feature, type TDeviceReference } from './feature.abstract.js';
|
|
7
|
+
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
|
|
8
|
+
import type {
|
|
9
|
+
TFanProtocol,
|
|
10
|
+
TFanDirection,
|
|
11
|
+
IFanCapabilities,
|
|
12
|
+
IFanState,
|
|
13
|
+
IFanFeatureInfo,
|
|
14
|
+
IFanProtocolClient,
|
|
15
|
+
} from '../interfaces/smarthome.interfaces.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for creating a FanFeature
|
|
19
|
+
*/
|
|
20
|
+
export interface IFanFeatureOptions extends IFeatureOptions {
|
|
21
|
+
/** Protocol type */
|
|
22
|
+
protocol: TFanProtocol;
|
|
23
|
+
/** Entity ID (for Home Assistant) */
|
|
24
|
+
entityId: string;
|
|
25
|
+
/** Protocol client for controlling the fan */
|
|
26
|
+
protocolClient: IFanProtocolClient;
|
|
27
|
+
/** Fan capabilities */
|
|
28
|
+
capabilities?: Partial<IFanCapabilities>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fan Feature - speed, oscillation, and direction control
|
|
33
|
+
*
|
|
34
|
+
* Protocol-agnostic: works with Home Assistant, MQTT, Bond, etc.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const fan = device.getFeature<FanFeature>('fan');
|
|
39
|
+
* if (fan) {
|
|
40
|
+
* await fan.turnOn(75); // 75% speed
|
|
41
|
+
* await fan.setOscillating(true);
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class FanFeature extends Feature {
|
|
46
|
+
public readonly type = 'fan' as const;
|
|
47
|
+
public readonly protocol: TFanProtocol;
|
|
48
|
+
|
|
49
|
+
/** Entity ID (e.g., "fan.bedroom") */
|
|
50
|
+
public readonly entityId: string;
|
|
51
|
+
|
|
52
|
+
/** Capabilities */
|
|
53
|
+
public readonly capabilities: IFanCapabilities;
|
|
54
|
+
|
|
55
|
+
/** Current state */
|
|
56
|
+
protected _isOn: boolean = false;
|
|
57
|
+
protected _percentage?: number;
|
|
58
|
+
protected _presetMode?: string;
|
|
59
|
+
protected _oscillating?: boolean;
|
|
60
|
+
protected _direction?: TFanDirection;
|
|
61
|
+
|
|
62
|
+
/** Protocol client for controlling the fan */
|
|
63
|
+
private protocolClient: IFanProtocolClient;
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
device: TDeviceReference,
|
|
67
|
+
port: number,
|
|
68
|
+
options: IFanFeatureOptions
|
|
69
|
+
) {
|
|
70
|
+
super(device, port, options);
|
|
71
|
+
this.protocol = options.protocol;
|
|
72
|
+
this.entityId = options.entityId;
|
|
73
|
+
this.protocolClient = options.protocolClient;
|
|
74
|
+
|
|
75
|
+
this.capabilities = {
|
|
76
|
+
supportsSpeed: options.capabilities?.supportsSpeed ?? true,
|
|
77
|
+
supportsOscillate: options.capabilities?.supportsOscillate ?? false,
|
|
78
|
+
supportsDirection: options.capabilities?.supportsDirection ?? false,
|
|
79
|
+
supportsPresetModes: options.capabilities?.supportsPresetModes ?? false,
|
|
80
|
+
presetModes: options.capabilities?.presetModes,
|
|
81
|
+
speedCount: options.capabilities?.speedCount,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Properties
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get current on/off state (cached)
|
|
91
|
+
*/
|
|
92
|
+
public get isOn(): boolean {
|
|
93
|
+
return this._isOn;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get current speed percentage 0-100 (cached)
|
|
98
|
+
*/
|
|
99
|
+
public get percentage(): number | undefined {
|
|
100
|
+
return this._percentage;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get current preset mode (cached)
|
|
105
|
+
*/
|
|
106
|
+
public get presetMode(): string | undefined {
|
|
107
|
+
return this._presetMode;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get oscillating state (cached)
|
|
112
|
+
*/
|
|
113
|
+
public get oscillating(): boolean | undefined {
|
|
114
|
+
return this._oscillating;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get direction (cached)
|
|
119
|
+
*/
|
|
120
|
+
public get direction(): TFanDirection | undefined {
|
|
121
|
+
return this._direction;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get available preset modes
|
|
126
|
+
*/
|
|
127
|
+
public get presetModes(): string[] | undefined {
|
|
128
|
+
return this.capabilities.presetModes;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Connection
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
protected async doConnect(): Promise<void> {
|
|
136
|
+
try {
|
|
137
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
138
|
+
this.updateStateInternal(state);
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore errors fetching initial state
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
protected async doDisconnect(): Promise<void> {
|
|
145
|
+
// Nothing to disconnect
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Fan Control
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Turn on the fan
|
|
154
|
+
* @param percentage Optional speed percentage
|
|
155
|
+
*/
|
|
156
|
+
public async turnOn(percentage?: number): Promise<void> {
|
|
157
|
+
await this.protocolClient.turnOn(this.entityId, percentage);
|
|
158
|
+
this._isOn = true;
|
|
159
|
+
if (percentage !== undefined) {
|
|
160
|
+
this._percentage = percentage;
|
|
161
|
+
}
|
|
162
|
+
this.emit('state:changed', this.getState());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Turn off the fan
|
|
167
|
+
*/
|
|
168
|
+
public async turnOff(): Promise<void> {
|
|
169
|
+
await this.protocolClient.turnOff(this.entityId);
|
|
170
|
+
this._isOn = false;
|
|
171
|
+
this.emit('state:changed', this.getState());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Toggle the fan
|
|
176
|
+
*/
|
|
177
|
+
public async toggle(): Promise<void> {
|
|
178
|
+
await this.protocolClient.toggle(this.entityId);
|
|
179
|
+
this._isOn = !this._isOn;
|
|
180
|
+
this.emit('state:changed', this.getState());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Set speed percentage
|
|
185
|
+
* @param percentage Speed 0-100
|
|
186
|
+
*/
|
|
187
|
+
public async setPercentage(percentage: number): Promise<void> {
|
|
188
|
+
if (!this.capabilities.supportsSpeed) {
|
|
189
|
+
throw new Error('Fan does not support speed control');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const clamped = Math.max(0, Math.min(100, Math.round(percentage)));
|
|
193
|
+
await this.protocolClient.setPercentage(this.entityId, clamped);
|
|
194
|
+
this._percentage = clamped;
|
|
195
|
+
this._isOn = clamped > 0;
|
|
196
|
+
this.emit('state:changed', this.getState());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Set preset mode
|
|
201
|
+
* @param mode Preset mode name
|
|
202
|
+
*/
|
|
203
|
+
public async setPresetMode(mode: string): Promise<void> {
|
|
204
|
+
if (!this.capabilities.supportsPresetModes) {
|
|
205
|
+
throw new Error('Fan does not support preset modes');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await this.protocolClient.setPresetMode(this.entityId, mode);
|
|
209
|
+
this._presetMode = mode;
|
|
210
|
+
this._isOn = true;
|
|
211
|
+
this.emit('state:changed', this.getState());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Set oscillating state
|
|
216
|
+
* @param oscillating Whether to oscillate
|
|
217
|
+
*/
|
|
218
|
+
public async setOscillating(oscillating: boolean): Promise<void> {
|
|
219
|
+
if (!this.capabilities.supportsOscillate) {
|
|
220
|
+
throw new Error('Fan does not support oscillation');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await this.protocolClient.setOscillating(this.entityId, oscillating);
|
|
224
|
+
this._oscillating = oscillating;
|
|
225
|
+
this.emit('state:changed', this.getState());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Set direction
|
|
230
|
+
* @param direction forward or reverse
|
|
231
|
+
*/
|
|
232
|
+
public async setDirection(direction: TFanDirection): Promise<void> {
|
|
233
|
+
if (!this.capabilities.supportsDirection) {
|
|
234
|
+
throw new Error('Fan does not support direction control');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await this.protocolClient.setDirection(this.entityId, direction);
|
|
238
|
+
this._direction = direction;
|
|
239
|
+
this.emit('state:changed', this.getState());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get current state as object
|
|
244
|
+
*/
|
|
245
|
+
public getState(): IFanState {
|
|
246
|
+
return {
|
|
247
|
+
isOn: this._isOn,
|
|
248
|
+
percentage: this._percentage,
|
|
249
|
+
presetMode: this._presetMode,
|
|
250
|
+
oscillating: this._oscillating,
|
|
251
|
+
direction: this._direction,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Refresh state from the device
|
|
257
|
+
*/
|
|
258
|
+
public async refreshState(): Promise<IFanState> {
|
|
259
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
260
|
+
this.updateStateInternal(state);
|
|
261
|
+
return state;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Update state from external source
|
|
266
|
+
*/
|
|
267
|
+
public updateState(state: IFanState): void {
|
|
268
|
+
this.updateStateInternal(state);
|
|
269
|
+
this.emit('state:changed', state);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Internal state update
|
|
274
|
+
*/
|
|
275
|
+
private updateStateInternal(state: IFanState): void {
|
|
276
|
+
this._isOn = state.isOn;
|
|
277
|
+
this._percentage = state.percentage;
|
|
278
|
+
this._presetMode = state.presetMode;
|
|
279
|
+
this._oscillating = state.oscillating;
|
|
280
|
+
this._direction = state.direction;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Feature Info
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
public getFeatureInfo(): IFanFeatureInfo {
|
|
288
|
+
return {
|
|
289
|
+
...this.getBaseFeatureInfo(),
|
|
290
|
+
type: 'fan',
|
|
291
|
+
protocol: this.protocol,
|
|
292
|
+
capabilities: this.capabilities,
|
|
293
|
+
currentState: this.getState(),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light Feature
|
|
3
|
+
* Provides control for smart lights (brightness, color, effects)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Feature, type TDeviceReference } from './feature.abstract.js';
|
|
7
|
+
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
|
|
8
|
+
import type {
|
|
9
|
+
TLightProtocol,
|
|
10
|
+
ILightCapabilities,
|
|
11
|
+
ILightState,
|
|
12
|
+
ILightFeatureInfo,
|
|
13
|
+
ILightProtocolClient,
|
|
14
|
+
} from '../interfaces/smarthome.interfaces.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for creating a LightFeature
|
|
18
|
+
*/
|
|
19
|
+
export interface ILightFeatureOptions extends IFeatureOptions {
|
|
20
|
+
/** Protocol type */
|
|
21
|
+
protocol: TLightProtocol;
|
|
22
|
+
/** Entity ID (for Home Assistant) */
|
|
23
|
+
entityId: string;
|
|
24
|
+
/** Protocol client for controlling the light */
|
|
25
|
+
protocolClient: ILightProtocolClient;
|
|
26
|
+
/** Light capabilities */
|
|
27
|
+
capabilities?: Partial<ILightCapabilities>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Light Feature - brightness, color, and effect control
|
|
32
|
+
*
|
|
33
|
+
* Protocol-agnostic: works with Home Assistant, Hue, MQTT, Zigbee, etc.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const light = device.getFeature<LightFeature>('light');
|
|
38
|
+
* if (light) {
|
|
39
|
+
* await light.turnOn({ brightness: 200 });
|
|
40
|
+
* await light.setColorTemp(4000); // 4000K warm white
|
|
41
|
+
* await light.setRgbColor(255, 100, 50);
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class LightFeature extends Feature {
|
|
46
|
+
public readonly type = 'light' as const;
|
|
47
|
+
public readonly protocol: TLightProtocol;
|
|
48
|
+
|
|
49
|
+
/** Entity ID (e.g., "light.living_room") */
|
|
50
|
+
public readonly entityId: string;
|
|
51
|
+
|
|
52
|
+
/** Capabilities */
|
|
53
|
+
public readonly capabilities: ILightCapabilities;
|
|
54
|
+
|
|
55
|
+
/** Current state */
|
|
56
|
+
protected _isOn: boolean = false;
|
|
57
|
+
protected _brightness?: number;
|
|
58
|
+
protected _colorTemp?: number;
|
|
59
|
+
protected _colorTempMireds?: number;
|
|
60
|
+
protected _rgbColor?: [number, number, number];
|
|
61
|
+
protected _hsColor?: [number, number];
|
|
62
|
+
protected _xyColor?: [number, number];
|
|
63
|
+
protected _effect?: string;
|
|
64
|
+
|
|
65
|
+
/** Protocol client for controlling the light */
|
|
66
|
+
private protocolClient: ILightProtocolClient;
|
|
67
|
+
|
|
68
|
+
constructor(
|
|
69
|
+
device: TDeviceReference,
|
|
70
|
+
port: number,
|
|
71
|
+
options: ILightFeatureOptions
|
|
72
|
+
) {
|
|
73
|
+
super(device, port, options);
|
|
74
|
+
this.protocol = options.protocol;
|
|
75
|
+
this.entityId = options.entityId;
|
|
76
|
+
this.protocolClient = options.protocolClient;
|
|
77
|
+
|
|
78
|
+
// Set capabilities with defaults
|
|
79
|
+
this.capabilities = {
|
|
80
|
+
supportsBrightness: options.capabilities?.supportsBrightness ?? false,
|
|
81
|
+
supportsColorTemp: options.capabilities?.supportsColorTemp ?? false,
|
|
82
|
+
supportsRgb: options.capabilities?.supportsRgb ?? false,
|
|
83
|
+
supportsHs: options.capabilities?.supportsHs ?? false,
|
|
84
|
+
supportsXy: options.capabilities?.supportsXy ?? false,
|
|
85
|
+
supportsEffects: options.capabilities?.supportsEffects ?? false,
|
|
86
|
+
supportsTransition: options.capabilities?.supportsTransition ?? true,
|
|
87
|
+
effects: options.capabilities?.effects,
|
|
88
|
+
minMireds: options.capabilities?.minMireds,
|
|
89
|
+
maxMireds: options.capabilities?.maxMireds,
|
|
90
|
+
minColorTempKelvin: options.capabilities?.minColorTempKelvin,
|
|
91
|
+
maxColorTempKelvin: options.capabilities?.maxColorTempKelvin,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Properties
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get current on/off state (cached)
|
|
101
|
+
*/
|
|
102
|
+
public get isOn(): boolean {
|
|
103
|
+
return this._isOn;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get current brightness 0-255 (cached)
|
|
108
|
+
*/
|
|
109
|
+
public get brightness(): number | undefined {
|
|
110
|
+
return this._brightness;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get current color temperature in Kelvin (cached)
|
|
115
|
+
*/
|
|
116
|
+
public get colorTemp(): number | undefined {
|
|
117
|
+
return this._colorTemp;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get current color temperature in Mireds (cached)
|
|
122
|
+
*/
|
|
123
|
+
public get colorTempMireds(): number | undefined {
|
|
124
|
+
return this._colorTempMireds;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get current RGB color (cached)
|
|
129
|
+
*/
|
|
130
|
+
public get rgbColor(): [number, number, number] | undefined {
|
|
131
|
+
return this._rgbColor;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get current HS color [hue 0-360, saturation 0-100] (cached)
|
|
136
|
+
*/
|
|
137
|
+
public get hsColor(): [number, number] | undefined {
|
|
138
|
+
return this._hsColor;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get current XY color (cached)
|
|
143
|
+
*/
|
|
144
|
+
public get xyColor(): [number, number] | undefined {
|
|
145
|
+
return this._xyColor;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get current effect (cached)
|
|
150
|
+
*/
|
|
151
|
+
public get effect(): string | undefined {
|
|
152
|
+
return this._effect;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get available effects
|
|
157
|
+
*/
|
|
158
|
+
public get effects(): string[] | undefined {
|
|
159
|
+
return this.capabilities.effects;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Connection
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
protected async doConnect(): Promise<void> {
|
|
167
|
+
// Fetch initial state
|
|
168
|
+
try {
|
|
169
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
170
|
+
this.updateStateInternal(state);
|
|
171
|
+
} catch {
|
|
172
|
+
// Ignore errors fetching initial state
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
protected async doDisconnect(): Promise<void> {
|
|
177
|
+
// Nothing to disconnect
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Light Control
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Turn on the light
|
|
186
|
+
* @param options Optional settings to apply when turning on
|
|
187
|
+
*/
|
|
188
|
+
public async turnOn(options?: {
|
|
189
|
+
brightness?: number;
|
|
190
|
+
colorTemp?: number;
|
|
191
|
+
rgb?: [number, number, number];
|
|
192
|
+
transition?: number;
|
|
193
|
+
}): Promise<void> {
|
|
194
|
+
await this.protocolClient.turnOn(this.entityId, options);
|
|
195
|
+
this._isOn = true;
|
|
196
|
+
if (options?.brightness !== undefined) {
|
|
197
|
+
this._brightness = options.brightness;
|
|
198
|
+
}
|
|
199
|
+
if (options?.colorTemp !== undefined) {
|
|
200
|
+
this._colorTemp = options.colorTemp;
|
|
201
|
+
}
|
|
202
|
+
if (options?.rgb !== undefined) {
|
|
203
|
+
this._rgbColor = options.rgb;
|
|
204
|
+
}
|
|
205
|
+
this.emit('state:changed', this.getState());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Turn off the light
|
|
210
|
+
* @param options Optional transition time
|
|
211
|
+
*/
|
|
212
|
+
public async turnOff(options?: { transition?: number }): Promise<void> {
|
|
213
|
+
await this.protocolClient.turnOff(this.entityId, options);
|
|
214
|
+
this._isOn = false;
|
|
215
|
+
this.emit('state:changed', this.getState());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Toggle the light
|
|
220
|
+
*/
|
|
221
|
+
public async toggle(): Promise<void> {
|
|
222
|
+
await this.protocolClient.toggle(this.entityId);
|
|
223
|
+
this._isOn = !this._isOn;
|
|
224
|
+
this.emit('state:changed', this.getState());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Set brightness level
|
|
229
|
+
* @param brightness Brightness 0-255
|
|
230
|
+
* @param transition Optional transition time in seconds
|
|
231
|
+
*/
|
|
232
|
+
public async setBrightness(brightness: number, transition?: number): Promise<void> {
|
|
233
|
+
if (!this.capabilities.supportsBrightness) {
|
|
234
|
+
throw new Error('Light does not support brightness control');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const clamped = Math.max(0, Math.min(255, Math.round(brightness)));
|
|
238
|
+
await this.protocolClient.setBrightness(this.entityId, clamped, transition);
|
|
239
|
+
this._brightness = clamped;
|
|
240
|
+
if (clamped > 0) {
|
|
241
|
+
this._isOn = true;
|
|
242
|
+
}
|
|
243
|
+
this.emit('state:changed', this.getState());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Set color temperature in Kelvin
|
|
248
|
+
* @param kelvin Color temperature in Kelvin (e.g., 2700 warm, 6500 cool)
|
|
249
|
+
* @param transition Optional transition time in seconds
|
|
250
|
+
*/
|
|
251
|
+
public async setColorTemp(kelvin: number, transition?: number): Promise<void> {
|
|
252
|
+
if (!this.capabilities.supportsColorTemp) {
|
|
253
|
+
throw new Error('Light does not support color temperature');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Clamp to supported range if available
|
|
257
|
+
let clamped = kelvin;
|
|
258
|
+
if (this.capabilities.minColorTempKelvin && this.capabilities.maxColorTempKelvin) {
|
|
259
|
+
clamped = Math.max(
|
|
260
|
+
this.capabilities.minColorTempKelvin,
|
|
261
|
+
Math.min(this.capabilities.maxColorTempKelvin, kelvin)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await this.protocolClient.setColorTemp(this.entityId, clamped, transition);
|
|
266
|
+
this._colorTemp = clamped;
|
|
267
|
+
this._colorTempMireds = Math.round(1000000 / clamped);
|
|
268
|
+
this._isOn = true;
|
|
269
|
+
this.emit('state:changed', this.getState());
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Set RGB color
|
|
274
|
+
* @param r Red 0-255
|
|
275
|
+
* @param g Green 0-255
|
|
276
|
+
* @param b Blue 0-255
|
|
277
|
+
* @param transition Optional transition time in seconds
|
|
278
|
+
*/
|
|
279
|
+
public async setRgbColor(r: number, g: number, b: number, transition?: number): Promise<void> {
|
|
280
|
+
if (!this.capabilities.supportsRgb) {
|
|
281
|
+
throw new Error('Light does not support RGB color');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const clampedR = Math.max(0, Math.min(255, Math.round(r)));
|
|
285
|
+
const clampedG = Math.max(0, Math.min(255, Math.round(g)));
|
|
286
|
+
const clampedB = Math.max(0, Math.min(255, Math.round(b)));
|
|
287
|
+
|
|
288
|
+
await this.protocolClient.setRgbColor(this.entityId, clampedR, clampedG, clampedB, transition);
|
|
289
|
+
this._rgbColor = [clampedR, clampedG, clampedB];
|
|
290
|
+
this._isOn = true;
|
|
291
|
+
this.emit('state:changed', this.getState());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Set light effect
|
|
296
|
+
* @param effect Effect name from available effects
|
|
297
|
+
*/
|
|
298
|
+
public async setEffect(effect: string): Promise<void> {
|
|
299
|
+
if (!this.capabilities.supportsEffects) {
|
|
300
|
+
throw new Error('Light does not support effects');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await this.protocolClient.setEffect(this.entityId, effect);
|
|
304
|
+
this._effect = effect;
|
|
305
|
+
this._isOn = true;
|
|
306
|
+
this.emit('state:changed', this.getState());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get current state as object
|
|
311
|
+
*/
|
|
312
|
+
public getState(): ILightState {
|
|
313
|
+
return {
|
|
314
|
+
isOn: this._isOn,
|
|
315
|
+
brightness: this._brightness,
|
|
316
|
+
colorTemp: this._colorTemp,
|
|
317
|
+
colorTempMireds: this._colorTempMireds,
|
|
318
|
+
rgbColor: this._rgbColor,
|
|
319
|
+
hsColor: this._hsColor,
|
|
320
|
+
xyColor: this._xyColor,
|
|
321
|
+
effect: this._effect,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Refresh state from the device
|
|
327
|
+
*/
|
|
328
|
+
public async refreshState(): Promise<ILightState> {
|
|
329
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
330
|
+
this.updateStateInternal(state);
|
|
331
|
+
return state;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Update state from external source (e.g., state change event)
|
|
336
|
+
*/
|
|
337
|
+
public updateState(state: ILightState): void {
|
|
338
|
+
this.updateStateInternal(state);
|
|
339
|
+
this.emit('state:changed', state);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Internal state update
|
|
344
|
+
*/
|
|
345
|
+
private updateStateInternal(state: ILightState): void {
|
|
346
|
+
this._isOn = state.isOn;
|
|
347
|
+
this._brightness = state.brightness;
|
|
348
|
+
this._colorTemp = state.colorTemp;
|
|
349
|
+
this._colorTempMireds = state.colorTempMireds;
|
|
350
|
+
this._rgbColor = state.rgbColor;
|
|
351
|
+
this._hsColor = state.hsColor;
|
|
352
|
+
this._xyColor = state.xyColor;
|
|
353
|
+
this._effect = state.effect;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// Feature Info
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
public getFeatureInfo(): ILightFeatureInfo {
|
|
361
|
+
return {
|
|
362
|
+
...this.getBaseFeatureInfo(),
|
|
363
|
+
type: 'light',
|
|
364
|
+
protocol: this.protocol,
|
|
365
|
+
capabilities: this.capabilities,
|
|
366
|
+
currentState: this.getState(),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|