UNPKG

iobroker.roborock

Version:
675 lines (585 loc) 26.3 kB
import type { FeatureDependencies } from "../../../baseDeviceFeatures"; import { DeviceStateWriter } from "../../../deviceStateWriter"; import { parseQ10CarpetDpPayload, parseQ10RestrictedZoneDpPayload, parseQ10SuspectedPointsDpPayload, parseQ10VirtualWallDpPayload } from "../../../../map/q10/Q10YxMapParser"; import type { Q10RuntimeStatePatch } from "../../../../map/q10/types"; import { normalizeRoborockRoomDisplayName } from "../../../../roomNameNormalizer"; import { VACUUM_CONSTANTS } from "../../vacuumConstants"; type Q10ShadowDataServiceHost = { applyCleanRecordList: (data: Record<string, unknown>) => Promise<void>; applyConsumables: (data: Record<string, unknown>) => Promise<void>; applyStatusSnapshot: (resultObj: Record<string, unknown>) => Promise<void>; cleanupFloorMetadata: (folder: string) => Promise<void>; processStatusProperty: (property: string, value: ioBroker.StateValue) => Promise<void>; requestTimerRefresh: () => Promise<void>; }; export class Q10ShadowDataService { private q10SkipNextTimerRefreshFromDp93 = false; private q10AwaitingTimerListResult = false; private readonly stateWriter: DeviceStateWriter; constructor( private readonly deps: FeatureDependencies, private readonly duid: string, private readonly host: Q10ShadowDataServiceHost ) { this.stateWriter = new DeviceStateWriter(deps, duid); } public skipNextTimerRefreshFromDp93(): void { this.q10SkipNextTimerRefreshFromDp93 = true; } public markTimerListRequestPending(): void { this.q10AwaitingTimerListResult = true; } private normalizeQ10BooleanNumberFlags(statusData: Record<string, unknown>): void { for (const key of ["map_save_switch", "recent_clean_record", "valley_point_charging"] as const) { if (typeof statusData[key] === "boolean") { statusData[key] = statusData[key] ? 1 : 0; } } } private decodeQ10ShadowBytes(value: unknown): Uint8Array | null { if (typeof value !== "string" || value.length === 0) return null; try { return new Uint8Array(Buffer.from(value, "base64")); } catch { return null; } } private q10WeekDataToWeekArray(weekData: number): number[] { const weeks: number[] = []; if (((weekData >> 6) & 1) === 1) { weeks.push(0); } for (let day = 1; day < 7; day++) { if (((weekData >> (day - 1)) & 1) === 1) { weeks.push(day); } } return weeks; } private formatQ10Time(value: number): string { return value.toString().padStart(2, "0"); } private async resolveQ10FloorName(mapId: number): Promise<string> { const floorState = await this.deps.adapter.getStateAsync(`Devices.${this.duid}.floors.${mapId}.name`); return typeof floorState?.val === "string" && floorState.val.trim() ? floorState.val : `Map ${mapId}`; } private getDefaultRoomName(): string | undefined { return this.deps.adapter.translationManager?.get("default_room_name"); } private normalizeQ10RoomIds(value: unknown): number[] { if (!Array.isArray(value)) return []; return value .map((entry) => Number(entry)) .filter((entry) => Number.isInteger(entry) && entry > 0); } private async applyQ10CurrentCleanRoomIds(dp91: unknown): Promise<void> { let roomIds: number[] = []; if (dp91 && typeof dp91 === "object" && !Array.isArray(dp91)) { const payload = dp91 as Record<string, unknown>; roomIds = this.normalizeQ10RoomIds(payload.room_id_list); } await this.stateWriter.ensureAndSetValueState("deviceStatus.current_clean_room_ids", { name: "current_clean_room_ids", type: "string", role: "json" }, JSON.stringify(roomIds)); } private async applyQ10ShadowConsumables( topLevelDps: Record<string, unknown>, commonDps: Record<string, unknown> ): Promise<void> { const shadowConsumables = [ { value: topLevelDps["125"], stateKey: "main_brush_work_time", deviceName: "main_brush" }, { value: topLevelDps["126"], stateKey: "side_brush_work_time", deviceName: "side_brush" }, { value: topLevelDps["127"], stateKey: "filter_work_time", deviceName: "filter" }, { value: commonDps["67"], stateKey: "sensor_dirty_time", deviceName: "sensor" } ] as const; const availableShadowConsumables = shadowConsumables.filter((entry) => entry.value !== undefined); if (!availableShadowConsumables.length) return; await this.stateWriter.ensureFolder("consumables"); await this.stateWriter.ensureFolder("resetConsumables"); for (const entry of availableShadowConsumables) { const remainingHours = this.normalizeQ10ShadowConsumableHours(entry.deviceName, entry.value); if (remainingHours === undefined) continue; const localizedName = this.getLocalizedConsumableName(entry.deviceName); await this.stateWriter.ensureAndSetValueState(`consumables.${entry.stateKey}`, { name: `${localizedName} remaining time`, type: "number", unit: "h" }, remainingHours); await this.stateWriter.ensureState(`resetConsumables.reset_${entry.deviceName}`, { name: `Reset ${localizedName}`, type: "boolean", role: "button", write: true, def: false }, { resetParam: entry.stateKey }); } } private getLocalizedConsumableName(deviceName: string): string { const translationKey = VACUUM_CONSTANTS.consumableTranslationKeys[deviceName as keyof typeof VACUUM_CONSTANTS.consumableTranslationKeys]; return translationKey ? this.deps.adapter.translationManager.get(translationKey, deviceName) : deviceName; } private getConsumableLifeSpanHours(deviceName: string): number { switch (deviceName) { case "main_brush": return 300; case "side_brush": return 200; case "filter": return 150; case "sensor": return 30; default: return 0; } } private normalizeQ10ShadowConsumableHours(deviceName: string, value: unknown): number | undefined { const numericValue = Number(value); if (!Number.isFinite(numericValue)) return undefined; const maxHours = this.getConsumableLifeSpanHours(deviceName); if (numericValue <= 100) { // Q10 shadow snapshots seem to ship already-condensed remaining-life values. return Math.max(0, Math.round(numericValue)); } if (maxHours > 0) { const remainingHours = Math.round((maxHours * 3600 - numericValue) / 3600); return Math.max(0, remainingHours); } return Math.max(0, Math.round(numericValue)); } public async applyQ10StatusFromDpResult(dpResult: Record<string, unknown>): Promise<void> { const resultObj: Record<string, unknown> = { ...dpResult }; if (resultObj.state !== undefined) resultObj.status = resultObj.state; if (resultObj.fan_power !== undefined) resultObj.wind = resultObj.fan_power; if (resultObj.water_box_mode !== undefined) resultObj.water = resultObj.water_box_mode; try { await this.host.applyStatusSnapshot(resultObj); } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10StatusFromDpResult: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10NetworkFromDp81(net81: Record<string, unknown>): Promise<void> { if (!net81 || typeof net81 !== "object") return; try { await this.stateWriter.ensureFolder("networkInfo"); const keys: Array<{ key: string; stateKey: string }> = [ { key: "ipAdress", stateKey: "ipAdress" }, { key: "mac", stateKey: "mac" }, { key: "signal", stateKey: "rssi" }, { key: "wifiName", stateKey: "ssid" } ]; for (const { key, stateKey } of keys) { if (net81[key] === undefined) continue; await this.stateWriter.ensureAndSetValueState(`networkInfo.${stateKey}`, { name: stateKey, type: typeof net81[key] === "number" ? "number" : "string", }, net81[key] as ioBroker.StateValue); } } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10NetworkFromDp81: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10MapInfoFromDpResult(resultItem: Record<string, unknown>): Promise<void> { const mapInfoList = resultItem.map_info; if (!Array.isArray(mapInfoList) || mapInfoList.length === 0) return; try { const floorsFolder = "floors"; await this.stateWriter.ensureFolder(floorsFolder); for (const key of ["max_multi_map", "max_bak_map", "multi_map_count"]) { if (resultItem[key] === undefined) continue; await this.stateWriter.ensureAndSetValueState(`${floorsFolder}.${key}`, { name: key, type: "number", }, Number(resultItem[key])); } for (const map of mapInfoList) { const m = map as Record<string, unknown>; const mapFlag = m.mapFlag ?? m.mapflag; if (mapFlag === undefined) continue; const mapId = typeof mapFlag === "number" ? mapFlag : Number(mapFlag); const name = (m.name as string) || `Map ${mapId}`; const folder = `floors.${mapId}`; await this.host.cleanupFloorMetadata(this.stateWriter.path(folder)); await this.stateWriter.ensureFolder(folder, name); await this.stateWriter.ensureAndSetValueState(`${folder}.name`, { name: "Floor Name", type: "string" }, name); const addTime = m.add_time; if (typeof addTime === "number" && Number.isFinite(addTime)) { await this.stateWriter.ensureAndSetValueState(`${folder}.add_time`, { name: "Created At", type: "string", }, this.deps.adapter.formatRoborockDate(addTime)); } const rooms = m.rooms; if (!Array.isArray(rooms)) continue; for (const room of rooms) { const r = room as Record<string, unknown>; const roomId = r.id; if (roomId === undefined) continue; const rid = typeof roomId === "number" ? roomId : Number(roomId); const rawRoomName = typeof r.iot_name === "string" && r.iot_name.trim() ? r.iot_name : typeof r.name === "string" ? r.name : ""; const roomName = normalizeRoborockRoomDisplayName(rawRoomName, () => this.getDefaultRoomName()); await this.stateWriter.ensureState(`${folder}.${rid}`, { name: roomName, type: "boolean", role: "value", def: false, read: true, write: true }); } } } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10MapInfoFromDpResult: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10ConsumablesFromDpResult(dpResult: Record<string, unknown>): Promise<void> { try { await this.host.applyConsumables(dpResult); } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10ConsumablesFromDpResult: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10TimersFromDpResult(dpResult: unknown): Promise<void> { if (!Array.isArray(dpResult)) return; try { this.q10AwaitingTimerListResult = false; await this.stateWriter.ensureFolder("schedules"); for (const timer of dpResult) { if (!Array.isArray(timer) || timer.length < 3) continue; const [id, enabledValue, segments] = timer; const cron = Array.isArray(segments) ? segments[0] : ""; const folder = `schedules.${id}`; await this.stateWriter.ensureFolder(folder); await this.stateWriter.ensureAndSetState(`${folder}.enabled`, { name: "Enabled", type: "boolean", role: "switch", read: true, write: true }, enabledValue === "on"); await this.stateWriter.ensureAndSetValueState(`${folder}.cron`, { name: "CRON", type: "string", role: "text" }, String(cron ?? "")); } } catch (e: unknown) { this.q10AwaitingTimerListResult = false; this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10TimersFromDpResult: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10LocalTimerBlob(dpTimer: unknown): Promise<void> { const bytes = this.decodeQ10ShadowBytes(dpTimer); if (!bytes || bytes.length < 4) return; try { await this.stateWriter.ensureFolder("schedules"); const timerCount = bytes[3] ?? 0; let offset = 4; for (let index = 0; index < timerCount; index++) { if (offset + 14 > bytes.length) break; const lock = bytes[offset] ?? 0; offset += 1; const autoAreaId = Buffer.from(bytes.slice(offset, offset + 4)).readUInt32BE(0); offset += 4; const effective = bytes[offset] ?? 0; offset += 1; const weekData = bytes[offset] ?? 0; offset += 1; const hour = bytes[offset] ?? 0; offset += 1; const minute = bytes[offset] ?? 0; offset += 1; const mapId = Buffer.from(bytes.slice(offset, offset + 4)).readUInt32BE(0); offset += 4; const roomCount = bytes[offset] ?? 0; offset += 1; const rooms: number[] = []; for (let roomIndex = 0; roomIndex < roomCount; roomIndex++) { if (offset >= bytes.length) break; rooms.push(bytes[offset] ?? 0); offset += 1; } if (offset + 5 > bytes.length) break; const cleanMode = bytes[offset] ?? 0; offset += 1; const fanPower = bytes[offset] ?? 0; offset += 1; const waterBoxMode = bytes[offset] ?? 0; offset += 1; const cleanCount = bytes[offset] ?? 0; offset += 1; const cleanLine = bytes[offset] ?? 0; offset += 1; const folder = `schedules.local_${(index + 1).toString().padStart(2, "0")}`; const timeText = `${this.formatQ10Time(hour)}:${this.formatQ10Time(minute)}`; const weeks = this.q10WeekDataToWeekArray(weekData); const mapName = mapId > 0 ? await this.resolveQ10FloorName(mapId) : ""; await this.stateWriter.ensureFolder(folder, timeText); await this.stateWriter.ensureAndSetValueState(`${folder}.enabled`, { name: "Enabled", type: "boolean", role: "switch" }, effective === 1); await this.stateWriter.ensureAndSetValueState(`${folder}.locked`, { name: "Locked", type: "boolean" }, lock === 1); await this.stateWriter.ensureAndSetValueState(`${folder}.time`, { name: "Start Time", type: "string", role: "text" }, timeText); await this.stateWriter.ensureAndSetValueState(`${folder}.hour`, { name: "Hour", type: "number" }, hour); await this.stateWriter.ensureAndSetValueState(`${folder}.minute`, { name: "Minute", type: "number" }, minute); await this.stateWriter.ensureAndSetValueState(`${folder}.weeks`, { name: "Repeat Days", type: "string", role: "json" }, JSON.stringify(weeks)); await this.stateWriter.ensureAndSetValueState(`${folder}.auto_area_id`, { name: "Auto Area ID", type: "number" }, autoAreaId); await this.stateWriter.ensureAndSetValueState(`${folder}.clean_mode`, { name: "Clean Mode", type: "number" }, cleanMode); await this.stateWriter.ensureAndSetValueState(`${folder}.fan_power`, { name: "Fan Power", type: "number" }, fanPower); await this.stateWriter.ensureAndSetValueState(`${folder}.water_box_mode`, { name: "Water Level", type: "number" }, waterBoxMode); await this.stateWriter.ensureAndSetValueState(`${folder}.clean_count`, { name: "Clean Count", type: "number" }, cleanCount); await this.stateWriter.ensureAndSetValueState(`${folder}.clean_line`, { name: "Clean Line", type: "number" }, cleanLine); await this.stateWriter.ensureAndSetValueState(`${folder}.room_count`, { name: "Room Count", type: "number" }, roomCount); await this.stateWriter.ensureAndSetValueState(`${folder}.rooms`, { name: "Rooms", type: "string", role: "json" }, JSON.stringify(rooms)); await this.stateWriter.ensureAndSetValueState(`${folder}.map_id`, { name: "Map ID", type: "number" }, mapId); await this.stateWriter.ensureAndSetValueState(`${folder}.map_name`, { name: "Map Name", type: "string", role: "text" }, mapName); } } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10LocalTimerBlob: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10NotDisturbData(dpValue: unknown): Promise<void> { const bytes = this.decodeQ10ShadowBytes(dpValue); if (!bytes || bytes.length < 6) return; try { await this.host.processStatusProperty("not_disturb_start_hour", bytes[1] ?? 0); await this.host.processStatusProperty("not_disturb_start_minute", bytes[2] ?? 0); await this.host.processStatusProperty("not_disturb_end_hour", bytes[3] ?? 0); await this.host.processStatusProperty("not_disturb_end_minute", bytes[4] ?? 0); await this.host.processStatusProperty( "not_disturb_time", `${this.formatQ10Time(bytes[1] ?? 0)}:${this.formatQ10Time(bytes[2] ?? 0)}-${this.formatQ10Time(bytes[3] ?? 0)}:${this.formatQ10Time(bytes[4] ?? 0)}` ); } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10NotDisturbData: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10ValleyPointChargingData(dpValue: unknown): Promise<void> { const bytes = this.decodeQ10ShadowBytes(dpValue); if (!bytes || bytes.length < 6) return; try { await this.host.processStatusProperty("valley_point_charging_start_hour", bytes[1] ?? 0); await this.host.processStatusProperty("valley_point_charging_start_minute", bytes[2] ?? 0); await this.host.processStatusProperty("valley_point_charging_end_hour", bytes[3] ?? 0); await this.host.processStatusProperty("valley_point_charging_end_minute", bytes[4] ?? 0); await this.host.processStatusProperty( "valley_point_charging_time", `${this.formatQ10Time(bytes[1] ?? 0)}:${this.formatQ10Time(bytes[2] ?? 0)}-${this.formatQ10Time(bytes[3] ?? 0)}:${this.formatQ10Time(bytes[4] ?? 0)}` ); } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10ValleyPointChargingData: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10MultiMapListFromDp61(dp61: Record<string, unknown>): Promise<void> { const mapList = Array.isArray(dp61.data) ? dp61.data : []; if (mapList.length === 0) return; try { await this.stateWriter.ensureFolder("floors"); for (const entry of mapList) { if (!entry || typeof entry !== "object") continue; const map = entry as Record<string, unknown>; const mapId = map.id; if (mapId === undefined || mapId === null || mapId === "") continue; const mapIdText = String(mapId); const name = typeof map.name === "string" && map.name.trim() ? map.name : `Map ${mapIdText}`; const folder = `floors.${mapIdText}`; await this.host.cleanupFloorMetadata(this.stateWriter.path(folder)); await this.stateWriter.ensureFolder(folder, name); await this.stateWriter.ensureAndSetValueState(`${folder}.name`, { name: "Floor Name", type: "string" }, name); const timestamp = map.timestamp; if (typeof timestamp === "number" && Number.isFinite(timestamp)) { await this.stateWriter.ensureAndSetValueState(`${folder}.add_time`, { name: "Created At", type: "string", }, this.deps.adapter.formatRoborockDate(timestamp)); } } } catch (e: unknown) { this.deps.adapter.rLog("System", this.duid, "Warn", "B01", undefined, `Q10 applyQ10MultiMapListFromDp61: ${this.deps.adapter.errorMessage(e)}`, "warn"); } } public async applyQ10ShadowDpPayload(dps: Record<string, unknown>): Promise<void> { if (!dps || typeof dps !== "object") return; const statusData: Record<string, unknown> = {}; const liveMapPatch: Q10RuntimeStatePatch = {}; const topLevelMap: Record<string, string> = { "121": "status", "122": "battery", "123": "fan_power", "124": "water_box_mode", "125": "main_brush_life", "126": "side_brush_life", "127": "filter_life", "136": "clean_times", "137": "clean_mode", "138": "clean_task_type", "139": "back_type", "141": "cleaning_progress", "142": "fleeing_goods" }; for (const [dpId, field] of Object.entries(topLevelMap)) { if (dps[dpId] !== undefined) { statusData[field] = dps[dpId]; } } let net81: Record<string, unknown> | undefined; const common = dps["101"]; if (common && typeof common === "object" && !Array.isArray(common)) { const commonMap: Record<string, string> = { "6": "clean_time", "7": "clean_area", "25": "quiet_is_open", "26": "volume", "36": "voice_language", "37": "dust_switch", "40": "mop_state", "45": "auto_boost", "47": "child_lock", "50": "dust_setting", "51": "map_save_switch", "53": "recent_clean_record", "29": "total_clean_area", "30": "total_clean_count", "31": "total_clean_time", "60": "multi_map_switch", "76": "carpet_clean_type", "78": "clean_path_preference", "83": "robot_type", "86": "line_laser_obstacle_avoidance", "87": "cleaning_progress", "88": "ground_clean", "90": "fault", "93": "timer_type", "96": "add_clean_state", "104": "breakpoint_clean", "105": "valley_point_charging", "108": "voice_version", "109": "robot_country_code", "207": "user_plan" }; const commonDps = common as Record<string, unknown>; for (const [dpId, field] of Object.entries(commonMap)) { if (commonDps[dpId] !== undefined) { statusData[field] = commonDps[dpId]; } } if (commonDps["79"] && typeof commonDps["79"] === "object" && !Array.isArray(commonDps["79"])) { const timeZone = commonDps["79"] as Record<string, unknown>; if (timeZone.timeZoneSec !== undefined) statusData.time_zone = timeZone.timeZoneSec; if (timeZone.timeZoneCity !== undefined) statusData.time_zone_info = timeZone.timeZoneCity; } if (commonDps["92"] && typeof commonDps["92"] === "object" && !Array.isArray(commonDps["92"])) { const disturb = commonDps["92"] as Record<string, unknown>; if (disturb.disturb_light !== undefined) statusData.disturb_light = disturb.disturb_light; if (disturb.disturb_voice !== undefined) statusData.disturb_voice = disturb.disturb_voice; if (disturb.disturb_resume_clean !== undefined) statusData.disturb_resume_clean = disturb.disturb_resume_clean; if (disturb.disturb_dust_enable !== undefined) statusData.disturb_dust_enable = disturb.disturb_dust_enable; } const multiMapList = commonDps["61"]; if (multiMapList && typeof multiMapList === "object" && !Array.isArray(multiMapList)) { await this.applyQ10MultiMapListFromDp61(multiMapList as Record<string, unknown>); } const cleanRecords = commonDps["52"]; if (cleanRecords && typeof cleanRecords === "object" && !Array.isArray(cleanRecords)) { await this.host.applyCleanRecordList(cleanRecords as Record<string, unknown>); } if (commonDps["32"] !== undefined) { await this.applyQ10LocalTimerBlob(commonDps["32"]); } if (commonDps["33"] !== undefined) { await this.applyQ10NotDisturbData(commonDps["33"]); } if (commonDps["106"] !== undefined) { await this.applyQ10ValleyPointChargingData(commonDps["106"]); } if (commonDps["55"] !== undefined) { const bytes = this.decodeQ10ShadowBytes(commonDps["55"]); if (bytes) { const restricted = parseQ10RestrictedZoneDpPayload(Buffer.from(bytes)); liveMapPatch.forbidAreas = restricted.forbidAreas; liveMapPatch.mopAreas = restricted.mopAreas; liveMapPatch.thresholdAreas = restricted.thresholdAreas; } } if (commonDps["57"] !== undefined) { const bytes = this.decodeQ10ShadowBytes(commonDps["57"]); if (bytes) { liveMapPatch.virtualWalls = parseQ10VirtualWallDpPayload(Buffer.from(bytes)); } } if (commonDps["65"] && typeof commonDps["65"] === "object" && !Array.isArray(commonDps["65"])) { const dp65 = commonDps["65"] as Record<string, unknown>; if (Array.isArray(dp65.data)) { liveMapPatch.carpetAreas = parseQ10CarpetDpPayload(dp65.data); } } if (commonDps["98"] !== undefined) { liveMapPatch.suspectedPoints = [ ...(liveMapPatch.suspectedPoints ?? []), ...parseQ10SuspectedPointsDpPayload(commonDps["98"], "easycard") ]; } if (commonDps["100"] !== undefined) { liveMapPatch.suspectedPoints = [ ...(liveMapPatch.suspectedPoints ?? []), ...parseQ10SuspectedPointsDpPayload(commonDps["100"], "threshold") ]; } if (commonDps["103"] !== undefined) { liveMapPatch.suspectedPoints = [ ...(liveMapPatch.suspectedPoints ?? []), ...parseQ10SuspectedPointsDpPayload(commonDps["103"], "cliff") ]; } if (commonDps["91"] !== undefined) { await this.applyQ10CurrentCleanRoomIds(commonDps["91"]); } if (commonDps["93"] !== undefined) { const currentTimerTypeState = await this.deps.adapter.getStateAsync(`Devices.${this.duid}.deviceStatus.timer_type`); const nextTimerType = Number(commonDps["93"]); const currentTimerType = currentTimerTypeState && currentTimerTypeState.val !== null && currentTimerTypeState.val !== undefined ? Number(currentTimerTypeState.val) : Number.NaN; if (this.q10SkipNextTimerRefreshFromDp93) { this.q10SkipNextTimerRefreshFromDp93 = false; } else if (this.q10AwaitingTimerListResult) { // A list refresh is already in flight, so a repeated DP 93 echo must not trigger another 69 request. } else if (!Number.isFinite(currentTimerType) || currentTimerType !== nextTimerType) { await this.host.requestTimerRefresh(); } } const candidateNet81 = commonDps["81"]; if (candidateNet81 && typeof candidateNet81 === "object" && !Array.isArray(candidateNet81)) { net81 = candidateNet81 as Record<string, unknown>; } await this.applyQ10ShadowConsumables(dps, commonDps); } if (Object.keys(statusData).length > 0) { if (statusData.fan_power !== undefined) statusData.wind = statusData.fan_power; if (statusData.water_box_mode !== undefined) statusData.water = statusData.water_box_mode; this.normalizeQ10BooleanNumberFlags(statusData); await this.host.applyStatusSnapshot(statusData); } if (net81) { await this.applyQ10NetworkFromDp81(net81); } if ( (liveMapPatch.virtualWalls !== undefined || liveMapPatch.forbidAreas !== undefined || liveMapPatch.mopAreas !== undefined || liveMapPatch.thresholdAreas !== undefined || liveMapPatch.carpetAreas !== undefined || liveMapPatch.suspectedPoints !== undefined) && typeof (this.deps.adapter as any).mapManager?.applyQ10LiveStatePatch === "function" ) { await (this.deps.adapter as any).mapManager.applyQ10LiveStatePatch(this.duid, liveMapPatch); } } }