@camstack/addon-provider-gree 0.1.5 → 0.1.7

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 +235 -42
  2. package/dist/addon.mjs +235 -42
  3. package/package.json +2 -2
package/dist/addon.js CHANGED
@@ -10080,15 +10080,18 @@ var humiditySensorCapability = {
10080
10080
  runtimeState: HumiditySensorStatusSchema
10081
10081
  };
10082
10082
  /**
10083
- * Image display cap. Models HA `image.*` entities a single still image
10084
- * exposed by an integration (a snapshot, a chart, a generated picture).
10083
+ * Image display cap. Models a single still image exposed by an integration
10084
+ * a snapshot, a chart, a generated picture, or a robot's cleaning-map render.
10085
10085
  *
10086
- * Read-only: there are no setters. The provider resolves the HA
10087
- * `entity_picture` (a relative, signed-token path) into an ABSOLUTE URL
10088
- * the browser loads directly the token stays in the query string so no
10089
- * auth header is required. The slice carries that URL plus the upstream
10090
- * last-updated timestamp; the image changes when the entity state (a
10091
- * timestamp) changes.
10086
+ * Read-only: there are no setters. The provider resolves whatever upstream
10087
+ * source it has into an ABSOLUTE URL the browser loads directly:
10088
+ * - HA `image.*` entities the `entity_picture` signed-token path
10089
+ * (token stays in the query string, so no auth header is needed);
10090
+ * - a Dreame/robot map the cloud/OSS map-image URL (or an addon
10091
+ * data-plane URL serving the rendered map bytes), exposed as its own
10092
+ * Image child device grouped under the robot's container.
10093
+ * The slice carries that URL plus the upstream last-updated timestamp; the
10094
+ * image changes when the source's last-updated marker changes.
10092
10095
  */
10093
10096
  var ImageStatusSchema = object({
10094
10097
  /** Absolute signed URL the browser loads directly. Null when the
@@ -10114,18 +10117,47 @@ var imageCapability = {
10114
10117
  */
10115
10118
  runtimeState: ImageStatusSchema
10116
10119
  };
10120
+ /**
10121
+ * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
10122
+ * with a mowing lifecycle plus a dock action.
10123
+ *
10124
+ * Activity follows HA's canonical lawn-mower lifecycle: `idle` /
10125
+ * `mowing` / `paused` / `docked` / `error`. `batteryLevel` (0..100) is
10126
+ * nullable — some mowers don't report a battery percentage.
10127
+ *
10128
+ * `startMowing` begins a mowing run, `pause` halts it in place, and
10129
+ * `dock` sends the mower back to its charging station.
10130
+ */
10131
+ var LawnMowerActivitySchema = _enum([
10132
+ "idle",
10133
+ "mowing",
10134
+ "paused",
10135
+ "docked",
10136
+ "error"
10137
+ ]);
10138
+ /** Severity of the current device/error code — info (status), warning, error. */
10139
+ var DeviceCodeSeveritySchema = _enum([
10140
+ "info",
10141
+ "warning",
10142
+ "error"
10143
+ ]);
10117
10144
  var LawnMowerControlStatusSchema = object({
10118
10145
  /** Lifecycle activity of the mower. */
10119
- activity: _enum([
10120
- "idle",
10121
- "mowing",
10122
- "paused",
10123
- "docked",
10124
- "error"
10125
- ]),
10146
+ activity: LawnMowerActivitySchema,
10126
10147
  /** 0..100 battery percentage. Null when the device has no battery
10127
10148
  * reading. */
10128
10149
  batteryLevel: number().min(0).max(100).nullable(),
10150
+ /** 0..100 mowing-completion percentage of the current task, or null when no
10151
+ * task is active / progress is unavailable. */
10152
+ progressPercent: number().min(0).max(100).nullable(),
10153
+ /** Current device/event code (dynamic — mostly status, sometimes an error),
10154
+ * or null when unknown. */
10155
+ currentCode: number().nullable(),
10156
+ /** Human label for {@link currentCode}, or null when undecodable. */
10157
+ currentCodeLabel: string().nullable(),
10158
+ /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
10159
+ * attention; `info` is normal status. */
10160
+ severity: DeviceCodeSeveritySchema,
10129
10161
  /** Ms epoch when the slice was last updated. */
10130
10162
  lastChangedAt: number()
10131
10163
  });
@@ -12351,6 +12383,7 @@ var VacuumStateSchema = _enum([
12351
12383
  "paused",
12352
12384
  "returning",
12353
12385
  "docked",
12386
+ "drying",
12354
12387
  "error"
12355
12388
  ]);
12356
12389
  /**
@@ -12389,6 +12422,12 @@ var VacuumControlStatusSchema = object({
12389
12422
  detergent: TankStatusSchema.nullable(),
12390
12423
  /** Dust bin. Null when the hardware has no dust bin. */
12391
12424
  dustBin: TankStatusSchema.nullable(),
12425
+ /** 0..100 cleaning-completion percentage of the current task, or null. */
12426
+ progressPercent: number().min(0).max(100).nullable(),
12427
+ /** Current error code (0 / null = no error). */
12428
+ errorCode: number().nullable(),
12429
+ /** Human label for {@link errorCode}, or null when none / undecodable. */
12430
+ errorLabel: string().nullable(),
12392
12431
  /** Ms epoch when the slice was last updated. */
12393
12432
  lastChangedAt: number()
12394
12433
  });
@@ -13355,9 +13394,29 @@ var BaseDevice$1 = class {
13355
13394
  * is open (Reolink writes `hasPtz/hasIntercom`, Hikvision writes
13356
13395
  * `hasSupplementalLight/hasAlarmIo`, etc).
13357
13396
  *
13358
- * Default: no-op (driver had no probe to run).
13397
+ * Default: nothing to probe mark the device PROBED (set `lastProbedAt`) so
13398
+ * the kernel treats it as ready immediately. A device that derives its shape
13399
+ * from a spec (a container, or an accessory sensor) rather than from a
13400
+ * hardware probe has no probe to "complete"; without stamping `lastProbedAt`
13401
+ * it would look perpetually un-probed — logging "Initial probe did not
13402
+ * complete" on every boot and spinning a pointless retry chain. Drivers that
13403
+ * DO probe override this and write their own `feature-probe` slice (including
13404
+ * `lastProbedAt`) once their probe actually succeeds.
13359
13405
  */
13360
- async onProbe() {}
13406
+ async onProbe() {
13407
+ const base = this.runtimeState.getCapState("feature-probe") ?? {
13408
+ flags: {},
13409
+ deviceType: null,
13410
+ model: null,
13411
+ channelCount: null,
13412
+ lastProbedAt: 0,
13413
+ lastFetchedAt: 0
13414
+ };
13415
+ this.runtimeState.setCapState("feature-probe", {
13416
+ ...base,
13417
+ lastProbedAt: Date.now()
13418
+ });
13419
+ }
13361
13420
  /**
13362
13421
  * Phase 5 — fired after the device + its accessories are registered.
13363
13422
  * Drivers publish streams to the broker, kick off background tasks,
@@ -14819,17 +14878,32 @@ var ReleaseInputSchema = object({
14819
14878
  * the parent cascades into every accessory. */
14820
14879
  camDeviceId: number().int().nonnegative()
14821
14880
  });
14822
- var ResyncInputSchema = object({
14823
- /** Parent CamStack device id of an adopted device. The provider resolves its
14824
- * source (integration/broker + native id) and re-aligns the device's
14825
- * structural spec (type/role/capabilities/units) with the live mapping,
14826
- * rebuilding any child whose class changed while preserving operator edits. */
14827
- camDeviceId: number().int().nonnegative() });
14881
+ var ResyncInputSchema = object({
14882
+ /** Parent CamStack device id of an adopted device. The provider resolves its
14883
+ * source (integration/broker + native id) and re-aligns the device's
14884
+ * structural spec (type/role/capabilities/units) with the live mapping,
14885
+ * rebuilding any child whose class changed while preserving operator edits. */
14886
+ camDeviceId: number().int().nonnegative(),
14887
+ /** "Resync from zero" (#19). When true, the kernel PURGES every accessory
14888
+ * child of `camDeviceId` BEFORE the provider re-derives the device, so the
14889
+ * children are rebuilt fresh from source — correct names, coords, and units —
14890
+ * instead of being preserved by the incremental reconcile. Use to recover from
14891
+ * legacy generic/placeholder names that the normal name-precedence keeps frozen
14892
+ * (the operator's explicit reset). Push-driven integrations (no-op resync)
14893
+ * rebuild on their next snapshot; pull/command integrations rebuild in `resync`.
14894
+ * Operator edits on the PARENT (its name, layout, primary-child pick) survive —
14895
+ * only the children are torn down. Omitted/false ⇒ the normal incremental
14896
+ * re-sync that preserves children. */
14897
+ resetToSource: boolean().optional()
14898
+ });
14828
14899
  var ResyncResultSchema = object({
14829
14900
  /** True when the persisted spec actually changed (children may have been rebuilt). */
14830
14901
  changed: boolean(),
14831
14902
  /** Number of child devices rebuilt into a new class by this re-sync. */
14832
- rebuiltChildren: number().int().nonnegative()
14903
+ rebuiltChildren: number().int().nonnegative(),
14904
+ /** Number of accessory children torn down by a `resetToSource` purge before the
14905
+ * provider re-derived the device. 0/absent for a normal incremental re-sync. */
14906
+ removedChildren: number().int().nonnegative().optional()
14833
14907
  });
14834
14908
  var deviceAdoptionCapability = {
14835
14909
  name: "device-adoption",
@@ -16594,6 +16668,11 @@ var DeviceMetaSchema = object({
16594
16668
  addonId: string(),
16595
16669
  type: string(),
16596
16670
  name: string(),
16671
+ /** True once an operator explicitly renamed the device via `setName`. Drives
16672
+ * reconcile name-precedence (preserve operator name vs adopt fresh provider
16673
+ * name). Absent ⇒ treated as user-named (PRESERVE) for legacy rows. See
16674
+ * `DeviceMeta.userNamed`. */
16675
+ userNamed: boolean().optional(),
16597
16676
  location: string().nullable(),
16598
16677
  disabled: boolean(),
16599
16678
  parentDeviceId: number().nullable(),
@@ -16904,6 +16983,9 @@ method(object({
16904
16983
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16905
16984
  kind: "mutation",
16906
16985
  auth: "admin"
16986
+ }), method(ResyncInputSchema, ResyncResultSchema, {
16987
+ kind: "mutation",
16988
+ auth: "admin"
16907
16989
  }), method(object({
16908
16990
  deviceId: number(),
16909
16991
  key: string(),
@@ -20666,6 +20748,12 @@ Object.freeze({
20666
20748
  addonId: null,
20667
20749
  access: "create"
20668
20750
  },
20751
+ "deviceManager.adoptionResync": {
20752
+ capName: "device-manager",
20753
+ capScope: "system",
20754
+ addonId: null,
20755
+ access: "create"
20756
+ },
20669
20757
  "deviceManager.allocateDeviceId": {
20670
20758
  capName: "device-manager",
20671
20759
  capScope: "system",
@@ -25537,6 +25625,90 @@ function swingToOscillating(vertical, horizontal) {
25537
25625
  function oscillatingToVerticalSwing(oscillating) {
25538
25626
  return oscillating ? VerticalSwing.FullSwing : VerticalSwing.Default;
25539
25627
  }
25628
+ /**
25629
+ * Gree fan-speed names surfaced on the `climate-control` cap's `fanMode` /
25630
+ * `availableFanModes` strings, mirroring the HA Gree integration's `fan_modes`
25631
+ * attribute (`auto`/`low`/`medium_low`/`medium`/`medium_high`/`high`).
25632
+ *
25633
+ * NOTE: HA's Gree integration also folds `turbo` and `quiet` INTO `fan_modes`.
25634
+ * We deliberately keep turbo/quiet on the dedicated `preset` surface instead
25635
+ * (they are independent boolean device flags in the protocol — `Tur` / `Quiet`
25636
+ * — not points on the `WdSpd` speed axis), so the speed picker stays a pure
25637
+ * speed axis and the two flags stay independently togglable. The `fan-control`
25638
+ * percentage surface continues to mirror the same `WdSpd` speed for the
25639
+ * fan-style UI.
25640
+ */
25641
+ var GREE_FAN_MODES = [
25642
+ "auto",
25643
+ "low",
25644
+ "medium_low",
25645
+ "medium",
25646
+ "medium_high",
25647
+ "high"
25648
+ ];
25649
+ /** Map a library {@link FanSpeed} to its `climate-control` `fanMode` string. Pure. */
25650
+ function fanSpeedToFanMode(speed) {
25651
+ switch (speed) {
25652
+ case FanSpeed.Auto: return "auto";
25653
+ case FanSpeed.Low: return "low";
25654
+ case FanSpeed.MediumLow: return "medium_low";
25655
+ case FanSpeed.Medium: return "medium";
25656
+ case FanSpeed.MediumHigh: return "medium_high";
25657
+ case FanSpeed.High: return "high";
25658
+ }
25659
+ }
25660
+ /**
25661
+ * Map a `climate-control` `fanMode` string back to a library {@link FanSpeed},
25662
+ * or null when the string is not a known Gree speed. Pure.
25663
+ */
25664
+ function fanModeToFanSpeed(fanMode) {
25665
+ switch (fanMode) {
25666
+ case "auto": return FanSpeed.Auto;
25667
+ case "low": return FanSpeed.Low;
25668
+ case "medium_low": return FanSpeed.MediumLow;
25669
+ case "medium": return FanSpeed.Medium;
25670
+ case "medium_high": return FanSpeed.MediumHigh;
25671
+ case "high": return FanSpeed.High;
25672
+ default: return null;
25673
+ }
25674
+ }
25675
+ var GREE_PRESETS = [
25676
+ "none",
25677
+ "turbo",
25678
+ "quiet",
25679
+ "sleep",
25680
+ "eco",
25681
+ "8c_heat"
25682
+ ];
25683
+ /**
25684
+ * Derive the single active `preset` string from the device's comfort flags.
25685
+ * Precedence is fixed (turbo > quiet > sleep > eco > 8c_heat) so a coherent
25686
+ * single value is reported even if the device has more than one flag set.
25687
+ * Returns `'none'` when no flag is set. Pure.
25688
+ */
25689
+ function presetFromFlags(flags) {
25690
+ if (flags.turbo) return "turbo";
25691
+ if (flags.quiet) return "quiet";
25692
+ if (flags.sleep) return "sleep";
25693
+ if (flags.powerSave) return "eco";
25694
+ if (flags.steadyHeat) return "8c_heat";
25695
+ return "none";
25696
+ }
25697
+ /**
25698
+ * Resolve a requested `preset` string to the set of flag writes needed to make
25699
+ * it the active one — the chosen flag (if any) true, every other flag false.
25700
+ * Returns null when the string is not a known Gree preset. Pure.
25701
+ */
25702
+ function presetToFlagWrites(preset) {
25703
+ if (!GREE_PRESETS.includes(preset)) return null;
25704
+ return {
25705
+ turbo: preset === "turbo",
25706
+ quiet: preset === "quiet",
25707
+ sleep: preset === "sleep",
25708
+ powerSave: preset === "eco",
25709
+ steadyHeat: preset === "8c_heat"
25710
+ };
25711
+ }
25540
25712
  //#endregion
25541
25713
  //#region src/devices/gree-ac-device.ts
25542
25714
  var CLIMATE_CAP = "climate-control";
@@ -25545,9 +25717,9 @@ var CLIMATE_COLD_START = {
25545
25717
  mode: "off",
25546
25718
  availableModes: [...ADVERTISED_CAP_MODES],
25547
25719
  fanMode: "",
25548
- availableFanModes: [],
25720
+ availableFanModes: [...GREE_FAN_MODES],
25549
25721
  preset: "",
25550
- availablePresets: [],
25722
+ availablePresets: [...GREE_PRESETS],
25551
25723
  target: null,
25552
25724
  targetHigh: null,
25553
25725
  targetLow: null,
@@ -25583,17 +25755,21 @@ var greeAcSchema = object({
25583
25755
  * `stateChanged` push (and seeds on activate).
25584
25756
  *
25585
25757
  * Climate commands route to the bound handle: setMode `off`→`setPower(false)`,
25586
- * any other→`setPower(true)`+`setMode()`; setTarget→`setTargetTemperature()`.
25587
- * Fan commands: setPercentage→`setFanSpeed()` (percentageFanSpeed bucket);
25758
+ * any other→`setPower(true)`+`setMode()`; setTarget→`setTargetTemperature()`;
25759
+ * setFanMode→`setFanSpeed()` (climate `fanMode` string Gree speed); setPreset→
25760
+ * the comfort-flag setters (turbo/quiet/sleep/eco/8c_heat → `setTurbo`/`setQuiet`/
25761
+ * `setSleep`/`setPowerSave`/`setSteadyHeat`, exclusive). Fan commands:
25762
+ * setPercentage→`setFanSpeed()` (percentage↔FanSpeed bucket);
25588
25763
  * setOscillating→`setSwingVertical(FullSwing|Default)`.
25589
25764
  *
25590
- * TODO (deferred): the cap's `heat_cool` dual-setpoint, `setTargetHumidity`,
25591
- * fan-mode strings, and presets (turbo/quiet/sleep/powerSave/steadyHeat which the
25592
- * library DOES expose) are not yet mapped fan speed already occupies the
25593
- * `fan-control` percentage surface, and the cap preset/fan-mode surfaces are left
25594
- * empty. The fine-grained 12-position vertical / 7-position horizontal swing is
25595
- * collapsed to a single `oscillating` boolean. A later phase could add a vendor
25596
- * preset / swing-position cap.
25765
+ * TODO (deferred — needs protocol RE / live confirmation): `heat_cool`
25766
+ * dual-setpoint (Gree single-setpoint ACs have none), `setTargetHumidity`
25767
+ * (`Dwet`/`DwatSen` dehumidifier props exist in PROPS but the library exposes no
25768
+ * getter/setter yet), the light toggle (`setLight`), X-Fan / fresh-air / health
25769
+ * toggles (`setXfan`/`setFreshAir`/`setHealth`) all real library setters with
25770
+ * no cap surface (a vendor toggle cap would carry them), and the fine-grained
25771
+ * 12-position vertical / 7-position horizontal swing (collapsed to a single
25772
+ * `oscillating` boolean — a vendor swing-position cap would carry them).
25597
25773
  */
25598
25774
  var GreeAcDevice = class extends BaseDevice$1 {
25599
25775
  features = [];
@@ -25671,10 +25847,19 @@ var GreeAcDevice = class extends BaseDevice$1 {
25671
25847
  await ac.setMode(libMode);
25672
25848
  },
25673
25849
  setFanMode: async ({ fanMode }) => {
25674
- throw new Error(`gree ac: fan-mode strings not supported (use fan-control); got "${fanMode}"`);
25850
+ const speed = fanModeToFanSpeed(fanMode);
25851
+ if (speed === null) throw new Error(`gree ac: unsupported fan mode "${fanMode}"`);
25852
+ await this.requireAc().setFanSpeed(speed);
25675
25853
  },
25676
25854
  setPreset: async ({ preset }) => {
25677
- throw new Error(`gree ac: presets not yet supported (got "${preset}")`);
25855
+ const writes = presetToFlagWrites(preset);
25856
+ if (writes === null) throw new Error(`gree ac: unsupported preset "${preset}"`);
25857
+ const ac = this.requireAc();
25858
+ if (ac.turbo !== writes.turbo) await ac.setTurbo(writes.turbo);
25859
+ if (ac.quiet !== writes.quiet) await ac.setQuiet(writes.quiet);
25860
+ if (ac.sleep !== writes.sleep) await ac.setSleep(writes.sleep);
25861
+ if (ac.powerSave !== writes.powerSave) await ac.setPowerSave(writes.powerSave);
25862
+ if (ac.steadyHeat !== writes.steadyHeat) await ac.setSteadyHeat(writes.steadyHeat);
25678
25863
  },
25679
25864
  setTarget: async ({ target }) => {
25680
25865
  await this.requireAc().setTargetTemperature(target);
@@ -25708,13 +25893,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
25708
25893
  const ac = this.resolveAc();
25709
25894
  if (ac === null) return;
25710
25895
  const now = Date.now();
25896
+ const mode = ac.power ? libModeToCapMode(ac.mode) : "off";
25897
+ const preset = presetFromFlags({
25898
+ turbo: ac.turbo,
25899
+ quiet: ac.quiet,
25900
+ sleep: ac.sleep,
25901
+ powerSave: ac.powerSave,
25902
+ steadyHeat: ac.steadyHeat
25903
+ });
25711
25904
  const climateSlice = {
25712
- mode: ac.power ? libModeToCapMode(ac.mode) : "off",
25905
+ mode,
25713
25906
  availableModes: [...ADVERTISED_CAP_MODES],
25714
- fanMode: "",
25715
- availableFanModes: [],
25716
- preset: "",
25717
- availablePresets: [],
25907
+ fanMode: fanSpeedToFanMode(ac.fanSpeed),
25908
+ availableFanModes: [...GREE_FAN_MODES],
25909
+ preset,
25910
+ availablePresets: [...GREE_PRESETS],
25718
25911
  target: ac.targetTemperature,
25719
25912
  targetHigh: null,
25720
25913
  targetLow: null,
package/dist/addon.mjs CHANGED
@@ -10079,15 +10079,18 @@ var humiditySensorCapability = {
10079
10079
  runtimeState: HumiditySensorStatusSchema
10080
10080
  };
10081
10081
  /**
10082
- * Image display cap. Models HA `image.*` entities a single still image
10083
- * exposed by an integration (a snapshot, a chart, a generated picture).
10082
+ * Image display cap. Models a single still image exposed by an integration
10083
+ * a snapshot, a chart, a generated picture, or a robot's cleaning-map render.
10084
10084
  *
10085
- * Read-only: there are no setters. The provider resolves the HA
10086
- * `entity_picture` (a relative, signed-token path) into an ABSOLUTE URL
10087
- * the browser loads directly the token stays in the query string so no
10088
- * auth header is required. The slice carries that URL plus the upstream
10089
- * last-updated timestamp; the image changes when the entity state (a
10090
- * timestamp) changes.
10085
+ * Read-only: there are no setters. The provider resolves whatever upstream
10086
+ * source it has into an ABSOLUTE URL the browser loads directly:
10087
+ * - HA `image.*` entities the `entity_picture` signed-token path
10088
+ * (token stays in the query string, so no auth header is needed);
10089
+ * - a Dreame/robot map the cloud/OSS map-image URL (or an addon
10090
+ * data-plane URL serving the rendered map bytes), exposed as its own
10091
+ * Image child device grouped under the robot's container.
10092
+ * The slice carries that URL plus the upstream last-updated timestamp; the
10093
+ * image changes when the source's last-updated marker changes.
10091
10094
  */
10092
10095
  var ImageStatusSchema = object({
10093
10096
  /** Absolute signed URL the browser loads directly. Null when the
@@ -10113,18 +10116,47 @@ var imageCapability = {
10113
10116
  */
10114
10117
  runtimeState: ImageStatusSchema
10115
10118
  };
10119
+ /**
10120
+ * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
10121
+ * with a mowing lifecycle plus a dock action.
10122
+ *
10123
+ * Activity follows HA's canonical lawn-mower lifecycle: `idle` /
10124
+ * `mowing` / `paused` / `docked` / `error`. `batteryLevel` (0..100) is
10125
+ * nullable — some mowers don't report a battery percentage.
10126
+ *
10127
+ * `startMowing` begins a mowing run, `pause` halts it in place, and
10128
+ * `dock` sends the mower back to its charging station.
10129
+ */
10130
+ var LawnMowerActivitySchema = _enum([
10131
+ "idle",
10132
+ "mowing",
10133
+ "paused",
10134
+ "docked",
10135
+ "error"
10136
+ ]);
10137
+ /** Severity of the current device/error code — info (status), warning, error. */
10138
+ var DeviceCodeSeveritySchema = _enum([
10139
+ "info",
10140
+ "warning",
10141
+ "error"
10142
+ ]);
10116
10143
  var LawnMowerControlStatusSchema = object({
10117
10144
  /** Lifecycle activity of the mower. */
10118
- activity: _enum([
10119
- "idle",
10120
- "mowing",
10121
- "paused",
10122
- "docked",
10123
- "error"
10124
- ]),
10145
+ activity: LawnMowerActivitySchema,
10125
10146
  /** 0..100 battery percentage. Null when the device has no battery
10126
10147
  * reading. */
10127
10148
  batteryLevel: number().min(0).max(100).nullable(),
10149
+ /** 0..100 mowing-completion percentage of the current task, or null when no
10150
+ * task is active / progress is unavailable. */
10151
+ progressPercent: number().min(0).max(100).nullable(),
10152
+ /** Current device/event code (dynamic — mostly status, sometimes an error),
10153
+ * or null when unknown. */
10154
+ currentCode: number().nullable(),
10155
+ /** Human label for {@link currentCode}, or null when undecodable. */
10156
+ currentCodeLabel: string().nullable(),
10157
+ /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
10158
+ * attention; `info` is normal status. */
10159
+ severity: DeviceCodeSeveritySchema,
10128
10160
  /** Ms epoch when the slice was last updated. */
10129
10161
  lastChangedAt: number()
10130
10162
  });
@@ -12350,6 +12382,7 @@ var VacuumStateSchema = _enum([
12350
12382
  "paused",
12351
12383
  "returning",
12352
12384
  "docked",
12385
+ "drying",
12353
12386
  "error"
12354
12387
  ]);
12355
12388
  /**
@@ -12388,6 +12421,12 @@ var VacuumControlStatusSchema = object({
12388
12421
  detergent: TankStatusSchema.nullable(),
12389
12422
  /** Dust bin. Null when the hardware has no dust bin. */
12390
12423
  dustBin: TankStatusSchema.nullable(),
12424
+ /** 0..100 cleaning-completion percentage of the current task, or null. */
12425
+ progressPercent: number().min(0).max(100).nullable(),
12426
+ /** Current error code (0 / null = no error). */
12427
+ errorCode: number().nullable(),
12428
+ /** Human label for {@link errorCode}, or null when none / undecodable. */
12429
+ errorLabel: string().nullable(),
12391
12430
  /** Ms epoch when the slice was last updated. */
12392
12431
  lastChangedAt: number()
12393
12432
  });
@@ -13354,9 +13393,29 @@ var BaseDevice$1 = class {
13354
13393
  * is open (Reolink writes `hasPtz/hasIntercom`, Hikvision writes
13355
13394
  * `hasSupplementalLight/hasAlarmIo`, etc).
13356
13395
  *
13357
- * Default: no-op (driver had no probe to run).
13396
+ * Default: nothing to probe mark the device PROBED (set `lastProbedAt`) so
13397
+ * the kernel treats it as ready immediately. A device that derives its shape
13398
+ * from a spec (a container, or an accessory sensor) rather than from a
13399
+ * hardware probe has no probe to "complete"; without stamping `lastProbedAt`
13400
+ * it would look perpetually un-probed — logging "Initial probe did not
13401
+ * complete" on every boot and spinning a pointless retry chain. Drivers that
13402
+ * DO probe override this and write their own `feature-probe` slice (including
13403
+ * `lastProbedAt`) once their probe actually succeeds.
13358
13404
  */
13359
- async onProbe() {}
13405
+ async onProbe() {
13406
+ const base = this.runtimeState.getCapState("feature-probe") ?? {
13407
+ flags: {},
13408
+ deviceType: null,
13409
+ model: null,
13410
+ channelCount: null,
13411
+ lastProbedAt: 0,
13412
+ lastFetchedAt: 0
13413
+ };
13414
+ this.runtimeState.setCapState("feature-probe", {
13415
+ ...base,
13416
+ lastProbedAt: Date.now()
13417
+ });
13418
+ }
13360
13419
  /**
13361
13420
  * Phase 5 — fired after the device + its accessories are registered.
13362
13421
  * Drivers publish streams to the broker, kick off background tasks,
@@ -14818,17 +14877,32 @@ var ReleaseInputSchema = object({
14818
14877
  * the parent cascades into every accessory. */
14819
14878
  camDeviceId: number().int().nonnegative()
14820
14879
  });
14821
- var ResyncInputSchema = object({
14822
- /** Parent CamStack device id of an adopted device. The provider resolves its
14823
- * source (integration/broker + native id) and re-aligns the device's
14824
- * structural spec (type/role/capabilities/units) with the live mapping,
14825
- * rebuilding any child whose class changed while preserving operator edits. */
14826
- camDeviceId: number().int().nonnegative() });
14880
+ var ResyncInputSchema = object({
14881
+ /** Parent CamStack device id of an adopted device. The provider resolves its
14882
+ * source (integration/broker + native id) and re-aligns the device's
14883
+ * structural spec (type/role/capabilities/units) with the live mapping,
14884
+ * rebuilding any child whose class changed while preserving operator edits. */
14885
+ camDeviceId: number().int().nonnegative(),
14886
+ /** "Resync from zero" (#19). When true, the kernel PURGES every accessory
14887
+ * child of `camDeviceId` BEFORE the provider re-derives the device, so the
14888
+ * children are rebuilt fresh from source — correct names, coords, and units —
14889
+ * instead of being preserved by the incremental reconcile. Use to recover from
14890
+ * legacy generic/placeholder names that the normal name-precedence keeps frozen
14891
+ * (the operator's explicit reset). Push-driven integrations (no-op resync)
14892
+ * rebuild on their next snapshot; pull/command integrations rebuild in `resync`.
14893
+ * Operator edits on the PARENT (its name, layout, primary-child pick) survive —
14894
+ * only the children are torn down. Omitted/false ⇒ the normal incremental
14895
+ * re-sync that preserves children. */
14896
+ resetToSource: boolean().optional()
14897
+ });
14827
14898
  var ResyncResultSchema = object({
14828
14899
  /** True when the persisted spec actually changed (children may have been rebuilt). */
14829
14900
  changed: boolean(),
14830
14901
  /** Number of child devices rebuilt into a new class by this re-sync. */
14831
- rebuiltChildren: number().int().nonnegative()
14902
+ rebuiltChildren: number().int().nonnegative(),
14903
+ /** Number of accessory children torn down by a `resetToSource` purge before the
14904
+ * provider re-derived the device. 0/absent for a normal incremental re-sync. */
14905
+ removedChildren: number().int().nonnegative().optional()
14832
14906
  });
14833
14907
  var deviceAdoptionCapability = {
14834
14908
  name: "device-adoption",
@@ -16593,6 +16667,11 @@ var DeviceMetaSchema = object({
16593
16667
  addonId: string(),
16594
16668
  type: string(),
16595
16669
  name: string(),
16670
+ /** True once an operator explicitly renamed the device via `setName`. Drives
16671
+ * reconcile name-precedence (preserve operator name vs adopt fresh provider
16672
+ * name). Absent ⇒ treated as user-named (PRESERVE) for legacy rows. See
16673
+ * `DeviceMeta.userNamed`. */
16674
+ userNamed: boolean().optional(),
16596
16675
  location: string().nullable(),
16597
16676
  disabled: boolean(),
16598
16677
  parentDeviceId: number().nullable(),
@@ -16903,6 +16982,9 @@ method(object({
16903
16982
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16904
16983
  kind: "mutation",
16905
16984
  auth: "admin"
16985
+ }), method(ResyncInputSchema, ResyncResultSchema, {
16986
+ kind: "mutation",
16987
+ auth: "admin"
16906
16988
  }), method(object({
16907
16989
  deviceId: number(),
16908
16990
  key: string(),
@@ -20665,6 +20747,12 @@ Object.freeze({
20665
20747
  addonId: null,
20666
20748
  access: "create"
20667
20749
  },
20750
+ "deviceManager.adoptionResync": {
20751
+ capName: "device-manager",
20752
+ capScope: "system",
20753
+ addonId: null,
20754
+ access: "create"
20755
+ },
20668
20756
  "deviceManager.allocateDeviceId": {
20669
20757
  capName: "device-manager",
20670
20758
  capScope: "system",
@@ -25536,6 +25624,90 @@ function swingToOscillating(vertical, horizontal) {
25536
25624
  function oscillatingToVerticalSwing(oscillating) {
25537
25625
  return oscillating ? VerticalSwing.FullSwing : VerticalSwing.Default;
25538
25626
  }
25627
+ /**
25628
+ * Gree fan-speed names surfaced on the `climate-control` cap's `fanMode` /
25629
+ * `availableFanModes` strings, mirroring the HA Gree integration's `fan_modes`
25630
+ * attribute (`auto`/`low`/`medium_low`/`medium`/`medium_high`/`high`).
25631
+ *
25632
+ * NOTE: HA's Gree integration also folds `turbo` and `quiet` INTO `fan_modes`.
25633
+ * We deliberately keep turbo/quiet on the dedicated `preset` surface instead
25634
+ * (they are independent boolean device flags in the protocol — `Tur` / `Quiet`
25635
+ * — not points on the `WdSpd` speed axis), so the speed picker stays a pure
25636
+ * speed axis and the two flags stay independently togglable. The `fan-control`
25637
+ * percentage surface continues to mirror the same `WdSpd` speed for the
25638
+ * fan-style UI.
25639
+ */
25640
+ var GREE_FAN_MODES = [
25641
+ "auto",
25642
+ "low",
25643
+ "medium_low",
25644
+ "medium",
25645
+ "medium_high",
25646
+ "high"
25647
+ ];
25648
+ /** Map a library {@link FanSpeed} to its `climate-control` `fanMode` string. Pure. */
25649
+ function fanSpeedToFanMode(speed) {
25650
+ switch (speed) {
25651
+ case FanSpeed.Auto: return "auto";
25652
+ case FanSpeed.Low: return "low";
25653
+ case FanSpeed.MediumLow: return "medium_low";
25654
+ case FanSpeed.Medium: return "medium";
25655
+ case FanSpeed.MediumHigh: return "medium_high";
25656
+ case FanSpeed.High: return "high";
25657
+ }
25658
+ }
25659
+ /**
25660
+ * Map a `climate-control` `fanMode` string back to a library {@link FanSpeed},
25661
+ * or null when the string is not a known Gree speed. Pure.
25662
+ */
25663
+ function fanModeToFanSpeed(fanMode) {
25664
+ switch (fanMode) {
25665
+ case "auto": return FanSpeed.Auto;
25666
+ case "low": return FanSpeed.Low;
25667
+ case "medium_low": return FanSpeed.MediumLow;
25668
+ case "medium": return FanSpeed.Medium;
25669
+ case "medium_high": return FanSpeed.MediumHigh;
25670
+ case "high": return FanSpeed.High;
25671
+ default: return null;
25672
+ }
25673
+ }
25674
+ var GREE_PRESETS = [
25675
+ "none",
25676
+ "turbo",
25677
+ "quiet",
25678
+ "sleep",
25679
+ "eco",
25680
+ "8c_heat"
25681
+ ];
25682
+ /**
25683
+ * Derive the single active `preset` string from the device's comfort flags.
25684
+ * Precedence is fixed (turbo > quiet > sleep > eco > 8c_heat) so a coherent
25685
+ * single value is reported even if the device has more than one flag set.
25686
+ * Returns `'none'` when no flag is set. Pure.
25687
+ */
25688
+ function presetFromFlags(flags) {
25689
+ if (flags.turbo) return "turbo";
25690
+ if (flags.quiet) return "quiet";
25691
+ if (flags.sleep) return "sleep";
25692
+ if (flags.powerSave) return "eco";
25693
+ if (flags.steadyHeat) return "8c_heat";
25694
+ return "none";
25695
+ }
25696
+ /**
25697
+ * Resolve a requested `preset` string to the set of flag writes needed to make
25698
+ * it the active one — the chosen flag (if any) true, every other flag false.
25699
+ * Returns null when the string is not a known Gree preset. Pure.
25700
+ */
25701
+ function presetToFlagWrites(preset) {
25702
+ if (!GREE_PRESETS.includes(preset)) return null;
25703
+ return {
25704
+ turbo: preset === "turbo",
25705
+ quiet: preset === "quiet",
25706
+ sleep: preset === "sleep",
25707
+ powerSave: preset === "eco",
25708
+ steadyHeat: preset === "8c_heat"
25709
+ };
25710
+ }
25539
25711
  //#endregion
25540
25712
  //#region src/devices/gree-ac-device.ts
25541
25713
  var CLIMATE_CAP = "climate-control";
@@ -25544,9 +25716,9 @@ var CLIMATE_COLD_START = {
25544
25716
  mode: "off",
25545
25717
  availableModes: [...ADVERTISED_CAP_MODES],
25546
25718
  fanMode: "",
25547
- availableFanModes: [],
25719
+ availableFanModes: [...GREE_FAN_MODES],
25548
25720
  preset: "",
25549
- availablePresets: [],
25721
+ availablePresets: [...GREE_PRESETS],
25550
25722
  target: null,
25551
25723
  targetHigh: null,
25552
25724
  targetLow: null,
@@ -25582,17 +25754,21 @@ var greeAcSchema = object({
25582
25754
  * `stateChanged` push (and seeds on activate).
25583
25755
  *
25584
25756
  * Climate commands route to the bound handle: setMode `off`→`setPower(false)`,
25585
- * any other→`setPower(true)`+`setMode()`; setTarget→`setTargetTemperature()`.
25586
- * Fan commands: setPercentage→`setFanSpeed()` (percentageFanSpeed bucket);
25757
+ * any other→`setPower(true)`+`setMode()`; setTarget→`setTargetTemperature()`;
25758
+ * setFanMode→`setFanSpeed()` (climate `fanMode` string Gree speed); setPreset→
25759
+ * the comfort-flag setters (turbo/quiet/sleep/eco/8c_heat → `setTurbo`/`setQuiet`/
25760
+ * `setSleep`/`setPowerSave`/`setSteadyHeat`, exclusive). Fan commands:
25761
+ * setPercentage→`setFanSpeed()` (percentage↔FanSpeed bucket);
25587
25762
  * setOscillating→`setSwingVertical(FullSwing|Default)`.
25588
25763
  *
25589
- * TODO (deferred): the cap's `heat_cool` dual-setpoint, `setTargetHumidity`,
25590
- * fan-mode strings, and presets (turbo/quiet/sleep/powerSave/steadyHeat which the
25591
- * library DOES expose) are not yet mapped fan speed already occupies the
25592
- * `fan-control` percentage surface, and the cap preset/fan-mode surfaces are left
25593
- * empty. The fine-grained 12-position vertical / 7-position horizontal swing is
25594
- * collapsed to a single `oscillating` boolean. A later phase could add a vendor
25595
- * preset / swing-position cap.
25764
+ * TODO (deferred — needs protocol RE / live confirmation): `heat_cool`
25765
+ * dual-setpoint (Gree single-setpoint ACs have none), `setTargetHumidity`
25766
+ * (`Dwet`/`DwatSen` dehumidifier props exist in PROPS but the library exposes no
25767
+ * getter/setter yet), the light toggle (`setLight`), X-Fan / fresh-air / health
25768
+ * toggles (`setXfan`/`setFreshAir`/`setHealth`) all real library setters with
25769
+ * no cap surface (a vendor toggle cap would carry them), and the fine-grained
25770
+ * 12-position vertical / 7-position horizontal swing (collapsed to a single
25771
+ * `oscillating` boolean — a vendor swing-position cap would carry them).
25596
25772
  */
25597
25773
  var GreeAcDevice = class extends BaseDevice$1 {
25598
25774
  features = [];
@@ -25670,10 +25846,19 @@ var GreeAcDevice = class extends BaseDevice$1 {
25670
25846
  await ac.setMode(libMode);
25671
25847
  },
25672
25848
  setFanMode: async ({ fanMode }) => {
25673
- throw new Error(`gree ac: fan-mode strings not supported (use fan-control); got "${fanMode}"`);
25849
+ const speed = fanModeToFanSpeed(fanMode);
25850
+ if (speed === null) throw new Error(`gree ac: unsupported fan mode "${fanMode}"`);
25851
+ await this.requireAc().setFanSpeed(speed);
25674
25852
  },
25675
25853
  setPreset: async ({ preset }) => {
25676
- throw new Error(`gree ac: presets not yet supported (got "${preset}")`);
25854
+ const writes = presetToFlagWrites(preset);
25855
+ if (writes === null) throw new Error(`gree ac: unsupported preset "${preset}"`);
25856
+ const ac = this.requireAc();
25857
+ if (ac.turbo !== writes.turbo) await ac.setTurbo(writes.turbo);
25858
+ if (ac.quiet !== writes.quiet) await ac.setQuiet(writes.quiet);
25859
+ if (ac.sleep !== writes.sleep) await ac.setSleep(writes.sleep);
25860
+ if (ac.powerSave !== writes.powerSave) await ac.setPowerSave(writes.powerSave);
25861
+ if (ac.steadyHeat !== writes.steadyHeat) await ac.setSteadyHeat(writes.steadyHeat);
25677
25862
  },
25678
25863
  setTarget: async ({ target }) => {
25679
25864
  await this.requireAc().setTargetTemperature(target);
@@ -25707,13 +25892,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
25707
25892
  const ac = this.resolveAc();
25708
25893
  if (ac === null) return;
25709
25894
  const now = Date.now();
25895
+ const mode = ac.power ? libModeToCapMode(ac.mode) : "off";
25896
+ const preset = presetFromFlags({
25897
+ turbo: ac.turbo,
25898
+ quiet: ac.quiet,
25899
+ sleep: ac.sleep,
25900
+ powerSave: ac.powerSave,
25901
+ steadyHeat: ac.steadyHeat
25902
+ });
25710
25903
  const climateSlice = {
25711
- mode: ac.power ? libModeToCapMode(ac.mode) : "off",
25904
+ mode,
25712
25905
  availableModes: [...ADVERTISED_CAP_MODES],
25713
- fanMode: "",
25714
- availableFanModes: [],
25715
- preset: "",
25716
- availablePresets: [],
25906
+ fanMode: fanSpeedToFanMode(ac.fanSpeed),
25907
+ availableFanModes: [...GREE_FAN_MODES],
25908
+ preset,
25909
+ availablePresets: [...GREE_PRESETS],
25717
25910
  target: ac.targetTemperature,
25718
25911
  targetHigh: null,
25719
25912
  targetLow: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-gree",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Gree air-conditioner device-provider addon for CamStack — wraps the @apocaliss92/nodegree local-UDP client (LAN discovery + AES control), exposing climate-control and fan-control",
5
5
  "keywords": [
6
6
  "camstack",
@@ -44,7 +44,7 @@
44
44
  "instanceMode": "multiple",
45
45
  "brokerKind": "gree",
46
46
  "execution": {
47
- "placement": "any-node"
47
+ "placement": "hub-only"
48
48
  },
49
49
  "capabilities": [
50
50
  {