@apocaliss92/nodedreame 1.9.0 → 1.11.0

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
@@ -1591,6 +1591,22 @@ declare class VacuumDevice extends BaseDevice<VacuumDeviceEvents> {
1591
1591
  * `applyVacuumPFrame` merge primitive ships separately for that work.
1592
1592
  */
1593
1593
  getMap(input: VacuumGetMapInput): Promise<VacuumMap>;
1594
+ /** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
1595
+ resetMapStream(): void;
1596
+ /**
1597
+ * Fetch the latest advertised map frame and fold it into a continuously
1598
+ * updating map. An I-frame (re)seeds the merge base and renders standalone; a
1599
+ * P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
1600
+ * grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
1601
+ * out-of-order P-frame or a map-id change the base is dropped and `null`
1602
+ * returned — the next I-frame re-seeds. Returns `null` when no frame is
1603
+ * advertised yet, or a P-frame arrives before any I-frame.
1604
+ *
1605
+ * Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
1606
+ * map-watching consumer (e.g. the camstack map child) gets a fresh complete
1607
+ * frame on every push during a live clean.
1608
+ */
1609
+ fetchLatestMapStreaming(opts?: Omit<VacuumGetMapInput, 'filename'>): Promise<VacuumMap | null>;
1594
1610
  /** Props worth seeding on start() / polling — exported for the facade. */
1595
1611
  static readonly DEFAULT_PROPS: readonly [{
1596
1612
  readonly siid: 2;
@@ -1809,6 +1825,30 @@ interface MowerControlState {
1809
1825
  statusCode: number | null;
1810
1826
  zones: number[][];
1811
1827
  }
1828
+ /** Canonical CMS consumable counter keys (blade, brush, robot maintenance). */
1829
+ type MowerConsumableKey = 'blade' | 'brush' | 'maintenance';
1830
+ /** One CMS consumable counter: minutes used + total + remaining %. */
1831
+ interface MowerConsumableReading {
1832
+ readonly key: MowerConsumableKey;
1833
+ /** Minutes of run-time accrued on this counter (counts UP). */
1834
+ readonly usedMinutes: number;
1835
+ /** Counter's full-life duration in minutes. */
1836
+ readonly totalMinutes: number;
1837
+ /** Remaining life percentage (0..100, one decimal). */
1838
+ readonly remainingPercent: number;
1839
+ }
1840
+ /** Map a consumable key (incl. aliases) to its CMS counter index. */
1841
+ declare function mowerConsumableIndex(item: string): number | null;
1842
+ /**
1843
+ * Extract the raw `[blade, brush, robot]` minute counters from a CMS getter
1844
+ * result, or null if the response is malformed. Cast-free, no throw.
1845
+ */
1846
+ declare function extractMowerConsumableValues(result: unknown): number[] | null;
1847
+ /**
1848
+ * Parse a CMS getter result into typed consumable readings (blade/brush/
1849
+ * maintenance) with remaining %. Returns null on a malformed response.
1850
+ */
1851
+ declare function parseMowerConsumables(result: unknown): readonly MowerConsumableReading[] | null;
1812
1852
 
1813
1853
  /**
1814
1854
  * Per-model mower capability records + a resolver adapting the rich record into
@@ -2047,6 +2087,27 @@ declare class MowerDevice extends BaseDevice {
2047
2087
  startMowingEdges(contourIds: number[][]): Promise<unknown>;
2048
2088
  /** Spot mowing (2:50 o:103). */
2049
2089
  startMowingSpots(spotAreaIds: number[]): Promise<unknown>;
2090
+ /**
2091
+ * Read the raw CMS consumable counters `[blade, brush, robot]` (minutes used),
2092
+ * via the SCHEDULING_TASK (2:50) custom-action getter `{m:'g',t:'CMS'}`. This
2093
+ * is a LIVE device action (it wakes the mower); rejects with
2094
+ * {@link DreameDeviceOfflineError} when the mower is unreachable. Returns null
2095
+ * if the response is malformed.
2096
+ */
2097
+ getConsumableValues(): Promise<readonly number[] | null>;
2098
+ /**
2099
+ * Read the CMS consumables as typed readings (blade/brush/maintenance) with
2100
+ * remaining %. LIVE action — see {@link getConsumableValues}. Returns null on
2101
+ * a malformed response.
2102
+ */
2103
+ getConsumables(): Promise<readonly MowerConsumableReading[] | null>;
2104
+ /**
2105
+ * Reset one CMS consumable counter to zero. Reads the current counters, zeroes
2106
+ * the selected one (leaving the others), and writes them back via the
2107
+ * `{m:'s',t:'CMS',d:{value:[…]}}` setter. LIVE action. Throws on an unknown
2108
+ * item or when the current counters cannot be read.
2109
+ */
2110
+ resetConsumable(item: MowerConsumableKey): Promise<void>;
2050
2111
  /**
2051
2112
  * Seed the cache from the CLOUD SHADOW (last-known values) WITHOUT waking the
2052
2113
  * mower — reads {@link MowerDevice.DEFAULT_PROPS} from the cloud-cached
@@ -2432,4 +2493,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
2432
2493
  */
2433
2494
  declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
2434
2495
 
2435
- 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 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, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
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 };
package/dist/index.d.ts CHANGED
@@ -1591,6 +1591,22 @@ declare class VacuumDevice extends BaseDevice<VacuumDeviceEvents> {
1591
1591
  * `applyVacuumPFrame` merge primitive ships separately for that work.
1592
1592
  */
1593
1593
  getMap(input: VacuumGetMapInput): Promise<VacuumMap>;
1594
+ /** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
1595
+ resetMapStream(): void;
1596
+ /**
1597
+ * Fetch the latest advertised map frame and fold it into a continuously
1598
+ * updating map. An I-frame (re)seeds the merge base and renders standalone; a
1599
+ * P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
1600
+ * grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
1601
+ * out-of-order P-frame or a map-id change the base is dropped and `null`
1602
+ * returned — the next I-frame re-seeds. Returns `null` when no frame is
1603
+ * advertised yet, or a P-frame arrives before any I-frame.
1604
+ *
1605
+ * Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
1606
+ * map-watching consumer (e.g. the camstack map child) gets a fresh complete
1607
+ * frame on every push during a live clean.
1608
+ */
1609
+ fetchLatestMapStreaming(opts?: Omit<VacuumGetMapInput, 'filename'>): Promise<VacuumMap | null>;
1594
1610
  /** Props worth seeding on start() / polling — exported for the facade. */
1595
1611
  static readonly DEFAULT_PROPS: readonly [{
1596
1612
  readonly siid: 2;
@@ -1809,6 +1825,30 @@ interface MowerControlState {
1809
1825
  statusCode: number | null;
1810
1826
  zones: number[][];
1811
1827
  }
1828
+ /** Canonical CMS consumable counter keys (blade, brush, robot maintenance). */
1829
+ type MowerConsumableKey = 'blade' | 'brush' | 'maintenance';
1830
+ /** One CMS consumable counter: minutes used + total + remaining %. */
1831
+ interface MowerConsumableReading {
1832
+ readonly key: MowerConsumableKey;
1833
+ /** Minutes of run-time accrued on this counter (counts UP). */
1834
+ readonly usedMinutes: number;
1835
+ /** Counter's full-life duration in minutes. */
1836
+ readonly totalMinutes: number;
1837
+ /** Remaining life percentage (0..100, one decimal). */
1838
+ readonly remainingPercent: number;
1839
+ }
1840
+ /** Map a consumable key (incl. aliases) to its CMS counter index. */
1841
+ declare function mowerConsumableIndex(item: string): number | null;
1842
+ /**
1843
+ * Extract the raw `[blade, brush, robot]` minute counters from a CMS getter
1844
+ * result, or null if the response is malformed. Cast-free, no throw.
1845
+ */
1846
+ declare function extractMowerConsumableValues(result: unknown): number[] | null;
1847
+ /**
1848
+ * Parse a CMS getter result into typed consumable readings (blade/brush/
1849
+ * maintenance) with remaining %. Returns null on a malformed response.
1850
+ */
1851
+ declare function parseMowerConsumables(result: unknown): readonly MowerConsumableReading[] | null;
1812
1852
 
1813
1853
  /**
1814
1854
  * Per-model mower capability records + a resolver adapting the rich record into
@@ -2047,6 +2087,27 @@ declare class MowerDevice extends BaseDevice {
2047
2087
  startMowingEdges(contourIds: number[][]): Promise<unknown>;
2048
2088
  /** Spot mowing (2:50 o:103). */
2049
2089
  startMowingSpots(spotAreaIds: number[]): Promise<unknown>;
2090
+ /**
2091
+ * Read the raw CMS consumable counters `[blade, brush, robot]` (minutes used),
2092
+ * via the SCHEDULING_TASK (2:50) custom-action getter `{m:'g',t:'CMS'}`. This
2093
+ * is a LIVE device action (it wakes the mower); rejects with
2094
+ * {@link DreameDeviceOfflineError} when the mower is unreachable. Returns null
2095
+ * if the response is malformed.
2096
+ */
2097
+ getConsumableValues(): Promise<readonly number[] | null>;
2098
+ /**
2099
+ * Read the CMS consumables as typed readings (blade/brush/maintenance) with
2100
+ * remaining %. LIVE action — see {@link getConsumableValues}. Returns null on
2101
+ * a malformed response.
2102
+ */
2103
+ getConsumables(): Promise<readonly MowerConsumableReading[] | null>;
2104
+ /**
2105
+ * Reset one CMS consumable counter to zero. Reads the current counters, zeroes
2106
+ * the selected one (leaving the others), and writes them back via the
2107
+ * `{m:'s',t:'CMS',d:{value:[…]}}` setter. LIVE action. Throws on an unknown
2108
+ * item or when the current counters cannot be read.
2109
+ */
2110
+ resetConsumable(item: MowerConsumableKey): Promise<void>;
2050
2111
  /**
2051
2112
  * Seed the cache from the CLOUD SHADOW (last-known values) WITHOUT waking the
2052
2113
  * mower — reads {@link MowerDevice.DEFAULT_PROPS} from the cloud-cached
@@ -2432,4 +2493,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
2432
2493
  */
2433
2494
  declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
2434
2495
 
2435
- 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 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, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
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 };
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.9.0";
3
+ var LIBRARY_VERSION = "1.11.0";
4
4
 
5
5
  // src/transport/errors.ts
6
6
  var DreameError = class extends Error {
@@ -2552,6 +2552,157 @@ function decodeCleanedAreaPixels(pixels, width, height) {
2552
2552
  return { cleaned, dirty };
2553
2553
  }
2554
2554
 
2555
+ // src/models/vacuum/map/merge.ts
2556
+ var OutOfOrderFrameError = class extends Error {
2557
+ expectedFrameId;
2558
+ actualFrameId;
2559
+ constructor(expectedFrameId, actualFrameId) {
2560
+ super(`map: out-of-order P-frame (expected frame_id=${expectedFrameId}, got ${actualFrameId})`);
2561
+ this.name = "OutOfOrderFrameError";
2562
+ this.expectedFrameId = expectedFrameId;
2563
+ this.actualFrameId = actualFrameId;
2564
+ }
2565
+ };
2566
+ function mergePFrame(prevInflated, pFrameInflated) {
2567
+ const prevHeader = parseMapHeader(prevInflated);
2568
+ const pHeader = parseMapHeader(pFrameInflated);
2569
+ if (pHeader.frameType !== "P") {
2570
+ throw new MapDecodeError(`mergePFrame: expected P-frame, got frame_type=${pHeader.frameType}`);
2571
+ }
2572
+ if (pHeader.mapId !== prevHeader.mapId) {
2573
+ throw new MapDecodeError(
2574
+ `mergePFrame: map_id mismatch (prev=${prevHeader.mapId}, P=${pHeader.mapId}) \u2014 request a fresh I-frame`
2575
+ );
2576
+ }
2577
+ if (pHeader.frameId !== prevHeader.frameId + 1) {
2578
+ throw new OutOfOrderFrameError(prevHeader.frameId + 1, pHeader.frameId);
2579
+ }
2580
+ if (prevHeader.gridSize !== pHeader.gridSize && pHeader.width > 0 && pHeader.height > 0) {
2581
+ throw new MapDecodeError(
2582
+ `mergePFrame: grid_size changed mid-stream (${prevHeader.gridSize} \u2192 ${pHeader.gridSize})`
2583
+ );
2584
+ }
2585
+ const prevTail = parseMapJsonTail(sliceTailText(prevInflated, prevHeader));
2586
+ const pTail = parseMapJsonTail(sliceTailText(pFrameInflated, pHeader));
2587
+ const prevLeft = prevTail.origin?.[0] ?? prevHeader.left;
2588
+ const prevTop = prevTail.origin?.[1] ?? prevHeader.top;
2589
+ const grid = prevHeader.gridSize;
2590
+ const prevRight = prevLeft + prevHeader.width * grid;
2591
+ const prevBottom = prevTop + prevHeader.height * grid;
2592
+ const hasPixelDelta = pHeader.width > 0 && pHeader.height > 0;
2593
+ let unionLeft = prevLeft;
2594
+ let unionTop = prevTop;
2595
+ let unionWidth = prevHeader.width;
2596
+ let unionHeight = prevHeader.height;
2597
+ let pLeft = 0;
2598
+ let pTop = 0;
2599
+ if (hasPixelDelta) {
2600
+ pLeft = pTail.origin?.[0] ?? pHeader.left;
2601
+ pTop = pTail.origin?.[1] ?? pHeader.top;
2602
+ const pRight = pLeft + pHeader.width * grid;
2603
+ const pBottom = pTop + pHeader.height * grid;
2604
+ if ((pLeft - prevLeft) % grid !== 0 || (pTop - prevTop) % grid !== 0) {
2605
+ throw new MapDecodeError(
2606
+ `mergePFrame: P-frame origin not aligned to prev grid (offset=${pLeft - prevLeft},${pTop - prevTop} vs grid=${grid})`
2607
+ );
2608
+ }
2609
+ unionLeft = Math.min(prevLeft, pLeft);
2610
+ unionTop = Math.min(prevTop, pTop);
2611
+ const unionRight = Math.max(prevRight, pRight);
2612
+ const unionBottom = Math.max(prevBottom, pBottom);
2613
+ unionWidth = (unionRight - unionLeft) / grid;
2614
+ unionHeight = (unionBottom - unionTop) / grid;
2615
+ }
2616
+ const newPixels = Buffer.alloc(unionWidth * unionHeight);
2617
+ const prevPixelEnd = HEADER_SIZE + prevHeader.width * prevHeader.height;
2618
+ if (prevInflated.length < prevPixelEnd) {
2619
+ throw new MapDecodeError(
2620
+ `mergePFrame: prev buffer truncated (need ${prevPixelEnd} bytes for header+pixels, got ${prevInflated.length})`
2621
+ );
2622
+ }
2623
+ const prevPixels = prevInflated.subarray(HEADER_SIZE, prevPixelEnd);
2624
+ const prevDxPx = (prevLeft - unionLeft) / grid;
2625
+ const prevDyPx = (prevTop - unionTop) / grid;
2626
+ for (let y = 0; y < prevHeader.height; y++) {
2627
+ const srcOff = y * prevHeader.width;
2628
+ const dstOff = (prevDyPx + y) * unionWidth + prevDxPx;
2629
+ prevPixels.copy(newPixels, dstOff, srcOff, srcOff + prevHeader.width);
2630
+ }
2631
+ if (hasPixelDelta) {
2632
+ const pPixelEnd = HEADER_SIZE + pHeader.width * pHeader.height;
2633
+ if (pFrameInflated.length < pPixelEnd) {
2634
+ throw new MapDecodeError(
2635
+ `mergePFrame: P buffer truncated (need ${pPixelEnd} bytes for header+pixels, got ${pFrameInflated.length})`
2636
+ );
2637
+ }
2638
+ const pPixels = pFrameInflated.subarray(HEADER_SIZE, pPixelEnd);
2639
+ const pDxPx = (pLeft - unionLeft) / grid;
2640
+ const pDyPx = (pTop - unionTop) / grid;
2641
+ for (let y = 0; y < pHeader.height; y++) {
2642
+ const dstRow = (pDyPx + y) * unionWidth + pDxPx;
2643
+ const srcRow = y * pHeader.width;
2644
+ for (let x = 0; x < pHeader.width; x++) {
2645
+ newPixels[dstRow + x] = newPixels[dstRow + x] + pPixels[srcRow + x] & 255;
2646
+ }
2647
+ }
2648
+ }
2649
+ const newHeader = Buffer.alloc(HEADER_SIZE);
2650
+ newHeader.writeInt16LE(prevHeader.mapId, 0);
2651
+ newHeader.writeInt16LE(pHeader.frameId, 2);
2652
+ newHeader[4] = FRAME_TYPE.I;
2653
+ newHeader.writeInt16LE(pHeader.robotX, 5);
2654
+ newHeader.writeInt16LE(pHeader.robotY, 7);
2655
+ newHeader.writeInt16LE(pHeader.robotA, 9);
2656
+ newHeader.writeInt16LE(pHeader.chargerX, 11);
2657
+ newHeader.writeInt16LE(pHeader.chargerY, 13);
2658
+ newHeader.writeInt16LE(pHeader.chargerA, 15);
2659
+ newHeader.writeInt16LE(grid, 17);
2660
+ newHeader.writeInt16LE(unionWidth, 19);
2661
+ newHeader.writeInt16LE(unionHeight, 21);
2662
+ newHeader.writeInt16LE(unionLeft, 23);
2663
+ newHeader.writeInt16LE(unionTop, 25);
2664
+ const mergedTail = mergeTails(prevTail, pTail, unionLeft, unionTop);
2665
+ const tailBytes = Buffer.from(JSON.stringify(mergedTail), "utf8");
2666
+ return Buffer.concat([newHeader, newPixels, tailBytes]);
2667
+ }
2668
+ function mergePFrameEnvelope(prev, pframe, prevOpts, pframeOpts) {
2669
+ const prevBuf = typeof prev === "string" ? unwrapEnvelope(prev, prevOpts) : prev;
2670
+ const pBuf = typeof pframe === "string" ? unwrapEnvelope(pframe, pframeOpts) : pframe;
2671
+ return mergePFrame(prevBuf, pBuf);
2672
+ }
2673
+ function mergeTails(prev, p, unionLeft, unionTop) {
2674
+ const merged = { ...p };
2675
+ merged.origin = [unionLeft, unionTop];
2676
+ const prevTr = typeof prev.tr === "string" ? prev.tr : "";
2677
+ const pTr = typeof p.tr === "string" ? p.tr : "";
2678
+ if (prevTr || pTr) {
2679
+ merged.tr = prevTr + pTr;
2680
+ } else {
2681
+ delete merged.tr;
2682
+ }
2683
+ if (!("seg_inf" in p) && "seg_inf" in prev) {
2684
+ merged.seg_inf = prev.seg_inf;
2685
+ }
2686
+ if (!("sa" in p) && "sa" in prev) {
2687
+ merged.sa = prev.sa;
2688
+ }
2689
+ for (const key2 of PERSISTENT_TAIL_KEYS) {
2690
+ if (!(key2 in p) && key2 in prev) {
2691
+ merged[key2] = prev[key2];
2692
+ }
2693
+ }
2694
+ return merged;
2695
+ }
2696
+ var PERSISTENT_TAIL_KEYS = [
2697
+ "vw",
2698
+ "vws",
2699
+ "sneak_areas",
2700
+ "sneak_areas_end",
2701
+ "walls_info",
2702
+ "rism",
2703
+ "decmap"
2704
+ ];
2705
+
2555
2706
  // src/models/vacuum/map/decode.ts
2556
2707
  function decodeVacuumMap(input, opts = {}) {
2557
2708
  const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
@@ -2596,6 +2747,10 @@ function decodeVacuumMap(input, opts = {}) {
2596
2747
  cleanedArea
2597
2748
  };
2598
2749
  }
2750
+ function applyVacuumPFrame(prev, pframe, opts = {}) {
2751
+ const merged = typeof prev === "string" || typeof pframe === "string" ? mergePFrameEnvelope(prev, pframe, opts.prev, opts.pframe) : mergePFrame(prev, pframe);
2752
+ return { buffer: merged, data: decodeVacuumMap(merged) };
2753
+ }
2599
2754
  function mergeDimensions(header, tail) {
2600
2755
  const left = tail.origin?.[0] ?? header.left;
2601
2756
  const top = tail.origin?.[1] ?? header.top;
@@ -3533,6 +3688,14 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
3533
3688
  */
3534
3689
  async getMap(input) {
3535
3690
  this.#requireCap(this.#caps.canMap, "getMap", "map decoding");
3691
+ const blob = await this.#fetchMapBlob(input);
3692
+ const map = decodeVacuumMap(blob, this.#decodeOpts(input));
3693
+ this.#lastMap = map;
3694
+ this.emit("map", map);
3695
+ return map;
3696
+ }
3697
+ /** Fetch the raw OSS blob (still the base64+zlib envelope) for a map frame. */
3698
+ async #fetchMapBlob(input) {
3536
3699
  const session = this.currentSession();
3537
3700
  const region = this.region;
3538
3701
  const fetcher = input.fetcher ?? new OssFetcher();
@@ -3546,14 +3709,77 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
3546
3709
  ...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
3547
3710
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3548
3711
  };
3549
- const blob = await fetcher.fetchBlob(fetchInput);
3550
- const map = decodeVacuumMap(blob, {
3712
+ return fetcher.fetchBlob(fetchInput);
3713
+ }
3714
+ /** The AES key/iv decode options, threaded from a {@link VacuumGetMapInput}. */
3715
+ #decodeOpts(input) {
3716
+ return {
3551
3717
  ...input.key !== void 0 ? { key: input.key } : {},
3552
3718
  ...input.iv !== void 0 ? { iv: input.iv } : {}
3553
- });
3554
- this.#lastMap = map;
3555
- this.emit("map", map);
3556
- return map;
3719
+ };
3720
+ }
3721
+ /**
3722
+ * The merge base for live P-frame streaming — always an INFLATED frame buffer.
3723
+ * An I-frame (re)seeds it; each P-frame merge replaces it with the merged
3724
+ * inflated buffer (so further P-frames stack). `null` until the first I-frame
3725
+ * (or after an out-of-order / map-id reset).
3726
+ */
3727
+ #mapStreamBase = null;
3728
+ /** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
3729
+ resetMapStream() {
3730
+ this.#mapStreamBase = null;
3731
+ }
3732
+ /** Inflate an OSS blob to a raw frame buffer (live envelope → zlib; verbatim if already inflated). */
3733
+ #inflateFrame(blob, opts) {
3734
+ return looksLikeBase64Zlib(blob) ? unwrapEnvelope(blob.toString("latin1"), opts) : blob;
3735
+ }
3736
+ /**
3737
+ * Fetch the latest advertised map frame and fold it into a continuously
3738
+ * updating map. An I-frame (re)seeds the merge base and renders standalone; a
3739
+ * P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
3740
+ * grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
3741
+ * out-of-order P-frame or a map-id change the base is dropped and `null`
3742
+ * returned — the next I-frame re-seeds. Returns `null` when no frame is
3743
+ * advertised yet, or a P-frame arrives before any I-frame.
3744
+ *
3745
+ * Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
3746
+ * map-watching consumer (e.g. the camstack map child) gets a fresh complete
3747
+ * frame on every push during a live clean.
3748
+ */
3749
+ async fetchLatestMapStreaming(opts = {}) {
3750
+ this.#requireCap(this.#caps.canMap, "fetchLatestMapStreaming", "map decoding");
3751
+ const filename = this.mapFilename;
3752
+ if (filename === null) return null;
3753
+ const input = { filename, ...opts };
3754
+ const decodeOpts = this.#decodeOpts(input);
3755
+ const blob = await this.#fetchMapBlob(input);
3756
+ const inflated = this.#inflateFrame(blob, decodeOpts);
3757
+ const frame = decodeVacuumMap(inflated, decodeOpts);
3758
+ if (frame.frameType === "I") {
3759
+ this.#mapStreamBase = inflated;
3760
+ this.#lastMap = frame;
3761
+ this.emit("map", frame);
3762
+ return frame;
3763
+ }
3764
+ if (frame.frameType === "P") {
3765
+ if (this.#mapStreamBase === null) return null;
3766
+ try {
3767
+ const { buffer, data } = applyVacuumPFrame(this.#mapStreamBase, inflated);
3768
+ this.#mapStreamBase = buffer;
3769
+ this.#lastMap = data;
3770
+ this.emit("map", data);
3771
+ return data;
3772
+ } catch (err) {
3773
+ if (err instanceof OutOfOrderFrameError || err instanceof MapDecodeError) {
3774
+ this.#mapStreamBase = null;
3775
+ return null;
3776
+ }
3777
+ throw err;
3778
+ }
3779
+ }
3780
+ this.#lastMap = frame;
3781
+ this.emit("map", frame);
3782
+ return frame;
3557
3783
  }
3558
3784
  /** Props worth seeding on start() / polling — exported for the facade. */
3559
3785
  static DEFAULT_PROPS = [
@@ -3624,6 +3850,19 @@ function buildEdgePayload(contourIds) {
3624
3850
  function buildSpotPayload(spotAreaIds) {
3625
3851
  return { m: "a", p: 0, o: TASK_OPCODE.SPOT, d: { area: [...spotAreaIds] } };
3626
3852
  }
3853
+ function buildGetConsumablePayload() {
3854
+ return { m: "g", t: "CMS" };
3855
+ }
3856
+ function buildSetConsumablePayload(values) {
3857
+ const normalized = values.map((v) => Math.trunc(v));
3858
+ if (normalized.length !== 3) {
3859
+ throw new RangeError(`CMS values must contain exactly 3 counters; got ${normalized.length}`);
3860
+ }
3861
+ if (normalized.some((v) => v < 0)) {
3862
+ throw new RangeError(`CMS values cannot be negative; got ${JSON.stringify(normalized)}`);
3863
+ }
3864
+ return { m: "s", t: "CMS", d: { value: normalized } };
3865
+ }
3627
3866
 
3628
3867
  // src/models/mower/enums.ts
3629
3868
  var MowerStatus = /* @__PURE__ */ ((MowerStatus2) => {
@@ -3815,6 +4054,104 @@ function parseControlStatus(value) {
3815
4054
  zones
3816
4055
  };
3817
4056
  }
4057
+ var MOWER_CONSUMABLES = [
4058
+ { key: "blade", index: 0, totalMinutes: 6e3 },
4059
+ { key: "brush", index: 1, totalMinutes: 3e4 },
4060
+ { key: "maintenance", index: 2, totalMinutes: 3600 }
4061
+ ];
4062
+ function mowerConsumableIndex(item) {
4063
+ switch (item.trim().toLowerCase()) {
4064
+ case "blade":
4065
+ case "blades":
4066
+ return 0;
4067
+ case "brush":
4068
+ case "cleaning_brush":
4069
+ return 1;
4070
+ case "robot":
4071
+ case "maintenance":
4072
+ case "robot_maintenance":
4073
+ return 2;
4074
+ default:
4075
+ return null;
4076
+ }
4077
+ }
4078
+ function toInt(v) {
4079
+ if (typeof v === "number" && Number.isFinite(v)) {
4080
+ return Math.trunc(v);
4081
+ }
4082
+ if (typeof v === "string") {
4083
+ const n = Number(v);
4084
+ return Number.isFinite(n) ? Math.trunc(n) : null;
4085
+ }
4086
+ return null;
4087
+ }
4088
+ function extractCustomActionData(result) {
4089
+ if (!isRecord2(result)) {
4090
+ return null;
4091
+ }
4092
+ if (Array.isArray(result["value"])) {
4093
+ return result;
4094
+ }
4095
+ if (isRecord2(result["d"])) {
4096
+ return result["d"];
4097
+ }
4098
+ const out = result["out"];
4099
+ if (!Array.isArray(out)) {
4100
+ return null;
4101
+ }
4102
+ for (const entry of out) {
4103
+ if (!isRecord2(entry)) {
4104
+ continue;
4105
+ }
4106
+ const r = entry["r"];
4107
+ const code = entry["code"];
4108
+ const rError = r !== void 0 && r !== null && r !== 0;
4109
+ const codeError = code !== void 0 && code !== null && code !== 0;
4110
+ if (rError && codeError) {
4111
+ continue;
4112
+ }
4113
+ if (isRecord2(entry["d"])) {
4114
+ return entry["d"];
4115
+ }
4116
+ }
4117
+ return null;
4118
+ }
4119
+ function extractMowerConsumableValues(result) {
4120
+ const data = extractCustomActionData(result);
4121
+ if (data === null) {
4122
+ return null;
4123
+ }
4124
+ const values = data["value"];
4125
+ if (!Array.isArray(values) || values.length < 3) {
4126
+ return null;
4127
+ }
4128
+ const out = [];
4129
+ for (const v of values.slice(0, 3)) {
4130
+ const n = toInt(v);
4131
+ if (n === null) {
4132
+ return null;
4133
+ }
4134
+ out.push(n);
4135
+ }
4136
+ return out;
4137
+ }
4138
+ function parseMowerConsumables(result) {
4139
+ const values = extractMowerConsumableValues(result);
4140
+ if (values === null) {
4141
+ return null;
4142
+ }
4143
+ return MOWER_CONSUMABLES.map((c) => {
4144
+ const used = values[c.index] ?? 0;
4145
+ const remaining = c.totalMinutes - used;
4146
+ const pct = Math.max(0, Math.min(100, Math.round(remaining / c.totalMinutes * 1e3) / 10));
4147
+ return {
4148
+ key: c.key,
4149
+ usedMinutes: used,
4150
+ totalMinutes: c.totalMinutes,
4151
+ remainingPercent: pct
4152
+ };
4153
+ });
4154
+ }
3818
4155
 
3819
4156
  // src/models/mower/capabilities.ts
3820
4157
  var FALLBACK2 = {
@@ -4580,6 +4917,56 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
4580
4917
  }
4581
4918
  return this.#sendTask(buildSpotPayload(spotAreaIds.map((s) => Math.trunc(s))));
4582
4919
  }
4920
+ // -- CMS consumables ----------------------------------------------------
4921
+ /**
4922
+ * Read the raw CMS consumable counters `[blade, brush, robot]` (minutes used),
4923
+ * via the SCHEDULING_TASK (2:50) custom-action getter `{m:'g',t:'CMS'}`. This
4924
+ * is a LIVE device action (it wakes the mower); rejects with
4925
+ * {@link DreameDeviceOfflineError} when the mower is unreachable. Returns null
4926
+ * if the response is malformed.
4927
+ */
4928
+ async getConsumableValues() {
4929
+ const result = await this.callAction(
4930
+ MOWER_PROP.SCHEDULING_TASK.siid,
4931
+ MOWER_PROP.SCHEDULING_TASK.piid,
4932
+ [buildGetConsumablePayload()]
4933
+ );
4934
+ return extractMowerConsumableValues(result);
4935
+ }
4936
+ /**
4937
+ * Read the CMS consumables as typed readings (blade/brush/maintenance) with
4938
+ * remaining %. LIVE action — see {@link getConsumableValues}. Returns null on
4939
+ * a malformed response.
4940
+ */
4941
+ async getConsumables() {
4942
+ const result = await this.callAction(
4943
+ MOWER_PROP.SCHEDULING_TASK.siid,
4944
+ MOWER_PROP.SCHEDULING_TASK.piid,
4945
+ [buildGetConsumablePayload()]
4946
+ );
4947
+ return parseMowerConsumables(result);
4948
+ }
4949
+ /**
4950
+ * Reset one CMS consumable counter to zero. Reads the current counters, zeroes
4951
+ * the selected one (leaving the others), and writes them back via the
4952
+ * `{m:'s',t:'CMS',d:{value:[…]}}` setter. LIVE action. Throws on an unknown
4953
+ * item or when the current counters cannot be read.
4954
+ */
4955
+ async resetConsumable(item) {
4956
+ const index = mowerConsumableIndex(item);
4957
+ if (index === null) {
4958
+ throw new DreameError(`resetConsumable: unknown consumable item "${item}"`);
4959
+ }
4960
+ const current = await this.getConsumableValues();
4961
+ if (current === null) {
4962
+ throw new DreameError("resetConsumable: failed to read current CMS counters");
4963
+ }
4964
+ const next = [...current];
4965
+ next[index] = 0;
4966
+ await this.callAction(MOWER_PROP.SCHEDULING_TASK.siid, MOWER_PROP.SCHEDULING_TASK.piid, [
4967
+ buildSetConsumablePayload(next)
4968
+ ]);
4969
+ }
4583
4970
  /**
4584
4971
  * Seed the cache from the CLOUD SHADOW (last-known values) WITHOUT waking the
4585
4972
  * mower — reads {@link MowerDevice.DEFAULT_PROPS} from the cloud-cached
@@ -5281,9 +5668,12 @@ export {
5281
5668
  decodeAutoSwitchAll,
5282
5669
  encodeAiFeatureWrite,
5283
5670
  encodeAutoSwitchWrite,
5671
+ extractMowerConsumableValues,
5284
5672
  getMowerCapabilities,
5285
5673
  getVacuumCapabilities,
5286
5674
  isDreameConsumableKey,
5675
+ mowerConsumableIndex,
5676
+ parseMowerConsumables,
5287
5677
  renderMowerSvg,
5288
5678
  renderVacuumPng,
5289
5679
  resolveCapabilities,