@apocaliss92/nodedreame 1.11.0 → 1.11.2

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/index.d.cts CHANGED
@@ -1849,6 +1849,31 @@ declare function extractMowerConsumableValues(result: unknown): number[] | null;
1849
1849
  * maintenance) with remaining %. Returns null on a malformed response.
1850
1850
  */
1851
1851
  declare function parseMowerConsumables(result: unknown): readonly MowerConsumableReading[] | null;
1852
+ /** Decoded mowing task sub-state (heartbeat byte 13), or null when no mowing
1853
+ * session is in progress. Ported from antondaubert/dreame-mower
1854
+ * `property_misc.py` Property11Handler. */
1855
+ type MowerTaskSubState = 'idle' | 'starting' | 'mowing' | 'paused' | 'finished' | 'failed' | 'exit' | 'returning-to-dock';
1856
+ /** Ordered sub-state table; the heartbeat encodes `subState = base + index`. */
1857
+ declare const MOWER_TASK_SUBSTATES: readonly MowerTaskSubState[];
1858
+ /** Typed heartbeat decode: battery byte, main-state, and (when mowing) the task
1859
+ * sub-state. */
1860
+ interface MowerHeartbeat {
1861
+ /** Raw battery byte (byte 11) — percent with a charging flag in the high bits. */
1862
+ readonly rawBattery: number;
1863
+ /** `(byte12 & 0x0F) - 1`; 4 = mowing. */
1864
+ readonly mainState: number;
1865
+ /** Raw sub-state byte (byte 13). */
1866
+ readonly subStateRaw: number;
1867
+ /** Decoded sub-state when a mowing session is active, else null. */
1868
+ readonly taskSubState: MowerTaskSubState | null;
1869
+ }
1870
+ /**
1871
+ * Decode the 20-byte HEARTBEAT (1:1) blob. Returns null when the payload is not a
1872
+ * well-formed heartbeat (wrong length / missing 0xCE sentinels). The task
1873
+ * sub-state is only meaningful while `mainState === 4` (mowing); in any other
1874
+ * main state there is no active mowing task (returns `taskSubState: null`).
1875
+ */
1876
+ declare function parseMowerHeartbeat(value: unknown): MowerHeartbeat | null;
1852
1877
 
1853
1878
  /**
1854
1879
  * Per-model mower capability records + a resolver adapting the rich record into
@@ -2053,6 +2078,14 @@ declare class MowerDevice extends BaseDevice {
2053
2078
  * code — which still surfaces as the raw number via {@link faultRaw}.
2054
2079
  */
2055
2080
  get fault(): MowerFault | null;
2081
+ /**
2082
+ * Live mowing task sub-state decoded from the HEARTBEAT (1:1) blob —
2083
+ * idle/starting/mowing/paused/finished/failed/exit/returning-to-dock — or null
2084
+ * when no mowing session is in progress (or the heartbeat is absent). This is
2085
+ * the donor's "current task" sensor; the heartbeat is push-only (the mower does
2086
+ * not answer live reads), so it tracks the device while it is actively mowing.
2087
+ */
2088
+ get taskSubState(): MowerTaskSubState | null;
2056
2089
  /** Parsed scheduling task descriptor (2:50), or null. */
2057
2090
  get task(): MowerTaskDescriptor | null;
2058
2091
  /**
@@ -2135,6 +2168,9 @@ declare class MowerDevice extends BaseDevice {
2135
2168
  mapSvg(opts?: RenderMowerSvgOptions): Promise<string>;
2136
2169
  /** Props worth seeding on start() / polling — exported for the facade. */
2137
2170
  static readonly DEFAULT_PROPS: readonly [{
2171
+ readonly siid: 1;
2172
+ readonly piid: 1;
2173
+ }, {
2138
2174
  readonly siid: 2;
2139
2175
  readonly piid: 1;
2140
2176
  }, {
@@ -2493,4 +2529,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
2493
2529
  */
2494
2530
  declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
2495
2531
 
2496
- export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, mowerConsumableIndex, parseMowerConsumables, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
2532
+ export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, MOWER_TASK_SUBSTATES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerHeartbeat, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerTaskSubState, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, mowerConsumableIndex, parseMowerConsumables, parseMowerHeartbeat, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
package/dist/index.d.ts CHANGED
@@ -1849,6 +1849,31 @@ declare function extractMowerConsumableValues(result: unknown): number[] | null;
1849
1849
  * maintenance) with remaining %. Returns null on a malformed response.
1850
1850
  */
1851
1851
  declare function parseMowerConsumables(result: unknown): readonly MowerConsumableReading[] | null;
1852
+ /** Decoded mowing task sub-state (heartbeat byte 13), or null when no mowing
1853
+ * session is in progress. Ported from antondaubert/dreame-mower
1854
+ * `property_misc.py` Property11Handler. */
1855
+ type MowerTaskSubState = 'idle' | 'starting' | 'mowing' | 'paused' | 'finished' | 'failed' | 'exit' | 'returning-to-dock';
1856
+ /** Ordered sub-state table; the heartbeat encodes `subState = base + index`. */
1857
+ declare const MOWER_TASK_SUBSTATES: readonly MowerTaskSubState[];
1858
+ /** Typed heartbeat decode: battery byte, main-state, and (when mowing) the task
1859
+ * sub-state. */
1860
+ interface MowerHeartbeat {
1861
+ /** Raw battery byte (byte 11) — percent with a charging flag in the high bits. */
1862
+ readonly rawBattery: number;
1863
+ /** `(byte12 & 0x0F) - 1`; 4 = mowing. */
1864
+ readonly mainState: number;
1865
+ /** Raw sub-state byte (byte 13). */
1866
+ readonly subStateRaw: number;
1867
+ /** Decoded sub-state when a mowing session is active, else null. */
1868
+ readonly taskSubState: MowerTaskSubState | null;
1869
+ }
1870
+ /**
1871
+ * Decode the 20-byte HEARTBEAT (1:1) blob. Returns null when the payload is not a
1872
+ * well-formed heartbeat (wrong length / missing 0xCE sentinels). The task
1873
+ * sub-state is only meaningful while `mainState === 4` (mowing); in any other
1874
+ * main state there is no active mowing task (returns `taskSubState: null`).
1875
+ */
1876
+ declare function parseMowerHeartbeat(value: unknown): MowerHeartbeat | null;
1852
1877
 
1853
1878
  /**
1854
1879
  * Per-model mower capability records + a resolver adapting the rich record into
@@ -2053,6 +2078,14 @@ declare class MowerDevice extends BaseDevice {
2053
2078
  * code — which still surfaces as the raw number via {@link faultRaw}.
2054
2079
  */
2055
2080
  get fault(): MowerFault | null;
2081
+ /**
2082
+ * Live mowing task sub-state decoded from the HEARTBEAT (1:1) blob —
2083
+ * idle/starting/mowing/paused/finished/failed/exit/returning-to-dock — or null
2084
+ * when no mowing session is in progress (or the heartbeat is absent). This is
2085
+ * the donor's "current task" sensor; the heartbeat is push-only (the mower does
2086
+ * not answer live reads), so it tracks the device while it is actively mowing.
2087
+ */
2088
+ get taskSubState(): MowerTaskSubState | null;
2056
2089
  /** Parsed scheduling task descriptor (2:50), or null. */
2057
2090
  get task(): MowerTaskDescriptor | null;
2058
2091
  /**
@@ -2135,6 +2168,9 @@ declare class MowerDevice extends BaseDevice {
2135
2168
  mapSvg(opts?: RenderMowerSvgOptions): Promise<string>;
2136
2169
  /** Props worth seeding on start() / polling — exported for the facade. */
2137
2170
  static readonly DEFAULT_PROPS: readonly [{
2171
+ readonly siid: 1;
2172
+ readonly piid: 1;
2173
+ }, {
2138
2174
  readonly siid: 2;
2139
2175
  readonly piid: 1;
2140
2176
  }, {
@@ -2493,4 +2529,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
2493
2529
  */
2494
2530
  declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
2495
2531
 
2496
- export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, mowerConsumableIndex, parseMowerConsumables, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
2532
+ export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, MOWER_TASK_SUBSTATES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerHeartbeat, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerTaskSubState, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, mowerConsumableIndex, parseMowerConsumables, parseMowerHeartbeat, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/support/version.ts
2
2
  var LIBRARY_NAME = "nodedreame";
3
- var LIBRARY_VERSION = "1.11.0";
3
+ var LIBRARY_VERSION = "1.11.2";
4
4
 
5
5
  // src/transport/errors.ts
6
6
  var DreameError = class extends Error {
@@ -616,6 +616,7 @@ var DreamePush = class extends TypedEmitter {
616
616
  #region;
617
617
  #connect;
618
618
  #backoffMs;
619
+ #keepaliveSeconds;
619
620
  #rejectUnauthorized;
620
621
  #topic;
621
622
  #client = null;
@@ -636,6 +637,7 @@ var DreamePush = class extends TypedEmitter {
636
637
  this.#region = input.region;
637
638
  this.#connect = input.connect ?? defaultConnect;
638
639
  this.#backoffMs = input.reconnectBackoffMs ?? 5e3;
640
+ this.#keepaliveSeconds = input.keepaliveSeconds ?? 60;
639
641
  this.#rejectUnauthorized = input.rejectUnauthorized ?? false;
640
642
  this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
641
643
  this.#onClose = () => {
@@ -693,6 +695,7 @@ var DreamePush = class extends TypedEmitter {
693
695
  reconnectPeriod: 0,
694
696
  // we drive reconnect ourselves
695
697
  connectTimeout: 15e3,
698
+ keepalive: this.#keepaliveSeconds,
696
699
  rejectUnauthorized: this.#rejectUnauthorized,
697
700
  clean: true
698
701
  });
@@ -700,25 +703,37 @@ var DreamePush = class extends TypedEmitter {
700
703
  client.on("message", (_topic, payload) => this.#handleMessage(payload));
701
704
  client.on("error", (err) => this.emit("error", err));
702
705
  client.on("close", this.#onClose);
703
- await new Promise((resolve, reject) => {
704
- const onConnect = () => {
705
- client.removeListener("error", onError);
706
- client.subscribe(this.#topic, { qos: 0 }, (err) => {
707
- if (err) {
708
- reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
709
- return;
710
- }
711
- this.emit("connect");
712
- resolve();
713
- });
714
- };
715
- const onError = (err) => {
716
- client.removeListener("connect", onConnect);
717
- reject(new DreameTransportError(`mqtt connect failed: ${err.message}`, err));
718
- };
719
- client.once("connect", onConnect);
720
- client.once("error", onError);
721
- });
706
+ try {
707
+ await new Promise((resolve, reject) => {
708
+ const onConnect = () => {
709
+ client.removeListener("error", onError);
710
+ client.subscribe(this.#topic, { qos: 0 }, (err) => {
711
+ if (err) {
712
+ reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
713
+ return;
714
+ }
715
+ this.emit("connect");
716
+ resolve();
717
+ });
718
+ };
719
+ const onError = (err) => {
720
+ client.removeListener("connect", onConnect);
721
+ reject(new DreameTransportError(`mqtt connect failed: ${err.message}`, err));
722
+ };
723
+ client.once("connect", onConnect);
724
+ client.once("error", onError);
725
+ });
726
+ } catch (err) {
727
+ client.removeListener("close", this.#onClose);
728
+ try {
729
+ client.end(true, {}, () => void 0);
730
+ } catch {
731
+ }
732
+ if (this.#client === client) {
733
+ this.#client = null;
734
+ }
735
+ throw err;
736
+ }
722
737
  }
723
738
  #scheduleReconnect() {
724
739
  if (this.#closed || this.#reconnectTimer) {
@@ -1560,7 +1575,9 @@ function encodeAiFeatureWrite(raw, feature, value) {
1560
1575
  if (typeof raw === "number") {
1561
1576
  const bit = AI_FEATURE_BIT[feature];
1562
1577
  if (bit === void 0) {
1563
- throw new Error(`encodeAiFeatureWrite: feature "${feature}" has no int-bitmask representation`);
1578
+ throw new Error(
1579
+ `encodeAiFeatureWrite: feature "${feature}" has no int-bitmask representation`
1580
+ );
1564
1581
  }
1565
1582
  return value ? raw | bit : raw & ~bit;
1566
1583
  }
@@ -1677,18 +1694,53 @@ function encodeAutoSwitchWrite(key2, value) {
1677
1694
 
1678
1695
  // src/models/vacuum/consumables.ts
1679
1696
  var VACUUM_CONSUMABLES = [
1680
- { key: "main-brush", label: "Main Brush", life: { siid: 9, piid: 2 }, reset: { siid: 9, aiid: 1 } },
1681
- { key: "side-brush", label: "Side Brush", life: { siid: 10, piid: 2 }, reset: { siid: 10, aiid: 1 } },
1697
+ {
1698
+ key: "main-brush",
1699
+ label: "Main Brush",
1700
+ life: { siid: 9, piid: 2 },
1701
+ reset: { siid: 9, aiid: 1 }
1702
+ },
1703
+ {
1704
+ key: "side-brush",
1705
+ label: "Side Brush",
1706
+ life: { siid: 10, piid: 2 },
1707
+ reset: { siid: 10, aiid: 1 }
1708
+ },
1682
1709
  { key: "filter", label: "Filter", life: { siid: 11, piid: 1 }, reset: { siid: 11, aiid: 1 } },
1683
1710
  { key: "sensor", label: "Sensor", life: { siid: 16, piid: 1 }, reset: { siid: 16, aiid: 1 } },
1684
- { key: "tank-filter", label: "Tank Filter", life: { siid: 17, piid: 1 }, reset: { siid: 17, aiid: 1 } },
1711
+ {
1712
+ key: "tank-filter",
1713
+ label: "Tank Filter",
1714
+ life: { siid: 17, piid: 1 },
1715
+ reset: { siid: 17, aiid: 1 }
1716
+ },
1685
1717
  { key: "mop-pad", label: "Mop Pad", life: { siid: 18, piid: 1 }, reset: { siid: 18, aiid: 1 } },
1686
- { key: "silver-ion", label: "Silver-ion", life: { siid: 19, piid: 2 }, reset: { siid: 19, aiid: 1 } },
1687
- { key: "detergent", label: "Detergent", life: { siid: 20, piid: 1 }, reset: { siid: 20, aiid: 1 } },
1718
+ {
1719
+ key: "silver-ion",
1720
+ label: "Silver-ion",
1721
+ life: { siid: 19, piid: 2 },
1722
+ reset: { siid: 19, aiid: 1 }
1723
+ },
1724
+ {
1725
+ key: "detergent",
1726
+ label: "Detergent",
1727
+ life: { siid: 20, piid: 1 },
1728
+ reset: { siid: 20, aiid: 1 }
1729
+ },
1688
1730
  { key: "squeegee", label: "Squeegee", life: { siid: 24, piid: 1 }, reset: { siid: 24, aiid: 1 } },
1689
- { key: "deodorizer", label: "Deodorizer", life: { siid: 29, piid: 2 }, reset: { siid: 29, aiid: 1 } },
1731
+ {
1732
+ key: "deodorizer",
1733
+ label: "Deodorizer",
1734
+ life: { siid: 29, piid: 2 },
1735
+ reset: { siid: 29, aiid: 1 }
1736
+ },
1690
1737
  { key: "wheel", label: "Wheel", life: { siid: 30, piid: 2 }, reset: { siid: 30, aiid: 1 } },
1691
- { key: "scale-inhibitor", label: "Scale Inhibitor", life: { siid: 31, piid: 2 }, reset: { siid: 31, aiid: 1 } },
1738
+ {
1739
+ key: "scale-inhibitor",
1740
+ label: "Scale Inhibitor",
1741
+ life: { siid: 31, piid: 2 },
1742
+ reset: { siid: 31, aiid: 1 }
1743
+ },
1692
1744
  { key: "dust-bag", label: "Dust Bag", life: { siid: 27, piid: 17 }, reset: null }
1693
1745
  ];
1694
1746
  function consumableSpec(key2) {
@@ -3803,6 +3855,9 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
3803
3855
 
3804
3856
  // src/models/mower/properties.ts
3805
3857
  var MOWER_PROP = {
3858
+ /** VERIFIED-by-donor — 20-byte heartbeat (sentinel 0xCE at [0]/[19]); byte 11
3859
+ * battery, byte 12 main-state, byte 13 task sub-state. See parseHeartbeat. */
3860
+ HEARTBEAT: { siid: 1, piid: 1 },
3806
3861
  /** VERIFIED-by-donor — pose + coverage telemetry (binary). P4 uses progress % only. */
3807
3862
  POSE_COVERAGE: { siid: 1, piid: 4 },
3808
3863
  /** VERIFIED-by-donor — MowerStatus (DeviceStatus). */
@@ -4152,6 +4207,39 @@ function parseMowerConsumables(result) {
4152
4207
  };
4153
4208
  });
4154
4209
  }
4210
+ var MOWER_TASK_SUBSTATES = [
4211
+ "idle",
4212
+ "starting",
4213
+ "mowing",
4214
+ "paused",
4215
+ "finished",
4216
+ "failed",
4217
+ "exit",
4218
+ "returning-to-dock"
4219
+ ];
4220
+ var HEARTBEAT_SENTINEL = 206;
4221
+ var HEARTBEAT_SUBSTATE_BASE = 33;
4222
+ var HEARTBEAT_MAIN_STATE_MOWING = 4;
4223
+ function parseMowerHeartbeat(value) {
4224
+ if (!Array.isArray(value) || value.length < 14) {
4225
+ return null;
4226
+ }
4227
+ const bytes = [];
4228
+ for (const b of value) {
4229
+ if (typeof b !== "number") {
4230
+ return null;
4231
+ }
4232
+ bytes.push(b);
4233
+ }
4234
+ if (bytes[0] !== HEARTBEAT_SENTINEL || bytes[bytes.length - 1] !== HEARTBEAT_SENTINEL) {
4235
+ return null;
4236
+ }
4237
+ const rawBattery = bytes[11] ?? 0;
4238
+ const mainState = ((bytes[12] ?? 0) & 15) - 1;
4239
+ const subStateRaw = bytes[13] ?? 0;
4240
+ const taskSubState = mainState === HEARTBEAT_MAIN_STATE_MOWING ? MOWER_TASK_SUBSTATES[subStateRaw - HEARTBEAT_SUBSTATE_BASE] ?? null : null;
4241
+ return { rawBattery, mainState, subStateRaw, taskSubState };
4242
+ }
4155
4243
 
4156
4244
  // src/models/mower/capabilities.ts
4157
4245
  var FALLBACK2 = {
@@ -4825,6 +4913,19 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
4825
4913
  get fault() {
4826
4914
  return FAULT(this.faultRaw);
4827
4915
  }
4916
+ /**
4917
+ * Live mowing task sub-state decoded from the HEARTBEAT (1:1) blob —
4918
+ * idle/starting/mowing/paused/finished/failed/exit/returning-to-dock — or null
4919
+ * when no mowing session is in progress (or the heartbeat is absent). This is
4920
+ * the donor's "current task" sensor; the heartbeat is push-only (the mower does
4921
+ * not answer live reads), so it tracks the device while it is actively mowing.
4922
+ */
4923
+ get taskSubState() {
4924
+ const hb = parseMowerHeartbeat(
4925
+ this.getProperty(MOWER_PROP.HEARTBEAT.siid, MOWER_PROP.HEARTBEAT.piid)?.value
4926
+ );
4927
+ return hb?.taskSubState ?? null;
4928
+ }
4828
4929
  /** Parsed scheduling task descriptor (2:50), or null. */
4829
4930
  get task() {
4830
4931
  return parseTaskDescriptor(
@@ -5016,6 +5117,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
5016
5117
  }
5017
5118
  /** Props worth seeding on start() / polling — exported for the facade. */
5018
5119
  static DEFAULT_PROPS = [
5120
+ MOWER_PROP.HEARTBEAT,
5019
5121
  MOWER_PROP.STATUS,
5020
5122
  MOWER_PROP.DEVICE_CODE,
5021
5123
  MOWER_PROP.BATTERY,
@@ -5643,6 +5745,7 @@ export {
5643
5745
  DreameTransportError,
5644
5746
  LIBRARY_NAME,
5645
5747
  MODEL_CAPABILITIES2 as MOWER_MODEL_CAPABILITIES,
5748
+ MOWER_TASK_SUBSTATES,
5646
5749
  MiotError,
5647
5750
  MiotState,
5648
5751
  MowerCapabilityResolver,
@@ -5674,6 +5777,7 @@ export {
5674
5777
  isDreameConsumableKey,
5675
5778
  mowerConsumableIndex,
5676
5779
  parseMowerConsumables,
5780
+ parseMowerHeartbeat,
5677
5781
  renderMowerSvg,
5678
5782
  renderVacuumPng,
5679
5783
  resolveCapabilities,