@camstack/addon-provider-homematic 1.2.162 → 1.2.164
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/CHANGELOG.md +12 -0
- package/README.md +19 -0
- package/dist/addon.js +101576 -100165
- package/dist/addon.mjs +101575 -100141
- package/dist/index.js +2 -552
- package/dist/index.mjs +2 -551
- package/i18n/en.json +2 -2
- package/i18n/it.json +2 -2
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,551 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Persisted config every Homematic-backed device carries. `address` is
|
|
5
|
-
* the CCU device address (e.g. `0001D3C99AB123`) — the dispatch key the
|
|
6
|
-
* device uses to resolve its live `HmDevice` from the facade and to
|
|
7
|
-
* filter inbound `valueChanged` events to its own datapoints.
|
|
8
|
-
*/
|
|
9
|
-
var homematicDeviceSchema = object({
|
|
10
|
-
address: string().min(1).describe("CCU device address"),
|
|
11
|
-
/** The registry broker id this device belongs to (e.g. `hm_001`).
|
|
12
|
-
* Absent on devices persisted before the multi-CCU migration; those
|
|
13
|
-
* fall back to the primary broker via `homematicFacades.get(null)`. */
|
|
14
|
-
brokerId: string().optional()
|
|
15
|
-
});
|
|
16
|
-
/** Cold-start defaults so reads never miss a slice before the first push. */
|
|
17
|
-
var COLD_SWITCH = {
|
|
18
|
-
on: false,
|
|
19
|
-
lastChangedAt: 0
|
|
20
|
-
};
|
|
21
|
-
var COLD_COVER = {
|
|
22
|
-
state: "closed",
|
|
23
|
-
position: null,
|
|
24
|
-
tiltPosition: null,
|
|
25
|
-
lastChangedAt: 0
|
|
26
|
-
};
|
|
27
|
-
var COLD_LOCK = {
|
|
28
|
-
state: "unlocked",
|
|
29
|
-
lastChangedAt: 0
|
|
30
|
-
};
|
|
31
|
-
var COLD_MOTION = {
|
|
32
|
-
detected: false,
|
|
33
|
-
lastDetectedAt: null,
|
|
34
|
-
autoClearAfterMs: null
|
|
35
|
-
};
|
|
36
|
-
var COLD_CLIMATE = {
|
|
37
|
-
mode: "auto",
|
|
38
|
-
availableModes: [...[
|
|
39
|
-
"off",
|
|
40
|
-
"heat",
|
|
41
|
-
"auto"
|
|
42
|
-
]],
|
|
43
|
-
fanMode: "",
|
|
44
|
-
availableFanModes: [],
|
|
45
|
-
preset: "",
|
|
46
|
-
availablePresets: ["boost"],
|
|
47
|
-
target: null,
|
|
48
|
-
targetHigh: null,
|
|
49
|
-
targetLow: null,
|
|
50
|
-
targetHumidity: null,
|
|
51
|
-
currentTemp: null,
|
|
52
|
-
currentHumidity: null,
|
|
53
|
-
swingVertical: null,
|
|
54
|
-
swingHorizontal: null,
|
|
55
|
-
lastFetchedAt: 0
|
|
56
|
-
};
|
|
57
|
-
/**
|
|
58
|
-
* The standard HmIP key-channel datapoint vocabulary, used VERBATIM (NO
|
|
59
|
-
* normalization) as the `event-emitter` event types. These are the native
|
|
60
|
-
* uppercase tokens a `KEY_TRANSCEIVER` / `KEY_TRANSMITTER` / `VIRTUAL_KEY`
|
|
61
|
-
* / `KEY` channel emits on the facade `valueChanged` stream (via HA they
|
|
62
|
-
* surface lowercased as `press_short` etc.; natively they are uppercase):
|
|
63
|
-
* - PRESS_SHORT single short press
|
|
64
|
-
* - PRESS_LONG long press (repeats while held on some firmware)
|
|
65
|
-
* - PRESS_LONG_START long-press begin
|
|
66
|
-
* - PRESS_LONG_RELEASE long-press end
|
|
67
|
-
* - PRESS_CONT continuous-press tick while held
|
|
68
|
-
* Used both as the seed vocabulary fallback and as the fire-detection set
|
|
69
|
-
* (a non-key datapoint such as a maintenance LOW_BAT is ignored).
|
|
70
|
-
*/
|
|
71
|
-
var HM_KEY_PRESS_DATAPOINTS = [
|
|
72
|
-
"PRESS_SHORT",
|
|
73
|
-
"PRESS_LONG",
|
|
74
|
-
"PRESS_LONG_START",
|
|
75
|
-
"PRESS_LONG_RELEASE",
|
|
76
|
-
"PRESS_CONT"
|
|
77
|
-
];
|
|
78
|
-
/** Set form for O(1) fire-detection membership tests. */
|
|
79
|
-
var HM_KEY_PRESS_SET = new Set(HM_KEY_PRESS_DATAPOINTS);
|
|
80
|
-
/** Cold-start event-emitter slice; vocabulary supplied per-device. */
|
|
81
|
-
function coldEventEmitter(eventTypes) {
|
|
82
|
-
return {
|
|
83
|
-
eventTypes: [...eventTypes],
|
|
84
|
-
lastEvent: null,
|
|
85
|
-
eventCountSinceStart: 0
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Collect the union of native `PRESS_*` datapoints a device's key channels
|
|
90
|
-
* support, VERBATIM. Reads each key channel's `dataPoints` (when the
|
|
91
|
-
* facade enumerated them) and keeps only the recognised press tokens. Falls
|
|
92
|
-
* back to the standard HmIP key vocabulary when no datapoints were
|
|
93
|
-
* enumerated, so the cap always advertises a non-empty vocabulary.
|
|
94
|
-
*/
|
|
95
|
-
function keyEventVocabulary(keyChannels) {
|
|
96
|
-
const found = /* @__PURE__ */ new Set();
|
|
97
|
-
for (const channel of keyChannels) for (const dp of channel.dataPoints ?? []) if (HM_KEY_PRESS_SET.has(dp.parameter)) found.add(dp.parameter);
|
|
98
|
-
if (found.size === 0) return HM_KEY_PRESS_DATAPOINTS;
|
|
99
|
-
return HM_KEY_PRESS_DATAPOINTS.filter((dp) => found.has(dp));
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* The CamStack cap names that are ACTUATORS — driven by the library's typed
|
|
103
|
-
* custom entities (correct channel), NOT by the raw mapped channels. The
|
|
104
|
-
* raw-channel registration loop skips these so they are not double-registered
|
|
105
|
-
* against the wrong channel.
|
|
106
|
-
*/
|
|
107
|
-
var ACTUATOR_CAP_NAMES = new Set([
|
|
108
|
-
"switch",
|
|
109
|
-
"cover",
|
|
110
|
-
"light",
|
|
111
|
-
"lock-control",
|
|
112
|
-
"climate-control"
|
|
113
|
-
]);
|
|
114
|
-
/**
|
|
115
|
-
* Map a `nodehomematic` custom-entity `kind` to its CamStack actuator cap
|
|
116
|
-
* name. The union is discriminated by `kind`; the exhaustive switch needs no
|
|
117
|
-
* cast. Returns `null` for kinds with no actuator cap wired yet.
|
|
118
|
-
*/
|
|
119
|
-
function actuatorCapForEntityKind(kind) {
|
|
120
|
-
switch (kind) {
|
|
121
|
-
case "switch": return "switch";
|
|
122
|
-
case "cover":
|
|
123
|
-
case "blind": return "cover";
|
|
124
|
-
case "light": return "light";
|
|
125
|
-
case "lock": return "lock-control";
|
|
126
|
-
case "climate": return "climate-control";
|
|
127
|
-
default: return null;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/** Parse the numeric channel index suffix (`:N`) from a channel address.
|
|
131
|
-
* Returns the raw suffix string (kept as a string for the event payload,
|
|
132
|
-
* mirroring the CCU's `device:index` addressing). Null when absent. */
|
|
133
|
-
function channelIndexFromAddress(channelAddress) {
|
|
134
|
-
const idx = channelAddress.lastIndexOf(":");
|
|
135
|
-
if (idx === -1) return null;
|
|
136
|
-
const suffix = channelAddress.slice(idx + 1);
|
|
137
|
-
return suffix.length > 0 ? suffix : null;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* One CamStack device per Homematic device. Maps the device's channels to
|
|
141
|
-
* CamStack caps (via `homematic-mapping.ts`) and wires each cap's
|
|
142
|
-
* get/set surface to the `nodehomematic` facade's typed helpers
|
|
143
|
-
* (`switchTurnOn` / `coverSetPosition` / `climateSetTemperature` /
|
|
144
|
-
* `lockLock`) and raw `getValue`/`setValue` for read-only sensors.
|
|
145
|
-
*
|
|
146
|
-
* Status flows back via the facade `valueChanged` stream: each push is
|
|
147
|
-
* decoded by parameter name into the relevant cap's runtime-state slice
|
|
148
|
-
* (see `homematic-decode.ts` for the pure decoders).
|
|
149
|
-
*
|
|
150
|
-
* MAPPED (control + status):
|
|
151
|
-
* - switch (HmIP-BSM / BS2)
|
|
152
|
-
* - cover (HmIP-BROLL)
|
|
153
|
-
* - climate-control (HmIP-eTRV-C / BWTH / STHD / WTH-2 / HEATING)
|
|
154
|
-
* - lock-control (HmIP-DLD)
|
|
155
|
-
* MAPPED (read-only sensors):
|
|
156
|
-
* - temperature-sensor / humidity-sensor (climate channels)
|
|
157
|
-
* - motion + ambient-light-sensor (HmIP-SMI)
|
|
158
|
-
* - power-meter (HmIP-BSM / ESI energy)
|
|
159
|
-
* MAPPED (push-only events):
|
|
160
|
-
* - event-emitter (HmIP-WRC* wall switches / HmIP-RC* remotes / key
|
|
161
|
-
* channels) — one singleton cap over all key channels;
|
|
162
|
-
* native PRESS_* datapoints fired verbatim with the
|
|
163
|
-
* button index in `data.channel`.
|
|
164
|
-
*/
|
|
165
|
-
var HomematicDevice = class extends BaseDevice {
|
|
166
|
-
features = [];
|
|
167
|
-
/** CCU device address — dispatch + event-filter key. */
|
|
168
|
-
address;
|
|
169
|
-
/**
|
|
170
|
-
* Registry broker id this device belongs to. Undefined for devices
|
|
171
|
-
* persisted before the multi-CCU migration; those resolve to the primary
|
|
172
|
-
* broker via `homematicFacades.get(null)`.
|
|
173
|
-
*/
|
|
174
|
-
brokerId;
|
|
175
|
-
/** Unsubscribe for the facade `valueChanged` listener. */
|
|
176
|
-
valueChangedUnsub = null;
|
|
177
|
-
/** Channel address that owns each registered cap (for decode routing). */
|
|
178
|
-
capChannels = /* @__PURE__ */ new Map();
|
|
179
|
-
/** Native press-datapoint vocabulary for this device's key channels
|
|
180
|
-
* (verbatim, NO normalization). Empty when the device has no key
|
|
181
|
-
* channel — gates the `event-emitter` fire path. */
|
|
182
|
-
eventTypes = [];
|
|
183
|
-
/** Monotonic event sequence — two identical presses still produce
|
|
184
|
-
* distinct slice writes / bus emissions. */
|
|
185
|
-
#eventSeq = 0;
|
|
186
|
-
/** Count of key presses observed since this device instance started. */
|
|
187
|
-
#eventCount = 0;
|
|
188
|
-
constructor(ctx) {
|
|
189
|
-
const persisted = ctx.persistedConfig ?? {};
|
|
190
|
-
const address = persisted.address;
|
|
191
|
-
if (typeof address !== "string" || address.length === 0) throw new Error(`HomematicDevice: missing address in persisted config (id=${ctx.id})`);
|
|
192
|
-
super(ctx, homematicDeviceSchema, { type: ctx.deviceMeta.type });
|
|
193
|
-
this.address = address;
|
|
194
|
-
this.brokerId = typeof persisted.brokerId === "string" ? persisted.brokerId : void 0;
|
|
195
|
-
this.registerAdminLinkCap();
|
|
196
|
-
this.registerCapabilities();
|
|
197
|
-
this.attachValueChangedListener();
|
|
198
|
-
this.updateSourceInfo({
|
|
199
|
-
id: this.address,
|
|
200
|
-
system: "homematic"
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
/** The live facade for this device's CCU, or null when disconnected. */
|
|
204
|
-
get facade() {
|
|
205
|
-
return homematicFacades.get(this.brokerId ?? null);
|
|
206
|
-
}
|
|
207
|
-
/** Resolve this device's current `HmDevice` snapshot from the facade. */
|
|
208
|
-
resolveHmDevice() {
|
|
209
|
-
const facade = this.facade;
|
|
210
|
-
if (!facade) return null;
|
|
211
|
-
return facade.devices().find((d) => d.address === this.address) ?? null;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* device-admin-link — a Homematic device has no web server; the CCU3 /
|
|
215
|
-
* RaspberryMatic WebUI is where it is configured. The CCU has no stable
|
|
216
|
-
* per-device deep link (the WebUI addresses devices through a session-bound
|
|
217
|
-
* frameset), so every device on a CCU gets that CCU's root, labelled for
|
|
218
|
-
* what it is. A device persisted before multi-CCU support carries no
|
|
219
|
-
* `brokerId` and cannot be attributed to a CCU at all — it answers null.
|
|
220
|
-
*
|
|
221
|
-
* Registered OUTSIDE `registerCapabilities`, which returns early when the
|
|
222
|
-
* facade has not yet seen the device: whether the CCU has a WebUI does not
|
|
223
|
-
* depend on that, and an early return there used to be how a device ended up
|
|
224
|
-
* with no caps at all.
|
|
225
|
-
*/
|
|
226
|
-
registerAdminLinkCap() {
|
|
227
|
-
const brokerId = this.brokerId;
|
|
228
|
-
this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
|
|
229
|
-
if (deviceId !== this.id) throw new Error(`HomematicDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
230
|
-
if (brokerId === void 0) {
|
|
231
|
-
this.ctx.logger.debug("homematic admin link unavailable: no brokerId on device", {
|
|
232
|
-
tags: { deviceId: this.id },
|
|
233
|
-
meta: { address: this.address }
|
|
234
|
-
});
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
const cfg = await this.ctx.api.broker.getBrokerConfig.query({ id: brokerId });
|
|
238
|
-
const host = typeof cfg?.["host"] === "string" ? cfg["host"] : "";
|
|
239
|
-
const built = buildDeviceAdminUrl({
|
|
240
|
-
host,
|
|
241
|
-
port: null,
|
|
242
|
-
scheme: cfg?.["tls"] === true ? "https" : "http",
|
|
243
|
-
path: "/"
|
|
244
|
-
});
|
|
245
|
-
if (!built.ok) {
|
|
246
|
-
this.ctx.logger.debug("homematic admin link unavailable", {
|
|
247
|
-
tags: { deviceId: this.id },
|
|
248
|
-
meta: {
|
|
249
|
-
reason: built.reason,
|
|
250
|
-
brokerId
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
url: built.url,
|
|
257
|
-
label: "CCU WebUI",
|
|
258
|
-
target: "integration",
|
|
259
|
-
host
|
|
260
|
-
};
|
|
261
|
-
} });
|
|
262
|
-
}
|
|
263
|
-
registerCapabilities() {
|
|
264
|
-
const hmDevice = this.resolveHmDevice();
|
|
265
|
-
if (!hmDevice) {
|
|
266
|
-
this.ctx.logger.warn("HomematicDevice: device not found on facade at construction", { meta: { address: this.address } });
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
this.registerActuatorCapsFromCustomEntities();
|
|
270
|
-
const keyChannels = [];
|
|
271
|
-
for (const { channel, mapping } of mapHmDeviceChannels(hmDevice)) for (const capName of mapping.capabilities) {
|
|
272
|
-
if (capName === "event-emitter") {
|
|
273
|
-
keyChannels.push(channel);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (ACTUATOR_CAP_NAMES.has(capName)) continue;
|
|
277
|
-
this.registerCap(capName, channel.address);
|
|
278
|
-
}
|
|
279
|
-
if (keyChannels.length > 0) this.registerEventEmitterCap(keyChannels);
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Register the device's ACTUATOR caps (switch / cover / light / lock /
|
|
283
|
-
* climate-control) from the library's typed custom entities. Each
|
|
284
|
-
* `HmCustomEntity` carries the EXACT channel the lib bound the entity to —
|
|
285
|
-
* the channel the facade control helpers require. The cap channel stored in
|
|
286
|
-
* `capChannels` is therefore the custom-entity channel, so `coverOpen`
|
|
287
|
-
* (etc.) dispatch to the correct one.
|
|
288
|
-
*
|
|
289
|
-
* When `customEntities()` is empty (facade not fully built yet at
|
|
290
|
-
* construction time), this is a no-op beyond a debug note; the actuator
|
|
291
|
-
* caps register on the next reconcile/re-probe once the entities exist.
|
|
292
|
-
*/
|
|
293
|
-
registerActuatorCapsFromCustomEntities() {
|
|
294
|
-
const entities = this.facade?.customEntities()?.filter((e) => e.device === this.address) ?? [];
|
|
295
|
-
if (entities.length === 0) {
|
|
296
|
-
this.ctx.logger.debug("HomematicDevice: no custom entities at registration; actuator caps deferred to next reconcile", { meta: { address: this.address } });
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
for (const entity of entities) {
|
|
300
|
-
const capName = actuatorCapForEntityKind(entity.kind);
|
|
301
|
-
if (capName === null) continue;
|
|
302
|
-
this.registerCap(capName, entity.channel);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
/** Dispatch one cap registration to its concrete wiring. */
|
|
306
|
-
registerCap(capName, channelAddress) {
|
|
307
|
-
this.capChannels.set(capName, channelAddress);
|
|
308
|
-
switch (capName) {
|
|
309
|
-
case "switch": return this.registerSwitchCap(channelAddress);
|
|
310
|
-
case "cover": return this.registerCoverCap(channelAddress);
|
|
311
|
-
case "climate-control": return this.registerClimateCap(channelAddress);
|
|
312
|
-
case "lock-control": return this.registerLockCap(channelAddress);
|
|
313
|
-
case "motion": return this.registerMotionCap();
|
|
314
|
-
case "ambient-light-sensor": return registerAmbientLightCap(this.ctx, this.runtimeState);
|
|
315
|
-
case "temperature-sensor": return registerTemperatureCap(this.ctx, this.runtimeState);
|
|
316
|
-
case "humidity-sensor": return registerHumidityCap(this.ctx, this.runtimeState);
|
|
317
|
-
case "power-meter": return registerPowerMeterCap(this.ctx, this.runtimeState);
|
|
318
|
-
default:
|
|
319
|
-
this.ctx.logger.debug("HomematicDevice: cap not yet wired", { meta: {
|
|
320
|
-
capName,
|
|
321
|
-
channelAddress,
|
|
322
|
-
address: this.address
|
|
323
|
-
} });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
registerSwitchCap(channelAddress) {
|
|
328
|
-
this.ctx.registerNativeCap(switchCapability, {
|
|
329
|
-
getStatus: async () => this.runtimeState.getCapState("switch") ?? COLD_SWITCH,
|
|
330
|
-
setState: async ({ on }) => {
|
|
331
|
-
const facade = this.requireFacade();
|
|
332
|
-
if (on) await facade.switchTurnOn(this.address, channelAddress);
|
|
333
|
-
else await facade.switchTurnOff(this.address, channelAddress);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
this.runtimeState.setCapState("switch", COLD_SWITCH);
|
|
337
|
-
}
|
|
338
|
-
registerCoverCap(channelAddress) {
|
|
339
|
-
this.ctx.registerNativeCap(coverCapability, {
|
|
340
|
-
getStatus: async () => this.runtimeState.getCapState("cover") ?? COLD_COVER,
|
|
341
|
-
open: async () => {
|
|
342
|
-
await this.requireFacade().coverOpen(this.address, channelAddress);
|
|
343
|
-
},
|
|
344
|
-
close: async () => {
|
|
345
|
-
await this.requireFacade().coverClose(this.address, channelAddress);
|
|
346
|
-
},
|
|
347
|
-
stop: async () => {
|
|
348
|
-
await this.requireFacade().coverStop(this.address, channelAddress);
|
|
349
|
-
},
|
|
350
|
-
setPosition: async ({ position }) => {
|
|
351
|
-
await this.requireFacade().coverSetPosition(this.address, channelAddress, position);
|
|
352
|
-
},
|
|
353
|
-
setTiltPosition: async () => this.unsupported("cover tilt")
|
|
354
|
-
});
|
|
355
|
-
this.runtimeState.setCapState("cover", COLD_COVER);
|
|
356
|
-
}
|
|
357
|
-
registerClimateCap(channelAddress) {
|
|
358
|
-
this.ctx.registerNativeCap(climateControlCapability, {
|
|
359
|
-
getStatus: async () => this.runtimeState.getCapState("climate-control") ?? COLD_CLIMATE,
|
|
360
|
-
setMode: async ({ mode }) => {
|
|
361
|
-
await this.requireFacade().climateSetMode(this.address, channelAddress, toHmClimateMode(mode));
|
|
362
|
-
},
|
|
363
|
-
setTarget: async ({ target }) => {
|
|
364
|
-
await this.requireFacade().climateSetTemperature(this.address, channelAddress, target);
|
|
365
|
-
},
|
|
366
|
-
setPreset: async ({ preset }) => {
|
|
367
|
-
await this.requireFacade().climateSetBoost(this.address, channelAddress, preset === "boost");
|
|
368
|
-
},
|
|
369
|
-
setFanMode: async () => this.unsupported("climate fan mode"),
|
|
370
|
-
setTargetRange: async () => this.unsupported("climate dual setpoint"),
|
|
371
|
-
setTargetHumidity: async () => this.unsupported("climate humidity control"),
|
|
372
|
-
setSwingVertical: async () => this.unsupported("climate vertical swing"),
|
|
373
|
-
setSwingHorizontal: async () => this.unsupported("climate horizontal swing")
|
|
374
|
-
});
|
|
375
|
-
this.runtimeState.setCapState("climate-control", COLD_CLIMATE);
|
|
376
|
-
}
|
|
377
|
-
registerLockCap(channelAddress) {
|
|
378
|
-
this.ctx.registerNativeCap(lockControlCapability, {
|
|
379
|
-
getStatus: async () => this.runtimeState.getCapState("lock-control") ?? COLD_LOCK,
|
|
380
|
-
lock: async () => {
|
|
381
|
-
await this.requireFacade().lockLock(this.address, channelAddress);
|
|
382
|
-
},
|
|
383
|
-
unlock: async () => {
|
|
384
|
-
await this.requireFacade().lockUnlock(this.address, channelAddress);
|
|
385
|
-
},
|
|
386
|
-
open: async () => {
|
|
387
|
-
await this.requireFacade().lockOpen(this.address, channelAddress);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
this.runtimeState.setCapState("lock-control", COLD_LOCK);
|
|
391
|
-
}
|
|
392
|
-
registerMotionCap() {
|
|
393
|
-
this.ctx.registerNativeCap(motionCapability, {
|
|
394
|
-
getStatus: async () => this.runtimeState.getCapState("motion") ?? COLD_MOTION,
|
|
395
|
-
isDetected: async () => (this.runtimeState.getCapState("motion") ?? COLD_MOTION).detected
|
|
396
|
-
});
|
|
397
|
-
this.runtimeState.setCapState("motion", coldMotionKeepingRise(this.runtimeState.getCapState("motion")));
|
|
398
|
-
}
|
|
399
|
-
registerEventEmitterCap(keyChannels) {
|
|
400
|
-
this.eventTypes = keyEventVocabulary(keyChannels);
|
|
401
|
-
this.ctx.registerNativeCap(eventEmitterCapability, { getStatus: async () => this.runtimeState.getCapState("event-emitter") ?? coldEventEmitter(this.eventTypes) });
|
|
402
|
-
this.runtimeState.setCapState("event-emitter", coldEventEmitter(this.eventTypes));
|
|
403
|
-
}
|
|
404
|
-
attachValueChangedListener() {
|
|
405
|
-
const facade = this.facade;
|
|
406
|
-
if (!facade) return;
|
|
407
|
-
this.valueChangedUnsub = facade.on("valueChanged", (event) => {
|
|
408
|
-
if (event.device !== this.address) return;
|
|
409
|
-
this.handleValueChanged(event);
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Translate an inbound facade `valueChanged` into the relevant cap
|
|
414
|
-
* slice, keyed by parameter name. Only updates caps the device
|
|
415
|
-
* actually registered (`capChannels`).
|
|
416
|
-
*/
|
|
417
|
-
handleValueChanged(event) {
|
|
418
|
-
const now = event.ts || Date.now();
|
|
419
|
-
const { parameter, value } = event;
|
|
420
|
-
if (this.capChannels.has("switch") && parameter === "STATE" && typeof value === "boolean") this.runtimeState.setCapState("switch", {
|
|
421
|
-
on: value,
|
|
422
|
-
lastChangedAt: now
|
|
423
|
-
});
|
|
424
|
-
if (this.capChannels.has("cover")) {
|
|
425
|
-
const next = decodeCover(this.runtimeState.getCapState("cover") ?? COLD_COVER, parameter, value, now);
|
|
426
|
-
if (next) this.runtimeState.setCapState("cover", next);
|
|
427
|
-
}
|
|
428
|
-
if (this.capChannels.has("lock-control")) {
|
|
429
|
-
const next = decodeLock(parameter, value, now);
|
|
430
|
-
if (next) this.runtimeState.setCapState("lock-control", next);
|
|
431
|
-
}
|
|
432
|
-
if (this.capChannels.has("climate-control")) {
|
|
433
|
-
const next = decodeClimate(this.runtimeState.getCapState("climate-control") ?? COLD_CLIMATE, parameter, value, now);
|
|
434
|
-
if (next) this.runtimeState.setCapState("climate-control", next);
|
|
435
|
-
}
|
|
436
|
-
if (this.capChannels.has("motion") && parameter === "MOTION" && typeof value === "boolean") this.runtimeState.setCapState("motion", {
|
|
437
|
-
detected: value,
|
|
438
|
-
lastDetectedAt: value ? now : this.runtimeState.getCapState("motion")?.lastDetectedAt ?? null,
|
|
439
|
-
autoClearAfterMs: null
|
|
440
|
-
});
|
|
441
|
-
if (this.eventTypes.length > 0) this.decodeKeyPress(event, now);
|
|
442
|
-
this.decodeSensorPush(parameter, value, now);
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Translate a key-channel `PRESS_*` datapoint into an `EventFire`: emit
|
|
446
|
-
* `EventCategory.EventEmitted` on the bus and write the `event-emitter`
|
|
447
|
-
* slice. The `eventType` is the native datapoint name VERBATIM (no
|
|
448
|
-
* normalization); the button index (`:N` suffix of the channel address)
|
|
449
|
-
* travels in `data.channel`. Non-key datapoints and value resets (the
|
|
450
|
-
* key datapoint dropping back to `false`) are ignored.
|
|
451
|
-
*/
|
|
452
|
-
decodeKeyPress(event, now) {
|
|
453
|
-
const { parameter, value, channel } = event;
|
|
454
|
-
if (!HM_KEY_PRESS_SET.has(parameter)) return;
|
|
455
|
-
if (typeof value === "boolean" && value === false) return;
|
|
456
|
-
const channelIndex = channelIndexFromAddress(channel);
|
|
457
|
-
const fire = {
|
|
458
|
-
deviceId: this.id,
|
|
459
|
-
eventType: parameter,
|
|
460
|
-
data: channelIndex !== null ? { channel: channelIndex } : {},
|
|
461
|
-
timestamp: now,
|
|
462
|
-
seq: ++this.#eventSeq
|
|
463
|
-
};
|
|
464
|
-
this.ctx.eventBus.emit(createEvent(EventCategory.EventEmitted, {
|
|
465
|
-
type: "device",
|
|
466
|
-
id: this.stableId,
|
|
467
|
-
deviceId: this.id,
|
|
468
|
-
addonId: this.ctx.deviceMeta.addonId
|
|
469
|
-
}, fire));
|
|
470
|
-
const slice = {
|
|
471
|
-
eventTypes: [...this.eventTypes],
|
|
472
|
-
lastEvent: fire,
|
|
473
|
-
eventCountSinceStart: ++this.#eventCount
|
|
474
|
-
};
|
|
475
|
-
this.runtimeState.setCapState("event-emitter", slice);
|
|
476
|
-
}
|
|
477
|
-
/** Decode read-only sensor pushes into their slices. */
|
|
478
|
-
decodeSensorPush(parameter, value, now) {
|
|
479
|
-
if (typeof value !== "number") return;
|
|
480
|
-
if (this.capChannels.has("temperature-sensor") && parameter === "ACTUAL_TEMPERATURE") this.runtimeState.setCapState("temperature-sensor", {
|
|
481
|
-
temperature: value,
|
|
482
|
-
lastFetchedAt: now
|
|
483
|
-
});
|
|
484
|
-
if (this.capChannels.has("humidity-sensor") && parameter === "HUMIDITY") this.runtimeState.setCapState("humidity-sensor", {
|
|
485
|
-
humidity: value,
|
|
486
|
-
lastFetchedAt: now
|
|
487
|
-
});
|
|
488
|
-
if (this.capChannels.has("ambient-light-sensor") && parameter === "ILLUMINATION") this.runtimeState.setCapState("ambient-light-sensor", {
|
|
489
|
-
illuminance: Math.max(0, value),
|
|
490
|
-
lastFetchedAt: now
|
|
491
|
-
});
|
|
492
|
-
this.decodePowerPush(parameter, value, now);
|
|
493
|
-
}
|
|
494
|
-
/** Decode HmIP energy-meter parameters into the power-meter slice. */
|
|
495
|
-
decodePowerPush(parameter, value, now) {
|
|
496
|
-
if (!this.capChannels.has("power-meter")) return;
|
|
497
|
-
const prev = this.runtimeState.getCapState("power-meter") ?? { lastFetchedAt: 0 };
|
|
498
|
-
switch (parameter) {
|
|
499
|
-
case "POWER":
|
|
500
|
-
this.runtimeState.setCapState("power-meter", {
|
|
501
|
-
...prev,
|
|
502
|
-
power: value,
|
|
503
|
-
lastFetchedAt: now
|
|
504
|
-
});
|
|
505
|
-
return;
|
|
506
|
-
case "ENERGY_COUNTER":
|
|
507
|
-
this.runtimeState.setCapState("power-meter", {
|
|
508
|
-
...prev,
|
|
509
|
-
energy: value / 1e3,
|
|
510
|
-
lastFetchedAt: now
|
|
511
|
-
});
|
|
512
|
-
return;
|
|
513
|
-
case "VOLTAGE":
|
|
514
|
-
this.runtimeState.setCapState("power-meter", {
|
|
515
|
-
...prev,
|
|
516
|
-
voltage: value,
|
|
517
|
-
lastFetchedAt: now
|
|
518
|
-
});
|
|
519
|
-
return;
|
|
520
|
-
case "CURRENT":
|
|
521
|
-
this.runtimeState.setCapState("power-meter", {
|
|
522
|
-
...prev,
|
|
523
|
-
current: value / 1e3,
|
|
524
|
-
lastFetchedAt: now
|
|
525
|
-
});
|
|
526
|
-
return;
|
|
527
|
-
default: return;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
/** Reject an operation the underlying HmIP hardware does not support. */
|
|
531
|
-
unsupported(surface) {
|
|
532
|
-
throw new Error(`HomematicDevice: ${surface} not supported (address=${this.address})`);
|
|
533
|
-
}
|
|
534
|
-
/** Narrow the optional facade to a present one or throw a clear error. */
|
|
535
|
-
requireFacade() {
|
|
536
|
-
const facade = this.facade;
|
|
537
|
-
if (!facade) throw new Error(`HomematicDevice: CCU connection unavailable (address=${this.address})`);
|
|
538
|
-
return facade;
|
|
539
|
-
}
|
|
540
|
-
async removeDevice() {
|
|
541
|
-
if (this.valueChangedUnsub) {
|
|
542
|
-
try {
|
|
543
|
-
this.valueChangedUnsub();
|
|
544
|
-
} catch {}
|
|
545
|
-
this.valueChangedUnsub = null;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
DeviceType.Container, DeviceType.Switch, DeviceType.Cover, DeviceType.Thermostat, DeviceType.Lock, DeviceType.Sensor, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic;
|
|
550
|
-
//#endregion
|
|
551
|
-
export { HomematicDevice, HomematicProviderAddon, buildConnectionFormSchema, deriveHmEntitySpecs, homematicAddonConfigSchema, homematicConfigSchema, homematicDeviceSchema, inferHmDeviceType, mapHmChannel, mapHmDeviceChannels, toHomematicOptions };
|
|
1
|
+
import { HomematicProviderAddon, a as buildConnectionFormSchema, c as homematicGatewayDeviceSchema, i as mapHmDeviceChannels, l as toHomematicOptions, n as inferHmDeviceType, o as connectionFromSettings, r as mapHmChannel, s as homematicConfigSchema, t as deriveHmEntitySpecs } from "./addon.mjs";
|
|
2
|
+
export { HomematicProviderAddon, buildConnectionFormSchema, connectionFromSettings, deriveHmEntitySpecs, homematicConfigSchema, homematicGatewayDeviceSchema, inferHmDeviceType, mapHmChannel, mapHmDeviceChannels, toHomematicOptions };
|
package/i18n/en.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"globalSettings.s_hm-broker.title": "Homematic broker",
|
|
3
|
-
"globalSettings.s_hm-broker.brokerHelp.label": "Brokers are managed separately",
|
|
4
2
|
"connection.s_connection.title": "CCU connection",
|
|
3
|
+
"connection.s_connection.__homematic-discovered.label": "CCU",
|
|
5
4
|
"connection.s_connection.host.label": "Host / IP",
|
|
5
|
+
"connection.s_connection.host.description": "Filled when one CCU is found. Type the address if it is not.",
|
|
6
6
|
"connection.s_interfaces.title": "Interfaces",
|
|
7
7
|
"connection.s_interfaces.description": "CCU-Jack and CUxD are not supported.",
|
|
8
8
|
"connection.s_interfaces.enableHmipRf.label": "Enable Homematic IP",
|
package/i18n/it.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"globalSettings.s_hm-broker.title": "Broker Homematic",
|
|
3
|
-
"globalSettings.s_hm-broker.brokerHelp.label": "I broker si gestiscono separatamente",
|
|
4
2
|
"connection.s_connection.title": "Connessione alla CCU",
|
|
3
|
+
"connection.s_connection.__homematic-discovered.label": "CCU",
|
|
5
4
|
"connection.s_connection.host.label": "Host / IP",
|
|
5
|
+
"connection.s_connection.host.description": "Compilato quando viene trovata una CCU. Digita l'indirizzo se non viene trovata.",
|
|
6
6
|
"connection.s_interfaces.title": "Interfacce",
|
|
7
7
|
"connection.s_interfaces.description": "CCU-Jack e CUxD non sono supportati.",
|
|
8
8
|
"connection.s_interfaces.enableHmipRf.label": "Attiva Homematic IP",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-homematic",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.164",
|
|
4
4
|
"description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"category": "providers",
|
|
40
40
|
"name": "Homematic",
|
|
41
41
|
"version": "0.1.0",
|
|
42
|
-
"description": "
|
|
42
|
+
"description": "Adopt a Homematic CCU3 / RaspberryMatic (as a Gateway device) and its switches, covers, lights, locks, thermostats and sensors, from an integration over the local network",
|
|
43
43
|
"entry": "./dist/addon.js",
|
|
44
44
|
"icon": "assets/icon.svg",
|
|
45
45
|
"color": "#e2001a",
|
|
46
46
|
"instanceMode": "multiple",
|
|
47
|
-
"
|
|
47
|
+
"mode": "account",
|
|
48
48
|
"execution": {
|
|
49
49
|
"placement": "hub-only"
|
|
50
50
|
},
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"name": "device-provider"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
"name": "
|
|
56
|
+
"name": "device-adoption"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
"name": "
|
|
59
|
+
"name": "connection-test"
|
|
60
60
|
}
|
|
61
61
|
]
|
|
62
62
|
}
|