@camstack/addon-provider-bosch 0.1.28 → 0.1.30
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 +16 -1
- package/README.md +20 -13
- package/dist/{account-integration-offline-2MI94wOE.js → account-integration-offline-BMoYdiX2.js} +126 -1
- package/dist/{account-integration-offline-DKaazSyb.mjs → account-integration-offline-pv6cqOKu.mjs} +115 -2
- package/dist/{dist-DEZDBpGW.js → dist-BioIJ3NA.js} +1818 -126
- package/dist/{dist-Cbcvh8mB.mjs → dist-DBUnIovD.mjs} +1807 -127
- package/dist/home-connect/addon.js +349 -259
- package/dist/home-connect/addon.mjs +349 -259
- package/dist/homecom/addon.js +94 -12
- package/dist/homecom/addon.mjs +94 -12
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/i18n/en.json +15 -2
- package/i18n/it.json +15 -2
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require("node:crypto");
|
|
2
|
-
//#region ../types/dist/event-category-
|
|
2
|
+
//#region ../types/dist/event-category-hdMR7CTW.mjs
|
|
3
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
5
5
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -764,6 +764,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
764
764
|
*/
|
|
765
765
|
EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
|
|
766
766
|
/**
|
|
767
|
+
* An action run started or ended. Emitted by the `actions` surface.
|
|
768
|
+
* Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
|
|
769
|
+
*/
|
|
770
|
+
EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
|
|
771
|
+
/**
|
|
767
772
|
* Progress update from a running model conversion job.
|
|
768
773
|
* Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
|
|
769
774
|
* Emitted by `addon-model-studio` on the converting node.
|
|
@@ -938,7 +943,7 @@ var propertyKeyTypes = /* @__PURE__*/ new Set([
|
|
|
938
943
|
"number",
|
|
939
944
|
"symbol"
|
|
940
945
|
]);
|
|
941
|
-
function escapeRegex(str) {
|
|
946
|
+
function escapeRegex$1(str) {
|
|
942
947
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
943
948
|
}
|
|
944
949
|
function clone(inst, def, params) {
|
|
@@ -1668,7 +1673,7 @@ var $ZodCheckUpperCase = /*@__PURE__*/ $constructor("$ZodCheckUpperCase", (inst,
|
|
|
1668
1673
|
});
|
|
1669
1674
|
var $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, def) => {
|
|
1670
1675
|
$ZodCheck.init(inst, def);
|
|
1671
|
-
const escapedRegex = escapeRegex(def.includes);
|
|
1676
|
+
const escapedRegex = escapeRegex$1(def.includes);
|
|
1672
1677
|
const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
|
|
1673
1678
|
def.pattern = pattern;
|
|
1674
1679
|
inst._zod.onattach.push((inst) => {
|
|
@@ -1691,7 +1696,7 @@ var $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, d
|
|
|
1691
1696
|
});
|
|
1692
1697
|
var $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (inst, def) => {
|
|
1693
1698
|
$ZodCheck.init(inst, def);
|
|
1694
|
-
const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
|
|
1699
|
+
const pattern = new RegExp(`^${escapeRegex$1(def.prefix)}.*`);
|
|
1695
1700
|
def.pattern ?? (def.pattern = pattern);
|
|
1696
1701
|
inst._zod.onattach.push((inst) => {
|
|
1697
1702
|
const bag = inst._zod.bag;
|
|
@@ -1713,7 +1718,7 @@ var $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (ins
|
|
|
1713
1718
|
});
|
|
1714
1719
|
var $ZodCheckEndsWith = /*@__PURE__*/ $constructor("$ZodCheckEndsWith", (inst, def) => {
|
|
1715
1720
|
$ZodCheck.init(inst, def);
|
|
1716
|
-
const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
|
|
1721
|
+
const pattern = new RegExp(`.*${escapeRegex$1(def.suffix)}$`);
|
|
1717
1722
|
def.pattern ?? (def.pattern = pattern);
|
|
1718
1723
|
inst._zod.onattach.push((inst) => {
|
|
1719
1724
|
const bag = inst._zod.bag;
|
|
@@ -2949,7 +2954,7 @@ var $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
|
|
|
2949
2954
|
const values = getEnumValues(def.entries);
|
|
2950
2955
|
const valuesSet = new Set(values);
|
|
2951
2956
|
inst._zod.values = valuesSet;
|
|
2952
|
-
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
|
|
2957
|
+
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex$1(o) : o.toString()).join("|")})$`);
|
|
2953
2958
|
inst._zod.parse = (payload, _ctx) => {
|
|
2954
2959
|
const input = payload.value;
|
|
2955
2960
|
if (valuesSet.has(input)) return payload;
|
|
@@ -2967,7 +2972,7 @@ var $ZodLiteral = /*@__PURE__*/ $constructor("$ZodLiteral", (inst, def) => {
|
|
|
2967
2972
|
if (def.values.length === 0) throw new Error("Cannot create literal schema with no valid values");
|
|
2968
2973
|
const values = new Set(def.values);
|
|
2969
2974
|
inst._zod.values = values;
|
|
2970
|
-
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
|
|
2975
|
+
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex$1(o) : o ? escapeRegex$1(o.toString()) : String(o)).join("|")})$`);
|
|
2971
2976
|
inst._zod.parse = (payload, _ctx) => {
|
|
2972
2977
|
const input = payload.value;
|
|
2973
2978
|
if (values.has(input)) return payload;
|
|
@@ -5442,7 +5447,7 @@ var ZodIssueCode = {
|
|
|
5442
5447
|
var ZodFirstPartyTypeKind;
|
|
5443
5448
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5444
5449
|
//#endregion
|
|
5445
|
-
//#region ../types/dist/sleep-
|
|
5450
|
+
//#region ../types/dist/sleep-BHjNez61.mjs
|
|
5446
5451
|
/**
|
|
5447
5452
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5448
5453
|
* window to float samples (D455).
|
|
@@ -8588,6 +8593,8 @@ Object.freeze({ "doorbell": "lastPressedAt" });
|
|
|
8588
8593
|
/** Every capability name, so addon code can check a name without the schema graph. */
|
|
8589
8594
|
var CAP_NAMES = Object.freeze([
|
|
8590
8595
|
"accessories",
|
|
8596
|
+
"action-source",
|
|
8597
|
+
"actions",
|
|
8591
8598
|
"addon-pages",
|
|
8592
8599
|
"addon-pages-source",
|
|
8593
8600
|
"addon-routes",
|
|
@@ -8661,6 +8668,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
8661
8668
|
"filesystem-browse",
|
|
8662
8669
|
"flood",
|
|
8663
8670
|
"gas",
|
|
8671
|
+
"group-active",
|
|
8664
8672
|
"group-commands",
|
|
8665
8673
|
"group-status",
|
|
8666
8674
|
"heating-valve",
|
|
@@ -8683,6 +8691,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
8683
8691
|
"login-method",
|
|
8684
8692
|
"media-player",
|
|
8685
8693
|
"mesh-network",
|
|
8694
|
+
"meter",
|
|
8686
8695
|
"metrics-provider",
|
|
8687
8696
|
"model-convert",
|
|
8688
8697
|
"model-distributor",
|
|
@@ -9481,6 +9490,10 @@ Object.freeze({
|
|
|
9481
9490
|
"path": "detected",
|
|
9482
9491
|
"valueType": "boolean"
|
|
9483
9492
|
}],
|
|
9493
|
+
"group-active": [{
|
|
9494
|
+
"path": "active",
|
|
9495
|
+
"valueType": "boolean"
|
|
9496
|
+
}],
|
|
9484
9497
|
"group-status": [
|
|
9485
9498
|
{
|
|
9486
9499
|
"path": "included",
|
|
@@ -9740,6 +9753,10 @@ Object.freeze({
|
|
|
9740
9753
|
]
|
|
9741
9754
|
}
|
|
9742
9755
|
],
|
|
9756
|
+
"meter": [{
|
|
9757
|
+
"path": "value",
|
|
9758
|
+
"valueType": "number"
|
|
9759
|
+
}],
|
|
9743
9760
|
"motion": [
|
|
9744
9761
|
{
|
|
9745
9762
|
"path": "detected",
|
|
@@ -9880,6 +9897,18 @@ Object.freeze({
|
|
|
9880
9897
|
"path": "foodLevel",
|
|
9881
9898
|
"valueType": "number"
|
|
9882
9899
|
},
|
|
9900
|
+
{
|
|
9901
|
+
"path": "bowlFood",
|
|
9902
|
+
"valueType": "number"
|
|
9903
|
+
},
|
|
9904
|
+
{
|
|
9905
|
+
"path": "eating",
|
|
9906
|
+
"valueType": "boolean"
|
|
9907
|
+
},
|
|
9908
|
+
{
|
|
9909
|
+
"path": "supportsMarkRefilled",
|
|
9910
|
+
"valueType": "boolean"
|
|
9911
|
+
},
|
|
9883
9912
|
{
|
|
9884
9913
|
"path": "food1",
|
|
9885
9914
|
"valueType": "number"
|
|
@@ -10483,7 +10512,7 @@ object({
|
|
|
10483
10512
|
params: record(string(), union([string(), number()])).optional()
|
|
10484
10513
|
});
|
|
10485
10514
|
//#endregion
|
|
10486
|
-
//#region ../types/dist/err-msg-
|
|
10515
|
+
//#region ../types/dist/err-msg-DIorGYeC.mjs
|
|
10487
10516
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
10488
10517
|
DeviceType["Camera"] = "camera";
|
|
10489
10518
|
DeviceType["Hub"] = "hub";
|
|
@@ -10577,8 +10606,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
10577
10606
|
DeviceType["PetFeeder"] = "pet-feeder";
|
|
10578
10607
|
/** Built-in dishwasher — program lifecycle, door / remote-start gates,
|
|
10579
10608
|
* salt and rinse-aid levels. Installed with the `dishwasher` cap.
|
|
10580
|
-
* Sources: native Bosch Home Connect, reusable by similar integrations.
|
|
10609
|
+
* Sources: native Bosch Home Connect, reusable by similar integrations.
|
|
10610
|
+
* A plug-built dishwasher carries running-state and power instead. */
|
|
10581
10611
|
DeviceType["Dishwasher"] = "dishwasher";
|
|
10612
|
+
/** Washing machine. A plug-built one is running while its power stays
|
|
10613
|
+
* above a threshold. No program cap — that surface is the dishwasher's. */
|
|
10614
|
+
DeviceType["Washer"] = "washer";
|
|
10615
|
+
/** Tumble dryer. Same plug-built running surface as {@link Washer}. */
|
|
10616
|
+
DeviceType["Dryer"] = "dryer";
|
|
10582
10617
|
return DeviceType;
|
|
10583
10618
|
}({});
|
|
10584
10619
|
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
@@ -10836,7 +10871,7 @@ function errMsg(err) {
|
|
|
10836
10871
|
return String(err);
|
|
10837
10872
|
}
|
|
10838
10873
|
//#endregion
|
|
10839
|
-
//#region ../types/dist/composition-
|
|
10874
|
+
//#region ../types/dist/composition-Bb8iwHEJ.mjs
|
|
10840
10875
|
/**
|
|
10841
10876
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
10842
10877
|
*
|
|
@@ -11151,6 +11186,13 @@ var table = {
|
|
|
11151
11186
|
apply: () => {
|
|
11152
11187
|
throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
|
|
11153
11188
|
}
|
|
11189
|
+
},
|
|
11190
|
+
schmitt: {
|
|
11191
|
+
minArgs: 3,
|
|
11192
|
+
maxArgs: 5,
|
|
11193
|
+
apply: () => {
|
|
11194
|
+
throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
|
|
11195
|
+
}
|
|
11154
11196
|
}
|
|
11155
11197
|
};
|
|
11156
11198
|
Object.freeze(Object.assign(Object.create(null), table));
|
|
@@ -11592,6 +11634,99 @@ function validateExpressionSource(src) {
|
|
|
11592
11634
|
return null;
|
|
11593
11635
|
}
|
|
11594
11636
|
/**
|
|
11637
|
+
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
11638
|
+
*
|
|
11639
|
+
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
11640
|
+
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
11641
|
+
* a cycle, so running starts only after 30 s above the line.
|
|
11642
|
+
*
|
|
11643
|
+
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
11644
|
+
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
11645
|
+
* report "finished" in the middle of the program, so the stop dwell is
|
|
11646
|
+
* 10 minutes on every type. The number is stored per type so one machine
|
|
11647
|
+
* can move later without moving the others.
|
|
11648
|
+
*/
|
|
11649
|
+
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
11650
|
+
var APPLIANCE_KINDS = [
|
|
11651
|
+
"washer",
|
|
11652
|
+
"dryer",
|
|
11653
|
+
"dishwasher"
|
|
11654
|
+
];
|
|
11655
|
+
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
11656
|
+
/**
|
|
11657
|
+
* Statistics and integrals over a history read (D944).
|
|
11658
|
+
*
|
|
11659
|
+
* Pure: the same points always give the same answer, so a restart recomputes
|
|
11660
|
+
* rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
|
|
11661
|
+
* never the mean of the samples. A hole with no held value is not integrated
|
|
11662
|
+
* as zero. A truncated read is not published.
|
|
11663
|
+
*/
|
|
11664
|
+
var STATISTIC_STATS = [
|
|
11665
|
+
"min",
|
|
11666
|
+
"max",
|
|
11667
|
+
"mean",
|
|
11668
|
+
"sum",
|
|
11669
|
+
"count",
|
|
11670
|
+
"change"
|
|
11671
|
+
];
|
|
11672
|
+
/**
|
|
11673
|
+
* The range a statistics or integral helper asks history for (D944).
|
|
11674
|
+
*
|
|
11675
|
+
* Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
|
|
11676
|
+
* or 25 hours across a DST change. A tier is eligible only when its retention
|
|
11677
|
+
* is strictly longer than the longest that range can be. Forever qualifies.
|
|
11678
|
+
* Equal to a tier's retention is a refusal, not a short answer.
|
|
11679
|
+
*/
|
|
11680
|
+
var MINUTE_MS = 6e4;
|
|
11681
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
11682
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
11683
|
+
var ROLLING_SPANS = [
|
|
11684
|
+
"1h",
|
|
11685
|
+
"24h",
|
|
11686
|
+
"2d",
|
|
11687
|
+
"7d",
|
|
11688
|
+
"30d",
|
|
11689
|
+
"custom"
|
|
11690
|
+
];
|
|
11691
|
+
var CALENDAR_PERIODS = [
|
|
11692
|
+
"today",
|
|
11693
|
+
"yesterday",
|
|
11694
|
+
"this-week",
|
|
11695
|
+
"this-month"
|
|
11696
|
+
];
|
|
11697
|
+
var RESET_PERIODS = [
|
|
11698
|
+
"never",
|
|
11699
|
+
"daily",
|
|
11700
|
+
"weekly",
|
|
11701
|
+
"monthly"
|
|
11702
|
+
];
|
|
11703
|
+
5 * MINUTE_MS;
|
|
11704
|
+
24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
|
|
11705
|
+
7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
|
|
11706
|
+
var rollingWindowObject = object({
|
|
11707
|
+
kind: literal("rolling"),
|
|
11708
|
+
span: _enum(ROLLING_SPANS),
|
|
11709
|
+
customMinutes: number().int().min(5).optional()
|
|
11710
|
+
}).check((payload) => {
|
|
11711
|
+
if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
|
|
11712
|
+
code: "custom",
|
|
11713
|
+
message: "a custom range needs a duration in minutes",
|
|
11714
|
+
path: ["customMinutes"],
|
|
11715
|
+
input: payload.value
|
|
11716
|
+
});
|
|
11717
|
+
if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
|
|
11718
|
+
code: "custom",
|
|
11719
|
+
message: "only a custom range carries a duration",
|
|
11720
|
+
path: ["customMinutes"],
|
|
11721
|
+
input: payload.value
|
|
11722
|
+
});
|
|
11723
|
+
});
|
|
11724
|
+
var RollingWindowSchema = rollingWindowObject;
|
|
11725
|
+
var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
|
|
11726
|
+
kind: literal("calendar"),
|
|
11727
|
+
period: _enum(CALENDAR_PERIODS)
|
|
11728
|
+
})]);
|
|
11729
|
+
/**
|
|
11595
11730
|
* A composition source identity: `(addonId, stableId)`, never a numeric device
|
|
11596
11731
|
* id. Split out so helper schemas can name it without a cycle through
|
|
11597
11732
|
* `composition.ts` (D659).
|
|
@@ -11617,7 +11752,13 @@ var GROUP_TEMPLATE_IDS = [
|
|
|
11617
11752
|
"occupancy",
|
|
11618
11753
|
"binary",
|
|
11619
11754
|
"switch",
|
|
11620
|
-
"light"
|
|
11755
|
+
"light",
|
|
11756
|
+
"fan",
|
|
11757
|
+
"cover",
|
|
11758
|
+
"valve",
|
|
11759
|
+
"lock",
|
|
11760
|
+
"media-player",
|
|
11761
|
+
"climate"
|
|
11621
11762
|
];
|
|
11622
11763
|
var NUMERIC_AGGREGATES = [
|
|
11623
11764
|
"mean",
|
|
@@ -11647,14 +11788,73 @@ var HelperMemberSchema = object({
|
|
|
11647
11788
|
cap: string().min(1),
|
|
11648
11789
|
fieldPath: string().min(1)
|
|
11649
11790
|
});
|
|
11650
|
-
var HelperSpecSchema = discriminatedUnion("kind", [
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11791
|
+
var HelperSpecSchema = discriminatedUnion("kind", [
|
|
11792
|
+
object({
|
|
11793
|
+
kind: literal("group"),
|
|
11794
|
+
templateId: GroupTemplateIdSchema,
|
|
11795
|
+
aggregate: AggregateReducerSchema,
|
|
11796
|
+
members: array(HelperMemberSchema).min(1).max(64),
|
|
11797
|
+
/** Actuator groups only. Readings leave this unset. */
|
|
11798
|
+
active: ActiveCriterionSchema.optional()
|
|
11799
|
+
}),
|
|
11800
|
+
object({
|
|
11801
|
+
kind: literal("appliance"),
|
|
11802
|
+
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
11803
|
+
appliance: _enum(APPLIANCE_KINDS),
|
|
11804
|
+
thresholdWatts: number().positive(),
|
|
11805
|
+
startDwellMs: number().int().nonnegative(),
|
|
11806
|
+
stopDwellMs: number().int().nonnegative(),
|
|
11807
|
+
member: HelperMemberSchema
|
|
11808
|
+
}),
|
|
11809
|
+
object({
|
|
11810
|
+
kind: literal("statistic"),
|
|
11811
|
+
stat: _enum(STATISTIC_STATS),
|
|
11812
|
+
window: StatisticWindowSchema,
|
|
11813
|
+
source: HelperMemberSchema
|
|
11814
|
+
}),
|
|
11815
|
+
object({
|
|
11816
|
+
kind: literal("integral"),
|
|
11817
|
+
reset: _enum(RESET_PERIODS),
|
|
11818
|
+
output: _enum(["energy", "meter"]),
|
|
11819
|
+
source: HelperMemberSchema
|
|
11820
|
+
}),
|
|
11821
|
+
object({
|
|
11822
|
+
kind: literal("threshold"),
|
|
11823
|
+
source: HelperMemberSchema,
|
|
11824
|
+
high: number(),
|
|
11825
|
+
low: number(),
|
|
11826
|
+
onForMs: number().int().nonnegative(),
|
|
11827
|
+
offForMs: number().int().nonnegative()
|
|
11828
|
+
}),
|
|
11829
|
+
object({
|
|
11830
|
+
kind: literal("arithmetic"),
|
|
11831
|
+
op: _enum([
|
|
11832
|
+
"sum",
|
|
11833
|
+
"difference",
|
|
11834
|
+
"ratio"
|
|
11835
|
+
]),
|
|
11836
|
+
inputs: array(HelperMemberSchema).min(2).max(64)
|
|
11837
|
+
}),
|
|
11838
|
+
object({
|
|
11839
|
+
kind: literal("expression"),
|
|
11840
|
+
expr: string().min(1),
|
|
11841
|
+
inputs: array(object({
|
|
11842
|
+
name: string().min(1),
|
|
11843
|
+
member: HelperMemberSchema
|
|
11844
|
+
})).min(1).max(64),
|
|
11845
|
+
output: _enum(["number", "boolean"])
|
|
11846
|
+
}),
|
|
11847
|
+
object({
|
|
11848
|
+
kind: literal("derivative"),
|
|
11849
|
+
window: RollingWindowSchema,
|
|
11850
|
+
per: _enum([
|
|
11851
|
+
"second",
|
|
11852
|
+
"minute",
|
|
11853
|
+
"hour"
|
|
11854
|
+
]),
|
|
11855
|
+
source: HelperMemberSchema
|
|
11856
|
+
})
|
|
11857
|
+
]);
|
|
11658
11858
|
/** A composed device's stableId is this prefix plus the block id. */
|
|
11659
11859
|
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
11660
11860
|
/** Longest snippet a `code` source may carry (slice 4). */
|
|
@@ -11715,6 +11915,12 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
|
11715
11915
|
"all"
|
|
11716
11916
|
]),
|
|
11717
11917
|
reads: array(CompositionFieldReadSchema).min(1).max(64),
|
|
11918
|
+
/**
|
|
11919
|
+
* Set when the reads are an enum state, not a yes/no: the listed states
|
|
11920
|
+
* count as active (a cover is `open`). A state outside the list is inactive;
|
|
11921
|
+
* a read that is not a state at all is left out. Only `any` / `all` use it.
|
|
11922
|
+
*/
|
|
11923
|
+
activeWhen: array(string().min(1)).min(1).optional(),
|
|
11718
11924
|
/** `value` is the reading; the others are the member counts. */
|
|
11719
11925
|
publish: _enum([
|
|
11720
11926
|
"value",
|
|
@@ -11728,6 +11934,26 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
|
11728
11934
|
"omit",
|
|
11729
11935
|
"offline"
|
|
11730
11936
|
])
|
|
11937
|
+
}),
|
|
11938
|
+
object({
|
|
11939
|
+
kind: literal("series"),
|
|
11940
|
+
read: CompositionFieldReadSchema,
|
|
11941
|
+
/** `value` is the statistic or the integral. The others are the gap counts. */
|
|
11942
|
+
publish: _enum([
|
|
11943
|
+
"value",
|
|
11944
|
+
"included",
|
|
11945
|
+
"excluded",
|
|
11946
|
+
"total"
|
|
11947
|
+
]),
|
|
11948
|
+
/**
|
|
11949
|
+
* What an unanswered `value` does. Counts ignore this. `offline` leaves the
|
|
11950
|
+
* device unseeded until history answers, so a restart cannot publish 0.
|
|
11951
|
+
*/
|
|
11952
|
+
empty: _enum([
|
|
11953
|
+
"null",
|
|
11954
|
+
"omit",
|
|
11955
|
+
"offline"
|
|
11956
|
+
])
|
|
11731
11957
|
})
|
|
11732
11958
|
]);
|
|
11733
11959
|
var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
|
|
@@ -15239,7 +15465,23 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
15239
15465
|
kind: literal("schedule"),
|
|
15240
15466
|
cron: string().min(1).max(120)
|
|
15241
15467
|
}),
|
|
15242
|
-
object({ kind: literal("manual") })
|
|
15468
|
+
object({ kind: literal("manual") }),
|
|
15469
|
+
object({
|
|
15470
|
+
kind: literal("detection"),
|
|
15471
|
+
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
15472
|
+
* matches every raw class that maps to it; any other name matches
|
|
15473
|
+
* exactly. */
|
|
15474
|
+
classes: array(string().min(1)).min(1),
|
|
15475
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
15476
|
+
/** The track must have visited at least one of these zones. */
|
|
15477
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
15478
|
+
/** Best-so-far confidence the track must reach. */
|
|
15479
|
+
minScore: number().min(0).max(1).optional()
|
|
15480
|
+
}),
|
|
15481
|
+
object({
|
|
15482
|
+
kind: literal("interval"),
|
|
15483
|
+
minutes: number().int().min(1).max(1440)
|
|
15484
|
+
})
|
|
15243
15485
|
]);
|
|
15244
15486
|
/**
|
|
15245
15487
|
* One action step.
|
|
@@ -15259,28 +15501,47 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
15259
15501
|
* is called. The runner still receives a number and is untouched; the
|
|
15260
15502
|
* resolution is the recipe's job, not the runner's.
|
|
15261
15503
|
*/
|
|
15504
|
+
var AutomationWaitStepSchema = object({
|
|
15505
|
+
kind: literal("wait"),
|
|
15506
|
+
seconds: number().min(0).max(300)
|
|
15507
|
+
});
|
|
15508
|
+
var AutomationCapStepSchema = object({
|
|
15509
|
+
kind: literal("cap"),
|
|
15510
|
+
/**
|
|
15511
|
+
* One device, several same-family devices (locks / climate family / …),
|
|
15512
|
+
* or `{ $var }` for the device that triggered the run. An array is
|
|
15513
|
+
* fan-out at codegen — each id gets the same cap.method call.
|
|
15514
|
+
*/
|
|
15515
|
+
deviceId: union([
|
|
15516
|
+
number().int(),
|
|
15517
|
+
array(number().int()).min(1),
|
|
15518
|
+
object({ $var: string().min(1) })
|
|
15519
|
+
]),
|
|
15520
|
+
cap: string().min(1),
|
|
15521
|
+
method: string().min(1),
|
|
15522
|
+
/** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
|
|
15523
|
+
* SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
|
|
15524
|
+
* the expression leaf. */
|
|
15525
|
+
args: record(string(), unknown()).optional()
|
|
15526
|
+
});
|
|
15527
|
+
/**
|
|
15528
|
+
* The steps an internal Action may contain: cap and wait, and nothing that can
|
|
15529
|
+
* call back into an Action — so an Action can never reach itself (no cycles).
|
|
15530
|
+
*/
|
|
15531
|
+
var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
|
|
15262
15532
|
var AutomationActionSchema = discriminatedUnion("kind", [
|
|
15533
|
+
AutomationWaitStepSchema,
|
|
15534
|
+
AutomationCapStepSchema,
|
|
15263
15535
|
object({
|
|
15264
|
-
kind: literal("
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
*/
|
|
15274
|
-
deviceId: union([
|
|
15275
|
-
number().int(),
|
|
15276
|
-
array(number().int()).min(1),
|
|
15277
|
-
object({ $var: string().min(1) })
|
|
15278
|
-
]),
|
|
15279
|
-
cap: string().min(1),
|
|
15280
|
-
method: string().min(1),
|
|
15281
|
-
/** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
|
|
15282
|
-
* evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
|
|
15283
|
-
args: record(string(), unknown()).optional()
|
|
15536
|
+
kind: literal("action"),
|
|
15537
|
+
/** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
|
|
15538
|
+
actionId: string().min(1),
|
|
15539
|
+
/** Values for the action's declared variables; the others take their default. */
|
|
15540
|
+
variables: record(string(), union([
|
|
15541
|
+
string(),
|
|
15542
|
+
number(),
|
|
15543
|
+
boolean()
|
|
15544
|
+
])).optional()
|
|
15284
15545
|
}),
|
|
15285
15546
|
object({
|
|
15286
15547
|
kind: literal("code"),
|
|
@@ -15289,10 +15550,356 @@ var AutomationActionSchema = discriminatedUnion("kind", [
|
|
|
15289
15550
|
code: string().min(1).max(2e4)
|
|
15290
15551
|
})
|
|
15291
15552
|
]);
|
|
15553
|
+
/**
|
|
15554
|
+
* How a trigger that arrives while the automation is busy (or cooling down) is
|
|
15555
|
+
* handled. One of the automation's execution modes, never a property of its
|
|
15556
|
+
* actions. Absent means `single`.
|
|
15557
|
+
*
|
|
15558
|
+
* - `single` — drop the trigger while a run is in progress (traced + logged).
|
|
15559
|
+
* - `restart` — abort the run in progress (at its next step boundary), start fresh.
|
|
15560
|
+
* - `queued` — queue up to `max` triggers, drop the rest (traced).
|
|
15561
|
+
* - `parallel` — up to `max` runs at once, drop the rest (traced).
|
|
15562
|
+
* - `cooldown` — `single`, and no new run until `minutes` after the last run
|
|
15563
|
+
* STARTED; persisted across a restart. A manual trigger bypasses only this
|
|
15564
|
+
* wait, never the single/queued limits.
|
|
15565
|
+
*/
|
|
15566
|
+
var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
15567
|
+
object({ mode: literal("single") }),
|
|
15568
|
+
object({ mode: literal("restart") }),
|
|
15569
|
+
object({
|
|
15570
|
+
mode: literal("queued"),
|
|
15571
|
+
max: number().int().min(1).max(10)
|
|
15572
|
+
}),
|
|
15573
|
+
object({
|
|
15574
|
+
mode: literal("parallel"),
|
|
15575
|
+
max: number().int().min(1).max(10)
|
|
15576
|
+
}),
|
|
15577
|
+
object({
|
|
15578
|
+
mode: literal("cooldown"),
|
|
15579
|
+
minutes: number().int().min(1).max(1440)
|
|
15580
|
+
})
|
|
15581
|
+
]);
|
|
15292
15582
|
var AutomationRecipeSchema = object({
|
|
15293
15583
|
triggers: array(AutomationTriggerSchema),
|
|
15294
15584
|
conditions: AutomationConditionSchema.optional(),
|
|
15295
|
-
actions: array(AutomationActionSchema)
|
|
15585
|
+
actions: array(AutomationActionSchema),
|
|
15586
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
15587
|
+
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
15588
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
15589
|
+
});
|
|
15590
|
+
new Set([
|
|
15591
|
+
{
|
|
15592
|
+
id: "person",
|
|
15593
|
+
name: "Person"
|
|
15594
|
+
},
|
|
15595
|
+
{
|
|
15596
|
+
id: "vehicle",
|
|
15597
|
+
name: "Vehicle"
|
|
15598
|
+
},
|
|
15599
|
+
{
|
|
15600
|
+
id: "animal",
|
|
15601
|
+
name: "Animal"
|
|
15602
|
+
},
|
|
15603
|
+
{
|
|
15604
|
+
id: "package",
|
|
15605
|
+
name: "Package"
|
|
15606
|
+
}
|
|
15607
|
+
].map((l) => l.id));
|
|
15608
|
+
var COCO_TO_MACRO = {
|
|
15609
|
+
mapping: {
|
|
15610
|
+
person: "person",
|
|
15611
|
+
bicycle: "vehicle",
|
|
15612
|
+
car: "vehicle",
|
|
15613
|
+
motorcycle: "vehicle",
|
|
15614
|
+
airplane: "vehicle",
|
|
15615
|
+
bus: "vehicle",
|
|
15616
|
+
train: "vehicle",
|
|
15617
|
+
truck: "vehicle",
|
|
15618
|
+
boat: "vehicle",
|
|
15619
|
+
bird: "animal",
|
|
15620
|
+
cat: "animal",
|
|
15621
|
+
dog: "animal",
|
|
15622
|
+
horse: "animal",
|
|
15623
|
+
sheep: "animal",
|
|
15624
|
+
cow: "animal",
|
|
15625
|
+
elephant: "animal",
|
|
15626
|
+
bear: "animal",
|
|
15627
|
+
zebra: "animal",
|
|
15628
|
+
giraffe: "animal",
|
|
15629
|
+
suitcase: "package",
|
|
15630
|
+
backpack: "package",
|
|
15631
|
+
handbag: "package"
|
|
15632
|
+
},
|
|
15633
|
+
preserveOriginal: false
|
|
15634
|
+
};
|
|
15635
|
+
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
15636
|
+
/**
|
|
15637
|
+
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
15638
|
+
* so the editor can paint every block green or red with its reason.
|
|
15639
|
+
*
|
|
15640
|
+
* A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
|
|
15641
|
+
* every condition node (containers and leaves, including the ones a
|
|
15642
|
+
* short-circuit never reached), the run-mode verdict and every action step —
|
|
15643
|
+
* each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
|
|
15644
|
+
* for an Action run). Skipped
|
|
15645
|
+
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
15646
|
+
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
15647
|
+
*
|
|
15648
|
+
* {@link AutomationStepTraceSchema} is deliberately free of automation
|
|
15649
|
+
* vocabulary so an Action run records the same step shape.
|
|
15650
|
+
*/
|
|
15651
|
+
/** A value a trace may show: JSON primitives and short lists, nothing deeper. */
|
|
15652
|
+
var TraceValueSchema = union([
|
|
15653
|
+
string(),
|
|
15654
|
+
number(),
|
|
15655
|
+
boolean(),
|
|
15656
|
+
_null(),
|
|
15657
|
+
array(union([string(), number()]))
|
|
15658
|
+
]);
|
|
15659
|
+
var AutomationRunOutcomeSchema = _enum([
|
|
15660
|
+
"ran",
|
|
15661
|
+
"skipped-trigger",
|
|
15662
|
+
"skipped-conditions",
|
|
15663
|
+
"skipped-cooldown",
|
|
15664
|
+
"skipped-overlap",
|
|
15665
|
+
"failed",
|
|
15666
|
+
"aborted"
|
|
15667
|
+
]);
|
|
15668
|
+
/** `not-evaluated` = a short-circuit never reached the node. */
|
|
15669
|
+
var TraceNodeOutcomeSchema = _enum([
|
|
15670
|
+
"pass",
|
|
15671
|
+
"fail",
|
|
15672
|
+
"not-evaluated"
|
|
15673
|
+
]);
|
|
15674
|
+
var AutomationTriggerTraceSchema = object({
|
|
15675
|
+
kind: string().min(1),
|
|
15676
|
+
outcome: _enum(["pass", "fail"]),
|
|
15677
|
+
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
15678
|
+
reason: string(),
|
|
15679
|
+
/** deviceId, className, trackId, score … — only what the trigger exposes. */
|
|
15680
|
+
vars: record(string(), TraceValueSchema)
|
|
15681
|
+
});
|
|
15682
|
+
var AutomationConditionTraceNodeSchema = object({
|
|
15683
|
+
/** `conditions` for the root, `conditions.<i>` for its i-th child, … */
|
|
15684
|
+
path: string().min(1),
|
|
15685
|
+
kind: _enum([
|
|
15686
|
+
"all",
|
|
15687
|
+
"any",
|
|
15688
|
+
"not",
|
|
15689
|
+
"condition",
|
|
15690
|
+
"expression"
|
|
15691
|
+
]),
|
|
15692
|
+
outcome: TraceNodeOutcomeSchema,
|
|
15693
|
+
reason: string(),
|
|
15694
|
+
deviceId: number().int().optional(),
|
|
15695
|
+
cap: string().optional(),
|
|
15696
|
+
fieldPath: string().optional(),
|
|
15697
|
+
operator: string().optional(),
|
|
15698
|
+
expected: TraceValueSchema.optional(),
|
|
15699
|
+
/** What the mirror held; `null` when it held nothing (unknown). */
|
|
15700
|
+
observed: TraceValueSchema.optional()
|
|
15701
|
+
});
|
|
15702
|
+
var AutomationRunVerdictSchema = object({
|
|
15703
|
+
mode: AutomationRunModeSchema,
|
|
15704
|
+
decision: _enum([
|
|
15705
|
+
"run",
|
|
15706
|
+
"queued",
|
|
15707
|
+
"restart",
|
|
15708
|
+
"drop",
|
|
15709
|
+
"cooldown",
|
|
15710
|
+
"bypass"
|
|
15711
|
+
]),
|
|
15712
|
+
reason: string()
|
|
15713
|
+
});
|
|
15714
|
+
/** One executed (or not-run) step — shared by automations and Action runs. */
|
|
15715
|
+
var AutomationStepTraceSchema = object({
|
|
15716
|
+
/** `actions.<i>` */
|
|
15717
|
+
path: string().min(1),
|
|
15718
|
+
kind: string().min(1),
|
|
15719
|
+
cap: string().optional(),
|
|
15720
|
+
method: string().optional(),
|
|
15721
|
+
deviceId: number().int().optional(),
|
|
15722
|
+
startedAt: number().optional(),
|
|
15723
|
+
endedAt: number().optional(),
|
|
15724
|
+
outcome: _enum([
|
|
15725
|
+
"ok",
|
|
15726
|
+
"error",
|
|
15727
|
+
"not-run"
|
|
15728
|
+
]),
|
|
15729
|
+
message: string().optional()
|
|
15730
|
+
});
|
|
15731
|
+
var AutomationRunTraceSchema = object({
|
|
15732
|
+
runId: string().min(1),
|
|
15733
|
+
startedAt: number(),
|
|
15734
|
+
endedAt: number(),
|
|
15735
|
+
trigger: AutomationTriggerTraceSchema,
|
|
15736
|
+
conditions: array(AutomationConditionTraceNodeSchema),
|
|
15737
|
+
verdict: AutomationRunVerdictSchema.optional(),
|
|
15738
|
+
steps: array(AutomationStepTraceSchema),
|
|
15739
|
+
outcome: AutomationRunOutcomeSchema,
|
|
15740
|
+
/** How many identical consecutive skips this trace stands for (>= 1). */
|
|
15741
|
+
repeat: number().int().min(1),
|
|
15742
|
+
/** When the latest of those repeats happened. */
|
|
15743
|
+
lastAt: number()
|
|
15744
|
+
});
|
|
15745
|
+
/**
|
|
15746
|
+
* What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
|
|
15747
|
+
* `action:<actionId>`): the cooldown stamp and the trace ring.
|
|
15748
|
+
*/
|
|
15749
|
+
var AutomationStateDocSchema = object({
|
|
15750
|
+
version: literal(1),
|
|
15751
|
+
lastRunStartedAt: number().nullable(),
|
|
15752
|
+
traces: array(AutomationRunTraceSchema)
|
|
15753
|
+
});
|
|
15754
|
+
/**
|
|
15755
|
+
* Actions — named, runnable things an operator (or an automation) can start,
|
|
15756
|
+
* declared as LIST ENTRIES, never as devices.
|
|
15757
|
+
*
|
|
15758
|
+
* An entry is either **internal** (a short list of cap/wait steps kept in the
|
|
15759
|
+
* hub's `actions` record kind) or declared by an addon through the
|
|
15760
|
+
* `action-source` collection (a Home Assistant script, a scene …). The one
|
|
15761
|
+
* surface the UI and automations talk to is the `actions` cap; it keeps no
|
|
15762
|
+
* catalog of its own (D3) — every `list` asks the providers live.
|
|
15763
|
+
*
|
|
15764
|
+
* This file is the data half: the entry schema, the variables an entry may
|
|
15765
|
+
* declare, and the pure resolution of the values a run was given. It holds no
|
|
15766
|
+
* runtime.
|
|
15767
|
+
*/
|
|
15768
|
+
/** What a variable's value is. `select` takes one of `options`. */
|
|
15769
|
+
var ActionVariableTypeSchema = _enum([
|
|
15770
|
+
"string",
|
|
15771
|
+
"number",
|
|
15772
|
+
"boolean",
|
|
15773
|
+
"select"
|
|
15774
|
+
]);
|
|
15775
|
+
var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15776
|
+
var ActionVariableValueSchema = union([
|
|
15777
|
+
string(),
|
|
15778
|
+
number(),
|
|
15779
|
+
boolean()
|
|
15780
|
+
]);
|
|
15781
|
+
/**
|
|
15782
|
+
* One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
|
|
15783
|
+
* value) or `{{vars.name}}` (inside a string) in their args.
|
|
15784
|
+
*/
|
|
15785
|
+
var ActionVariableSchema = object({
|
|
15786
|
+
name: string().regex(ACTION_VARIABLE_NAME_RE),
|
|
15787
|
+
label: string().min(1).max(120).optional(),
|
|
15788
|
+
type: ActionVariableTypeSchema,
|
|
15789
|
+
required: boolean().optional(),
|
|
15790
|
+
default: ActionVariableValueSchema.optional(),
|
|
15791
|
+
/** The allowed values of a `select`. */
|
|
15792
|
+
options: array(string().min(1)).min(1).optional()
|
|
15793
|
+
}).superRefine((v, ctx) => {
|
|
15794
|
+
if (v.type === "select" && v.options === void 0) ctx.addIssue({
|
|
15795
|
+
code: "custom",
|
|
15796
|
+
message: "a select variable needs options",
|
|
15797
|
+
path: ["options"]
|
|
15798
|
+
});
|
|
15799
|
+
if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
|
|
15800
|
+
code: "custom",
|
|
15801
|
+
message: variableValueError(v, v.default) ?? "invalid default",
|
|
15802
|
+
path: ["default"]
|
|
15803
|
+
});
|
|
15804
|
+
});
|
|
15805
|
+
/** Why `value` is not a legal value for the variable, or null when it is. */
|
|
15806
|
+
function variableValueError(variable, value) {
|
|
15807
|
+
switch (variable.type) {
|
|
15808
|
+
case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
|
|
15809
|
+
case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
|
|
15810
|
+
case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
|
|
15811
|
+
case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
|
|
15812
|
+
}
|
|
15813
|
+
}
|
|
15814
|
+
var ActionSourceKindSchema = _enum(["internal", "addon"]);
|
|
15815
|
+
function isInternalActionId(actionId) {
|
|
15816
|
+
return actionId.startsWith("internal:") && actionId.length > 9;
|
|
15817
|
+
}
|
|
15818
|
+
/** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
|
|
15819
|
+
function addonIdOfActionId(actionId) {
|
|
15820
|
+
if (isInternalActionId(actionId)) return null;
|
|
15821
|
+
const at = actionId.indexOf(":");
|
|
15822
|
+
return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
|
|
15823
|
+
}
|
|
15824
|
+
var ActionStatusSchema = object({
|
|
15825
|
+
isRunning: boolean(),
|
|
15826
|
+
/** Ms epoch the latest run STARTED; null when it never ran. */
|
|
15827
|
+
lastRunAt: number().nullable(),
|
|
15828
|
+
/** The latest run's failure; null on success or when it never ran. */
|
|
15829
|
+
lastError: string().nullable()
|
|
15830
|
+
});
|
|
15831
|
+
var ActionEntrySchema = object({
|
|
15832
|
+
/** `internal:<uuid>` or `<addonId>:<localId>`. */
|
|
15833
|
+
id: string().min(3),
|
|
15834
|
+
name: string().min(1).max(120),
|
|
15835
|
+
/** The addon that declared it; `core-blocks` for an internal one. */
|
|
15836
|
+
addonId: string().min(1),
|
|
15837
|
+
source: ActionSourceKindSchema,
|
|
15838
|
+
/** An addon-declared entry is edited where it was declared. */
|
|
15839
|
+
readOnly: boolean(),
|
|
15840
|
+
/** Internal only: cap and wait steps. No action-in-action, so no cycles. */
|
|
15841
|
+
steps: array(ActionStepSchema).min(1).max(50).optional(),
|
|
15842
|
+
variables: array(ActionVariableSchema).max(20).optional(),
|
|
15843
|
+
status: ActionStatusSchema
|
|
15844
|
+
}).superRefine((entry, ctx) => {
|
|
15845
|
+
if (entry.source === "internal") {
|
|
15846
|
+
if (!isInternalActionId(entry.id)) ctx.addIssue({
|
|
15847
|
+
code: "custom",
|
|
15848
|
+
message: "an internal action id is internal:<uuid>",
|
|
15849
|
+
path: ["id"]
|
|
15850
|
+
});
|
|
15851
|
+
return;
|
|
15852
|
+
}
|
|
15853
|
+
if (entry.steps !== void 0) ctx.addIssue({
|
|
15854
|
+
code: "custom",
|
|
15855
|
+
message: "only an internal action carries steps",
|
|
15856
|
+
path: ["steps"]
|
|
15857
|
+
});
|
|
15858
|
+
if (!entry.readOnly) ctx.addIssue({
|
|
15859
|
+
code: "custom",
|
|
15860
|
+
message: "an addon action is read-only here",
|
|
15861
|
+
path: ["readOnly"]
|
|
15862
|
+
});
|
|
15863
|
+
if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
|
|
15864
|
+
code: "custom",
|
|
15865
|
+
message: "an addon action id is <addonId>:<localId>",
|
|
15866
|
+
path: ["id"]
|
|
15867
|
+
});
|
|
15868
|
+
});
|
|
15869
|
+
/** What an operator writes for an internal action. */
|
|
15870
|
+
var ActionInputSchema = object({
|
|
15871
|
+
name: string().min(1).max(120),
|
|
15872
|
+
steps: array(ActionStepSchema).min(1).max(50),
|
|
15873
|
+
variables: array(ActionVariableSchema).max(20).optional()
|
|
15874
|
+
});
|
|
15875
|
+
/** Who started a run. Absent = the operator pressed it. */
|
|
15876
|
+
var ActionRunOriginSchema = object({
|
|
15877
|
+
/** The automation block whose step started it. */
|
|
15878
|
+
blockId: string().min(1).optional() });
|
|
15879
|
+
/** `started` = the caller did not ask to wait. */
|
|
15880
|
+
var ActionRunOutcomeSchema = union([AutomationRunOutcomeSchema, literal("started")]);
|
|
15881
|
+
var ActionRunResultSchema = object({
|
|
15882
|
+
runId: string().min(1),
|
|
15883
|
+
outcome: ActionRunOutcomeSchema,
|
|
15884
|
+
/** Why it did not run, or what failed. */
|
|
15885
|
+
message: string().optional()
|
|
15886
|
+
});
|
|
15887
|
+
_enum([
|
|
15888
|
+
"not-found",
|
|
15889
|
+
"read-only",
|
|
15890
|
+
"invalid-variables",
|
|
15891
|
+
"invalid-steps",
|
|
15892
|
+
"not-linked",
|
|
15893
|
+
"busy"
|
|
15894
|
+
]);
|
|
15895
|
+
object({
|
|
15896
|
+
actionId: string(),
|
|
15897
|
+
runId: string(),
|
|
15898
|
+
isRunning: boolean(),
|
|
15899
|
+
/** Set when the run ended. */
|
|
15900
|
+
outcome: AutomationRunOutcomeSchema.optional(),
|
|
15901
|
+
lastRunAt: number().nullable(),
|
|
15902
|
+
lastError: string().nullable()
|
|
15296
15903
|
});
|
|
15297
15904
|
/**
|
|
15298
15905
|
* `addon-pages` — system-scoped singleton aggregator cap. Public-facing
|
|
@@ -16887,7 +17494,22 @@ var BrokerProviderInfoSchema = object({
|
|
|
16887
17494
|
*/
|
|
16888
17495
|
interactiveConnectPath: string().min(1).optional(),
|
|
16889
17496
|
/** Relative hub path to disconnect the linked account (same popup pattern). */
|
|
16890
|
-
interactiveDisconnectPath: string().min(1).optional()
|
|
17497
|
+
interactiveDisconnectPath: string().min(1).optional(),
|
|
17498
|
+
/**
|
|
17499
|
+
* The kind signs in through a server-held interactive session instead of
|
|
17500
|
+
* (or before) typed credentials. `basePath` is a hub-relative route set
|
|
17501
|
+
* served by the owning addon (`/addon/<id>/...`) speaking the sign-in
|
|
17502
|
+
* contract: `POST {basePath}/start` (body: the form's settings) answers
|
|
17503
|
+
* `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
|
|
17504
|
+
* `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
|
|
17505
|
+
* 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
|
|
17506
|
+
* (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
|
|
17507
|
+
* existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
|
|
17508
|
+
* (`{ sessionId }`) drops the session. The dialog that edits a broker
|
|
17509
|
+
* offers it as the default path; the fields the session fills are
|
|
17510
|
+
* marked `filledBySignIn` in the kind's settings schema.
|
|
17511
|
+
*/
|
|
17512
|
+
signIn: object({ basePath: string().min(1) }).optional()
|
|
16891
17513
|
}))
|
|
16892
17514
|
});
|
|
16893
17515
|
var ListInputSchema = object({
|
|
@@ -17992,7 +18614,8 @@ var CompositionProblemSchema = object({
|
|
|
17992
18614
|
"native-capability-replace",
|
|
17993
18615
|
"mixed-quantity",
|
|
17994
18616
|
"helper-mismatch",
|
|
17995
|
-
"member-constraint"
|
|
18617
|
+
"member-constraint",
|
|
18618
|
+
"retention"
|
|
17996
18619
|
]),
|
|
17997
18620
|
path: string(),
|
|
17998
18621
|
message: string()
|
|
@@ -18226,13 +18849,17 @@ Object.freeze({
|
|
|
18226
18849
|
latchedAt: {
|
|
18227
18850
|
kind: "fixed",
|
|
18228
18851
|
result: ["number", "null"]
|
|
18852
|
+
},
|
|
18853
|
+
schmitt: {
|
|
18854
|
+
kind: "fixed",
|
|
18855
|
+
result: ["boolean", "null"]
|
|
18229
18856
|
}
|
|
18230
18857
|
});
|
|
18231
18858
|
/**
|
|
18232
18859
|
* One catalog row per base reading a group helper can build (D944 §3).
|
|
18233
18860
|
* Data, not a form default written into the block.
|
|
18234
18861
|
*/
|
|
18235
|
-
var NOW = {
|
|
18862
|
+
var NOW$2 = {
|
|
18236
18863
|
fieldPath: "lastFetchedAt",
|
|
18237
18864
|
expr: "now"
|
|
18238
18865
|
};
|
|
@@ -18246,6 +18873,8 @@ function numeric(input) {
|
|
|
18246
18873
|
family: "numeric",
|
|
18247
18874
|
deviceType: DeviceType.Sensor,
|
|
18248
18875
|
memberDeviceType: DeviceType.Sensor,
|
|
18876
|
+
memberDeviceTypes: [DeviceType.Sensor],
|
|
18877
|
+
activeWhen: void 0,
|
|
18249
18878
|
commandCaps: [],
|
|
18250
18879
|
baseUnit: baseUnitOf(input.quantity),
|
|
18251
18880
|
aggregates: NUMERIC_AGGREGATES,
|
|
@@ -18261,6 +18890,8 @@ function binary(input) {
|
|
|
18261
18890
|
family: "binary",
|
|
18262
18891
|
deviceType: DeviceType.Sensor,
|
|
18263
18892
|
memberDeviceType: DeviceType.Sensor,
|
|
18893
|
+
memberDeviceTypes: [DeviceType.Sensor],
|
|
18894
|
+
activeWhen: void 0,
|
|
18264
18895
|
commandCaps: [],
|
|
18265
18896
|
quantity: "none",
|
|
18266
18897
|
baseUnit: void 0,
|
|
@@ -18278,7 +18909,7 @@ var GROUP_TEMPLATES = [
|
|
|
18278
18909
|
quantity: "temperature",
|
|
18279
18910
|
defaultAggregate: "mean",
|
|
18280
18911
|
empty: "null",
|
|
18281
|
-
companions: [NOW]
|
|
18912
|
+
companions: [NOW$2]
|
|
18282
18913
|
}),
|
|
18283
18914
|
numeric({
|
|
18284
18915
|
id: "humidity",
|
|
@@ -18289,7 +18920,7 @@ var GROUP_TEMPLATES = [
|
|
|
18289
18920
|
quantity: "humidity",
|
|
18290
18921
|
defaultAggregate: "mean",
|
|
18291
18922
|
empty: "null",
|
|
18292
|
-
companions: [NOW]
|
|
18923
|
+
companions: [NOW$2]
|
|
18293
18924
|
}),
|
|
18294
18925
|
numeric({
|
|
18295
18926
|
id: "pressure",
|
|
@@ -18300,7 +18931,7 @@ var GROUP_TEMPLATES = [
|
|
|
18300
18931
|
quantity: "pressure",
|
|
18301
18932
|
defaultAggregate: "mean",
|
|
18302
18933
|
empty: "null",
|
|
18303
|
-
companions: [NOW]
|
|
18934
|
+
companions: [NOW$2]
|
|
18304
18935
|
}),
|
|
18305
18936
|
numeric({
|
|
18306
18937
|
id: "illuminance",
|
|
@@ -18311,7 +18942,7 @@ var GROUP_TEMPLATES = [
|
|
|
18311
18942
|
quantity: "illuminance",
|
|
18312
18943
|
defaultAggregate: "mean",
|
|
18313
18944
|
empty: "null",
|
|
18314
|
-
companions: [NOW]
|
|
18945
|
+
companions: [NOW$2]
|
|
18315
18946
|
}),
|
|
18316
18947
|
numeric({
|
|
18317
18948
|
id: "power",
|
|
@@ -18322,7 +18953,7 @@ var GROUP_TEMPLATES = [
|
|
|
18322
18953
|
quantity: "power",
|
|
18323
18954
|
defaultAggregate: "sum",
|
|
18324
18955
|
empty: "omit",
|
|
18325
|
-
companions: [NOW]
|
|
18956
|
+
companions: [NOW$2]
|
|
18326
18957
|
}),
|
|
18327
18958
|
numeric({
|
|
18328
18959
|
id: "energy",
|
|
@@ -18333,7 +18964,7 @@ var GROUP_TEMPLATES = [
|
|
|
18333
18964
|
quantity: "energy",
|
|
18334
18965
|
defaultAggregate: "sum",
|
|
18335
18966
|
empty: "omit",
|
|
18336
|
-
companions: [NOW]
|
|
18967
|
+
companions: [NOW$2]
|
|
18337
18968
|
}),
|
|
18338
18969
|
numeric({
|
|
18339
18970
|
id: "battery",
|
|
@@ -18368,7 +18999,7 @@ var GROUP_TEMPLATES = [
|
|
|
18368
18999
|
quantity: "device",
|
|
18369
19000
|
defaultAggregate: "mean",
|
|
18370
19001
|
empty: "null",
|
|
18371
|
-
companions: [NOW]
|
|
19002
|
+
companions: [NOW$2]
|
|
18372
19003
|
}),
|
|
18373
19004
|
binary({
|
|
18374
19005
|
id: "motion",
|
|
@@ -18462,6 +19093,11 @@ var GROUP_TEMPLATES = [
|
|
|
18462
19093
|
summary: "On when at least one switch is on, or when all of them are. Offline and unknown members are left out of that decision, never read as off, and still receive the command.",
|
|
18463
19094
|
deviceType: DeviceType.Switch,
|
|
18464
19095
|
outputCap: "switch",
|
|
19096
|
+
outputField: "on",
|
|
19097
|
+
member: {
|
|
19098
|
+
cap: "switch",
|
|
19099
|
+
fieldPath: "on"
|
|
19100
|
+
},
|
|
18465
19101
|
commandCaps: ["switch"]
|
|
18466
19102
|
}),
|
|
18467
19103
|
actuator({
|
|
@@ -18469,34 +19105,136 @@ var GROUP_TEMPLATES = [
|
|
|
18469
19105
|
summary: "On when at least one light is on, or when all of them are. Brightness and colour are offered only when every member has them. A command is not sent to a member that lacks it.",
|
|
18470
19106
|
deviceType: DeviceType.Light,
|
|
18471
19107
|
outputCap: "switch",
|
|
19108
|
+
outputField: "on",
|
|
19109
|
+
member: {
|
|
19110
|
+
cap: "switch",
|
|
19111
|
+
fieldPath: "on"
|
|
19112
|
+
},
|
|
18472
19113
|
commandCaps: [
|
|
18473
19114
|
"switch",
|
|
18474
19115
|
"brightness",
|
|
18475
19116
|
"color"
|
|
18476
19117
|
]
|
|
19118
|
+
}),
|
|
19119
|
+
actuator({
|
|
19120
|
+
id: "fan",
|
|
19121
|
+
summary: "On when at least one fan is on, or when all of them are. Speed, preset, direction and oscillation are offered only when every fan has them. A command is not sent to a fan that lacks it.",
|
|
19122
|
+
deviceType: DeviceType.Fan,
|
|
19123
|
+
outputCap: "switch",
|
|
19124
|
+
outputField: "on",
|
|
19125
|
+
member: {
|
|
19126
|
+
cap: "switch",
|
|
19127
|
+
fieldPath: "on"
|
|
19128
|
+
},
|
|
19129
|
+
commandCaps: ["switch", "fan-control"]
|
|
19130
|
+
}),
|
|
19131
|
+
actuator({
|
|
19132
|
+
id: "cover",
|
|
19133
|
+
summary: "Open when at least one cover is open or part-open, or only when all of them are. Position and tilt are offered only when every cover has them. A cover that reports nothing is left out, never read as closed.",
|
|
19134
|
+
deviceType: DeviceType.Cover,
|
|
19135
|
+
outputCap: "group-active",
|
|
19136
|
+
outputField: "active",
|
|
19137
|
+
member: {
|
|
19138
|
+
cap: "cover",
|
|
19139
|
+
fieldPath: "state"
|
|
19140
|
+
},
|
|
19141
|
+
activeWhen: [
|
|
19142
|
+
"open",
|
|
19143
|
+
"opening",
|
|
19144
|
+
"stopped"
|
|
19145
|
+
],
|
|
19146
|
+
commandCaps: ["cover"]
|
|
19147
|
+
}),
|
|
19148
|
+
actuator({
|
|
19149
|
+
id: "valve",
|
|
19150
|
+
summary: "Open when at least one valve is open or part-open, or only when all of them are. Position is offered only when every valve has it. A valve that reports nothing is left out, never read as closed.",
|
|
19151
|
+
deviceType: DeviceType.Valve,
|
|
19152
|
+
outputCap: "group-active",
|
|
19153
|
+
outputField: "active",
|
|
19154
|
+
member: {
|
|
19155
|
+
cap: "valve",
|
|
19156
|
+
fieldPath: "state"
|
|
19157
|
+
},
|
|
19158
|
+
activeWhen: [
|
|
19159
|
+
"open",
|
|
19160
|
+
"opening",
|
|
19161
|
+
"stopped"
|
|
19162
|
+
],
|
|
19163
|
+
commandCaps: ["valve"]
|
|
19164
|
+
}),
|
|
19165
|
+
actuator({
|
|
19166
|
+
id: "lock",
|
|
19167
|
+
summary: "Locked when at least one lock is locked, or only when all of them are. A jammed or moving lock is not locked. Locking is one tap; unlocking the group needs a confirmation every time, and nothing is sent without it.",
|
|
19168
|
+
deviceType: DeviceType.Lock,
|
|
19169
|
+
outputCap: "group-active",
|
|
19170
|
+
outputField: "active",
|
|
19171
|
+
member: {
|
|
19172
|
+
cap: "lock-control",
|
|
19173
|
+
fieldPath: "state"
|
|
19174
|
+
},
|
|
19175
|
+
activeWhen: ["locked"],
|
|
19176
|
+
commandCaps: ["lock-control"]
|
|
19177
|
+
}),
|
|
19178
|
+
actuator({
|
|
19179
|
+
id: "media-player",
|
|
19180
|
+
summary: "Playing when at least one player is playing, or only when all of them are. Volume is offered only when every player has it. A player that reports nothing is left out, never read as stopped.",
|
|
19181
|
+
deviceType: DeviceType.MediaPlayer,
|
|
19182
|
+
outputCap: "group-active",
|
|
19183
|
+
outputField: "active",
|
|
19184
|
+
member: {
|
|
19185
|
+
cap: "media-player",
|
|
19186
|
+
fieldPath: "state"
|
|
19187
|
+
},
|
|
19188
|
+
activeWhen: ["playing", "buffering"],
|
|
19189
|
+
commandCaps: ["media-player"]
|
|
19190
|
+
}),
|
|
19191
|
+
actuator({
|
|
19192
|
+
id: "climate",
|
|
19193
|
+
summary: "Running when at least one thermostat or air conditioner is not off, or only when all of them are. Target temperature and fan are offered only when every device has them. A device that reports nothing is left out, never read as off.",
|
|
19194
|
+
deviceType: DeviceType.Thermostat,
|
|
19195
|
+
memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
19196
|
+
outputCap: "group-active",
|
|
19197
|
+
outputField: "active",
|
|
19198
|
+
member: {
|
|
19199
|
+
cap: "climate-control",
|
|
19200
|
+
fieldPath: "mode"
|
|
19201
|
+
},
|
|
19202
|
+
activeWhen: [
|
|
19203
|
+
"heat",
|
|
19204
|
+
"cool",
|
|
19205
|
+
"auto",
|
|
19206
|
+
"heat_cool",
|
|
19207
|
+
"fan_only",
|
|
19208
|
+
"dry"
|
|
19209
|
+
],
|
|
19210
|
+
commandCaps: ["climate-control"]
|
|
18477
19211
|
})
|
|
18478
19212
|
];
|
|
18479
19213
|
function actuator(input) {
|
|
18480
19214
|
return {
|
|
18481
|
-
|
|
19215
|
+
id: input.id,
|
|
19216
|
+
summary: input.summary,
|
|
19217
|
+
deviceType: input.deviceType,
|
|
19218
|
+
outputCap: input.outputCap,
|
|
19219
|
+
outputField: input.outputField,
|
|
19220
|
+
commandCaps: input.commandCaps,
|
|
18482
19221
|
family: "actuator",
|
|
18483
19222
|
role: void 0,
|
|
18484
19223
|
memberDeviceType: input.deviceType,
|
|
18485
|
-
|
|
19224
|
+
memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
|
|
19225
|
+
activeWhen: input.activeWhen,
|
|
18486
19226
|
quantity: "none",
|
|
18487
19227
|
baseUnit: void 0,
|
|
18488
19228
|
aggregates: BINARY_AGGREGATES,
|
|
18489
19229
|
defaultAggregate: "any",
|
|
18490
19230
|
empty: "offline",
|
|
18491
|
-
accept: [
|
|
18492
|
-
cap: "switch",
|
|
18493
|
-
fieldPath: "on"
|
|
18494
|
-
}],
|
|
19231
|
+
accept: [input.member],
|
|
18495
19232
|
companions: [CHANGED]
|
|
18496
19233
|
};
|
|
18497
19234
|
}
|
|
18498
19235
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
18499
19236
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
19237
|
+
DeviceType.Camera;
|
|
18500
19238
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
18501
19239
|
/**
|
|
18502
19240
|
* What a composition may BUILD on a device type (G2, D676): the caps a new
|
|
@@ -18811,7 +19549,18 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
18811
19549
|
kind: "mutation",
|
|
18812
19550
|
auth: "admin",
|
|
18813
19551
|
access: "view"
|
|
18814
|
-
})
|
|
19552
|
+
}), method(object({
|
|
19553
|
+
blockId: string().min(1),
|
|
19554
|
+
trace: AutomationRunTraceSchema.optional(),
|
|
19555
|
+
runStartedAt: number().optional(),
|
|
19556
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
19557
|
+
}), _void(), {
|
|
19558
|
+
kind: "mutation",
|
|
19559
|
+
auth: "admin"
|
|
19560
|
+
}), method(object({ blockId: string().min(1) }), AutomationStateDocSchema, { auth: "admin" }), method(object({ blockId: string().min(1) }), object({
|
|
19561
|
+
traces: array(AutomationRunTraceSchema),
|
|
19562
|
+
lastError: string().nullable()
|
|
19563
|
+
}), { auth: "admin" });
|
|
18815
19564
|
/**
|
|
18816
19565
|
* `custom-model-registry` — collection cap exposing operator-registered
|
|
18817
19566
|
* custom detection models. Each provider (today: `addon-model-studio`)
|
|
@@ -43794,7 +44543,7 @@ var NetworkAccessStatusSchema = object({
|
|
|
43794
44543
|
});
|
|
43795
44544
|
/**
|
|
43796
44545
|
* Optional, richer endpoint shape returned by providers that expose
|
|
43797
|
-
* MORE than one ingress concurrently (Tailscale
|
|
44546
|
+
* MORE than one ingress concurrently (Tailscale Funnel with mixed
|
|
43798
44547
|
* serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
|
|
43799
44548
|
* the originating provider config (mode + sourcePort) so the
|
|
43800
44549
|
* orchestrator UI can label rows distinctly. Providers that expose only
|
|
@@ -44187,51 +44936,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
|
|
|
44187
44936
|
kind: "mutation",
|
|
44188
44937
|
auth: "admin"
|
|
44189
44938
|
});
|
|
44190
|
-
new Set([
|
|
44191
|
-
{
|
|
44192
|
-
id: "person",
|
|
44193
|
-
name: "Person"
|
|
44194
|
-
},
|
|
44195
|
-
{
|
|
44196
|
-
id: "vehicle",
|
|
44197
|
-
name: "Vehicle"
|
|
44198
|
-
},
|
|
44199
|
-
{
|
|
44200
|
-
id: "animal",
|
|
44201
|
-
name: "Animal"
|
|
44202
|
-
},
|
|
44203
|
-
{
|
|
44204
|
-
id: "package",
|
|
44205
|
-
name: "Package"
|
|
44206
|
-
}
|
|
44207
|
-
].map((l) => l.id));
|
|
44208
|
-
var COCO_TO_MACRO = {
|
|
44209
|
-
mapping: {
|
|
44210
|
-
person: "person",
|
|
44211
|
-
bicycle: "vehicle",
|
|
44212
|
-
car: "vehicle",
|
|
44213
|
-
motorcycle: "vehicle",
|
|
44214
|
-
airplane: "vehicle",
|
|
44215
|
-
bus: "vehicle",
|
|
44216
|
-
train: "vehicle",
|
|
44217
|
-
truck: "vehicle",
|
|
44218
|
-
boat: "vehicle",
|
|
44219
|
-
bird: "animal",
|
|
44220
|
-
cat: "animal",
|
|
44221
|
-
dog: "animal",
|
|
44222
|
-
horse: "animal",
|
|
44223
|
-
sheep: "animal",
|
|
44224
|
-
cow: "animal",
|
|
44225
|
-
elephant: "animal",
|
|
44226
|
-
bear: "animal",
|
|
44227
|
-
zebra: "animal",
|
|
44228
|
-
giraffe: "animal",
|
|
44229
|
-
suitcase: "package",
|
|
44230
|
-
backpack: "package",
|
|
44231
|
-
handbag: "package"
|
|
44232
|
-
},
|
|
44233
|
-
preserveOriginal: false
|
|
44234
|
-
};
|
|
44235
44939
|
var AUDIO_MACRO_LABELS = [
|
|
44236
44940
|
{
|
|
44237
44941
|
id: "speech",
|
|
@@ -46481,7 +47185,20 @@ var NcRuleInputSchema = object({
|
|
|
46481
47185
|
* save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
|
|
46482
47186
|
* path does not run Zod defaults.
|
|
46483
47187
|
*/
|
|
46484
|
-
groupingSec: number().int().min(5).max(60).optional()
|
|
47188
|
+
groupingSec: number().int().min(5).max(60).optional(),
|
|
47189
|
+
/**
|
|
47190
|
+
* ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
|
|
47191
|
+
* carries "Correct" / "Wrong" buttons (and, where the channel has room, a
|
|
47192
|
+
* "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
|
|
47193
|
+
* same mark the Library writes, and records whose notification it was and
|
|
47194
|
+
* when. A channel with no buttons gets the same two choices as links.
|
|
47195
|
+
*
|
|
47196
|
+
* `.optional()`, never `.default()` (a Zod default does not run on the addon
|
|
47197
|
+
* cap path): absent and `false` both mean off, which is what every rule
|
|
47198
|
+
* authored before this field meant. An editor turns it off by writing
|
|
47199
|
+
* `false`, so it needs no `clear` key.
|
|
47200
|
+
*/
|
|
47201
|
+
askVerdict: boolean().optional()
|
|
46485
47202
|
});
|
|
46486
47203
|
/**
|
|
46487
47204
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -57024,6 +57741,84 @@ var audioMetricsCapability = {
|
|
|
57024
57741
|
*/
|
|
57025
57742
|
history: "none"
|
|
57026
57743
|
};
|
|
57744
|
+
method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
57745
|
+
actionId: string().min(1),
|
|
57746
|
+
variables: record(string(), ActionVariableValueSchema).optional()
|
|
57747
|
+
}), _void(), {
|
|
57748
|
+
kind: "mutation",
|
|
57749
|
+
auth: "admin"
|
|
57750
|
+
}), method(object({ actionId: string().min(1) }), _void(), {
|
|
57751
|
+
kind: "mutation",
|
|
57752
|
+
auth: "admin"
|
|
57753
|
+
});
|
|
57754
|
+
/**
|
|
57755
|
+
* actions — the ONE surface for runnable actions, for the UI and for
|
|
57756
|
+
* automations. Provided by the core-blocks builtin on the hub.
|
|
57757
|
+
*
|
|
57758
|
+
* An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
|
|
57759
|
+
* steps, kept in the hub's `actions` record kind) or declared by an addon over
|
|
57760
|
+
* `action-source`. `list` asks every `action-source` provider LIVE and overlays
|
|
57761
|
+
* the run status from the hub's `runs` ledger; there is no catalog here to
|
|
57762
|
+
* drift (D3).
|
|
57763
|
+
*
|
|
57764
|
+
* **Permissions follow the camera, as linked devices do.** An action is linked
|
|
57765
|
+
* to cameras by the operator (`setLinks`, D356: nothing links itself). A
|
|
57766
|
+
* non-admin may `get` / `run` an action only through a camera they may see that
|
|
57767
|
+
* links it: they name the camera (`deviceId`, a top-level field so the scope
|
|
57768
|
+
* gate sees it) and the provider refuses an action that camera does not link.
|
|
57769
|
+
* An admin, and an in-process caller (an automation block), may run any action.
|
|
57770
|
+
* Every refusal is typed (`[not-linked] …`) and logged.
|
|
57771
|
+
*
|
|
57772
|
+
* A second `run` of an action that is already running is REFUSED — traced and
|
|
57773
|
+
* logged — never queued.
|
|
57774
|
+
*/
|
|
57775
|
+
var ActionIdInput = object({ actionId: string().min(1) });
|
|
57776
|
+
var ActionLinksSchema = object({
|
|
57777
|
+
deviceId: number().int().nonnegative(),
|
|
57778
|
+
actionIds: array(string().min(1)).max(100)
|
|
57779
|
+
});
|
|
57780
|
+
method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
57781
|
+
actionId: string().min(1),
|
|
57782
|
+
deviceId: number().int().nonnegative().optional()
|
|
57783
|
+
}), object({ action: ActionEntrySchema.nullable() }), {
|
|
57784
|
+
role: "viewer",
|
|
57785
|
+
auth: "protected",
|
|
57786
|
+
kind: "query",
|
|
57787
|
+
caller: "required"
|
|
57788
|
+
}), method(object({
|
|
57789
|
+
actionId: string().min(1),
|
|
57790
|
+
variables: record(string(), ActionVariableValueSchema).optional(),
|
|
57791
|
+
wait: boolean().optional(),
|
|
57792
|
+
origin: ActionRunOriginSchema.optional(),
|
|
57793
|
+
deviceId: number().int().nonnegative().optional()
|
|
57794
|
+
}), ActionRunResultSchema, {
|
|
57795
|
+
role: "viewer",
|
|
57796
|
+
auth: "protected",
|
|
57797
|
+
kind: "mutation",
|
|
57798
|
+
caller: "required"
|
|
57799
|
+
}), method(ActionIdInput, _void(), {
|
|
57800
|
+
kind: "mutation",
|
|
57801
|
+
auth: "admin"
|
|
57802
|
+
}), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
|
|
57803
|
+
kind: "mutation",
|
|
57804
|
+
auth: "admin"
|
|
57805
|
+
}), method(object({
|
|
57806
|
+
actionId: string().min(1),
|
|
57807
|
+
action: ActionInputSchema.partial()
|
|
57808
|
+
}), object({ action: ActionEntrySchema }), {
|
|
57809
|
+
kind: "mutation",
|
|
57810
|
+
auth: "admin"
|
|
57811
|
+
}), method(ActionIdInput, _void(), {
|
|
57812
|
+
kind: "mutation",
|
|
57813
|
+
auth: "admin"
|
|
57814
|
+
}), method(object({ deviceId: number().int().nonnegative() }), array(ActionEntrySchema), {
|
|
57815
|
+
role: "viewer",
|
|
57816
|
+
auth: "protected",
|
|
57817
|
+
kind: "query"
|
|
57818
|
+
}), method(object({ deviceId: number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }), method(ActionLinksSchema, ActionLinksSchema, {
|
|
57819
|
+
kind: "mutation",
|
|
57820
|
+
auth: "admin"
|
|
57821
|
+
});
|
|
57027
57822
|
/**
|
|
57028
57823
|
* Automation-control cap. Models HA `automation.*` entities on
|
|
57029
57824
|
* `DeviceType.Automation`. An automation is a trigger+condition+
|
|
@@ -57201,7 +57996,8 @@ var BATTERY_DEVICE_TYPES = [
|
|
|
57201
57996
|
DeviceType.Switch,
|
|
57202
57997
|
DeviceType.PetFeeder,
|
|
57203
57998
|
DeviceType.Container,
|
|
57204
|
-
DeviceType.Thermostat
|
|
57999
|
+
DeviceType.Thermostat,
|
|
58000
|
+
DeviceType.EventEmitter
|
|
57205
58001
|
];
|
|
57206
58002
|
/** Generic Customize target for the percentage reading (D936). */
|
|
57207
58003
|
var batteryCustomize = {
|
|
@@ -60069,7 +60865,17 @@ var groupCommandsCapability = {
|
|
|
60069
60865
|
scope: "device",
|
|
60070
60866
|
deviceNative: true,
|
|
60071
60867
|
mode: "singleton",
|
|
60072
|
-
deviceTypes: [
|
|
60868
|
+
deviceTypes: [
|
|
60869
|
+
DeviceType.Switch,
|
|
60870
|
+
DeviceType.Light,
|
|
60871
|
+
DeviceType.Fan,
|
|
60872
|
+
DeviceType.Cover,
|
|
60873
|
+
DeviceType.Valve,
|
|
60874
|
+
DeviceType.Lock,
|
|
60875
|
+
DeviceType.MediaPlayer,
|
|
60876
|
+
DeviceType.Thermostat,
|
|
60877
|
+
DeviceType.Climate
|
|
60878
|
+
],
|
|
60073
60879
|
methods: {},
|
|
60074
60880
|
status: {
|
|
60075
60881
|
schema: GroupCommandsSchema,
|
|
@@ -60087,6 +60893,49 @@ var groupCommandsCapability = {
|
|
|
60087
60893
|
history: "none"
|
|
60088
60894
|
};
|
|
60089
60895
|
/**
|
|
60896
|
+
* Whether a helper actuator group counts as active (D944): at least one
|
|
60897
|
+
* included member is, or every included member is. What "active" means is the
|
|
60898
|
+
* type's: a cover is open, a lock is locked, a media player is playing, a
|
|
60899
|
+
* climate device is not off. A group of lights, switches and fans reports
|
|
60900
|
+
* `switch.on` instead; this cap is for the types whose own state is not a
|
|
60901
|
+
* yes/no. Unknown members are left out (`group-status` counts them) and an
|
|
60902
|
+
* empty included set takes the device offline, never `active: false`.
|
|
60903
|
+
*/
|
|
60904
|
+
var GroupActiveSchema = object({
|
|
60905
|
+
active: boolean().meta({ label: "Active" }),
|
|
60906
|
+
/** Ms epoch when the slice was last updated. */
|
|
60907
|
+
lastChangedAt: number()
|
|
60908
|
+
});
|
|
60909
|
+
var groupActiveCapability = {
|
|
60910
|
+
name: "group-active",
|
|
60911
|
+
scope: "device",
|
|
60912
|
+
deviceNative: true,
|
|
60913
|
+
mode: "singleton",
|
|
60914
|
+
deviceTypes: [
|
|
60915
|
+
DeviceType.Cover,
|
|
60916
|
+
DeviceType.Valve,
|
|
60917
|
+
DeviceType.Lock,
|
|
60918
|
+
DeviceType.MediaPlayer,
|
|
60919
|
+
DeviceType.Climate,
|
|
60920
|
+
DeviceType.Thermostat
|
|
60921
|
+
],
|
|
60922
|
+
methods: {},
|
|
60923
|
+
status: {
|
|
60924
|
+
schema: GroupActiveSchema,
|
|
60925
|
+
kind: "push"
|
|
60926
|
+
},
|
|
60927
|
+
runtimeState: GroupActiveSchema,
|
|
60928
|
+
/**
|
|
60929
|
+
* Runtime-state durability: **session** — recomputed from the members on the
|
|
60930
|
+
* next source event; a restored value would outlive the members it named.
|
|
60931
|
+
*
|
|
60932
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
60933
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
60934
|
+
*/
|
|
60935
|
+
durability: "session",
|
|
60936
|
+
history: "none"
|
|
60937
|
+
};
|
|
60938
|
+
/**
|
|
60090
60939
|
* How many members of a helper group answered (D944). Counts are integers,
|
|
60091
60940
|
* not measurements: zero included is a real count, never a stand-in reading.
|
|
60092
60941
|
* The reading itself stays on the output capability. History is none — the
|
|
@@ -60117,7 +60966,14 @@ var groupStatusCapability = {
|
|
|
60117
60966
|
deviceTypes: [
|
|
60118
60967
|
DeviceType.Sensor,
|
|
60119
60968
|
DeviceType.Switch,
|
|
60120
|
-
DeviceType.Light
|
|
60969
|
+
DeviceType.Light,
|
|
60970
|
+
DeviceType.Fan,
|
|
60971
|
+
DeviceType.Cover,
|
|
60972
|
+
DeviceType.Valve,
|
|
60973
|
+
DeviceType.Lock,
|
|
60974
|
+
DeviceType.MediaPlayer,
|
|
60975
|
+
DeviceType.Thermostat,
|
|
60976
|
+
DeviceType.Climate
|
|
60121
60977
|
],
|
|
60122
60978
|
methods: {},
|
|
60123
60979
|
status: {
|
|
@@ -61390,7 +62246,14 @@ var lockControlCapability = {
|
|
|
61390
62246
|
}),
|
|
61391
62247
|
unlock: method(object({
|
|
61392
62248
|
deviceId: number().int().nonnegative(),
|
|
61393
|
-
code: string().min(1).optional()
|
|
62249
|
+
code: string().min(1).optional(),
|
|
62250
|
+
/**
|
|
62251
|
+
* Honoured by a helper lock GROUP only (D944): unlocking several doors
|
|
62252
|
+
* with one call is refused unless the caller says it was confirmed. A
|
|
62253
|
+
* single lock ignores it. The refusal lives in the callee, so no
|
|
62254
|
+
* automation or API caller reaches the doors by skipping a dialog.
|
|
62255
|
+
*/
|
|
62256
|
+
confirm: boolean().optional()
|
|
61394
62257
|
}), _void(), {
|
|
61395
62258
|
kind: "mutation",
|
|
61396
62259
|
auth: "protected"
|
|
@@ -61427,6 +62290,44 @@ var lockControlCapability = {
|
|
|
61427
62290
|
volatileStateFields: ["lastChangedAt"]
|
|
61428
62291
|
};
|
|
61429
62292
|
/**
|
|
62293
|
+
* A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
|
|
62294
|
+
* so dashboards keep one series; every other instantaneous reading lands here.
|
|
62295
|
+
* `value` is the time integral in the source quantity's accumulated base
|
|
62296
|
+
* (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
|
|
62297
|
+
*/
|
|
62298
|
+
var MeterStatusSchema = object({
|
|
62299
|
+
value: number().nullable().meta(readingMeta("device", { label: "Total" })),
|
|
62300
|
+
lastFetchedAt: number()
|
|
62301
|
+
});
|
|
62302
|
+
var meterCapability = {
|
|
62303
|
+
name: "meter",
|
|
62304
|
+
scope: "device",
|
|
62305
|
+
deviceNative: true,
|
|
62306
|
+
mode: "singleton",
|
|
62307
|
+
deviceTypes: [DeviceType.Sensor],
|
|
62308
|
+
methods: {},
|
|
62309
|
+
status: {
|
|
62310
|
+
schema: MeterStatusSchema,
|
|
62311
|
+
kind: "push"
|
|
62312
|
+
},
|
|
62313
|
+
runtimeState: MeterStatusSchema,
|
|
62314
|
+
/**
|
|
62315
|
+
* Runtime-state durability: **restored** — the total is a cache of the
|
|
62316
|
+
* history integral. A restart shows it only until the history pass
|
|
62317
|
+
* recomputes the open period; it is not an in-memory buffer.
|
|
62318
|
+
*
|
|
62319
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
62320
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
62321
|
+
*/
|
|
62322
|
+
durability: "restored",
|
|
62323
|
+
history: { fields: { value: {
|
|
62324
|
+
kind: "counter",
|
|
62325
|
+
deadband: .001,
|
|
62326
|
+
minIntervalMs: 6e4
|
|
62327
|
+
} } },
|
|
62328
|
+
volatileStateFields: ["lastFetchedAt"]
|
|
62329
|
+
};
|
|
62330
|
+
/**
|
|
61430
62331
|
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
61431
62332
|
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
61432
62333
|
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
@@ -63371,10 +64272,20 @@ var PetFeederDeviceStatusSchema = _enum([
|
|
|
63371
64272
|
]);
|
|
63372
64273
|
var gramsPortion = number().int().min(4).max(200);
|
|
63373
64274
|
var PetFeederStatusSchema = object({
|
|
63374
|
-
/** Food
|
|
63375
|
-
*
|
|
63376
|
-
* bowl
|
|
64275
|
+
/** Food in the TANK (hopper), as the feeder's own sensor reports it
|
|
64276
|
+
* (`state.food`). A coarse level code, not grams — `0` is empty. The
|
|
64277
|
+
* amount in the bowl is `bowlFood`. On dual-hopper models this is the
|
|
64278
|
+
* combined reading; per-hopper levels live in `food1`/`food2`. Null when
|
|
64279
|
+
* the device has not reported yet. */
|
|
63377
64280
|
foodLevel: number().nullable().meta(readingMeta("none")),
|
|
64281
|
+
/** Food in the BOWL as the feeder reports it (`state.bowl`), in the
|
|
64282
|
+
* feeder's own units. Null when the model reports none. */
|
|
64283
|
+
bowlFood: number().nullable().meta(readingMeta("none")),
|
|
64284
|
+
/** True while the pet is eating (`state.eating`). */
|
|
64285
|
+
eating: boolean(),
|
|
64286
|
+
/** Whether `markFoodReplenished` does anything on this model. When false
|
|
64287
|
+
* the client must not offer it. */
|
|
64288
|
+
supportsMarkRefilled: boolean(),
|
|
63378
64289
|
/** Hopper-1 food level (grams) on dual-hopper feeders; null on
|
|
63379
64290
|
* single-hopper models. */
|
|
63380
64291
|
food1: number().nullable().meta(readingMeta("none")),
|
|
@@ -63382,8 +64293,8 @@ var PetFeederStatusSchema = object({
|
|
|
63382
64293
|
* single-hopper models. */
|
|
63383
64294
|
food2: number().nullable().meta(readingMeta("none")),
|
|
63384
64295
|
/** Derived low-food flag — mirrors the HA `food_level` binary_sensor
|
|
63385
|
-
* (`device_class: problem`, on = low). True when the
|
|
63386
|
-
*
|
|
64296
|
+
* (`device_class: problem`, on = low). True when the tank reports empty,
|
|
64297
|
+
* or, on a dual-hopper model, when EITHER hopper is empty. */
|
|
63387
64298
|
lowFood: boolean(),
|
|
63388
64299
|
/** Battery charge 0..100 (%). Null on mains-powered models or when the
|
|
63389
64300
|
* device has no battery reading. */
|
|
@@ -63451,7 +64362,9 @@ var petFeederCapability = {
|
|
|
63451
64362
|
kind: "mutation",
|
|
63452
64363
|
auth: "protected"
|
|
63453
64364
|
}),
|
|
63454
|
-
/**
|
|
64365
|
+
/** Tell the PetKit cloud a hopper was refilled so it stops its low-food
|
|
64366
|
+
* reminders (D4H/D4S/D4SH only — see `supportsMarkRefilled`). It does not
|
|
64367
|
+
* change any sensor reading. */
|
|
63455
64368
|
markFoodReplenished: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
63456
64369
|
kind: "mutation",
|
|
63457
64370
|
auth: "protected"
|
|
@@ -63524,6 +64437,7 @@ var petFeederCapability = {
|
|
|
63524
64437
|
*/
|
|
63525
64438
|
history: { fields: {
|
|
63526
64439
|
foodLevel: { kind: "gauge" },
|
|
64440
|
+
bowlFood: { kind: "gauge" },
|
|
63527
64441
|
food1: { kind: "gauge" },
|
|
63528
64442
|
food2: { kind: "gauge" },
|
|
63529
64443
|
batteryPower: {
|
|
@@ -63938,9 +64852,9 @@ var PowerMeterStatusSchema = object({
|
|
|
63938
64852
|
/** Cumulative energy in kilowatt-hours since the meter was reset. */
|
|
63939
64853
|
kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
|
|
63940
64854
|
/** Voltage in volts. */
|
|
63941
|
-
volts: number().optional().meta(readingMeta("voltage")),
|
|
64855
|
+
volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
|
|
63942
64856
|
/** Current in amperes. */
|
|
63943
|
-
amps: number().optional().meta(readingMeta("current")),
|
|
64857
|
+
amps: number().optional().meta(readingMeta("current", { label: "Current" })),
|
|
63944
64858
|
/** Apparent power in volt-amperes. */
|
|
63945
64859
|
va: number().optional().meta(readingMeta("apparent-power")),
|
|
63946
64860
|
/** Power factor (0..1) if reported. */
|
|
@@ -63948,12 +64862,74 @@ var PowerMeterStatusSchema = object({
|
|
|
63948
64862
|
/** Ms epoch when the slice was last updated. */
|
|
63949
64863
|
lastFetchedAt: number()
|
|
63950
64864
|
});
|
|
64865
|
+
/**
|
|
64866
|
+
* Loads that draw metered mains power (D951). The reading is a feature of the
|
|
64867
|
+
* device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
|
|
64868
|
+
* vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
|
|
64869
|
+
* meter or a standalone energy monitor IS the device. Cover, lock, valve,
|
|
64870
|
+
* camera and the like are left out: their draw is not what anyone meters.
|
|
64871
|
+
*/
|
|
64872
|
+
var POWER_METER_DEVICE_TYPES = [
|
|
64873
|
+
DeviceType.Sensor,
|
|
64874
|
+
DeviceType.Switch,
|
|
64875
|
+
DeviceType.Light,
|
|
64876
|
+
DeviceType.Fan,
|
|
64877
|
+
DeviceType.Climate,
|
|
64878
|
+
DeviceType.Thermostat,
|
|
64879
|
+
DeviceType.Humidifier,
|
|
64880
|
+
DeviceType.WaterHeater,
|
|
64881
|
+
DeviceType.MediaPlayer,
|
|
64882
|
+
DeviceType.Washer,
|
|
64883
|
+
DeviceType.Dryer,
|
|
64884
|
+
DeviceType.Dishwasher,
|
|
64885
|
+
DeviceType.Vacuum,
|
|
64886
|
+
DeviceType.LawnMower,
|
|
64887
|
+
DeviceType.PetFeeder
|
|
64888
|
+
];
|
|
64889
|
+
/** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
|
|
64890
|
+
var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
|
|
64891
|
+
/** One scalar target per reading; each is sourced from a device of its own (D951). */
|
|
64892
|
+
function powerTarget(id, unit) {
|
|
64893
|
+
return {
|
|
64894
|
+
id,
|
|
64895
|
+
cardinality: "scalar",
|
|
64896
|
+
vocabulary: {
|
|
64897
|
+
cap: "power-meter",
|
|
64898
|
+
stateField: id
|
|
64899
|
+
},
|
|
64900
|
+
deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
|
|
64901
|
+
apply: "add",
|
|
64902
|
+
path: id,
|
|
64903
|
+
sources: [{
|
|
64904
|
+
kind: "numeric",
|
|
64905
|
+
requiredUnit: unit,
|
|
64906
|
+
transforms: [{ kind: "identity" }]
|
|
64907
|
+
}],
|
|
64908
|
+
companions: [{
|
|
64909
|
+
kind: "clock",
|
|
64910
|
+
path: "lastFetchedAt",
|
|
64911
|
+
when: "always"
|
|
64912
|
+
}],
|
|
64913
|
+
sourceReport: { status: "assignment-status" },
|
|
64914
|
+
advancedExpression: true
|
|
64915
|
+
};
|
|
64916
|
+
}
|
|
64917
|
+
/** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
|
|
64918
|
+
var powerMeterCustomize = {
|
|
64919
|
+
version: 1,
|
|
64920
|
+
targets: [
|
|
64921
|
+
powerTarget("watts", "W"),
|
|
64922
|
+
powerTarget("kwhTotal", "kWh"),
|
|
64923
|
+
powerTarget("volts", "V"),
|
|
64924
|
+
powerTarget("amps", "A")
|
|
64925
|
+
]
|
|
64926
|
+
};
|
|
63951
64927
|
var powerMeterCapability = {
|
|
63952
64928
|
name: "power-meter",
|
|
63953
64929
|
scope: "device",
|
|
63954
64930
|
deviceNative: true,
|
|
63955
64931
|
mode: "singleton",
|
|
63956
|
-
deviceTypes:
|
|
64932
|
+
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
63957
64933
|
methods: {},
|
|
63958
64934
|
status: {
|
|
63959
64935
|
schema: PowerMeterStatusSchema,
|
|
@@ -64010,7 +64986,8 @@ var powerMeterCapability = {
|
|
|
64010
64986
|
} },
|
|
64011
64987
|
/** Clock fields: written, but excluded from the compare that decides
|
|
64012
64988
|
* whether persisting is worth a SQLite commit. */
|
|
64013
|
-
volatileStateFields: ["lastFetchedAt"]
|
|
64989
|
+
volatileStateFields: ["lastFetchedAt"],
|
|
64990
|
+
customize: powerMeterCustomize
|
|
64014
64991
|
};
|
|
64015
64992
|
/**
|
|
64016
64993
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
@@ -64699,6 +65676,8 @@ var runningCustomize = {
|
|
|
64699
65676
|
stateField: "running"
|
|
64700
65677
|
},
|
|
64701
65678
|
deviceTypes: [
|
|
65679
|
+
DeviceType.Washer,
|
|
65680
|
+
DeviceType.Dryer,
|
|
64702
65681
|
DeviceType.Dishwasher,
|
|
64703
65682
|
DeviceType.Vacuum,
|
|
64704
65683
|
DeviceType.LawnMower,
|
|
@@ -67188,7 +68167,8 @@ var switchCapability = {
|
|
|
67188
68167
|
deviceTypes: [
|
|
67189
68168
|
DeviceType.Switch,
|
|
67190
68169
|
DeviceType.Siren,
|
|
67191
|
-
DeviceType.Light
|
|
68170
|
+
DeviceType.Light,
|
|
68171
|
+
DeviceType.Fan
|
|
67192
68172
|
],
|
|
67193
68173
|
methods: { setState: method(object({
|
|
67194
68174
|
deviceId: number(),
|
|
@@ -69432,6 +70412,7 @@ function runningFromDishwasherOperation(operation, program, previous, fetchedAt,
|
|
|
69432
70412
|
if (operation === "Pause") return nativeSlice(false, "Pause", previous, transitionAtMs, fetchedAt, null);
|
|
69433
70413
|
return nativeSlice(false, operation, previous, transitionAtMs, fetchedAt, null);
|
|
69434
70414
|
}
|
|
70415
|
+
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
69435
70416
|
/**
|
|
69436
70417
|
* Frozen unit → spec table. `•` in the doc marks each dimension's canonical
|
|
69437
70418
|
* unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
|
|
@@ -69816,7 +70797,7 @@ function tryConvertUnit(value, from, to) {
|
|
|
69816
70797
|
return fromCanonical(toCanonical(value, fromSpec), toSpec);
|
|
69817
70798
|
}
|
|
69818
70799
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
69819
|
-
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Camera, DeviceType.Camera, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
70800
|
+
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
69820
70801
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
69821
70802
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
69822
70803
|
new Set(CAP_NAMES);
|
|
@@ -70145,6 +71126,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
70145
71126
|
featureProbe: featureProbeCapability,
|
|
70146
71127
|
flood: floodCapability,
|
|
70147
71128
|
gas: gasCapability,
|
|
71129
|
+
groupActive: groupActiveCapability,
|
|
70148
71130
|
groupCommands: groupCommandsCapability,
|
|
70149
71131
|
groupStatus: groupStatusCapability,
|
|
70150
71132
|
heatingValve: heatingValveCapability,
|
|
@@ -70156,6 +71138,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
70156
71138
|
lawnMowerControl: lawnMowerControlCapability,
|
|
70157
71139
|
lockControl: lockControlCapability,
|
|
70158
71140
|
mediaPlayer: mediaPlayerCapability,
|
|
71141
|
+
meter: meterCapability,
|
|
70159
71142
|
motion: motionCapability,
|
|
70160
71143
|
motionTrigger: motionTriggerCapability,
|
|
70161
71144
|
motionZones: motionZonesCapability,
|
|
@@ -71415,7 +72398,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
71415
72398
|
return toDeviceSummary(device, this.addonId);
|
|
71416
72399
|
}
|
|
71417
72400
|
};
|
|
71418
|
-
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
|
|
72401
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
|
|
71419
72402
|
new Set(Object.values(DeviceType));
|
|
71420
72403
|
Object.freeze({
|
|
71421
72404
|
"accessories.getStatus": {
|
|
@@ -71432,6 +72415,104 @@ Object.freeze({
|
|
|
71432
72415
|
access: "create",
|
|
71433
72416
|
role: "admin"
|
|
71434
72417
|
},
|
|
72418
|
+
"actions.create": {
|
|
72419
|
+
capName: "actions",
|
|
72420
|
+
capScope: "system",
|
|
72421
|
+
addonId: null,
|
|
72422
|
+
access: "create",
|
|
72423
|
+
role: "admin"
|
|
72424
|
+
},
|
|
72425
|
+
"actions.delete": {
|
|
72426
|
+
capName: "actions",
|
|
72427
|
+
capScope: "system",
|
|
72428
|
+
addonId: null,
|
|
72429
|
+
access: "delete",
|
|
72430
|
+
role: "admin"
|
|
72431
|
+
},
|
|
72432
|
+
"actions.get": {
|
|
72433
|
+
capName: "actions",
|
|
72434
|
+
capScope: "system",
|
|
72435
|
+
addonId: null,
|
|
72436
|
+
access: "view",
|
|
72437
|
+
role: "viewer"
|
|
72438
|
+
},
|
|
72439
|
+
"actions.getLinks": {
|
|
72440
|
+
capName: "actions",
|
|
72441
|
+
capScope: "system",
|
|
72442
|
+
addonId: null,
|
|
72443
|
+
access: "view",
|
|
72444
|
+
role: "admin"
|
|
72445
|
+
},
|
|
72446
|
+
"actions.getTraces": {
|
|
72447
|
+
capName: "actions",
|
|
72448
|
+
capScope: "system",
|
|
72449
|
+
addonId: null,
|
|
72450
|
+
access: "view",
|
|
72451
|
+
role: "admin"
|
|
72452
|
+
},
|
|
72453
|
+
"actions.list": {
|
|
72454
|
+
capName: "actions",
|
|
72455
|
+
capScope: "system",
|
|
72456
|
+
addonId: null,
|
|
72457
|
+
access: "view",
|
|
72458
|
+
role: "admin"
|
|
72459
|
+
},
|
|
72460
|
+
"actions.listForCamera": {
|
|
72461
|
+
capName: "actions",
|
|
72462
|
+
capScope: "system",
|
|
72463
|
+
addonId: null,
|
|
72464
|
+
access: "view",
|
|
72465
|
+
role: "viewer"
|
|
72466
|
+
},
|
|
72467
|
+
"actions.run": {
|
|
72468
|
+
capName: "actions",
|
|
72469
|
+
capScope: "system",
|
|
72470
|
+
addonId: null,
|
|
72471
|
+
access: "create",
|
|
72472
|
+
role: "viewer"
|
|
72473
|
+
},
|
|
72474
|
+
"actions.setLinks": {
|
|
72475
|
+
capName: "actions",
|
|
72476
|
+
capScope: "system",
|
|
72477
|
+
addonId: null,
|
|
72478
|
+
access: "create",
|
|
72479
|
+
role: "admin"
|
|
72480
|
+
},
|
|
72481
|
+
"actions.stop": {
|
|
72482
|
+
capName: "actions",
|
|
72483
|
+
capScope: "system",
|
|
72484
|
+
addonId: null,
|
|
72485
|
+
access: "create",
|
|
72486
|
+
role: "admin"
|
|
72487
|
+
},
|
|
72488
|
+
"actions.update": {
|
|
72489
|
+
capName: "actions",
|
|
72490
|
+
capScope: "system",
|
|
72491
|
+
addonId: null,
|
|
72492
|
+
access: "create",
|
|
72493
|
+
role: "admin"
|
|
72494
|
+
},
|
|
72495
|
+
"actionSource.listActions": {
|
|
72496
|
+
capName: "action-source",
|
|
72497
|
+
capScope: "system",
|
|
72498
|
+
addonId: null,
|
|
72499
|
+
access: "view",
|
|
72500
|
+
role: "admin"
|
|
72501
|
+
},
|
|
72502
|
+
"actionSource.runAction": {
|
|
72503
|
+
capName: "action-source",
|
|
72504
|
+
capScope: "system",
|
|
72505
|
+
addonId: null,
|
|
72506
|
+
access: "create",
|
|
72507
|
+
role: "admin"
|
|
72508
|
+
},
|
|
72509
|
+
"actionSource.stopAction": {
|
|
72510
|
+
capName: "action-source",
|
|
72511
|
+
capScope: "system",
|
|
72512
|
+
addonId: null,
|
|
72513
|
+
access: "create",
|
|
72514
|
+
role: "admin"
|
|
72515
|
+
},
|
|
71435
72516
|
"addonPages.listPages": {
|
|
71436
72517
|
capName: "addon-pages",
|
|
71437
72518
|
capScope: "system",
|
|
@@ -72713,6 +73794,13 @@ Object.freeze({
|
|
|
72713
73794
|
access: "view",
|
|
72714
73795
|
role: "admin"
|
|
72715
73796
|
},
|
|
73797
|
+
"coreBlocks.getAutomationRunState": {
|
|
73798
|
+
capName: "core-blocks",
|
|
73799
|
+
capScope: "system",
|
|
73800
|
+
addonId: null,
|
|
73801
|
+
access: "view",
|
|
73802
|
+
role: "admin"
|
|
73803
|
+
},
|
|
72716
73804
|
"coreBlocks.getBlockSettings": {
|
|
72717
73805
|
capName: "core-blocks",
|
|
72718
73806
|
capScope: "system",
|
|
@@ -72734,6 +73822,13 @@ Object.freeze({
|
|
|
72734
73822
|
access: "view",
|
|
72735
73823
|
role: "admin"
|
|
72736
73824
|
},
|
|
73825
|
+
"coreBlocks.getTraces": {
|
|
73826
|
+
capName: "core-blocks",
|
|
73827
|
+
capScope: "system",
|
|
73828
|
+
addonId: null,
|
|
73829
|
+
access: "view",
|
|
73830
|
+
role: "admin"
|
|
73831
|
+
},
|
|
72737
73832
|
"coreBlocks.getTypeDefs": {
|
|
72738
73833
|
capName: "core-blocks",
|
|
72739
73834
|
capScope: "system",
|
|
@@ -72769,6 +73864,13 @@ Object.freeze({
|
|
|
72769
73864
|
access: "create",
|
|
72770
73865
|
role: "admin"
|
|
72771
73866
|
},
|
|
73867
|
+
"coreBlocks.reportAutomationRun": {
|
|
73868
|
+
capName: "core-blocks",
|
|
73869
|
+
capScope: "system",
|
|
73870
|
+
addonId: null,
|
|
73871
|
+
access: "create",
|
|
73872
|
+
role: "admin"
|
|
73873
|
+
},
|
|
72772
73874
|
"coreBlocks.restart": {
|
|
72773
73875
|
capName: "core-blocks",
|
|
72774
73876
|
capScope: "system",
|
|
@@ -74638,6 +75740,13 @@ Object.freeze({
|
|
|
74638
75740
|
access: "view",
|
|
74639
75741
|
role: "viewer"
|
|
74640
75742
|
},
|
|
75743
|
+
"groupActive.getStatus": {
|
|
75744
|
+
capName: "group-active",
|
|
75745
|
+
capScope: "device",
|
|
75746
|
+
addonId: null,
|
|
75747
|
+
access: "view",
|
|
75748
|
+
role: "viewer"
|
|
75749
|
+
},
|
|
74641
75750
|
"groupCommands.getStatus": {
|
|
74642
75751
|
capName: "group-commands",
|
|
74643
75752
|
capScope: "device",
|
|
@@ -75604,6 +76713,13 @@ Object.freeze({
|
|
|
75604
76713
|
access: "create",
|
|
75605
76714
|
role: "admin"
|
|
75606
76715
|
},
|
|
76716
|
+
"meter.getStatus": {
|
|
76717
|
+
capName: "meter",
|
|
76718
|
+
capScope: "device",
|
|
76719
|
+
addonId: null,
|
|
76720
|
+
access: "view",
|
|
76721
|
+
role: "viewer"
|
|
76722
|
+
},
|
|
75607
76723
|
"metricsProvider.collectSnapshot": {
|
|
75608
76724
|
capName: "metrics-provider",
|
|
75609
76725
|
capScope: "system",
|
|
@@ -83702,7 +84818,39 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
83702
84818
|
phrase: t("When {device} finishes", "Quando {device} finisce"),
|
|
83703
84819
|
icon: "square"
|
|
83704
84820
|
}
|
|
83705
|
-
}
|
|
84821
|
+
},
|
|
84822
|
+
overrides: { byType: {
|
|
84823
|
+
[DeviceType.Washer]: {
|
|
84824
|
+
state: { running: {
|
|
84825
|
+
whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
|
|
84826
|
+
whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
|
|
84827
|
+
} },
|
|
84828
|
+
events: {
|
|
84829
|
+
onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
|
|
84830
|
+
onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
|
|
84831
|
+
}
|
|
84832
|
+
},
|
|
84833
|
+
[DeviceType.Dryer]: {
|
|
84834
|
+
state: { running: {
|
|
84835
|
+
whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
|
|
84836
|
+
whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
|
|
84837
|
+
} },
|
|
84838
|
+
events: {
|
|
84839
|
+
onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
|
|
84840
|
+
onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
|
|
84841
|
+
}
|
|
84842
|
+
},
|
|
84843
|
+
[DeviceType.Dishwasher]: {
|
|
84844
|
+
state: { running: {
|
|
84845
|
+
whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
|
|
84846
|
+
whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
|
|
84847
|
+
} },
|
|
84848
|
+
events: {
|
|
84849
|
+
onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
|
|
84850
|
+
onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
|
|
84851
|
+
}
|
|
84852
|
+
}
|
|
84853
|
+
} }
|
|
83706
84854
|
},
|
|
83707
84855
|
update: {
|
|
83708
84856
|
scope: "device",
|
|
@@ -84474,6 +85622,10 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
84474
85622
|
icon: "flame"
|
|
84475
85623
|
}]
|
|
84476
85624
|
},
|
|
85625
|
+
"group-active": {
|
|
85626
|
+
scope: "device",
|
|
85627
|
+
noun: t("Group state", "Stato del gruppo")
|
|
85628
|
+
},
|
|
84477
85629
|
"group-commands": {
|
|
84478
85630
|
scope: "device",
|
|
84479
85631
|
noun: t("Group command", "Comando di gruppo")
|
|
@@ -84530,6 +85682,17 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
84530
85682
|
}],
|
|
84531
85683
|
events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
|
|
84532
85684
|
},
|
|
85685
|
+
meter: {
|
|
85686
|
+
scope: "device",
|
|
85687
|
+
noun: t("Meter", "Contatore"),
|
|
85688
|
+
state: [{
|
|
85689
|
+
kind: "numeric",
|
|
85690
|
+
field: "value",
|
|
85691
|
+
quantity: t("the total", "il totale"),
|
|
85692
|
+
label: t("Total", "Totale"),
|
|
85693
|
+
icon: "gauge"
|
|
85694
|
+
}]
|
|
85695
|
+
},
|
|
84533
85696
|
"numeric-sensor": {
|
|
84534
85697
|
scope: "device",
|
|
84535
85698
|
noun: t("Sensor", "Sensore"),
|
|
@@ -84862,6 +86025,8 @@ var ML = "model and training machinery — no device action";
|
|
|
84862
86025
|
var INTERNAL = "internal plumbing between addons — never operator-facing";
|
|
84863
86026
|
/** Every system-scoped cap, with why it is not a device's vocabulary. */
|
|
84864
86027
|
var SYSTEM_CAP_VOCABULARY = {
|
|
86028
|
+
"action-source": system(INTERNAL),
|
|
86029
|
+
actions: system("declared actions — run from the Actions page and from the cameras that link them"),
|
|
84865
86030
|
"addon-pages": system(UI_PLUMBING),
|
|
84866
86031
|
"addon-pages-source": system(UI_PLUMBING),
|
|
84867
86032
|
"addon-routes": system(UI_PLUMBING),
|
|
@@ -85002,8 +86167,47 @@ function projectAction(cap, action) {
|
|
|
85002
86167
|
};
|
|
85003
86168
|
}
|
|
85004
86169
|
Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
|
|
85005
|
-
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
|
|
86170
|
+
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
|
|
85006
86171
|
DeviceFeature.BatteryOperated;
|
|
86172
|
+
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
86173
|
+
var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
|
|
86174
|
+
var HOMEKIT_REASON = "HomeKit has no laundry-appliance service. The export tab would pair an accessory with nothing on it: running-state is withheld until a null reading can be unavailable, and power is not a HomeKit service this export honours.";
|
|
86175
|
+
var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
|
|
86176
|
+
var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
|
|
86177
|
+
var MATTER_REASON = "No Matter export is built. The closest device types are Laundry Washer (0x0073) and Laundry Dryer (0x0074); a power threshold is not the Laundry Washer Mode cluster.";
|
|
86178
|
+
function withheld(reason) {
|
|
86179
|
+
return {
|
|
86180
|
+
[DeviceType.Washer]: {
|
|
86181
|
+
exported: false,
|
|
86182
|
+
reason
|
|
86183
|
+
},
|
|
86184
|
+
[DeviceType.Dryer]: {
|
|
86185
|
+
exported: false,
|
|
86186
|
+
reason
|
|
86187
|
+
},
|
|
86188
|
+
[DeviceType.Dishwasher]: {
|
|
86189
|
+
exported: false,
|
|
86190
|
+
reason
|
|
86191
|
+
}
|
|
86192
|
+
};
|
|
86193
|
+
}
|
|
86194
|
+
function offered(received) {
|
|
86195
|
+
return {
|
|
86196
|
+
[DeviceType.Washer]: {
|
|
86197
|
+
exported: true,
|
|
86198
|
+
as: received
|
|
86199
|
+
},
|
|
86200
|
+
[DeviceType.Dryer]: {
|
|
86201
|
+
exported: true,
|
|
86202
|
+
as: received
|
|
86203
|
+
},
|
|
86204
|
+
[DeviceType.Dishwasher]: {
|
|
86205
|
+
exported: true,
|
|
86206
|
+
as: received
|
|
86207
|
+
}
|
|
86208
|
+
};
|
|
86209
|
+
}
|
|
86210
|
+
offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
|
|
85007
86211
|
Object.freeze({
|
|
85008
86212
|
"accessories.getStatus": [{
|
|
85009
86213
|
name: "deviceId",
|
|
@@ -85019,6 +86223,31 @@ Object.freeze({
|
|
|
85019
86223
|
form: "single",
|
|
85020
86224
|
optional: false
|
|
85021
86225
|
}],
|
|
86226
|
+
"actions.get": [{
|
|
86227
|
+
name: "deviceId",
|
|
86228
|
+
form: "single",
|
|
86229
|
+
optional: true
|
|
86230
|
+
}],
|
|
86231
|
+
"actions.getLinks": [{
|
|
86232
|
+
name: "deviceId",
|
|
86233
|
+
form: "single",
|
|
86234
|
+
optional: false
|
|
86235
|
+
}],
|
|
86236
|
+
"actions.listForCamera": [{
|
|
86237
|
+
name: "deviceId",
|
|
86238
|
+
form: "single",
|
|
86239
|
+
optional: false
|
|
86240
|
+
}],
|
|
86241
|
+
"actions.run": [{
|
|
86242
|
+
name: "deviceId",
|
|
86243
|
+
form: "single",
|
|
86244
|
+
optional: true
|
|
86245
|
+
}],
|
|
86246
|
+
"actions.setLinks": [{
|
|
86247
|
+
name: "deviceId",
|
|
86248
|
+
form: "single",
|
|
86249
|
+
optional: false
|
|
86250
|
+
}],
|
|
85022
86251
|
"addonSettings.getDeviceSettings": [{
|
|
85023
86252
|
name: "deviceId",
|
|
85024
86253
|
form: "single",
|
|
@@ -85970,6 +87199,11 @@ Object.freeze({
|
|
|
85970
87199
|
form: "single",
|
|
85971
87200
|
optional: false
|
|
85972
87201
|
}],
|
|
87202
|
+
"groupActive.getStatus": [{
|
|
87203
|
+
name: "deviceId",
|
|
87204
|
+
form: "single",
|
|
87205
|
+
optional: false
|
|
87206
|
+
}],
|
|
85973
87207
|
"groupCommands.getStatus": [{
|
|
85974
87208
|
name: "deviceId",
|
|
85975
87209
|
form: "single",
|
|
@@ -86180,6 +87414,11 @@ Object.freeze({
|
|
|
86180
87414
|
form: "single",
|
|
86181
87415
|
optional: false
|
|
86182
87416
|
}],
|
|
87417
|
+
"meter.getStatus": [{
|
|
87418
|
+
name: "deviceId",
|
|
87419
|
+
form: "single",
|
|
87420
|
+
optional: false
|
|
87421
|
+
}],
|
|
86183
87422
|
"motion.getStatus": [{
|
|
86184
87423
|
name: "deviceId",
|
|
86185
87424
|
form: "single",
|
|
@@ -87787,7 +89026,6 @@ Object.freeze({
|
|
|
87787
89026
|
"network-access": "ingress",
|
|
87788
89027
|
"smtp-provider": "email"
|
|
87789
89028
|
});
|
|
87790
|
-
DeviceType.Camera;
|
|
87791
89029
|
/**
|
|
87792
89030
|
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
87793
89031
|
* `z.infer`, they do not restate the shape.
|
|
@@ -89181,6 +90419,448 @@ function ingestReading(input) {
|
|
|
89181
90419
|
value: converted
|
|
89182
90420
|
};
|
|
89183
90421
|
}
|
|
90422
|
+
/**
|
|
90423
|
+
* Where an OAuth round-trip ends: the admin-ui completion route
|
|
90424
|
+
* (`/sso-complete`, the page every popup sign-in already lands on), with the
|
|
90425
|
+
* OUTCOME in the URL fragment.
|
|
90426
|
+
*
|
|
90427
|
+
* The outcome is a status plus a reason CODE from a closed list. It never
|
|
90428
|
+
* carries a token, a code, a client secret or a free-form error message: the
|
|
90429
|
+
* page words each code itself, and an unknown code reads as `unknown` rather
|
|
90430
|
+
* than being echoed. Fragment only, so nothing here reaches a server log.
|
|
90431
|
+
*
|
|
90432
|
+
* Pure and browser-safe.
|
|
90433
|
+
*/
|
|
90434
|
+
/** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
|
|
90435
|
+
var OAUTH_LANDING_PATH = "/sso-complete";
|
|
90436
|
+
/**
|
|
90437
|
+
* The landing URL for `outcome`. `origin` is the hub address the flow already
|
|
90438
|
+
* runs on (from the `local-network` palette role, never a literal); `null`
|
|
90439
|
+
* gives a hub-relative URL.
|
|
90440
|
+
*/
|
|
90441
|
+
function oauthLandingUrl(origin, outcome) {
|
|
90442
|
+
const params = new URLSearchParams({
|
|
90443
|
+
return: "oauth",
|
|
90444
|
+
status: outcome.status
|
|
90445
|
+
});
|
|
90446
|
+
if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
|
|
90447
|
+
if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
|
|
90448
|
+
if (outcome.integration !== void 0) params.set("integration", outcome.integration);
|
|
90449
|
+
if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
|
|
90450
|
+
if (outcome.action === "disconnect") params.set("action", "disconnect");
|
|
90451
|
+
return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
|
|
90452
|
+
}
|
|
90453
|
+
var LOOPBACK_HOSTS = new Set([
|
|
90454
|
+
"127.0.0.1",
|
|
90455
|
+
"localhost",
|
|
90456
|
+
"[::1]"
|
|
90457
|
+
]);
|
|
90458
|
+
function hostWithoutPort(host) {
|
|
90459
|
+
if (host.startsWith("[")) return host.slice(0, host.indexOf("]") + 1);
|
|
90460
|
+
const colon = host.indexOf(":");
|
|
90461
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
90462
|
+
}
|
|
90463
|
+
function isLoopbackHost(host) {
|
|
90464
|
+
return LOOPBACK_HOSTS.has(hostWithoutPort(host.toLowerCase()));
|
|
90465
|
+
}
|
|
90466
|
+
/** An allow-list entry is valid when it is a bare host[:port] and `http` only targets loopback. */
|
|
90467
|
+
function isValidUpstream(upstream) {
|
|
90468
|
+
if (upstream.host.length === 0 || !/^[a-z0-9.[\]:-]+$/.test(upstream.host)) return false;
|
|
90469
|
+
return upstream.scheme === "https" || isLoopbackHost(upstream.host);
|
|
90470
|
+
}
|
|
90471
|
+
function findUpstream(route, host) {
|
|
90472
|
+
const wanted = host.toLowerCase();
|
|
90473
|
+
return route.upstreams.find((upstream) => upstream.host === wanted);
|
|
90474
|
+
}
|
|
90475
|
+
/** `<basePath>/<host><pathname><search>` — what the browser is sent to. */
|
|
90476
|
+
function proxyPathFor(route, host, pathname, search = "") {
|
|
90477
|
+
return `${route.basePath}/${host}${pathname.startsWith("/") ? pathname : `/${pathname}`}${search}`;
|
|
90478
|
+
}
|
|
90479
|
+
/** Split `/<attemptId>/<host>/<rest…>[?query]` (the data-plane view of a request). */
|
|
90480
|
+
function parseProxyPath(rawUrl) {
|
|
90481
|
+
const queryAt = rawUrl.indexOf("?");
|
|
90482
|
+
const path = queryAt === -1 ? rawUrl : rawUrl.slice(0, queryAt);
|
|
90483
|
+
const search = queryAt === -1 ? "" : rawUrl.slice(queryAt);
|
|
90484
|
+
const match = /^\/([A-Za-z0-9_-]{8,128})\/([^/]+)(\/.*)?$/.exec(path);
|
|
90485
|
+
if (match === null) return null;
|
|
90486
|
+
const attemptId = match[1];
|
|
90487
|
+
const host = match[2];
|
|
90488
|
+
if (attemptId === void 0 || host === void 0) return null;
|
|
90489
|
+
return {
|
|
90490
|
+
attemptId,
|
|
90491
|
+
host: host.toLowerCase(),
|
|
90492
|
+
pathname: match[3] ?? "/",
|
|
90493
|
+
search
|
|
90494
|
+
};
|
|
90495
|
+
}
|
|
90496
|
+
/** The vendor URL a proxied request targets, or null when the host would not stay `host`. */
|
|
90497
|
+
function upstreamUrlFor(upstream, parsed) {
|
|
90498
|
+
try {
|
|
90499
|
+
const url = new URL(`${upstream.scheme}://${upstream.host}${parsed.pathname}${parsed.search}`);
|
|
90500
|
+
return url.host.toLowerCase() === upstream.host ? url : null;
|
|
90501
|
+
} catch {
|
|
90502
|
+
return null;
|
|
90503
|
+
}
|
|
90504
|
+
}
|
|
90505
|
+
function escapeRegex(value) {
|
|
90506
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
90507
|
+
}
|
|
90508
|
+
/** A Location (or any URL value) to a vendor host becomes a proxy path; anything else is unchanged. */
|
|
90509
|
+
function rewriteLocation(location, currentUpstream, route) {
|
|
90510
|
+
let resolved;
|
|
90511
|
+
try {
|
|
90512
|
+
resolved = new URL(location, `${currentUpstream.scheme}://${currentUpstream.host}/`);
|
|
90513
|
+
} catch {
|
|
90514
|
+
return location;
|
|
90515
|
+
}
|
|
90516
|
+
if (resolved.protocol !== "https:" && resolved.protocol !== "http:") return location;
|
|
90517
|
+
const target = findUpstream(route, resolved.host);
|
|
90518
|
+
if (target === void 0) return location;
|
|
90519
|
+
return `${proxyPathFor(route, target.host, resolved.pathname, resolved.search)}${resolved.hash}`;
|
|
90520
|
+
}
|
|
90521
|
+
/** Characters that may precede a URL that STARTS a value (never `=`, `?`, `&`, `%3D`). */
|
|
90522
|
+
var VALUE_START = String.raw`(?<=^|["'(\s>,\[{])`;
|
|
90523
|
+
/**
|
|
90524
|
+
* Rewrite vendor URLs in a text body: absolute (`https://host/…`), protocol-relative
|
|
90525
|
+
* (`//host/…`) and JSON-escaped (`https:\/\/host\/…`) when they start a value; and, in
|
|
90526
|
+
* HTML/CSS, root-relative references (`href="/x"`, `url(/x)`) which would otherwise
|
|
90527
|
+
* escape the proxy mount and land on the hub.
|
|
90528
|
+
*/
|
|
90529
|
+
function rewriteBody(body, kind, currentUpstream, route) {
|
|
90530
|
+
let out = body;
|
|
90531
|
+
for (const upstream of route.upstreams) {
|
|
90532
|
+
const host = escapeRegex(upstream.host);
|
|
90533
|
+
const plain = new RegExp(`${VALUE_START}(?:https?:)?//${host}(?=[/"'?#\\s)\\]},<]|$)`, "g");
|
|
90534
|
+
out = out.replace(plain, `${route.basePath}/${upstream.host}`);
|
|
90535
|
+
const escapedBase = `${route.basePath}/${upstream.host}`.replace(/\//g, "\\/");
|
|
90536
|
+
const escaped = new RegExp(`${VALUE_START}(?:https?:)?\\\\/\\\\/${host}(?=\\\\/|["'?#\\s)\\]},<]|$)`, "g");
|
|
90537
|
+
out = out.replace(escaped, escapedBase);
|
|
90538
|
+
}
|
|
90539
|
+
const current = `${route.basePath}/${currentUpstream.host}/`;
|
|
90540
|
+
const notMounted = `(?!/)(?!${escapeRegex(route.basePath.slice(1))}/)`;
|
|
90541
|
+
if (kind === "html") {
|
|
90542
|
+
const attribute = String.raw`(\b(?:href|src|action|formaction|poster|data-href|data-src)\s*=\s*["'])/`;
|
|
90543
|
+
out = out.replace(new RegExp(`${attribute}${notMounted}`, "gi"), `$1${current}`);
|
|
90544
|
+
}
|
|
90545
|
+
if (kind === "html" || kind === "css") {
|
|
90546
|
+
const cssUrl = String.raw`(url\(\s*["']?)/`;
|
|
90547
|
+
out = out.replace(new RegExp(`${cssUrl}${notMounted}`, "gi"), `$1${current}`);
|
|
90548
|
+
}
|
|
90549
|
+
return out;
|
|
90550
|
+
}
|
|
90551
|
+
/**
|
|
90552
|
+
* A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
|
|
90553
|
+
* origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
|
|
90554
|
+
*/
|
|
90555
|
+
function rewriteSetCookie(cookie, route) {
|
|
90556
|
+
const kept = cookie.split(";").map((part) => part.trim()).filter((part, index) => {
|
|
90557
|
+
if (index === 0) return true;
|
|
90558
|
+
const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
|
|
90559
|
+
return name !== "domain" && name !== "path";
|
|
90560
|
+
});
|
|
90561
|
+
kept.push(`Path=${route.basePath}/`);
|
|
90562
|
+
return kept.join("; ");
|
|
90563
|
+
}
|
|
90564
|
+
/**
|
|
90565
|
+
* A browser `Referer` that points into this proxy, turned back into the vendor URL it
|
|
90566
|
+
* stands for. A Referer that is not ours is dropped (null): it names the hub.
|
|
90567
|
+
*/
|
|
90568
|
+
function rewriteReferer(referer, route) {
|
|
90569
|
+
let url;
|
|
90570
|
+
try {
|
|
90571
|
+
url = new URL(referer, "http://hub.invalid");
|
|
90572
|
+
} catch {
|
|
90573
|
+
return null;
|
|
90574
|
+
}
|
|
90575
|
+
if (!url.pathname.startsWith(`${route.basePath}/`)) return null;
|
|
90576
|
+
const rest = /^\/([^/]+)(\/.*)?$/.exec(url.pathname.slice(route.basePath.length));
|
|
90577
|
+
const host = rest?.[1]?.toLowerCase();
|
|
90578
|
+
if (host === void 0) return null;
|
|
90579
|
+
const upstream = findUpstream(route, host);
|
|
90580
|
+
if (upstream === void 0) return null;
|
|
90581
|
+
return `${upstream.scheme}://${upstream.host}${rest?.[2] ?? "/"}${url.search}`;
|
|
90582
|
+
}
|
|
90583
|
+
/** Cookies the browser holds for the hub itself must never reach a vendor. */
|
|
90584
|
+
function filterForwardedCookies(cookieHeader) {
|
|
90585
|
+
return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
|
|
90586
|
+
}
|
|
90587
|
+
var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
|
|
90588
|
+
var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
|
|
90589
|
+
/** Request headers that are never forwarded to the vendor. */
|
|
90590
|
+
var DROP_REQUEST_HEADERS = new Set([
|
|
90591
|
+
"host",
|
|
90592
|
+
"connection",
|
|
90593
|
+
"keep-alive",
|
|
90594
|
+
"proxy-authorization",
|
|
90595
|
+
"proxy-connection",
|
|
90596
|
+
"te",
|
|
90597
|
+
"trailer",
|
|
90598
|
+
"transfer-encoding",
|
|
90599
|
+
"upgrade",
|
|
90600
|
+
"content-length",
|
|
90601
|
+
"accept-encoding",
|
|
90602
|
+
"authorization",
|
|
90603
|
+
"cookie",
|
|
90604
|
+
"origin",
|
|
90605
|
+
"referer",
|
|
90606
|
+
"x-camstack-dataplane-secret",
|
|
90607
|
+
"x-forwarded-for",
|
|
90608
|
+
"x-forwarded-host",
|
|
90609
|
+
"x-forwarded-proto",
|
|
90610
|
+
"x-forwarded-prefix",
|
|
90611
|
+
"x-real-ip",
|
|
90612
|
+
"forwarded"
|
|
90613
|
+
]);
|
|
90614
|
+
/**
|
|
90615
|
+
* Response headers dropped: framing/CSP/HSTS that break a page under another origin, and
|
|
90616
|
+
* the ones that no longer describe the (decoded, possibly rewritten) body.
|
|
90617
|
+
*/
|
|
90618
|
+
var DROP_RESPONSE_HEADERS = new Set([
|
|
90619
|
+
"content-security-policy",
|
|
90620
|
+
"content-security-policy-report-only",
|
|
90621
|
+
"x-frame-options",
|
|
90622
|
+
"strict-transport-security",
|
|
90623
|
+
"public-key-pins",
|
|
90624
|
+
"report-to",
|
|
90625
|
+
"nel",
|
|
90626
|
+
"content-encoding",
|
|
90627
|
+
"content-length",
|
|
90628
|
+
"transfer-encoding",
|
|
90629
|
+
"connection",
|
|
90630
|
+
"keep-alive",
|
|
90631
|
+
"set-cookie",
|
|
90632
|
+
"location"
|
|
90633
|
+
]);
|
|
90634
|
+
function bodyKindOf(contentType) {
|
|
90635
|
+
const type = contentType.toLowerCase();
|
|
90636
|
+
if (type.includes("text/html") || type.includes("application/xhtml")) return "html";
|
|
90637
|
+
if (type.includes("text/css")) return "css";
|
|
90638
|
+
if (type.startsWith("text/") || type.includes("javascript") || type.includes("json") || type.includes("xml")) return "other";
|
|
90639
|
+
return null;
|
|
90640
|
+
}
|
|
90641
|
+
function firstHeader(value) {
|
|
90642
|
+
return Array.isArray(value) ? value[0] : value;
|
|
90643
|
+
}
|
|
90644
|
+
async function readRequestBody(req) {
|
|
90645
|
+
const chunks = [];
|
|
90646
|
+
let size = 0;
|
|
90647
|
+
for await (const chunk of req) {
|
|
90648
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
|
|
90649
|
+
size += buf.length;
|
|
90650
|
+
if (size > MAX_REQUEST_BODY_BYTES) return "too-large";
|
|
90651
|
+
chunks.push(buf);
|
|
90652
|
+
}
|
|
90653
|
+
return Buffer.concat(chunks);
|
|
90654
|
+
}
|
|
90655
|
+
function sendPlain(res, status, text) {
|
|
90656
|
+
res.writeHead(status, {
|
|
90657
|
+
"content-type": "text/plain; charset=utf-8",
|
|
90658
|
+
"cache-control": "no-store"
|
|
90659
|
+
});
|
|
90660
|
+
res.end(text);
|
|
90661
|
+
}
|
|
90662
|
+
var VendorLoginProxy = class {
|
|
90663
|
+
#addonId;
|
|
90664
|
+
#prefix;
|
|
90665
|
+
#logger;
|
|
90666
|
+
#now;
|
|
90667
|
+
#fetch;
|
|
90668
|
+
#attempts = /* @__PURE__ */ new Map();
|
|
90669
|
+
constructor(options) {
|
|
90670
|
+
this.#addonId = options.addonId;
|
|
90671
|
+
this.#prefix = options.prefix;
|
|
90672
|
+
this.#logger = options.logger;
|
|
90673
|
+
this.#now = options.now ?? (() => Date.now());
|
|
90674
|
+
this.#fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
90675
|
+
}
|
|
90676
|
+
/** The `DataPlaneHandler` to pass to `ctx.dataPlane.serve({ prefix, access: 'admin', … })`. */
|
|
90677
|
+
handler = (req, res) => this.#handle(req, res);
|
|
90678
|
+
/** Register an attempt. A one-shot capability: refused hosts are rejected up front. */
|
|
90679
|
+
open(spec) {
|
|
90680
|
+
for (const upstream of spec.upstreams) if (!isValidUpstream(upstream)) throw new Error(`vendor login proxy: upstream ${upstream.host} is not allowed`);
|
|
90681
|
+
this.#attempts.set(spec.attemptId, {
|
|
90682
|
+
expiresAtMs: spec.expiresAtMs,
|
|
90683
|
+
upstreams: spec.upstreams,
|
|
90684
|
+
evaluate: async (view) => {
|
|
90685
|
+
const verdict = await spec.terminal(view);
|
|
90686
|
+
if (verdict.kind === "continue") return verdict;
|
|
90687
|
+
return {
|
|
90688
|
+
kind: "done",
|
|
90689
|
+
landing: await spec.onDone(verdict.payload)
|
|
90690
|
+
};
|
|
90691
|
+
}
|
|
90692
|
+
});
|
|
90693
|
+
}
|
|
90694
|
+
close(attemptId) {
|
|
90695
|
+
this.#attempts.delete(attemptId);
|
|
90696
|
+
}
|
|
90697
|
+
isOpen(attemptId) {
|
|
90698
|
+
const attempt = this.#attempts.get(attemptId);
|
|
90699
|
+
return attempt !== void 0 && this.#now() <= attempt.expiresAtMs;
|
|
90700
|
+
}
|
|
90701
|
+
/**
|
|
90702
|
+
* The hub-relative path that opens `vendorUrl` through the proxy for this attempt:
|
|
90703
|
+
* the URL the browser is sent to. Null when the host is not in the attempt's list.
|
|
90704
|
+
*/
|
|
90705
|
+
entryPath(attemptId, vendorUrl) {
|
|
90706
|
+
const attempt = this.#attempts.get(attemptId);
|
|
90707
|
+
if (attempt === void 0) return null;
|
|
90708
|
+
const url = new URL(vendorUrl);
|
|
90709
|
+
if (findUpstream({
|
|
90710
|
+
basePath: "",
|
|
90711
|
+
upstreams: attempt.upstreams
|
|
90712
|
+
}, url.host) === void 0) return null;
|
|
90713
|
+
return `/addon/${this.#addonId}/${this.#prefix}/${attemptId}/${url.host}${url.pathname}${url.search}`;
|
|
90714
|
+
}
|
|
90715
|
+
#routeFor(attemptId, attempt, forwardedPrefix) {
|
|
90716
|
+
return {
|
|
90717
|
+
basePath: `${forwardedPrefix}/addon/${this.#addonId}/${this.#prefix}/${attemptId}`,
|
|
90718
|
+
upstreams: attempt.upstreams
|
|
90719
|
+
};
|
|
90720
|
+
}
|
|
90721
|
+
async #handle(req, res) {
|
|
90722
|
+
try {
|
|
90723
|
+
await this.#serve(req, res);
|
|
90724
|
+
} catch (err) {
|
|
90725
|
+
this.#logger.warn("vendor login proxy: request failed", { meta: {
|
|
90726
|
+
addonId: this.#addonId,
|
|
90727
|
+
error: err instanceof Error ? err.name : "unknown"
|
|
90728
|
+
} });
|
|
90729
|
+
if (!res.headersSent) sendPlain(res, 502, "The sign-in page could not be reached.");
|
|
90730
|
+
else res.end();
|
|
90731
|
+
}
|
|
90732
|
+
}
|
|
90733
|
+
async #serve(req, res) {
|
|
90734
|
+
const parsed = parseProxyPath(req.url ?? "/");
|
|
90735
|
+
if (parsed === null) {
|
|
90736
|
+
sendPlain(res, 404, "Not found");
|
|
90737
|
+
return;
|
|
90738
|
+
}
|
|
90739
|
+
const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
|
|
90740
|
+
const attempt = this.#attempts.get(parsed.attemptId);
|
|
90741
|
+
if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
|
|
90742
|
+
if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
|
|
90743
|
+
this.#logger.info("vendor login proxy: attempt refused", { meta: {
|
|
90744
|
+
addonId: this.#addonId,
|
|
90745
|
+
reason: attempt === void 0 ? "missing" : "expired"
|
|
90746
|
+
} });
|
|
90747
|
+
this.#redirect(res, forwardedPrefix, {
|
|
90748
|
+
status: "error",
|
|
90749
|
+
addonId: this.#addonId,
|
|
90750
|
+
reason: "invalid_state"
|
|
90751
|
+
});
|
|
90752
|
+
return;
|
|
90753
|
+
}
|
|
90754
|
+
const route = this.#routeFor(parsed.attemptId, attempt, forwardedPrefix);
|
|
90755
|
+
const upstream = findUpstream(route, parsed.host);
|
|
90756
|
+
const target = upstream === void 0 ? null : upstreamUrlFor(upstream, parsed);
|
|
90757
|
+
if (upstream === void 0 || target === null) {
|
|
90758
|
+
this.#logger.warn("vendor login proxy: host refused", { meta: {
|
|
90759
|
+
addonId: this.#addonId,
|
|
90760
|
+
host: parsed.host
|
|
90761
|
+
} });
|
|
90762
|
+
sendPlain(res, 404, "Not found");
|
|
90763
|
+
return;
|
|
90764
|
+
}
|
|
90765
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
90766
|
+
let body;
|
|
90767
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
90768
|
+
const read = await readRequestBody(req);
|
|
90769
|
+
if (read === "too-large") {
|
|
90770
|
+
sendPlain(res, 413, "Request too large");
|
|
90771
|
+
return;
|
|
90772
|
+
}
|
|
90773
|
+
body = read;
|
|
90774
|
+
}
|
|
90775
|
+
const headers = this.#requestHeaders(req, route, upstream);
|
|
90776
|
+
const upstreamRes = await this.#fetch(target, {
|
|
90777
|
+
method,
|
|
90778
|
+
headers,
|
|
90779
|
+
redirect: "manual",
|
|
90780
|
+
...body !== void 0 && body.length > 0 ? { body: new Uint8Array(body) } : {}
|
|
90781
|
+
});
|
|
90782
|
+
const location = upstreamRes.headers.get("location");
|
|
90783
|
+
const setCookies = upstreamRes.headers.getSetCookie();
|
|
90784
|
+
const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
|
|
90785
|
+
const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
|
|
90786
|
+
const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
|
|
90787
|
+
this.#logger.debug("vendor login proxy: upstream answered", { meta: {
|
|
90788
|
+
addonId: this.#addonId,
|
|
90789
|
+
host: upstream.host,
|
|
90790
|
+
status: upstreamRes.status,
|
|
90791
|
+
method
|
|
90792
|
+
} });
|
|
90793
|
+
const settled = await attempt.evaluate({
|
|
90794
|
+
status: upstreamRes.status,
|
|
90795
|
+
url: target.toString(),
|
|
90796
|
+
location,
|
|
90797
|
+
setCookies,
|
|
90798
|
+
bodyText: text
|
|
90799
|
+
});
|
|
90800
|
+
if (settled.kind === "done") {
|
|
90801
|
+
this.#attempts.delete(parsed.attemptId);
|
|
90802
|
+
this.#logger.info("vendor login proxy: sign-in finished", { meta: {
|
|
90803
|
+
addonId: this.#addonId,
|
|
90804
|
+
status: settled.landing.status
|
|
90805
|
+
} });
|
|
90806
|
+
this.#redirect(res, forwardedPrefix, settled.landing);
|
|
90807
|
+
return;
|
|
90808
|
+
}
|
|
90809
|
+
const out = {};
|
|
90810
|
+
upstreamRes.headers.forEach((value, name) => {
|
|
90811
|
+
if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
|
|
90812
|
+
});
|
|
90813
|
+
if (setCookies.length > 0) out["set-cookie"] = setCookies.map((c) => rewriteSetCookie(c, route));
|
|
90814
|
+
if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
|
|
90815
|
+
out["cache-control"] = "no-store";
|
|
90816
|
+
if (text !== null && kind !== null) {
|
|
90817
|
+
const rewritten = rewriteBody(text, kind, upstream, route);
|
|
90818
|
+
res.writeHead(upstreamRes.status, out);
|
|
90819
|
+
res.end(rewritten);
|
|
90820
|
+
return;
|
|
90821
|
+
}
|
|
90822
|
+
res.writeHead(upstreamRes.status, out);
|
|
90823
|
+
await this.#pipeBody(upstreamRes, res);
|
|
90824
|
+
}
|
|
90825
|
+
#requestHeaders(req, route, upstream) {
|
|
90826
|
+
const headers = {};
|
|
90827
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
90828
|
+
const lower = name.toLowerCase();
|
|
90829
|
+
if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
|
|
90830
|
+
headers[lower] = Array.isArray(value) ? value.join(", ") : value;
|
|
90831
|
+
}
|
|
90832
|
+
const cookie = filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? "");
|
|
90833
|
+
if (cookie.length > 0) headers["cookie"] = cookie;
|
|
90834
|
+
if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
|
|
90835
|
+
const referer = firstHeader(req.headers["referer"]);
|
|
90836
|
+
if (referer !== void 0) {
|
|
90837
|
+
const back = rewriteReferer(referer, route);
|
|
90838
|
+
if (back !== null) headers["referer"] = back;
|
|
90839
|
+
}
|
|
90840
|
+
return headers;
|
|
90841
|
+
}
|
|
90842
|
+
async #pipeBody(upstreamRes, res) {
|
|
90843
|
+
if (upstreamRes.body === null) {
|
|
90844
|
+
res.end();
|
|
90845
|
+
return;
|
|
90846
|
+
}
|
|
90847
|
+
for await (const chunk of upstreamRes.body) {
|
|
90848
|
+
if (!res.write(chunk)) await new Promise((resolve) => {
|
|
90849
|
+
res.once("drain", resolve);
|
|
90850
|
+
res.once("close", resolve);
|
|
90851
|
+
});
|
|
90852
|
+
if (res.destroyed) return;
|
|
90853
|
+
}
|
|
90854
|
+
res.end();
|
|
90855
|
+
}
|
|
90856
|
+
#redirect(res, forwardedPrefix, outcome) {
|
|
90857
|
+
res.writeHead(302, {
|
|
90858
|
+
location: `${forwardedPrefix}${oauthLandingUrl(null, outcome)}`,
|
|
90859
|
+
"cache-control": "no-store"
|
|
90860
|
+
});
|
|
90861
|
+
res.end();
|
|
90862
|
+
}
|
|
90863
|
+
};
|
|
89184
90864
|
var MB = 1024 * 1024;
|
|
89185
90865
|
1024 * MB, 3072 * MB;
|
|
89186
90866
|
//#endregion
|
|
@@ -89226,6 +90906,12 @@ Object.defineProperty(exports, "RUNNING_STATE_COLD", {
|
|
|
89226
90906
|
return RUNNING_STATE_COLD;
|
|
89227
90907
|
}
|
|
89228
90908
|
});
|
|
90909
|
+
Object.defineProperty(exports, "VendorLoginProxy", {
|
|
90910
|
+
enumerable: true,
|
|
90911
|
+
get: function() {
|
|
90912
|
+
return VendorLoginProxy;
|
|
90913
|
+
}
|
|
90914
|
+
});
|
|
89229
90915
|
Object.defineProperty(exports, "_enum", {
|
|
89230
90916
|
enumerable: true,
|
|
89231
90917
|
get: function() {
|
|
@@ -89250,6 +90936,12 @@ Object.defineProperty(exports, "binaryCapability", {
|
|
|
89250
90936
|
return binaryCapability;
|
|
89251
90937
|
}
|
|
89252
90938
|
});
|
|
90939
|
+
Object.defineProperty(exports, "boolean", {
|
|
90940
|
+
enumerable: true,
|
|
90941
|
+
get: function() {
|
|
90942
|
+
return boolean;
|
|
90943
|
+
}
|
|
90944
|
+
});
|
|
89253
90945
|
Object.defineProperty(exports, "climateControlCapability", {
|
|
89254
90946
|
enumerable: true,
|
|
89255
90947
|
get: function() {
|