@camstack/addon-provider-velux 0.2.153 → 0.2.155
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/dist/addon.js +584 -162
- package/dist/addon.mjs +584 -162
- package/package.json +2 -2
package/dist/addon.js
CHANGED
|
@@ -33,7 +33,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
//#endregion
|
|
34
34
|
let node_tls = require("node:tls");
|
|
35
35
|
let timers_promises = require("timers/promises");
|
|
36
|
-
//#region ../types/dist/event-category-
|
|
36
|
+
//#region ../types/dist/event-category-BmPm98n0.mjs
|
|
37
37
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
38
38
|
EventCategory["SystemBoot"] = "system.boot";
|
|
39
39
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -517,6 +517,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
517
517
|
EventCategory["RunningStarted"] = "running.started";
|
|
518
518
|
/** Mirrors `runningStateCapability.events.onFinished`. */
|
|
519
519
|
EventCategory["RunningFinished"] = "running.finished";
|
|
520
|
+
/** Mirrors `deviceRunsCapability.events.onRunOpened` - a durable run record opened. */
|
|
521
|
+
EventCategory["DeviceRunOpened"] = "device-run.opened";
|
|
522
|
+
/** Mirrors `deviceRunsCapability.events.onRunClosed` - a durable run record closed. */
|
|
523
|
+
EventCategory["DeviceRunClosed"] = "device-run.closed";
|
|
520
524
|
/**
|
|
521
525
|
* Cap event fired by every device that registers the `event-emitter`
|
|
522
526
|
* capability. Mirrors `eventEmitterCapability.events.onEvent`. Carries
|
|
@@ -5865,7 +5869,7 @@ var ZodIssueCode = {
|
|
|
5865
5869
|
var ZodFirstPartyTypeKind;
|
|
5866
5870
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5867
5871
|
//#endregion
|
|
5868
|
-
//#region ../types/dist/sleep-
|
|
5872
|
+
//#region ../types/dist/sleep-BKzhQTSm.mjs
|
|
5869
5873
|
/**
|
|
5870
5874
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5871
5875
|
* window to float samples (D455).
|
|
@@ -9378,6 +9382,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9378
9382
|
"device-ops",
|
|
9379
9383
|
"device-provider",
|
|
9380
9384
|
"device-removal-hook",
|
|
9385
|
+
"device-runs",
|
|
9381
9386
|
"device-state",
|
|
9382
9387
|
"device-status",
|
|
9383
9388
|
"dishwasher",
|
|
@@ -11322,7 +11327,7 @@ object({
|
|
|
11322
11327
|
params: record(string(), union([string(), number()])).optional()
|
|
11323
11328
|
});
|
|
11324
11329
|
//#endregion
|
|
11325
|
-
//#region ../types/dist/composition-
|
|
11330
|
+
//#region ../types/dist/composition-BiKWlUCc.mjs
|
|
11326
11331
|
/**
|
|
11327
11332
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11328
11333
|
*
|
|
@@ -12255,7 +12260,15 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12255
12260
|
thresholdWatts: number().positive(),
|
|
12256
12261
|
startDwellMs: number().int().nonnegative(),
|
|
12257
12262
|
stopDwellMs: number().int().nonnegative(),
|
|
12258
|
-
member: HelperMemberSchema
|
|
12263
|
+
member: HelperMemberSchema,
|
|
12264
|
+
/**
|
|
12265
|
+
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12266
|
+
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12267
|
+
* that plug (power-source link of a NEW-target composition).
|
|
12268
|
+
*/
|
|
12269
|
+
powerSwitch: HelperMemberSchema.optional(),
|
|
12270
|
+
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12271
|
+
offSeconds: number().optional()
|
|
12259
12272
|
}),
|
|
12260
12273
|
object({
|
|
12261
12274
|
kind: literal("statistic"),
|
|
@@ -13653,7 +13666,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
13653
13666
|
/** Why it is `blocked` or `held` (`dependency-failed`, `snapshot-refused`, …). */
|
|
13654
13667
|
reason: string().nullable(),
|
|
13655
13668
|
nodeId: string().nullable(),
|
|
13656
|
-
bootId: string().nullable()
|
|
13669
|
+
bootId: string().nullable(),
|
|
13670
|
+
/**
|
|
13671
|
+
* The catalogued meta of this id (description, body kind, store, `dependsOn`…),
|
|
13672
|
+
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
13673
|
+
* catalogue lists the id (a newer database).
|
|
13674
|
+
*/
|
|
13675
|
+
meta: MigrationMetaSchema.optional()
|
|
13657
13676
|
});
|
|
13658
13677
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
13659
13678
|
var MigrationPendingEntrySchema = object({
|
|
@@ -19460,6 +19479,14 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19460
19479
|
}
|
|
19461
19480
|
} }
|
|
19462
19481
|
},
|
|
19482
|
+
"device-runs": {
|
|
19483
|
+
scope: "device",
|
|
19484
|
+
noun: t("Runs", "Esecuzioni"),
|
|
19485
|
+
events: {
|
|
19486
|
+
onRunOpened: { notOperatorFacing: "the durable record of a run that the running-state start flip already announces" },
|
|
19487
|
+
onRunClosed: { notOperatorFacing: "the durable record of a run that the running-state finish flip already announces" }
|
|
19488
|
+
}
|
|
19489
|
+
},
|
|
19463
19490
|
update: {
|
|
19464
19491
|
scope: "device",
|
|
19465
19492
|
noun: t("Firmware update", "Aggiornamento firmware"),
|
|
@@ -46042,6 +46069,117 @@ function actuator(input) {
|
|
|
46042
46069
|
};
|
|
46043
46070
|
}
|
|
46044
46071
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
46072
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
46073
|
+
"no-power-source",
|
|
46074
|
+
"switch-missing",
|
|
46075
|
+
"switch-offline",
|
|
46076
|
+
"switch-unknown",
|
|
46077
|
+
"restart-in-progress",
|
|
46078
|
+
"switch-call-failed"
|
|
46079
|
+
]);
|
|
46080
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
46081
|
+
accepted: literal(false),
|
|
46082
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
46083
|
+
/** One line for the operator, naming the switch. */
|
|
46084
|
+
message: string()
|
|
46085
|
+
})]);
|
|
46086
|
+
var PowerSourceInfoSchema = object({
|
|
46087
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
46088
|
+
linked: boolean(),
|
|
46089
|
+
switchDeviceId: number().nullable(),
|
|
46090
|
+
switchName: string().nullable(),
|
|
46091
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
46092
|
+
switchOn: boolean().nullable(),
|
|
46093
|
+
switchOnline: boolean().nullable(),
|
|
46094
|
+
restartInProgress: boolean(),
|
|
46095
|
+
offSeconds: number().int()
|
|
46096
|
+
});
|
|
46097
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
46098
|
+
var PowerSourceStateSchema = object({
|
|
46099
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
46100
|
+
switchOn: boolean().nullable(),
|
|
46101
|
+
offSeconds: number().int(),
|
|
46102
|
+
lastFetchedAt: number()
|
|
46103
|
+
});
|
|
46104
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
46105
|
+
DeviceType.Gateway,
|
|
46106
|
+
DeviceType.Hub,
|
|
46107
|
+
DeviceType.Camera,
|
|
46108
|
+
DeviceType.Sensor,
|
|
46109
|
+
DeviceType.MediaPlayer,
|
|
46110
|
+
DeviceType.Vacuum,
|
|
46111
|
+
DeviceType.Generic,
|
|
46112
|
+
DeviceType.Washer,
|
|
46113
|
+
DeviceType.Dryer,
|
|
46114
|
+
DeviceType.Dishwasher,
|
|
46115
|
+
DeviceType.PetFountain,
|
|
46116
|
+
DeviceType.Fan,
|
|
46117
|
+
DeviceType.Humidifier,
|
|
46118
|
+
DeviceType.Climate,
|
|
46119
|
+
DeviceType.WaterHeater
|
|
46120
|
+
];
|
|
46121
|
+
var powerSourceCustomize = {
|
|
46122
|
+
version: 1,
|
|
46123
|
+
targets: [{
|
|
46124
|
+
id: "switch",
|
|
46125
|
+
cardinality: "scalar",
|
|
46126
|
+
vocabulary: {
|
|
46127
|
+
cap: "power-source",
|
|
46128
|
+
stateField: "switchOn"
|
|
46129
|
+
},
|
|
46130
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
46131
|
+
apply: "add",
|
|
46132
|
+
path: "switchOn",
|
|
46133
|
+
sources: [{
|
|
46134
|
+
kind: "switch",
|
|
46135
|
+
transforms: [{ kind: "identity" }]
|
|
46136
|
+
}],
|
|
46137
|
+
companions: [{
|
|
46138
|
+
kind: "constant",
|
|
46139
|
+
path: "offSeconds",
|
|
46140
|
+
value: 10,
|
|
46141
|
+
when: "add"
|
|
46142
|
+
}, {
|
|
46143
|
+
kind: "clock",
|
|
46144
|
+
path: "lastFetchedAt",
|
|
46145
|
+
when: "always"
|
|
46146
|
+
}],
|
|
46147
|
+
sourceReport: { status: "assignment-status" }
|
|
46148
|
+
}]
|
|
46149
|
+
};
|
|
46150
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
46151
|
+
var powerSourceCapability = {
|
|
46152
|
+
name: "power-source",
|
|
46153
|
+
scope: "device",
|
|
46154
|
+
mode: "singleton",
|
|
46155
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
46156
|
+
methods: {
|
|
46157
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
46158
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
46159
|
+
kind: "mutation",
|
|
46160
|
+
auth: "admin"
|
|
46161
|
+
}),
|
|
46162
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
46163
|
+
kind: "mutation",
|
|
46164
|
+
auth: "admin"
|
|
46165
|
+
}),
|
|
46166
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
46167
|
+
kind: "mutation",
|
|
46168
|
+
auth: "admin"
|
|
46169
|
+
})
|
|
46170
|
+
},
|
|
46171
|
+
runtimeState: PowerSourceStateSchema,
|
|
46172
|
+
customize: powerSourceCustomize,
|
|
46173
|
+
/**
|
|
46174
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
46175
|
+
* its next report; a restored value would outlive the reading it names.
|
|
46176
|
+
*
|
|
46177
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
46178
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
46179
|
+
*/
|
|
46180
|
+
durability: "session",
|
|
46181
|
+
history: "none"
|
|
46182
|
+
};
|
|
46045
46183
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
46046
46184
|
DeviceType.Camera;
|
|
46047
46185
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -47467,6 +47605,101 @@ method(object({ deviceId: number().int() }), array(SeriesInfoSchema).readonly())
|
|
|
47467
47605
|
kind: "mutation",
|
|
47468
47606
|
auth: "admin"
|
|
47469
47607
|
});
|
|
47608
|
+
/**
|
|
47609
|
+
* device-runs - the durable record of ONE run of a device: a washing cycle, a
|
|
47610
|
+
* dishwasher cycle, later a mow or a clean (D992).
|
|
47611
|
+
*
|
|
47612
|
+
* A run is a DOCUMENT (opened at one instant, closed at another, carrying what
|
|
47613
|
+
* was measured over it), never a sample - device-history keeps the numbers over
|
|
47614
|
+
* time and its `device-runs` provider keeps these documents beside them, because
|
|
47615
|
+
* the energy of a run is a question about device-history's own series. The
|
|
47616
|
+
* record is written from the `running-state` flip on `DeviceStateChanged`; it
|
|
47617
|
+
* survives a restart (the open row is on disk) and a missed flip (boot compares
|
|
47618
|
+
* the open rows against the live snapshots).
|
|
47619
|
+
*
|
|
47620
|
+
* Every measurement is `number | null` and `null` means "could not be
|
|
47621
|
+
* measured", never `0` (D393). A run closed by the boot reconcile because the
|
|
47622
|
+
* device stopped while nobody was watching has `outcome: 'unknown'`.
|
|
47623
|
+
*/
|
|
47624
|
+
var DeviceRunKindSchema = _enum([
|
|
47625
|
+
"appliance",
|
|
47626
|
+
"mower",
|
|
47627
|
+
"vacuum"
|
|
47628
|
+
]);
|
|
47629
|
+
var DeviceRunStateSchema = _enum(["open", "closed"]);
|
|
47630
|
+
var DeviceRunOutcomeSchema = _enum([
|
|
47631
|
+
"completed",
|
|
47632
|
+
"aborted",
|
|
47633
|
+
"error",
|
|
47634
|
+
"unknown"
|
|
47635
|
+
]);
|
|
47636
|
+
/** What opened or closed the run. `reconcile` is the boot comparison against live state. */
|
|
47637
|
+
var DeviceRunSourceSchema = _enum([
|
|
47638
|
+
"running-state",
|
|
47639
|
+
"activity",
|
|
47640
|
+
"reconcile"
|
|
47641
|
+
]);
|
|
47642
|
+
/** How the energy of a run was obtained; `null` when it could not be. */
|
|
47643
|
+
var DeviceRunEnergyMethodSchema = _enum(["counter-delta", "history-integral"]);
|
|
47644
|
+
var DeviceRunPhaseSchema = object({
|
|
47645
|
+
phase: string(),
|
|
47646
|
+
at: number()
|
|
47647
|
+
});
|
|
47648
|
+
/** The kind-specific block; a later phase adds a branch, nothing else changes. */
|
|
47649
|
+
var DeviceRunDetailSchema = discriminatedUnion("kind", [object({
|
|
47650
|
+
kind: literal("appliance"),
|
|
47651
|
+
/** kWh used over the run. */
|
|
47652
|
+
energyKwh: number().nullable().meta(readingMeta("energy")),
|
|
47653
|
+
energyMethod: DeviceRunEnergyMethodSchema.nullable(),
|
|
47654
|
+
/** Highest power seen while the run was open. */
|
|
47655
|
+
peakW: number().nullable().meta(readingMeta("power")),
|
|
47656
|
+
/** Time-weighted mean power while the run was open. */
|
|
47657
|
+
avgW: number().nullable().meta(readingMeta("power")),
|
|
47658
|
+
/** The program the appliance said it ran when the run opened. */
|
|
47659
|
+
program: string().nullable(),
|
|
47660
|
+
remainingAtStartSec: number().nullable().meta(readingMeta("duration")),
|
|
47661
|
+
/** The last phase the device reported before the run closed. */
|
|
47662
|
+
finalPhase: string().nullable(),
|
|
47663
|
+
/**
|
|
47664
|
+
* The meter's energy counter when the run opened; `null` when the meter had
|
|
47665
|
+
* not said (or the run was opened late, by the boot reconcile). It lives in
|
|
47666
|
+
* the row so a run that outlives a restart still ends with `end - start`.
|
|
47667
|
+
*/
|
|
47668
|
+
energyStartKwh: number().nullable().meta(readingMeta("energy")),
|
|
47669
|
+
/** Milliseconds of power readings behind `avgW` - the weight a restart resumes the mean with. */
|
|
47670
|
+
powerObservedMs: number()
|
|
47671
|
+
})]);
|
|
47672
|
+
var DeviceRunSchema = object({
|
|
47673
|
+
/** `${deviceId}:${startedAt}`. */
|
|
47674
|
+
runId: string(),
|
|
47675
|
+
deviceId: number().int(),
|
|
47676
|
+
kind: DeviceRunKindSchema,
|
|
47677
|
+
state: DeviceRunStateSchema,
|
|
47678
|
+
startedAt: number(),
|
|
47679
|
+
endedAt: number().nullable(),
|
|
47680
|
+
durationMs: number().nullable(),
|
|
47681
|
+
outcome: DeviceRunOutcomeSchema.nullable(),
|
|
47682
|
+
startSource: DeviceRunSourceSchema,
|
|
47683
|
+
endSource: DeviceRunSourceSchema.nullable(),
|
|
47684
|
+
/** Where the device's running bit came from (`running-state.source`); `null` when it did not say. */
|
|
47685
|
+
runningSource: _enum(["native", "assigned"]).nullable(),
|
|
47686
|
+
phases: array(DeviceRunPhaseSchema),
|
|
47687
|
+
detail: DeviceRunDetailSchema
|
|
47688
|
+
});
|
|
47689
|
+
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, method(object({
|
|
47690
|
+
deviceId: number().int(),
|
|
47691
|
+
outcome: DeviceRunOutcomeSchema.optional(),
|
|
47692
|
+
atFrom: number().optional(),
|
|
47693
|
+
atTo: number().optional(),
|
|
47694
|
+
limit: number().int().positive().max(200).default(50),
|
|
47695
|
+
cursor: string().optional()
|
|
47696
|
+
}), object({
|
|
47697
|
+
runs: array(DeviceRunSchema).readonly(),
|
|
47698
|
+
nextCursor: string().nullable()
|
|
47699
|
+
})), method(object({
|
|
47700
|
+
deviceId: number().int(),
|
|
47701
|
+
runId: string()
|
|
47702
|
+
}), DeviceRunSchema.nullable()), method(object({ deviceId: number().int() }), DeviceRunSchema.nullable());
|
|
47470
47703
|
var DeviceCaStatusSchema = discriminatedUnion("state", [
|
|
47471
47704
|
object({ state: literal("absent") }),
|
|
47472
47705
|
object({
|
|
@@ -47602,8 +47835,8 @@ method(object({}), object({
|
|
|
47602
47835
|
});
|
|
47603
47836
|
var KeychainKeySourceSchema = object({
|
|
47604
47837
|
kind: _enum([
|
|
47605
|
-
"
|
|
47606
|
-
"previous-
|
|
47838
|
+
"mounted-file",
|
|
47839
|
+
"previous-mounted-file",
|
|
47607
47840
|
"platform",
|
|
47608
47841
|
"default-file"
|
|
47609
47842
|
]),
|
|
@@ -52099,7 +52332,8 @@ var PALETTE_ENTRY_IDS = [
|
|
|
52099
52332
|
"switch",
|
|
52100
52333
|
"siren",
|
|
52101
52334
|
"button",
|
|
52102
|
-
"doorbell"
|
|
52335
|
+
"doorbell",
|
|
52336
|
+
"appliance-cycle"
|
|
52103
52337
|
];
|
|
52104
52338
|
var AUDIO_ENTRY_ID = "audio";
|
|
52105
52339
|
/** Six rungs, quiet → loud. The opaque end is the entry's hex. */
|
|
@@ -52170,7 +52404,8 @@ var CAMSTACK_HEX = {
|
|
|
52170
52404
|
switch: "#7a812a",
|
|
52171
52405
|
siren: "#ac5d7f",
|
|
52172
52406
|
button: "#b276cd",
|
|
52173
|
-
doorbell: "#0087ab"
|
|
52407
|
+
doorbell: "#0087ab",
|
|
52408
|
+
"appliance-cycle": "#2a8c8c"
|
|
52174
52409
|
};
|
|
52175
52410
|
var CAMSTACK_FALLBACK = [
|
|
52176
52411
|
"#b34dc7",
|
|
@@ -52235,7 +52470,8 @@ var HIGH_CONTRAST_HEX = {
|
|
|
52235
52470
|
switch: "#d05259",
|
|
52236
52471
|
siren: "#cd67b3",
|
|
52237
52472
|
button: "#c9746a",
|
|
52238
|
-
doorbell: "#338eeb"
|
|
52473
|
+
doorbell: "#338eeb",
|
|
52474
|
+
"appliance-cycle": "#00848a"
|
|
52239
52475
|
};
|
|
52240
52476
|
var HIGH_CONTRAST_FALLBACK = [
|
|
52241
52477
|
"#817f3b",
|
|
@@ -52300,7 +52536,8 @@ var OKABE_ITO_HEX = {
|
|
|
52300
52536
|
switch: "#ad2c4e",
|
|
52301
52537
|
siren: "#348e08",
|
|
52302
52538
|
button: "#d27303",
|
|
52303
|
-
doorbell: "#55770f"
|
|
52539
|
+
doorbell: "#55770f",
|
|
52540
|
+
"appliance-cycle": "#00737d"
|
|
52304
52541
|
};
|
|
52305
52542
|
var OKABE_ITO_FALLBACK = [
|
|
52306
52543
|
"#9a3784",
|
|
@@ -52509,6 +52746,7 @@ sub("switch", "control", "control", "switch", "Switch");
|
|
|
52509
52746
|
sub("siren", "control", "control", "siren", "Siren");
|
|
52510
52747
|
sub("button", "control", "control", "button", "Button");
|
|
52511
52748
|
sub("doorbell", "control", "control", "doorbell", "Doorbell");
|
|
52749
|
+
sub("appliance-cycle", "control", "control", "switch", "Appliance cycle");
|
|
52512
52750
|
for (const l of AUDIO_MACRO_LABELS) {
|
|
52513
52751
|
const kind = `audio-${l.id}`;
|
|
52514
52752
|
if (entries.has(kind)) continue;
|
|
@@ -61140,7 +61378,8 @@ var CAP_TO_KIND = {
|
|
|
61140
61378
|
cover: "cover",
|
|
61141
61379
|
switch: "switch",
|
|
61142
61380
|
button: "button",
|
|
61143
|
-
doorbell: "doorbell"
|
|
61381
|
+
doorbell: "doorbell",
|
|
61382
|
+
"running-state": "appliance-cycle"
|
|
61144
61383
|
};
|
|
61145
61384
|
function buildDescriptor(capName, kind) {
|
|
61146
61385
|
const t = EVENT_TAXONOMY[kind];
|
|
@@ -64270,10 +64509,7 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
|
|
|
64270
64509
|
}), InstallPackageResultSchema, {
|
|
64271
64510
|
kind: "mutation",
|
|
64272
64511
|
auth: "admin"
|
|
64273
|
-
}), method(object({
|
|
64274
|
-
kind: "mutation",
|
|
64275
|
-
auth: "admin"
|
|
64276
|
-
}), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({
|
|
64512
|
+
}), method(object({
|
|
64277
64513
|
packageName: string().min(1),
|
|
64278
64514
|
nodeId: string().optional(),
|
|
64279
64515
|
everywhere: boolean().optional()
|
|
@@ -65534,19 +65770,19 @@ var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status:
|
|
|
65534
65770
|
reason: _enum(["unknown-template", "not-a-media-player"]),
|
|
65535
65771
|
detail: string()
|
|
65536
65772
|
})]);
|
|
65537
|
-
var deviceInput
|
|
65773
|
+
var deviceInput = object({ deviceId: number().int().nonnegative() });
|
|
65538
65774
|
DeviceType.MediaPlayer;
|
|
65539
|
-
method(deviceInput
|
|
65775
|
+
method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
|
|
65540
65776
|
kind: "mutation",
|
|
65541
65777
|
auth: "admin"
|
|
65542
|
-
}), method(deviceInput
|
|
65778
|
+
}), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
|
|
65543
65779
|
role: RemoteRoleSchema,
|
|
65544
65780
|
/** Sends in a row (volume steps). */
|
|
65545
65781
|
repeat: number().int().min(1).max(50).default(1)
|
|
65546
65782
|
}), RemoteSendResultSchema, {
|
|
65547
65783
|
kind: "mutation",
|
|
65548
65784
|
auth: "protected"
|
|
65549
|
-
}), method(deviceInput
|
|
65785
|
+
}), method(deviceInput.extend({
|
|
65550
65786
|
command: RemoteMediaCommandSchema,
|
|
65551
65787
|
muted: boolean().optional(),
|
|
65552
65788
|
source: string().optional(),
|
|
@@ -66414,6 +66650,60 @@ onColorChanged: { data: object({
|
|
|
66414
66650
|
*/
|
|
66415
66651
|
history: "none"
|
|
66416
66652
|
};
|
|
66653
|
+
/**
|
|
66654
|
+
* `connection-test` — pre-creation validation of an integration's settings,
|
|
66655
|
+
* system-scoped collection (one provider per integration addon).
|
|
66656
|
+
*
|
|
66657
|
+
* ── Why this cap exists ─────────────────────────────────────────────────────
|
|
66658
|
+
*
|
|
66659
|
+
* Before it, `integrations.testConnection` had exactly two behaviours: a broker
|
|
66660
|
+
* branch (`settings.brokerId` → `broker.testConnection`) and a DEFAULT branch
|
|
66661
|
+
* that probed `settings.main_stream_url ?? settings.url` with ffprobe. Every
|
|
66662
|
+
* account-based integration — Dreo, Dreame, Tuya, Petkit, Wyze — fell into that
|
|
66663
|
+
* default and was told `{"success":false,"error":"No stream URL provided"}`.
|
|
66664
|
+
* That answer neither validated nor refused anything, so:
|
|
66665
|
+
*
|
|
66666
|
+
* - the Test button had NEVER worked for an account integration, and
|
|
66667
|
+
* - `integrations.create` had nothing to gate on, so an integration with a
|
|
66668
|
+
* wrong password was created happily and failed later, silently, at
|
|
66669
|
+
* reconcile time.
|
|
66670
|
+
*
|
|
66671
|
+
* The structural gap was that at CREATE time the integration does not exist
|
|
66672
|
+
* yet: there is no `integrationId`, no `brokerId`, no live client — and every
|
|
66673
|
+
* existing provider surface (`device-provider`, `device-adoption`) is keyed by
|
|
66674
|
+
* one of those. There was no way to ask "are these settings valid?" before the
|
|
66675
|
+
* row existed.
|
|
66676
|
+
*
|
|
66677
|
+
* ── The contract ────────────────────────────────────────────────────────────
|
|
66678
|
+
*
|
|
66679
|
+
* The provider knows how to validate its OWN settings. The framework does not
|
|
66680
|
+
* guess from the shape of the settings — a stream-URL probe is one provider's
|
|
66681
|
+
* implementation of this cap, not the universal fallback. A provider that does
|
|
66682
|
+
* not register this cap is a KNOWN "cannot validate", never a silent pass.
|
|
66683
|
+
*
|
|
66684
|
+
* `testSettings` takes the candidate settings blob exactly as the wizard's
|
|
66685
|
+
* config step collected it (the same blob that would be handed to
|
|
66686
|
+
* `integrations.create`), and MUST NOT persist anything, mint an integration,
|
|
66687
|
+
* or mutate live state. It opens a throwaway session, asks, and closes it.
|
|
66688
|
+
*
|
|
66689
|
+
* ── The three outcomes are NOT interchangeable ──────────────────────────────
|
|
66690
|
+
*
|
|
66691
|
+
* This is the whole point of the discriminated union: a `null` from a timeout
|
|
66692
|
+
* must never look like a `null` from a refusal.
|
|
66693
|
+
*
|
|
66694
|
+
* - `validated` — the remote ACCEPTED these credentials. Observed.
|
|
66695
|
+
* - `rejected` — the remote REFUSED these credentials. Observed.
|
|
66696
|
+
* This is the ONLY outcome that blocks creation.
|
|
66697
|
+
* - `inconclusive` — the check could not complete (DNS, timeout, 5xx, an
|
|
66698
|
+
* unexpected shape). NOTHING was observed about the
|
|
66699
|
+
* credentials. Never rendered, or counted, as a failure.
|
|
66700
|
+
*
|
|
66701
|
+
* A provider that cannot tell a refusal from a transport fault must return
|
|
66702
|
+
* `inconclusive`. Guessing `rejected` would block creation on a flaky network;
|
|
66703
|
+
* guessing `validated` would wave a wrong password through.
|
|
66704
|
+
*/
|
|
66705
|
+
/** Ceiling on how long a pre-creation probe may hold the wizard. */
|
|
66706
|
+
var CONNECTION_TEST_TIMEOUT_MS = 2e4;
|
|
66417
66707
|
var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
|
|
66418
66708
|
object({
|
|
66419
66709
|
outcome: literal("validated"),
|
|
@@ -69866,117 +70156,6 @@ var lockControlCapability = {
|
|
|
69866
70156
|
* whether persisting is worth a SQLite commit. */
|
|
69867
70157
|
volatileStateFields: ["lastChangedAt"]
|
|
69868
70158
|
};
|
|
69869
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
69870
|
-
"no-power-source",
|
|
69871
|
-
"switch-missing",
|
|
69872
|
-
"switch-offline",
|
|
69873
|
-
"switch-unknown",
|
|
69874
|
-
"restart-in-progress",
|
|
69875
|
-
"switch-call-failed"
|
|
69876
|
-
]);
|
|
69877
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
69878
|
-
accepted: literal(false),
|
|
69879
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
69880
|
-
/** One line for the operator, naming the switch. */
|
|
69881
|
-
message: string()
|
|
69882
|
-
})]);
|
|
69883
|
-
var PowerSourceInfoSchema = object({
|
|
69884
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
69885
|
-
linked: boolean(),
|
|
69886
|
-
switchDeviceId: number().nullable(),
|
|
69887
|
-
switchName: string().nullable(),
|
|
69888
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
69889
|
-
switchOn: boolean().nullable(),
|
|
69890
|
-
switchOnline: boolean().nullable(),
|
|
69891
|
-
restartInProgress: boolean(),
|
|
69892
|
-
offSeconds: number().int()
|
|
69893
|
-
});
|
|
69894
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
69895
|
-
var PowerSourceStateSchema = object({
|
|
69896
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
69897
|
-
switchOn: boolean().nullable(),
|
|
69898
|
-
offSeconds: number().int(),
|
|
69899
|
-
lastFetchedAt: number()
|
|
69900
|
-
});
|
|
69901
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
69902
|
-
DeviceType.Gateway,
|
|
69903
|
-
DeviceType.Hub,
|
|
69904
|
-
DeviceType.Camera,
|
|
69905
|
-
DeviceType.Sensor,
|
|
69906
|
-
DeviceType.MediaPlayer,
|
|
69907
|
-
DeviceType.Vacuum,
|
|
69908
|
-
DeviceType.Generic,
|
|
69909
|
-
DeviceType.Washer,
|
|
69910
|
-
DeviceType.Dryer,
|
|
69911
|
-
DeviceType.Dishwasher,
|
|
69912
|
-
DeviceType.PetFountain,
|
|
69913
|
-
DeviceType.Fan,
|
|
69914
|
-
DeviceType.Humidifier,
|
|
69915
|
-
DeviceType.Climate,
|
|
69916
|
-
DeviceType.WaterHeater
|
|
69917
|
-
];
|
|
69918
|
-
var powerSourceCustomize = {
|
|
69919
|
-
version: 1,
|
|
69920
|
-
targets: [{
|
|
69921
|
-
id: "switch",
|
|
69922
|
-
cardinality: "scalar",
|
|
69923
|
-
vocabulary: {
|
|
69924
|
-
cap: "power-source",
|
|
69925
|
-
stateField: "switchOn"
|
|
69926
|
-
},
|
|
69927
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
69928
|
-
apply: "add",
|
|
69929
|
-
path: "switchOn",
|
|
69930
|
-
sources: [{
|
|
69931
|
-
kind: "switch",
|
|
69932
|
-
transforms: [{ kind: "identity" }]
|
|
69933
|
-
}],
|
|
69934
|
-
companions: [{
|
|
69935
|
-
kind: "constant",
|
|
69936
|
-
path: "offSeconds",
|
|
69937
|
-
value: 10,
|
|
69938
|
-
when: "add"
|
|
69939
|
-
}, {
|
|
69940
|
-
kind: "clock",
|
|
69941
|
-
path: "lastFetchedAt",
|
|
69942
|
-
when: "always"
|
|
69943
|
-
}],
|
|
69944
|
-
sourceReport: { status: "assignment-status" }
|
|
69945
|
-
}]
|
|
69946
|
-
};
|
|
69947
|
-
var deviceInput = object({ deviceId: number().int().nonnegative() });
|
|
69948
|
-
var powerSourceCapability = {
|
|
69949
|
-
name: "power-source",
|
|
69950
|
-
scope: "device",
|
|
69951
|
-
mode: "singleton",
|
|
69952
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
69953
|
-
methods: {
|
|
69954
|
-
getStatus: systemMethod(deviceInput, PowerSourceInfoSchema, { auth: "admin" }),
|
|
69955
|
-
powerOn: systemMethod(deviceInput, PowerSourceActionResultSchema, {
|
|
69956
|
-
kind: "mutation",
|
|
69957
|
-
auth: "admin"
|
|
69958
|
-
}),
|
|
69959
|
-
powerOff: systemMethod(deviceInput, PowerSourceActionResultSchema, {
|
|
69960
|
-
kind: "mutation",
|
|
69961
|
-
auth: "admin"
|
|
69962
|
-
}),
|
|
69963
|
-
forceRestart: systemMethod(deviceInput, PowerSourceActionResultSchema, {
|
|
69964
|
-
kind: "mutation",
|
|
69965
|
-
auth: "admin"
|
|
69966
|
-
})
|
|
69967
|
-
},
|
|
69968
|
-
runtimeState: PowerSourceStateSchema,
|
|
69969
|
-
customize: powerSourceCustomize,
|
|
69970
|
-
/**
|
|
69971
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
69972
|
-
* its next report; a restored value would outlive the reading it names.
|
|
69973
|
-
*
|
|
69974
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
69975
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
69976
|
-
*/
|
|
69977
|
-
durability: "session",
|
|
69978
|
-
history: "none"
|
|
69979
|
-
};
|
|
69980
70159
|
/**
|
|
69981
70160
|
* A monotonic total between resets (D944). Energy uses `power-meter.energy`
|
|
69982
70161
|
* so dashboards keep one series; every other instantaneous reading lands here.
|
|
@@ -73518,6 +73697,10 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
|
|
|
73518
73697
|
/** Generic Customize target for the running boolean (D936). */
|
|
73519
73698
|
var runningCustomize = {
|
|
73520
73699
|
version: 1,
|
|
73700
|
+
overridable: [{
|
|
73701
|
+
path: "running",
|
|
73702
|
+
by: "running"
|
|
73703
|
+
}],
|
|
73521
73704
|
targets: [{
|
|
73522
73705
|
id: "running",
|
|
73523
73706
|
cardinality: "scalar",
|
|
@@ -73575,7 +73758,7 @@ var runningCustomize = {
|
|
|
73575
73758
|
kind: "constant",
|
|
73576
73759
|
path: "phase",
|
|
73577
73760
|
value: null,
|
|
73578
|
-
when: "
|
|
73761
|
+
when: "add"
|
|
73579
73762
|
},
|
|
73580
73763
|
{
|
|
73581
73764
|
kind: "clock",
|
|
@@ -76885,7 +77068,7 @@ var ExternalIdentityKeySchema = object({
|
|
|
76885
77068
|
provider: string().min(1),
|
|
76886
77069
|
subject: string().min(1)
|
|
76887
77070
|
});
|
|
76888
|
-
method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserInputSchema, UserSummarySchema, {
|
|
77071
|
+
method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), object({ defaultPasswordInUse: boolean() }), { auth: "admin" }), method(CreateUserInputSchema, UserSummarySchema, {
|
|
76889
77072
|
kind: "mutation",
|
|
76890
77073
|
auth: "admin",
|
|
76891
77074
|
access: "create"
|
|
@@ -80224,13 +80407,6 @@ Object.freeze({
|
|
|
80224
80407
|
access: "view",
|
|
80225
80408
|
role: "admin"
|
|
80226
80409
|
},
|
|
80227
|
-
"addons.installFromWorkspace": {
|
|
80228
|
-
capName: "addons",
|
|
80229
|
-
capScope: "system",
|
|
80230
|
-
addonId: null,
|
|
80231
|
-
access: "create",
|
|
80232
|
-
role: "admin"
|
|
80233
|
-
},
|
|
80234
80410
|
"addons.installPackage": {
|
|
80235
80411
|
capName: "addons",
|
|
80236
80412
|
capScope: "system",
|
|
@@ -80238,13 +80414,6 @@ Object.freeze({
|
|
|
80238
80414
|
access: "create",
|
|
80239
80415
|
role: "admin"
|
|
80240
80416
|
},
|
|
80241
|
-
"addons.isWorkspaceAvailable": {
|
|
80242
|
-
capName: "addons",
|
|
80243
|
-
capScope: "system",
|
|
80244
|
-
addonId: null,
|
|
80245
|
-
access: "view",
|
|
80246
|
-
role: "admin"
|
|
80247
|
-
},
|
|
80248
80417
|
"addons.list": {
|
|
80249
80418
|
capName: "addons",
|
|
80250
80419
|
capScope: "system",
|
|
@@ -80294,13 +80463,6 @@ Object.freeze({
|
|
|
80294
80463
|
access: "view",
|
|
80295
80464
|
role: "admin"
|
|
80296
80465
|
},
|
|
80297
|
-
"addons.listWorkspacePackages": {
|
|
80298
|
-
capName: "addons",
|
|
80299
|
-
capScope: "system",
|
|
80300
|
-
addonId: null,
|
|
80301
|
-
access: "view",
|
|
80302
|
-
role: "admin"
|
|
80303
|
-
},
|
|
80304
80466
|
"addons.onAddonLogs": {
|
|
80305
80467
|
capName: "addons",
|
|
80306
80468
|
capScope: "system",
|
|
@@ -82968,6 +83130,27 @@ Object.freeze({
|
|
|
82968
83130
|
access: "create",
|
|
82969
83131
|
role: "admin"
|
|
82970
83132
|
},
|
|
83133
|
+
"deviceRuns.getCurrentRun": {
|
|
83134
|
+
capName: "device-runs",
|
|
83135
|
+
capScope: "device",
|
|
83136
|
+
addonId: null,
|
|
83137
|
+
access: "view",
|
|
83138
|
+
role: "viewer"
|
|
83139
|
+
},
|
|
83140
|
+
"deviceRuns.getRun": {
|
|
83141
|
+
capName: "device-runs",
|
|
83142
|
+
capScope: "device",
|
|
83143
|
+
addonId: null,
|
|
83144
|
+
access: "view",
|
|
83145
|
+
role: "viewer"
|
|
83146
|
+
},
|
|
83147
|
+
"deviceRuns.listRuns": {
|
|
83148
|
+
capName: "device-runs",
|
|
83149
|
+
capScope: "device",
|
|
83150
|
+
addonId: null,
|
|
83151
|
+
access: "view",
|
|
83152
|
+
role: "viewer"
|
|
83153
|
+
},
|
|
82971
83154
|
"deviceState.claimFields": {
|
|
82972
83155
|
capName: "device-state",
|
|
82973
83156
|
capScope: "system",
|
|
@@ -89681,6 +89864,13 @@ Object.freeze({
|
|
|
89681
89864
|
access: "view",
|
|
89682
89865
|
role: "admin"
|
|
89683
89866
|
},
|
|
89867
|
+
"userManagement.getAdminBootstrapStatus": {
|
|
89868
|
+
capName: "user-management",
|
|
89869
|
+
capScope: "system",
|
|
89870
|
+
addonId: null,
|
|
89871
|
+
access: "view",
|
|
89872
|
+
role: "admin"
|
|
89873
|
+
},
|
|
89684
89874
|
"userManagement.getTotpStatus": {
|
|
89685
89875
|
capName: "user-management",
|
|
89686
89876
|
capScope: "system",
|
|
@@ -91166,6 +91356,21 @@ Object.freeze({
|
|
|
91166
91356
|
form: "single",
|
|
91167
91357
|
optional: false
|
|
91168
91358
|
}],
|
|
91359
|
+
"deviceRuns.getCurrentRun": [{
|
|
91360
|
+
name: "deviceId",
|
|
91361
|
+
form: "single",
|
|
91362
|
+
optional: false
|
|
91363
|
+
}],
|
|
91364
|
+
"deviceRuns.getRun": [{
|
|
91365
|
+
name: "deviceId",
|
|
91366
|
+
form: "single",
|
|
91367
|
+
optional: false
|
|
91368
|
+
}],
|
|
91369
|
+
"deviceRuns.listRuns": [{
|
|
91370
|
+
name: "deviceId",
|
|
91371
|
+
form: "single",
|
|
91372
|
+
optional: false
|
|
91373
|
+
}],
|
|
91169
91374
|
"deviceState.claimFields": [{
|
|
91170
91375
|
name: "deviceId",
|
|
91171
91376
|
form: "single",
|
|
@@ -93648,13 +93853,114 @@ NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
|
|
|
93648
93853
|
*
|
|
93649
93854
|
* Pure, RN-safe: the viewer imports the support rule from here.
|
|
93650
93855
|
*/
|
|
93856
|
+
/**
|
|
93857
|
+
* Control caps that still have a raise and a clear an operator names: an
|
|
93858
|
+
* appliance cycle raises when it STARTS and clears when it FINISHES. Kept out
|
|
93859
|
+
* of `SOURCE_CAP_ACTIVE_FIELD` on purpose — that table also makes a device a
|
|
93860
|
+
* camera-sensor / recorder-trigger source, and a dishwasher is neither.
|
|
93861
|
+
*/
|
|
93862
|
+
var CYCLE_RAISED_FIELD_BY_CAP = { "running-state": "running" };
|
|
93651
93863
|
/** Event kind → the boolean field of its cap slice that means "raised". */
|
|
93652
93864
|
var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.entries(EVENT_KIND_BY_CAP).flatMap(([capName, descriptor]) => {
|
|
93865
|
+
const cycleField = CYCLE_RAISED_FIELD_BY_CAP[capName];
|
|
93866
|
+
if (cycleField !== void 0) return [[descriptor.kind, cycleField]];
|
|
93653
93867
|
const field = SOURCE_CAP_ACTIVE_FIELD[capName];
|
|
93654
93868
|
if (field === void 0 || descriptor.category !== "sensor") return [];
|
|
93655
93869
|
return [[descriptor.kind, field]];
|
|
93656
93870
|
})));
|
|
93657
93871
|
Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
|
|
93872
|
+
Object.freeze({
|
|
93873
|
+
contact: {
|
|
93874
|
+
starts: {
|
|
93875
|
+
en: "opened",
|
|
93876
|
+
it: "si apre"
|
|
93877
|
+
},
|
|
93878
|
+
finishes: {
|
|
93879
|
+
en: "closed",
|
|
93880
|
+
it: "si chiude"
|
|
93881
|
+
}
|
|
93882
|
+
},
|
|
93883
|
+
"motion-sensor": {
|
|
93884
|
+
starts: {
|
|
93885
|
+
en: "motion detected",
|
|
93886
|
+
it: "movimento rilevato"
|
|
93887
|
+
},
|
|
93888
|
+
finishes: {
|
|
93889
|
+
en: "motion stopped",
|
|
93890
|
+
it: "movimento terminato"
|
|
93891
|
+
}
|
|
93892
|
+
},
|
|
93893
|
+
smoke: {
|
|
93894
|
+
starts: {
|
|
93895
|
+
en: "smoke detected",
|
|
93896
|
+
it: "fumo rilevato"
|
|
93897
|
+
},
|
|
93898
|
+
finishes: {
|
|
93899
|
+
en: "smoke gone",
|
|
93900
|
+
it: "fumo scomparso"
|
|
93901
|
+
}
|
|
93902
|
+
},
|
|
93903
|
+
flood: {
|
|
93904
|
+
starts: {
|
|
93905
|
+
en: "water detected",
|
|
93906
|
+
it: "acqua rilevata"
|
|
93907
|
+
},
|
|
93908
|
+
finishes: {
|
|
93909
|
+
en: "dry again",
|
|
93910
|
+
it: "di nuovo asciutto"
|
|
93911
|
+
}
|
|
93912
|
+
},
|
|
93913
|
+
gas: {
|
|
93914
|
+
starts: {
|
|
93915
|
+
en: "gas detected",
|
|
93916
|
+
it: "gas rilevato"
|
|
93917
|
+
},
|
|
93918
|
+
finishes: {
|
|
93919
|
+
en: "gas gone",
|
|
93920
|
+
it: "gas scomparso"
|
|
93921
|
+
}
|
|
93922
|
+
},
|
|
93923
|
+
"carbon-monoxide": {
|
|
93924
|
+
starts: {
|
|
93925
|
+
en: "carbon monoxide detected",
|
|
93926
|
+
it: "monossido rilevato"
|
|
93927
|
+
},
|
|
93928
|
+
finishes: {
|
|
93929
|
+
en: "carbon monoxide gone",
|
|
93930
|
+
it: "monossido scomparso"
|
|
93931
|
+
}
|
|
93932
|
+
},
|
|
93933
|
+
vibration: {
|
|
93934
|
+
starts: {
|
|
93935
|
+
en: "vibration detected",
|
|
93936
|
+
it: "vibrazione rilevata"
|
|
93937
|
+
},
|
|
93938
|
+
finishes: {
|
|
93939
|
+
en: "vibration stopped",
|
|
93940
|
+
it: "vibrazione terminata"
|
|
93941
|
+
}
|
|
93942
|
+
},
|
|
93943
|
+
tamper: {
|
|
93944
|
+
starts: {
|
|
93945
|
+
en: "tampered with",
|
|
93946
|
+
it: "manomesso"
|
|
93947
|
+
},
|
|
93948
|
+
finishes: {
|
|
93949
|
+
en: "tamper cleared",
|
|
93950
|
+
it: "manomissione rientrata"
|
|
93951
|
+
}
|
|
93952
|
+
},
|
|
93953
|
+
"appliance-cycle": {
|
|
93954
|
+
starts: {
|
|
93955
|
+
en: "the cycle started",
|
|
93956
|
+
it: "il ciclo parte"
|
|
93957
|
+
},
|
|
93958
|
+
finishes: {
|
|
93959
|
+
en: "the cycle finished",
|
|
93960
|
+
it: "il ciclo è terminato"
|
|
93961
|
+
}
|
|
93962
|
+
}
|
|
93963
|
+
});
|
|
93658
93964
|
object({
|
|
93659
93965
|
phase: _enum(["raised", "resolved"]),
|
|
93660
93966
|
episodeId: string().min(1),
|
|
@@ -93861,6 +94167,10 @@ var STATE_SUMMARY_VARS = [
|
|
|
93861
94167
|
[...RULE_TEXT],
|
|
93862
94168
|
[...RULE_TEXT],
|
|
93863
94169
|
[...RULE_TEXT],
|
|
94170
|
+
[...RULE_TEXT],
|
|
94171
|
+
[...RULE_TEXT],
|
|
94172
|
+
[...RULE_TEXT],
|
|
94173
|
+
[...RULE_TEXT],
|
|
93864
94174
|
[...RULE_BODY_ONLY],
|
|
93865
94175
|
[...RULE_TEXT],
|
|
93866
94176
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
@@ -93878,6 +94188,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
93878
94188
|
[...RULE_TEXT],
|
|
93879
94189
|
[...RULE_TEXT],
|
|
93880
94190
|
[...RULE_TEXT],
|
|
94191
|
+
[...RULE_TEXT],
|
|
93881
94192
|
([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
|
|
93882
94193
|
[...RULE_TEXT],
|
|
93883
94194
|
[...RULE_TEXT],
|
|
@@ -94122,6 +94433,7 @@ new Set([
|
|
|
94122
94433
|
"siren",
|
|
94123
94434
|
"button",
|
|
94124
94435
|
"doorbell",
|
|
94436
|
+
"appliance-cycle",
|
|
94125
94437
|
"audio-speech",
|
|
94126
94438
|
"audio-scream",
|
|
94127
94439
|
"audio-crying",
|
|
@@ -103431,8 +103743,90 @@ var VeluxClientRegistry = class {
|
|
|
103431
103743
|
};
|
|
103432
103744
|
//#endregion
|
|
103433
103745
|
//#region src/klf-probe.ts
|
|
103434
|
-
/**
|
|
103435
|
-
|
|
103746
|
+
/**
|
|
103747
|
+
* Defaults add up, worst case, to 12 + 3 + 1.5 + 1.5 = 18 s, which is the
|
|
103748
|
+
* wizard's `CONNECTION_TEST_TIMEOUT_MS` ceiling (20 s) minus the 2 s margin of
|
|
103749
|
+
* `DEFAULT_TEST_DEADLINE_MS`, so a stuck gateway is named here instead of the
|
|
103750
|
+
* wizard giving up unnamed. The handshake gets 12 s because a real KLF-200 needs
|
|
103751
|
+
* about 4.2 s for ONE handshake (TLSv1.2, DHE-RSA-AES256-SHA256 on a weak CPU).
|
|
103752
|
+
*
|
|
103753
|
+
* A probe opens exactly ONE connection: the gateway frees a closed slot very
|
|
103754
|
+
* slowly (measured 2026-11-01: a bare TCP connect and close before the TLS connect
|
|
103755
|
+
* left the real session without a handshake for 20 s), so no pre-check connects first.
|
|
103756
|
+
*/
|
|
103757
|
+
var DEFAULT_KLF_PROBE_TIMEOUTS_MS = {
|
|
103758
|
+
tls: 12e3,
|
|
103759
|
+
password: 3e3,
|
|
103760
|
+
version: 1500,
|
|
103761
|
+
logout: 1500
|
|
103762
|
+
};
|
|
103763
|
+
var PHASE_TEXT = {
|
|
103764
|
+
tls: "did not accept a connection or complete the TLS handshake on port 51200",
|
|
103765
|
+
password: "did not answer the password",
|
|
103766
|
+
version: "did not answer the version request",
|
|
103767
|
+
logout: "did not close the session"
|
|
103768
|
+
};
|
|
103769
|
+
/** A probe step ran out of time. Names the step, so a bound is never a silent hang. */
|
|
103770
|
+
var KlfProbeTimeoutError = class extends Error {
|
|
103771
|
+
phase;
|
|
103772
|
+
host;
|
|
103773
|
+
limitMs;
|
|
103774
|
+
constructor(phase, host, limitMs) {
|
|
103775
|
+
super(`The gateway ${host} ${PHASE_TEXT[phase]} within ${Math.round(limitMs / 100) / 10}s (timed out)`);
|
|
103776
|
+
this.phase = phase;
|
|
103777
|
+
this.host = host;
|
|
103778
|
+
this.limitMs = limitMs;
|
|
103779
|
+
this.name = "KlfProbeTimeoutError";
|
|
103780
|
+
}
|
|
103781
|
+
};
|
|
103782
|
+
async function withDeadline(work, ms, onTimeout) {
|
|
103783
|
+
let timer;
|
|
103784
|
+
try {
|
|
103785
|
+
return await Promise.race([work, new Promise((_resolve, reject) => {
|
|
103786
|
+
timer = setTimeout(() => reject(onTimeout()), ms);
|
|
103787
|
+
})]);
|
|
103788
|
+
} finally {
|
|
103789
|
+
clearTimeout(timer);
|
|
103790
|
+
}
|
|
103791
|
+
}
|
|
103792
|
+
/** How often the login watcher checks whether the handshake has finished. */
|
|
103793
|
+
var PHASE_POLL_MS = 20;
|
|
103794
|
+
/**
|
|
103795
|
+
* The library's `loginAsync` runs the TLS handshake and the password answer as one
|
|
103796
|
+
* call. `KLF200SocketProtocol` appears when the handshake is done and the password
|
|
103797
|
+
* is on its way, so watching it splits the call into two bounded, named steps.
|
|
103798
|
+
*/
|
|
103799
|
+
async function loginInPhases(connection, host, password, limits) {
|
|
103800
|
+
let handshakeDone = false;
|
|
103801
|
+
let passwordTimer;
|
|
103802
|
+
let tlsTimer;
|
|
103803
|
+
let poll;
|
|
103804
|
+
const tlsError = () => new KlfProbeTimeoutError("tls", host, limits.tls);
|
|
103805
|
+
const passwordError = () => new KlfProbeTimeoutError("password", host, limits.password);
|
|
103806
|
+
const bounds = new Promise((_resolve, reject) => {
|
|
103807
|
+
tlsTimer = setTimeout(() => reject(tlsError()), limits.tls);
|
|
103808
|
+
poll = setInterval(() => {
|
|
103809
|
+
if (handshakeDone || connection.KLF200SocketProtocol === void 0) return;
|
|
103810
|
+
handshakeDone = true;
|
|
103811
|
+
clearTimeout(tlsTimer);
|
|
103812
|
+
clearInterval(poll);
|
|
103813
|
+
passwordTimer = setTimeout(() => reject(passwordError()), limits.password);
|
|
103814
|
+
}, PHASE_POLL_MS);
|
|
103815
|
+
});
|
|
103816
|
+
const libraryS = (limits.tls + limits.password) / 1e3;
|
|
103817
|
+
try {
|
|
103818
|
+
await Promise.race([connection.loginAsync(password, libraryS), bounds]);
|
|
103819
|
+
} catch (err) {
|
|
103820
|
+
if (err instanceof KlfProbeTimeoutError) throw err;
|
|
103821
|
+
if (/^timeout$/i.test(errMsg(err)) || err instanceof Error && err.name === "TimeoutError") throw handshakeDone ? passwordError() : tlsError();
|
|
103822
|
+
throw err;
|
|
103823
|
+
} finally {
|
|
103824
|
+
clearTimeout(tlsTimer);
|
|
103825
|
+
clearTimeout(passwordTimer);
|
|
103826
|
+
clearInterval(poll);
|
|
103827
|
+
bounds.catch(() => void 0);
|
|
103828
|
+
}
|
|
103829
|
+
}
|
|
103436
103830
|
/**
|
|
103437
103831
|
* Opens ONE session, authenticates, reads the firmware version and closes the
|
|
103438
103832
|
* session again, whatever happens. The gateway accepts two sessions in total, so
|
|
@@ -103440,28 +103834,44 @@ var PROBE_LOGIN_TIMEOUT_S = 15;
|
|
|
103440
103834
|
* list. A refused password rejects with `VeluxLoginRefusedError`.
|
|
103441
103835
|
*/
|
|
103442
103836
|
async function probeKlfGateway(options) {
|
|
103837
|
+
const limits = {
|
|
103838
|
+
...DEFAULT_KLF_PROBE_TIMEOUTS_MS,
|
|
103839
|
+
...options.timeoutsMs
|
|
103840
|
+
};
|
|
103443
103841
|
const connection = options.tls === void 0 ? new Connection(options.host) : new Connection(options.host, options.tls);
|
|
103444
103842
|
try {
|
|
103445
103843
|
try {
|
|
103446
|
-
await connection.
|
|
103844
|
+
await loginInPhases(connection, options.host, options.password, limits);
|
|
103447
103845
|
} catch (err) {
|
|
103846
|
+
if (err instanceof KlfProbeTimeoutError) throw err;
|
|
103847
|
+
if (err instanceof Error && typeof Reflect.get(err, "code") === "string") throw err;
|
|
103448
103848
|
throw loginFailure(err);
|
|
103449
103849
|
}
|
|
103450
103850
|
try {
|
|
103451
|
-
const { SoftwareVersion } = await new Gateway(connection).getVersionAsync();
|
|
103851
|
+
const { SoftwareVersion } = await withDeadline(new Gateway(connection).getVersionAsync(), limits.version, () => new KlfProbeTimeoutError("version", options.host, limits.version));
|
|
103452
103852
|
return { firmware: SoftwareVersion.toString() };
|
|
103453
103853
|
} catch {
|
|
103454
103854
|
return { firmware: null };
|
|
103455
103855
|
}
|
|
103456
103856
|
} finally {
|
|
103457
103857
|
try {
|
|
103458
|
-
await connection.logoutAsync();
|
|
103858
|
+
await withDeadline(connection.logoutAsync(limits.logout / 1e3), limits.logout + 500, () => new KlfProbeTimeoutError("logout", options.host, limits.logout));
|
|
103459
103859
|
} catch (err) {
|
|
103460
103860
|
errMsg(err);
|
|
103861
|
+
await connection[Symbol.asyncDispose]().catch(() => void 0);
|
|
103461
103862
|
}
|
|
103462
103863
|
}
|
|
103463
103864
|
}
|
|
103464
103865
|
var TEST_LABEL = "Logs in to the gateway with this password, then closes the session";
|
|
103866
|
+
/** Under `CONNECTION_TEST_TIMEOUT_MS`, so the reason reaches the operator first. */
|
|
103867
|
+
var DEFAULT_TEST_DEADLINE_MS = CONNECTION_TEST_TIMEOUT_MS - 2e3;
|
|
103868
|
+
/** The whole test outlived its ceiling; no step of the probe answered in time. */
|
|
103869
|
+
var KlfTestDeadlineError = class extends Error {
|
|
103870
|
+
constructor(host, limitMs) {
|
|
103871
|
+
super(`The gateway ${host} did not answer within ${Math.round(limitMs / 1e3)}s (timed out)`);
|
|
103872
|
+
this.name = "KlfTestDeadlineError";
|
|
103873
|
+
}
|
|
103874
|
+
};
|
|
103465
103875
|
var UNREACHABLE_CODES = new Set([
|
|
103466
103876
|
"ECONNREFUSED",
|
|
103467
103877
|
"EHOSTUNREACH",
|
|
@@ -103483,6 +103893,10 @@ function classifyProbeFailure(err) {
|
|
|
103483
103893
|
kind: "wrong-password",
|
|
103484
103894
|
text: err.message
|
|
103485
103895
|
};
|
|
103896
|
+
if (err instanceof KlfProbeTimeoutError || err instanceof KlfTestDeadlineError) return {
|
|
103897
|
+
kind: "unreachable",
|
|
103898
|
+
text: err.message
|
|
103899
|
+
};
|
|
103486
103900
|
const text = typeof err === "string" ? err : errMsg(err);
|
|
103487
103901
|
const code = typeof err === "object" && err !== null ? Reflect.get(err, "code") : void 0;
|
|
103488
103902
|
if (typeof code === "string" && UNREACHABLE_CODES.has(code)) return {
|
|
@@ -103507,7 +103921,7 @@ function classifyProbeFailure(err) {
|
|
|
103507
103921
|
};
|
|
103508
103922
|
}
|
|
103509
103923
|
function buildVeluxConnectionTestProvider(deps) {
|
|
103510
|
-
const { logger, probe = probeKlfGateway, liveSessions = () => 0 } = deps;
|
|
103924
|
+
const { logger, probe = probeKlfGateway, liveSessions = () => 0, deadlineMs = DEFAULT_TEST_DEADLINE_MS } = deps;
|
|
103511
103925
|
/** The tail of the test queue per host: a second test waits for the first to log out. */
|
|
103512
103926
|
const queues = /* @__PURE__ */ new Map();
|
|
103513
103927
|
async function run(host, password) {
|
|
@@ -103523,10 +103937,13 @@ function buildVeluxConnectionTestProvider(deps) {
|
|
|
103523
103937
|
}
|
|
103524
103938
|
const startedAt = Date.now();
|
|
103525
103939
|
try {
|
|
103526
|
-
|
|
103940
|
+
let timer;
|
|
103941
|
+
const result = await Promise.race([probe({
|
|
103527
103942
|
host,
|
|
103528
103943
|
password
|
|
103529
|
-
})
|
|
103944
|
+
}), new Promise((_resolve, reject) => {
|
|
103945
|
+
timer = setTimeout(() => reject(new KlfTestDeadlineError(host, deadlineMs)), deadlineMs);
|
|
103946
|
+
})]).finally(() => clearTimeout(timer));
|
|
103530
103947
|
return {
|
|
103531
103948
|
outcome: "validated",
|
|
103532
103949
|
latencyMs: Date.now() - startedAt,
|
|
@@ -103761,7 +104178,12 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
103761
104178
|
],
|
|
103762
104179
|
customActions: veluxActions,
|
|
103763
104180
|
actionHandlers: { discoverGateway: async () => {
|
|
104181
|
+
const startedAt = Date.now();
|
|
103764
104182
|
const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
|
|
104183
|
+
this.ctx.logger.info("Velux gateway scan finished", { meta: {
|
|
104184
|
+
found: candidates.length,
|
|
104185
|
+
ms: Date.now() - startedAt
|
|
104186
|
+
} });
|
|
103765
104187
|
const hosts = candidates.map((candidate) => ({
|
|
103766
104188
|
host: String(candidate.prefilledConfig["host"] ?? ""),
|
|
103767
104189
|
name: candidate.suggestedName,
|