@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as
|
|
1
|
+
import { $ as NcTemplateVarDescriptorSchema, $t as sensorRaisedOf, A as NC_DEFAULT_SNOOZE_MINUTES, An as unknown, At as failureContributionCapability, B as NcRuleInputSchema, Bt as mayWriteToLocation, C as MAX_BIRTH_DECISION_RECORDS, Cn as discriminatedUnion, Ct as deriveRecordingMode, D as NC_ALARM_SYSTEM_EVENT_KINDS, Dn as partialRecord, Dt as evaluateSensorEdge, E as MediaFileKindEnum, En as object, Et as encodeVectorBase64, F as NC_TEMPLATE_VARS, Ft as isMediaOwnerType, G as NcScheduleSchema, Gt as pipelineAnalyticsCapability, H as NcRuleSchema, Ht as occupancyScope, I as NcAlarmConfigSchema, It as isScheduleActive, J as NcSnoozeSuppressedSchema, Jt as readTimelapseGeneratedAt, K as NcSnoozeInputSchema, Kt as plateGalleryCapability, L as NcAlarmSettingsPatchSchema, Lt as isSourceCap, Mt as isDetectionMacroClass, N as NC_TANK_SYSTEM_EVENT_KINDS, Nt as isEventOwnerType, O as NC_CONDITION_CATALOG, On as record, Ot as evictionPolicyOfLocation, P as NC_TAXONOMY, Pt as isKnownTemplateVar, Q as NcTemplatePreviewSchema, Qt as sceneMonitorCapability, R as NcConditionDescriptorSchema, Rt as isSystemDelivery, S as MAX_ARCHIVED_DEBUG_NOTES, Sn as boolean, St as cosineSimilarity$1, T as MOTION_CLOSE_AFTER_MS, Tn as number, U as NcRuleTargetSchema, V as NcRulePatchSchema, Vt as notificationRulesCapability, W as NcRuleUpdateInputSchema, Wt as pickClusterStepModels, X as NcTaxonomySchema, Y as NcSystemEventKindSchema, Yt as resolveLocationMode, Z as NcTemplatePreviewInputSchema, Zt as ruleBelongsToAlarm$1, _ as FIRST_LEVEL_MACRO_CLASSES, _n as isDeviceScopedCap, _t as assertTimelapseCadences, a as BirthDecisionRecordSchema, an as templateVarsFor, b as LabelAttributionSchema, bn as _enum, bt as buildEventKindDescriptor, c as CoverStateSchema, cn as errMsg$1, ct as SCENE_DIVERGED, dn as CamProfileSchema, dt as TimelapseRuleInputSchema, en as servedTemplateVars, et as OpsLogEntrySchema, f as DETECTION_MACRO_CLASSES, fn as DeviceType, ft as TimelapseRulePatchSchema, g as EVENT_OWNER_TYPES, gn as hydrateSchema, gt as alarmPanelCapability, h as EVENT_KIND_BY_CAP, hn as defineCustomActions, ht as addonWidgetsSourceCapability, i as BaseDevice, in as systemEventFilterApplies, it as RetrainStatusSchema, j as NC_SENSOR_TRANSITION_KINDS, jn as EventCategory, k as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, kn as string, kt as faceGalleryCapability, l as DEFAULT_EVENT_COLOR, ln as canonicalHash, lt as SceneMonitorSchema, m as DeclaredDevices, mn as customAction, mt as TrackSourceSchema, n as AlarmArmModeSchema, nn as subKindsOf, nt as RECORDING_EXPORT_MAX_READ_BYTES, o as CLUSTER_MODEL_SCOPED_STEPS, on as vectorDimFromBase64, ot as SCENE_DEFAULT_ANCHOR_THRESHOLD, pn as createEvent, pt as TimelapseRuleSchema, q as NcSnoozeSchema, qt as readDeviceStateFrom, r as ArchivedDebugNoteSchema, rn as systemEventFamilyProblems, rt as RECORDING_SOURCE_CAMSTACK_ADDON, s as COCO_TO_MACRO, sn as zoneAnalyticsCapability, st as SCENE_DEFAULT_UNCOVERED_POLICY, t as AUDIO_MACRO_LABELS, tn as storageOccupancyCapability, un as BaseAddon, ut as TIMELAPSE_DENSE_FLOOR_SEC, v as FULL_IMAGE_BBOX, vn as nodePin, vt as audioMetricsCapability, wn as literal, wt as describeSystemEventFamilyProblem, x as LockStateSchema, xn as array, xt as consumableLowPercentOf, y as FailureCounters, yn as sleep$1, yt as audioModeOf, z as NcConditionsSchema, zt as kebabToCamel } from "../dist-zp1v7cCp.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -6656,6 +6656,8 @@ var SceneEngine = class {
|
|
|
6656
6656
|
var DEFAULT_LOAD_LIMIT = 1e5;
|
|
6657
6657
|
var DurableLedger = class DurableLedger {
|
|
6658
6658
|
mirror = /* @__PURE__ */ new Map();
|
|
6659
|
+
/** Whether ANY load has read the store successfully — see {@link hasAnswered}. */
|
|
6660
|
+
answered = false;
|
|
6659
6661
|
spec;
|
|
6660
6662
|
store;
|
|
6661
6663
|
logger;
|
|
@@ -6711,6 +6713,7 @@ var DurableLedger = class DurableLedger {
|
|
|
6711
6713
|
}
|
|
6712
6714
|
this.mirror.clear();
|
|
6713
6715
|
for (const [key, row] of next) this.mirror.set(key, row);
|
|
6716
|
+
this.answered = true;
|
|
6714
6717
|
if (skipped > 0) this.logger.warn("durable rows skipped as malformed — they gate NOTHING", { meta: {
|
|
6715
6718
|
collection: this.spec.collection,
|
|
6716
6719
|
skipped
|
|
@@ -6725,6 +6728,16 @@ var DurableLedger = class DurableLedger {
|
|
|
6725
6728
|
return [...this.mirror.values()];
|
|
6726
6729
|
}
|
|
6727
6730
|
}
|
|
6731
|
+
/**
|
|
6732
|
+
* Whether the mirror is an ANSWER from the store: true once any load has
|
|
6733
|
+
* succeeded, and never false again (a later failed load keeps that answer).
|
|
6734
|
+
* Before it, an empty mirror is "not read yet", never "the store is empty" —
|
|
6735
|
+
* {@link load} swallows its failure, so a caller that must tell the two apart
|
|
6736
|
+
* asks here (D642 review I1).
|
|
6737
|
+
*/
|
|
6738
|
+
hasAnswered() {
|
|
6739
|
+
return this.answered;
|
|
6740
|
+
}
|
|
6728
6741
|
/** Every mirrored row, insertion-ordered. */
|
|
6729
6742
|
snapshot() {
|
|
6730
6743
|
return [...this.mirror.values()];
|
|
@@ -8458,6 +8471,78 @@ var NcAlertStore = class {
|
|
|
8458
8471
|
};
|
|
8459
8472
|
}
|
|
8460
8473
|
};
|
|
8474
|
+
//#endregion
|
|
8475
|
+
//#region src/notification-center/alerts/consumable-subject.ts
|
|
8476
|
+
/**
|
|
8477
|
+
* A consumable EPISODE's name, and the thresholds the rule set asks for (D642).
|
|
8478
|
+
*
|
|
8479
|
+
* Before D642 one cluster-wide threshold judged every item, so an item had one
|
|
8480
|
+
* episode: `consumable:<deviceId>:<key>`. The threshold is now the RULE's, and
|
|
8481
|
+
* the ledger keeps one episode per threshold a rule asks for — each with its
|
|
8482
|
+
* own hysteresis, pending raise and reminder — named
|
|
8483
|
+
* `consumable@<threshold>:<deviceId>:<key>`.
|
|
8484
|
+
*
|
|
8485
|
+
* The threshold rides the FAMILY segment on purpose: the device id stays the
|
|
8486
|
+
* second segment (`alertSubjectDeviceId`, D636 — the log tag and the indexed
|
|
8487
|
+
* column read it), and the item key stays last and whole, whatever it holds.
|
|
8488
|
+
*
|
|
8489
|
+
* Pure. No clock, no I/O.
|
|
8490
|
+
*/
|
|
8491
|
+
var FAMILY = "consumable";
|
|
8492
|
+
/** The episode of `key` on `deviceId`, judged at `thresholdPercent`. */
|
|
8493
|
+
function consumableSubject(thresholdPercent, deviceId, key) {
|
|
8494
|
+
return `${FAMILY}@${String(thresholdPercent)}:${String(deviceId)}:${key}`;
|
|
8495
|
+
}
|
|
8496
|
+
/** The pre-D642 subject of the same item — read only to carry it over. */
|
|
8497
|
+
function legacyConsumableSubject(deviceId, key) {
|
|
8498
|
+
return `${FAMILY}:${String(deviceId)}:${key}`;
|
|
8499
|
+
}
|
|
8500
|
+
/** `null` for a subject that is not a consumable's. */
|
|
8501
|
+
function parseConsumableSubject(subject) {
|
|
8502
|
+
const first = subject.indexOf(":");
|
|
8503
|
+
const second = first < 0 ? -1 : subject.indexOf(":", first + 1);
|
|
8504
|
+
if (first < 0 || second < 0) return null;
|
|
8505
|
+
const family = subject.slice(0, first);
|
|
8506
|
+
const deviceId = Number(subject.slice(first + 1, second));
|
|
8507
|
+
const key = subject.slice(second + 1);
|
|
8508
|
+
if (!Number.isInteger(deviceId) || key.length === 0) return null;
|
|
8509
|
+
if (family === FAMILY) return {
|
|
8510
|
+
thresholdPercent: 15,
|
|
8511
|
+
deviceId,
|
|
8512
|
+
key,
|
|
8513
|
+
legacy: true
|
|
8514
|
+
};
|
|
8515
|
+
if (!family.startsWith(`${FAMILY}@`)) return null;
|
|
8516
|
+
const thresholdPercent = Number(family.slice(11));
|
|
8517
|
+
if (!Number.isInteger(thresholdPercent)) return null;
|
|
8518
|
+
return {
|
|
8519
|
+
thresholdPercent,
|
|
8520
|
+
deviceId,
|
|
8521
|
+
key,
|
|
8522
|
+
legacy: false
|
|
8523
|
+
};
|
|
8524
|
+
}
|
|
8525
|
+
/**
|
|
8526
|
+
* Every consumable threshold the rule set asks for, ascending.
|
|
8527
|
+
*
|
|
8528
|
+
* A rule asks when it is a system-event rule whose kinds include a consumable
|
|
8529
|
+
* kind — enabled or not: a disabled rule's episode stays PENDING (no rule
|
|
8530
|
+
* matched it) and is re-offered the moment the rule is switched back on
|
|
8531
|
+
* (D636), which is what that rule would have said had it been on. With no such
|
|
8532
|
+
* rule at all the default is still judged, so an episode exists for the first
|
|
8533
|
+
* rule an operator writes to hear (D636's "offered once per rule change").
|
|
8534
|
+
*/
|
|
8535
|
+
function consumableThresholdCensus(rules) {
|
|
8536
|
+
const thresholds = /* @__PURE__ */ new Set();
|
|
8537
|
+
for (const rule of rules) {
|
|
8538
|
+
const condition = rule.conditions.systemEvent;
|
|
8539
|
+
if (rule.delivery !== "system-event" || condition === void 0) continue;
|
|
8540
|
+
if (!condition.kinds.some((kind) => NC_CONSUMABLE_SYSTEM_EVENT_KINDS.includes(kind))) continue;
|
|
8541
|
+
thresholds.add(consumableLowPercentOf(condition));
|
|
8542
|
+
}
|
|
8543
|
+
if (thresholds.size === 0) thresholds.add(15);
|
|
8544
|
+
return [...thresholds].toSorted((a, b) => a - b);
|
|
8545
|
+
}
|
|
8461
8546
|
function deriveConsumableBand(input) {
|
|
8462
8547
|
const { level, status } = input.reading;
|
|
8463
8548
|
const measured = level !== null && Number.isFinite(level);
|
|
@@ -10121,6 +10206,9 @@ function systemEventExclusion(condition, subject) {
|
|
|
10121
10206
|
if (wantedPackages !== void 0 && applies("packageNames")) {
|
|
10122
10207
|
if (!subjectPackages(subject).some((name) => wantedPackages.includes(name))) return "packageNames";
|
|
10123
10208
|
}
|
|
10209
|
+
if (applies("consumableLowPercent") && subject.consumableLowPercent !== void 0) {
|
|
10210
|
+
if (subject.consumableLowPercent !== consumableLowPercentOf(condition)) return "consumableLowPercent";
|
|
10211
|
+
}
|
|
10124
10212
|
}
|
|
10125
10213
|
function subjectNodes(subject) {
|
|
10126
10214
|
const ids = [...subject.nodeIds ?? []];
|
|
@@ -11000,6 +11088,7 @@ function systemSubject(input) {
|
|
|
11000
11088
|
...input.packageNames !== void 0 ? { packageNames: input.packageNames } : {},
|
|
11001
11089
|
...input.textCount !== void 0 ? { textCount: input.textCount } : {},
|
|
11002
11090
|
...input.alarmSource !== void 0 ? { alarmSource: input.alarmSource } : {},
|
|
11091
|
+
...input.consumableLowPercent !== void 0 ? { consumableLowPercent: input.consumableLowPercent } : {},
|
|
11003
11092
|
data
|
|
11004
11093
|
}
|
|
11005
11094
|
},
|
|
@@ -11101,9 +11190,11 @@ function incomingFromBatteryBand(input) {
|
|
|
11101
11190
|
* these to be treated as the battery notifications are — with two differences
|
|
11102
11191
|
* that come from the subject and not from taste:
|
|
11103
11192
|
*
|
|
11104
|
-
* - **the subject carries the ITEM.** `consumable
|
|
11105
|
-
* vacuum has a brush AND a filter AND a mop pad, and one subject
|
|
11106
|
-
* three would let the brush's alert cancel the filter's.
|
|
11193
|
+
* - **the subject carries the ITEM.** `consumable@<threshold>:<deviceId>:<key>`,
|
|
11194
|
+
* because a vacuum has a brush AND a filter AND a mop pad, and one subject
|
|
11195
|
+
* for all three would let the brush's alert cancel the filter's. It carries
|
|
11196
|
+
* the THRESHOLD too (D642): each rule's threshold is its own episode, and
|
|
11197
|
+
* the engine pairs a rule with the episode judged at its own number.
|
|
11107
11198
|
* - **the body variant follows what is KNOWN.** An item with no percentage
|
|
11108
11199
|
* gets a finished sentence rather than a template with a stand-in number in
|
|
11109
11200
|
* it, for the same reason a binary battery does.
|
|
@@ -11112,17 +11203,46 @@ function incomingFromBatteryBand(input) {
|
|
|
11112
11203
|
*/
|
|
11113
11204
|
function incomingFromConsumableBand(input) {
|
|
11114
11205
|
const kind = input.band === "low" ? "device-consumable-low" : "device-consumable-normal";
|
|
11115
|
-
const
|
|
11206
|
+
const level = input.item.level;
|
|
11207
|
+
const percentage = level !== null && Number.isFinite(level) ? Math.round(level) : null;
|
|
11208
|
+
const measured = percentage !== null;
|
|
11116
11209
|
return systemSubject({
|
|
11117
11210
|
facts: input.facts,
|
|
11118
11211
|
kind,
|
|
11119
|
-
subject:
|
|
11212
|
+
subject: consumableSubject(input.thresholdPercent, input.deviceId, input.item.key),
|
|
11120
11213
|
titleKey: `system.${kind}.title`,
|
|
11121
11214
|
bodyKey: measured ? `system.${kind}.body` : `system.${kind}.body.unmeasured`,
|
|
11122
11215
|
textParams: {
|
|
11123
11216
|
...deviceTextParams(input.deviceId, input.identity),
|
|
11124
11217
|
item: input.item.label,
|
|
11125
|
-
...
|
|
11218
|
+
...percentage !== null ? { percentage: String(percentage) } : {}
|
|
11219
|
+
},
|
|
11220
|
+
deviceId: input.deviceId,
|
|
11221
|
+
consumableLowPercent: input.thresholdPercent,
|
|
11222
|
+
...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
|
|
11223
|
+
});
|
|
11224
|
+
}
|
|
11225
|
+
/**
|
|
11226
|
+
* A vacuum TANK band, as a system-event subject (D642).
|
|
11227
|
+
*
|
|
11228
|
+
* Built like {@link incomingFromConsumableBand}: `tank:<deviceId>:<tank>` is
|
|
11229
|
+
* one subject per tank (the dust bin's alert must not cancel the clean water's),
|
|
11230
|
+
* and the recovery keeps it. The BODY VARIANT names the tank in the reader's
|
|
11231
|
+
* language — the params are frozen before the language is known, so the tank's
|
|
11232
|
+
* name cannot be a param; `tank` carries the stable token for a custom
|
|
11233
|
+
* template.
|
|
11234
|
+
*/
|
|
11235
|
+
function incomingFromTankBand(input) {
|
|
11236
|
+
const kind = input.band === "low" ? "device-tank-low" : input.band === "full" ? "device-tank-full" : "device-tank-normal";
|
|
11237
|
+
return systemSubject({
|
|
11238
|
+
facts: input.facts,
|
|
11239
|
+
kind,
|
|
11240
|
+
subject: `tank:${String(input.deviceId)}:${input.tank}`,
|
|
11241
|
+
titleKey: `system.${kind}.title`,
|
|
11242
|
+
bodyKey: `system.${kind}.body.${input.tank}`,
|
|
11243
|
+
textParams: {
|
|
11244
|
+
...deviceTextParams(input.deviceId, input.identity),
|
|
11245
|
+
tank: input.tank
|
|
11126
11246
|
},
|
|
11127
11247
|
deviceId: input.deviceId,
|
|
11128
11248
|
...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
|
|
@@ -11710,6 +11830,91 @@ function deliveryForKind(kind) {
|
|
|
11710
11830
|
return kind;
|
|
11711
11831
|
}
|
|
11712
11832
|
//#endregion
|
|
11833
|
+
//#region src/notification-center/alerts/tank-band.ts
|
|
11834
|
+
/**
|
|
11835
|
+
* A vacuum TANK reduced to the only thing worth storing: its band (D642).
|
|
11836
|
+
*
|
|
11837
|
+
* The `vacuum-control` slice carries four tanks — clean water, dirty water,
|
|
11838
|
+
* detergent, dust bin — each `null` when the hardware has none, else
|
|
11839
|
+
* `{ level, status }`. The operator asked for them to notify like consumables:
|
|
11840
|
+
* clean water / detergent running low, dirty water / dust bin full.
|
|
11841
|
+
*
|
|
11842
|
+
* ## The verdict is the provider's
|
|
11843
|
+
*
|
|
11844
|
+
* A tank's `status` is the device's own float switch (`ok` | `low` | `full`),
|
|
11845
|
+
* and it is the ONLY thing judged. The hub has no threshold for a tank level:
|
|
11846
|
+
* the rule for thresholds is that they belong to whoever generates the event,
|
|
11847
|
+
* and a tank that says `low` has already decided. A tank that reports only a
|
|
11848
|
+
* `level` is therefore not judged at all (`null`) rather than judged against a
|
|
11849
|
+
* number the hub would have to invent.
|
|
11850
|
+
*
|
|
11851
|
+
* ## `null` is the third answer, and it is not `normal`
|
|
11852
|
+
*
|
|
11853
|
+
* An unknown status says nothing — it neither opens nor closes an episode, for
|
|
11854
|
+
* the reason `battery-band.ts` gives: a reading nobody took is not evidence
|
|
11855
|
+
* that anything changed.
|
|
11856
|
+
*
|
|
11857
|
+
* Pure. No clock, no I/O.
|
|
11858
|
+
*/
|
|
11859
|
+
/** The four tanks `vacuum-control` names, by their slice field. */
|
|
11860
|
+
var NC_TANK_KEYS = [
|
|
11861
|
+
"cleanWater",
|
|
11862
|
+
"dirtyWater",
|
|
11863
|
+
"detergent",
|
|
11864
|
+
"dustBin"
|
|
11865
|
+
];
|
|
11866
|
+
/**
|
|
11867
|
+
* How long a tank change must HOLD before it counts (D642 review M2).
|
|
11868
|
+
*
|
|
11869
|
+
* A float switch sloshing at the edge of `low` / `full` flips its status back
|
|
11870
|
+
* and forth while the robot moves; each flip is a durable transition, and
|
|
11871
|
+
* without a hold each one is a raise and a recovery on the operator's phone.
|
|
11872
|
+
* One minute is two reload ticks: long enough to outlast a slosh, short enough
|
|
11873
|
+
* that a genuinely full dust bin is said within a minute and a half.
|
|
11874
|
+
*/
|
|
11875
|
+
var NC_TANK_HOLD_MS = 6e4;
|
|
11876
|
+
function deriveTankBand(reading) {
|
|
11877
|
+
switch (reading.status) {
|
|
11878
|
+
case "low": return "low";
|
|
11879
|
+
case "full": return "full";
|
|
11880
|
+
case "ok": return "normal";
|
|
11881
|
+
case null: return null;
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
function isRecord$4(value) {
|
|
11885
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11886
|
+
}
|
|
11887
|
+
function tankStatusOf(value) {
|
|
11888
|
+
return value === "ok" || value === "low" || value === "full" ? value : null;
|
|
11889
|
+
}
|
|
11890
|
+
/**
|
|
11891
|
+
* A `device.state-changed` payload carrying a `vacuum-control` slice, reduced
|
|
11892
|
+
* to its tanks. `null` for any other cap. A tank the hardware does not have
|
|
11893
|
+
* (`null`) is skipped; a malformed field reads as unknown, never as a
|
|
11894
|
+
* fabricated verdict (D393: absent is not zero).
|
|
11895
|
+
*/
|
|
11896
|
+
function vacuumTanksOf(data) {
|
|
11897
|
+
if (data["capName"] !== "vacuum-control") return null;
|
|
11898
|
+
const deviceId = data["deviceId"];
|
|
11899
|
+
const slice = data["slice"];
|
|
11900
|
+
if (typeof deviceId !== "number" || !isRecord$4(slice)) return null;
|
|
11901
|
+
const tanks = [];
|
|
11902
|
+
for (const key of NC_TANK_KEYS) {
|
|
11903
|
+
const raw = slice[key];
|
|
11904
|
+
if (!isRecord$4(raw)) continue;
|
|
11905
|
+
const level = raw["level"];
|
|
11906
|
+
tanks.push({
|
|
11907
|
+
key,
|
|
11908
|
+
level: typeof level === "number" && Number.isFinite(level) ? level : null,
|
|
11909
|
+
status: tankStatusOf(raw["status"])
|
|
11910
|
+
});
|
|
11911
|
+
}
|
|
11912
|
+
return tanks.length === 0 ? null : {
|
|
11913
|
+
deviceId,
|
|
11914
|
+
tanks
|
|
11915
|
+
};
|
|
11916
|
+
}
|
|
11917
|
+
//#endregion
|
|
11713
11918
|
//#region src/notification-center/alerts/battery-band.ts
|
|
11714
11919
|
/**
|
|
11715
11920
|
* The stand-in a provider writes for a binary NORMAL. Anything below it, on a
|
|
@@ -11740,10 +11945,14 @@ function deriveBatteryBand(input) {
|
|
|
11740
11945
|
* `NcDeviceAlerts` — the one place a SUSTAINED device fault becomes a
|
|
11741
11946
|
* notification, and keeps becoming one until it stops being true.
|
|
11742
11947
|
*
|
|
11743
|
-
* It owns
|
|
11948
|
+
* It owns these families and nothing else:
|
|
11744
11949
|
*
|
|
11745
11950
|
* - **battery band** — `battery:<deviceId>`, fed by the `battery` runtime-state
|
|
11746
11951
|
* slice that already arrives on `device.state-changed`;
|
|
11952
|
+
* - **consumable band** — `consumable@<threshold>:<deviceId>:<key>`, one
|
|
11953
|
+
* episode per item per threshold a RULE asks for (D642);
|
|
11954
|
+
* - **tank band** — `tank:<deviceId>:<tank>`, a vacuum's tanks, judged on the
|
|
11955
|
+
* provider's own verdict (D642);
|
|
11747
11956
|
* - **the reminder tick** — the operator's "how often do you say it again".
|
|
11748
11957
|
*
|
|
11749
11958
|
* It does NOT own device liveness. `device-offline` already has a producer, a
|
|
@@ -11783,6 +11992,10 @@ var PENDING_CADENCE_MAX_MS = 36e5;
|
|
|
11783
11992
|
function pendingCadenceMs(settings) {
|
|
11784
11993
|
return settings.renotifyMinutes > 0 ? Math.min(settings.renotifyMinutes * 6e4, PENDING_CADENCE_MAX_MS) : PENDING_CADENCE_MAX_MS;
|
|
11785
11994
|
}
|
|
11995
|
+
/** The RAM key of one item's last reading — every threshold's episode reads it. */
|
|
11996
|
+
function itemKeyOf(deviceId, key) {
|
|
11997
|
+
return `${String(deviceId)}:${key}`;
|
|
11998
|
+
}
|
|
11786
11999
|
var NcDeviceAlerts = class {
|
|
11787
12000
|
deps;
|
|
11788
12001
|
now;
|
|
@@ -11794,8 +12007,28 @@ var NcDeviceAlerts = class {
|
|
|
11794
12007
|
* durable.
|
|
11795
12008
|
*/
|
|
11796
12009
|
lastReading = /* @__PURE__ */ new Map();
|
|
11797
|
-
/**
|
|
12010
|
+
/**
|
|
12011
|
+
* The last reading per consumable ITEM (`<deviceId>:<key>`), for the
|
|
12012
|
+
* reminder sweep and for judging a threshold a rule gains later (D642).
|
|
12013
|
+
*/
|
|
11798
12014
|
lastConsumable = /* @__PURE__ */ new Map();
|
|
12015
|
+
/** The last reading per tank SUBJECT, for the reminder sweep. */
|
|
12016
|
+
lastTank = /* @__PURE__ */ new Map();
|
|
12017
|
+
/**
|
|
12018
|
+
* Tank changes on HOLD (review M2): a band that would move the episode,
|
|
12019
|
+
* first seen at `since`. It counts once it has held for `NC_TANK_HOLD_MS`,
|
|
12020
|
+
* confirmed by a later slice write or by the reload tick — no timer of its
|
|
12021
|
+
* own, so nothing outlives the device. Bounded by the device's tanks (four),
|
|
12022
|
+
* and dropped with the device by {@link forget}.
|
|
12023
|
+
*/
|
|
12024
|
+
tankHold = /* @__PURE__ */ new Map();
|
|
12025
|
+
/**
|
|
12026
|
+
* Thresholds some episode row carries that the LAST census did not ask for.
|
|
12027
|
+
* A threshold's rows are retired only when two consecutive censuses agree it
|
|
12028
|
+
* is gone (D49: a read that would destroy work needs a second read to
|
|
12029
|
+
* agree). RAM only: a restart costs one more tick of patience.
|
|
12030
|
+
*/
|
|
12031
|
+
absentThresholds = /* @__PURE__ */ new Set();
|
|
11799
12032
|
/**
|
|
11800
12033
|
* Subject → WHEN its PENDING raise was last offered, and to which
|
|
11801
12034
|
* {@link NcDeviceAlertsDeps.rulesVersion}. RAM only, on purpose: after a
|
|
@@ -11853,31 +12086,137 @@ var NcDeviceAlerts = class {
|
|
|
11853
12086
|
* observed.
|
|
11854
12087
|
*/
|
|
11855
12088
|
observeConsumable(deviceId, item) {
|
|
11856
|
-
|
|
11857
|
-
|
|
12089
|
+
this.lastConsumable.set(itemKeyOf(deviceId, item.key), {
|
|
12090
|
+
deviceId,
|
|
12091
|
+
item
|
|
12092
|
+
});
|
|
12093
|
+
const thresholds = this.deps.consumableThresholds();
|
|
12094
|
+
if (thresholds === null) {
|
|
12095
|
+
this.logger()?.debug("consumable reading kept — the rules have not been read yet", {
|
|
12096
|
+
tags: { deviceId },
|
|
12097
|
+
meta: { item: item.key }
|
|
12098
|
+
});
|
|
12099
|
+
return;
|
|
12100
|
+
}
|
|
11858
12101
|
const settings = this.deps.settings();
|
|
12102
|
+
for (const thresholdPercent of thresholds) this.judgeConsumable(deviceId, item, thresholdPercent, settings);
|
|
12103
|
+
}
|
|
12104
|
+
/**
|
|
12105
|
+
* ONE vacuum tank, from a device's `vacuum-control` slice (D642).
|
|
12106
|
+
*
|
|
12107
|
+
* The provider's verdict is the band (`tank-band.ts`); an unknown status
|
|
12108
|
+
* leaves everything untouched, an open episode included.
|
|
12109
|
+
*/
|
|
12110
|
+
observeTank(deviceId, tank) {
|
|
12111
|
+
const band = deriveTankBand(tank);
|
|
12112
|
+
if (band === null) return;
|
|
12113
|
+
const subject = `tank:${String(deviceId)}:${tank.key}`;
|
|
12114
|
+
this.lastTank.set(subject, {
|
|
12115
|
+
deviceId,
|
|
12116
|
+
tank
|
|
12117
|
+
});
|
|
12118
|
+
if (!this.tankChangeHeld(subject, deviceId, band)) return;
|
|
12119
|
+
this.apply(subject, band !== "normal", this.deps.settings(), (facts, alerting) => incomingFromTankBand({
|
|
12120
|
+
facts,
|
|
12121
|
+
deviceId,
|
|
12122
|
+
band: alerting ? band : "normal",
|
|
12123
|
+
tank: tank.key,
|
|
12124
|
+
identity: this.deps.lookup(deviceId)
|
|
12125
|
+
}));
|
|
12126
|
+
}
|
|
12127
|
+
/**
|
|
12128
|
+
* Has this tank band held long enough to move its episode? A band that
|
|
12129
|
+
* would not move it (a repeat, or low ⇄ full inside one open episode) passes
|
|
12130
|
+
* at once and clears any hold — that is what makes a flapping switch start
|
|
12131
|
+
* its hold over every time it flips back. A first HEALTHY observation passes
|
|
12132
|
+
* too: it seeds silently, so there is nothing to hold.
|
|
12133
|
+
*/
|
|
12134
|
+
tankChangeHeld(subject, deviceId, band) {
|
|
12135
|
+
const row = this.deps.rows.get(subject);
|
|
12136
|
+
const alerting = band !== "normal";
|
|
12137
|
+
if (!(row === void 0 ? alerting : row.alerting !== alerting)) {
|
|
12138
|
+
this.tankHold.delete(subject);
|
|
12139
|
+
return true;
|
|
12140
|
+
}
|
|
12141
|
+
const nowMs = this.now();
|
|
12142
|
+
const held = this.tankHold.get(subject);
|
|
12143
|
+
if (held === void 0 || held.band !== band) {
|
|
12144
|
+
this.tankHold.set(subject, {
|
|
12145
|
+
band,
|
|
12146
|
+
since: nowMs
|
|
12147
|
+
});
|
|
12148
|
+
this.logger()?.debug("tank change on hold — counts once it has held", {
|
|
12149
|
+
tags: { deviceId },
|
|
12150
|
+
meta: {
|
|
12151
|
+
subject,
|
|
12152
|
+
band,
|
|
12153
|
+
holdMs: NC_TANK_HOLD_MS
|
|
12154
|
+
}
|
|
12155
|
+
});
|
|
12156
|
+
return false;
|
|
12157
|
+
}
|
|
12158
|
+
if (nowMs - held.since < 6e4) return false;
|
|
12159
|
+
this.tankHold.delete(subject);
|
|
12160
|
+
return true;
|
|
12161
|
+
}
|
|
12162
|
+
/** The tick's half of the hold: a change that has held with no new slice write. */
|
|
12163
|
+
confirmHeldTanks() {
|
|
12164
|
+
const nowMs = this.now();
|
|
12165
|
+
for (const [subject, held] of [...this.tankHold]) {
|
|
12166
|
+
if (nowMs - held.since < 6e4) continue;
|
|
12167
|
+
const reading = this.lastTank.get(subject);
|
|
12168
|
+
if (reading === void 0) {
|
|
12169
|
+
this.tankHold.delete(subject);
|
|
12170
|
+
continue;
|
|
12171
|
+
}
|
|
12172
|
+
this.observeTank(reading.deviceId, reading.tank);
|
|
12173
|
+
}
|
|
12174
|
+
}
|
|
12175
|
+
/** One item against ONE threshold: its own episode, its own hysteresis. */
|
|
12176
|
+
judgeConsumable(deviceId, item, thresholdPercent, settings) {
|
|
12177
|
+
const subject = consumableSubject(thresholdPercent, deviceId, item.key);
|
|
12178
|
+
const previous = this.deps.rows.get(subject) ?? this.adoptLegacy(subject, deviceId, item.key);
|
|
11859
12179
|
const band = deriveConsumableBand({
|
|
11860
12180
|
reading: {
|
|
11861
12181
|
level: item.level,
|
|
11862
12182
|
status: item.status
|
|
11863
12183
|
},
|
|
11864
|
-
thresholdPercent
|
|
12184
|
+
thresholdPercent,
|
|
11865
12185
|
previous: previous?.alerting === true ? "low" : previous === void 0 ? void 0 : "normal"
|
|
11866
12186
|
});
|
|
11867
12187
|
if (band === null) return;
|
|
11868
|
-
this.lastConsumable.set(subject, {
|
|
11869
|
-
deviceId,
|
|
11870
|
-
item
|
|
11871
|
-
});
|
|
11872
12188
|
this.apply(subject, band === "low", settings, (facts, alerting) => incomingFromConsumableBand({
|
|
11873
12189
|
facts,
|
|
11874
12190
|
deviceId,
|
|
11875
12191
|
band: alerting ? "low" : "normal",
|
|
11876
12192
|
item,
|
|
12193
|
+
thresholdPercent,
|
|
11877
12194
|
identity: this.deps.lookup(deviceId)
|
|
11878
12195
|
}));
|
|
11879
12196
|
}
|
|
11880
12197
|
/**
|
|
12198
|
+
* A pre-D642 row of this item (`consumable:<id>:<key>`, judged at the
|
|
12199
|
+
* cluster threshold, which shipped as the rule default) is carried over to
|
|
12200
|
+
* the default-threshold episode rather than re-announced: the live vacuum
|
|
12201
|
+
* had two items low and reminding when D642 shipped. Any other threshold
|
|
12202
|
+
* starts fresh; an unadopted legacy row retires with the census.
|
|
12203
|
+
*/
|
|
12204
|
+
adoptLegacy(subject, deviceId, key) {
|
|
12205
|
+
const parts = parseConsumableSubject(subject);
|
|
12206
|
+
const legacySubject = legacyConsumableSubject(deviceId, key);
|
|
12207
|
+
const legacy = this.deps.rows.get(legacySubject);
|
|
12208
|
+
if (legacy === void 0 || parts === null) return void 0;
|
|
12209
|
+
const legacyParts = parseConsumableSubject(legacySubject);
|
|
12210
|
+
if (legacyParts === null || legacyParts.thresholdPercent !== parts.thresholdPercent) return;
|
|
12211
|
+
const adopted = {
|
|
12212
|
+
...legacy,
|
|
12213
|
+
subject
|
|
12214
|
+
};
|
|
12215
|
+
this.deps.rows.put(adopted);
|
|
12216
|
+
this.dropRow(legacySubject, "carried over to its threshold episode (D642)");
|
|
12217
|
+
return adopted;
|
|
12218
|
+
}
|
|
12219
|
+
/**
|
|
11881
12220
|
* The reminder sweep. Called from an existing periodic tick; it allocates
|
|
11882
12221
|
* nothing on the healthy path.
|
|
11883
12222
|
*
|
|
@@ -11888,6 +12227,9 @@ var NcDeviceAlerts = class {
|
|
|
11888
12227
|
*/
|
|
11889
12228
|
tick() {
|
|
11890
12229
|
const settings = this.deps.settings();
|
|
12230
|
+
const thresholds = this.deps.consumableThresholds();
|
|
12231
|
+
if (thresholds !== null) this.reconcileThresholds(thresholds, settings);
|
|
12232
|
+
this.confirmHeldTanks();
|
|
11891
12233
|
this.retryPending(settings);
|
|
11892
12234
|
if (settings.renotifyMinutes <= 0) return;
|
|
11893
12235
|
for (const row of this.deps.rows.all()) {
|
|
@@ -11920,14 +12262,58 @@ var NcDeviceAlerts = class {
|
|
|
11920
12262
|
const subjects = this.deps.rows.all().map((row) => row.subject).filter((subject) => alertSubjectDeviceId(subject) === deviceId);
|
|
11921
12263
|
for (const subject of subjects) {
|
|
11922
12264
|
this.lastReading.delete(subject);
|
|
11923
|
-
this.
|
|
12265
|
+
this.lastTank.delete(subject);
|
|
11924
12266
|
this.offeredAt.delete(subject);
|
|
11925
12267
|
await this.deps.rows.remove(subject);
|
|
11926
12268
|
}
|
|
11927
12269
|
this.lastReading.delete(`battery:${deviceId}`);
|
|
12270
|
+
for (const [key, reading] of this.lastConsumable) if (reading.deviceId === deviceId) this.lastConsumable.delete(key);
|
|
12271
|
+
for (const subject of this.tankHold.keys()) if (alertSubjectDeviceId(subject) === deviceId) this.tankHold.delete(subject);
|
|
12272
|
+
for (const subject of this.lastTank.keys()) if (alertSubjectDeviceId(subject) === deviceId) this.lastTank.delete(subject);
|
|
11928
12273
|
return subjects.length;
|
|
11929
12274
|
}
|
|
11930
12275
|
/**
|
|
12276
|
+
* Bring the consumable episodes in line with the thresholds the rules ask
|
|
12277
|
+
* for (D642). A threshold with no episode for an item yet is judged from the
|
|
12278
|
+
* item's last reading — a rule that gains a threshold hears about an item
|
|
12279
|
+
* already past it on the next tick, not at the next slice write. A
|
|
12280
|
+
* threshold that no rule asks for any more has its episodes RETIRED, but
|
|
12281
|
+
* only when two consecutive censuses agree it is gone; retiring is silent
|
|
12282
|
+
* (nothing recovered — nobody is listening at that number any more) and
|
|
12283
|
+
* logged per row.
|
|
12284
|
+
*/
|
|
12285
|
+
reconcileThresholds(thresholds, settings) {
|
|
12286
|
+
for (const { deviceId, item } of this.lastConsumable.values()) for (const thresholdPercent of thresholds) {
|
|
12287
|
+
if (this.deps.rows.get(consumableSubject(thresholdPercent, deviceId, item.key))) continue;
|
|
12288
|
+
this.judgeConsumable(deviceId, item, thresholdPercent, settings);
|
|
12289
|
+
}
|
|
12290
|
+
const absent = /* @__PURE__ */ new Set();
|
|
12291
|
+
for (const row of this.deps.rows.all()) {
|
|
12292
|
+
const parts = parseConsumableSubject(row.subject);
|
|
12293
|
+
if (parts === null || thresholds.includes(parts.thresholdPercent)) continue;
|
|
12294
|
+
absent.add(parts.thresholdPercent);
|
|
12295
|
+
if (this.absentThresholds.has(parts.thresholdPercent)) this.dropRow(row.subject, "retired — no rule judges consumables at this threshold (D642)");
|
|
12296
|
+
}
|
|
12297
|
+
this.absentThresholds = absent;
|
|
12298
|
+
}
|
|
12299
|
+
/** Remove one row, mirror and durable, with a line that names why. */
|
|
12300
|
+
dropRow(subject, reason) {
|
|
12301
|
+
this.offeredAt.delete(subject);
|
|
12302
|
+
this.logger()?.info(`device alert ${reason}`, {
|
|
12303
|
+
...tagsOf(subject),
|
|
12304
|
+
meta: { subject }
|
|
12305
|
+
});
|
|
12306
|
+
this.deps.rows.remove(subject).catch((error) => {
|
|
12307
|
+
this.logger()?.warn("device alert row could not be removed", {
|
|
12308
|
+
...tagsOf(subject),
|
|
12309
|
+
meta: {
|
|
12310
|
+
subject,
|
|
12311
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12312
|
+
}
|
|
12313
|
+
});
|
|
12314
|
+
});
|
|
12315
|
+
}
|
|
12316
|
+
/**
|
|
11931
12317
|
* Offer every PENDING raise once more — when the rule set changed since its
|
|
11932
12318
|
* last offer, OR when {@link pendingCadenceMs} has passed since it. The
|
|
11933
12319
|
* cadence is what delivers a raise refused only by a rule's SCHEDULE (or any
|
|
@@ -11959,14 +12345,25 @@ var NcDeviceAlerts = class {
|
|
|
11959
12345
|
}
|
|
11960
12346
|
/** How to re-word a subject's alert from the last reading, or `null`. */
|
|
11961
12347
|
restatementOf(subject) {
|
|
11962
|
-
const
|
|
11963
|
-
|
|
12348
|
+
const parts = parseConsumableSubject(subject);
|
|
12349
|
+
const consumable = parts === null || parts.legacy ? void 0 : this.lastConsumable.get(itemKeyOf(parts.deviceId, parts.key));
|
|
12350
|
+
if (parts !== null && consumable !== void 0) return (facts) => incomingFromConsumableBand({
|
|
11964
12351
|
facts,
|
|
11965
12352
|
deviceId: consumable.deviceId,
|
|
11966
12353
|
band: "low",
|
|
11967
12354
|
item: consumable.item,
|
|
12355
|
+
thresholdPercent: parts.thresholdPercent,
|
|
11968
12356
|
identity: this.deps.lookup(consumable.deviceId)
|
|
11969
12357
|
});
|
|
12358
|
+
const tank = this.lastTank.get(subject);
|
|
12359
|
+
const tankBand = tank === void 0 ? null : deriveTankBand(tank.tank);
|
|
12360
|
+
if (tank !== void 0 && (tankBand === "low" || tankBand === "full")) return (facts) => incomingFromTankBand({
|
|
12361
|
+
facts,
|
|
12362
|
+
deviceId: tank.deviceId,
|
|
12363
|
+
band: tankBand,
|
|
12364
|
+
tank: tank.tank.key,
|
|
12365
|
+
identity: this.deps.lookup(tank.deviceId)
|
|
12366
|
+
});
|
|
11970
12367
|
const battery = this.lastReading.get(subject);
|
|
11971
12368
|
if (battery !== void 0) return (facts) => incomingFromBatteryBand({
|
|
11972
12369
|
facts,
|
|
@@ -12066,11 +12463,9 @@ var NcDeviceAlerts = class {
|
|
|
12066
12463
|
report(subject, verdict, alerting) {
|
|
12067
12464
|
const logger = this.deps.logger;
|
|
12068
12465
|
if (logger === void 0) return;
|
|
12069
|
-
const deviceId = alertSubjectDeviceId(subject);
|
|
12070
|
-
const tags = deviceId !== void 0 ? { deviceId } : {};
|
|
12071
12466
|
if (verdict.kind === "silent" && !alerting) {
|
|
12072
12467
|
logger.debug("device alert cleared without a notification", {
|
|
12073
|
-
|
|
12468
|
+
...tagsOf(subject),
|
|
12074
12469
|
meta: {
|
|
12075
12470
|
subject,
|
|
12076
12471
|
reason: "notifyRecovery is off, or nothing changed"
|
|
@@ -12079,7 +12474,7 @@ var NcDeviceAlerts = class {
|
|
|
12079
12474
|
return;
|
|
12080
12475
|
}
|
|
12081
12476
|
logger.debug("device alert said nothing", {
|
|
12082
|
-
|
|
12477
|
+
...tagsOf(subject),
|
|
12083
12478
|
meta: {
|
|
12084
12479
|
subject,
|
|
12085
12480
|
verdict: verdict.kind
|
|
@@ -12490,6 +12885,21 @@ var en_default = {
|
|
|
12490
12885
|
"system.device-consumable-normal.title": "Consumable replaced",
|
|
12491
12886
|
"system.device-consumable-normal.body": "{{item}} on {{deviceName}} is back to {{percentage}}%",
|
|
12492
12887
|
"system.device-consumable-normal.body.unmeasured": "{{item}} on {{deviceName}} is OK again",
|
|
12888
|
+
"system.device-tank-low.title": "Tank low",
|
|
12889
|
+
"system.device-tank-full.title": "Tank full",
|
|
12890
|
+
"system.device-tank-normal.title": "Tank OK",
|
|
12891
|
+
"system.device-tank-low.body.cleanWater": "The clean water tank on {{deviceName}} is running low",
|
|
12892
|
+
"system.device-tank-full.body.cleanWater": "The clean water tank on {{deviceName}} is full",
|
|
12893
|
+
"system.device-tank-normal.body.cleanWater": "The clean water tank on {{deviceName}} is OK again",
|
|
12894
|
+
"system.device-tank-low.body.dirtyWater": "The dirty water tank on {{deviceName}} is running low",
|
|
12895
|
+
"system.device-tank-full.body.dirtyWater": "The dirty water tank on {{deviceName}} is full",
|
|
12896
|
+
"system.device-tank-normal.body.dirtyWater": "The dirty water tank on {{deviceName}} is OK again",
|
|
12897
|
+
"system.device-tank-low.body.detergent": "The detergent tank on {{deviceName}} is running low",
|
|
12898
|
+
"system.device-tank-full.body.detergent": "The detergent tank on {{deviceName}} is full",
|
|
12899
|
+
"system.device-tank-normal.body.detergent": "The detergent tank on {{deviceName}} is OK again",
|
|
12900
|
+
"system.device-tank-low.body.dustBin": "The dust bin on {{deviceName}} is running low",
|
|
12901
|
+
"system.device-tank-full.body.dustBin": "The dust bin on {{deviceName}} is full",
|
|
12902
|
+
"system.device-tank-normal.body.dustBin": "The dust bin on {{deviceName}} is OK again",
|
|
12493
12903
|
"system.camera-online.title": "Device online",
|
|
12494
12904
|
"system.camera-online.body": "Device {{deviceName}} is online",
|
|
12495
12905
|
"system.camera-offline.title": "Device offline",
|
|
@@ -12779,6 +13189,21 @@ var it_default = {
|
|
|
12779
13189
|
"system.device-consumable-normal.title": "Consumabile sostituito",
|
|
12780
13190
|
"system.device-consumable-normal.body": "{{item}} su {{deviceName}} è tornato al {{percentage}}%",
|
|
12781
13191
|
"system.device-consumable-normal.body.unmeasured": "{{item}} su {{deviceName}} è di nuovo a posto",
|
|
13192
|
+
"system.device-tank-low.title": "Serbatoio quasi vuoto",
|
|
13193
|
+
"system.device-tank-full.title": "Serbatoio pieno",
|
|
13194
|
+
"system.device-tank-normal.title": "Serbatoio a posto",
|
|
13195
|
+
"system.device-tank-low.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è quasi vuoto",
|
|
13196
|
+
"system.device-tank-full.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è pieno",
|
|
13197
|
+
"system.device-tank-normal.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è di nuovo a posto",
|
|
13198
|
+
"system.device-tank-low.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è quasi vuoto",
|
|
13199
|
+
"system.device-tank-full.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è pieno",
|
|
13200
|
+
"system.device-tank-normal.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è di nuovo a posto",
|
|
13201
|
+
"system.device-tank-low.body.detergent": "Il serbatoio del detergente di {{deviceName}} è quasi vuoto",
|
|
13202
|
+
"system.device-tank-full.body.detergent": "Il serbatoio del detergente di {{deviceName}} è pieno",
|
|
13203
|
+
"system.device-tank-normal.body.detergent": "Il serbatoio del detergente di {{deviceName}} è di nuovo a posto",
|
|
13204
|
+
"system.device-tank-low.body.dustBin": "Il contenitore della polvere di {{deviceName}} è quasi vuoto",
|
|
13205
|
+
"system.device-tank-full.body.dustBin": "Il contenitore della polvere di {{deviceName}} è pieno",
|
|
13206
|
+
"system.device-tank-normal.body.dustBin": "Il contenitore della polvere di {{deviceName}} è di nuovo a posto",
|
|
12782
13207
|
"system.camera-online.title": "Dispositivo online",
|
|
12783
13208
|
"system.camera-online.body": "Il dispositivo {{deviceName}} è online",
|
|
12784
13209
|
"system.camera-offline.title": "Dispositivo offline",
|
|
@@ -14162,6 +14587,10 @@ function systemEventKeys(kind) {
|
|
|
14162
14587
|
titleKey: `system.${kind}.title`,
|
|
14163
14588
|
bodyKey: `system.${kind}.body.ignored`
|
|
14164
14589
|
};
|
|
14590
|
+
if (NC_TANK_SYSTEM_EVENT_KINDS.includes(kind)) return {
|
|
14591
|
+
titleKey: `system.${kind}.title`,
|
|
14592
|
+
bodyKey: `system.${kind}.body.cleanWater`
|
|
14593
|
+
};
|
|
14165
14594
|
return {
|
|
14166
14595
|
titleKey: `system.${kind}.title`,
|
|
14167
14596
|
bodyKey: `system.${kind}.body`
|
|
@@ -16162,7 +16591,7 @@ function makeNcActionHandlers(deps) {
|
|
|
16162
16591
|
catalog: [...NC_CONDITION_CATALOG],
|
|
16163
16592
|
taxonomy: NC_TAXONOMY
|
|
16164
16593
|
}),
|
|
16165
|
-
"nc.getTemplateCatalog": async () => ({ vars:
|
|
16594
|
+
"nc.getTemplateCatalog": async () => ({ vars: servedTemplateVars() }),
|
|
16166
16595
|
"nc.previewTemplate": async (input) => {
|
|
16167
16596
|
const texts = deps.previewTexts;
|
|
16168
16597
|
if (texts === void 0) throw new Error("nc.previewTemplate: text catalog not wired");
|
|
@@ -23742,7 +24171,7 @@ var NcRuleStore = class {
|
|
|
23742
24171
|
*/
|
|
23743
24172
|
async load() {
|
|
23744
24173
|
await this.ledger.load();
|
|
23745
|
-
this.loaded = true;
|
|
24174
|
+
if (this.ledger.hasAnswered()) this.loaded = true;
|
|
23746
24175
|
this.logger.debug("notification rules loaded", { meta: { rules: this.ledger.size } });
|
|
23747
24176
|
}
|
|
23748
24177
|
/**
|
|
@@ -28172,7 +28601,6 @@ var ARM_ANYWAY_REJECTION_COPY = {
|
|
|
28172
28601
|
*/
|
|
28173
28602
|
var NC_DEVICE_ALERT_FALLBACK = {
|
|
28174
28603
|
batteryLowPercent: 20,
|
|
28175
|
-
consumableLowPercent: 15,
|
|
28176
28604
|
renotifyMinutes: 720,
|
|
28177
28605
|
notifyRecovery: true
|
|
28178
28606
|
};
|
|
@@ -29427,6 +29855,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29427
29855
|
return (await this.consumeEventAndReport(event)).outcomes.some((outcome) => outcome.matched);
|
|
29428
29856
|
},
|
|
29429
29857
|
rulesVersion: () => this.rulesFingerprint(),
|
|
29858
|
+
consumableThresholds: () => this.rules.isLoaded() ? consumableThresholdCensus(this.rules.list()) : null,
|
|
29430
29859
|
settings: () => this.deviceAlertSettings(),
|
|
29431
29860
|
lookup: (deviceId) => this.deviceDirectory.get(deviceId),
|
|
29432
29861
|
rows: this.alertStore.port(),
|
|
@@ -30535,6 +30964,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30535
30964
|
for (const item of consumables.items) this.deviceAlerts.observeConsumable(consumables.deviceId, item);
|
|
30536
30965
|
return;
|
|
30537
30966
|
}
|
|
30967
|
+
const tanks = event.category === "device.state-changed" ? vacuumTanksOf(event.data) : null;
|
|
30968
|
+
if (tanks !== null) {
|
|
30969
|
+
for (const tank of tanks.tanks) this.deviceAlerts.observeTank(tanks.deviceId, tank);
|
|
30970
|
+
return;
|
|
30971
|
+
}
|
|
30538
30972
|
const lookup = this.deviceDirectory.lookup();
|
|
30539
30973
|
const incoming = incomingFromSystemEvent(event, lookup);
|
|
30540
30974
|
if (incoming === null) {
|
|
@@ -30653,7 +31087,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30653
31087
|
catalog: [...NC_CONDITION_CATALOG],
|
|
30654
31088
|
taxonomy: NC_TAXONOMY
|
|
30655
31089
|
}),
|
|
30656
|
-
getTemplateCatalog: async () => ({ vars:
|
|
31090
|
+
getTemplateCatalog: async () => ({ vars: servedTemplateVars() }),
|
|
30657
31091
|
previewTemplate: async (input) => previewTemplate(this.texts, input),
|
|
30658
31092
|
getHistory: async ({ filter }) => {
|
|
30659
31093
|
const entries = await this.outbox.queryHistory({
|
|
@@ -32456,7 +32890,6 @@ var NotificationCenter = class NotificationCenter {
|
|
|
32456
32890
|
deviceAlertSettings() {
|
|
32457
32891
|
return this.deps.readDeviceAlertSettings?.() ?? {
|
|
32458
32892
|
batteryLowPercent: NC_DEVICE_ALERT_FALLBACK.batteryLowPercent,
|
|
32459
|
-
consumableLowPercent: NC_DEVICE_ALERT_FALLBACK.consumableLowPercent,
|
|
32460
32893
|
renotifyMinutes: NC_DEVICE_ALERT_FALLBACK.renotifyMinutes,
|
|
32461
32894
|
notifyRecovery: NC_DEVICE_ALERT_FALLBACK.notifyRecovery
|
|
32462
32895
|
};
|
|
@@ -56024,19 +56457,6 @@ var DeviceAlertSettingsSchema = object({
|
|
|
56024
56457
|
*/
|
|
56025
56458
|
batteryLowPercent: number().int().min(1).max(99).default(20),
|
|
56026
56459
|
/**
|
|
56027
|
-
* What counts as a nearly-exhausted CONSUMABLE — a filter, a brush, a mop pad.
|
|
56028
|
-
*
|
|
56029
|
-
* Its own number and not the battery's, because the two are different chores
|
|
56030
|
-
* on different clocks: a battery at 20 % is hours from flat, a brush at 20 %
|
|
56031
|
-
* is weeks from worn, and an operator who wants to be told early about one
|
|
56032
|
-
* does not necessarily want to be told early about the other.
|
|
56033
|
-
*
|
|
56034
|
-
* Applies to the MEASURED side only. An item whose provider states a
|
|
56035
|
-
* `replace` verdict is low whatever this says — the provider decided, and a
|
|
56036
|
-
* threshold cannot talk it out of it (`alerts/consumable-band.ts`).
|
|
56037
|
-
*/
|
|
56038
|
-
consumableLowPercent: number().int().min(1).max(99).default(15),
|
|
56039
|
-
/**
|
|
56040
56460
|
* How long a still-true alert waits before it says itself again.
|
|
56041
56461
|
*
|
|
56042
56462
|
* **0 = say it once.** That is the behaviour of every other notification this
|
|
@@ -77740,7 +78160,7 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
77740
78160
|
{
|
|
77741
78161
|
id: "device-alerts",
|
|
77742
78162
|
title: "Battery & offline alerts",
|
|
77743
|
-
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
|
|
78163
|
+
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.",
|
|
77744
78164
|
columns: 2,
|
|
77745
78165
|
fields: [
|
|
77746
78166
|
{
|
|
@@ -77754,17 +78174,6 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
77754
78174
|
default: DEVICE_ALERT_DEFAULTS.batteryLowPercent,
|
|
77755
78175
|
unit: "%"
|
|
77756
78176
|
},
|
|
77757
|
-
{
|
|
77758
|
-
type: "number",
|
|
77759
|
-
key: "consumableLowPercent",
|
|
77760
|
-
label: "Low consumable at",
|
|
77761
|
-
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.",
|
|
77762
|
-
min: 1,
|
|
77763
|
-
max: 99,
|
|
77764
|
-
step: 1,
|
|
77765
|
-
default: DEVICE_ALERT_DEFAULTS.consumableLowPercent,
|
|
77766
|
-
unit: "%"
|
|
77767
|
-
},
|
|
77768
78177
|
{
|
|
77769
78178
|
type: "number",
|
|
77770
78179
|
key: "alertRenotifyMinutes",
|
|
@@ -83924,7 +84333,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
83924
84333
|
},
|
|
83925
84334
|
readDeviceAlertSettings: () => ({
|
|
83926
84335
|
batteryLowPercent: this.deviceAlertSettingsMirror.batteryLowPercent,
|
|
83927
|
-
consumableLowPercent: this.deviceAlertSettingsMirror.consumableLowPercent,
|
|
83928
84336
|
renotifyMinutes: this.deviceAlertSettingsMirror.alertRenotifyMinutes,
|
|
83929
84337
|
notifyRecovery: this.deviceAlertSettingsMirror.alertNotifyRecovery
|
|
83930
84338
|
}),
|