@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
|
+
* Light Feature
|
|
3
|
+
* Provides control for smart lights (brightness, color, effects)
|
|
4
|
+
*/
|
|
5
|
+
import { Feature } from './feature.abstract.js';
|
|
6
|
+
/**
|
|
7
|
+
* Light Feature - brightness, color, and effect control
|
|
8
|
+
*
|
|
9
|
+
* Protocol-agnostic: works with Home Assistant, Hue, MQTT, Zigbee, etc.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const light = device.getFeature<LightFeature>('light');
|
|
14
|
+
* if (light) {
|
|
15
|
+
* await light.turnOn({ brightness: 200 });
|
|
16
|
+
* await light.setColorTemp(4000); // 4000K warm white
|
|
17
|
+
* await light.setRgbColor(255, 100, 50);
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class LightFeature extends Feature {
|
|
22
|
+
type = 'light';
|
|
23
|
+
protocol;
|
|
24
|
+
/** Entity ID (e.g., "light.living_room") */
|
|
25
|
+
entityId;
|
|
26
|
+
/** Capabilities */
|
|
27
|
+
capabilities;
|
|
28
|
+
/** Current state */
|
|
29
|
+
_isOn = false;
|
|
30
|
+
_brightness;
|
|
31
|
+
_colorTemp;
|
|
32
|
+
_colorTempMireds;
|
|
33
|
+
_rgbColor;
|
|
34
|
+
_hsColor;
|
|
35
|
+
_xyColor;
|
|
36
|
+
_effect;
|
|
37
|
+
/** Protocol client for controlling the light */
|
|
38
|
+
protocolClient;
|
|
39
|
+
constructor(device, port, options) {
|
|
40
|
+
super(device, port, options);
|
|
41
|
+
this.protocol = options.protocol;
|
|
42
|
+
this.entityId = options.entityId;
|
|
43
|
+
this.protocolClient = options.protocolClient;
|
|
44
|
+
// Set capabilities with defaults
|
|
45
|
+
this.capabilities = {
|
|
46
|
+
supportsBrightness: options.capabilities?.supportsBrightness ?? false,
|
|
47
|
+
supportsColorTemp: options.capabilities?.supportsColorTemp ?? false,
|
|
48
|
+
supportsRgb: options.capabilities?.supportsRgb ?? false,
|
|
49
|
+
supportsHs: options.capabilities?.supportsHs ?? false,
|
|
50
|
+
supportsXy: options.capabilities?.supportsXy ?? false,
|
|
51
|
+
supportsEffects: options.capabilities?.supportsEffects ?? false,
|
|
52
|
+
supportsTransition: options.capabilities?.supportsTransition ?? true,
|
|
53
|
+
effects: options.capabilities?.effects,
|
|
54
|
+
minMireds: options.capabilities?.minMireds,
|
|
55
|
+
maxMireds: options.capabilities?.maxMireds,
|
|
56
|
+
minColorTempKelvin: options.capabilities?.minColorTempKelvin,
|
|
57
|
+
maxColorTempKelvin: options.capabilities?.maxColorTempKelvin,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Properties
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Get current on/off state (cached)
|
|
65
|
+
*/
|
|
66
|
+
get isOn() {
|
|
67
|
+
return this._isOn;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get current brightness 0-255 (cached)
|
|
71
|
+
*/
|
|
72
|
+
get brightness() {
|
|
73
|
+
return this._brightness;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get current color temperature in Kelvin (cached)
|
|
77
|
+
*/
|
|
78
|
+
get colorTemp() {
|
|
79
|
+
return this._colorTemp;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get current color temperature in Mireds (cached)
|
|
83
|
+
*/
|
|
84
|
+
get colorTempMireds() {
|
|
85
|
+
return this._colorTempMireds;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get current RGB color (cached)
|
|
89
|
+
*/
|
|
90
|
+
get rgbColor() {
|
|
91
|
+
return this._rgbColor;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get current HS color [hue 0-360, saturation 0-100] (cached)
|
|
95
|
+
*/
|
|
96
|
+
get hsColor() {
|
|
97
|
+
return this._hsColor;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get current XY color (cached)
|
|
101
|
+
*/
|
|
102
|
+
get xyColor() {
|
|
103
|
+
return this._xyColor;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get current effect (cached)
|
|
107
|
+
*/
|
|
108
|
+
get effect() {
|
|
109
|
+
return this._effect;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get available effects
|
|
113
|
+
*/
|
|
114
|
+
get effects() {
|
|
115
|
+
return this.capabilities.effects;
|
|
116
|
+
}
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Connection
|
|
119
|
+
// ============================================================================
|
|
120
|
+
async doConnect() {
|
|
121
|
+
// Fetch initial state
|
|
122
|
+
try {
|
|
123
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
124
|
+
this.updateStateInternal(state);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore errors fetching initial state
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async doDisconnect() {
|
|
131
|
+
// Nothing to disconnect
|
|
132
|
+
}
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Light Control
|
|
135
|
+
// ============================================================================
|
|
136
|
+
/**
|
|
137
|
+
* Turn on the light
|
|
138
|
+
* @param options Optional settings to apply when turning on
|
|
139
|
+
*/
|
|
140
|
+
async turnOn(options) {
|
|
141
|
+
await this.protocolClient.turnOn(this.entityId, options);
|
|
142
|
+
this._isOn = true;
|
|
143
|
+
if (options?.brightness !== undefined) {
|
|
144
|
+
this._brightness = options.brightness;
|
|
145
|
+
}
|
|
146
|
+
if (options?.colorTemp !== undefined) {
|
|
147
|
+
this._colorTemp = options.colorTemp;
|
|
148
|
+
}
|
|
149
|
+
if (options?.rgb !== undefined) {
|
|
150
|
+
this._rgbColor = options.rgb;
|
|
151
|
+
}
|
|
152
|
+
this.emit('state:changed', this.getState());
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Turn off the light
|
|
156
|
+
* @param options Optional transition time
|
|
157
|
+
*/
|
|
158
|
+
async turnOff(options) {
|
|
159
|
+
await this.protocolClient.turnOff(this.entityId, options);
|
|
160
|
+
this._isOn = false;
|
|
161
|
+
this.emit('state:changed', this.getState());
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Toggle the light
|
|
165
|
+
*/
|
|
166
|
+
async toggle() {
|
|
167
|
+
await this.protocolClient.toggle(this.entityId);
|
|
168
|
+
this._isOn = !this._isOn;
|
|
169
|
+
this.emit('state:changed', this.getState());
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Set brightness level
|
|
173
|
+
* @param brightness Brightness 0-255
|
|
174
|
+
* @param transition Optional transition time in seconds
|
|
175
|
+
*/
|
|
176
|
+
async setBrightness(brightness, transition) {
|
|
177
|
+
if (!this.capabilities.supportsBrightness) {
|
|
178
|
+
throw new Error('Light does not support brightness control');
|
|
179
|
+
}
|
|
180
|
+
const clamped = Math.max(0, Math.min(255, Math.round(brightness)));
|
|
181
|
+
await this.protocolClient.setBrightness(this.entityId, clamped, transition);
|
|
182
|
+
this._brightness = clamped;
|
|
183
|
+
if (clamped > 0) {
|
|
184
|
+
this._isOn = true;
|
|
185
|
+
}
|
|
186
|
+
this.emit('state:changed', this.getState());
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Set color temperature in Kelvin
|
|
190
|
+
* @param kelvin Color temperature in Kelvin (e.g., 2700 warm, 6500 cool)
|
|
191
|
+
* @param transition Optional transition time in seconds
|
|
192
|
+
*/
|
|
193
|
+
async setColorTemp(kelvin, transition) {
|
|
194
|
+
if (!this.capabilities.supportsColorTemp) {
|
|
195
|
+
throw new Error('Light does not support color temperature');
|
|
196
|
+
}
|
|
197
|
+
// Clamp to supported range if available
|
|
198
|
+
let clamped = kelvin;
|
|
199
|
+
if (this.capabilities.minColorTempKelvin && this.capabilities.maxColorTempKelvin) {
|
|
200
|
+
clamped = Math.max(this.capabilities.minColorTempKelvin, Math.min(this.capabilities.maxColorTempKelvin, kelvin));
|
|
201
|
+
}
|
|
202
|
+
await this.protocolClient.setColorTemp(this.entityId, clamped, transition);
|
|
203
|
+
this._colorTemp = clamped;
|
|
204
|
+
this._colorTempMireds = Math.round(1000000 / clamped);
|
|
205
|
+
this._isOn = true;
|
|
206
|
+
this.emit('state:changed', this.getState());
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Set RGB color
|
|
210
|
+
* @param r Red 0-255
|
|
211
|
+
* @param g Green 0-255
|
|
212
|
+
* @param b Blue 0-255
|
|
213
|
+
* @param transition Optional transition time in seconds
|
|
214
|
+
*/
|
|
215
|
+
async setRgbColor(r, g, b, transition) {
|
|
216
|
+
if (!this.capabilities.supportsRgb) {
|
|
217
|
+
throw new Error('Light does not support RGB color');
|
|
218
|
+
}
|
|
219
|
+
const clampedR = Math.max(0, Math.min(255, Math.round(r)));
|
|
220
|
+
const clampedG = Math.max(0, Math.min(255, Math.round(g)));
|
|
221
|
+
const clampedB = Math.max(0, Math.min(255, Math.round(b)));
|
|
222
|
+
await this.protocolClient.setRgbColor(this.entityId, clampedR, clampedG, clampedB, transition);
|
|
223
|
+
this._rgbColor = [clampedR, clampedG, clampedB];
|
|
224
|
+
this._isOn = true;
|
|
225
|
+
this.emit('state:changed', this.getState());
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Set light effect
|
|
229
|
+
* @param effect Effect name from available effects
|
|
230
|
+
*/
|
|
231
|
+
async setEffect(effect) {
|
|
232
|
+
if (!this.capabilities.supportsEffects) {
|
|
233
|
+
throw new Error('Light does not support effects');
|
|
234
|
+
}
|
|
235
|
+
await this.protocolClient.setEffect(this.entityId, effect);
|
|
236
|
+
this._effect = effect;
|
|
237
|
+
this._isOn = true;
|
|
238
|
+
this.emit('state:changed', this.getState());
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get current state as object
|
|
242
|
+
*/
|
|
243
|
+
getState() {
|
|
244
|
+
return {
|
|
245
|
+
isOn: this._isOn,
|
|
246
|
+
brightness: this._brightness,
|
|
247
|
+
colorTemp: this._colorTemp,
|
|
248
|
+
colorTempMireds: this._colorTempMireds,
|
|
249
|
+
rgbColor: this._rgbColor,
|
|
250
|
+
hsColor: this._hsColor,
|
|
251
|
+
xyColor: this._xyColor,
|
|
252
|
+
effect: this._effect,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Refresh state from the device
|
|
257
|
+
*/
|
|
258
|
+
async refreshState() {
|
|
259
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
260
|
+
this.updateStateInternal(state);
|
|
261
|
+
return state;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Update state from external source (e.g., state change event)
|
|
265
|
+
*/
|
|
266
|
+
updateState(state) {
|
|
267
|
+
this.updateStateInternal(state);
|
|
268
|
+
this.emit('state:changed', state);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Internal state update
|
|
272
|
+
*/
|
|
273
|
+
updateStateInternal(state) {
|
|
274
|
+
this._isOn = state.isOn;
|
|
275
|
+
this._brightness = state.brightness;
|
|
276
|
+
this._colorTemp = state.colorTemp;
|
|
277
|
+
this._colorTempMireds = state.colorTempMireds;
|
|
278
|
+
this._rgbColor = state.rgbColor;
|
|
279
|
+
this._hsColor = state.hsColor;
|
|
280
|
+
this._xyColor = state.xyColor;
|
|
281
|
+
this._effect = state.effect;
|
|
282
|
+
}
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Feature Info
|
|
285
|
+
// ============================================================================
|
|
286
|
+
getFeatureInfo() {
|
|
287
|
+
return {
|
|
288
|
+
...this.getBaseFeatureInfo(),
|
|
289
|
+
type: 'light',
|
|
290
|
+
protocol: this.protocol,
|
|
291
|
+
capabilities: this.capabilities,
|
|
292
|
+
currentState: this.getState(),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmVhdHVyZS5saWdodC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2ZlYXR1cmVzL2ZlYXR1cmUubGlnaHQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxFQUFFLE9BQU8sRUFBeUIsTUFBTSx1QkFBdUIsQ0FBQztBQXdCdkU7Ozs7Ozs7Ozs7Ozs7O0dBY0c7QUFDSCxNQUFNLE9BQU8sWUFBYSxTQUFRLE9BQU87SUFDdkIsSUFBSSxHQUFHLE9BQWdCLENBQUM7SUFDeEIsUUFBUSxDQUFpQjtJQUV6Qyw0Q0FBNEM7SUFDNUIsUUFBUSxDQUFTO0lBRWpDLG1CQUFtQjtJQUNILFlBQVksQ0FBcUI7SUFFakQsb0JBQW9CO0lBQ1YsS0FBSyxHQUFZLEtBQUssQ0FBQztJQUN2QixXQUFXLENBQVU7SUFDckIsVUFBVSxDQUFVO0lBQ3BCLGdCQUFnQixDQUFVO0lBQzFCLFNBQVMsQ0FBNEI7SUFDckMsUUFBUSxDQUFvQjtJQUM1QixRQUFRLENBQW9CO0lBQzVCLE9BQU8sQ0FBVTtJQUUzQixnREFBZ0Q7SUFDeEMsY0FBYyxDQUF1QjtJQUU3QyxZQUNFLE1BQXdCLEVBQ3hCLElBQVksRUFDWixPQUE2QjtRQUU3QixLQUFLLENBQUMsTUFBTSxFQUFFLElBQUksRUFBRSxPQUFPLENBQUMsQ0FBQztRQUM3QixJQUFJLENBQUMsUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFRLENBQUM7UUFDakMsSUFBSSxDQUFDLFFBQVEsR0FBRyxPQUFPLENBQUMsUUFBUSxDQUFDO1FBQ2pDLElBQUksQ0FBQyxjQUFjLEdBQUcsT0FBTyxDQUFDLGNBQWMsQ0FBQztRQUU3QyxpQ0FBaUM7UUFDakMsSUFBSSxDQUFDLFlBQVksR0FBRztZQUNsQixrQkFBa0IsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLGtCQUFrQixJQUFJLEtBQUs7WUFDckUsaUJBQWlCLEVBQUUsT0FBTyxDQUFDLFlBQVksRUFBRSxpQkFBaUIsSUFBSSxLQUFLO1lBQ25FLFdBQVcsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLFdBQVcsSUFBSSxLQUFLO1lBQ3ZELFVBQVUsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLFVBQVUsSUFBSSxLQUFLO1lBQ3JELFVBQVUsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLFVBQVUsSUFBSSxLQUFLO1lBQ3JELGVBQWUsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLGVBQWUsSUFBSSxLQUFLO1lBQy9ELGtCQUFrQixFQUFFLE9BQU8sQ0FBQyxZQUFZLEVBQUUsa0JBQWtCLElBQUksSUFBSTtZQUNwRSxPQUFPLEVBQUUsT0FBTyxDQUFDLFlBQVksRUFBRSxPQUFPO1lBQ3RDLFNBQVMsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLFNBQVM7WUFDMUMsU0FBUyxFQUFFLE9BQU8sQ0FBQyxZQUFZLEVBQUUsU0FBUztZQUMxQyxrQkFBa0IsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLGtCQUFrQjtZQUM1RCxrQkFBa0IsRUFBRSxPQUFPLENBQUMsWUFBWSxFQUFFLGtCQUFrQjtTQUM3RCxDQUFDO0lBQ0osQ0FBQztJQUVELCtFQUErRTtJQUMvRSxhQUFhO0lBQ2IsK0VBQStFO0lBRS9FOztPQUVHO0lBQ0gsSUFBVyxJQUFJO1FBQ2IsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDO0lBQ3BCLENBQUM7SUFFRDs7T0FFRztJQUNILElBQVcsVUFBVTtRQUNuQixPQUFPLElBQUksQ0FBQyxXQUFXLENBQUM7SUFDMUIsQ0FBQztJQUVEOztPQUVHO0lBQ0gsSUFBVyxTQUFTO1FBQ2xCLE9BQU8sSUFBSSxDQUFDLFVBQVUsQ0FBQztJQUN6QixDQUFDO0lBRUQ7O09BRUc7SUFDSCxJQUFXLGVBQWU7UUFDeEIsT0FBTyxJQUFJLENBQUMsZ0JBQWdCLENBQUM7SUFDL0IsQ0FBQztJQUVEOztPQUVHO0lBQ0gsSUFBVyxRQUFRO1FBQ2pCLE9BQU8sSUFBSSxDQUFDLFNBQVMsQ0FBQztJQUN4QixDQUFDO0lBRUQ7O09BRUc7SUFDSCxJQUFXLE9BQU87UUFDaEIsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDO0lBQ3ZCLENBQUM7SUFFRDs7T0FFRztJQUNILElBQVcsT0FBTztRQUNoQixPQUFPLElBQUksQ0FBQyxRQUFRLENBQUM7SUFDdkIsQ0FBQztJQUVEOztPQUVHO0lBQ0gsSUFBVyxNQUFNO1FBQ2YsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDO0lBQ3RCLENBQUM7SUFFRDs7T0FFRztJQUNILElBQVcsT0FBTztRQUNoQixPQUFPLElBQUksQ0FBQyxZQUFZLENBQUMsT0FBTyxDQUFDO0lBQ25DLENBQUM7SUFFRCwrRUFBK0U7SUFDL0UsYUFBYTtJQUNiLCtFQUErRTtJQUVyRSxLQUFLLENBQUMsU0FBUztRQUN2QixzQkFBc0I7UUFDdEIsSUFBSSxDQUFDO1lBQ0gsTUFBTSxLQUFLLEdBQUcsTUFBTSxJQUFJLENBQUMsY0FBYyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDaEUsSUFBSSxDQUFDLG1CQUFtQixDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ2xDLENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCx1Q0FBdUM7UUFDekMsQ0FBQztJQUNILENBQUM7SUFFUyxLQUFLLENBQUMsWUFBWTtRQUMxQix3QkFBd0I7SUFDMUIsQ0FBQztJQUVELCtFQUErRTtJQUMvRSxnQkFBZ0I7SUFDaEIsK0VBQStFO0lBRS9FOzs7T0FHRztJQUNJLEtBQUssQ0FBQyxNQUFNLENBQUMsT0FLbkI7UUFDQyxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFDekQsSUFBSSxDQUFDLEtBQUssR0FBRyxJQUFJLENBQUM7UUFDbEIsSUFBSSxPQUFPLEVBQUUsVUFBVSxLQUFLLFNBQVMsRUFBRSxDQUFDO1lBQ3RDLElBQUksQ0FBQyxXQUFXLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQztRQUN4QyxDQUFDO1FBQ0QsSUFBSSxPQUFPLEVBQUUsU0FBUyxLQUFLLFNBQVMsRUFBRSxDQUFDO1lBQ3JDLElBQUksQ0FBQyxVQUFVLEdBQUcsT0FBTyxDQUFDLFNBQVMsQ0FBQztRQUN0QyxDQUFDO1FBQ0QsSUFBSSxPQUFPLEVBQUUsR0FBRyxLQUFLLFNBQVMsRUFBRSxDQUFDO1lBQy9CLElBQUksQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQztRQUMvQixDQUFDO1FBQ0QsSUFBSSxDQUFDLElBQUksQ0FBQyxlQUFlLEVBQUUsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDOUMsQ0FBQztJQUVEOzs7T0FHRztJQUNJLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBaUM7UUFDcEQsTUFBTSxJQUFJLENBQUMsY0FBYyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBQzFELElBQUksQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDO1FBQ25CLElBQUksQ0FBQyxJQUFJLENBQUMsZUFBZSxFQUFFLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQzlDLENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxNQUFNO1FBQ2pCLE1BQU0sSUFBSSxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ2hELElBQUksQ0FBQyxLQUFLLEdBQUcsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDO1FBQ3pCLElBQUksQ0FBQyxJQUFJLENBQUMsZUFBZSxFQUFFLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQzlDLENBQUM7SUFFRDs7OztPQUlHO0lBQ0ksS0FBSyxDQUFDLGFBQWEsQ0FBQyxVQUFrQixFQUFFLFVBQW1CO1FBQ2hFLElBQUksQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLGtCQUFrQixFQUFFLENBQUM7WUFDMUMsTUFBTSxJQUFJLEtBQUssQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDO1FBQy9ELENBQUM7UUFFRCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNuRSxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsT0FBTyxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQzVFLElBQUksQ0FBQyxXQUFXLEdBQUcsT0FBTyxDQUFDO1FBQzNCLElBQUksT0FBTyxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQ2hCLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxDQUFDO1FBQ3BCLENBQUM7UUFDRCxJQUFJLENBQUMsSUFBSSxDQUFDLGVBQWUsRUFBRSxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUM5QyxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLEtBQUssQ0FBQyxZQUFZLENBQUMsTUFBYyxFQUFFLFVBQW1CO1FBQzNELElBQUksQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLGlCQUFpQixFQUFFLENBQUM7WUFDekMsTUFBTSxJQUFJLEtBQUssQ0FBQywwQ0FBMEMsQ0FBQyxDQUFDO1FBQzlELENBQUM7UUFFRCx3Q0FBd0M7UUFDeEMsSUFBSSxPQUFPLEdBQUcsTUFBTSxDQUFDO1FBQ3JCLElBQUksSUFBSSxDQUFDLFlBQVksQ0FBQyxrQkFBa0IsSUFBSSxJQUFJLENBQUMsWUFBWSxDQUFDLGtCQUFrQixFQUFFLENBQUM7WUFDakYsT0FBTyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQ2hCLElBQUksQ0FBQyxZQUFZLENBQUMsa0JBQWtCLEVBQ3BDLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxrQkFBa0IsRUFBRSxNQUFNLENBQUMsQ0FDdkQsQ0FBQztRQUNKLENBQUM7UUFFRCxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsT0FBTyxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQzNFLElBQUksQ0FBQyxVQUFVLEdBQUcsT0FBTyxDQUFDO1FBQzFCLElBQUksQ0FBQyxnQkFBZ0IsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUMsQ0FBQztRQUN0RCxJQUFJLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQztRQUNsQixJQUFJLENBQUMsSUFBSSxDQUFDLGVBQWUsRUFBRSxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUM5QyxDQUFDO0lBRUQ7Ozs7OztPQU1HO0lBQ0ksS0FBSyxDQUFDLFdBQVcsQ0FBQyxDQUFTLEVBQUUsQ0FBUyxFQUFFLENBQVMsRUFBRSxVQUFtQjtRQUMzRSxJQUFJLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxXQUFXLEVBQUUsQ0FBQztZQUNuQyxNQUFNLElBQUksS0FBSyxDQUFDLGtDQUFrQyxDQUFDLENBQUM7UUFDdEQsQ0FBQztRQUVELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQzNELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQzNELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBRTNELE1BQU0sSUFBSSxDQUFDLGNBQWMsQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxVQUFVLENBQUMsQ0FBQztRQUMvRixJQUFJLENBQUMsU0FBUyxHQUFHLENBQUMsUUFBUSxFQUFFLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztRQUNoRCxJQUFJLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQztRQUNsQixJQUFJLENBQUMsSUFBSSxDQUFDLGVBQWUsRUFBRSxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUM5QyxDQUFDO0lBRUQ7OztPQUdHO0lBQ0ksS0FBSyxDQUFDLFNBQVMsQ0FBQyxNQUFjO1FBQ25DLElBQUksQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLGVBQWUsRUFBRSxDQUFDO1lBQ3ZDLE1BQU0sSUFBSSxLQUFLLENBQUMsZ0NBQWdDLENBQUMsQ0FBQztRQUNwRCxDQUFDO1FBRUQsTUFBTSxJQUFJLENBQUMsY0FBYyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1FBQzNELElBQUksQ0FBQyxPQUFPLEdBQUcsTUFBTSxDQUFDO1FBQ3RCLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxDQUFDO1FBQ2xCLElBQUksQ0FBQyxJQUFJLENBQUMsZUFBZSxFQUFFLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQzlDLENBQUM7SUFFRDs7T0FFRztJQUNJLFFBQVE7UUFDYixPQUFPO1lBQ0wsSUFBSSxFQUFFLElBQUksQ0FBQyxLQUFLO1lBQ2hCLFVBQVUsRUFBRSxJQUFJLENBQUMsV0FBVztZQUM1QixTQUFTLEVBQUUsSUFBSSxDQUFDLFVBQVU7WUFDMUIsZUFBZSxFQUFFLElBQUksQ0FBQyxnQkFBZ0I7WUFDdEMsUUFBUSxFQUFFLElBQUksQ0FBQyxTQUFTO1lBQ3hCLE9BQU8sRUFBRSxJQUFJLENBQUMsUUFBUTtZQUN0QixPQUFPLEVBQUUsSUFBSSxDQUFDLFFBQVE7WUFDdEIsTUFBTSxFQUFFLElBQUksQ0FBQyxPQUFPO1NBQ3JCLENBQUM7SUFDSixDQUFDO0lBRUQ7O09BRUc7SUFDSSxLQUFLLENBQUMsWUFBWTtRQUN2QixNQUFNLEtBQUssR0FBRyxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUNoRSxJQUFJLENBQUMsbUJBQW1CLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDaEMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBRUQ7O09BRUc7SUFDSSxXQUFXLENBQUMsS0FBa0I7UUFDbkMsSUFBSSxDQUFDLG1CQUFtQixDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ2hDLElBQUksQ0FBQyxJQUFJLENBQUMsZUFBZSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ3BDLENBQUM7SUFFRDs7T0FFRztJQUNLLG1CQUFtQixDQUFDLEtBQWtCO1FBQzVDLElBQUksQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDLElBQUksQ0FBQztRQUN4QixJQUFJLENBQUMsV0FBVyxHQUFHLEtBQUssQ0FBQyxVQUFVLENBQUM7UUFDcEMsSUFBSSxDQUFDLFVBQVUsR0FBRyxLQUFLLENBQUMsU0FBUyxDQUFDO1FBQ2xDLElBQUksQ0FBQyxnQkFBZ0IsR0FBRyxLQUFLLENBQUMsZUFBZSxDQUFDO1FBQzlDLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLFFBQVEsQ0FBQztRQUNoQyxJQUFJLENBQUMsUUFBUSxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUM7UUFDOUIsSUFBSSxDQUFDLFFBQVEsR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDO1FBQzlCLElBQUksQ0FBQyxPQUFPLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQztJQUM5QixDQUFDO0lBRUQsK0VBQStFO0lBQy9FLGVBQWU7SUFDZiwrRUFBK0U7SUFFeEUsY0FBYztRQUNuQixPQUFPO1lBQ0wsR0FBRyxJQUFJLENBQUMsa0JBQWtCLEVBQUU7WUFDNUIsSUFBSSxFQUFFLE9BQU87WUFDYixRQUFRLEVBQUUsSUFBSSxDQUFDLFFBQVE7WUFDdkIsWUFBWSxFQUFFLElBQUksQ0FBQyxZQUFZO1lBQy9CLFlBQVksRUFBRSxJQUFJLENBQUMsUUFBUSxFQUFFO1NBQzlCLENBQUM7SUFDSixDQUFDO0NBQ0YifQ==
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock Feature
|
|
3
|
+
* Provides control for smart locks
|
|
4
|
+
*/
|
|
5
|
+
import { Feature, type TDeviceReference } from './feature.abstract.js';
|
|
6
|
+
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
|
|
7
|
+
import type { TLockProtocol, TLockState, ILockCapabilities, ILockStateInfo, ILockFeatureInfo, ILockProtocolClient } from '../interfaces/smarthome.interfaces.js';
|
|
8
|
+
/**
|
|
9
|
+
* Options for creating a LockFeature
|
|
10
|
+
*/
|
|
11
|
+
export interface ILockFeatureOptions extends IFeatureOptions {
|
|
12
|
+
/** Protocol type */
|
|
13
|
+
protocol: TLockProtocol;
|
|
14
|
+
/** Entity ID (for Home Assistant) */
|
|
15
|
+
entityId: string;
|
|
16
|
+
/** Protocol client for controlling the lock */
|
|
17
|
+
protocolClient: ILockProtocolClient;
|
|
18
|
+
/** Whether the lock supports physical open */
|
|
19
|
+
supportsOpen?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Lock Feature - control for smart locks
|
|
23
|
+
*
|
|
24
|
+
* Protocol-agnostic: works with Home Assistant, MQTT, August, Yale, etc.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const lock = device.getFeature<LockFeature>('lock');
|
|
29
|
+
* if (lock) {
|
|
30
|
+
* await lock.lock();
|
|
31
|
+
* console.log(`Lock is ${lock.isLocked ? 'locked' : 'unlocked'}`);
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class LockFeature extends Feature {
|
|
36
|
+
readonly type: "lock";
|
|
37
|
+
readonly protocol: TLockProtocol;
|
|
38
|
+
/** Entity ID (e.g., "lock.front_door") */
|
|
39
|
+
readonly entityId: string;
|
|
40
|
+
/** Capabilities */
|
|
41
|
+
readonly capabilities: ILockCapabilities;
|
|
42
|
+
/** Current state */
|
|
43
|
+
protected _lockState: TLockState;
|
|
44
|
+
protected _isLocked: boolean;
|
|
45
|
+
/** Protocol client for controlling the lock */
|
|
46
|
+
private protocolClient;
|
|
47
|
+
constructor(device: TDeviceReference, port: number, options: ILockFeatureOptions);
|
|
48
|
+
/**
|
|
49
|
+
* Get current lock state (cached)
|
|
50
|
+
*/
|
|
51
|
+
get lockState(): TLockState;
|
|
52
|
+
/**
|
|
53
|
+
* Check if locked (cached)
|
|
54
|
+
*/
|
|
55
|
+
get isLocked(): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Check if unlocked
|
|
58
|
+
*/
|
|
59
|
+
get isUnlocked(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Check if currently locking
|
|
62
|
+
*/
|
|
63
|
+
get isLocking(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Check if currently unlocking
|
|
66
|
+
*/
|
|
67
|
+
get isUnlocking(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Check if jammed
|
|
70
|
+
*/
|
|
71
|
+
get isJammed(): boolean;
|
|
72
|
+
protected doConnect(): Promise<void>;
|
|
73
|
+
protected doDisconnect(): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Lock the lock
|
|
76
|
+
*/
|
|
77
|
+
lock(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Unlock the lock
|
|
80
|
+
*/
|
|
81
|
+
unlock(): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Open the lock (physically open the door if supported)
|
|
84
|
+
*/
|
|
85
|
+
open(): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Get current state as object
|
|
88
|
+
*/
|
|
89
|
+
getState(): ILockStateInfo;
|
|
90
|
+
/**
|
|
91
|
+
* Refresh state from the device
|
|
92
|
+
*/
|
|
93
|
+
refreshState(): Promise<ILockStateInfo>;
|
|
94
|
+
/**
|
|
95
|
+
* Update state from external source
|
|
96
|
+
*/
|
|
97
|
+
updateState(state: ILockStateInfo): void;
|
|
98
|
+
/**
|
|
99
|
+
* Internal state update
|
|
100
|
+
*/
|
|
101
|
+
private updateStateInternal;
|
|
102
|
+
getFeatureInfo(): ILockFeatureInfo;
|
|
103
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock Feature
|
|
3
|
+
* Provides control for smart locks
|
|
4
|
+
*/
|
|
5
|
+
import { Feature } from './feature.abstract.js';
|
|
6
|
+
/**
|
|
7
|
+
* Lock Feature - control for smart locks
|
|
8
|
+
*
|
|
9
|
+
* Protocol-agnostic: works with Home Assistant, MQTT, August, Yale, etc.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const lock = device.getFeature<LockFeature>('lock');
|
|
14
|
+
* if (lock) {
|
|
15
|
+
* await lock.lock();
|
|
16
|
+
* console.log(`Lock is ${lock.isLocked ? 'locked' : 'unlocked'}`);
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class LockFeature extends Feature {
|
|
21
|
+
type = 'lock';
|
|
22
|
+
protocol;
|
|
23
|
+
/** Entity ID (e.g., "lock.front_door") */
|
|
24
|
+
entityId;
|
|
25
|
+
/** Capabilities */
|
|
26
|
+
capabilities;
|
|
27
|
+
/** Current state */
|
|
28
|
+
_lockState = 'unknown';
|
|
29
|
+
_isLocked = false;
|
|
30
|
+
/** Protocol client for controlling the lock */
|
|
31
|
+
protocolClient;
|
|
32
|
+
constructor(device, port, options) {
|
|
33
|
+
super(device, port, options);
|
|
34
|
+
this.protocol = options.protocol;
|
|
35
|
+
this.entityId = options.entityId;
|
|
36
|
+
this.protocolClient = options.protocolClient;
|
|
37
|
+
this.capabilities = {
|
|
38
|
+
supportsOpen: options.supportsOpen ?? false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Properties
|
|
43
|
+
// ============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Get current lock state (cached)
|
|
46
|
+
*/
|
|
47
|
+
get lockState() {
|
|
48
|
+
return this._lockState;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if locked (cached)
|
|
52
|
+
*/
|
|
53
|
+
get isLocked() {
|
|
54
|
+
return this._isLocked;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if unlocked
|
|
58
|
+
*/
|
|
59
|
+
get isUnlocked() {
|
|
60
|
+
return this._lockState === 'unlocked';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if currently locking
|
|
64
|
+
*/
|
|
65
|
+
get isLocking() {
|
|
66
|
+
return this._lockState === 'locking';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if currently unlocking
|
|
70
|
+
*/
|
|
71
|
+
get isUnlocking() {
|
|
72
|
+
return this._lockState === 'unlocking';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if jammed
|
|
76
|
+
*/
|
|
77
|
+
get isJammed() {
|
|
78
|
+
return this._lockState === 'jammed';
|
|
79
|
+
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Connection
|
|
82
|
+
// ============================================================================
|
|
83
|
+
async doConnect() {
|
|
84
|
+
try {
|
|
85
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
86
|
+
this.updateStateInternal(state);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Ignore errors fetching initial state
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async doDisconnect() {
|
|
93
|
+
// Nothing to disconnect
|
|
94
|
+
}
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Lock Control
|
|
97
|
+
// ============================================================================
|
|
98
|
+
/**
|
|
99
|
+
* Lock the lock
|
|
100
|
+
*/
|
|
101
|
+
async lock() {
|
|
102
|
+
await this.protocolClient.lock(this.entityId);
|
|
103
|
+
this._lockState = 'locking';
|
|
104
|
+
this.emit('state:changed', this.getState());
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Unlock the lock
|
|
108
|
+
*/
|
|
109
|
+
async unlock() {
|
|
110
|
+
await this.protocolClient.unlock(this.entityId);
|
|
111
|
+
this._lockState = 'unlocking';
|
|
112
|
+
this.emit('state:changed', this.getState());
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Open the lock (physically open the door if supported)
|
|
116
|
+
*/
|
|
117
|
+
async open() {
|
|
118
|
+
if (!this.capabilities.supportsOpen) {
|
|
119
|
+
throw new Error('Lock does not support physical open');
|
|
120
|
+
}
|
|
121
|
+
await this.protocolClient.open(this.entityId);
|
|
122
|
+
this._lockState = 'unlocked';
|
|
123
|
+
this._isLocked = false;
|
|
124
|
+
this.emit('state:changed', this.getState());
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get current state as object
|
|
128
|
+
*/
|
|
129
|
+
getState() {
|
|
130
|
+
return {
|
|
131
|
+
state: this._lockState,
|
|
132
|
+
isLocked: this._isLocked,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Refresh state from the device
|
|
137
|
+
*/
|
|
138
|
+
async refreshState() {
|
|
139
|
+
const state = await this.protocolClient.getState(this.entityId);
|
|
140
|
+
this.updateStateInternal(state);
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Update state from external source
|
|
145
|
+
*/
|
|
146
|
+
updateState(state) {
|
|
147
|
+
this.updateStateInternal(state);
|
|
148
|
+
this.emit('state:changed', state);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Internal state update
|
|
152
|
+
*/
|
|
153
|
+
updateStateInternal(state) {
|
|
154
|
+
this._lockState = state.state;
|
|
155
|
+
this._isLocked = state.isLocked;
|
|
156
|
+
}
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Feature Info
|
|
159
|
+
// ============================================================================
|
|
160
|
+
getFeatureInfo() {
|
|
161
|
+
return {
|
|
162
|
+
...this.getBaseFeatureInfo(),
|
|
163
|
+
type: 'lock',
|
|
164
|
+
protocol: this.protocol,
|
|
165
|
+
capabilities: this.capabilities,
|
|
166
|
+
currentState: this.getState(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmVhdHVyZS5sb2NrLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZmVhdHVyZXMvZmVhdHVyZS5sb2NrLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sRUFBRSxPQUFPLEVBQXlCLE1BQU0sdUJBQXVCLENBQUM7QUF5QnZFOzs7Ozs7Ozs7Ozs7O0dBYUc7QUFDSCxNQUFNLE9BQU8sV0FBWSxTQUFRLE9BQU87SUFDdEIsSUFBSSxHQUFHLE1BQWUsQ0FBQztJQUN2QixRQUFRLENBQWdCO0lBRXhDLDBDQUEwQztJQUMxQixRQUFRLENBQVM7SUFFakMsbUJBQW1CO0lBQ0gsWUFBWSxDQUFvQjtJQUVoRCxvQkFBb0I7SUFDVixVQUFVLEdBQWUsU0FBUyxDQUFDO0lBQ25DLFNBQVMsR0FBWSxLQUFLLENBQUM7SUFFckMsK0NBQStDO0lBQ3ZDLGNBQWMsQ0FBc0I7SUFFNUMsWUFDRSxNQUF3QixFQUN4QixJQUFZLEVBQ1osT0FBNEI7UUFFNUIsS0FBSyxDQUFDLE1BQU0sRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFDN0IsSUFBSSxDQUFDLFFBQVEsR0FBRyxPQUFPLENBQUMsUUFBUSxDQUFDO1FBQ2pDLElBQUksQ0FBQyxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsQ0FBQztRQUNqQyxJQUFJLENBQUMsY0FBYyxHQUFHLE9BQU8sQ0FBQyxjQUFjLENBQUM7UUFFN0MsSUFBSSxDQUFDLFlBQVksR0FBRztZQUNsQixZQUFZLEVBQUUsT0FBTyxDQUFDLFlBQVksSUFBSSxLQUFLO1NBQzVDLENBQUM7SUFDSixDQUFDO0lBRUQsK0VBQStFO0lBQy9FLGFBQWE7SUFDYiwrRUFBK0U7SUFFL0U7O09BRUc7SUFDSCxJQUFXLFNBQVM7UUFDbEIsT0FBTyxJQUFJLENBQUMsVUFBVSxDQUFDO0lBQ3pCLENBQUM7SUFFRDs7T0FFRztJQUNILElBQVcsUUFBUTtRQUNqQixPQUFPLElBQUksQ0FBQyxTQUFT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
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playback Feature
|
|
3
|
+
* Provides media playback control capability
|
|
4
|
+
*/
|
|
5
|
+
import { Feature, type TDeviceReference } from './feature.abstract.js';
|
|
6
|
+
import type { TPlaybackProtocol, TPlaybackState, ITrackInfo, IPlaybackStatus, IPlaybackFeatureInfo, IFeatureOptions } from '../interfaces/feature.interfaces.js';
|
|
7
|
+
/**
|
|
8
|
+
* Options for creating a PlaybackFeature
|
|
9
|
+
*/
|
|
10
|
+
export interface IPlaybackFeatureOptions extends IFeatureOptions {
|
|
11
|
+
protocol: TPlaybackProtocol;
|
|
12
|
+
supportsQueue?: boolean;
|
|
13
|
+
supportsSeek?: boolean;
|
|
14
|
+
/** Protocol-specific client instance (Sonos, AirPlay, etc.) */
|
|
15
|
+
protocolClient?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Playback Feature - provides media playback control
|
|
19
|
+
*
|
|
20
|
+
* Abstract feature that can be backed by different protocols (Sonos, AirPlay, Chromecast, DLNA).
|
|
21
|
+
* Concrete implementations should extend this class for protocol-specific behavior.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const playback = device.getFeature<PlaybackFeature>('playback');
|
|
26
|
+
* if (playback) {
|
|
27
|
+
* await playback.play('spotify:track:123');
|
|
28
|
+
* const status = await playback.getPlaybackStatus();
|
|
29
|
+
* console.log(`Playing: ${status.track?.title}`);
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare class PlaybackFeature extends Feature {
|
|
34
|
+
readonly type: "playback";
|
|
35
|
+
readonly protocol: TPlaybackProtocol;
|
|
36
|
+
supportsQueue: boolean;
|
|
37
|
+
supportsSeek: boolean;
|
|
38
|
+
protected _playbackState: TPlaybackState;
|
|
39
|
+
protected _currentTrack: ITrackInfo | null;
|
|
40
|
+
protected _position: number;
|
|
41
|
+
protected _duration: number;
|
|
42
|
+
protected protocolClient: unknown;
|
|
43
|
+
constructor(device: TDeviceReference, port: number, options: IPlaybackFeatureOptions);
|
|
44
|
+
get playbackState(): TPlaybackState;
|
|
45
|
+
get currentTrack(): ITrackInfo | null;
|
|
46
|
+
get position(): number;
|
|
47
|
+
get duration(): number;
|
|
48
|
+
get isPlaying(): boolean;
|
|
49
|
+
protected doConnect(): Promise<void>;
|
|
50
|
+
protected doDisconnect(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Start playback
|
|
53
|
+
* @param uri Optional URI to play. If not provided, resumes current playback.
|
|
54
|
+
*/
|
|
55
|
+
play(uri?: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Pause playback
|
|
58
|
+
*/
|
|
59
|
+
pause(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Stop playback
|
|
62
|
+
*/
|
|
63
|
+
stop(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Skip to next track
|
|
66
|
+
*/
|
|
67
|
+
next(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Go to previous track
|
|
70
|
+
*/
|
|
71
|
+
previous(): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Seek to position
|
|
74
|
+
* @param seconds Position in seconds
|
|
75
|
+
*/
|
|
76
|
+
seek(seconds: number): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Get current track info
|
|
79
|
+
*/
|
|
80
|
+
getCurrentTrack(): Promise<ITrackInfo | null>;
|
|
81
|
+
/**
|
|
82
|
+
* Get playback status
|
|
83
|
+
*/
|
|
84
|
+
getPlaybackStatus(): Promise<IPlaybackStatus>;
|
|
85
|
+
/**
|
|
86
|
+
* Update track info (called by protocol-specific implementations)
|
|
87
|
+
*/
|
|
88
|
+
protected updateTrack(track: ITrackInfo | null): void;
|
|
89
|
+
/**
|
|
90
|
+
* Update position (called by protocol-specific implementations)
|
|
91
|
+
*/
|
|
92
|
+
protected updatePosition(position: number, duration: number): void;
|
|
93
|
+
getFeatureInfo(): IPlaybackFeatureInfo;
|
|
94
|
+
/**
|
|
95
|
+
* Create from discovery metadata
|
|
96
|
+
*/
|
|
97
|
+
static fromDiscovery(device: TDeviceReference, port: number, protocol: TPlaybackProtocol, metadata: Record<string, unknown>): PlaybackFeature;
|
|
98
|
+
}
|