@camstack/addon-post-analysis 1.2.282 → 1.2.283
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/dist/{dist-D0zzdvSd.js → dist-CT5xOY86.js} +192 -7
- package/dist/{dist-C3zTKShG.mjs → dist-zp1v7cCp.mjs} +169 -8
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B9WQ8rry.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-9UxXz_Yw.mjs} +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CZkZhroP.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-lUAYfFf-.mjs → hostInit-27I-ZEiW.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +462 -54
- package/dist/pipeline-analytics/index.mjs +462 -54
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DT-cQNv-.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-CT5xOY86.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -6663,6 +6663,8 @@ var SceneEngine = class {
|
|
|
6663
6663
|
var DEFAULT_LOAD_LIMIT = 1e5;
|
|
6664
6664
|
var DurableLedger = class DurableLedger {
|
|
6665
6665
|
mirror = /* @__PURE__ */ new Map();
|
|
6666
|
+
/** Whether ANY load has read the store successfully — see {@link hasAnswered}. */
|
|
6667
|
+
answered = false;
|
|
6666
6668
|
spec;
|
|
6667
6669
|
store;
|
|
6668
6670
|
logger;
|
|
@@ -6718,6 +6720,7 @@ var DurableLedger = class DurableLedger {
|
|
|
6718
6720
|
}
|
|
6719
6721
|
this.mirror.clear();
|
|
6720
6722
|
for (const [key, row] of next) this.mirror.set(key, row);
|
|
6723
|
+
this.answered = true;
|
|
6721
6724
|
if (skipped > 0) this.logger.warn("durable rows skipped as malformed — they gate NOTHING", { meta: {
|
|
6722
6725
|
collection: this.spec.collection,
|
|
6723
6726
|
skipped
|
|
@@ -6732,6 +6735,16 @@ var DurableLedger = class DurableLedger {
|
|
|
6732
6735
|
return [...this.mirror.values()];
|
|
6733
6736
|
}
|
|
6734
6737
|
}
|
|
6738
|
+
/**
|
|
6739
|
+
* Whether the mirror is an ANSWER from the store: true once any load has
|
|
6740
|
+
* succeeded, and never false again (a later failed load keeps that answer).
|
|
6741
|
+
* Before it, an empty mirror is "not read yet", never "the store is empty" —
|
|
6742
|
+
* {@link load} swallows its failure, so a caller that must tell the two apart
|
|
6743
|
+
* asks here (D642 review I1).
|
|
6744
|
+
*/
|
|
6745
|
+
hasAnswered() {
|
|
6746
|
+
return this.answered;
|
|
6747
|
+
}
|
|
6735
6748
|
/** Every mirrored row, insertion-ordered. */
|
|
6736
6749
|
snapshot() {
|
|
6737
6750
|
return [...this.mirror.values()];
|
|
@@ -8465,6 +8478,78 @@ var NcAlertStore = class {
|
|
|
8465
8478
|
};
|
|
8466
8479
|
}
|
|
8467
8480
|
};
|
|
8481
|
+
//#endregion
|
|
8482
|
+
//#region src/notification-center/alerts/consumable-subject.ts
|
|
8483
|
+
/**
|
|
8484
|
+
* A consumable EPISODE's name, and the thresholds the rule set asks for (D642).
|
|
8485
|
+
*
|
|
8486
|
+
* Before D642 one cluster-wide threshold judged every item, so an item had one
|
|
8487
|
+
* episode: `consumable:<deviceId>:<key>`. The threshold is now the RULE's, and
|
|
8488
|
+
* the ledger keeps one episode per threshold a rule asks for — each with its
|
|
8489
|
+
* own hysteresis, pending raise and reminder — named
|
|
8490
|
+
* `consumable@<threshold>:<deviceId>:<key>`.
|
|
8491
|
+
*
|
|
8492
|
+
* The threshold rides the FAMILY segment on purpose: the device id stays the
|
|
8493
|
+
* second segment (`alertSubjectDeviceId`, D636 — the log tag and the indexed
|
|
8494
|
+
* column read it), and the item key stays last and whole, whatever it holds.
|
|
8495
|
+
*
|
|
8496
|
+
* Pure. No clock, no I/O.
|
|
8497
|
+
*/
|
|
8498
|
+
var FAMILY = "consumable";
|
|
8499
|
+
/** The episode of `key` on `deviceId`, judged at `thresholdPercent`. */
|
|
8500
|
+
function consumableSubject(thresholdPercent, deviceId, key) {
|
|
8501
|
+
return `${FAMILY}@${String(thresholdPercent)}:${String(deviceId)}:${key}`;
|
|
8502
|
+
}
|
|
8503
|
+
/** The pre-D642 subject of the same item — read only to carry it over. */
|
|
8504
|
+
function legacyConsumableSubject(deviceId, key) {
|
|
8505
|
+
return `${FAMILY}:${String(deviceId)}:${key}`;
|
|
8506
|
+
}
|
|
8507
|
+
/** `null` for a subject that is not a consumable's. */
|
|
8508
|
+
function parseConsumableSubject(subject) {
|
|
8509
|
+
const first = subject.indexOf(":");
|
|
8510
|
+
const second = first < 0 ? -1 : subject.indexOf(":", first + 1);
|
|
8511
|
+
if (first < 0 || second < 0) return null;
|
|
8512
|
+
const family = subject.slice(0, first);
|
|
8513
|
+
const deviceId = Number(subject.slice(first + 1, second));
|
|
8514
|
+
const key = subject.slice(second + 1);
|
|
8515
|
+
if (!Number.isInteger(deviceId) || key.length === 0) return null;
|
|
8516
|
+
if (family === FAMILY) return {
|
|
8517
|
+
thresholdPercent: 15,
|
|
8518
|
+
deviceId,
|
|
8519
|
+
key,
|
|
8520
|
+
legacy: true
|
|
8521
|
+
};
|
|
8522
|
+
if (!family.startsWith(`${FAMILY}@`)) return null;
|
|
8523
|
+
const thresholdPercent = Number(family.slice(11));
|
|
8524
|
+
if (!Number.isInteger(thresholdPercent)) return null;
|
|
8525
|
+
return {
|
|
8526
|
+
thresholdPercent,
|
|
8527
|
+
deviceId,
|
|
8528
|
+
key,
|
|
8529
|
+
legacy: false
|
|
8530
|
+
};
|
|
8531
|
+
}
|
|
8532
|
+
/**
|
|
8533
|
+
* Every consumable threshold the rule set asks for, ascending.
|
|
8534
|
+
*
|
|
8535
|
+
* A rule asks when it is a system-event rule whose kinds include a consumable
|
|
8536
|
+
* kind — enabled or not: a disabled rule's episode stays PENDING (no rule
|
|
8537
|
+
* matched it) and is re-offered the moment the rule is switched back on
|
|
8538
|
+
* (D636), which is what that rule would have said had it been on. With no such
|
|
8539
|
+
* rule at all the default is still judged, so an episode exists for the first
|
|
8540
|
+
* rule an operator writes to hear (D636's "offered once per rule change").
|
|
8541
|
+
*/
|
|
8542
|
+
function consumableThresholdCensus(rules) {
|
|
8543
|
+
const thresholds = /* @__PURE__ */ new Set();
|
|
8544
|
+
for (const rule of rules) {
|
|
8545
|
+
const condition = rule.conditions.systemEvent;
|
|
8546
|
+
if (rule.delivery !== "system-event" || condition === void 0) continue;
|
|
8547
|
+
if (!condition.kinds.some((kind) => require_dist.NC_CONSUMABLE_SYSTEM_EVENT_KINDS.includes(kind))) continue;
|
|
8548
|
+
thresholds.add(require_dist.consumableLowPercentOf(condition));
|
|
8549
|
+
}
|
|
8550
|
+
if (thresholds.size === 0) thresholds.add(15);
|
|
8551
|
+
return [...thresholds].toSorted((a, b) => a - b);
|
|
8552
|
+
}
|
|
8468
8553
|
function deriveConsumableBand(input) {
|
|
8469
8554
|
const { level, status } = input.reading;
|
|
8470
8555
|
const measured = level !== null && Number.isFinite(level);
|
|
@@ -10128,6 +10213,9 @@ function systemEventExclusion(condition, subject) {
|
|
|
10128
10213
|
if (wantedPackages !== void 0 && applies("packageNames")) {
|
|
10129
10214
|
if (!subjectPackages(subject).some((name) => wantedPackages.includes(name))) return "packageNames";
|
|
10130
10215
|
}
|
|
10216
|
+
if (applies("consumableLowPercent") && subject.consumableLowPercent !== void 0) {
|
|
10217
|
+
if (subject.consumableLowPercent !== require_dist.consumableLowPercentOf(condition)) return "consumableLowPercent";
|
|
10218
|
+
}
|
|
10131
10219
|
}
|
|
10132
10220
|
function subjectNodes(subject) {
|
|
10133
10221
|
const ids = [...subject.nodeIds ?? []];
|
|
@@ -11007,6 +11095,7 @@ function systemSubject(input) {
|
|
|
11007
11095
|
...input.packageNames !== void 0 ? { packageNames: input.packageNames } : {},
|
|
11008
11096
|
...input.textCount !== void 0 ? { textCount: input.textCount } : {},
|
|
11009
11097
|
...input.alarmSource !== void 0 ? { alarmSource: input.alarmSource } : {},
|
|
11098
|
+
...input.consumableLowPercent !== void 0 ? { consumableLowPercent: input.consumableLowPercent } : {},
|
|
11010
11099
|
data
|
|
11011
11100
|
}
|
|
11012
11101
|
},
|
|
@@ -11108,9 +11197,11 @@ function incomingFromBatteryBand(input) {
|
|
|
11108
11197
|
* these to be treated as the battery notifications are — with two differences
|
|
11109
11198
|
* that come from the subject and not from taste:
|
|
11110
11199
|
*
|
|
11111
|
-
* - **the subject carries the ITEM.** `consumable
|
|
11112
|
-
* vacuum has a brush AND a filter AND a mop pad, and one subject
|
|
11113
|
-
* three would let the brush's alert cancel the filter's.
|
|
11200
|
+
* - **the subject carries the ITEM.** `consumable@<threshold>:<deviceId>:<key>`,
|
|
11201
|
+
* because a vacuum has a brush AND a filter AND a mop pad, and one subject
|
|
11202
|
+
* for all three would let the brush's alert cancel the filter's. It carries
|
|
11203
|
+
* the THRESHOLD too (D642): each rule's threshold is its own episode, and
|
|
11204
|
+
* the engine pairs a rule with the episode judged at its own number.
|
|
11114
11205
|
* - **the body variant follows what is KNOWN.** An item with no percentage
|
|
11115
11206
|
* gets a finished sentence rather than a template with a stand-in number in
|
|
11116
11207
|
* it, for the same reason a binary battery does.
|
|
@@ -11119,17 +11210,46 @@ function incomingFromBatteryBand(input) {
|
|
|
11119
11210
|
*/
|
|
11120
11211
|
function incomingFromConsumableBand(input) {
|
|
11121
11212
|
const kind = input.band === "low" ? "device-consumable-low" : "device-consumable-normal";
|
|
11122
|
-
const
|
|
11213
|
+
const level = input.item.level;
|
|
11214
|
+
const percentage = level !== null && Number.isFinite(level) ? Math.round(level) : null;
|
|
11215
|
+
const measured = percentage !== null;
|
|
11123
11216
|
return systemSubject({
|
|
11124
11217
|
facts: input.facts,
|
|
11125
11218
|
kind,
|
|
11126
|
-
subject:
|
|
11219
|
+
subject: consumableSubject(input.thresholdPercent, input.deviceId, input.item.key),
|
|
11127
11220
|
titleKey: `system.${kind}.title`,
|
|
11128
11221
|
bodyKey: measured ? `system.${kind}.body` : `system.${kind}.body.unmeasured`,
|
|
11129
11222
|
textParams: {
|
|
11130
11223
|
...deviceTextParams(input.deviceId, input.identity),
|
|
11131
11224
|
item: input.item.label,
|
|
11132
|
-
...
|
|
11225
|
+
...percentage !== null ? { percentage: String(percentage) } : {}
|
|
11226
|
+
},
|
|
11227
|
+
deviceId: input.deviceId,
|
|
11228
|
+
consumableLowPercent: input.thresholdPercent,
|
|
11229
|
+
...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
|
|
11230
|
+
});
|
|
11231
|
+
}
|
|
11232
|
+
/**
|
|
11233
|
+
* A vacuum TANK band, as a system-event subject (D642).
|
|
11234
|
+
*
|
|
11235
|
+
* Built like {@link incomingFromConsumableBand}: `tank:<deviceId>:<tank>` is
|
|
11236
|
+
* one subject per tank (the dust bin's alert must not cancel the clean water's),
|
|
11237
|
+
* and the recovery keeps it. The BODY VARIANT names the tank in the reader's
|
|
11238
|
+
* language — the params are frozen before the language is known, so the tank's
|
|
11239
|
+
* name cannot be a param; `tank` carries the stable token for a custom
|
|
11240
|
+
* template.
|
|
11241
|
+
*/
|
|
11242
|
+
function incomingFromTankBand(input) {
|
|
11243
|
+
const kind = input.band === "low" ? "device-tank-low" : input.band === "full" ? "device-tank-full" : "device-tank-normal";
|
|
11244
|
+
return systemSubject({
|
|
11245
|
+
facts: input.facts,
|
|
11246
|
+
kind,
|
|
11247
|
+
subject: `tank:${String(input.deviceId)}:${input.tank}`,
|
|
11248
|
+
titleKey: `system.${kind}.title`,
|
|
11249
|
+
bodyKey: `system.${kind}.body.${input.tank}`,
|
|
11250
|
+
textParams: {
|
|
11251
|
+
...deviceTextParams(input.deviceId, input.identity),
|
|
11252
|
+
tank: input.tank
|
|
11133
11253
|
},
|
|
11134
11254
|
deviceId: input.deviceId,
|
|
11135
11255
|
...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
|
|
@@ -11717,6 +11837,91 @@ function deliveryForKind(kind) {
|
|
|
11717
11837
|
return kind;
|
|
11718
11838
|
}
|
|
11719
11839
|
//#endregion
|
|
11840
|
+
//#region src/notification-center/alerts/tank-band.ts
|
|
11841
|
+
/**
|
|
11842
|
+
* A vacuum TANK reduced to the only thing worth storing: its band (D642).
|
|
11843
|
+
*
|
|
11844
|
+
* The `vacuum-control` slice carries four tanks — clean water, dirty water,
|
|
11845
|
+
* detergent, dust bin — each `null` when the hardware has none, else
|
|
11846
|
+
* `{ level, status }`. The operator asked for them to notify like consumables:
|
|
11847
|
+
* clean water / detergent running low, dirty water / dust bin full.
|
|
11848
|
+
*
|
|
11849
|
+
* ## The verdict is the provider's
|
|
11850
|
+
*
|
|
11851
|
+
* A tank's `status` is the device's own float switch (`ok` | `low` | `full`),
|
|
11852
|
+
* and it is the ONLY thing judged. The hub has no threshold for a tank level:
|
|
11853
|
+
* the rule for thresholds is that they belong to whoever generates the event,
|
|
11854
|
+
* and a tank that says `low` has already decided. A tank that reports only a
|
|
11855
|
+
* `level` is therefore not judged at all (`null`) rather than judged against a
|
|
11856
|
+
* number the hub would have to invent.
|
|
11857
|
+
*
|
|
11858
|
+
* ## `null` is the third answer, and it is not `normal`
|
|
11859
|
+
*
|
|
11860
|
+
* An unknown status says nothing — it neither opens nor closes an episode, for
|
|
11861
|
+
* the reason `battery-band.ts` gives: a reading nobody took is not evidence
|
|
11862
|
+
* that anything changed.
|
|
11863
|
+
*
|
|
11864
|
+
* Pure. No clock, no I/O.
|
|
11865
|
+
*/
|
|
11866
|
+
/** The four tanks `vacuum-control` names, by their slice field. */
|
|
11867
|
+
var NC_TANK_KEYS = [
|
|
11868
|
+
"cleanWater",
|
|
11869
|
+
"dirtyWater",
|
|
11870
|
+
"detergent",
|
|
11871
|
+
"dustBin"
|
|
11872
|
+
];
|
|
11873
|
+
/**
|
|
11874
|
+
* How long a tank change must HOLD before it counts (D642 review M2).
|
|
11875
|
+
*
|
|
11876
|
+
* A float switch sloshing at the edge of `low` / `full` flips its status back
|
|
11877
|
+
* and forth while the robot moves; each flip is a durable transition, and
|
|
11878
|
+
* without a hold each one is a raise and a recovery on the operator's phone.
|
|
11879
|
+
* One minute is two reload ticks: long enough to outlast a slosh, short enough
|
|
11880
|
+
* that a genuinely full dust bin is said within a minute and a half.
|
|
11881
|
+
*/
|
|
11882
|
+
var NC_TANK_HOLD_MS = 6e4;
|
|
11883
|
+
function deriveTankBand(reading) {
|
|
11884
|
+
switch (reading.status) {
|
|
11885
|
+
case "low": return "low";
|
|
11886
|
+
case "full": return "full";
|
|
11887
|
+
case "ok": return "normal";
|
|
11888
|
+
case null: return null;
|
|
11889
|
+
}
|
|
11890
|
+
}
|
|
11891
|
+
function isRecord$4(value) {
|
|
11892
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11893
|
+
}
|
|
11894
|
+
function tankStatusOf(value) {
|
|
11895
|
+
return value === "ok" || value === "low" || value === "full" ? value : null;
|
|
11896
|
+
}
|
|
11897
|
+
/**
|
|
11898
|
+
* A `device.state-changed` payload carrying a `vacuum-control` slice, reduced
|
|
11899
|
+
* to its tanks. `null` for any other cap. A tank the hardware does not have
|
|
11900
|
+
* (`null`) is skipped; a malformed field reads as unknown, never as a
|
|
11901
|
+
* fabricated verdict (D393: absent is not zero).
|
|
11902
|
+
*/
|
|
11903
|
+
function vacuumTanksOf(data) {
|
|
11904
|
+
if (data["capName"] !== "vacuum-control") return null;
|
|
11905
|
+
const deviceId = data["deviceId"];
|
|
11906
|
+
const slice = data["slice"];
|
|
11907
|
+
if (typeof deviceId !== "number" || !isRecord$4(slice)) return null;
|
|
11908
|
+
const tanks = [];
|
|
11909
|
+
for (const key of NC_TANK_KEYS) {
|
|
11910
|
+
const raw = slice[key];
|
|
11911
|
+
if (!isRecord$4(raw)) continue;
|
|
11912
|
+
const level = raw["level"];
|
|
11913
|
+
tanks.push({
|
|
11914
|
+
key,
|
|
11915
|
+
level: typeof level === "number" && Number.isFinite(level) ? level : null,
|
|
11916
|
+
status: tankStatusOf(raw["status"])
|
|
11917
|
+
});
|
|
11918
|
+
}
|
|
11919
|
+
return tanks.length === 0 ? null : {
|
|
11920
|
+
deviceId,
|
|
11921
|
+
tanks
|
|
11922
|
+
};
|
|
11923
|
+
}
|
|
11924
|
+
//#endregion
|
|
11720
11925
|
//#region src/notification-center/alerts/battery-band.ts
|
|
11721
11926
|
/**
|
|
11722
11927
|
* The stand-in a provider writes for a binary NORMAL. Anything below it, on a
|
|
@@ -11747,10 +11952,14 @@ function deriveBatteryBand(input) {
|
|
|
11747
11952
|
* `NcDeviceAlerts` — the one place a SUSTAINED device fault becomes a
|
|
11748
11953
|
* notification, and keeps becoming one until it stops being true.
|
|
11749
11954
|
*
|
|
11750
|
-
* It owns
|
|
11955
|
+
* It owns these families and nothing else:
|
|
11751
11956
|
*
|
|
11752
11957
|
* - **battery band** — `battery:<deviceId>`, fed by the `battery` runtime-state
|
|
11753
11958
|
* slice that already arrives on `device.state-changed`;
|
|
11959
|
+
* - **consumable band** — `consumable@<threshold>:<deviceId>:<key>`, one
|
|
11960
|
+
* episode per item per threshold a RULE asks for (D642);
|
|
11961
|
+
* - **tank band** — `tank:<deviceId>:<tank>`, a vacuum's tanks, judged on the
|
|
11962
|
+
* provider's own verdict (D642);
|
|
11754
11963
|
* - **the reminder tick** — the operator's "how often do you say it again".
|
|
11755
11964
|
*
|
|
11756
11965
|
* It does NOT own device liveness. `device-offline` already has a producer, a
|
|
@@ -11790,6 +11999,10 @@ var PENDING_CADENCE_MAX_MS = 36e5;
|
|
|
11790
11999
|
function pendingCadenceMs(settings) {
|
|
11791
12000
|
return settings.renotifyMinutes > 0 ? Math.min(settings.renotifyMinutes * 6e4, PENDING_CADENCE_MAX_MS) : PENDING_CADENCE_MAX_MS;
|
|
11792
12001
|
}
|
|
12002
|
+
/** The RAM key of one item's last reading — every threshold's episode reads it. */
|
|
12003
|
+
function itemKeyOf(deviceId, key) {
|
|
12004
|
+
return `${String(deviceId)}:${key}`;
|
|
12005
|
+
}
|
|
11793
12006
|
var NcDeviceAlerts = class {
|
|
11794
12007
|
deps;
|
|
11795
12008
|
now;
|
|
@@ -11801,8 +12014,28 @@ var NcDeviceAlerts = class {
|
|
|
11801
12014
|
* durable.
|
|
11802
12015
|
*/
|
|
11803
12016
|
lastReading = /* @__PURE__ */ new Map();
|
|
11804
|
-
/**
|
|
12017
|
+
/**
|
|
12018
|
+
* The last reading per consumable ITEM (`<deviceId>:<key>`), for the
|
|
12019
|
+
* reminder sweep and for judging a threshold a rule gains later (D642).
|
|
12020
|
+
*/
|
|
11805
12021
|
lastConsumable = /* @__PURE__ */ new Map();
|
|
12022
|
+
/** The last reading per tank SUBJECT, for the reminder sweep. */
|
|
12023
|
+
lastTank = /* @__PURE__ */ new Map();
|
|
12024
|
+
/**
|
|
12025
|
+
* Tank changes on HOLD (review M2): a band that would move the episode,
|
|
12026
|
+
* first seen at `since`. It counts once it has held for `NC_TANK_HOLD_MS`,
|
|
12027
|
+
* confirmed by a later slice write or by the reload tick — no timer of its
|
|
12028
|
+
* own, so nothing outlives the device. Bounded by the device's tanks (four),
|
|
12029
|
+
* and dropped with the device by {@link forget}.
|
|
12030
|
+
*/
|
|
12031
|
+
tankHold = /* @__PURE__ */ new Map();
|
|
12032
|
+
/**
|
|
12033
|
+
* Thresholds some episode row carries that the LAST census did not ask for.
|
|
12034
|
+
* A threshold's rows are retired only when two consecutive censuses agree it
|
|
12035
|
+
* is gone (D49: a read that would destroy work needs a second read to
|
|
12036
|
+
* agree). RAM only: a restart costs one more tick of patience.
|
|
12037
|
+
*/
|
|
12038
|
+
absentThresholds = /* @__PURE__ */ new Set();
|
|
11806
12039
|
/**
|
|
11807
12040
|
* Subject → WHEN its PENDING raise was last offered, and to which
|
|
11808
12041
|
* {@link NcDeviceAlertsDeps.rulesVersion}. RAM only, on purpose: after a
|
|
@@ -11860,31 +12093,137 @@ var NcDeviceAlerts = class {
|
|
|
11860
12093
|
* observed.
|
|
11861
12094
|
*/
|
|
11862
12095
|
observeConsumable(deviceId, item) {
|
|
11863
|
-
|
|
11864
|
-
|
|
12096
|
+
this.lastConsumable.set(itemKeyOf(deviceId, item.key), {
|
|
12097
|
+
deviceId,
|
|
12098
|
+
item
|
|
12099
|
+
});
|
|
12100
|
+
const thresholds = this.deps.consumableThresholds();
|
|
12101
|
+
if (thresholds === null) {
|
|
12102
|
+
this.logger()?.debug("consumable reading kept — the rules have not been read yet", {
|
|
12103
|
+
tags: { deviceId },
|
|
12104
|
+
meta: { item: item.key }
|
|
12105
|
+
});
|
|
12106
|
+
return;
|
|
12107
|
+
}
|
|
11865
12108
|
const settings = this.deps.settings();
|
|
12109
|
+
for (const thresholdPercent of thresholds) this.judgeConsumable(deviceId, item, thresholdPercent, settings);
|
|
12110
|
+
}
|
|
12111
|
+
/**
|
|
12112
|
+
* ONE vacuum tank, from a device's `vacuum-control` slice (D642).
|
|
12113
|
+
*
|
|
12114
|
+
* The provider's verdict is the band (`tank-band.ts`); an unknown status
|
|
12115
|
+
* leaves everything untouched, an open episode included.
|
|
12116
|
+
*/
|
|
12117
|
+
observeTank(deviceId, tank) {
|
|
12118
|
+
const band = deriveTankBand(tank);
|
|
12119
|
+
if (band === null) return;
|
|
12120
|
+
const subject = `tank:${String(deviceId)}:${tank.key}`;
|
|
12121
|
+
this.lastTank.set(subject, {
|
|
12122
|
+
deviceId,
|
|
12123
|
+
tank
|
|
12124
|
+
});
|
|
12125
|
+
if (!this.tankChangeHeld(subject, deviceId, band)) return;
|
|
12126
|
+
this.apply(subject, band !== "normal", this.deps.settings(), (facts, alerting) => incomingFromTankBand({
|
|
12127
|
+
facts,
|
|
12128
|
+
deviceId,
|
|
12129
|
+
band: alerting ? band : "normal",
|
|
12130
|
+
tank: tank.key,
|
|
12131
|
+
identity: this.deps.lookup(deviceId)
|
|
12132
|
+
}));
|
|
12133
|
+
}
|
|
12134
|
+
/**
|
|
12135
|
+
* Has this tank band held long enough to move its episode? A band that
|
|
12136
|
+
* would not move it (a repeat, or low ⇄ full inside one open episode) passes
|
|
12137
|
+
* at once and clears any hold — that is what makes a flapping switch start
|
|
12138
|
+
* its hold over every time it flips back. A first HEALTHY observation passes
|
|
12139
|
+
* too: it seeds silently, so there is nothing to hold.
|
|
12140
|
+
*/
|
|
12141
|
+
tankChangeHeld(subject, deviceId, band) {
|
|
12142
|
+
const row = this.deps.rows.get(subject);
|
|
12143
|
+
const alerting = band !== "normal";
|
|
12144
|
+
if (!(row === void 0 ? alerting : row.alerting !== alerting)) {
|
|
12145
|
+
this.tankHold.delete(subject);
|
|
12146
|
+
return true;
|
|
12147
|
+
}
|
|
12148
|
+
const nowMs = this.now();
|
|
12149
|
+
const held = this.tankHold.get(subject);
|
|
12150
|
+
if (held === void 0 || held.band !== band) {
|
|
12151
|
+
this.tankHold.set(subject, {
|
|
12152
|
+
band,
|
|
12153
|
+
since: nowMs
|
|
12154
|
+
});
|
|
12155
|
+
this.logger()?.debug("tank change on hold — counts once it has held", {
|
|
12156
|
+
tags: { deviceId },
|
|
12157
|
+
meta: {
|
|
12158
|
+
subject,
|
|
12159
|
+
band,
|
|
12160
|
+
holdMs: NC_TANK_HOLD_MS
|
|
12161
|
+
}
|
|
12162
|
+
});
|
|
12163
|
+
return false;
|
|
12164
|
+
}
|
|
12165
|
+
if (nowMs - held.since < 6e4) return false;
|
|
12166
|
+
this.tankHold.delete(subject);
|
|
12167
|
+
return true;
|
|
12168
|
+
}
|
|
12169
|
+
/** The tick's half of the hold: a change that has held with no new slice write. */
|
|
12170
|
+
confirmHeldTanks() {
|
|
12171
|
+
const nowMs = this.now();
|
|
12172
|
+
for (const [subject, held] of [...this.tankHold]) {
|
|
12173
|
+
if (nowMs - held.since < 6e4) continue;
|
|
12174
|
+
const reading = this.lastTank.get(subject);
|
|
12175
|
+
if (reading === void 0) {
|
|
12176
|
+
this.tankHold.delete(subject);
|
|
12177
|
+
continue;
|
|
12178
|
+
}
|
|
12179
|
+
this.observeTank(reading.deviceId, reading.tank);
|
|
12180
|
+
}
|
|
12181
|
+
}
|
|
12182
|
+
/** One item against ONE threshold: its own episode, its own hysteresis. */
|
|
12183
|
+
judgeConsumable(deviceId, item, thresholdPercent, settings) {
|
|
12184
|
+
const subject = consumableSubject(thresholdPercent, deviceId, item.key);
|
|
12185
|
+
const previous = this.deps.rows.get(subject) ?? this.adoptLegacy(subject, deviceId, item.key);
|
|
11866
12186
|
const band = deriveConsumableBand({
|
|
11867
12187
|
reading: {
|
|
11868
12188
|
level: item.level,
|
|
11869
12189
|
status: item.status
|
|
11870
12190
|
},
|
|
11871
|
-
thresholdPercent
|
|
12191
|
+
thresholdPercent,
|
|
11872
12192
|
previous: previous?.alerting === true ? "low" : previous === void 0 ? void 0 : "normal"
|
|
11873
12193
|
});
|
|
11874
12194
|
if (band === null) return;
|
|
11875
|
-
this.lastConsumable.set(subject, {
|
|
11876
|
-
deviceId,
|
|
11877
|
-
item
|
|
11878
|
-
});
|
|
11879
12195
|
this.apply(subject, band === "low", settings, (facts, alerting) => incomingFromConsumableBand({
|
|
11880
12196
|
facts,
|
|
11881
12197
|
deviceId,
|
|
11882
12198
|
band: alerting ? "low" : "normal",
|
|
11883
12199
|
item,
|
|
12200
|
+
thresholdPercent,
|
|
11884
12201
|
identity: this.deps.lookup(deviceId)
|
|
11885
12202
|
}));
|
|
11886
12203
|
}
|
|
11887
12204
|
/**
|
|
12205
|
+
* A pre-D642 row of this item (`consumable:<id>:<key>`, judged at the
|
|
12206
|
+
* cluster threshold, which shipped as the rule default) is carried over to
|
|
12207
|
+
* the default-threshold episode rather than re-announced: the live vacuum
|
|
12208
|
+
* had two items low and reminding when D642 shipped. Any other threshold
|
|
12209
|
+
* starts fresh; an unadopted legacy row retires with the census.
|
|
12210
|
+
*/
|
|
12211
|
+
adoptLegacy(subject, deviceId, key) {
|
|
12212
|
+
const parts = parseConsumableSubject(subject);
|
|
12213
|
+
const legacySubject = legacyConsumableSubject(deviceId, key);
|
|
12214
|
+
const legacy = this.deps.rows.get(legacySubject);
|
|
12215
|
+
if (legacy === void 0 || parts === null) return void 0;
|
|
12216
|
+
const legacyParts = parseConsumableSubject(legacySubject);
|
|
12217
|
+
if (legacyParts === null || legacyParts.thresholdPercent !== parts.thresholdPercent) return;
|
|
12218
|
+
const adopted = {
|
|
12219
|
+
...legacy,
|
|
12220
|
+
subject
|
|
12221
|
+
};
|
|
12222
|
+
this.deps.rows.put(adopted);
|
|
12223
|
+
this.dropRow(legacySubject, "carried over to its threshold episode (D642)");
|
|
12224
|
+
return adopted;
|
|
12225
|
+
}
|
|
12226
|
+
/**
|
|
11888
12227
|
* The reminder sweep. Called from an existing periodic tick; it allocates
|
|
11889
12228
|
* nothing on the healthy path.
|
|
11890
12229
|
*
|
|
@@ -11895,6 +12234,9 @@ var NcDeviceAlerts = class {
|
|
|
11895
12234
|
*/
|
|
11896
12235
|
tick() {
|
|
11897
12236
|
const settings = this.deps.settings();
|
|
12237
|
+
const thresholds = this.deps.consumableThresholds();
|
|
12238
|
+
if (thresholds !== null) this.reconcileThresholds(thresholds, settings);
|
|
12239
|
+
this.confirmHeldTanks();
|
|
11898
12240
|
this.retryPending(settings);
|
|
11899
12241
|
if (settings.renotifyMinutes <= 0) return;
|
|
11900
12242
|
for (const row of this.deps.rows.all()) {
|
|
@@ -11927,14 +12269,58 @@ var NcDeviceAlerts = class {
|
|
|
11927
12269
|
const subjects = this.deps.rows.all().map((row) => row.subject).filter((subject) => alertSubjectDeviceId(subject) === deviceId);
|
|
11928
12270
|
for (const subject of subjects) {
|
|
11929
12271
|
this.lastReading.delete(subject);
|
|
11930
|
-
this.
|
|
12272
|
+
this.lastTank.delete(subject);
|
|
11931
12273
|
this.offeredAt.delete(subject);
|
|
11932
12274
|
await this.deps.rows.remove(subject);
|
|
11933
12275
|
}
|
|
11934
12276
|
this.lastReading.delete(`battery:${deviceId}`);
|
|
12277
|
+
for (const [key, reading] of this.lastConsumable) if (reading.deviceId === deviceId) this.lastConsumable.delete(key);
|
|
12278
|
+
for (const subject of this.tankHold.keys()) if (alertSubjectDeviceId(subject) === deviceId) this.tankHold.delete(subject);
|
|
12279
|
+
for (const subject of this.lastTank.keys()) if (alertSubjectDeviceId(subject) === deviceId) this.lastTank.delete(subject);
|
|
11935
12280
|
return subjects.length;
|
|
11936
12281
|
}
|
|
11937
12282
|
/**
|
|
12283
|
+
* Bring the consumable episodes in line with the thresholds the rules ask
|
|
12284
|
+
* for (D642). A threshold with no episode for an item yet is judged from the
|
|
12285
|
+
* item's last reading — a rule that gains a threshold hears about an item
|
|
12286
|
+
* already past it on the next tick, not at the next slice write. A
|
|
12287
|
+
* threshold that no rule asks for any more has its episodes RETIRED, but
|
|
12288
|
+
* only when two consecutive censuses agree it is gone; retiring is silent
|
|
12289
|
+
* (nothing recovered — nobody is listening at that number any more) and
|
|
12290
|
+
* logged per row.
|
|
12291
|
+
*/
|
|
12292
|
+
reconcileThresholds(thresholds, settings) {
|
|
12293
|
+
for (const { deviceId, item } of this.lastConsumable.values()) for (const thresholdPercent of thresholds) {
|
|
12294
|
+
if (this.deps.rows.get(consumableSubject(thresholdPercent, deviceId, item.key))) continue;
|
|
12295
|
+
this.judgeConsumable(deviceId, item, thresholdPercent, settings);
|
|
12296
|
+
}
|
|
12297
|
+
const absent = /* @__PURE__ */ new Set();
|
|
12298
|
+
for (const row of this.deps.rows.all()) {
|
|
12299
|
+
const parts = parseConsumableSubject(row.subject);
|
|
12300
|
+
if (parts === null || thresholds.includes(parts.thresholdPercent)) continue;
|
|
12301
|
+
absent.add(parts.thresholdPercent);
|
|
12302
|
+
if (this.absentThresholds.has(parts.thresholdPercent)) this.dropRow(row.subject, "retired — no rule judges consumables at this threshold (D642)");
|
|
12303
|
+
}
|
|
12304
|
+
this.absentThresholds = absent;
|
|
12305
|
+
}
|
|
12306
|
+
/** Remove one row, mirror and durable, with a line that names why. */
|
|
12307
|
+
dropRow(subject, reason) {
|
|
12308
|
+
this.offeredAt.delete(subject);
|
|
12309
|
+
this.logger()?.info(`device alert ${reason}`, {
|
|
12310
|
+
...tagsOf(subject),
|
|
12311
|
+
meta: { subject }
|
|
12312
|
+
});
|
|
12313
|
+
this.deps.rows.remove(subject).catch((error) => {
|
|
12314
|
+
this.logger()?.warn("device alert row could not be removed", {
|
|
12315
|
+
...tagsOf(subject),
|
|
12316
|
+
meta: {
|
|
12317
|
+
subject,
|
|
12318
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12319
|
+
}
|
|
12320
|
+
});
|
|
12321
|
+
});
|
|
12322
|
+
}
|
|
12323
|
+
/**
|
|
11938
12324
|
* Offer every PENDING raise once more — when the rule set changed since its
|
|
11939
12325
|
* last offer, OR when {@link pendingCadenceMs} has passed since it. The
|
|
11940
12326
|
* cadence is what delivers a raise refused only by a rule's SCHEDULE (or any
|
|
@@ -11966,14 +12352,25 @@ var NcDeviceAlerts = class {
|
|
|
11966
12352
|
}
|
|
11967
12353
|
/** How to re-word a subject's alert from the last reading, or `null`. */
|
|
11968
12354
|
restatementOf(subject) {
|
|
11969
|
-
const
|
|
11970
|
-
|
|
12355
|
+
const parts = parseConsumableSubject(subject);
|
|
12356
|
+
const consumable = parts === null || parts.legacy ? void 0 : this.lastConsumable.get(itemKeyOf(parts.deviceId, parts.key));
|
|
12357
|
+
if (parts !== null && consumable !== void 0) return (facts) => incomingFromConsumableBand({
|
|
11971
12358
|
facts,
|
|
11972
12359
|
deviceId: consumable.deviceId,
|
|
11973
12360
|
band: "low",
|
|
11974
12361
|
item: consumable.item,
|
|
12362
|
+
thresholdPercent: parts.thresholdPercent,
|
|
11975
12363
|
identity: this.deps.lookup(consumable.deviceId)
|
|
11976
12364
|
});
|
|
12365
|
+
const tank = this.lastTank.get(subject);
|
|
12366
|
+
const tankBand = tank === void 0 ? null : deriveTankBand(tank.tank);
|
|
12367
|
+
if (tank !== void 0 && (tankBand === "low" || tankBand === "full")) return (facts) => incomingFromTankBand({
|
|
12368
|
+
facts,
|
|
12369
|
+
deviceId: tank.deviceId,
|
|
12370
|
+
band: tankBand,
|
|
12371
|
+
tank: tank.tank.key,
|
|
12372
|
+
identity: this.deps.lookup(tank.deviceId)
|
|
12373
|
+
});
|
|
11977
12374
|
const battery = this.lastReading.get(subject);
|
|
11978
12375
|
if (battery !== void 0) return (facts) => incomingFromBatteryBand({
|
|
11979
12376
|
facts,
|
|
@@ -12073,11 +12470,9 @@ var NcDeviceAlerts = class {
|
|
|
12073
12470
|
report(subject, verdict, alerting) {
|
|
12074
12471
|
const logger = this.deps.logger;
|
|
12075
12472
|
if (logger === void 0) return;
|
|
12076
|
-
const deviceId = alertSubjectDeviceId(subject);
|
|
12077
|
-
const tags = deviceId !== void 0 ? { deviceId } : {};
|
|
12078
12473
|
if (verdict.kind === "silent" && !alerting) {
|
|
12079
12474
|
logger.debug("device alert cleared without a notification", {
|
|
12080
|
-
|
|
12475
|
+
...tagsOf(subject),
|
|
12081
12476
|
meta: {
|
|
12082
12477
|
subject,
|
|
12083
12478
|
reason: "notifyRecovery is off, or nothing changed"
|
|
@@ -12086,7 +12481,7 @@ var NcDeviceAlerts = class {
|
|
|
12086
12481
|
return;
|
|
12087
12482
|
}
|
|
12088
12483
|
logger.debug("device alert said nothing", {
|
|
12089
|
-
|
|
12484
|
+
...tagsOf(subject),
|
|
12090
12485
|
meta: {
|
|
12091
12486
|
subject,
|
|
12092
12487
|
verdict: verdict.kind
|
|
@@ -12497,6 +12892,21 @@ var en_default = {
|
|
|
12497
12892
|
"system.device-consumable-normal.title": "Consumable replaced",
|
|
12498
12893
|
"system.device-consumable-normal.body": "{{item}} on {{deviceName}} is back to {{percentage}}%",
|
|
12499
12894
|
"system.device-consumable-normal.body.unmeasured": "{{item}} on {{deviceName}} is OK again",
|
|
12895
|
+
"system.device-tank-low.title": "Tank low",
|
|
12896
|
+
"system.device-tank-full.title": "Tank full",
|
|
12897
|
+
"system.device-tank-normal.title": "Tank OK",
|
|
12898
|
+
"system.device-tank-low.body.cleanWater": "The clean water tank on {{deviceName}} is running low",
|
|
12899
|
+
"system.device-tank-full.body.cleanWater": "The clean water tank on {{deviceName}} is full",
|
|
12900
|
+
"system.device-tank-normal.body.cleanWater": "The clean water tank on {{deviceName}} is OK again",
|
|
12901
|
+
"system.device-tank-low.body.dirtyWater": "The dirty water tank on {{deviceName}} is running low",
|
|
12902
|
+
"system.device-tank-full.body.dirtyWater": "The dirty water tank on {{deviceName}} is full",
|
|
12903
|
+
"system.device-tank-normal.body.dirtyWater": "The dirty water tank on {{deviceName}} is OK again",
|
|
12904
|
+
"system.device-tank-low.body.detergent": "The detergent tank on {{deviceName}} is running low",
|
|
12905
|
+
"system.device-tank-full.body.detergent": "The detergent tank on {{deviceName}} is full",
|
|
12906
|
+
"system.device-tank-normal.body.detergent": "The detergent tank on {{deviceName}} is OK again",
|
|
12907
|
+
"system.device-tank-low.body.dustBin": "The dust bin on {{deviceName}} is running low",
|
|
12908
|
+
"system.device-tank-full.body.dustBin": "The dust bin on {{deviceName}} is full",
|
|
12909
|
+
"system.device-tank-normal.body.dustBin": "The dust bin on {{deviceName}} is OK again",
|
|
12500
12910
|
"system.camera-online.title": "Device online",
|
|
12501
12911
|
"system.camera-online.body": "Device {{deviceName}} is online",
|
|
12502
12912
|
"system.camera-offline.title": "Device offline",
|
|
@@ -12786,6 +13196,21 @@ var it_default = {
|
|
|
12786
13196
|
"system.device-consumable-normal.title": "Consumabile sostituito",
|
|
12787
13197
|
"system.device-consumable-normal.body": "{{item}} su {{deviceName}} è tornato al {{percentage}}%",
|
|
12788
13198
|
"system.device-consumable-normal.body.unmeasured": "{{item}} su {{deviceName}} è di nuovo a posto",
|
|
13199
|
+
"system.device-tank-low.title": "Serbatoio quasi vuoto",
|
|
13200
|
+
"system.device-tank-full.title": "Serbatoio pieno",
|
|
13201
|
+
"system.device-tank-normal.title": "Serbatoio a posto",
|
|
13202
|
+
"system.device-tank-low.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è quasi vuoto",
|
|
13203
|
+
"system.device-tank-full.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è pieno",
|
|
13204
|
+
"system.device-tank-normal.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è di nuovo a posto",
|
|
13205
|
+
"system.device-tank-low.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è quasi vuoto",
|
|
13206
|
+
"system.device-tank-full.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è pieno",
|
|
13207
|
+
"system.device-tank-normal.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è di nuovo a posto",
|
|
13208
|
+
"system.device-tank-low.body.detergent": "Il serbatoio del detergente di {{deviceName}} è quasi vuoto",
|
|
13209
|
+
"system.device-tank-full.body.detergent": "Il serbatoio del detergente di {{deviceName}} è pieno",
|
|
13210
|
+
"system.device-tank-normal.body.detergent": "Il serbatoio del detergente di {{deviceName}} è di nuovo a posto",
|
|
13211
|
+
"system.device-tank-low.body.dustBin": "Il contenitore della polvere di {{deviceName}} è quasi vuoto",
|
|
13212
|
+
"system.device-tank-full.body.dustBin": "Il contenitore della polvere di {{deviceName}} è pieno",
|
|
13213
|
+
"system.device-tank-normal.body.dustBin": "Il contenitore della polvere di {{deviceName}} è di nuovo a posto",
|
|
12789
13214
|
"system.camera-online.title": "Dispositivo online",
|
|
12790
13215
|
"system.camera-online.body": "Il dispositivo {{deviceName}} è online",
|
|
12791
13216
|
"system.camera-offline.title": "Dispositivo offline",
|
|
@@ -14169,6 +14594,10 @@ function systemEventKeys(kind) {
|
|
|
14169
14594
|
titleKey: `system.${kind}.title`,
|
|
14170
14595
|
bodyKey: `system.${kind}.body.ignored`
|
|
14171
14596
|
};
|
|
14597
|
+
if (require_dist.NC_TANK_SYSTEM_EVENT_KINDS.includes(kind)) return {
|
|
14598
|
+
titleKey: `system.${kind}.title`,
|
|
14599
|
+
bodyKey: `system.${kind}.body.cleanWater`
|
|
14600
|
+
};
|
|
14172
14601
|
return {
|
|
14173
14602
|
titleKey: `system.${kind}.title`,
|
|
14174
14603
|
bodyKey: `system.${kind}.body`
|
|
@@ -16184,7 +16613,7 @@ function makeNcActionHandlers(deps) {
|
|
|
16184
16613
|
catalog: [...require_dist.NC_CONDITION_CATALOG],
|
|
16185
16614
|
taxonomy: require_dist.NC_TAXONOMY
|
|
16186
16615
|
}),
|
|
16187
|
-
"nc.getTemplateCatalog": async () => ({ vars:
|
|
16616
|
+
"nc.getTemplateCatalog": async () => ({ vars: require_dist.servedTemplateVars() }),
|
|
16188
16617
|
"nc.previewTemplate": async (input) => {
|
|
16189
16618
|
const texts = deps.previewTexts;
|
|
16190
16619
|
if (texts === void 0) throw new Error("nc.previewTemplate: text catalog not wired");
|
|
@@ -23764,7 +24193,7 @@ var NcRuleStore = class {
|
|
|
23764
24193
|
*/
|
|
23765
24194
|
async load() {
|
|
23766
24195
|
await this.ledger.load();
|
|
23767
|
-
this.loaded = true;
|
|
24196
|
+
if (this.ledger.hasAnswered()) this.loaded = true;
|
|
23768
24197
|
this.logger.debug("notification rules loaded", { meta: { rules: this.ledger.size } });
|
|
23769
24198
|
}
|
|
23770
24199
|
/**
|
|
@@ -28194,7 +28623,6 @@ var ARM_ANYWAY_REJECTION_COPY = {
|
|
|
28194
28623
|
*/
|
|
28195
28624
|
var NC_DEVICE_ALERT_FALLBACK = {
|
|
28196
28625
|
batteryLowPercent: 20,
|
|
28197
|
-
consumableLowPercent: 15,
|
|
28198
28626
|
renotifyMinutes: 720,
|
|
28199
28627
|
notifyRecovery: true
|
|
28200
28628
|
};
|
|
@@ -29449,6 +29877,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29449
29877
|
return (await this.consumeEventAndReport(event)).outcomes.some((outcome) => outcome.matched);
|
|
29450
29878
|
},
|
|
29451
29879
|
rulesVersion: () => this.rulesFingerprint(),
|
|
29880
|
+
consumableThresholds: () => this.rules.isLoaded() ? consumableThresholdCensus(this.rules.list()) : null,
|
|
29452
29881
|
settings: () => this.deviceAlertSettings(),
|
|
29453
29882
|
lookup: (deviceId) => this.deviceDirectory.get(deviceId),
|
|
29454
29883
|
rows: this.alertStore.port(),
|
|
@@ -30557,6 +30986,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30557
30986
|
for (const item of consumables.items) this.deviceAlerts.observeConsumable(consumables.deviceId, item);
|
|
30558
30987
|
return;
|
|
30559
30988
|
}
|
|
30989
|
+
const tanks = event.category === "device.state-changed" ? vacuumTanksOf(event.data) : null;
|
|
30990
|
+
if (tanks !== null) {
|
|
30991
|
+
for (const tank of tanks.tanks) this.deviceAlerts.observeTank(tanks.deviceId, tank);
|
|
30992
|
+
return;
|
|
30993
|
+
}
|
|
30560
30994
|
const lookup = this.deviceDirectory.lookup();
|
|
30561
30995
|
const incoming = incomingFromSystemEvent(event, lookup);
|
|
30562
30996
|
if (incoming === null) {
|
|
@@ -30675,7 +31109,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30675
31109
|
catalog: [...require_dist.NC_CONDITION_CATALOG],
|
|
30676
31110
|
taxonomy: require_dist.NC_TAXONOMY
|
|
30677
31111
|
}),
|
|
30678
|
-
getTemplateCatalog: async () => ({ vars:
|
|
31112
|
+
getTemplateCatalog: async () => ({ vars: require_dist.servedTemplateVars() }),
|
|
30679
31113
|
previewTemplate: async (input) => previewTemplate(this.texts, input),
|
|
30680
31114
|
getHistory: async ({ filter }) => {
|
|
30681
31115
|
const entries = await this.outbox.queryHistory({
|
|
@@ -32478,7 +32912,6 @@ var NotificationCenter = class NotificationCenter {
|
|
|
32478
32912
|
deviceAlertSettings() {
|
|
32479
32913
|
return this.deps.readDeviceAlertSettings?.() ?? {
|
|
32480
32914
|
batteryLowPercent: NC_DEVICE_ALERT_FALLBACK.batteryLowPercent,
|
|
32481
|
-
consumableLowPercent: NC_DEVICE_ALERT_FALLBACK.consumableLowPercent,
|
|
32482
32915
|
renotifyMinutes: NC_DEVICE_ALERT_FALLBACK.renotifyMinutes,
|
|
32483
32916
|
notifyRecovery: NC_DEVICE_ALERT_FALLBACK.notifyRecovery
|
|
32484
32917
|
};
|
|
@@ -56100,19 +56533,6 @@ var DeviceAlertSettingsSchema = require_dist.object({
|
|
|
56100
56533
|
*/
|
|
56101
56534
|
batteryLowPercent: require_dist.number().int().min(1).max(99).default(20),
|
|
56102
56535
|
/**
|
|
56103
|
-
* What counts as a nearly-exhausted CONSUMABLE — a filter, a brush, a mop pad.
|
|
56104
|
-
*
|
|
56105
|
-
* Its own number and not the battery's, because the two are different chores
|
|
56106
|
-
* on different clocks: a battery at 20 % is hours from flat, a brush at 20 %
|
|
56107
|
-
* is weeks from worn, and an operator who wants to be told early about one
|
|
56108
|
-
* does not necessarily want to be told early about the other.
|
|
56109
|
-
*
|
|
56110
|
-
* Applies to the MEASURED side only. An item whose provider states a
|
|
56111
|
-
* `replace` verdict is low whatever this says — the provider decided, and a
|
|
56112
|
-
* threshold cannot talk it out of it (`alerts/consumable-band.ts`).
|
|
56113
|
-
*/
|
|
56114
|
-
consumableLowPercent: require_dist.number().int().min(1).max(99).default(15),
|
|
56115
|
-
/**
|
|
56116
56536
|
* How long a still-true alert waits before it says itself again.
|
|
56117
56537
|
*
|
|
56118
56538
|
* **0 = say it once.** That is the behaviour of every other notification this
|
|
@@ -77816,7 +78236,7 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
77816
78236
|
{
|
|
77817
78237
|
id: "device-alerts",
|
|
77818
78238
|
title: "Battery & offline alerts",
|
|
77819
|
-
description: "What counts as a low battery, how often a still-true alert is repeated, and whether the return to normal is announced. These govern the
|
|
78239
|
+
description: "What counts as a low battery, how often a still-true alert is repeated, and whether the return to normal is announced. These govern the device-event rules; the rules themselves still choose which devices and which kinds — and each rule sets its own consumable threshold. A device with a binary low-battery indicator (no real percentage) is judged by the indicator, and the threshold below does not apply to it.",
|
|
77820
78240
|
columns: 2,
|
|
77821
78241
|
fields: [
|
|
77822
78242
|
{
|
|
@@ -77830,17 +78250,6 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
77830
78250
|
default: DEVICE_ALERT_DEFAULTS.batteryLowPercent,
|
|
77831
78251
|
unit: "%"
|
|
77832
78252
|
},
|
|
77833
|
-
{
|
|
77834
|
-
type: "number",
|
|
77835
|
-
key: "consumableLowPercent",
|
|
77836
|
-
label: "Low consumable at",
|
|
77837
|
-
description: "At or below this, a measured consumable — a filter, a brush, a mop pad — is nearly exhausted. Its own number rather than the battery’s: a battery at 20% is hours from flat, a brush at 20% is weeks from worn. An item whose device reports “replace” outright is announced whatever this says.",
|
|
77838
|
-
min: 1,
|
|
77839
|
-
max: 99,
|
|
77840
|
-
step: 1,
|
|
77841
|
-
default: DEVICE_ALERT_DEFAULTS.consumableLowPercent,
|
|
77842
|
-
unit: "%"
|
|
77843
|
-
},
|
|
77844
78253
|
{
|
|
77845
78254
|
type: "number",
|
|
77846
78255
|
key: "alertRenotifyMinutes",
|
|
@@ -84000,7 +84409,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
84000
84409
|
},
|
|
84001
84410
|
readDeviceAlertSettings: () => ({
|
|
84002
84411
|
batteryLowPercent: this.deviceAlertSettingsMirror.batteryLowPercent,
|
|
84003
|
-
consumableLowPercent: this.deviceAlertSettingsMirror.consumableLowPercent,
|
|
84004
84412
|
renotifyMinutes: this.deviceAlertSettingsMirror.alertRenotifyMinutes,
|
|
84005
84413
|
notifyRecovery: this.deviceAlertSettingsMirror.alertNotifyRecovery
|
|
84006
84414
|
}),
|