UNPKG

iobroker.roborock

Version:
1,173 lines (1,030 loc) 35.5 kB
import { MapManager } from "../../../map/MapManager"; import { RoborockLocales } from "../../../roborock_locales"; import { BaseDeviceFeatures, DeviceModelConfig, FeatureDependencies } from "../../baseDeviceFeatures"; import { Feature } from "../../features.enum"; import { ADAPTER_ERROR_MAPPING } from "../adapterErrorMapping"; import { B01ConsumableService } from "../services/B01ConsumableService"; import { B01ControlService } from "../services/B01ControlService"; import { B01MapService } from "../services/B01MapService"; import { StationService } from "../services/StationService"; import { VACUUM_CONSTANTS } from "../vacuumConstants"; import type { B01Variant } from "../../../b01Variant"; import type { B01DeviceStatus } from "../../../map/b01/types"; import deviceDataSet = require("../../../../../lib/protocols/q7_dataset.json"); export class B01BaseVacuumFeatures extends BaseDeviceFeatures { // B01-specific properties protected mapManager: MapManager; protected locales: RoborockLocales; // Services protected consumableService: B01ConsumableService; protected stationService: StationService; protected lastMapUpdate = 0; protected mapService: B01MapService; protected controlService: B01ControlService; public readonly b01Variant: B01Variant; protected mappedRooms: Array<{ id: number; name: string }> | null = null; protected async cleanupVariantCommandObjects(): Promise<void> { return; } protected shouldPrecreateConsumableResetStates(): boolean { return true; } protected getVariantCommonDeviceStates(_attribute: string | number): Partial<ioBroker.StateCommon> | undefined { void _attribute; return undefined; } private async createConsumableResetStateObjects(): Promise<void> { await this.deps.ensureFolder(`Devices.${this.duid}.resetConsumables`); const resets: Record<string, string> = { "reset_main_brush": "Reset Main Brush", "reset_side_brush": "Reset Side Brush", "reset_filter": "Reset Filter" }; for (const [id, name] of Object.entries(resets)) { await this.deps.ensureState(`Devices.${this.duid}.resetConsumables.${id}`, { name: name, type: "boolean", role: "button", def: false, read: false, write: true }); } } protected async deleteObjectIfExists(id: string, recursive = false): Promise<void> { const existing = await this.deps.adapter.getObjectAsync(id); if (!existing) return; await this.deps.adapter.delObjectAsync(id, recursive ? { recursive: true } : undefined); } constructor( dependencies: FeatureDependencies, duid: string, robotModel: string, config: DeviceModelConfig, profile?: unknown, // Accept profile to match legacy subclasses, but ignore it b01Variant: B01Variant = "Q7" ) { super(dependencies, duid, robotModel, config); void profile; this.b01Variant = b01Variant; this.mapManager = this.deps.adapter.mapManager; this.locales = new RoborockLocales(deviceDataSet); this.consumableService = new B01ConsumableService(this.deps, this.duid); this.stationService = new StationService(this.deps, this.duid); // Initialize Services this.mapService = new B01MapService(dependencies, duid, (rooms) => this.setMappedRooms(rooms)); this.controlService = new B01ControlService(); this.deps.adapter.rLog("System", this.duid, "Info", "B01", undefined, `Constructing ${this.constructor.name} for ${robotModel} (${b01Variant})`, "info"); } /** * Configures the command set for B01 devices. * @see test/unit/features_specification.test.ts for the B01 command and property specification. */ public override async setupProtocolFeatures(): Promise<void> { this.deps.adapter.rLog("System", this.duid, "Debug", "B01", undefined, "Configuring B01 Command Set...", "debug"); // 1. CLEAR all inherited base commands. B01 uses its own protocol. this.commands = {}; // 2. Add properties for prop.get const properties = [ "wind", "water", "clean_mode", "status", "error_code", "battery", "clean_time", "clean_area", "map_status", "dock_status", "water_box_level_off", "dust_collection_status" ]; const propStates: Record<string, string> = {}; properties.forEach(p => propStates[p] = p); this.addCommand("prop.get", { type: "string", role: "text", name: "Property Get", def: "status", states: propStates }); // 3. Status Control (Buttons) this.addCommand("app_start", { type: "boolean", role: "button", name: this.deps.adapter.translations["app_start"] || "Start Cleaning", def: false }); this.addCommand("app_stop", { type: "boolean", role: "button", name: this.deps.adapter.translations["app_stop"] || "Stop", def: false }); this.addCommand("app_pause", { type: "boolean", role: "button", name: this.deps.adapter.translations["app_pause"] || "Pause Cleaning", def: false }); this.addCommand("app_charge", { type: "boolean", role: "button", name: this.deps.adapter.translations["app_charge"] || "Return to Dock", def: false }); this.addCommand("find_me", { type: "boolean", role: "button", name: this.deps.adapter.translations["find_me"] || "Find Me", def: false }); // 3b. Segment / Room cleaning (B01: service.set_room_clean with room_ids) this.addCommand("app_segment_clean", { type: "boolean", role: "button", name: this.deps.adapter.translations["app_segment_clean"] || "Segment Cleaning", def: false }); // 4. Fan Power (wind) this.addCommand("wind", { type: "number", role: "value", name: this.locales.getNameAll("wind"), def: 2, states: { 1: "Quiet", 2: "Balanced", 3: "Turbo", 4: "Max", 5: "Max+" } }); // 5. Water Level (water) this.addCommand("water", { type: "number", role: "value", name: this.locales.getNameAll("water"), def: 1, states: { 1: "Low", 2: "Medium", 3: "High" } }); this.addCommand("clean_path_preference", { type: "number", role: "value", name: this.locales.getNameAll("clean_path_preference"), def: 0, states: { 0: "Standard", 1: "Fast", 2: "Deep" } }); // 12. Update Map this.addCommand("update_map", { type: "boolean", role: "button", name: "Update Map", def: false }); // 13. Consumable Resets are handled in createCommandObjects and initializeDeviceData // 14. Additional B01 Commands this.addCommand("child_lock", { type: "boolean", role: "switch", name: this.locales.getNameAll("child_lock"), def: false }); this.addCommand("carpet_turbo", { type: "boolean", role: "switch", name: this.locales.getNameAll("carpet_turbo"), def: false }); this.addCommand("light_mode", { type: "boolean", role: "switch", name: this.locales.getNameAll("light_mode"), def: true }); this.addCommand("green_laser", { type: "boolean", role: "switch", name: this.locales.getNameAll("green_laser"), def: true }); this.addCommand("repeat_state", { type: "number", role: "value", name: this.locales.getNameAll("repeat_state"), def: 0, states: { 0: "Off", 1: "On" } // Assuming 0/1 typical for repeat }); // 10. Robot Mode (Verified in logs: 0=Vacuum, 1=Vac&Mop, 2=Mop) this.addCommand("mode", { type: "number", role: "value", name: this.locales.getNameAll("mode"), def: 0, states: { 0: "Vacuum", 1: "Vacuum & Mop", 2: "Mop" } }); const cmds = Object.keys(this.commands); this.deps.adapter.rLog("System", this.duid, "Info", "B01", undefined, `B01 Protocol Enforced. Commands in memory: ${cmds.join(", ")}`, "info"); } public override async createCommandObjects(): Promise<void> { await this.cleanupVariantCommandObjects(); await super.createCommandObjects(); if (this.shouldPrecreateConsumableResetStates()) { await this.createConsumableResetStateObjects(); } } /** * Allows feature handlers to provide/modify parameters for a command before sending. * B01 uses this to map individual command states to prop.set or service calls. */ public override async getCommandParams(method: string, params?: unknown, id?: string): Promise<unknown> { void id; if (method === "app_segment_clean") { const namespace = this.deps.adapter.namespace; const currentMapIdState = await this.deps.adapter.getStateAsync(`Devices.${this.duid}.deviceStatus.current_map_id`); const currentMapId = (currentMapIdState && typeof currentMapIdState.val === "number" && currentMapIdState.val > 0) ? currentMapIdState.val : null; const pattern = currentMapId != null ? `${namespace}.Devices.${this.duid}.floors.${currentMapId}.*` : `${namespace}.Devices.${this.duid}.floors.*.*`; const states = await this.deps.adapter.getStatesAsync(pattern); const roomIds: number[] = []; const nonRoomKeys = new Set(["name", "mapFlag", "load", "map_id"]); if (states) { for (const [stateId, state] of Object.entries(states)) { if (!state || (state.val !== true && state.val !== "true" && state.val !== 1)) continue; const parts = stateId.split("."); const lastSegment = parts[parts.length - 1]; if (nonRoomKeys.has(lastSegment)) continue; const rid = Number(lastSegment); if (!isNaN(rid) && rid >= 0 && !roomIds.includes(rid)) { roomIds.push(rid); } } } if (roomIds.length > 0) { this.deps.adapter.rLog("System", this.duid, "Info", "B01", undefined, `Starting room cleaning for rooms: ${roomIds.join(", ")}`, "info"); } else { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, "No rooms selected for segment cleaning. Start full clean (room_ids: []).", "warn"); } return { method: "service.set_room_clean", params: { clean_type: 0, ctrl_value: 1, room_ids: roomIds } }; } // Delegate all other command parameter mapping to the Control Service return this.controlService.getCommandParams(method, params); } public override async initializeDeviceData(): Promise<void> { await this.updateStatus(); await this.updateMap(); await this.updateMultiMapsList(); await this.updateRoomMapping(); await this.deps.adapter.checkForNewFirmware(this.duid); await Promise.all([ this.updateFirmwareFeatures(), this.updateExtraStatus(), this.updateNetworkInfo(), this.updateTimers() ]); } public override async updateMultiMapsList(): Promise<void> { if (this.mapService) { await this.mapService.updateMultiMapsList(); } } public async updateRoomMapping(): Promise<void> { if (this.mappedRooms && this.mapService) { await this.mapService.updateRoomMapping(this.mappedRooms, { refreshFloors: true }); } } public setMappedRooms(rooms: Array<{ id: number; name: string }>): void { this.mappedRooms = rooms; // When map arrives async, create room selection states under current floor void this.updateRoomMapping(); } public override async updateStatus(): Promise<void> { const props = VACUUM_CONSTANTS.b01StatusProps; let resultObj: Record<string, any> | undefined; try { const result = await this.deps.adapter.requestsHandler.sendRequest(this.duid, "prop.get", { property: props }); if (Array.isArray(result) && result.length === props.length) { resultObj = {}; props.forEach((key: string, index: number) => { resultObj![key] = result[index]; }); } else if (typeof result === "object" && result !== null) { resultObj = result; } if (resultObj) { if (!this.runtimeDetectionComplete) { await this.detectAndApplyRuntimeFeatures(resultObj); } await this.processStatus(resultObj); const c = await this.deps.adapter.getStateAsync(`Devices.${this.duid}.cleaningInfo.clean_count`); this.deps.adapter.rLog("System", this.duid, "Debug", "B01", undefined, `status=${resultObj.status ?? "?"}, clean_count=${c?.val ?? "?"}`, "debug"); } } catch (e: any) { this.deps.adapter.rLog("System", this.duid, "Warn", undefined, undefined, `Failed to update status (B01): ${e.message}`, "warn"); throw e; } } public override async updateConsumables(data?: unknown): Promise<void> { await this.consumableService.updateConsumables(data); } public override async updateTimers(): Promise<void> { await super.updateTimers(); } // Override processStatus to apply B01 specific conversions (dm² to m²) protected async processStatus(resultObj: Record<string, unknown>): Promise<void> { this.updateMapManagerRuntimeStatus(resultObj); // Handle docking station status separately const dssValue = resultObj["dss"]; const washStatus = resultObj["wash_status"]; const washPhase = resultObj["wash_phase"]; if (dssValue !== undefined || washStatus !== undefined || washPhase !== undefined) { if (dssValue !== undefined) delete resultObj["dss"]; await this.updateDockingStationStatus({ dss: dssValue !== undefined ? Number(dssValue) : undefined, washStatus: washStatus !== undefined ? Number(washStatus) : undefined, washPhase: washPhase !== undefined ? Number(washPhase) : undefined }); } // Map B01 specific keys to standard ioBroker states for compatibility await this.deps.ensureFolder(`Devices.${this.duid}.deviceStatus`); for (const key in resultObj) { await this.processStatusProperty(key, resultObj[key]); } } private updateMapManagerRuntimeStatus(resultObj: Readonly<Record<string, unknown>>): void { const nextStatus: Partial<B01DeviceStatus> = {}; const stateValue = resultObj.status ?? resultObj.state; const workModeValue = resultObj.work_mode ?? resultObj.workMode ?? resultObj.clean_task_type; const cleanModeValue = resultObj.clean_mode ?? resultObj.cleanMode ?? resultObj.mode; const faultValue = resultObj.fault ?? resultObj.error_code; const dustCollectValue = resultObj.dust_action ?? resultObj.dust_collection_status; const batteryValue = resultObj.battery; if (stateValue !== undefined && stateValue !== null) nextStatus.deviceState = Number(stateValue); if (workModeValue !== undefined && workModeValue !== null) nextStatus.deviceWorkMode = Number(workModeValue); if (cleanModeValue !== undefined && cleanModeValue !== null) nextStatus.deviceCleanMode = Number(cleanModeValue); if (faultValue !== undefined && faultValue !== null) nextStatus.deviceFault = Number(faultValue); if (batteryValue !== undefined && batteryValue !== null) nextStatus.deviceBattery = Number(batteryValue); if (dustCollectValue !== undefined && dustCollectValue !== null) { nextStatus.isDustCollect = dustCollectValue === 1 || dustCollectValue === true || dustCollectValue === "1"; } if (Object.keys(nextStatus).length > 0) { this.mapManager.updateB01DeviceStatus(this.duid, nextStatus); } } protected async processStatusProperty(key: string, inputVal: unknown): Promise<void> { let val = inputVal; if (typeof val === "string") { const trimmed = val.trim(); if ((trimmed.startsWith("{") || trimmed.startsWith("[")) && (trimmed.endsWith("}") || trimmed.endsWith("]"))) { try { val = JSON.parse(trimmed); } catch (e: any) { this.deps.adapter.rLog("System", this.duid, "Debug", this.protocolVersion || undefined, undefined, `Failed to parse JSON property ${key}: ${e.message}`, "debug"); } } } // Formatting for timestamps only (clean_finish is 0/1 flag, not a timestamp) if (key === "last_clean_t" && typeof val === "number") { val = this.deps.adapter.formatRoborockDate(val); } // Get definition or create default const def = this.getCommonDeviceStates(key); let type: ioBroker.CommonType = typeof val as ioBroker.CommonType; if (type === "object" && val !== null) type = "object"; if (key === "last_clean_t" && typeof val === "string") type = "string"; const common: any = def ? { ...def } : { name: key, type: type, role: "value", read: true, write: false }; // Enrich with defaults if missing if (!common.name) common.name = key; if (!common.role) common.role = "value"; if (common.read === undefined) common.read = true; if (common.write === undefined) common.write = false; // Manual Metadata Overrides - Removed hardcoded EN strings to use getNameAll translations if (key === "cleaning_time" || key === "real_clean_time" || key === "total_clean_time") { common.role = "value.interval"; common.unit = key === "cleaning_time" ? "min" : "s"; } // Serialize complex objects if (typeof val === "object" && val !== null) { val = JSON.stringify(val); } // B01 Area/Time Conversion if (["clean_time", "cleaning_time", "total_clean_time"].includes(key)) { // cleaning_time is in minutes; no conversion. // last_clean_t might be timestamp or duration? usually timestamp if clean_finish. const numericVal = Number(val as number | string); val = isNaN(numericVal) ? 0 : numericVal; } else if (["clean_area", "cleaning_area", "last_clean_area", "total_clean_area"].includes(key)) { // B01 sends dm² (e.g. 2129 -> 21.29 m²) const numericVal = Number(((val as number) / 100).toFixed(2)); val = isNaN(numericVal) ? 0 : numericVal; } if (common.type === "string" && typeof val !== "string") { val = String(val); } else if (common.type === "number" && typeof val !== "number") { val = Number(val); } else if (common.type === "boolean" && typeof val !== "boolean") { val = !!val; } // Use ensureState for optimized change detection to prevent write storms const stateId = `Devices.${this.duid}.deviceStatus.${key}`; await this.deps.ensureState(stateId, common); this.deps.adapter.setStateChanged(`Devices.${this.duid}.deviceStatus.${key}`, { val: val as ioBroker.StateValue, ack: true }); } public override async updateMap(): Promise<void> { await this.mapService.updateMap(); } public override async updateCleanSummary(): Promise<void> { await this.mapService.updateCleanSummary(); } public async getCleaningRecordMap(startTime: number, recordDetails?: unknown): Promise<{ mapBase64CleanUncropped: string; mapBase64: string; mapBase64Truncated: string; mapData: string } | null> { return this.mapService.getCleaningRecordMap(startTime, recordDetails); } protected async processCleanSummary(result: unknown): Promise<void> { await this.mapService.processCleanSummary(result); } @BaseDeviceFeatures.DeviceFeature(Feature.DockingStationStatus) protected async initDockingStationStatus(): Promise<void> { await this.stationService.initDockingStationStatus(); // B01 Specific Station States await this.deps.ensureState(`Devices.${this.duid}.dockingStationStatus.washingTaskStatus`, { name: "Washing Task Status", type: "number", role: "value", read: true, write: false, states: { "0": "Idle", "1": "Washing", "2": "Deep Washing", "3": "Drying", "4": "Completed", "5": "Paused", "6": "Error" } }); await this.deps.ensureState(`Devices.${this.duid}.dockingStationStatus.washingMode`, { name: "Washing Mode", type: "number", role: "value", read: true, write: false, states: { "0": "Standard", "1": "Deep" } }); } /** * Updates docking station status. Dynamic: if the device sends dss/washStatus/washPhase, * we ensure folder/states exist (lazy init) and update. No feature-guard. */ public async updateDockingStationStatus(status: { dss?: number, washStatus?: number, washPhase?: number }): Promise<void> { const hasAny = status.dss !== undefined || status.washStatus !== undefined || status.washPhase !== undefined; if (!hasAny) return; await this.initDockingStationStatus(); // idempotent: ensure folder + states (incl. B01 washingTaskStatus/washingMode) if (status.dss !== undefined) { await this.stationService.updateDockingStationStatus(status.dss); } if (status.washStatus !== undefined) { await this.deps.adapter.setStateChanged(`Devices.${this.duid}.dockingStationStatus.washingTaskStatus`, { val: status.washStatus, ack: true }); } if (status.washPhase !== undefined) { await this.deps.adapter.setStateChanged(`Devices.${this.duid}.dockingStationStatus.washingMode`, { val: status.washPhase, ack: true }); } } public override async detectAndApplyRuntimeFeatures(statusData: Readonly<Record<string, unknown>>): Promise<boolean> { let changed = false; if (statusData["dss"] !== undefined) { const dss = Number(statusData["dss"]); // DockingStationStatus: no applyFeature – folder/states created lazily in updateDockingStationStatus() // Bits 6-7: Dust bag status (0=not supported/missing) if (((dss >> 6) & 0b11) > 0) { await this.applyFeature(Feature.AutoEmptyDock); } // Bits 4-5: Dirty water tank status (0=not supported/missing) // Bits 10-11: Clean water tank status if (((dss >> 4) & 0b11) > 0 || ((dss >> 10) & 0b11) > 0) { await this.applyFeature(Feature.MopWash); } changed = true; } if (!this.runtimeDetectionComplete) { this.runtimeDetectionComplete = true; changed = true; } return changed; } protected override getDynamicFeatures(): Set<Feature> { return new Set<Feature>(); // B01 does not use bitfield feature flags } public override getCommonDeviceStates(attribute: string | number): Partial<ioBroker.StateCommon> | undefined { // Map B01 properties to readable states const lang = this.deps.adapter.language || "en"; const variantState = this.getVariantCommonDeviceStates(attribute); if (variantState) return variantState; // Fan Power (wind) if (attribute === "wind" || attribute === "fan_power") { // Map B01 (1-5) to Standard (101-105) for text retrieval const b01FanMap: Record<number, number> = { 1: 101, 2: 102, 3: 103, 4: 104, 5: 105 }; const states: Record<number, string> = {}; for (const [b01Val, stdVal] of Object.entries(b01FanMap)) { states[parseInt(b01Val)] = this.locales.getFanPowerText(stdVal, lang); } return { type: "number", name: this.locales.getNameAll(String(attribute)), states: states }; } // Water Level (water) if (attribute === "water" || attribute === "water_box_mode") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: this.locales.getWaterBoxModeText(200, lang), // Off 1: this.locales.getWaterBoxModeText(201, lang), // Low 2: this.locales.getWaterBoxModeText(202, lang), // Medium 3: this.locales.getWaterBoxModeText(203, lang) // High } }; } // Status if (attribute === "status" || attribute === "state") { const states: Record<number, string> = { 0: "Unknown", 1: "Initiating", 2: "Sleeping", 3: "Idle", 4: "Remote Control", 5: "Cleaning", 6: "Returning Dock", 7: "Manual Mode", 8: "Charging", 9: "Charging Error", 10: "Paused", 11: "Spot Cleaning", 12: "In Error", 13: "Shutting Down", 14: "Updating", 15: "Docking", 16: "Go To", 17: "Zone Clean", 18: "Room Clean", 22: "Emptying Dust Container", 23: "Washing Mop", 26: "Going to Wash Mop", 28: "In Call", 29: "Mapping", 100: "Fully Charged" }; // Override with dataset translations if available for (const code in states) { const key = this.locales.getStatusKey(code); if (key) { states[Number(code)] = this.locales.getText(key, lang); } } return { type: "number", name: this.locales.getNameAll(String(attribute)), states: states }; } // Cleaning Stats if (attribute === "clean_time" || attribute === "cleaning_time" || attribute === "total_clean_time") { return { type: "number", name: this.locales.getNameAll(String(attribute)), unit: "min" }; } if (attribute === "clean_area" || attribute === "cleaning_area" || attribute === "last_clean_area") { return { type: "number", name: this.locales.getNameAll(String(attribute)), unit: "m²" }; } if (attribute === "quantity") { return { type: "number", name: this.locales.getNameAll(String(attribute)), unit: "%" }; } if (attribute === "real_clean_time") { return { type: "number", role: "value.interval", name: this.locales.getNameAll(String(attribute)), unit: "s" }; } if (attribute === "clean_finish") { return { type: "number", role: "value", name: this.locales.getNameAll(String(attribute)), }; } if (attribute === "last_clean_t") { return { type: "string", name: this.locales.getNameAll(String(attribute)) }; } // Error Codes if (attribute === "error_code" || attribute === "fault") { const faultCodes = this.locales.getErrorCodes(); const standardErrors = VACUUM_CONSTANTS.errorCodes; const states: Record<string, string> = {}; // Use standard firmware error range (1-30) as the foundation before applying model-specific overrides to ensure basic faults are covered // Provide localized fallback if available const standardErrorsToUse: Record<number, string> = { ...(standardErrors as unknown as Record<number, string>) }; // Apply Adapter Error Mapping for specific language using TranslationManager // This replaces the legacy s7_maxv_dataset.json fallback for (const [codeStr, key] of Object.entries(ADAPTER_ERROR_MAPPING)) { const code = Number(codeStr); // TranslationManager uses adapter.language internally via init() const translated = this.deps.adapter.translationManager.get(key); if (translated && translated !== key) { standardErrorsToUse[code] = translated; } } // Add standard error codes (with fallback) for (const [code, desc] of Object.entries(standardErrorsToUse)) { if (typeof desc === "string") { states[code] = desc; } } // 2. Add/Overlay model-specific fault codes from dataset faultCodes.forEach((code) => { const entry = this.locales.getErrorText(code, lang); let text = ""; if (typeof entry === "string") { text = entry; } else if (entry && typeof entry === "object") { const obj = entry as any; text = obj.summary || obj.title || ""; } // Check if the resolved text matches a standardized placeholder format to avoid overwriting default labels const isPlaceholder = text === `F_${code}`; if (isPlaceholder || !text) { // Skip overwriting if it's a placeholder or empty } else { states[code] = text; } }); return { type: "number", name: this.locales.getNameAll(String(attribute)), states: states, }; } // 5. Dock Status if (attribute === "dock_status") { const states: Record<number, string> = { 0: "Undocked", 1: "Docking", 2: "Docked", 3: "Leaving Dock", 255: "Unknown" }; // TODO: Add dataset mapping for dock_status if possible return { type: "number", name: this.locales.getNameAll(String(attribute)), states: states }; } // 6. Dust Collection Status if (attribute === "dust_collection_status") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Idle", 1: "Collecting", 2: "Collection Weaker", 3: "Collection Failed", 4: "Dustbin Full" } }; } // 7. Map Status if (attribute === "map_status") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Unmapped", 1: "Mapped", 2: "Mapping", 3: "Loading" } }; } // 8. Charge State if (attribute === "charge_state") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Charging", 1: "Not Charging", 2: "Fully Charged", 3: "Charge Failed" } }; } // 9. Work Mode if (attribute === "work_mode") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Standard", 1: "Custom", 2: "Silent" } }; } // 10. Robot Mode if (attribute === "mode") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Vacuum", 1: "Vacuum & Mop", 2: "Mop" } }; } // 10. Tank State (Water Box) if (attribute === "tank_state") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Installed", 1: "Removed", 2: "Empty", 3: "Unknown" } }; } // 11. Sweep Type (Mop Route) if (attribute === "sweep_type" || attribute === "mop_mode") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: this.locales.getMopModeText(300, lang) || "Standard", 1: this.locales.getMopModeText(301, lang) || "Deep", 300: this.locales.getMopModeText(300, lang), 301: this.locales.getMopModeText(301, lang), 302: this.locales.getMopModeText(302, lang), 303: this.locales.getMopModeText(303, lang) } }; } // 12. Cloth State (Mop Pad) if (attribute === "cloth_state") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: this.locales.getClothStateText(0, lang), 1: this.locales.getClothStateText(1, lang), 2: this.locales.getClothStateText(2, lang) } }; } // 13. Multi Floor if (attribute === "multi_floor") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Disabled", 1: "Enabled" } }; } // 14. Quiet Is Open (DND Mode) if (attribute === "quiet_is_open") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Off", 1: "On" } }; } // 15. Dust Collection Frequency if (attribute === "dust_frequency") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Smart", 1: "Low", 2: "Medium", 3: "High", 4: "Never" } }; } // 16. Clean Path Preference if (attribute === "clean_path_preference") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Standard", 1: "Fast" } }; } // 17. Repeat State if (attribute === "repeat_state") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Off", 1: "On" } }; } // 18. Dust Action if (attribute === "dust_action") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Idle", 1: "Emptying" } }; } // 19. Build Map if (attribute === "build_map") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Off", 1: "On" } }; } // 20. Map Num if (attribute === "map_num") { return { type: "number", name: this.locales.getNameAll(String(attribute)), unit: "maps" }; } // 22. Custom Type if (attribute === "custom_type") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { 0: "Off", 1: "On" } }; } // 23. Add Sweep Status if (attribute === "add_sweep_status") { return { type: "number", name: this.locales.getNameAll(String(attribute)), states: { "-1": "Unknown", 0: this.locales.getText("common_off", lang), 1: this.locales.getText("common_on", lang) } }; } // 24. Language if (attribute === "language") { return { type: "string", name: this.locales.getNameAll(String(attribute)), }; } // 25. Time Zone if (attribute === "time_zone") { return { type: "number", name: this.locales.getNameAll(String(attribute)), }; } if (attribute === "time_zone_info") { return { type: "string", name: this.locales.getNameAll(String(attribute)), }; } // 26. Recommend (B01 sends object e.g. { sill, wall, room_id[] }; store as JSON string) if (attribute === "recommend") { return { type: "string", name: this.locales.getNameAll(String(attribute)), }; } // 27. PU Charging if (attribute === "pu_charging") { return { type: "number", name: this.locales.getNameAll(String(attribute)), }; } // Default fallback using getNameAll const keyName = String(attribute); const translatedName = this.locales.getNameAll(keyName); if (translatedName !== keyName) { return { name: translatedName, type: "mixed", role: "value", read: true, write: false }; } return undefined; } public override async updateNetworkInfo(): Promise<void> { try { // B01: Request via service.get_net_info? Or just rely on prop.get("net_status")? // prop.get "net_status" usually just gives connected state. // Let's try service.get_network_info first as it's common. const res = await this.deps.adapter.requestsHandler.sendRequest(this.duid, "service.get_net_info", {}); if (!res) return; // Typically returns { ssid, ip, mac, rssi... } const info = Array.isArray(res) ? res[0] : res; if (!info) return; await this.deps.ensureFolder(`Devices.${this.duid}.networkInfo`); for (const key in info) { const rawType = typeof info[key]; const type = (rawType === "object" ? "mixed" : rawType) as ioBroker.CommonType; await this.deps.ensureState(`Devices.${this.duid}.networkInfo.${key}`, { name: key, type: type, read: true, write: false }); await this.deps.adapter.setStateChanged(`Devices.${this.duid}.networkInfo.${key}`, { val: info[key], ack: true }); } } catch (e: any) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Failed to update network info: ${e.message}`, "warn"); } } protected static readonly MAPPED_CLEAN_SUMMARY: Record<string, string> = { 0: "clean_time", 1: "clean_area", 2: "clean_count", 3: "records", record_list: "records" }; /** * Override to handle B01 specific unit conversions (mm² -> m², s -> min) */ protected override async processResultKey(folder: string, key: string, val: unknown): Promise<void> { if (typeof val === "number") { // Time: seconds -> minutes if (key === "clean_time" || key === "cleaning_time" || key === "total_clean_time") { val = Math.round(val / 60); } else if (key === "clean_area" || key === "cleaning_area" || key === "last_clean_area" || key === "total_clean_area") { val = Math.round(val / 1000000); } } await super.processResultKey(folder, key, val); } public getCommonConsumable(attribute: string | number): Partial<ioBroker.StateCommon> | undefined { return VACUUM_CONSTANTS.consumables[attribute as keyof typeof VACUUM_CONSTANTS.consumables] as Partial<ioBroker.StateCommon>; } public isResetableConsumable(consumable: string): boolean { return VACUUM_CONSTANTS.resetConsumables.has(consumable); } public getCommonCleaningInfo(attribute: string | number): Partial<ioBroker.StateCommon> | undefined { return (VACUUM_CONSTANTS.cleaningInfo as any)[attribute]; } public getCommonCleaningRecords(attribute: string | number): Partial<ioBroker.StateCommon> | undefined { return (VACUUM_CONSTANTS.cleaningRecords as any)[attribute]; } public getFirmwareFeatureName(featureID: string | number): string { return (VACUUM_CONSTANTS.firmwareFeatures as any)[featureID] || `FeatureID_${featureID}`; } }