@camstack/addon-provider-dreame 0.2.150 → 0.2.151

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.
Files changed (3) hide show
  1. package/dist/addon.js +302 -143
  2. package/dist/addon.mjs +302 -143
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5476,7 +5476,7 @@ var ZodIssueCode$1 = {
5476
5476
  var ZodFirstPartyTypeKind$1;
5477
5477
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5478
5478
  //#endregion
5479
- //#region ../types/dist/sleep-DJFUfKvA.mjs
5479
+ //#region ../types/dist/sleep-CUp20JkO.mjs
5480
5480
  /**
5481
5481
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5482
5482
  * window to float samples (D455).
@@ -8368,7 +8368,11 @@ var ContributionSectionSchema = object({
8368
8368
  literal(4)
8369
8369
  ]).optional(),
8370
8370
  tab: string().optional(),
8371
- location: _enum(["settings", "top-tab"]).optional(),
8371
+ location: _enum([
8372
+ "settings",
8373
+ "top-tab",
8374
+ "hero"
8375
+ ]).optional(),
8372
8376
  order: number().optional(),
8373
8377
  fields: array(any())
8374
8378
  });
@@ -13315,7 +13319,11 @@ var ConfigSectionWithValuesSchema = object({
13315
13319
  literal(4)
13316
13320
  ]).optional(),
13317
13321
  tab: string().optional(),
13318
- location: _enum(["settings", "top-tab"]).optional(),
13322
+ location: _enum([
13323
+ "settings",
13324
+ "top-tab",
13325
+ "hero"
13326
+ ]).optional(),
13319
13327
  order: number().optional(),
13320
13328
  fields: array(any())
13321
13329
  });
@@ -17195,6 +17203,52 @@ var TierStatusSchema = object({
17195
17203
  countedAt: number().nullable()
17196
17204
  });
17197
17205
  /** Operator/diagnostic view: rows per tier, last runs, refusals and drops since boot. */
17206
+ /**
17207
+ * Why a series is orphan. `device-gone`: its device is absent from the
17208
+ * successful device listings (the departure clock). `undeclared`: its `cap:`
17209
+ * key is declared by no capability any more (a field a cap removed).
17210
+ */
17211
+ var DeviceHistoryOrphanReasonSchema = _enum(["device-gone", "undeclared"]);
17212
+ /** Rows held per stored tier. */
17213
+ var TierRowsSchema = object({
17214
+ raw: number().int(),
17215
+ "5m": number().int(),
17216
+ "1h": number().int(),
17217
+ "1d": number().int()
17218
+ });
17219
+ var OrphanSeriesSchema = object({
17220
+ seriesId: number().int(),
17221
+ deviceId: number().int(),
17222
+ key: string(),
17223
+ scope: string(),
17224
+ /** The metric's display name; `null` when its metric row is gone too. */
17225
+ displayName: string().nullable(),
17226
+ reason: DeviceHistoryOrphanReasonSchema,
17227
+ /** `device-gone`: since when the device has been absent; `null` otherwise. */
17228
+ missingSince: number().nullable(),
17229
+ firstTs: number(),
17230
+ lastTs: number(),
17231
+ rows: TierRowsSchema
17232
+ });
17233
+ var DeviceHistoryOrphanDeleteRefusalSchema = _enum([
17234
+ "not-found",
17235
+ "not-orphan",
17236
+ "failed"
17237
+ ]);
17238
+ var OrphanDeleteEntrySchema = discriminatedUnion("ok", [object({
17239
+ seriesId: number().int(),
17240
+ ok: literal(true),
17241
+ deleted: TierRowsSchema
17242
+ }), object({
17243
+ seriesId: number().int(),
17244
+ ok: literal(false),
17245
+ reason: DeviceHistoryOrphanDeleteRefusalSchema
17246
+ })]);
17247
+ var OrphanListSchema = object({
17248
+ series: array(OrphanSeriesSchema).readonly(),
17249
+ /** More orphans exist than {@link DEVICE_HISTORY_ORPHAN_LIST_MAX}. */
17250
+ truncated: boolean()
17251
+ });
17198
17252
  var DeviceHistoryStatusSchema = object({
17199
17253
  startedAt: number(),
17200
17254
  /** The store is declared, series loaded and the first reconcile has answered. */
@@ -17292,7 +17346,10 @@ method(object({ deviceId: number().int() }), array(SeriesInfoSchema).readonly())
17292
17346
  }), object({ enabled: boolean() }), {
17293
17347
  kind: "mutation",
17294
17348
  auth: "admin"
17295
- }), method(object({}), DeviceHistoryStatusSchema, { auth: "admin" });
17349
+ }), method(object({}), DeviceHistoryStatusSchema, { auth: "admin" }), method(object({}), OrphanListSchema, { auth: "admin" }), method(object({ seriesIds: array(number().int()).min(1).max(50) }), object({ results: array(OrphanDeleteEntrySchema).readonly() }), {
17350
+ kind: "mutation",
17351
+ auth: "admin"
17352
+ });
17296
17353
  var DeviceCaStatusSchema = discriminatedUnion("state", [
17297
17354
  object({ state: literal("absent") }),
17298
17355
  object({
@@ -55189,17 +55246,73 @@ var connectivityCapability = {
55189
55246
  volatileStateFields: ["lastChangedAt"]
55190
55247
  };
55191
55248
  /**
55192
- * Generic device-consumables capability — surfaces a device's
55193
- * maintenance items (vacuum filters/brushes, replaceable cartridges,
55194
- * descaling cycles, …) with their remaining life and an optional
55195
- * "Replaced" reset action. Device-agnostic: any provider that knows its
55196
- * device tracks consumables can register it; the cap declares no
55197
- * vocabulary of its own — the provider names each item verbatim.
55249
+ * Generic device-consumables capability — the ONE place a device reports what
55250
+ * it uses up or fills: wear parts (vacuum filters/brushes, cartridges,
55251
+ * descaling cycles), supplies it draws from (a vacuum's clean-water tank and
55252
+ * detergent, a dishwasher's salt and rinse aid) and waste it collects (a dirty
55253
+ * water tank, a dust bin). Each item carries its remaining life or fill and an
55254
+ * optional "Replaced" reset action. Device-agnostic: any provider that knows
55255
+ * its device tracks consumables registers it; the provider names each item
55256
+ * verbatim and says which `kind` it is.
55257
+ *
55258
+ * The admin UI renders it as ONE collapsible section directly under the
55259
+ * device page's hero card (`placement: 'hero'`), for every device bound to
55260
+ * this cap — never drawn by a device type's hero card itself (D915).
55198
55261
  *
55199
55262
  * Like `childLayout`, the cap is INERT until a provider sets items: no
55200
55263
  * provider populates it by guessing (no HA inference). The UI renders a
55201
55264
  * "No consumables reported" placeholder when `items` is empty.
55202
55265
  */
55266
+ /**
55267
+ * What an item IS, which decides which direction is bad:
55268
+ * - `wear` — a part that wears out (a brush, a filter); low life is bad.
55269
+ * - `supply` — something the device draws from (clean water, detergent,
55270
+ * salt, rinse aid); low or empty is bad.
55271
+ * - `waste` — something the device fills (dirty water, a dust bin); full is
55272
+ * bad.
55273
+ * Absent = `wear`.
55274
+ */
55275
+ var ConsumableKindSchema = _enum([
55276
+ "wear",
55277
+ "supply",
55278
+ "waste"
55279
+ ]);
55280
+ /**
55281
+ * What an item LOOKS like, which decides how a client draws it — independent
55282
+ * of `kind`, which decides which direction is bad:
55283
+ * - `part` — a part that wears (a brush, a filter, a mop pad): a wear bar,
55284
+ * remaining time and a reset.
55285
+ * - `tank` — a container of liquid (clean water, dirty water): a fill gauge.
55286
+ * - `product` — something bought and refilled (detergent, salt, rinse aid):
55287
+ * a stepped level and "refill".
55288
+ * - `bin` — a container that collects waste (a dust bin): a fill gauge
55289
+ * that warns when full.
55290
+ * Absent = derived from the kind ({@link consumableFormOf}).
55291
+ */
55292
+ var ConsumableFormSchema = _enum([
55293
+ "part",
55294
+ "tank",
55295
+ "product",
55296
+ "bin"
55297
+ ]);
55298
+ /** The provider's own discrete verdict on an item. */
55299
+ var ConsumableStatusSchema = _enum([
55300
+ "ok",
55301
+ "low",
55302
+ "empty",
55303
+ "full",
55304
+ "replace"
55305
+ ]);
55306
+ _enum([
55307
+ "ok",
55308
+ "low",
55309
+ "empty",
55310
+ "full"
55311
+ ]);
55312
+ /** The kind of an item, with the documented default for an absent one. */
55313
+ function consumableKindOf(item) {
55314
+ return item.kind ?? "wear";
55315
+ }
55203
55316
  /** Units a remaining-life counter is reported in. Closed: the panel renders
55204
55317
  * each. */
55205
55318
  var ConsumableRemainingUnitSchema = _enum([
@@ -55214,18 +55327,23 @@ var ConsumableRemainingSchema = object({
55214
55327
  unit: ConsumableRemainingUnitSchema
55215
55328
  });
55216
55329
  /** A single consumable item. Either a continuous `level` (remaining
55217
- * life %) or a discrete `status` may be known — both may be null when a
55218
- * provider only knows the item exists. `level` and `status` are not
55219
- * mutually exclusive; a provider may report both. */
55330
+ * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
55331
+ * may be known — both may be null when a provider only knows the item
55332
+ * exists. `level` and `status` are not mutually exclusive; a provider may
55333
+ * report both. */
55220
55334
  var ConsumableItemSchema = object({
55221
55335
  /** Stable id, e.g. 'main-brush'. */
55222
55336
  key: string().min(1),
55223
55337
  /** Display name. */
55224
55338
  label: string().min(1),
55225
- /** Remaining life % when known (0..100). */
55339
+ /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
55226
55340
  level: number().min(0).max(100).nullable(),
55227
- /** Discrete state when known (binary mode). */
55228
- status: _enum(["ok", "replace"]).nullable(),
55341
+ /** The provider's discrete verdict when known (binary mode). */
55342
+ status: ConsumableStatusSchema.nullable(),
55343
+ /** What the item is — decides whether low or full is bad. Absent = `wear`. */
55344
+ kind: ConsumableKindSchema.optional(),
55345
+ /** How the item is drawn (part, tank, product, bin). Absent = derived from `kind`. */
55346
+ form: ConsumableFormSchema.optional(),
55229
55347
  /** Ms epoch of the last replace, when known. */
55230
55348
  lastResetAt: number().nullable(),
55231
55349
  /** Whether `reset()` is meaningful for this item. */
@@ -55248,7 +55366,7 @@ var consumablesCapability = {
55248
55366
  kind: "widget",
55249
55367
  widgetId: "host/consumables-panel",
55250
55368
  tab: "consumables",
55251
- topTab: true,
55369
+ placement: "hero",
55252
55370
  label: "Consumables",
55253
55371
  order: 5
55254
55372
  } },
@@ -55309,9 +55427,37 @@ reset: method(object({
55309
55427
  */
55310
55428
  durability: "restored",
55311
55429
  /**
55312
- * History (D850): none — `items` is an array keyed by runtime ids; a history path never enters it.
55430
+ * History (D850, D917): one series per (item field, item `key`) — the item key
55431
+ * is the series scope, the item `label` names it. A vacuum tank's fill, a
55432
+ * brush's remaining life and a dishwasher's salt are each recorded the moment
55433
+ * their provider reports them; an item that appears later gets its series.
55313
55434
  */
55314
- history: "none",
55435
+ history: {
55436
+ fields: {},
55437
+ items: {
55438
+ path: "items",
55439
+ key: "key",
55440
+ label: "label",
55441
+ fields: {
55442
+ level: {
55443
+ kind: "gauge",
55444
+ unit: "%",
55445
+ deadband: 2,
55446
+ displayName: "Level"
55447
+ },
55448
+ status: {
55449
+ kind: "enum",
55450
+ displayName: "Status"
55451
+ },
55452
+ "remaining.value": {
55453
+ kind: "gauge",
55454
+ unitFrom: "remaining.unit",
55455
+ deadband: 1,
55456
+ displayName: "Remaining"
55457
+ }
55458
+ }
55459
+ }
55460
+ },
55315
55461
  /** Clocks, not content: a re-read that changed no item costs no commit. */
55316
55462
  volatileStateFields: ["lastChangedAt", "lastFetchedAt"]
55317
55463
  };
@@ -55726,8 +55872,9 @@ onStatusChanged: { data: object({
55726
55872
  volatileStateFields: ["lastChangedAt"]
55727
55873
  };
55728
55874
  /**
55729
- * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
55730
- * consumable levels (salt, rinse aid) and common program options.
55875
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates and
55876
+ * common program options. Salt and rinse aid are NOT here: they are items of
55877
+ * the device's `consumables` cap, like every other device's (D915).
55731
55878
  *
55732
55879
  * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
55733
55880
  * by any integration that exposes the same program / option surface.
@@ -55775,11 +55922,6 @@ var DishwasherAvailabilitySchema = _enum([
55775
55922
  "offline",
55776
55923
  "description-missing"
55777
55924
  ]);
55778
- var DishwasherConsumableLevelSchema = _enum([
55779
- "ok",
55780
- "low",
55781
- "empty"
55782
- ]);
55783
55925
  var DishwasherStatusSchema = object({
55784
55926
  availability: DishwasherAvailabilitySchema,
55785
55927
  /** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
@@ -55812,8 +55954,6 @@ var DishwasherStatusSchema = object({
55812
55954
  powerOn: boolean().nullable(),
55813
55955
  /** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
55814
55956
  powerState: string().nullable(),
55815
- saltLevel: DishwasherConsumableLevelSchema.nullable(),
55816
- rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
55817
55957
  /**
55818
55958
  * True while the appliance asks for its filter to be cleaned (smart-filter, filter-system or
55819
55959
  * combined care-and-filter reminder raised); false when every such reminder reads clear; null
@@ -64777,60 +64917,17 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
64777
64917
  auth: "admin",
64778
64918
  access: "delete"
64779
64919
  });
64780
- /**
64781
- * Robot-vacuum cap. Models HA `vacuum.*` entities — anything with a
64782
- * cleaning lifecycle plus a return-to-base / locate surface and an
64783
- * optional fan-speed selector.
64784
- *
64785
- * State follows HA's canonical vacuum lifecycle: `idle` / `cleaning` /
64786
- * `paused` / `returning` / `docked` / `error`. `batteryLevel`
64787
- * (0..100) is nullable — some vacuums don't report a battery
64788
- * percentage. `fanSpeed` is the current speed token (provider-verbatim,
64789
- * e.g. `'standard'` / `'turbo'`) and `availableFanSpeeds` lists the
64790
- * tokens the hardware accepts so the UI renders only supported choices.
64791
- *
64792
- * The `setFanSpeed` method takes the bare `speed` token — the provider
64793
- * validates it against the vacuum's own list. `locate` triggers the
64794
- * find-me chirp; `returnToBase` sends it home.
64795
- *
64796
- * Consumable / waste tanks: `cleanWater` / `dirtyWater` / `detergent` /
64797
- * `dustBin` each carry a nullable `{ level (0..100 %), status ('ok' |
64798
- * 'low' | 'full') }` reading. Native providers (e.g. Dreame, Roborock)
64799
- * SHOULD populate whichever tanks the hardware has — leave a field `null`
64800
- * only when the device has no such tank at all, and use a `TankStatus`
64801
- * with both inner fields `null` when the tank exists but its level is
64802
- * currently unknown. HA `vacuum.*` entities expose no per-tank telemetry,
64803
- * so the HA provider leaves all four `null`.
64804
- */
64805
- var VacuumStateSchema = _enum([
64806
- "idle",
64807
- "cleaning",
64808
- "paused",
64809
- "returning",
64810
- "docked",
64811
- "drying",
64812
- "error"
64813
- ]);
64814
- /**
64815
- * One consumable / waste tank on a robot vacuum (clean-water, dirty-water,
64816
- * detergent or dust-bin). A tank can report a numeric fill `level` (0..100),
64817
- * a discrete `status` (binary-style hardware), or both. Both `null` means the
64818
- * level is currently unknown; the OWNING field being `null` means the
64819
- * hardware has no such tank at all.
64820
- */
64821
- var TankStatusSchema = object({
64822
- /** Numeric fill 0..100 when the hardware reports a percentage; null otherwise. */
64823
- level: number().min(0).max(100).nullable(),
64824
- /** Discrete state when the hardware is binary-mode; null otherwise. */
64825
- status: _enum([
64826
- "ok",
64827
- "low",
64828
- "full"
64829
- ]).nullable()
64830
- });
64831
64920
  var VacuumControlStatusSchema = object({
64832
64921
  /** Lifecycle state of the vacuum. */
64833
- state: VacuumStateSchema,
64922
+ state: _enum([
64923
+ "idle",
64924
+ "cleaning",
64925
+ "paused",
64926
+ "returning",
64927
+ "docked",
64928
+ "drying",
64929
+ "error"
64930
+ ]),
64834
64931
  /** 0..100 battery percentage. Null when the device has no battery
64835
64932
  * reading. */
64836
64933
  batteryLevel: number().min(0).max(100).nullable(),
@@ -64839,14 +64936,6 @@ var VacuumControlStatusSchema = object({
64839
64936
  fanSpeed: string().nullable(),
64840
64937
  /** Speed tokens the hardware accepts — drives the UI selector. */
64841
64938
  availableFanSpeeds: array(string()),
64842
- /** Clean-water (mop) tank. Null when the hardware has no clean-water tank. */
64843
- cleanWater: TankStatusSchema.nullable(),
64844
- /** Dirty-water (recovery) tank. Null when the hardware has no dirty-water tank. */
64845
- dirtyWater: TankStatusSchema.nullable(),
64846
- /** Detergent tank. Null when the hardware has no detergent tank. */
64847
- detergent: TankStatusSchema.nullable(),
64848
- /** Dust bin. Null when the hardware has no dust bin. */
64849
- dustBin: TankStatusSchema.nullable(),
64850
64939
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64851
64940
  progressPercent: number().min(0).max(100).nullable(),
64852
64941
  /** Current error code (0 / null = no error). */
@@ -64908,35 +64997,14 @@ var vacuumControlCapability = {
64908
64997
  */
64909
64998
  durability: "session",
64910
64999
  /**
64911
- * History (D850): recorded series. Tank levels are fixed-object paths (`cleanWater.level`, ...).
65000
+ * History (D850): recorded series. The tanks moved to `consumables` (D915), whose
65001
+ * items are keyed by runtime ids and record no history.
64912
65002
  */
64913
- history: { fields: {
64914
- batteryLevel: {
64915
- kind: "gauge",
64916
- unit: "%",
64917
- deadband: 2
64918
- },
64919
- "cleanWater.level": {
64920
- kind: "gauge",
64921
- unit: "%",
64922
- deadband: 2
64923
- },
64924
- "dirtyWater.level": {
64925
- kind: "gauge",
64926
- unit: "%",
64927
- deadband: 2
64928
- },
64929
- "detergent.level": {
64930
- kind: "gauge",
64931
- unit: "%",
64932
- deadband: 2
64933
- },
64934
- "dustBin.level": {
64935
- kind: "gauge",
64936
- unit: "%",
64937
- deadband: 2
64938
- }
64939
- } }
65003
+ history: { fields: { batteryLevel: {
65004
+ kind: "gauge",
65005
+ unit: "%",
65006
+ deadband: 2
65007
+ } } }
64940
65008
  };
64941
65009
  var ValveStatusSchema = object({
64942
65010
  /** Lifecycle state of the valve. */
@@ -69346,6 +69414,12 @@ Object.freeze({
69346
69414
  addonId: null,
69347
69415
  access: "create"
69348
69416
  },
69417
+ "deviceHistory.deleteOrphanSeries": {
69418
+ capName: "device-history",
69419
+ capScope: "system",
69420
+ addonId: null,
69421
+ access: "delete"
69422
+ },
69349
69423
  "deviceHistory.getDeviceEnabled": {
69350
69424
  capName: "device-history",
69351
69425
  capScope: "system",
@@ -69364,6 +69438,12 @@ Object.freeze({
69364
69438
  addonId: null,
69365
69439
  access: "view"
69366
69440
  },
69441
+ "deviceHistory.listOrphanSeries": {
69442
+ capName: "device-history",
69443
+ capScope: "system",
69444
+ addonId: null,
69445
+ access: "view"
69446
+ },
69367
69447
  "deviceHistory.listSeries": {
69368
69448
  capName: "device-history",
69369
69449
  capScope: "system",
@@ -79369,7 +79449,7 @@ var STATE_SUMMARY_VARS = [
79369
79449
  ([...RULE_TEXT], [...COMBINED_ALARM]),
79370
79450
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
79371
79451
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
79372
- ([...RULE_TEXT], [...CONSUMABLE]),
79452
+ ([...RULE_TEXT], [...CONSUMABLE, ...TANK]),
79373
79453
  ([...RULE_TEXT], [...TANK]),
79374
79454
  [...RULE_TEXT],
79375
79455
  ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
@@ -135239,8 +135319,7 @@ function dirtyWaterTankStatus(value, errorCode) {
135239
135319
  return null;
135240
135320
  }
135241
135321
  /**
135242
- * The four `vacuum-control` tank fields (clean-water / dirty-water / detergent /
135243
- * dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
135322
+ * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
135244
135323
  * discrete `status` populated from the live MIoT enum props when supplied. Pure.
135245
135324
  *
135246
135325
  * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
@@ -135271,6 +135350,57 @@ function decodeVacuumTanks(caps, props, errorCode = null) {
135271
135350
  dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
135272
135351
  };
135273
135352
  }
135353
+ /** How each tank is named and what it is, in the order the panel lists them. */
135354
+ var TANK_ITEMS = [
135355
+ {
135356
+ key: "cleanWater",
135357
+ label: "Clean water tank",
135358
+ kind: "supply",
135359
+ form: "tank"
135360
+ },
135361
+ {
135362
+ key: "dirtyWater",
135363
+ label: "Dirty water tank",
135364
+ kind: "waste",
135365
+ form: "tank"
135366
+ },
135367
+ {
135368
+ key: "detergent",
135369
+ label: "Detergent",
135370
+ kind: "supply",
135371
+ form: "product"
135372
+ },
135373
+ {
135374
+ key: "dustBin",
135375
+ label: "Dust bin",
135376
+ kind: "waste",
135377
+ form: "bin"
135378
+ }
135379
+ ];
135380
+ /**
135381
+ * The tanks as `consumables` items (D915): every PRESENT tank becomes a
135382
+ * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
135383
+ * keyed by its old field name, drawn as a `tank` (the two water tanks), a
135384
+ * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
135385
+ * episodes were built on, so an open episode carries on. A tank is never
135386
+ * resettable: emptying or refilling it is the reset. Pure.
135387
+ */
135388
+ function vacuumTankItems(tanks) {
135389
+ return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
135390
+ const tank = tanks[key];
135391
+ if (tank === null) return [];
135392
+ return [{
135393
+ key,
135394
+ label,
135395
+ level: tank.level,
135396
+ status: tank.status,
135397
+ kind,
135398
+ form,
135399
+ lastResetAt: null,
135400
+ resettable: false
135401
+ }];
135402
+ });
135403
+ }
135274
135404
  /**
135275
135405
  * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
135276
135406
  * value is absent / non-finite. The library reports remaining life as a 0..100
@@ -135299,7 +135429,8 @@ function buildVacuumConsumableItems(readings) {
135299
135429
  level: clampConsumablePct(c.leftPct),
135300
135430
  status: null,
135301
135431
  lastResetAt: null,
135302
- resettable: c.resettable
135432
+ resettable: c.resettable,
135433
+ form: "part"
135303
135434
  }));
135304
135435
  }
135305
135436
  /** Structural equality of two JSON-shaped values, key order ignored. */
@@ -182684,7 +182815,8 @@ function buildMowerConsumables(readings) {
182684
182815
  level: r.remainingPercent,
182685
182816
  status: null,
182686
182817
  lastResetAt: null,
182687
- resettable: true
182818
+ resettable: true,
182819
+ form: "part"
182688
182820
  }));
182689
182821
  }
182690
182822
  //#endregion
@@ -182708,7 +182840,7 @@ var CONSUMABLES_COLD_START = {
182708
182840
  lastFetchedAt: 0
182709
182841
  };
182710
182842
  function itemEqual(a, b) {
182711
- return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable;
182843
+ return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable && a.kind === b.kind && a.remaining?.value === b.remaining?.value && a.remaining?.unit === b.remaining?.unit;
182712
182844
  }
182713
182845
  function itemsEqual(a, b) {
182714
182846
  return a.length === b.length && a.every((item, i) => {
@@ -183698,6 +183830,31 @@ var DreameSwitchDevice = class extends DreameChildDevice {
183698
183830
  };
183699
183831
  //#endregion
183700
183832
  //#region src/devices/dreame-vacuum-consumables.ts
183833
+ /**
183834
+ * The vacuum's `consumables` slice: when it is re-read, and what a reset does.
183835
+ *
183836
+ * The life %s and their time-left twins are settings the robot rarely re-pushes
183837
+ * over MQTT, and the library does not poll them. So they are re-read from the
183838
+ * cloud SHADOW (no robot wake) on activate, every {@link CONSUMABLES_REFRESH_MS},
183839
+ * and on the edge INTO `docked` — the moment a run has just used them up. The
183840
+ * same read reseeds the station tank-status props (dirty water, dust bag, …),
183841
+ * which otherwise arrive only as pushes.
183842
+ *
183843
+ * A reset goes through the library, which re-reads the item it reset (live,
183844
+ * else shadow) before it resolves; the slice is rebuilt from that. A shadow
183845
+ * answer may still be the pre-reset value, so it earns ONE follow-up refresh.
183846
+ *
183847
+ * The TANKS ride the same slice (D915): every push hands over the tank items
183848
+ * the device decoded, appended after the wear items. Either half may be
183849
+ * unknown for a while after a restart — the shadow has not answered, or a
183850
+ * refresh lands before the first push brought the tanks — and the slice is
183851
+ * `restored`, so an unknown half keeps the restored rows instead of erasing
183852
+ * them.
183853
+ */
183854
+ /** A tank item (`supply` / `waste`) as opposed to a wear item. */
183855
+ function isTankItem(item) {
183856
+ return consumableKindOf(item) !== "wear";
183857
+ }
183701
183858
  /** Hourly: a consumable moves by hours of cleaning, and the dock edge covers a run. */
183702
183859
  var CONSUMABLES_REFRESH_MS = 60 * 6e4;
183703
183860
  /** Time for the robot to report a reset to the cloud before the shadow is re-read. */
@@ -183714,6 +183871,8 @@ var DreameVacuumConsumables = class {
183714
183871
  #refresher;
183715
183872
  /** Last pushed lifecycle state; `null` until the first push after boot. */
183716
183873
  #prevState = null;
183874
+ /** Last pushed tank items; `null` until the first push after boot. */
183875
+ #tanks = null;
183717
183876
  constructor(deps) {
183718
183877
  this.#deps = deps;
183719
183878
  this.#refresher = new DreameConsumablesRefresher({
@@ -183733,12 +183892,14 @@ var DreameVacuumConsumables = class {
183733
183892
  this.#refresher.stop();
183734
183893
  }
183735
183894
  /**
183736
- * Every `stateChanged` push: replay the cache into the slice (written only if
183737
- * an item changed) and refresh on the edge into `docked`. The first push after
183738
- * boot is not an edge — activation already reads — and neither is a hop
183739
- * between station routines (`drying` → `docked`): only the end of a RUN is.
183895
+ * Every `stateChanged` push: replay the cache and the pushed tanks into the
183896
+ * slice (written only if an item changed) and refresh on the edge into
183897
+ * `docked`. The first push after boot is not an edge — activation already
183898
+ * reads — and neither is a hop between station routines (`drying` →
183899
+ * `docked`): only the end of a RUN is.
183740
183900
  */
183741
- onPush(state) {
183901
+ onPush(state, tanks) {
183902
+ this.#tanks = tanks;
183742
183903
  this.#apply(false);
183743
183904
  const enteredDock = this.#prevState !== null && DOCK_EDGE_FROM.has(this.#prevState) && state === "docked";
183744
183905
  this.#prevState = state;
@@ -183788,7 +183949,11 @@ var DreameVacuumConsumables = class {
183788
183949
  const { store, resolveVacuum, now } = this.#deps;
183789
183950
  const vacuum = resolveVacuum();
183790
183951
  if (vacuum === null) return;
183791
- writeConsumables(store, buildVacuumConsumableItems(vacuum.supportedConsumables), (now ?? Date.now)(), landed);
183952
+ const prev = store.getCapState("consumables")?.items ?? [];
183953
+ const read = buildVacuumConsumableItems(vacuum.supportedConsumables);
183954
+ const wear = read.length > 0 ? read : prev.filter((item) => !isTankItem(item));
183955
+ const tanks = this.#tanks ?? prev.filter(isTankItem);
183956
+ writeConsumables(store, [...wear, ...tanks], (now ?? Date.now)(), landed);
183792
183957
  }
183793
183958
  };
183794
183959
  function resetMeta(result) {
@@ -183808,10 +183973,6 @@ var CONTROL_COLD_START = {
183808
183973
  batteryLevel: null,
183809
183974
  fanSpeed: null,
183810
183975
  availableFanSpeeds: [],
183811
- cleanWater: null,
183812
- dirtyWater: null,
183813
- detergent: null,
183814
- dustBin: null,
183815
183976
  progressPercent: null,
183816
183977
  errorCode: null,
183817
183978
  errorLabel: null,
@@ -183827,11 +183988,13 @@ var CONTROL_COLD_START = {
183827
183988
  * `setFanSpeed`→`setSuction()` (token validated against the model's supported
183828
183989
  * suction levels).
183829
183990
  *
183830
- * Tank state (cleanWater/dirtyWater/detergent/dustBin): the library exposes NO
183831
- * tank fill LEVEL, so `level` is always null. PRESENCE comes from the model
183832
- * capability flags and a discrete `status` from the station's tank-status enum
183833
- * props (`decodeVacuumTanks`, ids in `VACUUM_TANK_PROPS`); an absent tank is
183834
- * `null`, an unknown status is `null` — never `ok`.
183991
+ * Tanks (clean water / dirty water / detergent / dust bin) are `supply` /
183992
+ * `waste` items of the `consumables` slice (D915), not `vacuum-control` fields:
183993
+ * the library exposes NO tank fill LEVEL, so `level` is always null. PRESENCE
183994
+ * comes from the model capability flags and a discrete `status` from the
183995
+ * station's tank-status enum props (`decodeVacuumTanks`, ids in
183996
+ * `VACUUM_TANK_PROPS`); an absent tank is no item, an unknown status is `null`
183997
+ * — never `ok`.
183835
183998
  *
183836
183999
  * Consumables: whatever the model REPORTS (the library's presence-driven
183837
184000
  * `supportedConsumables`), re-read from the cloud shadow on activate, hourly and
@@ -183955,16 +184118,12 @@ var DreameVacuumDevice = class extends DreameChildDevice {
183955
184118
  recomputeSlice() {
183956
184119
  const vacuum = this.resolveVacuum();
183957
184120
  if (vacuum === null) return;
183958
- const tanks = decodeVacuumTanks(vacuum.vacuumCapabilities, this.readTankProps(), vacuum.errorCode);
184121
+ const tanks = vacuumTankItems(decodeVacuumTanks(vacuum.vacuumCapabilities, this.readTankProps(), vacuum.errorCode));
183959
184122
  const controlSlice = {
183960
184123
  state: mapMiotStateToVacuumState(vacuum.status),
183961
184124
  batteryLevel: clampBatteryLevel$1(vacuum.battery),
183962
184125
  fanSpeed: suctionLevelToken(vacuum.suction),
183963
184126
  availableFanSpeeds: supportedFanSpeedTokens(vacuum.supportedSuctionLevels),
183964
- cleanWater: tanks.cleanWater,
183965
- dirtyWater: tanks.dirtyWater,
183966
- detergent: tanks.detergent,
183967
- dustBin: tanks.dustBin,
183968
184127
  progressPercent: clampPercent$1(vacuum.taskProgressPct),
183969
184128
  errorCode: vacuum.errorCode,
183970
184129
  errorLabel: vacuum.errorCode ? `Error ${vacuum.errorCode}` : null,
@@ -183972,7 +184131,7 @@ var DreameVacuumDevice = class extends DreameChildDevice {
183972
184131
  };
183973
184132
  const toWrite = nextVacuumControlSlice(this.runtimeState.getCapState(CAP_NAME$1), controlSlice);
183974
184133
  if (toWrite !== null) this.runtimeState.setCapState(CAP_NAME$1, toWrite);
183975
- this.consumables.onPush(controlSlice.state);
184134
+ this.consumables.onPush(controlSlice.state, tanks);
183976
184135
  }
183977
184136
  /**
183978
184137
  * Read the tank-status MIoT enum props ({@link VACUUM_TANK_PROPS}) from the
package/dist/addon.mjs CHANGED
@@ -5473,7 +5473,7 @@ var ZodIssueCode$1 = {
5473
5473
  var ZodFirstPartyTypeKind$1;
5474
5474
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5475
5475
  //#endregion
5476
- //#region ../types/dist/sleep-DJFUfKvA.mjs
5476
+ //#region ../types/dist/sleep-CUp20JkO.mjs
5477
5477
  /**
5478
5478
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5479
5479
  * window to float samples (D455).
@@ -8365,7 +8365,11 @@ var ContributionSectionSchema = object({
8365
8365
  literal(4)
8366
8366
  ]).optional(),
8367
8367
  tab: string().optional(),
8368
- location: _enum(["settings", "top-tab"]).optional(),
8368
+ location: _enum([
8369
+ "settings",
8370
+ "top-tab",
8371
+ "hero"
8372
+ ]).optional(),
8369
8373
  order: number().optional(),
8370
8374
  fields: array(any())
8371
8375
  });
@@ -13312,7 +13316,11 @@ var ConfigSectionWithValuesSchema = object({
13312
13316
  literal(4)
13313
13317
  ]).optional(),
13314
13318
  tab: string().optional(),
13315
- location: _enum(["settings", "top-tab"]).optional(),
13319
+ location: _enum([
13320
+ "settings",
13321
+ "top-tab",
13322
+ "hero"
13323
+ ]).optional(),
13316
13324
  order: number().optional(),
13317
13325
  fields: array(any())
13318
13326
  });
@@ -17192,6 +17200,52 @@ var TierStatusSchema = object({
17192
17200
  countedAt: number().nullable()
17193
17201
  });
17194
17202
  /** Operator/diagnostic view: rows per tier, last runs, refusals and drops since boot. */
17203
+ /**
17204
+ * Why a series is orphan. `device-gone`: its device is absent from the
17205
+ * successful device listings (the departure clock). `undeclared`: its `cap:`
17206
+ * key is declared by no capability any more (a field a cap removed).
17207
+ */
17208
+ var DeviceHistoryOrphanReasonSchema = _enum(["device-gone", "undeclared"]);
17209
+ /** Rows held per stored tier. */
17210
+ var TierRowsSchema = object({
17211
+ raw: number().int(),
17212
+ "5m": number().int(),
17213
+ "1h": number().int(),
17214
+ "1d": number().int()
17215
+ });
17216
+ var OrphanSeriesSchema = object({
17217
+ seriesId: number().int(),
17218
+ deviceId: number().int(),
17219
+ key: string(),
17220
+ scope: string(),
17221
+ /** The metric's display name; `null` when its metric row is gone too. */
17222
+ displayName: string().nullable(),
17223
+ reason: DeviceHistoryOrphanReasonSchema,
17224
+ /** `device-gone`: since when the device has been absent; `null` otherwise. */
17225
+ missingSince: number().nullable(),
17226
+ firstTs: number(),
17227
+ lastTs: number(),
17228
+ rows: TierRowsSchema
17229
+ });
17230
+ var DeviceHistoryOrphanDeleteRefusalSchema = _enum([
17231
+ "not-found",
17232
+ "not-orphan",
17233
+ "failed"
17234
+ ]);
17235
+ var OrphanDeleteEntrySchema = discriminatedUnion("ok", [object({
17236
+ seriesId: number().int(),
17237
+ ok: literal(true),
17238
+ deleted: TierRowsSchema
17239
+ }), object({
17240
+ seriesId: number().int(),
17241
+ ok: literal(false),
17242
+ reason: DeviceHistoryOrphanDeleteRefusalSchema
17243
+ })]);
17244
+ var OrphanListSchema = object({
17245
+ series: array(OrphanSeriesSchema).readonly(),
17246
+ /** More orphans exist than {@link DEVICE_HISTORY_ORPHAN_LIST_MAX}. */
17247
+ truncated: boolean()
17248
+ });
17195
17249
  var DeviceHistoryStatusSchema = object({
17196
17250
  startedAt: number(),
17197
17251
  /** The store is declared, series loaded and the first reconcile has answered. */
@@ -17289,7 +17343,10 @@ method(object({ deviceId: number().int() }), array(SeriesInfoSchema).readonly())
17289
17343
  }), object({ enabled: boolean() }), {
17290
17344
  kind: "mutation",
17291
17345
  auth: "admin"
17292
- }), method(object({}), DeviceHistoryStatusSchema, { auth: "admin" });
17346
+ }), method(object({}), DeviceHistoryStatusSchema, { auth: "admin" }), method(object({}), OrphanListSchema, { auth: "admin" }), method(object({ seriesIds: array(number().int()).min(1).max(50) }), object({ results: array(OrphanDeleteEntrySchema).readonly() }), {
17347
+ kind: "mutation",
17348
+ auth: "admin"
17349
+ });
17293
17350
  var DeviceCaStatusSchema = discriminatedUnion("state", [
17294
17351
  object({ state: literal("absent") }),
17295
17352
  object({
@@ -55186,17 +55243,73 @@ var connectivityCapability = {
55186
55243
  volatileStateFields: ["lastChangedAt"]
55187
55244
  };
55188
55245
  /**
55189
- * Generic device-consumables capability — surfaces a device's
55190
- * maintenance items (vacuum filters/brushes, replaceable cartridges,
55191
- * descaling cycles, …) with their remaining life and an optional
55192
- * "Replaced" reset action. Device-agnostic: any provider that knows its
55193
- * device tracks consumables can register it; the cap declares no
55194
- * vocabulary of its own — the provider names each item verbatim.
55246
+ * Generic device-consumables capability — the ONE place a device reports what
55247
+ * it uses up or fills: wear parts (vacuum filters/brushes, cartridges,
55248
+ * descaling cycles), supplies it draws from (a vacuum's clean-water tank and
55249
+ * detergent, a dishwasher's salt and rinse aid) and waste it collects (a dirty
55250
+ * water tank, a dust bin). Each item carries its remaining life or fill and an
55251
+ * optional "Replaced" reset action. Device-agnostic: any provider that knows
55252
+ * its device tracks consumables registers it; the provider names each item
55253
+ * verbatim and says which `kind` it is.
55254
+ *
55255
+ * The admin UI renders it as ONE collapsible section directly under the
55256
+ * device page's hero card (`placement: 'hero'`), for every device bound to
55257
+ * this cap — never drawn by a device type's hero card itself (D915).
55195
55258
  *
55196
55259
  * Like `childLayout`, the cap is INERT until a provider sets items: no
55197
55260
  * provider populates it by guessing (no HA inference). The UI renders a
55198
55261
  * "No consumables reported" placeholder when `items` is empty.
55199
55262
  */
55263
+ /**
55264
+ * What an item IS, which decides which direction is bad:
55265
+ * - `wear` — a part that wears out (a brush, a filter); low life is bad.
55266
+ * - `supply` — something the device draws from (clean water, detergent,
55267
+ * salt, rinse aid); low or empty is bad.
55268
+ * - `waste` — something the device fills (dirty water, a dust bin); full is
55269
+ * bad.
55270
+ * Absent = `wear`.
55271
+ */
55272
+ var ConsumableKindSchema = _enum([
55273
+ "wear",
55274
+ "supply",
55275
+ "waste"
55276
+ ]);
55277
+ /**
55278
+ * What an item LOOKS like, which decides how a client draws it — independent
55279
+ * of `kind`, which decides which direction is bad:
55280
+ * - `part` — a part that wears (a brush, a filter, a mop pad): a wear bar,
55281
+ * remaining time and a reset.
55282
+ * - `tank` — a container of liquid (clean water, dirty water): a fill gauge.
55283
+ * - `product` — something bought and refilled (detergent, salt, rinse aid):
55284
+ * a stepped level and "refill".
55285
+ * - `bin` — a container that collects waste (a dust bin): a fill gauge
55286
+ * that warns when full.
55287
+ * Absent = derived from the kind ({@link consumableFormOf}).
55288
+ */
55289
+ var ConsumableFormSchema = _enum([
55290
+ "part",
55291
+ "tank",
55292
+ "product",
55293
+ "bin"
55294
+ ]);
55295
+ /** The provider's own discrete verdict on an item. */
55296
+ var ConsumableStatusSchema = _enum([
55297
+ "ok",
55298
+ "low",
55299
+ "empty",
55300
+ "full",
55301
+ "replace"
55302
+ ]);
55303
+ _enum([
55304
+ "ok",
55305
+ "low",
55306
+ "empty",
55307
+ "full"
55308
+ ]);
55309
+ /** The kind of an item, with the documented default for an absent one. */
55310
+ function consumableKindOf(item) {
55311
+ return item.kind ?? "wear";
55312
+ }
55200
55313
  /** Units a remaining-life counter is reported in. Closed: the panel renders
55201
55314
  * each. */
55202
55315
  var ConsumableRemainingUnitSchema = _enum([
@@ -55211,18 +55324,23 @@ var ConsumableRemainingSchema = object({
55211
55324
  unit: ConsumableRemainingUnitSchema
55212
55325
  });
55213
55326
  /** A single consumable item. Either a continuous `level` (remaining
55214
- * life %) or a discrete `status` may be known — both may be null when a
55215
- * provider only knows the item exists. `level` and `status` are not
55216
- * mutually exclusive; a provider may report both. */
55327
+ * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
55328
+ * may be known — both may be null when a provider only knows the item
55329
+ * exists. `level` and `status` are not mutually exclusive; a provider may
55330
+ * report both. */
55217
55331
  var ConsumableItemSchema = object({
55218
55332
  /** Stable id, e.g. 'main-brush'. */
55219
55333
  key: string().min(1),
55220
55334
  /** Display name. */
55221
55335
  label: string().min(1),
55222
- /** Remaining life % when known (0..100). */
55336
+ /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
55223
55337
  level: number().min(0).max(100).nullable(),
55224
- /** Discrete state when known (binary mode). */
55225
- status: _enum(["ok", "replace"]).nullable(),
55338
+ /** The provider's discrete verdict when known (binary mode). */
55339
+ status: ConsumableStatusSchema.nullable(),
55340
+ /** What the item is — decides whether low or full is bad. Absent = `wear`. */
55341
+ kind: ConsumableKindSchema.optional(),
55342
+ /** How the item is drawn (part, tank, product, bin). Absent = derived from `kind`. */
55343
+ form: ConsumableFormSchema.optional(),
55226
55344
  /** Ms epoch of the last replace, when known. */
55227
55345
  lastResetAt: number().nullable(),
55228
55346
  /** Whether `reset()` is meaningful for this item. */
@@ -55245,7 +55363,7 @@ var consumablesCapability = {
55245
55363
  kind: "widget",
55246
55364
  widgetId: "host/consumables-panel",
55247
55365
  tab: "consumables",
55248
- topTab: true,
55366
+ placement: "hero",
55249
55367
  label: "Consumables",
55250
55368
  order: 5
55251
55369
  } },
@@ -55306,9 +55424,37 @@ reset: method(object({
55306
55424
  */
55307
55425
  durability: "restored",
55308
55426
  /**
55309
- * History (D850): none — `items` is an array keyed by runtime ids; a history path never enters it.
55427
+ * History (D850, D917): one series per (item field, item `key`) — the item key
55428
+ * is the series scope, the item `label` names it. A vacuum tank's fill, a
55429
+ * brush's remaining life and a dishwasher's salt are each recorded the moment
55430
+ * their provider reports them; an item that appears later gets its series.
55310
55431
  */
55311
- history: "none",
55432
+ history: {
55433
+ fields: {},
55434
+ items: {
55435
+ path: "items",
55436
+ key: "key",
55437
+ label: "label",
55438
+ fields: {
55439
+ level: {
55440
+ kind: "gauge",
55441
+ unit: "%",
55442
+ deadband: 2,
55443
+ displayName: "Level"
55444
+ },
55445
+ status: {
55446
+ kind: "enum",
55447
+ displayName: "Status"
55448
+ },
55449
+ "remaining.value": {
55450
+ kind: "gauge",
55451
+ unitFrom: "remaining.unit",
55452
+ deadband: 1,
55453
+ displayName: "Remaining"
55454
+ }
55455
+ }
55456
+ }
55457
+ },
55312
55458
  /** Clocks, not content: a re-read that changed no item costs no commit. */
55313
55459
  volatileStateFields: ["lastChangedAt", "lastFetchedAt"]
55314
55460
  };
@@ -55723,8 +55869,9 @@ onStatusChanged: { data: object({
55723
55869
  volatileStateFields: ["lastChangedAt"]
55724
55870
  };
55725
55871
  /**
55726
- * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
55727
- * consumable levels (salt, rinse aid) and common program options.
55872
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates and
55873
+ * common program options. Salt and rinse aid are NOT here: they are items of
55874
+ * the device's `consumables` cap, like every other device's (D915).
55728
55875
  *
55729
55876
  * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
55730
55877
  * by any integration that exposes the same program / option surface.
@@ -55772,11 +55919,6 @@ var DishwasherAvailabilitySchema = _enum([
55772
55919
  "offline",
55773
55920
  "description-missing"
55774
55921
  ]);
55775
- var DishwasherConsumableLevelSchema = _enum([
55776
- "ok",
55777
- "low",
55778
- "empty"
55779
- ]);
55780
55922
  var DishwasherStatusSchema = object({
55781
55923
  availability: DishwasherAvailabilitySchema,
55782
55924
  /** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
@@ -55809,8 +55951,6 @@ var DishwasherStatusSchema = object({
55809
55951
  powerOn: boolean().nullable(),
55810
55952
  /** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
55811
55953
  powerState: string().nullable(),
55812
- saltLevel: DishwasherConsumableLevelSchema.nullable(),
55813
- rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
55814
55954
  /**
55815
55955
  * True while the appliance asks for its filter to be cleaned (smart-filter, filter-system or
55816
55956
  * combined care-and-filter reminder raised); false when every such reminder reads clear; null
@@ -64774,60 +64914,17 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
64774
64914
  auth: "admin",
64775
64915
  access: "delete"
64776
64916
  });
64777
- /**
64778
- * Robot-vacuum cap. Models HA `vacuum.*` entities — anything with a
64779
- * cleaning lifecycle plus a return-to-base / locate surface and an
64780
- * optional fan-speed selector.
64781
- *
64782
- * State follows HA's canonical vacuum lifecycle: `idle` / `cleaning` /
64783
- * `paused` / `returning` / `docked` / `error`. `batteryLevel`
64784
- * (0..100) is nullable — some vacuums don't report a battery
64785
- * percentage. `fanSpeed` is the current speed token (provider-verbatim,
64786
- * e.g. `'standard'` / `'turbo'`) and `availableFanSpeeds` lists the
64787
- * tokens the hardware accepts so the UI renders only supported choices.
64788
- *
64789
- * The `setFanSpeed` method takes the bare `speed` token — the provider
64790
- * validates it against the vacuum's own list. `locate` triggers the
64791
- * find-me chirp; `returnToBase` sends it home.
64792
- *
64793
- * Consumable / waste tanks: `cleanWater` / `dirtyWater` / `detergent` /
64794
- * `dustBin` each carry a nullable `{ level (0..100 %), status ('ok' |
64795
- * 'low' | 'full') }` reading. Native providers (e.g. Dreame, Roborock)
64796
- * SHOULD populate whichever tanks the hardware has — leave a field `null`
64797
- * only when the device has no such tank at all, and use a `TankStatus`
64798
- * with both inner fields `null` when the tank exists but its level is
64799
- * currently unknown. HA `vacuum.*` entities expose no per-tank telemetry,
64800
- * so the HA provider leaves all four `null`.
64801
- */
64802
- var VacuumStateSchema = _enum([
64803
- "idle",
64804
- "cleaning",
64805
- "paused",
64806
- "returning",
64807
- "docked",
64808
- "drying",
64809
- "error"
64810
- ]);
64811
- /**
64812
- * One consumable / waste tank on a robot vacuum (clean-water, dirty-water,
64813
- * detergent or dust-bin). A tank can report a numeric fill `level` (0..100),
64814
- * a discrete `status` (binary-style hardware), or both. Both `null` means the
64815
- * level is currently unknown; the OWNING field being `null` means the
64816
- * hardware has no such tank at all.
64817
- */
64818
- var TankStatusSchema = object({
64819
- /** Numeric fill 0..100 when the hardware reports a percentage; null otherwise. */
64820
- level: number().min(0).max(100).nullable(),
64821
- /** Discrete state when the hardware is binary-mode; null otherwise. */
64822
- status: _enum([
64823
- "ok",
64824
- "low",
64825
- "full"
64826
- ]).nullable()
64827
- });
64828
64917
  var VacuumControlStatusSchema = object({
64829
64918
  /** Lifecycle state of the vacuum. */
64830
- state: VacuumStateSchema,
64919
+ state: _enum([
64920
+ "idle",
64921
+ "cleaning",
64922
+ "paused",
64923
+ "returning",
64924
+ "docked",
64925
+ "drying",
64926
+ "error"
64927
+ ]),
64831
64928
  /** 0..100 battery percentage. Null when the device has no battery
64832
64929
  * reading. */
64833
64930
  batteryLevel: number().min(0).max(100).nullable(),
@@ -64836,14 +64933,6 @@ var VacuumControlStatusSchema = object({
64836
64933
  fanSpeed: string().nullable(),
64837
64934
  /** Speed tokens the hardware accepts — drives the UI selector. */
64838
64935
  availableFanSpeeds: array(string()),
64839
- /** Clean-water (mop) tank. Null when the hardware has no clean-water tank. */
64840
- cleanWater: TankStatusSchema.nullable(),
64841
- /** Dirty-water (recovery) tank. Null when the hardware has no dirty-water tank. */
64842
- dirtyWater: TankStatusSchema.nullable(),
64843
- /** Detergent tank. Null when the hardware has no detergent tank. */
64844
- detergent: TankStatusSchema.nullable(),
64845
- /** Dust bin. Null when the hardware has no dust bin. */
64846
- dustBin: TankStatusSchema.nullable(),
64847
64936
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64848
64937
  progressPercent: number().min(0).max(100).nullable(),
64849
64938
  /** Current error code (0 / null = no error). */
@@ -64905,35 +64994,14 @@ var vacuumControlCapability = {
64905
64994
  */
64906
64995
  durability: "session",
64907
64996
  /**
64908
- * History (D850): recorded series. Tank levels are fixed-object paths (`cleanWater.level`, ...).
64997
+ * History (D850): recorded series. The tanks moved to `consumables` (D915), whose
64998
+ * items are keyed by runtime ids and record no history.
64909
64999
  */
64910
- history: { fields: {
64911
- batteryLevel: {
64912
- kind: "gauge",
64913
- unit: "%",
64914
- deadband: 2
64915
- },
64916
- "cleanWater.level": {
64917
- kind: "gauge",
64918
- unit: "%",
64919
- deadband: 2
64920
- },
64921
- "dirtyWater.level": {
64922
- kind: "gauge",
64923
- unit: "%",
64924
- deadband: 2
64925
- },
64926
- "detergent.level": {
64927
- kind: "gauge",
64928
- unit: "%",
64929
- deadband: 2
64930
- },
64931
- "dustBin.level": {
64932
- kind: "gauge",
64933
- unit: "%",
64934
- deadband: 2
64935
- }
64936
- } }
65000
+ history: { fields: { batteryLevel: {
65001
+ kind: "gauge",
65002
+ unit: "%",
65003
+ deadband: 2
65004
+ } } }
64937
65005
  };
64938
65006
  var ValveStatusSchema = object({
64939
65007
  /** Lifecycle state of the valve. */
@@ -69343,6 +69411,12 @@ Object.freeze({
69343
69411
  addonId: null,
69344
69412
  access: "create"
69345
69413
  },
69414
+ "deviceHistory.deleteOrphanSeries": {
69415
+ capName: "device-history",
69416
+ capScope: "system",
69417
+ addonId: null,
69418
+ access: "delete"
69419
+ },
69346
69420
  "deviceHistory.getDeviceEnabled": {
69347
69421
  capName: "device-history",
69348
69422
  capScope: "system",
@@ -69361,6 +69435,12 @@ Object.freeze({
69361
69435
  addonId: null,
69362
69436
  access: "view"
69363
69437
  },
69438
+ "deviceHistory.listOrphanSeries": {
69439
+ capName: "device-history",
69440
+ capScope: "system",
69441
+ addonId: null,
69442
+ access: "view"
69443
+ },
69364
69444
  "deviceHistory.listSeries": {
69365
69445
  capName: "device-history",
69366
69446
  capScope: "system",
@@ -79366,7 +79446,7 @@ var STATE_SUMMARY_VARS = [
79366
79446
  ([...RULE_TEXT], [...COMBINED_ALARM]),
79367
79447
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
79368
79448
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
79369
- ([...RULE_TEXT], [...CONSUMABLE]),
79449
+ ([...RULE_TEXT], [...CONSUMABLE, ...TANK]),
79370
79450
  ([...RULE_TEXT], [...TANK]),
79371
79451
  [...RULE_TEXT],
79372
79452
  ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
@@ -135236,8 +135316,7 @@ function dirtyWaterTankStatus(value, errorCode) {
135236
135316
  return null;
135237
135317
  }
135238
135318
  /**
135239
- * The four `vacuum-control` tank fields (clean-water / dirty-water / detergent /
135240
- * dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
135319
+ * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
135241
135320
  * discrete `status` populated from the live MIoT enum props when supplied. Pure.
135242
135321
  *
135243
135322
  * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
@@ -135268,6 +135347,57 @@ function decodeVacuumTanks(caps, props, errorCode = null) {
135268
135347
  dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
135269
135348
  };
135270
135349
  }
135350
+ /** How each tank is named and what it is, in the order the panel lists them. */
135351
+ var TANK_ITEMS = [
135352
+ {
135353
+ key: "cleanWater",
135354
+ label: "Clean water tank",
135355
+ kind: "supply",
135356
+ form: "tank"
135357
+ },
135358
+ {
135359
+ key: "dirtyWater",
135360
+ label: "Dirty water tank",
135361
+ kind: "waste",
135362
+ form: "tank"
135363
+ },
135364
+ {
135365
+ key: "detergent",
135366
+ label: "Detergent",
135367
+ kind: "supply",
135368
+ form: "product"
135369
+ },
135370
+ {
135371
+ key: "dustBin",
135372
+ label: "Dust bin",
135373
+ kind: "waste",
135374
+ form: "bin"
135375
+ }
135376
+ ];
135377
+ /**
135378
+ * The tanks as `consumables` items (D915): every PRESENT tank becomes a
135379
+ * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
135380
+ * keyed by its old field name, drawn as a `tank` (the two water tanks), a
135381
+ * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
135382
+ * episodes were built on, so an open episode carries on. A tank is never
135383
+ * resettable: emptying or refilling it is the reset. Pure.
135384
+ */
135385
+ function vacuumTankItems(tanks) {
135386
+ return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
135387
+ const tank = tanks[key];
135388
+ if (tank === null) return [];
135389
+ return [{
135390
+ key,
135391
+ label,
135392
+ level: tank.level,
135393
+ status: tank.status,
135394
+ kind,
135395
+ form,
135396
+ lastResetAt: null,
135397
+ resettable: false
135398
+ }];
135399
+ });
135400
+ }
135271
135401
  /**
135272
135402
  * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
135273
135403
  * value is absent / non-finite. The library reports remaining life as a 0..100
@@ -135296,7 +135426,8 @@ function buildVacuumConsumableItems(readings) {
135296
135426
  level: clampConsumablePct(c.leftPct),
135297
135427
  status: null,
135298
135428
  lastResetAt: null,
135299
- resettable: c.resettable
135429
+ resettable: c.resettable,
135430
+ form: "part"
135300
135431
  }));
135301
135432
  }
135302
135433
  /** Structural equality of two JSON-shaped values, key order ignored. */
@@ -182681,7 +182812,8 @@ function buildMowerConsumables(readings) {
182681
182812
  level: r.remainingPercent,
182682
182813
  status: null,
182683
182814
  lastResetAt: null,
182684
- resettable: true
182815
+ resettable: true,
182816
+ form: "part"
182685
182817
  }));
182686
182818
  }
182687
182819
  //#endregion
@@ -182705,7 +182837,7 @@ var CONSUMABLES_COLD_START = {
182705
182837
  lastFetchedAt: 0
182706
182838
  };
182707
182839
  function itemEqual(a, b) {
182708
- return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable;
182840
+ return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable && a.kind === b.kind && a.remaining?.value === b.remaining?.value && a.remaining?.unit === b.remaining?.unit;
182709
182841
  }
182710
182842
  function itemsEqual(a, b) {
182711
182843
  return a.length === b.length && a.every((item, i) => {
@@ -183695,6 +183827,31 @@ var DreameSwitchDevice = class extends DreameChildDevice {
183695
183827
  };
183696
183828
  //#endregion
183697
183829
  //#region src/devices/dreame-vacuum-consumables.ts
183830
+ /**
183831
+ * The vacuum's `consumables` slice: when it is re-read, and what a reset does.
183832
+ *
183833
+ * The life %s and their time-left twins are settings the robot rarely re-pushes
183834
+ * over MQTT, and the library does not poll them. So they are re-read from the
183835
+ * cloud SHADOW (no robot wake) on activate, every {@link CONSUMABLES_REFRESH_MS},
183836
+ * and on the edge INTO `docked` — the moment a run has just used them up. The
183837
+ * same read reseeds the station tank-status props (dirty water, dust bag, …),
183838
+ * which otherwise arrive only as pushes.
183839
+ *
183840
+ * A reset goes through the library, which re-reads the item it reset (live,
183841
+ * else shadow) before it resolves; the slice is rebuilt from that. A shadow
183842
+ * answer may still be the pre-reset value, so it earns ONE follow-up refresh.
183843
+ *
183844
+ * The TANKS ride the same slice (D915): every push hands over the tank items
183845
+ * the device decoded, appended after the wear items. Either half may be
183846
+ * unknown for a while after a restart — the shadow has not answered, or a
183847
+ * refresh lands before the first push brought the tanks — and the slice is
183848
+ * `restored`, so an unknown half keeps the restored rows instead of erasing
183849
+ * them.
183850
+ */
183851
+ /** A tank item (`supply` / `waste`) as opposed to a wear item. */
183852
+ function isTankItem(item) {
183853
+ return consumableKindOf(item) !== "wear";
183854
+ }
183698
183855
  /** Hourly: a consumable moves by hours of cleaning, and the dock edge covers a run. */
183699
183856
  var CONSUMABLES_REFRESH_MS = 60 * 6e4;
183700
183857
  /** Time for the robot to report a reset to the cloud before the shadow is re-read. */
@@ -183711,6 +183868,8 @@ var DreameVacuumConsumables = class {
183711
183868
  #refresher;
183712
183869
  /** Last pushed lifecycle state; `null` until the first push after boot. */
183713
183870
  #prevState = null;
183871
+ /** Last pushed tank items; `null` until the first push after boot. */
183872
+ #tanks = null;
183714
183873
  constructor(deps) {
183715
183874
  this.#deps = deps;
183716
183875
  this.#refresher = new DreameConsumablesRefresher({
@@ -183730,12 +183889,14 @@ var DreameVacuumConsumables = class {
183730
183889
  this.#refresher.stop();
183731
183890
  }
183732
183891
  /**
183733
- * Every `stateChanged` push: replay the cache into the slice (written only if
183734
- * an item changed) and refresh on the edge into `docked`. The first push after
183735
- * boot is not an edge — activation already reads — and neither is a hop
183736
- * between station routines (`drying` → `docked`): only the end of a RUN is.
183892
+ * Every `stateChanged` push: replay the cache and the pushed tanks into the
183893
+ * slice (written only if an item changed) and refresh on the edge into
183894
+ * `docked`. The first push after boot is not an edge — activation already
183895
+ * reads — and neither is a hop between station routines (`drying` →
183896
+ * `docked`): only the end of a RUN is.
183737
183897
  */
183738
- onPush(state) {
183898
+ onPush(state, tanks) {
183899
+ this.#tanks = tanks;
183739
183900
  this.#apply(false);
183740
183901
  const enteredDock = this.#prevState !== null && DOCK_EDGE_FROM.has(this.#prevState) && state === "docked";
183741
183902
  this.#prevState = state;
@@ -183785,7 +183946,11 @@ var DreameVacuumConsumables = class {
183785
183946
  const { store, resolveVacuum, now } = this.#deps;
183786
183947
  const vacuum = resolveVacuum();
183787
183948
  if (vacuum === null) return;
183788
- writeConsumables(store, buildVacuumConsumableItems(vacuum.supportedConsumables), (now ?? Date.now)(), landed);
183949
+ const prev = store.getCapState("consumables")?.items ?? [];
183950
+ const read = buildVacuumConsumableItems(vacuum.supportedConsumables);
183951
+ const wear = read.length > 0 ? read : prev.filter((item) => !isTankItem(item));
183952
+ const tanks = this.#tanks ?? prev.filter(isTankItem);
183953
+ writeConsumables(store, [...wear, ...tanks], (now ?? Date.now)(), landed);
183789
183954
  }
183790
183955
  };
183791
183956
  function resetMeta(result) {
@@ -183805,10 +183970,6 @@ var CONTROL_COLD_START = {
183805
183970
  batteryLevel: null,
183806
183971
  fanSpeed: null,
183807
183972
  availableFanSpeeds: [],
183808
- cleanWater: null,
183809
- dirtyWater: null,
183810
- detergent: null,
183811
- dustBin: null,
183812
183973
  progressPercent: null,
183813
183974
  errorCode: null,
183814
183975
  errorLabel: null,
@@ -183824,11 +183985,13 @@ var CONTROL_COLD_START = {
183824
183985
  * `setFanSpeed`→`setSuction()` (token validated against the model's supported
183825
183986
  * suction levels).
183826
183987
  *
183827
- * Tank state (cleanWater/dirtyWater/detergent/dustBin): the library exposes NO
183828
- * tank fill LEVEL, so `level` is always null. PRESENCE comes from the model
183829
- * capability flags and a discrete `status` from the station's tank-status enum
183830
- * props (`decodeVacuumTanks`, ids in `VACUUM_TANK_PROPS`); an absent tank is
183831
- * `null`, an unknown status is `null` — never `ok`.
183988
+ * Tanks (clean water / dirty water / detergent / dust bin) are `supply` /
183989
+ * `waste` items of the `consumables` slice (D915), not `vacuum-control` fields:
183990
+ * the library exposes NO tank fill LEVEL, so `level` is always null. PRESENCE
183991
+ * comes from the model capability flags and a discrete `status` from the
183992
+ * station's tank-status enum props (`decodeVacuumTanks`, ids in
183993
+ * `VACUUM_TANK_PROPS`); an absent tank is no item, an unknown status is `null`
183994
+ * — never `ok`.
183832
183995
  *
183833
183996
  * Consumables: whatever the model REPORTS (the library's presence-driven
183834
183997
  * `supportedConsumables`), re-read from the cloud shadow on activate, hourly and
@@ -183952,16 +184115,12 @@ var DreameVacuumDevice = class extends DreameChildDevice {
183952
184115
  recomputeSlice() {
183953
184116
  const vacuum = this.resolveVacuum();
183954
184117
  if (vacuum === null) return;
183955
- const tanks = decodeVacuumTanks(vacuum.vacuumCapabilities, this.readTankProps(), vacuum.errorCode);
184118
+ const tanks = vacuumTankItems(decodeVacuumTanks(vacuum.vacuumCapabilities, this.readTankProps(), vacuum.errorCode));
183956
184119
  const controlSlice = {
183957
184120
  state: mapMiotStateToVacuumState(vacuum.status),
183958
184121
  batteryLevel: clampBatteryLevel$1(vacuum.battery),
183959
184122
  fanSpeed: suctionLevelToken(vacuum.suction),
183960
184123
  availableFanSpeeds: supportedFanSpeedTokens(vacuum.supportedSuctionLevels),
183961
- cleanWater: tanks.cleanWater,
183962
- dirtyWater: tanks.dirtyWater,
183963
- detergent: tanks.detergent,
183964
- dustBin: tanks.dustBin,
183965
184124
  progressPercent: clampPercent$1(vacuum.taskProgressPct),
183966
184125
  errorCode: vacuum.errorCode,
183967
184126
  errorLabel: vacuum.errorCode ? `Error ${vacuum.errorCode}` : null,
@@ -183969,7 +184128,7 @@ var DreameVacuumDevice = class extends DreameChildDevice {
183969
184128
  };
183970
184129
  const toWrite = nextVacuumControlSlice(this.runtimeState.getCapState(CAP_NAME$1), controlSlice);
183971
184130
  if (toWrite !== null) this.runtimeState.setCapState(CAP_NAME$1, toWrite);
183972
- this.consumables.onPush(controlSlice.state);
184131
+ this.consumables.onPush(controlSlice.state, tanks);
183973
184132
  }
183974
184133
  /**
183975
184134
  * Read the tank-status MIoT enum props ({@link VACUUM_TANK_PROPS}) from the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.2.150",
3
+ "version": "0.2.151",
4
4
  "description": "Dreame robot-vacuum / lawn-mower device-provider addon for CamStack — wraps the @apocaliss92/nodedreame Dreamehome cloud client",
5
5
  "keywords": [
6
6
  "camstack",