@camstack/addon-provider-gree 0.1.5 → 0.1.6

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 +186 -33
  2. package/dist/addon.mjs +186 -33
  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
  });
@@ -16904,6 +16943,9 @@ method(object({
16904
16943
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16905
16944
  kind: "mutation",
16906
16945
  auth: "admin"
16946
+ }), method(ResyncInputSchema, ResyncResultSchema, {
16947
+ kind: "mutation",
16948
+ auth: "admin"
16907
16949
  }), method(object({
16908
16950
  deviceId: number(),
16909
16951
  key: string(),
@@ -20666,6 +20708,12 @@ Object.freeze({
20666
20708
  addonId: null,
20667
20709
  access: "create"
20668
20710
  },
20711
+ "deviceManager.adoptionResync": {
20712
+ capName: "device-manager",
20713
+ capScope: "system",
20714
+ addonId: null,
20715
+ access: "create"
20716
+ },
20669
20717
  "deviceManager.allocateDeviceId": {
20670
20718
  capName: "device-manager",
20671
20719
  capScope: "system",
@@ -25537,6 +25585,90 @@ function swingToOscillating(vertical, horizontal) {
25537
25585
  function oscillatingToVerticalSwing(oscillating) {
25538
25586
  return oscillating ? VerticalSwing.FullSwing : VerticalSwing.Default;
25539
25587
  }
25588
+ /**
25589
+ * Gree fan-speed names surfaced on the `climate-control` cap's `fanMode` /
25590
+ * `availableFanModes` strings, mirroring the HA Gree integration's `fan_modes`
25591
+ * attribute (`auto`/`low`/`medium_low`/`medium`/`medium_high`/`high`).
25592
+ *
25593
+ * NOTE: HA's Gree integration also folds `turbo` and `quiet` INTO `fan_modes`.
25594
+ * We deliberately keep turbo/quiet on the dedicated `preset` surface instead
25595
+ * (they are independent boolean device flags in the protocol — `Tur` / `Quiet`
25596
+ * — not points on the `WdSpd` speed axis), so the speed picker stays a pure
25597
+ * speed axis and the two flags stay independently togglable. The `fan-control`
25598
+ * percentage surface continues to mirror the same `WdSpd` speed for the
25599
+ * fan-style UI.
25600
+ */
25601
+ var GREE_FAN_MODES = [
25602
+ "auto",
25603
+ "low",
25604
+ "medium_low",
25605
+ "medium",
25606
+ "medium_high",
25607
+ "high"
25608
+ ];
25609
+ /** Map a library {@link FanSpeed} to its `climate-control` `fanMode` string. Pure. */
25610
+ function fanSpeedToFanMode(speed) {
25611
+ switch (speed) {
25612
+ case FanSpeed.Auto: return "auto";
25613
+ case FanSpeed.Low: return "low";
25614
+ case FanSpeed.MediumLow: return "medium_low";
25615
+ case FanSpeed.Medium: return "medium";
25616
+ case FanSpeed.MediumHigh: return "medium_high";
25617
+ case FanSpeed.High: return "high";
25618
+ }
25619
+ }
25620
+ /**
25621
+ * Map a `climate-control` `fanMode` string back to a library {@link FanSpeed},
25622
+ * or null when the string is not a known Gree speed. Pure.
25623
+ */
25624
+ function fanModeToFanSpeed(fanMode) {
25625
+ switch (fanMode) {
25626
+ case "auto": return FanSpeed.Auto;
25627
+ case "low": return FanSpeed.Low;
25628
+ case "medium_low": return FanSpeed.MediumLow;
25629
+ case "medium": return FanSpeed.Medium;
25630
+ case "medium_high": return FanSpeed.MediumHigh;
25631
+ case "high": return FanSpeed.High;
25632
+ default: return null;
25633
+ }
25634
+ }
25635
+ var GREE_PRESETS = [
25636
+ "none",
25637
+ "turbo",
25638
+ "quiet",
25639
+ "sleep",
25640
+ "eco",
25641
+ "8c_heat"
25642
+ ];
25643
+ /**
25644
+ * Derive the single active `preset` string from the device's comfort flags.
25645
+ * Precedence is fixed (turbo > quiet > sleep > eco > 8c_heat) so a coherent
25646
+ * single value is reported even if the device has more than one flag set.
25647
+ * Returns `'none'` when no flag is set. Pure.
25648
+ */
25649
+ function presetFromFlags(flags) {
25650
+ if (flags.turbo) return "turbo";
25651
+ if (flags.quiet) return "quiet";
25652
+ if (flags.sleep) return "sleep";
25653
+ if (flags.powerSave) return "eco";
25654
+ if (flags.steadyHeat) return "8c_heat";
25655
+ return "none";
25656
+ }
25657
+ /**
25658
+ * Resolve a requested `preset` string to the set of flag writes needed to make
25659
+ * it the active one — the chosen flag (if any) true, every other flag false.
25660
+ * Returns null when the string is not a known Gree preset. Pure.
25661
+ */
25662
+ function presetToFlagWrites(preset) {
25663
+ if (!GREE_PRESETS.includes(preset)) return null;
25664
+ return {
25665
+ turbo: preset === "turbo",
25666
+ quiet: preset === "quiet",
25667
+ sleep: preset === "sleep",
25668
+ powerSave: preset === "eco",
25669
+ steadyHeat: preset === "8c_heat"
25670
+ };
25671
+ }
25540
25672
  //#endregion
25541
25673
  //#region src/devices/gree-ac-device.ts
25542
25674
  var CLIMATE_CAP = "climate-control";
@@ -25545,9 +25677,9 @@ var CLIMATE_COLD_START = {
25545
25677
  mode: "off",
25546
25678
  availableModes: [...ADVERTISED_CAP_MODES],
25547
25679
  fanMode: "",
25548
- availableFanModes: [],
25680
+ availableFanModes: [...GREE_FAN_MODES],
25549
25681
  preset: "",
25550
- availablePresets: [],
25682
+ availablePresets: [...GREE_PRESETS],
25551
25683
  target: null,
25552
25684
  targetHigh: null,
25553
25685
  targetLow: null,
@@ -25583,17 +25715,21 @@ var greeAcSchema = object({
25583
25715
  * `stateChanged` push (and seeds on activate).
25584
25716
  *
25585
25717
  * 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);
25718
+ * any other→`setPower(true)`+`setMode()`; setTarget→`setTargetTemperature()`;
25719
+ * setFanMode→`setFanSpeed()` (climate `fanMode` string Gree speed); setPreset→
25720
+ * the comfort-flag setters (turbo/quiet/sleep/eco/8c_heat → `setTurbo`/`setQuiet`/
25721
+ * `setSleep`/`setPowerSave`/`setSteadyHeat`, exclusive). Fan commands:
25722
+ * setPercentage→`setFanSpeed()` (percentage↔FanSpeed bucket);
25588
25723
  * setOscillating→`setSwingVertical(FullSwing|Default)`.
25589
25724
  *
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.
25725
+ * TODO (deferred — needs protocol RE / live confirmation): `heat_cool`
25726
+ * dual-setpoint (Gree single-setpoint ACs have none), `setTargetHumidity`
25727
+ * (`Dwet`/`DwatSen` dehumidifier props exist in PROPS but the library exposes no
25728
+ * getter/setter yet), the light toggle (`setLight`), X-Fan / fresh-air / health
25729
+ * toggles (`setXfan`/`setFreshAir`/`setHealth`) all real library setters with
25730
+ * no cap surface (a vendor toggle cap would carry them), and the fine-grained
25731
+ * 12-position vertical / 7-position horizontal swing (collapsed to a single
25732
+ * `oscillating` boolean — a vendor swing-position cap would carry them).
25597
25733
  */
25598
25734
  var GreeAcDevice = class extends BaseDevice$1 {
25599
25735
  features = [];
@@ -25671,10 +25807,19 @@ var GreeAcDevice = class extends BaseDevice$1 {
25671
25807
  await ac.setMode(libMode);
25672
25808
  },
25673
25809
  setFanMode: async ({ fanMode }) => {
25674
- throw new Error(`gree ac: fan-mode strings not supported (use fan-control); got "${fanMode}"`);
25810
+ const speed = fanModeToFanSpeed(fanMode);
25811
+ if (speed === null) throw new Error(`gree ac: unsupported fan mode "${fanMode}"`);
25812
+ await this.requireAc().setFanSpeed(speed);
25675
25813
  },
25676
25814
  setPreset: async ({ preset }) => {
25677
- throw new Error(`gree ac: presets not yet supported (got "${preset}")`);
25815
+ const writes = presetToFlagWrites(preset);
25816
+ if (writes === null) throw new Error(`gree ac: unsupported preset "${preset}"`);
25817
+ const ac = this.requireAc();
25818
+ if (ac.turbo !== writes.turbo) await ac.setTurbo(writes.turbo);
25819
+ if (ac.quiet !== writes.quiet) await ac.setQuiet(writes.quiet);
25820
+ if (ac.sleep !== writes.sleep) await ac.setSleep(writes.sleep);
25821
+ if (ac.powerSave !== writes.powerSave) await ac.setPowerSave(writes.powerSave);
25822
+ if (ac.steadyHeat !== writes.steadyHeat) await ac.setSteadyHeat(writes.steadyHeat);
25678
25823
  },
25679
25824
  setTarget: async ({ target }) => {
25680
25825
  await this.requireAc().setTargetTemperature(target);
@@ -25708,13 +25853,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
25708
25853
  const ac = this.resolveAc();
25709
25854
  if (ac === null) return;
25710
25855
  const now = Date.now();
25856
+ const mode = ac.power ? libModeToCapMode(ac.mode) : "off";
25857
+ const preset = presetFromFlags({
25858
+ turbo: ac.turbo,
25859
+ quiet: ac.quiet,
25860
+ sleep: ac.sleep,
25861
+ powerSave: ac.powerSave,
25862
+ steadyHeat: ac.steadyHeat
25863
+ });
25711
25864
  const climateSlice = {
25712
- mode: ac.power ? libModeToCapMode(ac.mode) : "off",
25865
+ mode,
25713
25866
  availableModes: [...ADVERTISED_CAP_MODES],
25714
- fanMode: "",
25715
- availableFanModes: [],
25716
- preset: "",
25717
- availablePresets: [],
25867
+ fanMode: fanSpeedToFanMode(ac.fanSpeed),
25868
+ availableFanModes: [...GREE_FAN_MODES],
25869
+ preset,
25870
+ availablePresets: [...GREE_PRESETS],
25718
25871
  target: ac.targetTemperature,
25719
25872
  targetHigh: null,
25720
25873
  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
  });
@@ -16903,6 +16942,9 @@ method(object({
16903
16942
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16904
16943
  kind: "mutation",
16905
16944
  auth: "admin"
16945
+ }), method(ResyncInputSchema, ResyncResultSchema, {
16946
+ kind: "mutation",
16947
+ auth: "admin"
16906
16948
  }), method(object({
16907
16949
  deviceId: number(),
16908
16950
  key: string(),
@@ -20665,6 +20707,12 @@ Object.freeze({
20665
20707
  addonId: null,
20666
20708
  access: "create"
20667
20709
  },
20710
+ "deviceManager.adoptionResync": {
20711
+ capName: "device-manager",
20712
+ capScope: "system",
20713
+ addonId: null,
20714
+ access: "create"
20715
+ },
20668
20716
  "deviceManager.allocateDeviceId": {
20669
20717
  capName: "device-manager",
20670
20718
  capScope: "system",
@@ -25536,6 +25584,90 @@ function swingToOscillating(vertical, horizontal) {
25536
25584
  function oscillatingToVerticalSwing(oscillating) {
25537
25585
  return oscillating ? VerticalSwing.FullSwing : VerticalSwing.Default;
25538
25586
  }
25587
+ /**
25588
+ * Gree fan-speed names surfaced on the `climate-control` cap's `fanMode` /
25589
+ * `availableFanModes` strings, mirroring the HA Gree integration's `fan_modes`
25590
+ * attribute (`auto`/`low`/`medium_low`/`medium`/`medium_high`/`high`).
25591
+ *
25592
+ * NOTE: HA's Gree integration also folds `turbo` and `quiet` INTO `fan_modes`.
25593
+ * We deliberately keep turbo/quiet on the dedicated `preset` surface instead
25594
+ * (they are independent boolean device flags in the protocol — `Tur` / `Quiet`
25595
+ * — not points on the `WdSpd` speed axis), so the speed picker stays a pure
25596
+ * speed axis and the two flags stay independently togglable. The `fan-control`
25597
+ * percentage surface continues to mirror the same `WdSpd` speed for the
25598
+ * fan-style UI.
25599
+ */
25600
+ var GREE_FAN_MODES = [
25601
+ "auto",
25602
+ "low",
25603
+ "medium_low",
25604
+ "medium",
25605
+ "medium_high",
25606
+ "high"
25607
+ ];
25608
+ /** Map a library {@link FanSpeed} to its `climate-control` `fanMode` string. Pure. */
25609
+ function fanSpeedToFanMode(speed) {
25610
+ switch (speed) {
25611
+ case FanSpeed.Auto: return "auto";
25612
+ case FanSpeed.Low: return "low";
25613
+ case FanSpeed.MediumLow: return "medium_low";
25614
+ case FanSpeed.Medium: return "medium";
25615
+ case FanSpeed.MediumHigh: return "medium_high";
25616
+ case FanSpeed.High: return "high";
25617
+ }
25618
+ }
25619
+ /**
25620
+ * Map a `climate-control` `fanMode` string back to a library {@link FanSpeed},
25621
+ * or null when the string is not a known Gree speed. Pure.
25622
+ */
25623
+ function fanModeToFanSpeed(fanMode) {
25624
+ switch (fanMode) {
25625
+ case "auto": return FanSpeed.Auto;
25626
+ case "low": return FanSpeed.Low;
25627
+ case "medium_low": return FanSpeed.MediumLow;
25628
+ case "medium": return FanSpeed.Medium;
25629
+ case "medium_high": return FanSpeed.MediumHigh;
25630
+ case "high": return FanSpeed.High;
25631
+ default: return null;
25632
+ }
25633
+ }
25634
+ var GREE_PRESETS = [
25635
+ "none",
25636
+ "turbo",
25637
+ "quiet",
25638
+ "sleep",
25639
+ "eco",
25640
+ "8c_heat"
25641
+ ];
25642
+ /**
25643
+ * Derive the single active `preset` string from the device's comfort flags.
25644
+ * Precedence is fixed (turbo > quiet > sleep > eco > 8c_heat) so a coherent
25645
+ * single value is reported even if the device has more than one flag set.
25646
+ * Returns `'none'` when no flag is set. Pure.
25647
+ */
25648
+ function presetFromFlags(flags) {
25649
+ if (flags.turbo) return "turbo";
25650
+ if (flags.quiet) return "quiet";
25651
+ if (flags.sleep) return "sleep";
25652
+ if (flags.powerSave) return "eco";
25653
+ if (flags.steadyHeat) return "8c_heat";
25654
+ return "none";
25655
+ }
25656
+ /**
25657
+ * Resolve a requested `preset` string to the set of flag writes needed to make
25658
+ * it the active one — the chosen flag (if any) true, every other flag false.
25659
+ * Returns null when the string is not a known Gree preset. Pure.
25660
+ */
25661
+ function presetToFlagWrites(preset) {
25662
+ if (!GREE_PRESETS.includes(preset)) return null;
25663
+ return {
25664
+ turbo: preset === "turbo",
25665
+ quiet: preset === "quiet",
25666
+ sleep: preset === "sleep",
25667
+ powerSave: preset === "eco",
25668
+ steadyHeat: preset === "8c_heat"
25669
+ };
25670
+ }
25539
25671
  //#endregion
25540
25672
  //#region src/devices/gree-ac-device.ts
25541
25673
  var CLIMATE_CAP = "climate-control";
@@ -25544,9 +25676,9 @@ var CLIMATE_COLD_START = {
25544
25676
  mode: "off",
25545
25677
  availableModes: [...ADVERTISED_CAP_MODES],
25546
25678
  fanMode: "",
25547
- availableFanModes: [],
25679
+ availableFanModes: [...GREE_FAN_MODES],
25548
25680
  preset: "",
25549
- availablePresets: [],
25681
+ availablePresets: [...GREE_PRESETS],
25550
25682
  target: null,
25551
25683
  targetHigh: null,
25552
25684
  targetLow: null,
@@ -25582,17 +25714,21 @@ var greeAcSchema = object({
25582
25714
  * `stateChanged` push (and seeds on activate).
25583
25715
  *
25584
25716
  * 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);
25717
+ * any other→`setPower(true)`+`setMode()`; setTarget→`setTargetTemperature()`;
25718
+ * setFanMode→`setFanSpeed()` (climate `fanMode` string Gree speed); setPreset→
25719
+ * the comfort-flag setters (turbo/quiet/sleep/eco/8c_heat → `setTurbo`/`setQuiet`/
25720
+ * `setSleep`/`setPowerSave`/`setSteadyHeat`, exclusive). Fan commands:
25721
+ * setPercentage→`setFanSpeed()` (percentage↔FanSpeed bucket);
25587
25722
  * setOscillating→`setSwingVertical(FullSwing|Default)`.
25588
25723
  *
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.
25724
+ * TODO (deferred — needs protocol RE / live confirmation): `heat_cool`
25725
+ * dual-setpoint (Gree single-setpoint ACs have none), `setTargetHumidity`
25726
+ * (`Dwet`/`DwatSen` dehumidifier props exist in PROPS but the library exposes no
25727
+ * getter/setter yet), the light toggle (`setLight`), X-Fan / fresh-air / health
25728
+ * toggles (`setXfan`/`setFreshAir`/`setHealth`) all real library setters with
25729
+ * no cap surface (a vendor toggle cap would carry them), and the fine-grained
25730
+ * 12-position vertical / 7-position horizontal swing (collapsed to a single
25731
+ * `oscillating` boolean — a vendor swing-position cap would carry them).
25596
25732
  */
25597
25733
  var GreeAcDevice = class extends BaseDevice$1 {
25598
25734
  features = [];
@@ -25670,10 +25806,19 @@ var GreeAcDevice = class extends BaseDevice$1 {
25670
25806
  await ac.setMode(libMode);
25671
25807
  },
25672
25808
  setFanMode: async ({ fanMode }) => {
25673
- throw new Error(`gree ac: fan-mode strings not supported (use fan-control); got "${fanMode}"`);
25809
+ const speed = fanModeToFanSpeed(fanMode);
25810
+ if (speed === null) throw new Error(`gree ac: unsupported fan mode "${fanMode}"`);
25811
+ await this.requireAc().setFanSpeed(speed);
25674
25812
  },
25675
25813
  setPreset: async ({ preset }) => {
25676
- throw new Error(`gree ac: presets not yet supported (got "${preset}")`);
25814
+ const writes = presetToFlagWrites(preset);
25815
+ if (writes === null) throw new Error(`gree ac: unsupported preset "${preset}"`);
25816
+ const ac = this.requireAc();
25817
+ if (ac.turbo !== writes.turbo) await ac.setTurbo(writes.turbo);
25818
+ if (ac.quiet !== writes.quiet) await ac.setQuiet(writes.quiet);
25819
+ if (ac.sleep !== writes.sleep) await ac.setSleep(writes.sleep);
25820
+ if (ac.powerSave !== writes.powerSave) await ac.setPowerSave(writes.powerSave);
25821
+ if (ac.steadyHeat !== writes.steadyHeat) await ac.setSteadyHeat(writes.steadyHeat);
25677
25822
  },
25678
25823
  setTarget: async ({ target }) => {
25679
25824
  await this.requireAc().setTargetTemperature(target);
@@ -25707,13 +25852,21 @@ var GreeAcDevice = class extends BaseDevice$1 {
25707
25852
  const ac = this.resolveAc();
25708
25853
  if (ac === null) return;
25709
25854
  const now = Date.now();
25855
+ const mode = ac.power ? libModeToCapMode(ac.mode) : "off";
25856
+ const preset = presetFromFlags({
25857
+ turbo: ac.turbo,
25858
+ quiet: ac.quiet,
25859
+ sleep: ac.sleep,
25860
+ powerSave: ac.powerSave,
25861
+ steadyHeat: ac.steadyHeat
25862
+ });
25710
25863
  const climateSlice = {
25711
- mode: ac.power ? libModeToCapMode(ac.mode) : "off",
25864
+ mode,
25712
25865
  availableModes: [...ADVERTISED_CAP_MODES],
25713
- fanMode: "",
25714
- availableFanModes: [],
25715
- preset: "",
25716
- availablePresets: [],
25866
+ fanMode: fanSpeedToFanMode(ac.fanSpeed),
25867
+ availableFanModes: [...GREE_FAN_MODES],
25868
+ preset,
25869
+ availablePresets: [...GREE_PRESETS],
25717
25870
  target: ac.targetTemperature,
25718
25871
  targetHigh: null,
25719
25872
  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.6",
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
  {