@camstack/addon-provider-dreame 0.1.15 → 0.1.17

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/addon.js CHANGED
@@ -4680,7 +4680,7 @@ function preprocess(fn, schema) {
4680
4680
  });
4681
4681
  }
4682
4682
  //#endregion
4683
- //#region ../types/dist/sleep-B3AOslwX.mjs
4683
+ //#region ../types/dist/sleep-C2M2zF7x.mjs
4684
4684
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4685
4685
  EventCategory["SystemBoot"] = "system.boot";
4686
4686
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -6248,6 +6248,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
6248
6248
  DeviceType["Switch"] = "switch";
6249
6249
  DeviceType["Sensor"] = "sensor";
6250
6250
  DeviceType["Thermostat"] = "thermostat";
6251
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
6252
+ * `climate-control` cap surface with `Thermostat` but renders a
6253
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
6254
+ * independent vertical/horizontal swing). Sources: native Gree, and
6255
+ * reusable by other AC integrations. */
6256
+ DeviceType["Climate"] = "climate";
6251
6257
  DeviceType["Button"] = "button";
6252
6258
  /** Generic stateless event emitter — carries a device's EXACT declared
6253
6259
  * event vocabulary verbatim (no normalization). Installed with the
@@ -9043,7 +9049,7 @@ var climateControlCapability = {
9043
9049
  scope: "device",
9044
9050
  deviceNative: true,
9045
9051
  mode: "singleton",
9046
- deviceTypes: [DeviceType.Thermostat],
9052
+ deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
9047
9053
  methods: {
9048
9054
  setMode: method(object({
9049
9055
  deviceId: number().int().nonnegative(),
@@ -13642,10 +13648,30 @@ var deviceProviderCapability = {
13642
13648
  type: string()
13643
13649
  }))),
13644
13650
  supportsDiscovery: method(object({}), boolean()),
13645
- discoverDevices: method(object({}), array(DiscoveryCandidateSchema), {
13651
+ /**
13652
+ * Run a network scan. `params` carries optional provider-specific scan
13653
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
13654
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
13655
+ * (provider uses its local-network default).
13656
+ */
13657
+ discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
13646
13658
  kind: "mutation",
13647
13659
  auth: "admin"
13648
13660
  }),
13661
+ /**
13662
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
13663
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
13664
+ * `null` when the provider takes no extra scan params — the generic
13665
+ * aggregated scan never renders this; the per-integration scan does.
13666
+ */
13667
+ getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
13668
+ /**
13669
+ * The DeviceType this provider creates via manual add (Camera for
13670
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
13671
+ * provider does not support manual creation. Lets the Add-Device dialog
13672
+ * pick the right type instead of assuming Camera.
13673
+ */
13674
+ getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
13649
13675
  adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
13650
13676
  kind: "mutation",
13651
13677
  auth: "admin"
@@ -13769,9 +13795,23 @@ var BaseDeviceProvider = class extends BaseAddon {
13769
13795
  async supportsDiscovery() {
13770
13796
  return false;
13771
13797
  }
13772
- async discoverDevices() {
13798
+ async discoverDevices(_input) {
13773
13799
  return [];
13774
13800
  }
13801
+ /** Extra per-scan input form (e.g. a broadcast address for another subnet).
13802
+ * Null = no extra params. Override in providers that support scoped scans. */
13803
+ async getDiscoveryParamsSchema() {
13804
+ return null;
13805
+ }
13806
+ /**
13807
+ * The DeviceType this provider creates via manual add — derived from the
13808
+ * `deviceClasses` map (first registered type). `null` when manual creation is
13809
+ * unsupported. Lets the Add-Device dialog pick the right type per provider.
13810
+ */
13811
+ async getManualCreationType() {
13812
+ if (!await this.supportsManualCreation()) return { deviceType: null };
13813
+ return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
13814
+ }
13775
13815
  async adoptDiscoveredDevice(_input) {
13776
13816
  throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
13777
13817
  }
@@ -15633,7 +15673,10 @@ method(object({
15633
15673
  }), FieldProbeResultSchema, {
15634
15674
  kind: "mutation",
15635
15675
  auth: "admin"
15636
- }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15676
+ }), method(object({
15677
+ addonId: string(),
15678
+ integrationId: string()
15679
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15637
15680
  addonId: string(),
15638
15681
  integrationId: string()
15639
15682
  }), AdoptionStatusSchema, {
@@ -15648,7 +15691,24 @@ method(object({
15648
15691
  }), method(ResyncInputSchema, ResyncResultSchema, {
15649
15692
  kind: "mutation",
15650
15693
  auth: "admin"
15694
+ }), method(object({}), object({ providers: array(object({
15695
+ addonId: string(),
15696
+ label: string()
15697
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
15698
+ addonId: string(),
15699
+ label: string(),
15700
+ candidates: array(DiscoveryCandidateSchema).readonly(),
15701
+ error: string().nullable()
15702
+ })).readonly() }), {
15703
+ kind: "mutation",
15704
+ auth: "admin"
15651
15705
  }), method(object({
15706
+ addonId: string(),
15707
+ params: record(string(), unknown()).optional()
15708
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
15709
+ kind: "mutation",
15710
+ auth: "admin"
15711
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
15652
15712
  deviceId: number(),
15653
15713
  key: string(),
15654
15714
  value: unknown()
@@ -20788,6 +20848,12 @@ Object.freeze({
20788
20848
  addonId: null,
20789
20849
  access: "create"
20790
20850
  },
20851
+ "deviceManager.adoptionListCandidateFilters": {
20852
+ capName: "device-manager",
20853
+ capScope: "system",
20854
+ addonId: null,
20855
+ access: "view"
20856
+ },
20791
20857
  "deviceManager.adoptionListCandidates": {
20792
20858
  capName: "device-manager",
20793
20859
  capScope: "system",
@@ -20836,12 +20902,30 @@ Object.freeze({
20836
20902
  addonId: null,
20837
20903
  access: "create"
20838
20904
  },
20905
+ "deviceManager.discoverAllProviders": {
20906
+ capName: "device-manager",
20907
+ capScope: "system",
20908
+ addonId: null,
20909
+ access: "create"
20910
+ },
20839
20911
  "deviceManager.discoverDevices": {
20840
20912
  capName: "device-manager",
20841
20913
  capScope: "system",
20842
20914
  addonId: null,
20843
20915
  access: "create"
20844
20916
  },
20917
+ "deviceManager.discoverProvider": {
20918
+ capName: "device-manager",
20919
+ capScope: "system",
20920
+ addonId: null,
20921
+ access: "create"
20922
+ },
20923
+ "deviceManager.discoveryProviders": {
20924
+ capName: "device-manager",
20925
+ capScope: "system",
20926
+ addonId: null,
20927
+ access: "view"
20928
+ },
20845
20929
  "deviceManager.enable": {
20846
20930
  capName: "device-manager",
20847
20931
  capScope: "system",
@@ -20992,6 +21076,18 @@ Object.freeze({
20992
21076
  addonId: null,
20993
21077
  access: "create"
20994
21078
  },
21079
+ "deviceManager.providerCreationType": {
21080
+ capName: "device-manager",
21081
+ capScope: "system",
21082
+ addonId: null,
21083
+ access: "view"
21084
+ },
21085
+ "deviceManager.providerDiscoveryParamsSchema": {
21086
+ capName: "device-manager",
21087
+ capScope: "system",
21088
+ addonId: null,
21089
+ access: "view"
21090
+ },
20995
21091
  "deviceManager.registerDevice": {
20996
21092
  capName: "device-manager",
20997
21093
  capScope: "system",
@@ -21208,6 +21304,18 @@ Object.freeze({
21208
21304
  addonId: null,
21209
21305
  access: "view"
21210
21306
  },
21307
+ "deviceProvider.getDiscoveryParamsSchema": {
21308
+ capName: "device-provider",
21309
+ capScope: "system",
21310
+ addonId: null,
21311
+ access: "view"
21312
+ },
21313
+ "deviceProvider.getManualCreationType": {
21314
+ capName: "device-provider",
21315
+ capScope: "system",
21316
+ addonId: null,
21317
+ access: "view"
21318
+ },
21211
21319
  "deviceProvider.getStatus": {
21212
21320
  capName: "device-provider",
21213
21321
  capScope: "system",
@@ -75112,6 +75220,34 @@ function enumOptions(e) {
75112
75220
  label: humanizeEnumName(name)
75113
75221
  }));
75114
75222
  }
75223
+ /**
75224
+ * MiotError codes that are BENIGN completion / reminder states, not active
75225
+ * faults. HA does not surface these as errors — a fresh cloud-shadow read of an
75226
+ * idle docked robot legitimately returns `TaskComplete` (68). We relabel them to
75227
+ * the `Clear` label so the Error sensor only shows real faults (HA parity).
75228
+ * Extend this set if other non-fault codes surface as spurious "errors".
75229
+ */
75230
+ var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
75231
+ /** `MiotError` options with the benign completion codes remapped to the `Clear`
75232
+ * label, so the Error sensor reads as no-error for them (matches HA). */
75233
+ function errorEnumOptions() {
75234
+ const clearLabel = humanizeEnumName(MiotError[MiotError.Clear]);
75235
+ return enumOptions(MiotError).map((o) => BENIGN_ERROR_CODES.has(o.value) ? {
75236
+ ...o,
75237
+ label: clearLabel
75238
+ } : o);
75239
+ }
75240
+ /**
75241
+ * Resolve the CURRENT catalog options for an enum-sensor by its MIoT coordinates
75242
+ * (across both the vacuum and mower enum-sensor catalogs). The enum-sensor child
75243
+ * persists its option map at adoption; preferring the live catalog here lets a
75244
+ * label change (e.g. the benign-error remap) take effect for already-adopted
75245
+ * children on the next boot — without a re-adopt. Null when no catalog entry
75246
+ * matches (persisted config is then the only source).
75247
+ */
75248
+ function enumSensorOptionsFor(siid, piid) {
75249
+ return [...DREAME_ENUM_SENSORS, ...DREAME_MOWER_ENUM_SENSORS].find((es) => es.siid === siid && es.piid === piid)?.options ?? null;
75250
+ }
75115
75251
  /** Humanise a Tasshack SNAKE_CASE enum name → spaced Title Case
75116
75252
  * ('BACK_HOME' → 'Back Home'). */
75117
75253
  function humanizeSnake(name) {
@@ -75237,7 +75373,7 @@ var DREAME_ENUM_SENSORS = [
75237
75373
  label: "Error",
75238
75374
  siid: 2,
75239
75375
  piid: 2,
75240
- options: enumOptions(MiotError),
75376
+ options: errorEnumOptions(),
75241
75377
  section: DIAG
75242
75378
  },
75243
75379
  {
@@ -76964,7 +77100,7 @@ var DreameEnumSensorDevice = class extends DreameChildDevice {
76964
77100
  const cfg = dreameEnumSensorSchema.parse(ctx.persistedConfig ?? {});
76965
77101
  this.#siid = cfg.siid;
76966
77102
  this.#piid = cfg.piid;
76967
- this.#options = cfg.options;
77103
+ this.#options = enumSensorOptionsFor(cfg.siid, cfg.piid) ?? cfg.options;
76968
77104
  this.updateSourceInfo({
76969
77105
  id: `${this.dreameDeviceId}:enum:${cfg.siid}-${cfg.piid}`,
76970
77106
  system: "dreame"
@@ -77214,6 +77350,15 @@ var CONSUMABLES_CAP_NAME$1 = "consumables";
77214
77350
  var CONSUMABLES_WAKE_RETRIES = 2;
77215
77351
  /** Spacing between wake-window retries. */
77216
77352
  var CONSUMABLES_RETRY_DELAY_MS = 1500;
77353
+ /**
77354
+ * Cloud-shadow (no-wake) refresh cadence for the mower's battery / charging /
77355
+ * status props. The shadow read (`refreshFromCache` → `getCachedProperties`)
77356
+ * hits the cloud SHADOW, not the device — so it succeeds even while the mower
77357
+ * sleeps at base, UNLIKE the synchronous CMS action above (which 80001s). The
77358
+ * mower never PUSHES battery over MQTT, so without this poll `batteryLevel`
77359
+ * stays null. A gentle interval keeps the level current as it charges.
77360
+ */
77361
+ var SHADOW_POLL_MS = 12e4;
77217
77362
  var COLD_START = {
77218
77363
  activity: "idle",
77219
77364
  batteryLevel: null,
@@ -77253,6 +77398,8 @@ var DreameMowerDevice = class extends DreameChildDevice {
77253
77398
  #prevStatus = null;
77254
77399
  /** Last seen `chargingStatus` value — second awake-and-charging trigger. */
77255
77400
  #prevCharging = null;
77401
+ /** Periodic cloud-shadow refresh timer (battery / charging / status). */
77402
+ #shadowTimer = null;
77256
77403
  constructor(ctx) {
77257
77404
  super(ctx);
77258
77405
  }
@@ -77288,6 +77435,35 @@ var DreameMowerDevice = class extends DreameChildDevice {
77288
77435
  async onActivate() {
77289
77436
  await super.onActivate();
77290
77437
  this.refreshConsumables(CONSUMABLES_WAKE_RETRIES);
77438
+ this.#refreshShadow();
77439
+ this.#shadowTimer = setInterval(() => {
77440
+ this.#refreshShadow();
77441
+ }, SHADOW_POLL_MS);
77442
+ }
77443
+ async removeDevice() {
77444
+ if (this.#shadowTimer) {
77445
+ clearInterval(this.#shadowTimer);
77446
+ this.#shadowTimer = null;
77447
+ }
77448
+ await super.removeDevice();
77449
+ }
77450
+ /**
77451
+ * Seed the cached MIoT props (battery / charging / status …) from the CLOUD
77452
+ * SHADOW — a no-wake read that succeeds even while the mower sleeps at base.
77453
+ * `refreshFromCache` emits `stateChanged`, which drives `recomputeSlice`, so
77454
+ * the battery level lands in the slice. Best-effort; failures log at debug.
77455
+ */
77456
+ async #refreshShadow() {
77457
+ const mower = this.resolveMower();
77458
+ if (mower === null) return;
77459
+ try {
77460
+ await mower.refreshFromCache();
77461
+ } catch (err) {
77462
+ this.ctx.logger.debug("dreame mower: cloud-shadow refresh failed", { meta: {
77463
+ dreameDeviceId: this.dreameDeviceId,
77464
+ error: errMsg(err)
77465
+ } });
77466
+ }
77291
77467
  }
77292
77468
  recomputeSlice() {
77293
77469
  const mower = this.resolveMower();
package/dist/addon.mjs CHANGED
@@ -4680,7 +4680,7 @@ function preprocess(fn, schema) {
4680
4680
  });
4681
4681
  }
4682
4682
  //#endregion
4683
- //#region ../types/dist/sleep-B3AOslwX.mjs
4683
+ //#region ../types/dist/sleep-C2M2zF7x.mjs
4684
4684
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4685
4685
  EventCategory["SystemBoot"] = "system.boot";
4686
4686
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -6248,6 +6248,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
6248
6248
  DeviceType["Switch"] = "switch";
6249
6249
  DeviceType["Sensor"] = "sensor";
6250
6250
  DeviceType["Thermostat"] = "thermostat";
6251
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
6252
+ * `climate-control` cap surface with `Thermostat` but renders a
6253
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
6254
+ * independent vertical/horizontal swing). Sources: native Gree, and
6255
+ * reusable by other AC integrations. */
6256
+ DeviceType["Climate"] = "climate";
6251
6257
  DeviceType["Button"] = "button";
6252
6258
  /** Generic stateless event emitter — carries a device's EXACT declared
6253
6259
  * event vocabulary verbatim (no normalization). Installed with the
@@ -9043,7 +9049,7 @@ var climateControlCapability = {
9043
9049
  scope: "device",
9044
9050
  deviceNative: true,
9045
9051
  mode: "singleton",
9046
- deviceTypes: [DeviceType.Thermostat],
9052
+ deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
9047
9053
  methods: {
9048
9054
  setMode: method(object({
9049
9055
  deviceId: number().int().nonnegative(),
@@ -13642,10 +13648,30 @@ var deviceProviderCapability = {
13642
13648
  type: string()
13643
13649
  }))),
13644
13650
  supportsDiscovery: method(object({}), boolean()),
13645
- discoverDevices: method(object({}), array(DiscoveryCandidateSchema), {
13651
+ /**
13652
+ * Run a network scan. `params` carries optional provider-specific scan
13653
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
13654
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
13655
+ * (provider uses its local-network default).
13656
+ */
13657
+ discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
13646
13658
  kind: "mutation",
13647
13659
  auth: "admin"
13648
13660
  }),
13661
+ /**
13662
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
13663
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
13664
+ * `null` when the provider takes no extra scan params — the generic
13665
+ * aggregated scan never renders this; the per-integration scan does.
13666
+ */
13667
+ getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
13668
+ /**
13669
+ * The DeviceType this provider creates via manual add (Camera for
13670
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
13671
+ * provider does not support manual creation. Lets the Add-Device dialog
13672
+ * pick the right type instead of assuming Camera.
13673
+ */
13674
+ getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
13649
13675
  adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
13650
13676
  kind: "mutation",
13651
13677
  auth: "admin"
@@ -13769,9 +13795,23 @@ var BaseDeviceProvider = class extends BaseAddon {
13769
13795
  async supportsDiscovery() {
13770
13796
  return false;
13771
13797
  }
13772
- async discoverDevices() {
13798
+ async discoverDevices(_input) {
13773
13799
  return [];
13774
13800
  }
13801
+ /** Extra per-scan input form (e.g. a broadcast address for another subnet).
13802
+ * Null = no extra params. Override in providers that support scoped scans. */
13803
+ async getDiscoveryParamsSchema() {
13804
+ return null;
13805
+ }
13806
+ /**
13807
+ * The DeviceType this provider creates via manual add — derived from the
13808
+ * `deviceClasses` map (first registered type). `null` when manual creation is
13809
+ * unsupported. Lets the Add-Device dialog pick the right type per provider.
13810
+ */
13811
+ async getManualCreationType() {
13812
+ if (!await this.supportsManualCreation()) return { deviceType: null };
13813
+ return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
13814
+ }
13775
13815
  async adoptDiscoveredDevice(_input) {
13776
13816
  throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
13777
13817
  }
@@ -15633,7 +15673,10 @@ method(object({
15633
15673
  }), FieldProbeResultSchema, {
15634
15674
  kind: "mutation",
15635
15675
  auth: "admin"
15636
- }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15676
+ }), method(object({
15677
+ addonId: string(),
15678
+ integrationId: string()
15679
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15637
15680
  addonId: string(),
15638
15681
  integrationId: string()
15639
15682
  }), AdoptionStatusSchema, {
@@ -15648,7 +15691,24 @@ method(object({
15648
15691
  }), method(ResyncInputSchema, ResyncResultSchema, {
15649
15692
  kind: "mutation",
15650
15693
  auth: "admin"
15694
+ }), method(object({}), object({ providers: array(object({
15695
+ addonId: string(),
15696
+ label: string()
15697
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
15698
+ addonId: string(),
15699
+ label: string(),
15700
+ candidates: array(DiscoveryCandidateSchema).readonly(),
15701
+ error: string().nullable()
15702
+ })).readonly() }), {
15703
+ kind: "mutation",
15704
+ auth: "admin"
15651
15705
  }), method(object({
15706
+ addonId: string(),
15707
+ params: record(string(), unknown()).optional()
15708
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
15709
+ kind: "mutation",
15710
+ auth: "admin"
15711
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
15652
15712
  deviceId: number(),
15653
15713
  key: string(),
15654
15714
  value: unknown()
@@ -20788,6 +20848,12 @@ Object.freeze({
20788
20848
  addonId: null,
20789
20849
  access: "create"
20790
20850
  },
20851
+ "deviceManager.adoptionListCandidateFilters": {
20852
+ capName: "device-manager",
20853
+ capScope: "system",
20854
+ addonId: null,
20855
+ access: "view"
20856
+ },
20791
20857
  "deviceManager.adoptionListCandidates": {
20792
20858
  capName: "device-manager",
20793
20859
  capScope: "system",
@@ -20836,12 +20902,30 @@ Object.freeze({
20836
20902
  addonId: null,
20837
20903
  access: "create"
20838
20904
  },
20905
+ "deviceManager.discoverAllProviders": {
20906
+ capName: "device-manager",
20907
+ capScope: "system",
20908
+ addonId: null,
20909
+ access: "create"
20910
+ },
20839
20911
  "deviceManager.discoverDevices": {
20840
20912
  capName: "device-manager",
20841
20913
  capScope: "system",
20842
20914
  addonId: null,
20843
20915
  access: "create"
20844
20916
  },
20917
+ "deviceManager.discoverProvider": {
20918
+ capName: "device-manager",
20919
+ capScope: "system",
20920
+ addonId: null,
20921
+ access: "create"
20922
+ },
20923
+ "deviceManager.discoveryProviders": {
20924
+ capName: "device-manager",
20925
+ capScope: "system",
20926
+ addonId: null,
20927
+ access: "view"
20928
+ },
20845
20929
  "deviceManager.enable": {
20846
20930
  capName: "device-manager",
20847
20931
  capScope: "system",
@@ -20992,6 +21076,18 @@ Object.freeze({
20992
21076
  addonId: null,
20993
21077
  access: "create"
20994
21078
  },
21079
+ "deviceManager.providerCreationType": {
21080
+ capName: "device-manager",
21081
+ capScope: "system",
21082
+ addonId: null,
21083
+ access: "view"
21084
+ },
21085
+ "deviceManager.providerDiscoveryParamsSchema": {
21086
+ capName: "device-manager",
21087
+ capScope: "system",
21088
+ addonId: null,
21089
+ access: "view"
21090
+ },
20995
21091
  "deviceManager.registerDevice": {
20996
21092
  capName: "device-manager",
20997
21093
  capScope: "system",
@@ -21208,6 +21304,18 @@ Object.freeze({
21208
21304
  addonId: null,
21209
21305
  access: "view"
21210
21306
  },
21307
+ "deviceProvider.getDiscoveryParamsSchema": {
21308
+ capName: "device-provider",
21309
+ capScope: "system",
21310
+ addonId: null,
21311
+ access: "view"
21312
+ },
21313
+ "deviceProvider.getManualCreationType": {
21314
+ capName: "device-provider",
21315
+ capScope: "system",
21316
+ addonId: null,
21317
+ access: "view"
21318
+ },
21211
21319
  "deviceProvider.getStatus": {
21212
21320
  capName: "device-provider",
21213
21321
  capScope: "system",
@@ -75112,6 +75220,34 @@ function enumOptions(e) {
75112
75220
  label: humanizeEnumName(name)
75113
75221
  }));
75114
75222
  }
75223
+ /**
75224
+ * MiotError codes that are BENIGN completion / reminder states, not active
75225
+ * faults. HA does not surface these as errors — a fresh cloud-shadow read of an
75226
+ * idle docked robot legitimately returns `TaskComplete` (68). We relabel them to
75227
+ * the `Clear` label so the Error sensor only shows real faults (HA parity).
75228
+ * Extend this set if other non-fault codes surface as spurious "errors".
75229
+ */
75230
+ var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
75231
+ /** `MiotError` options with the benign completion codes remapped to the `Clear`
75232
+ * label, so the Error sensor reads as no-error for them (matches HA). */
75233
+ function errorEnumOptions() {
75234
+ const clearLabel = humanizeEnumName(MiotError[MiotError.Clear]);
75235
+ return enumOptions(MiotError).map((o) => BENIGN_ERROR_CODES.has(o.value) ? {
75236
+ ...o,
75237
+ label: clearLabel
75238
+ } : o);
75239
+ }
75240
+ /**
75241
+ * Resolve the CURRENT catalog options for an enum-sensor by its MIoT coordinates
75242
+ * (across both the vacuum and mower enum-sensor catalogs). The enum-sensor child
75243
+ * persists its option map at adoption; preferring the live catalog here lets a
75244
+ * label change (e.g. the benign-error remap) take effect for already-adopted
75245
+ * children on the next boot — without a re-adopt. Null when no catalog entry
75246
+ * matches (persisted config is then the only source).
75247
+ */
75248
+ function enumSensorOptionsFor(siid, piid) {
75249
+ return [...DREAME_ENUM_SENSORS, ...DREAME_MOWER_ENUM_SENSORS].find((es) => es.siid === siid && es.piid === piid)?.options ?? null;
75250
+ }
75115
75251
  /** Humanise a Tasshack SNAKE_CASE enum name → spaced Title Case
75116
75252
  * ('BACK_HOME' → 'Back Home'). */
75117
75253
  function humanizeSnake(name) {
@@ -75237,7 +75373,7 @@ var DREAME_ENUM_SENSORS = [
75237
75373
  label: "Error",
75238
75374
  siid: 2,
75239
75375
  piid: 2,
75240
- options: enumOptions(MiotError),
75376
+ options: errorEnumOptions(),
75241
75377
  section: DIAG
75242
75378
  },
75243
75379
  {
@@ -76964,7 +77100,7 @@ var DreameEnumSensorDevice = class extends DreameChildDevice {
76964
77100
  const cfg = dreameEnumSensorSchema.parse(ctx.persistedConfig ?? {});
76965
77101
  this.#siid = cfg.siid;
76966
77102
  this.#piid = cfg.piid;
76967
- this.#options = cfg.options;
77103
+ this.#options = enumSensorOptionsFor(cfg.siid, cfg.piid) ?? cfg.options;
76968
77104
  this.updateSourceInfo({
76969
77105
  id: `${this.dreameDeviceId}:enum:${cfg.siid}-${cfg.piid}`,
76970
77106
  system: "dreame"
@@ -77214,6 +77350,15 @@ var CONSUMABLES_CAP_NAME$1 = "consumables";
77214
77350
  var CONSUMABLES_WAKE_RETRIES = 2;
77215
77351
  /** Spacing between wake-window retries. */
77216
77352
  var CONSUMABLES_RETRY_DELAY_MS = 1500;
77353
+ /**
77354
+ * Cloud-shadow (no-wake) refresh cadence for the mower's battery / charging /
77355
+ * status props. The shadow read (`refreshFromCache` → `getCachedProperties`)
77356
+ * hits the cloud SHADOW, not the device — so it succeeds even while the mower
77357
+ * sleeps at base, UNLIKE the synchronous CMS action above (which 80001s). The
77358
+ * mower never PUSHES battery over MQTT, so without this poll `batteryLevel`
77359
+ * stays null. A gentle interval keeps the level current as it charges.
77360
+ */
77361
+ var SHADOW_POLL_MS = 12e4;
77217
77362
  var COLD_START = {
77218
77363
  activity: "idle",
77219
77364
  batteryLevel: null,
@@ -77253,6 +77398,8 @@ var DreameMowerDevice = class extends DreameChildDevice {
77253
77398
  #prevStatus = null;
77254
77399
  /** Last seen `chargingStatus` value — second awake-and-charging trigger. */
77255
77400
  #prevCharging = null;
77401
+ /** Periodic cloud-shadow refresh timer (battery / charging / status). */
77402
+ #shadowTimer = null;
77256
77403
  constructor(ctx) {
77257
77404
  super(ctx);
77258
77405
  }
@@ -77288,6 +77435,35 @@ var DreameMowerDevice = class extends DreameChildDevice {
77288
77435
  async onActivate() {
77289
77436
  await super.onActivate();
77290
77437
  this.refreshConsumables(CONSUMABLES_WAKE_RETRIES);
77438
+ this.#refreshShadow();
77439
+ this.#shadowTimer = setInterval(() => {
77440
+ this.#refreshShadow();
77441
+ }, SHADOW_POLL_MS);
77442
+ }
77443
+ async removeDevice() {
77444
+ if (this.#shadowTimer) {
77445
+ clearInterval(this.#shadowTimer);
77446
+ this.#shadowTimer = null;
77447
+ }
77448
+ await super.removeDevice();
77449
+ }
77450
+ /**
77451
+ * Seed the cached MIoT props (battery / charging / status …) from the CLOUD
77452
+ * SHADOW — a no-wake read that succeeds even while the mower sleeps at base.
77453
+ * `refreshFromCache` emits `stateChanged`, which drives `recomputeSlice`, so
77454
+ * the battery level lands in the slice. Best-effort; failures log at debug.
77455
+ */
77456
+ async #refreshShadow() {
77457
+ const mower = this.resolveMower();
77458
+ if (mower === null) return;
77459
+ try {
77460
+ await mower.refreshFromCache();
77461
+ } catch (err) {
77462
+ this.ctx.logger.debug("dreame mower: cloud-shadow refresh failed", { meta: {
77463
+ dreameDeviceId: this.dreameDeviceId,
77464
+ error: errMsg(err)
77465
+ } });
77466
+ }
77291
77467
  }
77292
77468
  recomputeSlice() {
77293
77469
  const mower = this.resolveMower();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",