@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,407 @@
1
+ /**
2
+ * Climate Feature
3
+ * Provides control for thermostats and HVAC systems
4
+ */
5
+
6
+ import { Feature, type TDeviceReference } from './feature.abstract.js';
7
+ import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
8
+ import type {
9
+ TClimateProtocol,
10
+ THvacMode,
11
+ THvacAction,
12
+ IClimateCapabilities,
13
+ IClimateState,
14
+ IClimateFeatureInfo,
15
+ IClimateProtocolClient,
16
+ } from '../interfaces/smarthome.interfaces.js';
17
+
18
+ /**
19
+ * Options for creating a ClimateFeature
20
+ */
21
+ export interface IClimateFeatureOptions extends IFeatureOptions {
22
+ /** Protocol type */
23
+ protocol: TClimateProtocol;
24
+ /** Entity ID (for Home Assistant) */
25
+ entityId: string;
26
+ /** Protocol client for controlling the climate device */
27
+ protocolClient: IClimateProtocolClient;
28
+ /** Climate capabilities */
29
+ capabilities?: Partial<IClimateCapabilities>;
30
+ }
31
+
32
+ /**
33
+ * Climate Feature - thermostat and HVAC control
34
+ *
35
+ * Protocol-agnostic: works with Home Assistant, Nest, Ecobee, MQTT, etc.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const climate = device.getFeature<ClimateFeature>('climate');
40
+ * if (climate) {
41
+ * await climate.setHvacMode('heat');
42
+ * await climate.setTargetTemp(21);
43
+ * console.log(`Current: ${climate.currentTemp}°C, Target: ${climate.targetTemp}°C`);
44
+ * }
45
+ * ```
46
+ */
47
+ export class ClimateFeature extends Feature {
48
+ public readonly type = 'climate' as const;
49
+ public readonly protocol: TClimateProtocol;
50
+
51
+ /** Entity ID (e.g., "climate.living_room") */
52
+ public readonly entityId: string;
53
+
54
+ /** Capabilities */
55
+ public readonly capabilities: IClimateCapabilities;
56
+
57
+ /** Current state */
58
+ protected _currentTemp?: number;
59
+ protected _targetTemp?: number;
60
+ protected _targetTempHigh?: number;
61
+ protected _targetTempLow?: number;
62
+ protected _hvacMode: THvacMode = 'off';
63
+ protected _hvacAction?: THvacAction;
64
+ protected _presetMode?: string;
65
+ protected _fanMode?: string;
66
+ protected _swingMode?: string;
67
+ protected _humidity?: number;
68
+ protected _targetHumidity?: number;
69
+ protected _auxHeat?: boolean;
70
+
71
+ /** Protocol client for controlling the climate device */
72
+ private protocolClient: IClimateProtocolClient;
73
+
74
+ constructor(
75
+ device: TDeviceReference,
76
+ port: number,
77
+ options: IClimateFeatureOptions
78
+ ) {
79
+ super(device, port, options);
80
+ this.protocol = options.protocol;
81
+ this.entityId = options.entityId;
82
+ this.protocolClient = options.protocolClient;
83
+
84
+ this.capabilities = {
85
+ hvacModes: options.capabilities?.hvacModes ?? ['off', 'heat', 'cool', 'auto'],
86
+ presetModes: options.capabilities?.presetModes,
87
+ fanModes: options.capabilities?.fanModes,
88
+ swingModes: options.capabilities?.swingModes,
89
+ supportsTargetTemp: options.capabilities?.supportsTargetTemp ?? true,
90
+ supportsTargetTempRange: options.capabilities?.supportsTargetTempRange ?? false,
91
+ supportsHumidity: options.capabilities?.supportsHumidity ?? false,
92
+ supportsAuxHeat: options.capabilities?.supportsAuxHeat ?? false,
93
+ minTemp: options.capabilities?.minTemp ?? 7,
94
+ maxTemp: options.capabilities?.maxTemp ?? 35,
95
+ tempStep: options.capabilities?.tempStep ?? 0.5,
96
+ minHumidity: options.capabilities?.minHumidity,
97
+ maxHumidity: options.capabilities?.maxHumidity,
98
+ };
99
+ }
100
+
101
+ // ============================================================================
102
+ // Properties
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Get current temperature (cached)
107
+ */
108
+ public get currentTemp(): number | undefined {
109
+ return this._currentTemp;
110
+ }
111
+
112
+ /**
113
+ * Get target temperature (cached)
114
+ */
115
+ public get targetTemp(): number | undefined {
116
+ return this._targetTemp;
117
+ }
118
+
119
+ /**
120
+ * Get target temperature high (for heat_cool mode, cached)
121
+ */
122
+ public get targetTempHigh(): number | undefined {
123
+ return this._targetTempHigh;
124
+ }
125
+
126
+ /**
127
+ * Get target temperature low (for heat_cool mode, cached)
128
+ */
129
+ public get targetTempLow(): number | undefined {
130
+ return this._targetTempLow;
131
+ }
132
+
133
+ /**
134
+ * Get current HVAC mode (cached)
135
+ */
136
+ public get hvacMode(): THvacMode {
137
+ return this._hvacMode;
138
+ }
139
+
140
+ /**
141
+ * Get current HVAC action (cached)
142
+ */
143
+ public get hvacAction(): THvacAction | undefined {
144
+ return this._hvacAction;
145
+ }
146
+
147
+ /**
148
+ * Get current preset mode (cached)
149
+ */
150
+ public get presetMode(): string | undefined {
151
+ return this._presetMode;
152
+ }
153
+
154
+ /**
155
+ * Get current fan mode (cached)
156
+ */
157
+ public get fanMode(): string | undefined {
158
+ return this._fanMode;
159
+ }
160
+
161
+ /**
162
+ * Get current swing mode (cached)
163
+ */
164
+ public get swingMode(): string | undefined {
165
+ return this._swingMode;
166
+ }
167
+
168
+ /**
169
+ * Get current humidity (cached)
170
+ */
171
+ public get humidity(): number | undefined {
172
+ return this._humidity;
173
+ }
174
+
175
+ /**
176
+ * Get target humidity (cached)
177
+ */
178
+ public get targetHumidity(): number | undefined {
179
+ return this._targetHumidity;
180
+ }
181
+
182
+ /**
183
+ * Get aux heat state (cached)
184
+ */
185
+ public get auxHeat(): boolean | undefined {
186
+ return this._auxHeat;
187
+ }
188
+
189
+ /**
190
+ * Get available HVAC modes
191
+ */
192
+ public get hvacModes(): THvacMode[] {
193
+ return this.capabilities.hvacModes;
194
+ }
195
+
196
+ /**
197
+ * Get available preset modes
198
+ */
199
+ public get presetModes(): string[] | undefined {
200
+ return this.capabilities.presetModes;
201
+ }
202
+
203
+ /**
204
+ * Get available fan modes
205
+ */
206
+ public get fanModes(): string[] | undefined {
207
+ return this.capabilities.fanModes;
208
+ }
209
+
210
+ // ============================================================================
211
+ // Connection
212
+ // ============================================================================
213
+
214
+ protected async doConnect(): Promise<void> {
215
+ try {
216
+ const state = await this.protocolClient.getState(this.entityId);
217
+ this.updateStateInternal(state);
218
+ } catch {
219
+ // Ignore errors fetching initial state
220
+ }
221
+ }
222
+
223
+ protected async doDisconnect(): Promise<void> {
224
+ // Nothing to disconnect
225
+ }
226
+
227
+ // ============================================================================
228
+ // Climate Control
229
+ // ============================================================================
230
+
231
+ /**
232
+ * Set HVAC mode
233
+ * @param mode HVAC mode (off, heat, cool, etc.)
234
+ */
235
+ public async setHvacMode(mode: THvacMode): Promise<void> {
236
+ if (!this.capabilities.hvacModes.includes(mode)) {
237
+ throw new Error(`HVAC mode ${mode} not supported`);
238
+ }
239
+
240
+ await this.protocolClient.setHvacMode(this.entityId, mode);
241
+ this._hvacMode = mode;
242
+ this.emit('state:changed', this.getState());
243
+ }
244
+
245
+ /**
246
+ * Set target temperature
247
+ * @param temp Target temperature
248
+ */
249
+ public async setTargetTemp(temp: number): Promise<void> {
250
+ if (!this.capabilities.supportsTargetTemp) {
251
+ throw new Error('Climate device does not support target temperature');
252
+ }
253
+
254
+ const clamped = Math.max(
255
+ this.capabilities.minTemp,
256
+ Math.min(this.capabilities.maxTemp, temp)
257
+ );
258
+
259
+ await this.protocolClient.setTargetTemp(this.entityId, clamped);
260
+ this._targetTemp = clamped;
261
+ this.emit('state:changed', this.getState());
262
+ }
263
+
264
+ /**
265
+ * Set target temperature range (for heat_cool mode)
266
+ * @param low Low temperature
267
+ * @param high High temperature
268
+ */
269
+ public async setTargetTempRange(low: number, high: number): Promise<void> {
270
+ if (!this.capabilities.supportsTargetTempRange) {
271
+ throw new Error('Climate device does not support temperature range');
272
+ }
273
+
274
+ const clampedLow = Math.max(this.capabilities.minTemp, Math.min(this.capabilities.maxTemp, low));
275
+ const clampedHigh = Math.max(this.capabilities.minTemp, Math.min(this.capabilities.maxTemp, high));
276
+
277
+ await this.protocolClient.setTargetTempRange(this.entityId, clampedLow, clampedHigh);
278
+ this._targetTempLow = clampedLow;
279
+ this._targetTempHigh = clampedHigh;
280
+ this.emit('state:changed', this.getState());
281
+ }
282
+
283
+ /**
284
+ * Set preset mode
285
+ * @param preset Preset mode name
286
+ */
287
+ public async setPresetMode(preset: string): Promise<void> {
288
+ if (!this.capabilities.presetModes?.includes(preset)) {
289
+ throw new Error(`Preset mode ${preset} not supported`);
290
+ }
291
+
292
+ await this.protocolClient.setPresetMode(this.entityId, preset);
293
+ this._presetMode = preset;
294
+ this.emit('state:changed', this.getState());
295
+ }
296
+
297
+ /**
298
+ * Set fan mode
299
+ * @param mode Fan mode name
300
+ */
301
+ public async setFanMode(mode: string): Promise<void> {
302
+ if (!this.capabilities.fanModes?.includes(mode)) {
303
+ throw new Error(`Fan mode ${mode} not supported`);
304
+ }
305
+
306
+ await this.protocolClient.setFanMode(this.entityId, mode);
307
+ this._fanMode = mode;
308
+ this.emit('state:changed', this.getState());
309
+ }
310
+
311
+ /**
312
+ * Set swing mode
313
+ * @param mode Swing mode name
314
+ */
315
+ public async setSwingMode(mode: string): Promise<void> {
316
+ if (!this.capabilities.swingModes?.includes(mode)) {
317
+ throw new Error(`Swing mode ${mode} not supported`);
318
+ }
319
+
320
+ await this.protocolClient.setSwingMode(this.entityId, mode);
321
+ this._swingMode = mode;
322
+ this.emit('state:changed', this.getState());
323
+ }
324
+
325
+ /**
326
+ * Set aux heat
327
+ * @param enabled Whether aux heat is enabled
328
+ */
329
+ public async setAuxHeat(enabled: boolean): Promise<void> {
330
+ if (!this.capabilities.supportsAuxHeat) {
331
+ throw new Error('Climate device does not support aux heat');
332
+ }
333
+
334
+ await this.protocolClient.setAuxHeat(this.entityId, enabled);
335
+ this._auxHeat = enabled;
336
+ this.emit('state:changed', this.getState());
337
+ }
338
+
339
+ /**
340
+ * Get current state as object
341
+ */
342
+ public getState(): IClimateState {
343
+ return {
344
+ currentTemp: this._currentTemp,
345
+ targetTemp: this._targetTemp,
346
+ targetTempHigh: this._targetTempHigh,
347
+ targetTempLow: this._targetTempLow,
348
+ hvacMode: this._hvacMode,
349
+ hvacAction: this._hvacAction,
350
+ presetMode: this._presetMode,
351
+ fanMode: this._fanMode,
352
+ swingMode: this._swingMode,
353
+ humidity: this._humidity,
354
+ targetHumidity: this._targetHumidity,
355
+ auxHeat: this._auxHeat,
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Refresh state from the device
361
+ */
362
+ public async refreshState(): Promise<IClimateState> {
363
+ const state = await this.protocolClient.getState(this.entityId);
364
+ this.updateStateInternal(state);
365
+ return state;
366
+ }
367
+
368
+ /**
369
+ * Update state from external source
370
+ */
371
+ public updateState(state: IClimateState): void {
372
+ this.updateStateInternal(state);
373
+ this.emit('state:changed', state);
374
+ }
375
+
376
+ /**
377
+ * Internal state update
378
+ */
379
+ private updateStateInternal(state: IClimateState): void {
380
+ this._currentTemp = state.currentTemp;
381
+ this._targetTemp = state.targetTemp;
382
+ this._targetTempHigh = state.targetTempHigh;
383
+ this._targetTempLow = state.targetTempLow;
384
+ this._hvacMode = state.hvacMode;
385
+ this._hvacAction = state.hvacAction;
386
+ this._presetMode = state.presetMode;
387
+ this._fanMode = state.fanMode;
388
+ this._swingMode = state.swingMode;
389
+ this._humidity = state.humidity;
390
+ this._targetHumidity = state.targetHumidity;
391
+ this._auxHeat = state.auxHeat;
392
+ }
393
+
394
+ // ============================================================================
395
+ // Feature Info
396
+ // ============================================================================
397
+
398
+ public getFeatureInfo(): IClimateFeatureInfo {
399
+ return {
400
+ ...this.getBaseFeatureInfo(),
401
+ type: 'climate',
402
+ protocol: this.protocol,
403
+ capabilities: this.capabilities,
404
+ currentState: this.getState(),
405
+ };
406
+ }
407
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Cover Feature
3
+ * Provides control for covers, blinds, garage doors, etc.
4
+ */
5
+
6
+ import { Feature, type TDeviceReference } from './feature.abstract.js';
7
+ import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
8
+ import type {
9
+ TCoverProtocol,
10
+ TCoverDeviceClass,
11
+ TCoverState,
12
+ ICoverCapabilities,
13
+ ICoverStateInfo,
14
+ ICoverFeatureInfo,
15
+ ICoverProtocolClient,
16
+ } from '../interfaces/smarthome.interfaces.js';
17
+
18
+ /**
19
+ * Options for creating a CoverFeature
20
+ */
21
+ export interface ICoverFeatureOptions extends IFeatureOptions {
22
+ /** Protocol type */
23
+ protocol: TCoverProtocol;
24
+ /** Entity ID (for Home Assistant) */
25
+ entityId: string;
26
+ /** Protocol client for controlling the cover */
27
+ protocolClient: ICoverProtocolClient;
28
+ /** Device class */
29
+ deviceClass?: TCoverDeviceClass;
30
+ /** Cover capabilities */
31
+ capabilities?: Partial<ICoverCapabilities>;
32
+ }
33
+
34
+ /**
35
+ * Cover Feature - control for blinds, garage doors, etc.
36
+ *
37
+ * Protocol-agnostic: works with Home Assistant, MQTT, Somfy, etc.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const cover = device.getFeature<CoverFeature>('cover');
42
+ * if (cover) {
43
+ * await cover.open();
44
+ * await cover.setPosition(50); // 50% open
45
+ * }
46
+ * ```
47
+ */
48
+ export class CoverFeature extends Feature {
49
+ public readonly type = 'cover' as const;
50
+ public readonly protocol: TCoverProtocol;
51
+
52
+ /** Entity ID (e.g., "cover.garage_door") */
53
+ public readonly entityId: string;
54
+
55
+ /** Capabilities */
56
+ public readonly capabilities: ICoverCapabilities;
57
+
58
+ /** Current cover state (not connection state) */
59
+ protected _coverState: TCoverState = 'unknown';
60
+ protected _position?: number;
61
+ protected _tiltPosition?: number;
62
+
63
+ /** Protocol client for controlling the cover */
64
+ private protocolClient: ICoverProtocolClient;
65
+
66
+ constructor(
67
+ device: TDeviceReference,
68
+ port: number,
69
+ options: ICoverFeatureOptions
70
+ ) {
71
+ super(device, port, options);
72
+ this.protocol = options.protocol;
73
+ this.entityId = options.entityId;
74
+ this.protocolClient = options.protocolClient;
75
+
76
+ this.capabilities = {
77
+ deviceClass: options.deviceClass,
78
+ supportsOpen: options.capabilities?.supportsOpen ?? true,
79
+ supportsClose: options.capabilities?.supportsClose ?? true,
80
+ supportsStop: options.capabilities?.supportsStop ?? true,
81
+ supportsPosition: options.capabilities?.supportsPosition ?? false,
82
+ supportsTilt: options.capabilities?.supportsTilt ?? false,
83
+ };
84
+ }
85
+
86
+ // ============================================================================
87
+ // Properties
88
+ // ============================================================================
89
+
90
+ /**
91
+ * Get current state (cached)
92
+ */
93
+ public get coverState(): TCoverState {
94
+ return this._coverState;
95
+ }
96
+
97
+ /**
98
+ * Get current position 0-100 (cached)
99
+ * 0 = closed, 100 = fully open
100
+ */
101
+ public get position(): number | undefined {
102
+ return this._position;
103
+ }
104
+
105
+ /**
106
+ * Get current tilt position 0-100 (cached)
107
+ */
108
+ public get tiltPosition(): number | undefined {
109
+ return this._tiltPosition;
110
+ }
111
+
112
+ /**
113
+ * Check if cover is open
114
+ */
115
+ public get isOpen(): boolean {
116
+ return this._coverState === 'open';
117
+ }
118
+
119
+ /**
120
+ * Check if cover is closed
121
+ */
122
+ public get isClosed(): boolean {
123
+ return this._coverState === 'closed';
124
+ }
125
+
126
+ /**
127
+ * Check if cover is opening
128
+ */
129
+ public get isOpening(): boolean {
130
+ return this._coverState === 'opening';
131
+ }
132
+
133
+ /**
134
+ * Check if cover is closing
135
+ */
136
+ public get isClosing(): boolean {
137
+ return this._coverState === 'closing';
138
+ }
139
+
140
+ // ============================================================================
141
+ // Connection
142
+ // ============================================================================
143
+
144
+ protected async doConnect(): Promise<void> {
145
+ try {
146
+ const state = await this.protocolClient.getState(this.entityId);
147
+ this.updateStateInternal(state);
148
+ } catch {
149
+ // Ignore errors fetching initial state
150
+ }
151
+ }
152
+
153
+ protected async doDisconnect(): Promise<void> {
154
+ // Nothing to disconnect
155
+ }
156
+
157
+ // ============================================================================
158
+ // Cover Control
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Open the cover
163
+ */
164
+ public async open(): Promise<void> {
165
+ if (!this.capabilities.supportsOpen) {
166
+ throw new Error('Cover does not support open');
167
+ }
168
+ await this.protocolClient.open(this.entityId);
169
+ this._coverState = 'opening';
170
+ this.emit('state:changed', this.getState());
171
+ }
172
+
173
+ /**
174
+ * Close the cover
175
+ */
176
+ public async close(): Promise<void> {
177
+ if (!this.capabilities.supportsClose) {
178
+ throw new Error('Cover does not support close');
179
+ }
180
+ await this.protocolClient.close(this.entityId);
181
+ this._coverState = 'closing';
182
+ this.emit('state:changed', this.getState());
183
+ }
184
+
185
+ /**
186
+ * Stop the cover
187
+ */
188
+ public async stop(): Promise<void> {
189
+ if (!this.capabilities.supportsStop) {
190
+ throw new Error('Cover does not support stop');
191
+ }
192
+ await this.protocolClient.stop(this.entityId);
193
+ this._coverState = 'stopped';
194
+ this.emit('state:changed', this.getState());
195
+ }
196
+
197
+ /**
198
+ * Set cover position
199
+ * @param position Position 0-100 (0 = closed, 100 = open)
200
+ */
201
+ public async setPosition(position: number): Promise<void> {
202
+ if (!this.capabilities.supportsPosition) {
203
+ throw new Error('Cover does not support position control');
204
+ }
205
+
206
+ const clamped = Math.max(0, Math.min(100, Math.round(position)));
207
+ await this.protocolClient.setPosition(this.entityId, clamped);
208
+ this._position = clamped;
209
+ this._coverState = clamped === 0 ? 'closed' : clamped === 100 ? 'open' : 'stopped';
210
+ this.emit('state:changed', this.getState());
211
+ }
212
+
213
+ /**
214
+ * Set tilt position
215
+ * @param position Tilt position 0-100
216
+ */
217
+ public async setTiltPosition(position: number): Promise<void> {
218
+ if (!this.capabilities.supportsTilt) {
219
+ throw new Error('Cover does not support tilt control');
220
+ }
221
+
222
+ const clamped = Math.max(0, Math.min(100, Math.round(position)));
223
+ await this.protocolClient.setTiltPosition(this.entityId, clamped);
224
+ this._tiltPosition = clamped;
225
+ this.emit('state:changed', this.getState());
226
+ }
227
+
228
+ /**
229
+ * Get current state as object
230
+ */
231
+ public getState(): ICoverStateInfo {
232
+ return {
233
+ state: this._coverState,
234
+ position: this._position,
235
+ tiltPosition: this._tiltPosition,
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Refresh state from the device
241
+ */
242
+ public async refreshState(): Promise<ICoverStateInfo> {
243
+ const state = await this.protocolClient.getState(this.entityId);
244
+ this.updateStateInternal(state);
245
+ return state;
246
+ }
247
+
248
+ /**
249
+ * Update state from external source
250
+ */
251
+ public updateState(state: ICoverStateInfo): void {
252
+ this.updateStateInternal(state);
253
+ this.emit('state:changed', state);
254
+ }
255
+
256
+ /**
257
+ * Internal state update
258
+ */
259
+ private updateStateInternal(state: ICoverStateInfo): void {
260
+ this._coverState = state.state;
261
+ this._position = state.position;
262
+ this._tiltPosition = state.tiltPosition;
263
+ }
264
+
265
+ // ============================================================================
266
+ // Feature Info
267
+ // ============================================================================
268
+
269
+ public getFeatureInfo(): ICoverFeatureInfo {
270
+ return {
271
+ ...this.getBaseFeatureInfo(),
272
+ type: 'cover',
273
+ protocol: this.protocol,
274
+ capabilities: this.capabilities,
275
+ currentState: this.getState(),
276
+ };
277
+ }
278
+ }