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