@camstack/addon-provider-enpal 0.1.17 → 0.1.18
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 +6 -0
- package/assets/icon.svg +7 -10
- package/dist/addon.js +380 -27
- package/dist/addon.mjs +380 -27
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.18] - 2026-11-12
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Test connection works with only the box address, and the addon icon is the Enpal mark.
|
|
15
|
+
|
|
10
16
|
## [0.1.17] - 2026-11-12
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/assets/icon.svg
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
<path d="M17.7 17.7l1.4 1.4" />
|
|
9
|
-
<path d="M4.9 19.1l1.4-1.4" />
|
|
10
|
-
<path d="M17.7 6.3l1.4-1.4" />
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
2
|
+
<!-- Enpal mark: navy tile and amber E from the public favicon. -->
|
|
3
|
+
<rect width="24" height="24" fill="#072543"/>
|
|
4
|
+
<rect x="5" y="5" width="3" height="14" fill="#FFB000"/>
|
|
5
|
+
<rect x="5" y="5" width="14" height="3" fill="#FFB000"/>
|
|
6
|
+
<rect x="5" y="10.5" width="9" height="3" fill="#FFB000"/>
|
|
7
|
+
<rect x="5" y="16" width="14" height="3" fill="#FFB000"/>
|
|
11
8
|
</svg>
|
package/dist/addon.js
CHANGED
|
@@ -3,7 +3,7 @@ let node_child_process = require("node:child_process");
|
|
|
3
3
|
let node_fs = require("node:fs");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
let node_url = require("node:url");
|
|
6
|
-
//#region ../types/dist/event-category-
|
|
6
|
+
//#region ../types/dist/event-category-Dv-Gcv1f.mjs
|
|
7
7
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
8
8
|
EventCategory["SystemBoot"] = "system.boot";
|
|
9
9
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -444,6 +444,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
444
444
|
*/
|
|
445
445
|
EventCategory["DeviceStateChanged"] = "device.state-changed";
|
|
446
446
|
/**
|
|
447
|
+
* A device COMMAND (a vocabulary action, D920/D983) settled in the hub's
|
|
448
|
+
* transition audit (D1022): confirmed, already in place, unconfirmed or
|
|
449
|
+
* failed. Never a device's own change. Payload: `DeviceCommandSettledEvent`
|
|
450
|
+
* (`{deviceId, action, outcome, actor, at}`, no arguments). It feeds
|
|
451
|
+
* the Notification Center `device-command` kind. Telemetry (D8): losing one
|
|
452
|
+
* costs a notification, never state; the audit row is the record.
|
|
453
|
+
*/
|
|
454
|
+
EventCategory["DeviceCommandSettled"] = "device.command-settled";
|
|
455
|
+
/**
|
|
447
456
|
* The NATIVE shadow of a cap a composition has claimed fields of moved
|
|
448
457
|
* (D663). Payload: `{ deviceId, capName, native }` — what the provider
|
|
449
458
|
* wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
|
|
@@ -6048,7 +6057,7 @@ var ZodIssueCode = {
|
|
|
6048
6057
|
var ZodFirstPartyTypeKind;
|
|
6049
6058
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6050
6059
|
//#endregion
|
|
6051
|
-
//#region ../types/dist/sleep-
|
|
6060
|
+
//#region ../types/dist/sleep-Czv-4wFH.mjs
|
|
6052
6061
|
/**
|
|
6053
6062
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6054
6063
|
* window to float samples (D455).
|
|
@@ -6731,6 +6740,13 @@ async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
|
6731
6740
|
else skipped.push(id);
|
|
6732
6741
|
};
|
|
6733
6742
|
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6743
|
+
const deferred = /* @__PURE__ */ new Set();
|
|
6744
|
+
const deferredBy = (reason) => {
|
|
6745
|
+
if (selected === null || reason === null) return null;
|
|
6746
|
+
const dep = /^dependency-(?:pending|blocked):(.+)$/.exec(reason)?.[1];
|
|
6747
|
+
if (dep === void 0 || !byId.has(dep)) return null;
|
|
6748
|
+
return !selected.has(dep) || deferred.has(dep) ? dep : null;
|
|
6749
|
+
};
|
|
6734
6750
|
for (const step of declaration.steps) {
|
|
6735
6751
|
if (selected !== null && !selected.has(step.id)) continue;
|
|
6736
6752
|
const bound = byId.get(step.id);
|
|
@@ -6742,6 +6758,19 @@ async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
|
6742
6758
|
try {
|
|
6743
6759
|
const gate = await host.startMigration({ id: step.id });
|
|
6744
6760
|
if (!gate.proceed) {
|
|
6761
|
+
const waitsFor = deferredBy(gate.reason);
|
|
6762
|
+
if (waitsFor !== null) {
|
|
6763
|
+
log.info("migration deferred: its predecessor runs in another phase of this owner", {
|
|
6764
|
+
tags,
|
|
6765
|
+
meta: {
|
|
6766
|
+
waitsFor,
|
|
6767
|
+
reason: gate.reason
|
|
6768
|
+
}
|
|
6769
|
+
});
|
|
6770
|
+
deferred.add(step.id);
|
|
6771
|
+
skipped.push(step.id);
|
|
6772
|
+
continue;
|
|
6773
|
+
}
|
|
6745
6774
|
log.warn("migration not run", {
|
|
6746
6775
|
tags,
|
|
6747
6776
|
meta: {
|
|
@@ -10279,6 +10308,13 @@ var DeviceTransitionSchema = object({
|
|
|
10279
10308
|
actor: TransitionActorSchema,
|
|
10280
10309
|
command: TransitionCommandSchema.optional()
|
|
10281
10310
|
});
|
|
10311
|
+
object({
|
|
10312
|
+
deviceId: number$1().int(),
|
|
10313
|
+
action: string(),
|
|
10314
|
+
outcome: TransitionOutcomeSchema,
|
|
10315
|
+
actor: TransitionActorSchema,
|
|
10316
|
+
at: number$1()
|
|
10317
|
+
});
|
|
10282
10318
|
var READING_QUANTITIES = [
|
|
10283
10319
|
"temperature",
|
|
10284
10320
|
"pressure",
|
|
@@ -12302,7 +12338,10 @@ var ACTUATOR_CAP_VOCABULARY = {
|
|
|
12302
12338
|
"ir-transmitter": {
|
|
12303
12339
|
scope: "device",
|
|
12304
12340
|
noun: t("IR / RF transmitter", "Trasmettitore IR / RF"),
|
|
12305
|
-
notOperatorFacing: {
|
|
12341
|
+
notOperatorFacing: {
|
|
12342
|
+
send: "a raw pulse train is not something an operator composes in an automation; the remote that owns the command table sends it",
|
|
12343
|
+
learn: "an interactive capture the operator starts from the source dialog and answers by pressing a physical remote; no automation can supply the signal"
|
|
12344
|
+
}
|
|
12306
12345
|
},
|
|
12307
12346
|
control: {
|
|
12308
12347
|
scope: "device",
|
|
@@ -51167,11 +51206,13 @@ var IrCodecError = class extends Error {
|
|
|
51167
51206
|
this.code = code;
|
|
51168
51207
|
}
|
|
51169
51208
|
};
|
|
51209
|
+
/** Maximum number of mark/space entries one pulse train may carry. */
|
|
51210
|
+
var IR_TIMINGS_MAX = 1024;
|
|
51170
51211
|
/**
|
|
51171
51212
|
* A pulse train in signed microseconds: mark (+) first, then space (-), strictly
|
|
51172
51213
|
* alternating, no zeros, 2..1024 entries.
|
|
51173
51214
|
*/
|
|
51174
|
-
var IrTimingsSchema = array(number$1().int()).min(2).max(
|
|
51215
|
+
var IrTimingsSchema = array(number$1().int()).min(2).max(IR_TIMINGS_MAX).superRefine((timings, ctx) => {
|
|
51175
51216
|
timings.forEach((value, index) => {
|
|
51176
51217
|
const expectMark = index % 2 === 0;
|
|
51177
51218
|
if (value === 0 || value > 0 !== expectMark) ctx.addIssue({
|
|
@@ -51218,15 +51259,186 @@ function decodePronto(code) {
|
|
|
51218
51259
|
carrierHz: Math.round(1e6 / unitUs)
|
|
51219
51260
|
};
|
|
51220
51261
|
}
|
|
51221
|
-
/**
|
|
51222
|
-
|
|
51223
|
-
|
|
51224
|
-
|
|
51225
|
-
|
|
51226
|
-
|
|
51227
|
-
|
|
51228
|
-
|
|
51229
|
-
|
|
51262
|
+
/**
|
|
51263
|
+
* FastLZ level 1: a byte-oriented LZ77 stream. A control byte below 32 starts a
|
|
51264
|
+
* literal run of `ctrl + 1` bytes; otherwise it is a back-reference whose length
|
|
51265
|
+
* is `ctrl >> 5` (extended by one byte when 7) and whose distance is split
|
|
51266
|
+
* between the low 5 bits and the next byte.
|
|
51267
|
+
*/
|
|
51268
|
+
var MAX_DISTANCE = 8192;
|
|
51269
|
+
var MAX_LITERAL_RUN = 32;
|
|
51270
|
+
var MIN_MATCH = 3;
|
|
51271
|
+
var MAX_MATCH = 264;
|
|
51272
|
+
/**
|
|
51273
|
+
* Most bytes a decoded IR code may hold: two bytes per timing. A level-1 stream
|
|
51274
|
+
* can expand ~130x, so the decoder checks this bound before every write.
|
|
51275
|
+
*/
|
|
51276
|
+
var IR_DECOMPRESSED_MAX = IR_TIMINGS_MAX * 2;
|
|
51277
|
+
function fastlzDecompress(input) {
|
|
51278
|
+
const out = [];
|
|
51279
|
+
let ip = 0;
|
|
51280
|
+
const room = (count) => {
|
|
51281
|
+
if (out.length + count > IR_DECOMPRESSED_MAX) throw new IrCodecError("tuya-malformed", "decompressed code is larger than any IR train");
|
|
51282
|
+
};
|
|
51283
|
+
const next = () => {
|
|
51284
|
+
const byte = input[ip++];
|
|
51285
|
+
if (byte === void 0) throw new IrCodecError("tuya-malformed", "compressed stream is truncated");
|
|
51286
|
+
return byte;
|
|
51287
|
+
};
|
|
51288
|
+
while (ip < input.length) {
|
|
51289
|
+
const ctrl = next();
|
|
51290
|
+
if (ctrl < 32) {
|
|
51291
|
+
room(ctrl + 1);
|
|
51292
|
+
for (let i = 0; i <= ctrl; i++) out.push(next());
|
|
51293
|
+
continue;
|
|
51294
|
+
}
|
|
51295
|
+
let len = (ctrl >> 5) - 1;
|
|
51296
|
+
if (len === 6) len += next();
|
|
51297
|
+
const distance = ((ctrl & 31) << 8) + next() + 1;
|
|
51298
|
+
const start = out.length - distance;
|
|
51299
|
+
if (start < 0) throw new IrCodecError("tuya-malformed", "back-reference points before the stream");
|
|
51300
|
+
room(len + 3);
|
|
51301
|
+
for (let i = 0; i < len + 3; i++) {
|
|
51302
|
+
const byte = out[start + i];
|
|
51303
|
+
if (byte === void 0) throw new IrCodecError("tuya-malformed", "back-reference is invalid");
|
|
51304
|
+
out.push(byte);
|
|
51305
|
+
}
|
|
51306
|
+
}
|
|
51307
|
+
return Uint8Array.from(out);
|
|
51308
|
+
}
|
|
51309
|
+
function fastlzCompress(input) {
|
|
51310
|
+
const out = [];
|
|
51311
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
51312
|
+
let literalStart = 0;
|
|
51313
|
+
const flushLiterals = (end) => {
|
|
51314
|
+
for (let from = literalStart; from < end; from += MAX_LITERAL_RUN) {
|
|
51315
|
+
const to = Math.min(from + MAX_LITERAL_RUN, end);
|
|
51316
|
+
out.push(to - from - 1);
|
|
51317
|
+
for (let i = from; i < to; i++) out.push(input[i] ?? 0);
|
|
51318
|
+
}
|
|
51319
|
+
};
|
|
51320
|
+
let pos = 0;
|
|
51321
|
+
while (pos < input.length) {
|
|
51322
|
+
let matchLen = 0;
|
|
51323
|
+
let matchDistance = 0;
|
|
51324
|
+
if (pos + MIN_MATCH <= input.length) {
|
|
51325
|
+
const key = (input[pos] ?? 0) << 16 | (input[pos + 1] ?? 0) << 8 | (input[pos + 2] ?? 0);
|
|
51326
|
+
const candidate = lastSeen.get(key);
|
|
51327
|
+
lastSeen.set(key, pos);
|
|
51328
|
+
if (candidate !== void 0 && pos - candidate <= MAX_DISTANCE) {
|
|
51329
|
+
let len = 0;
|
|
51330
|
+
while (pos + len < input.length && len < MAX_MATCH && input[candidate + len] === input[pos + len]) len++;
|
|
51331
|
+
if (len >= MIN_MATCH) {
|
|
51332
|
+
matchLen = len;
|
|
51333
|
+
matchDistance = pos - candidate - 1;
|
|
51334
|
+
}
|
|
51335
|
+
}
|
|
51336
|
+
}
|
|
51337
|
+
if (matchLen === 0) {
|
|
51338
|
+
pos++;
|
|
51339
|
+
continue;
|
|
51340
|
+
}
|
|
51341
|
+
flushLiterals(pos);
|
|
51342
|
+
const enc = matchLen - 2;
|
|
51343
|
+
if (enc < 7) out.push(enc << 5 | matchDistance >> 8);
|
|
51344
|
+
else out.push(224 | matchDistance >> 8, enc - 7);
|
|
51345
|
+
out.push(matchDistance & 255);
|
|
51346
|
+
pos += matchLen;
|
|
51347
|
+
literalStart = pos;
|
|
51348
|
+
}
|
|
51349
|
+
flushLiterals(input.length);
|
|
51350
|
+
return Uint8Array.from(out);
|
|
51351
|
+
}
|
|
51352
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
51353
|
+
var MAX_DURATION_US = 65535;
|
|
51354
|
+
function toBase64(bytes) {
|
|
51355
|
+
let out = "";
|
|
51356
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
51357
|
+
const a = bytes[i] ?? 0;
|
|
51358
|
+
const b = bytes[i + 1] ?? 0;
|
|
51359
|
+
const c = bytes[i + 2] ?? 0;
|
|
51360
|
+
const word = a << 16 | b << 8 | c;
|
|
51361
|
+
out += ALPHABET[word >> 18 & 63];
|
|
51362
|
+
out += ALPHABET[word >> 12 & 63];
|
|
51363
|
+
out += i + 1 < bytes.length ? ALPHABET[word >> 6 & 63] : "=";
|
|
51364
|
+
out += i + 2 < bytes.length ? ALPHABET[word & 63] : "=";
|
|
51365
|
+
}
|
|
51366
|
+
return out;
|
|
51367
|
+
}
|
|
51368
|
+
function fromBase64(text) {
|
|
51369
|
+
const clean = text.trim().replace(/=+$/, "");
|
|
51370
|
+
if (clean.length === 0 || clean.length % 4 === 1) throw new IrCodecError("tuya-malformed", "not a base64 string");
|
|
51371
|
+
const out = [];
|
|
51372
|
+
let bits = 0;
|
|
51373
|
+
let acc = 0;
|
|
51374
|
+
for (const ch of clean) {
|
|
51375
|
+
const value = ALPHABET.indexOf(ch);
|
|
51376
|
+
if (value < 0) throw new IrCodecError("tuya-malformed", "not a base64 string");
|
|
51377
|
+
acc = acc << 6 | value;
|
|
51378
|
+
bits += 6;
|
|
51379
|
+
if (bits >= 8) {
|
|
51380
|
+
bits -= 8;
|
|
51381
|
+
out.push(acc >> bits & 255);
|
|
51382
|
+
}
|
|
51383
|
+
}
|
|
51384
|
+
return Uint8Array.from(out);
|
|
51385
|
+
}
|
|
51386
|
+
/** Signed pulse train to a Tuya IR code: base64(FastLZ(uint16 LE durations)). */
|
|
51387
|
+
function encodeTuya(timings) {
|
|
51388
|
+
const bytes = new Uint8Array(timings.length * 2);
|
|
51389
|
+
timings.forEach((value, index) => {
|
|
51390
|
+
const duration = Math.abs(value);
|
|
51391
|
+
if (duration === 0 || duration > MAX_DURATION_US) throw new IrCodecError("invalid-timings", `timing ${index} does not fit 16 bits`);
|
|
51392
|
+
bytes[index * 2] = duration & 255;
|
|
51393
|
+
bytes[index * 2 + 1] = duration >> 8;
|
|
51394
|
+
});
|
|
51395
|
+
return toBase64(fastlzCompress(bytes));
|
|
51396
|
+
}
|
|
51397
|
+
/** A Tuya IR code to a signed pulse train (mark first). Throws `IrCodecError`. */
|
|
51398
|
+
function decodeTuya(code) {
|
|
51399
|
+
const bytes = fastlzDecompress(fromBase64(code));
|
|
51400
|
+
if (bytes.length === 0 || bytes.length % 2 !== 0) throw new IrCodecError("tuya-malformed", "payload is not a list of 16-bit durations");
|
|
51401
|
+
const timings = [];
|
|
51402
|
+
for (let i = 0; i < bytes.length; i += 2) {
|
|
51403
|
+
const duration = (bytes[i] ?? 0) | (bytes[i + 1] ?? 0) << 8;
|
|
51404
|
+
timings.push(i / 2 % 2 === 0 ? duration : -duration);
|
|
51405
|
+
}
|
|
51406
|
+
const checked = IrTimingsSchema.safeParse(timings);
|
|
51407
|
+
if (!checked.success) throw new IrCodecError("tuya-malformed", checked.error.message);
|
|
51408
|
+
return checked.data;
|
|
51409
|
+
}
|
|
51410
|
+
/** Device-native IR code formats a transmitter can replay verbatim. */
|
|
51411
|
+
var IrNativeFormatSchema = _enum(["tuya"]);
|
|
51412
|
+
/**
|
|
51413
|
+
* Longest native code accepted: base64 of the worst-case compressed stream
|
|
51414
|
+
* (every decoded byte as a literal, plus one control byte per 32), with margin.
|
|
51415
|
+
*/
|
|
51416
|
+
var IR_NATIVE_CODE_MAX_CHARS = Math.ceil((IR_DECOMPRESSED_MAX + Math.ceil(IR_DECOMPRESSED_MAX / 32)) / 3) * 4 + 256;
|
|
51417
|
+
var IrNativeCodeSchema = object({
|
|
51418
|
+
format: IrNativeFormatSchema,
|
|
51419
|
+
code: string().min(1).max(IR_NATIVE_CODE_MAX_CHARS)
|
|
51420
|
+
});
|
|
51421
|
+
var IR_NATIVE_CODECS = { tuya: {
|
|
51422
|
+
encode: encodeTuya,
|
|
51423
|
+
decode: decodeTuya
|
|
51424
|
+
} };
|
|
51425
|
+
/** What a remote command stores: a raw pulse train, a learned Pronto code, or a device-native code. */
|
|
51426
|
+
var IrPayloadSchema = discriminatedUnion("kind", [
|
|
51427
|
+
object({
|
|
51428
|
+
kind: literal("raw"),
|
|
51429
|
+
timings: array(number$1().int()),
|
|
51430
|
+
settings: TransmitSettingsSchema.optional()
|
|
51431
|
+
}),
|
|
51432
|
+
object({
|
|
51433
|
+
kind: literal("pronto"),
|
|
51434
|
+
code: string()
|
|
51435
|
+
}),
|
|
51436
|
+
object({
|
|
51437
|
+
kind: literal("native"),
|
|
51438
|
+
format: IrNativeFormatSchema,
|
|
51439
|
+
code: string()
|
|
51440
|
+
})
|
|
51441
|
+
]);
|
|
51230
51442
|
/** Resolve a payload to what `ir-transmitter.send` takes. Throws `IrCodecError`. */
|
|
51231
51443
|
function toTimings(payload) {
|
|
51232
51444
|
if (payload.kind === "pronto") {
|
|
@@ -51236,6 +51448,10 @@ function toTimings(payload) {
|
|
|
51236
51448
|
settings: { carrierHz: decoded.carrierHz }
|
|
51237
51449
|
};
|
|
51238
51450
|
}
|
|
51451
|
+
if (payload.kind === "native") return {
|
|
51452
|
+
timings: IR_NATIVE_CODECS[payload.format].decode(payload.code),
|
|
51453
|
+
settings: {}
|
|
51454
|
+
};
|
|
51239
51455
|
const checked = IrTimingsSchema.safeParse(payload.timings);
|
|
51240
51456
|
if (!checked.success) throw new IrCodecError("invalid-timings", checked.error.message);
|
|
51241
51457
|
return {
|
|
@@ -58995,6 +59211,15 @@ var HeroRowModelSchema = object({
|
|
|
58995
59211
|
extras: array(HeroControlSchema).max(2).optional(),
|
|
58996
59212
|
trailing: HeroFactSchema.nullable()
|
|
58997
59213
|
});
|
|
59214
|
+
new Set([
|
|
59215
|
+
"temperature",
|
|
59216
|
+
"humidity",
|
|
59217
|
+
"pressure",
|
|
59218
|
+
"illuminance",
|
|
59219
|
+
"reading",
|
|
59220
|
+
"air-quality",
|
|
59221
|
+
"valve"
|
|
59222
|
+
]);
|
|
58998
59223
|
DeviceType.Climate;
|
|
58999
59224
|
DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
|
|
59000
59225
|
/**
|
|
@@ -60083,13 +60308,32 @@ var IrSendResultSchema = object({
|
|
|
60083
60308
|
var IrTransmitterInfoSchema = object({
|
|
60084
60309
|
media: array(IrMediumSchema),
|
|
60085
60310
|
settable: array(TransmitSettingKeySchema),
|
|
60086
|
-
maxTimings: number$1().int().positive()
|
|
60311
|
+
maxTimings: number$1().int().positive(),
|
|
60312
|
+
/** The transmitter can learn a code (`learn`). */
|
|
60313
|
+
learn: boolean().default(false),
|
|
60314
|
+
/** Native code formats the transmitter replays verbatim (`send.native`). */
|
|
60315
|
+
formats: array(IrNativeFormatSchema).default([])
|
|
60316
|
+
});
|
|
60317
|
+
var IR_LEARN_TIMEOUT_DEFAULT_MS = 2e4;
|
|
60318
|
+
var IR_LEARN_TIMEOUT_MAX_MS = 6e4;
|
|
60319
|
+
var IrLearnResultSchema = object({
|
|
60320
|
+
/** The code in the device's own format, when it reports one. */
|
|
60321
|
+
native: IrNativeCodeSchema.nullable(),
|
|
60322
|
+
timings: IrTimingsSchema,
|
|
60323
|
+
carrierHz: number$1().int().positive().nullable()
|
|
60087
60324
|
});
|
|
60088
60325
|
DeviceType.Generic, method(object({
|
|
60089
60326
|
deviceId: number$1().int().nonnegative(),
|
|
60090
|
-
timings: IrTimingsSchema,
|
|
60327
|
+
timings: IrTimingsSchema.optional(),
|
|
60328
|
+
native: IrNativeCodeSchema.optional(),
|
|
60091
60329
|
settings: TransmitSettingsSchema.optional()
|
|
60092
|
-
}), IrSendResultSchema, {
|
|
60330
|
+
}).refine((input) => input.timings === void 0 !== (input.native === void 0), { message: "send takes exactly one of timings or native" }), IrSendResultSchema, {
|
|
60331
|
+
kind: "mutation",
|
|
60332
|
+
auth: "protected"
|
|
60333
|
+
}), method(object({
|
|
60334
|
+
deviceId: number$1().int().nonnegative(),
|
|
60335
|
+
timeoutMs: number$1().int().positive().max(IR_LEARN_TIMEOUT_MAX_MS).default(IR_LEARN_TIMEOUT_DEFAULT_MS)
|
|
60336
|
+
}), IrLearnResultSchema, {
|
|
60093
60337
|
kind: "mutation",
|
|
60094
60338
|
auth: "protected"
|
|
60095
60339
|
}), method(object({ deviceId: number$1().int().nonnegative() }), IrTransmitterInfoSchema, {
|
|
@@ -69875,6 +70119,7 @@ var NcSystemEventKindSchema = _enum([
|
|
|
69875
70119
|
"recording-degraded",
|
|
69876
70120
|
"device-attention-required",
|
|
69877
70121
|
"device-attention-resolved",
|
|
70122
|
+
"device-command",
|
|
69878
70123
|
"storage-location-low",
|
|
69879
70124
|
"storage-location-critical",
|
|
69880
70125
|
"storage-location-ok",
|
|
@@ -69937,11 +70182,6 @@ var NC_ALARM_SYSTEM_EVENT_KINDS = [
|
|
|
69937
70182
|
"alarm-sensor-bypass",
|
|
69938
70183
|
"alarm-disarmed"
|
|
69939
70184
|
];
|
|
69940
|
-
/**
|
|
69941
|
-
* One coherent system-event condition. `kinds` is the required opt-in safety
|
|
69942
|
-
* gate; the remaining lists are optional narrowing filters relevant to the
|
|
69943
|
-
* selected kinds.
|
|
69944
|
-
*/
|
|
69945
70185
|
var NcSystemEventConditionSchema = object({
|
|
69946
70186
|
kinds: array(NcSystemEventKindSchema).min(1),
|
|
69947
70187
|
deviceIds: array(number$1().int()).min(1).optional(),
|
|
@@ -69973,6 +70213,28 @@ var NcSystemEventConditionSchema = object({
|
|
|
69973
70213
|
nodeIds: array(string().min(1)).min(1).optional(),
|
|
69974
70214
|
packageNames: array(string().min(1)).min(1).optional(),
|
|
69975
70215
|
/**
|
|
70216
|
+
* `device-command` only: the vocabulary ACTION IDS (D983, `powerSource.forceRestart`)
|
|
70217
|
+
* this rule speaks about - the same dictionary the automation action blocks
|
|
70218
|
+
* pick from. The hub refuses an id the action index does not know.
|
|
70219
|
+
*/
|
|
70220
|
+
commandActions: array(string().min(1)).min(1).optional(),
|
|
70221
|
+
/** `device-command` only: how the command settled. A device's own change is not a command. */
|
|
70222
|
+
commandOutcomes: array(_enum([
|
|
70223
|
+
"confirmed",
|
|
70224
|
+
"already",
|
|
70225
|
+
"unconfirmed",
|
|
70226
|
+
"failed"
|
|
70227
|
+
])).min(1).optional(),
|
|
70228
|
+
/** `device-command` only: who asked (`user`, `addon`, `action`, `scheduler`, `composition`, `node`). */
|
|
70229
|
+
commandActorKinds: array(_enum([
|
|
70230
|
+
"user",
|
|
70231
|
+
"addon",
|
|
70232
|
+
"action",
|
|
70233
|
+
"scheduler",
|
|
70234
|
+
"composition",
|
|
70235
|
+
"node"
|
|
70236
|
+
])).min(1).optional(),
|
|
70237
|
+
/**
|
|
69976
70238
|
* At or below this remaining-life %, a consumable is LOW for THIS rule
|
|
69977
70239
|
* (D642). Absent = {@link NC_CONSUMABLE_LOW_PERCENT_DEFAULT}; at most
|
|
69978
70240
|
* {@link NC_CONSUMABLE_LOW_PERCENT_MAX}, so every episode can recover. Scoped to the
|
|
@@ -70953,6 +71215,13 @@ var NcRuleInputSchema = object({
|
|
|
70953
71215
|
*/
|
|
70954
71216
|
groupingSec: number$1().int().min(5).max(60).optional(),
|
|
70955
71217
|
/**
|
|
71218
|
+
* Hold deliveries of this rule to one target and, when two or more arrive
|
|
71219
|
+
* before the window ends, send them as one summary. Absent = the kind's
|
|
71220
|
+
* default (`groupWithinSecOf`): 5 s for a system-event rule, 0 for every
|
|
71221
|
+
* other kind. `0` sends each delivery as it does today.
|
|
71222
|
+
*/
|
|
71223
|
+
groupWithinSec: number$1().int().min(0).max(300).optional(),
|
|
71224
|
+
/**
|
|
70956
71225
|
* ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
|
|
70957
71226
|
* carries "Correct" / "Wrong" buttons (and, where the channel has room, a
|
|
70958
71227
|
* "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
|
|
@@ -71389,7 +71658,12 @@ var NcHistoryEntrySchema = object({
|
|
|
71389
71658
|
* Assistant export) must render as "no image right now", never as a
|
|
71390
71659
|
* broken link.
|
|
71391
71660
|
*/
|
|
71392
|
-
artifactIds: array(string().min(1)).optional()
|
|
71661
|
+
artifactIds: array(string().min(1)).optional(),
|
|
71662
|
+
/**
|
|
71663
|
+
* When this row was folded into a group-window summary, the outbox id of
|
|
71664
|
+
* that one notification. Absent on a row that was sent as itself.
|
|
71665
|
+
*/
|
|
71666
|
+
groupedInto: string().min(1).optional()
|
|
71393
71667
|
});
|
|
71394
71668
|
/**
|
|
71395
71669
|
* Query filter for `getHistory` (spec §4.2). Every field is a narrowing
|
|
@@ -91954,6 +92228,13 @@ Object.freeze({
|
|
|
91954
92228
|
access: "view",
|
|
91955
92229
|
role: "viewer"
|
|
91956
92230
|
},
|
|
92231
|
+
"irTransmitter.learn": {
|
|
92232
|
+
capName: "ir-transmitter",
|
|
92233
|
+
capScope: "device",
|
|
92234
|
+
addonId: null,
|
|
92235
|
+
access: "create",
|
|
92236
|
+
role: "viewer"
|
|
92237
|
+
},
|
|
91957
92238
|
"irTransmitter.send": {
|
|
91958
92239
|
capName: "ir-transmitter",
|
|
91959
92240
|
capScope: "device",
|
|
@@ -99805,6 +100086,11 @@ Object.freeze({
|
|
|
99805
100086
|
form: "single",
|
|
99806
100087
|
optional: false
|
|
99807
100088
|
}],
|
|
100089
|
+
"irTransmitter.learn": [{
|
|
100090
|
+
name: "deviceId",
|
|
100091
|
+
form: "single",
|
|
100092
|
+
optional: false
|
|
100093
|
+
}],
|
|
99808
100094
|
"irTransmitter.send": [{
|
|
99809
100095
|
name: "deviceId",
|
|
99810
100096
|
form: "single",
|
|
@@ -101744,7 +102030,10 @@ var NC_SYSTEM_EVENT_FILTER_KEYS = [
|
|
|
101744
102030
|
"deviceTypes",
|
|
101745
102031
|
"nodeIds",
|
|
101746
102032
|
"packageNames",
|
|
101747
|
-
"consumableLowPercent"
|
|
102033
|
+
"consumableLowPercent",
|
|
102034
|
+
"commandActions",
|
|
102035
|
+
"commandOutcomes",
|
|
102036
|
+
"commandActorKinds"
|
|
101748
102037
|
];
|
|
101749
102038
|
new Map(Object.entries({
|
|
101750
102039
|
"device-online": "device-events",
|
|
@@ -101767,6 +102056,7 @@ new Map(Object.entries({
|
|
|
101767
102056
|
"recording-degraded": "device-events",
|
|
101768
102057
|
"device-attention-required": "device-events",
|
|
101769
102058
|
"device-attention-resolved": "device-events",
|
|
102059
|
+
"device-command": "device-events",
|
|
101770
102060
|
"node-online": "system",
|
|
101771
102061
|
"node-offline": "system",
|
|
101772
102062
|
"node-restarting": "system",
|
|
@@ -101867,6 +102157,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
101867
102157
|
label: "Needs attention",
|
|
101868
102158
|
kinds: ["device-attention-required", "device-attention-resolved"]
|
|
101869
102159
|
},
|
|
102160
|
+
{
|
|
102161
|
+
id: "device-command",
|
|
102162
|
+
family: "device-events",
|
|
102163
|
+
label: "Commands given to devices",
|
|
102164
|
+
kinds: ["device-command"]
|
|
102165
|
+
},
|
|
101870
102166
|
{
|
|
101871
102167
|
id: "nodes",
|
|
101872
102168
|
family: "system",
|
|
@@ -102139,6 +102435,7 @@ var DEVICE_EVENTS = [
|
|
|
102139
102435
|
"recording-healthy",
|
|
102140
102436
|
"recording-degraded",
|
|
102141
102437
|
...DEVICE_ATTENTION,
|
|
102438
|
+
"device-command",
|
|
102142
102439
|
...ALARM
|
|
102143
102440
|
];
|
|
102144
102441
|
/** Every kind a summary may speak about — the two summarisable families. */
|
|
@@ -102292,6 +102589,9 @@ var STATE_SUMMARY_VARS = [
|
|
|
102292
102589
|
[...RULE_BODY_ONLY],
|
|
102293
102590
|
[...RULE_TEXT],
|
|
102294
102591
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
102592
|
+
[...RULE_TEXT],
|
|
102593
|
+
[...RULE_TEXT],
|
|
102594
|
+
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
102295
102595
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
102296
102596
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
102297
102597
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
@@ -105374,7 +105674,39 @@ var enpalConnectionsEntryKind = defineEntryKind("connections", {
|
|
|
105374
105674
|
});
|
|
105375
105675
|
/** Every kind this addon declares, keyed by its name. */
|
|
105376
105676
|
var enpalRecordKinds = { connections: enpalConnectionsEntryKind };
|
|
105677
|
+
var REJECTED_KINDS = new Set([
|
|
105678
|
+
"unauthorized",
|
|
105679
|
+
"token-missing",
|
|
105680
|
+
"empty"
|
|
105681
|
+
]);
|
|
105682
|
+
function connectionForTest(values) {
|
|
105683
|
+
const host = values["host"];
|
|
105684
|
+
if (typeof host !== "string" || host.trim().length === 0) return null;
|
|
105685
|
+
const parsed = enpalConnectionSchema.safeParse({ ...values });
|
|
105686
|
+
return parsed.success ? parsed.data : null;
|
|
105687
|
+
}
|
|
105688
|
+
function testFailure(error) {
|
|
105689
|
+
if (error instanceof EnpalError) return {
|
|
105690
|
+
success: false,
|
|
105691
|
+
status: REJECTED_KINDS.has(error.kind) ? "rejected" : "inconclusive",
|
|
105692
|
+
error: error.message
|
|
105693
|
+
};
|
|
105694
|
+
return {
|
|
105695
|
+
success: false,
|
|
105696
|
+
status: "inconclusive",
|
|
105697
|
+
error: "The Enpal box did not answer"
|
|
105698
|
+
};
|
|
105699
|
+
}
|
|
105700
|
+
function liveProbe(blazor) {
|
|
105701
|
+
return (connection) => readEnpalSnapshot({
|
|
105702
|
+
connection,
|
|
105703
|
+
fetch: globalThis.fetch,
|
|
105704
|
+
now: () => Date.now(),
|
|
105705
|
+
blazor: blazor?.()
|
|
105706
|
+
});
|
|
105707
|
+
}
|
|
105377
105708
|
function buildEnpalInstances(records, deps) {
|
|
105709
|
+
const probe = deps.probe ?? liveProbe(deps.blazor);
|
|
105378
105710
|
return new RecordIntegrationInstances(records, enpalConnectionsEntryKind, {
|
|
105379
105711
|
description: {
|
|
105380
105712
|
role: "devices",
|
|
@@ -105384,18 +105716,36 @@ function buildEnpalInstances(records, deps) {
|
|
|
105384
105716
|
},
|
|
105385
105717
|
instanceMode: "multiple",
|
|
105386
105718
|
kind: "server",
|
|
105387
|
-
canTest:
|
|
105719
|
+
canTest: true,
|
|
105388
105720
|
kinds: [{
|
|
105389
105721
|
id: "connection",
|
|
105390
105722
|
label: "Enpal",
|
|
105391
105723
|
functionCap: "device-provider",
|
|
105392
|
-
role: "devices"
|
|
105724
|
+
role: "devices",
|
|
105725
|
+
canTest: true
|
|
105393
105726
|
}]
|
|
105394
105727
|
},
|
|
105395
105728
|
formSchema: () => buildConnectionFormSchema(),
|
|
105396
105729
|
initialBody: (name) => ({ name }),
|
|
105397
105730
|
configured: (body) => (body.host ?? "").length > 0,
|
|
105398
|
-
onChanged: () => deps.onChanged()
|
|
105731
|
+
onChanged: () => deps.onChanged(),
|
|
105732
|
+
test: async ({ values }) => {
|
|
105733
|
+
const connection = connectionForTest(values);
|
|
105734
|
+
if (connection === null) return {
|
|
105735
|
+
success: false,
|
|
105736
|
+
status: "rejected",
|
|
105737
|
+
error: "Host is required"
|
|
105738
|
+
};
|
|
105739
|
+
try {
|
|
105740
|
+
await probe(connection);
|
|
105741
|
+
return {
|
|
105742
|
+
success: true,
|
|
105743
|
+
status: "validated"
|
|
105744
|
+
};
|
|
105745
|
+
} catch (error) {
|
|
105746
|
+
return testFailure(error);
|
|
105747
|
+
}
|
|
105748
|
+
}
|
|
105399
105749
|
});
|
|
105400
105750
|
}
|
|
105401
105751
|
/**
|
|
@@ -105688,7 +106038,10 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
105688
106038
|
...rowsDepsOver(api),
|
|
105689
106039
|
kind: enpalConnectionsEntryKind
|
|
105690
106040
|
}));
|
|
105691
|
-
this.instances = buildEnpalInstances(records, {
|
|
106041
|
+
this.instances = buildEnpalInstances(records, {
|
|
106042
|
+
onChanged: () => this.reconcileIntegrations(),
|
|
106043
|
+
blazor: () => this.blazorReader ?? void 0
|
|
106044
|
+
});
|
|
105692
106045
|
this.blazorReader = await this.createBlazorBridge();
|
|
105693
106046
|
this.clients = new EnpalClientRegistry(this.ctx.logger, {
|
|
105694
106047
|
blazor: () => this.blazorReader ?? void 0,
|
package/dist/addon.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
//#region ../types/dist/event-category-
|
|
5
|
+
//#region ../types/dist/event-category-Dv-Gcv1f.mjs
|
|
6
6
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
7
7
|
EventCategory["SystemBoot"] = "system.boot";
|
|
8
8
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -443,6 +443,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
443
443
|
*/
|
|
444
444
|
EventCategory["DeviceStateChanged"] = "device.state-changed";
|
|
445
445
|
/**
|
|
446
|
+
* A device COMMAND (a vocabulary action, D920/D983) settled in the hub's
|
|
447
|
+
* transition audit (D1022): confirmed, already in place, unconfirmed or
|
|
448
|
+
* failed. Never a device's own change. Payload: `DeviceCommandSettledEvent`
|
|
449
|
+
* (`{deviceId, action, outcome, actor, at}`, no arguments). It feeds
|
|
450
|
+
* the Notification Center `device-command` kind. Telemetry (D8): losing one
|
|
451
|
+
* costs a notification, never state; the audit row is the record.
|
|
452
|
+
*/
|
|
453
|
+
EventCategory["DeviceCommandSettled"] = "device.command-settled";
|
|
454
|
+
/**
|
|
446
455
|
* The NATIVE shadow of a cap a composition has claimed fields of moved
|
|
447
456
|
* (D663). Payload: `{ deviceId, capName, native }` — what the provider
|
|
448
457
|
* wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
|
|
@@ -6047,7 +6056,7 @@ var ZodIssueCode = {
|
|
|
6047
6056
|
var ZodFirstPartyTypeKind;
|
|
6048
6057
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6049
6058
|
//#endregion
|
|
6050
|
-
//#region ../types/dist/sleep-
|
|
6059
|
+
//#region ../types/dist/sleep-Czv-4wFH.mjs
|
|
6051
6060
|
/**
|
|
6052
6061
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6053
6062
|
* window to float samples (D455).
|
|
@@ -6730,6 +6739,13 @@ async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
|
6730
6739
|
else skipped.push(id);
|
|
6731
6740
|
};
|
|
6732
6741
|
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6742
|
+
const deferred = /* @__PURE__ */ new Set();
|
|
6743
|
+
const deferredBy = (reason) => {
|
|
6744
|
+
if (selected === null || reason === null) return null;
|
|
6745
|
+
const dep = /^dependency-(?:pending|blocked):(.+)$/.exec(reason)?.[1];
|
|
6746
|
+
if (dep === void 0 || !byId.has(dep)) return null;
|
|
6747
|
+
return !selected.has(dep) || deferred.has(dep) ? dep : null;
|
|
6748
|
+
};
|
|
6733
6749
|
for (const step of declaration.steps) {
|
|
6734
6750
|
if (selected !== null && !selected.has(step.id)) continue;
|
|
6735
6751
|
const bound = byId.get(step.id);
|
|
@@ -6741,6 +6757,19 @@ async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
|
6741
6757
|
try {
|
|
6742
6758
|
const gate = await host.startMigration({ id: step.id });
|
|
6743
6759
|
if (!gate.proceed) {
|
|
6760
|
+
const waitsFor = deferredBy(gate.reason);
|
|
6761
|
+
if (waitsFor !== null) {
|
|
6762
|
+
log.info("migration deferred: its predecessor runs in another phase of this owner", {
|
|
6763
|
+
tags,
|
|
6764
|
+
meta: {
|
|
6765
|
+
waitsFor,
|
|
6766
|
+
reason: gate.reason
|
|
6767
|
+
}
|
|
6768
|
+
});
|
|
6769
|
+
deferred.add(step.id);
|
|
6770
|
+
skipped.push(step.id);
|
|
6771
|
+
continue;
|
|
6772
|
+
}
|
|
6744
6773
|
log.warn("migration not run", {
|
|
6745
6774
|
tags,
|
|
6746
6775
|
meta: {
|
|
@@ -10278,6 +10307,13 @@ var DeviceTransitionSchema = object({
|
|
|
10278
10307
|
actor: TransitionActorSchema,
|
|
10279
10308
|
command: TransitionCommandSchema.optional()
|
|
10280
10309
|
});
|
|
10310
|
+
object({
|
|
10311
|
+
deviceId: number$1().int(),
|
|
10312
|
+
action: string(),
|
|
10313
|
+
outcome: TransitionOutcomeSchema,
|
|
10314
|
+
actor: TransitionActorSchema,
|
|
10315
|
+
at: number$1()
|
|
10316
|
+
});
|
|
10281
10317
|
var READING_QUANTITIES = [
|
|
10282
10318
|
"temperature",
|
|
10283
10319
|
"pressure",
|
|
@@ -12301,7 +12337,10 @@ var ACTUATOR_CAP_VOCABULARY = {
|
|
|
12301
12337
|
"ir-transmitter": {
|
|
12302
12338
|
scope: "device",
|
|
12303
12339
|
noun: t("IR / RF transmitter", "Trasmettitore IR / RF"),
|
|
12304
|
-
notOperatorFacing: {
|
|
12340
|
+
notOperatorFacing: {
|
|
12341
|
+
send: "a raw pulse train is not something an operator composes in an automation; the remote that owns the command table sends it",
|
|
12342
|
+
learn: "an interactive capture the operator starts from the source dialog and answers by pressing a physical remote; no automation can supply the signal"
|
|
12343
|
+
}
|
|
12305
12344
|
},
|
|
12306
12345
|
control: {
|
|
12307
12346
|
scope: "device",
|
|
@@ -51166,11 +51205,13 @@ var IrCodecError = class extends Error {
|
|
|
51166
51205
|
this.code = code;
|
|
51167
51206
|
}
|
|
51168
51207
|
};
|
|
51208
|
+
/** Maximum number of mark/space entries one pulse train may carry. */
|
|
51209
|
+
var IR_TIMINGS_MAX = 1024;
|
|
51169
51210
|
/**
|
|
51170
51211
|
* A pulse train in signed microseconds: mark (+) first, then space (-), strictly
|
|
51171
51212
|
* alternating, no zeros, 2..1024 entries.
|
|
51172
51213
|
*/
|
|
51173
|
-
var IrTimingsSchema = array(number$1().int()).min(2).max(
|
|
51214
|
+
var IrTimingsSchema = array(number$1().int()).min(2).max(IR_TIMINGS_MAX).superRefine((timings, ctx) => {
|
|
51174
51215
|
timings.forEach((value, index) => {
|
|
51175
51216
|
const expectMark = index % 2 === 0;
|
|
51176
51217
|
if (value === 0 || value > 0 !== expectMark) ctx.addIssue({
|
|
@@ -51217,15 +51258,186 @@ function decodePronto(code) {
|
|
|
51217
51258
|
carrierHz: Math.round(1e6 / unitUs)
|
|
51218
51259
|
};
|
|
51219
51260
|
}
|
|
51220
|
-
/**
|
|
51221
|
-
|
|
51222
|
-
|
|
51223
|
-
|
|
51224
|
-
|
|
51225
|
-
|
|
51226
|
-
|
|
51227
|
-
|
|
51228
|
-
|
|
51261
|
+
/**
|
|
51262
|
+
* FastLZ level 1: a byte-oriented LZ77 stream. A control byte below 32 starts a
|
|
51263
|
+
* literal run of `ctrl + 1` bytes; otherwise it is a back-reference whose length
|
|
51264
|
+
* is `ctrl >> 5` (extended by one byte when 7) and whose distance is split
|
|
51265
|
+
* between the low 5 bits and the next byte.
|
|
51266
|
+
*/
|
|
51267
|
+
var MAX_DISTANCE = 8192;
|
|
51268
|
+
var MAX_LITERAL_RUN = 32;
|
|
51269
|
+
var MIN_MATCH = 3;
|
|
51270
|
+
var MAX_MATCH = 264;
|
|
51271
|
+
/**
|
|
51272
|
+
* Most bytes a decoded IR code may hold: two bytes per timing. A level-1 stream
|
|
51273
|
+
* can expand ~130x, so the decoder checks this bound before every write.
|
|
51274
|
+
*/
|
|
51275
|
+
var IR_DECOMPRESSED_MAX = IR_TIMINGS_MAX * 2;
|
|
51276
|
+
function fastlzDecompress(input) {
|
|
51277
|
+
const out = [];
|
|
51278
|
+
let ip = 0;
|
|
51279
|
+
const room = (count) => {
|
|
51280
|
+
if (out.length + count > IR_DECOMPRESSED_MAX) throw new IrCodecError("tuya-malformed", "decompressed code is larger than any IR train");
|
|
51281
|
+
};
|
|
51282
|
+
const next = () => {
|
|
51283
|
+
const byte = input[ip++];
|
|
51284
|
+
if (byte === void 0) throw new IrCodecError("tuya-malformed", "compressed stream is truncated");
|
|
51285
|
+
return byte;
|
|
51286
|
+
};
|
|
51287
|
+
while (ip < input.length) {
|
|
51288
|
+
const ctrl = next();
|
|
51289
|
+
if (ctrl < 32) {
|
|
51290
|
+
room(ctrl + 1);
|
|
51291
|
+
for (let i = 0; i <= ctrl; i++) out.push(next());
|
|
51292
|
+
continue;
|
|
51293
|
+
}
|
|
51294
|
+
let len = (ctrl >> 5) - 1;
|
|
51295
|
+
if (len === 6) len += next();
|
|
51296
|
+
const distance = ((ctrl & 31) << 8) + next() + 1;
|
|
51297
|
+
const start = out.length - distance;
|
|
51298
|
+
if (start < 0) throw new IrCodecError("tuya-malformed", "back-reference points before the stream");
|
|
51299
|
+
room(len + 3);
|
|
51300
|
+
for (let i = 0; i < len + 3; i++) {
|
|
51301
|
+
const byte = out[start + i];
|
|
51302
|
+
if (byte === void 0) throw new IrCodecError("tuya-malformed", "back-reference is invalid");
|
|
51303
|
+
out.push(byte);
|
|
51304
|
+
}
|
|
51305
|
+
}
|
|
51306
|
+
return Uint8Array.from(out);
|
|
51307
|
+
}
|
|
51308
|
+
function fastlzCompress(input) {
|
|
51309
|
+
const out = [];
|
|
51310
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
51311
|
+
let literalStart = 0;
|
|
51312
|
+
const flushLiterals = (end) => {
|
|
51313
|
+
for (let from = literalStart; from < end; from += MAX_LITERAL_RUN) {
|
|
51314
|
+
const to = Math.min(from + MAX_LITERAL_RUN, end);
|
|
51315
|
+
out.push(to - from - 1);
|
|
51316
|
+
for (let i = from; i < to; i++) out.push(input[i] ?? 0);
|
|
51317
|
+
}
|
|
51318
|
+
};
|
|
51319
|
+
let pos = 0;
|
|
51320
|
+
while (pos < input.length) {
|
|
51321
|
+
let matchLen = 0;
|
|
51322
|
+
let matchDistance = 0;
|
|
51323
|
+
if (pos + MIN_MATCH <= input.length) {
|
|
51324
|
+
const key = (input[pos] ?? 0) << 16 | (input[pos + 1] ?? 0) << 8 | (input[pos + 2] ?? 0);
|
|
51325
|
+
const candidate = lastSeen.get(key);
|
|
51326
|
+
lastSeen.set(key, pos);
|
|
51327
|
+
if (candidate !== void 0 && pos - candidate <= MAX_DISTANCE) {
|
|
51328
|
+
let len = 0;
|
|
51329
|
+
while (pos + len < input.length && len < MAX_MATCH && input[candidate + len] === input[pos + len]) len++;
|
|
51330
|
+
if (len >= MIN_MATCH) {
|
|
51331
|
+
matchLen = len;
|
|
51332
|
+
matchDistance = pos - candidate - 1;
|
|
51333
|
+
}
|
|
51334
|
+
}
|
|
51335
|
+
}
|
|
51336
|
+
if (matchLen === 0) {
|
|
51337
|
+
pos++;
|
|
51338
|
+
continue;
|
|
51339
|
+
}
|
|
51340
|
+
flushLiterals(pos);
|
|
51341
|
+
const enc = matchLen - 2;
|
|
51342
|
+
if (enc < 7) out.push(enc << 5 | matchDistance >> 8);
|
|
51343
|
+
else out.push(224 | matchDistance >> 8, enc - 7);
|
|
51344
|
+
out.push(matchDistance & 255);
|
|
51345
|
+
pos += matchLen;
|
|
51346
|
+
literalStart = pos;
|
|
51347
|
+
}
|
|
51348
|
+
flushLiterals(input.length);
|
|
51349
|
+
return Uint8Array.from(out);
|
|
51350
|
+
}
|
|
51351
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
51352
|
+
var MAX_DURATION_US = 65535;
|
|
51353
|
+
function toBase64(bytes) {
|
|
51354
|
+
let out = "";
|
|
51355
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
51356
|
+
const a = bytes[i] ?? 0;
|
|
51357
|
+
const b = bytes[i + 1] ?? 0;
|
|
51358
|
+
const c = bytes[i + 2] ?? 0;
|
|
51359
|
+
const word = a << 16 | b << 8 | c;
|
|
51360
|
+
out += ALPHABET[word >> 18 & 63];
|
|
51361
|
+
out += ALPHABET[word >> 12 & 63];
|
|
51362
|
+
out += i + 1 < bytes.length ? ALPHABET[word >> 6 & 63] : "=";
|
|
51363
|
+
out += i + 2 < bytes.length ? ALPHABET[word & 63] : "=";
|
|
51364
|
+
}
|
|
51365
|
+
return out;
|
|
51366
|
+
}
|
|
51367
|
+
function fromBase64(text) {
|
|
51368
|
+
const clean = text.trim().replace(/=+$/, "");
|
|
51369
|
+
if (clean.length === 0 || clean.length % 4 === 1) throw new IrCodecError("tuya-malformed", "not a base64 string");
|
|
51370
|
+
const out = [];
|
|
51371
|
+
let bits = 0;
|
|
51372
|
+
let acc = 0;
|
|
51373
|
+
for (const ch of clean) {
|
|
51374
|
+
const value = ALPHABET.indexOf(ch);
|
|
51375
|
+
if (value < 0) throw new IrCodecError("tuya-malformed", "not a base64 string");
|
|
51376
|
+
acc = acc << 6 | value;
|
|
51377
|
+
bits += 6;
|
|
51378
|
+
if (bits >= 8) {
|
|
51379
|
+
bits -= 8;
|
|
51380
|
+
out.push(acc >> bits & 255);
|
|
51381
|
+
}
|
|
51382
|
+
}
|
|
51383
|
+
return Uint8Array.from(out);
|
|
51384
|
+
}
|
|
51385
|
+
/** Signed pulse train to a Tuya IR code: base64(FastLZ(uint16 LE durations)). */
|
|
51386
|
+
function encodeTuya(timings) {
|
|
51387
|
+
const bytes = new Uint8Array(timings.length * 2);
|
|
51388
|
+
timings.forEach((value, index) => {
|
|
51389
|
+
const duration = Math.abs(value);
|
|
51390
|
+
if (duration === 0 || duration > MAX_DURATION_US) throw new IrCodecError("invalid-timings", `timing ${index} does not fit 16 bits`);
|
|
51391
|
+
bytes[index * 2] = duration & 255;
|
|
51392
|
+
bytes[index * 2 + 1] = duration >> 8;
|
|
51393
|
+
});
|
|
51394
|
+
return toBase64(fastlzCompress(bytes));
|
|
51395
|
+
}
|
|
51396
|
+
/** A Tuya IR code to a signed pulse train (mark first). Throws `IrCodecError`. */
|
|
51397
|
+
function decodeTuya(code) {
|
|
51398
|
+
const bytes = fastlzDecompress(fromBase64(code));
|
|
51399
|
+
if (bytes.length === 0 || bytes.length % 2 !== 0) throw new IrCodecError("tuya-malformed", "payload is not a list of 16-bit durations");
|
|
51400
|
+
const timings = [];
|
|
51401
|
+
for (let i = 0; i < bytes.length; i += 2) {
|
|
51402
|
+
const duration = (bytes[i] ?? 0) | (bytes[i + 1] ?? 0) << 8;
|
|
51403
|
+
timings.push(i / 2 % 2 === 0 ? duration : -duration);
|
|
51404
|
+
}
|
|
51405
|
+
const checked = IrTimingsSchema.safeParse(timings);
|
|
51406
|
+
if (!checked.success) throw new IrCodecError("tuya-malformed", checked.error.message);
|
|
51407
|
+
return checked.data;
|
|
51408
|
+
}
|
|
51409
|
+
/** Device-native IR code formats a transmitter can replay verbatim. */
|
|
51410
|
+
var IrNativeFormatSchema = _enum(["tuya"]);
|
|
51411
|
+
/**
|
|
51412
|
+
* Longest native code accepted: base64 of the worst-case compressed stream
|
|
51413
|
+
* (every decoded byte as a literal, plus one control byte per 32), with margin.
|
|
51414
|
+
*/
|
|
51415
|
+
var IR_NATIVE_CODE_MAX_CHARS = Math.ceil((IR_DECOMPRESSED_MAX + Math.ceil(IR_DECOMPRESSED_MAX / 32)) / 3) * 4 + 256;
|
|
51416
|
+
var IrNativeCodeSchema = object({
|
|
51417
|
+
format: IrNativeFormatSchema,
|
|
51418
|
+
code: string().min(1).max(IR_NATIVE_CODE_MAX_CHARS)
|
|
51419
|
+
});
|
|
51420
|
+
var IR_NATIVE_CODECS = { tuya: {
|
|
51421
|
+
encode: encodeTuya,
|
|
51422
|
+
decode: decodeTuya
|
|
51423
|
+
} };
|
|
51424
|
+
/** What a remote command stores: a raw pulse train, a learned Pronto code, or a device-native code. */
|
|
51425
|
+
var IrPayloadSchema = discriminatedUnion("kind", [
|
|
51426
|
+
object({
|
|
51427
|
+
kind: literal("raw"),
|
|
51428
|
+
timings: array(number$1().int()),
|
|
51429
|
+
settings: TransmitSettingsSchema.optional()
|
|
51430
|
+
}),
|
|
51431
|
+
object({
|
|
51432
|
+
kind: literal("pronto"),
|
|
51433
|
+
code: string()
|
|
51434
|
+
}),
|
|
51435
|
+
object({
|
|
51436
|
+
kind: literal("native"),
|
|
51437
|
+
format: IrNativeFormatSchema,
|
|
51438
|
+
code: string()
|
|
51439
|
+
})
|
|
51440
|
+
]);
|
|
51229
51441
|
/** Resolve a payload to what `ir-transmitter.send` takes. Throws `IrCodecError`. */
|
|
51230
51442
|
function toTimings(payload) {
|
|
51231
51443
|
if (payload.kind === "pronto") {
|
|
@@ -51235,6 +51447,10 @@ function toTimings(payload) {
|
|
|
51235
51447
|
settings: { carrierHz: decoded.carrierHz }
|
|
51236
51448
|
};
|
|
51237
51449
|
}
|
|
51450
|
+
if (payload.kind === "native") return {
|
|
51451
|
+
timings: IR_NATIVE_CODECS[payload.format].decode(payload.code),
|
|
51452
|
+
settings: {}
|
|
51453
|
+
};
|
|
51238
51454
|
const checked = IrTimingsSchema.safeParse(payload.timings);
|
|
51239
51455
|
if (!checked.success) throw new IrCodecError("invalid-timings", checked.error.message);
|
|
51240
51456
|
return {
|
|
@@ -58994,6 +59210,15 @@ var HeroRowModelSchema = object({
|
|
|
58994
59210
|
extras: array(HeroControlSchema).max(2).optional(),
|
|
58995
59211
|
trailing: HeroFactSchema.nullable()
|
|
58996
59212
|
});
|
|
59213
|
+
new Set([
|
|
59214
|
+
"temperature",
|
|
59215
|
+
"humidity",
|
|
59216
|
+
"pressure",
|
|
59217
|
+
"illuminance",
|
|
59218
|
+
"reading",
|
|
59219
|
+
"air-quality",
|
|
59220
|
+
"valve"
|
|
59221
|
+
]);
|
|
58997
59222
|
DeviceType.Climate;
|
|
58998
59223
|
DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
|
|
58999
59224
|
/**
|
|
@@ -60082,13 +60307,32 @@ var IrSendResultSchema = object({
|
|
|
60082
60307
|
var IrTransmitterInfoSchema = object({
|
|
60083
60308
|
media: array(IrMediumSchema),
|
|
60084
60309
|
settable: array(TransmitSettingKeySchema),
|
|
60085
|
-
maxTimings: number$1().int().positive()
|
|
60310
|
+
maxTimings: number$1().int().positive(),
|
|
60311
|
+
/** The transmitter can learn a code (`learn`). */
|
|
60312
|
+
learn: boolean().default(false),
|
|
60313
|
+
/** Native code formats the transmitter replays verbatim (`send.native`). */
|
|
60314
|
+
formats: array(IrNativeFormatSchema).default([])
|
|
60315
|
+
});
|
|
60316
|
+
var IR_LEARN_TIMEOUT_DEFAULT_MS = 2e4;
|
|
60317
|
+
var IR_LEARN_TIMEOUT_MAX_MS = 6e4;
|
|
60318
|
+
var IrLearnResultSchema = object({
|
|
60319
|
+
/** The code in the device's own format, when it reports one. */
|
|
60320
|
+
native: IrNativeCodeSchema.nullable(),
|
|
60321
|
+
timings: IrTimingsSchema,
|
|
60322
|
+
carrierHz: number$1().int().positive().nullable()
|
|
60086
60323
|
});
|
|
60087
60324
|
DeviceType.Generic, method(object({
|
|
60088
60325
|
deviceId: number$1().int().nonnegative(),
|
|
60089
|
-
timings: IrTimingsSchema,
|
|
60326
|
+
timings: IrTimingsSchema.optional(),
|
|
60327
|
+
native: IrNativeCodeSchema.optional(),
|
|
60090
60328
|
settings: TransmitSettingsSchema.optional()
|
|
60091
|
-
}), IrSendResultSchema, {
|
|
60329
|
+
}).refine((input) => input.timings === void 0 !== (input.native === void 0), { message: "send takes exactly one of timings or native" }), IrSendResultSchema, {
|
|
60330
|
+
kind: "mutation",
|
|
60331
|
+
auth: "protected"
|
|
60332
|
+
}), method(object({
|
|
60333
|
+
deviceId: number$1().int().nonnegative(),
|
|
60334
|
+
timeoutMs: number$1().int().positive().max(IR_LEARN_TIMEOUT_MAX_MS).default(IR_LEARN_TIMEOUT_DEFAULT_MS)
|
|
60335
|
+
}), IrLearnResultSchema, {
|
|
60092
60336
|
kind: "mutation",
|
|
60093
60337
|
auth: "protected"
|
|
60094
60338
|
}), method(object({ deviceId: number$1().int().nonnegative() }), IrTransmitterInfoSchema, {
|
|
@@ -69874,6 +70118,7 @@ var NcSystemEventKindSchema = _enum([
|
|
|
69874
70118
|
"recording-degraded",
|
|
69875
70119
|
"device-attention-required",
|
|
69876
70120
|
"device-attention-resolved",
|
|
70121
|
+
"device-command",
|
|
69877
70122
|
"storage-location-low",
|
|
69878
70123
|
"storage-location-critical",
|
|
69879
70124
|
"storage-location-ok",
|
|
@@ -69936,11 +70181,6 @@ var NC_ALARM_SYSTEM_EVENT_KINDS = [
|
|
|
69936
70181
|
"alarm-sensor-bypass",
|
|
69937
70182
|
"alarm-disarmed"
|
|
69938
70183
|
];
|
|
69939
|
-
/**
|
|
69940
|
-
* One coherent system-event condition. `kinds` is the required opt-in safety
|
|
69941
|
-
* gate; the remaining lists are optional narrowing filters relevant to the
|
|
69942
|
-
* selected kinds.
|
|
69943
|
-
*/
|
|
69944
70184
|
var NcSystemEventConditionSchema = object({
|
|
69945
70185
|
kinds: array(NcSystemEventKindSchema).min(1),
|
|
69946
70186
|
deviceIds: array(number$1().int()).min(1).optional(),
|
|
@@ -69972,6 +70212,28 @@ var NcSystemEventConditionSchema = object({
|
|
|
69972
70212
|
nodeIds: array(string().min(1)).min(1).optional(),
|
|
69973
70213
|
packageNames: array(string().min(1)).min(1).optional(),
|
|
69974
70214
|
/**
|
|
70215
|
+
* `device-command` only: the vocabulary ACTION IDS (D983, `powerSource.forceRestart`)
|
|
70216
|
+
* this rule speaks about - the same dictionary the automation action blocks
|
|
70217
|
+
* pick from. The hub refuses an id the action index does not know.
|
|
70218
|
+
*/
|
|
70219
|
+
commandActions: array(string().min(1)).min(1).optional(),
|
|
70220
|
+
/** `device-command` only: how the command settled. A device's own change is not a command. */
|
|
70221
|
+
commandOutcomes: array(_enum([
|
|
70222
|
+
"confirmed",
|
|
70223
|
+
"already",
|
|
70224
|
+
"unconfirmed",
|
|
70225
|
+
"failed"
|
|
70226
|
+
])).min(1).optional(),
|
|
70227
|
+
/** `device-command` only: who asked (`user`, `addon`, `action`, `scheduler`, `composition`, `node`). */
|
|
70228
|
+
commandActorKinds: array(_enum([
|
|
70229
|
+
"user",
|
|
70230
|
+
"addon",
|
|
70231
|
+
"action",
|
|
70232
|
+
"scheduler",
|
|
70233
|
+
"composition",
|
|
70234
|
+
"node"
|
|
70235
|
+
])).min(1).optional(),
|
|
70236
|
+
/**
|
|
69975
70237
|
* At or below this remaining-life %, a consumable is LOW for THIS rule
|
|
69976
70238
|
* (D642). Absent = {@link NC_CONSUMABLE_LOW_PERCENT_DEFAULT}; at most
|
|
69977
70239
|
* {@link NC_CONSUMABLE_LOW_PERCENT_MAX}, so every episode can recover. Scoped to the
|
|
@@ -70952,6 +71214,13 @@ var NcRuleInputSchema = object({
|
|
|
70952
71214
|
*/
|
|
70953
71215
|
groupingSec: number$1().int().min(5).max(60).optional(),
|
|
70954
71216
|
/**
|
|
71217
|
+
* Hold deliveries of this rule to one target and, when two or more arrive
|
|
71218
|
+
* before the window ends, send them as one summary. Absent = the kind's
|
|
71219
|
+
* default (`groupWithinSecOf`): 5 s for a system-event rule, 0 for every
|
|
71220
|
+
* other kind. `0` sends each delivery as it does today.
|
|
71221
|
+
*/
|
|
71222
|
+
groupWithinSec: number$1().int().min(0).max(300).optional(),
|
|
71223
|
+
/**
|
|
70955
71224
|
* ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
|
|
70956
71225
|
* carries "Correct" / "Wrong" buttons (and, where the channel has room, a
|
|
70957
71226
|
* "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
|
|
@@ -71388,7 +71657,12 @@ var NcHistoryEntrySchema = object({
|
|
|
71388
71657
|
* Assistant export) must render as "no image right now", never as a
|
|
71389
71658
|
* broken link.
|
|
71390
71659
|
*/
|
|
71391
|
-
artifactIds: array(string().min(1)).optional()
|
|
71660
|
+
artifactIds: array(string().min(1)).optional(),
|
|
71661
|
+
/**
|
|
71662
|
+
* When this row was folded into a group-window summary, the outbox id of
|
|
71663
|
+
* that one notification. Absent on a row that was sent as itself.
|
|
71664
|
+
*/
|
|
71665
|
+
groupedInto: string().min(1).optional()
|
|
71392
71666
|
});
|
|
71393
71667
|
/**
|
|
71394
71668
|
* Query filter for `getHistory` (spec §4.2). Every field is a narrowing
|
|
@@ -91953,6 +92227,13 @@ Object.freeze({
|
|
|
91953
92227
|
access: "view",
|
|
91954
92228
|
role: "viewer"
|
|
91955
92229
|
},
|
|
92230
|
+
"irTransmitter.learn": {
|
|
92231
|
+
capName: "ir-transmitter",
|
|
92232
|
+
capScope: "device",
|
|
92233
|
+
addonId: null,
|
|
92234
|
+
access: "create",
|
|
92235
|
+
role: "viewer"
|
|
92236
|
+
},
|
|
91956
92237
|
"irTransmitter.send": {
|
|
91957
92238
|
capName: "ir-transmitter",
|
|
91958
92239
|
capScope: "device",
|
|
@@ -99804,6 +100085,11 @@ Object.freeze({
|
|
|
99804
100085
|
form: "single",
|
|
99805
100086
|
optional: false
|
|
99806
100087
|
}],
|
|
100088
|
+
"irTransmitter.learn": [{
|
|
100089
|
+
name: "deviceId",
|
|
100090
|
+
form: "single",
|
|
100091
|
+
optional: false
|
|
100092
|
+
}],
|
|
99807
100093
|
"irTransmitter.send": [{
|
|
99808
100094
|
name: "deviceId",
|
|
99809
100095
|
form: "single",
|
|
@@ -101743,7 +102029,10 @@ var NC_SYSTEM_EVENT_FILTER_KEYS = [
|
|
|
101743
102029
|
"deviceTypes",
|
|
101744
102030
|
"nodeIds",
|
|
101745
102031
|
"packageNames",
|
|
101746
|
-
"consumableLowPercent"
|
|
102032
|
+
"consumableLowPercent",
|
|
102033
|
+
"commandActions",
|
|
102034
|
+
"commandOutcomes",
|
|
102035
|
+
"commandActorKinds"
|
|
101747
102036
|
];
|
|
101748
102037
|
new Map(Object.entries({
|
|
101749
102038
|
"device-online": "device-events",
|
|
@@ -101766,6 +102055,7 @@ new Map(Object.entries({
|
|
|
101766
102055
|
"recording-degraded": "device-events",
|
|
101767
102056
|
"device-attention-required": "device-events",
|
|
101768
102057
|
"device-attention-resolved": "device-events",
|
|
102058
|
+
"device-command": "device-events",
|
|
101769
102059
|
"node-online": "system",
|
|
101770
102060
|
"node-offline": "system",
|
|
101771
102061
|
"node-restarting": "system",
|
|
@@ -101866,6 +102156,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
101866
102156
|
label: "Needs attention",
|
|
101867
102157
|
kinds: ["device-attention-required", "device-attention-resolved"]
|
|
101868
102158
|
},
|
|
102159
|
+
{
|
|
102160
|
+
id: "device-command",
|
|
102161
|
+
family: "device-events",
|
|
102162
|
+
label: "Commands given to devices",
|
|
102163
|
+
kinds: ["device-command"]
|
|
102164
|
+
},
|
|
101869
102165
|
{
|
|
101870
102166
|
id: "nodes",
|
|
101871
102167
|
family: "system",
|
|
@@ -102138,6 +102434,7 @@ var DEVICE_EVENTS = [
|
|
|
102138
102434
|
"recording-healthy",
|
|
102139
102435
|
"recording-degraded",
|
|
102140
102436
|
...DEVICE_ATTENTION,
|
|
102437
|
+
"device-command",
|
|
102141
102438
|
...ALARM
|
|
102142
102439
|
];
|
|
102143
102440
|
/** Every kind a summary may speak about — the two summarisable families. */
|
|
@@ -102291,6 +102588,9 @@ var STATE_SUMMARY_VARS = [
|
|
|
102291
102588
|
[...RULE_BODY_ONLY],
|
|
102292
102589
|
[...RULE_TEXT],
|
|
102293
102590
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
102591
|
+
[...RULE_TEXT],
|
|
102592
|
+
[...RULE_TEXT],
|
|
102593
|
+
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
102294
102594
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
102295
102595
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
102296
102596
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
@@ -105373,7 +105673,39 @@ var enpalConnectionsEntryKind = defineEntryKind("connections", {
|
|
|
105373
105673
|
});
|
|
105374
105674
|
/** Every kind this addon declares, keyed by its name. */
|
|
105375
105675
|
var enpalRecordKinds = { connections: enpalConnectionsEntryKind };
|
|
105676
|
+
var REJECTED_KINDS = new Set([
|
|
105677
|
+
"unauthorized",
|
|
105678
|
+
"token-missing",
|
|
105679
|
+
"empty"
|
|
105680
|
+
]);
|
|
105681
|
+
function connectionForTest(values) {
|
|
105682
|
+
const host = values["host"];
|
|
105683
|
+
if (typeof host !== "string" || host.trim().length === 0) return null;
|
|
105684
|
+
const parsed = enpalConnectionSchema.safeParse({ ...values });
|
|
105685
|
+
return parsed.success ? parsed.data : null;
|
|
105686
|
+
}
|
|
105687
|
+
function testFailure(error) {
|
|
105688
|
+
if (error instanceof EnpalError) return {
|
|
105689
|
+
success: false,
|
|
105690
|
+
status: REJECTED_KINDS.has(error.kind) ? "rejected" : "inconclusive",
|
|
105691
|
+
error: error.message
|
|
105692
|
+
};
|
|
105693
|
+
return {
|
|
105694
|
+
success: false,
|
|
105695
|
+
status: "inconclusive",
|
|
105696
|
+
error: "The Enpal box did not answer"
|
|
105697
|
+
};
|
|
105698
|
+
}
|
|
105699
|
+
function liveProbe(blazor) {
|
|
105700
|
+
return (connection) => readEnpalSnapshot({
|
|
105701
|
+
connection,
|
|
105702
|
+
fetch: globalThis.fetch,
|
|
105703
|
+
now: () => Date.now(),
|
|
105704
|
+
blazor: blazor?.()
|
|
105705
|
+
});
|
|
105706
|
+
}
|
|
105376
105707
|
function buildEnpalInstances(records, deps) {
|
|
105708
|
+
const probe = deps.probe ?? liveProbe(deps.blazor);
|
|
105377
105709
|
return new RecordIntegrationInstances(records, enpalConnectionsEntryKind, {
|
|
105378
105710
|
description: {
|
|
105379
105711
|
role: "devices",
|
|
@@ -105383,18 +105715,36 @@ function buildEnpalInstances(records, deps) {
|
|
|
105383
105715
|
},
|
|
105384
105716
|
instanceMode: "multiple",
|
|
105385
105717
|
kind: "server",
|
|
105386
|
-
canTest:
|
|
105718
|
+
canTest: true,
|
|
105387
105719
|
kinds: [{
|
|
105388
105720
|
id: "connection",
|
|
105389
105721
|
label: "Enpal",
|
|
105390
105722
|
functionCap: "device-provider",
|
|
105391
|
-
role: "devices"
|
|
105723
|
+
role: "devices",
|
|
105724
|
+
canTest: true
|
|
105392
105725
|
}]
|
|
105393
105726
|
},
|
|
105394
105727
|
formSchema: () => buildConnectionFormSchema(),
|
|
105395
105728
|
initialBody: (name) => ({ name }),
|
|
105396
105729
|
configured: (body) => (body.host ?? "").length > 0,
|
|
105397
|
-
onChanged: () => deps.onChanged()
|
|
105730
|
+
onChanged: () => deps.onChanged(),
|
|
105731
|
+
test: async ({ values }) => {
|
|
105732
|
+
const connection = connectionForTest(values);
|
|
105733
|
+
if (connection === null) return {
|
|
105734
|
+
success: false,
|
|
105735
|
+
status: "rejected",
|
|
105736
|
+
error: "Host is required"
|
|
105737
|
+
};
|
|
105738
|
+
try {
|
|
105739
|
+
await probe(connection);
|
|
105740
|
+
return {
|
|
105741
|
+
success: true,
|
|
105742
|
+
status: "validated"
|
|
105743
|
+
};
|
|
105744
|
+
} catch (error) {
|
|
105745
|
+
return testFailure(error);
|
|
105746
|
+
}
|
|
105747
|
+
}
|
|
105398
105748
|
});
|
|
105399
105749
|
}
|
|
105400
105750
|
/**
|
|
@@ -105687,7 +106037,10 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
105687
106037
|
...rowsDepsOver(api),
|
|
105688
106038
|
kind: enpalConnectionsEntryKind
|
|
105689
106039
|
}));
|
|
105690
|
-
this.instances = buildEnpalInstances(records, {
|
|
106040
|
+
this.instances = buildEnpalInstances(records, {
|
|
106041
|
+
onChanged: () => this.reconcileIntegrations(),
|
|
106042
|
+
blazor: () => this.blazorReader ?? void 0
|
|
106043
|
+
});
|
|
105691
106044
|
this.blazorReader = await this.createBlazorBridge();
|
|
105692
106045
|
this.clients = new EnpalClientRegistry(this.ctx.logger, {
|
|
105693
106046
|
blazor: () => this.blazorReader ?? void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-enpal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Enpal solar-box device-provider addon for CamStack — PV, battery, grid, house and heat-pump readings from the box's own web-page channel, its local InfluxDB or its device page",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
],
|
|
68
68
|
"requires": {
|
|
69
|
-
"server": ">=1.2.
|
|
69
|
+
"server": ">=1.2.849"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"files": [
|