iobroker.roborock
Version:
785 lines (677 loc) • 28.5 kB
text/typescript
// src/lib/features/base_device_features.ts
import { z } from "zod";
import type { Roborock } from "../../main";
import { DeviceStateWriter } from "./deviceStateWriter";
import { Feature } from "./features.enum";
// --- Types & Interfaces ---
/**
* Command object properties.
*/
export type CommandSpec = {
type: ioBroker.CommonType | "json"; // 'json' type used for internal logic
def?: any;
states?: Record<string | number, string>;
min?: number;
max?: number;
unit?: string;
role?: string;
};
/**
* Feature implementation function, 'this' context is bound.
*/
export type FeatureImplementation = () => Promise<void> | void;
/**
* Model-specific configuration.
*/
export interface DeviceModelConfig {
staticFeatures: Feature[]; // Features this model always has
}
/**
* Feature class constructor signature.
*/
export type FeatureClassConstructor = new (_dependencies: FeatureDependencies, _duid: string) => BaseDeviceFeatures;
/**
* Dependencies injected into feature classes.
*/
export interface FeatureDependencies {
adapter: Roborock;
config: Roborock["config"];
http_api: Roborock["http_api"];
ensureState: Roborock["ensureState"];
ensureFolder: Roborock["ensureFolder"];
log: Roborock["log"];
// Add other dependencies if needed
}
// --- Registry & Decorator ---
/** Maps robotModelId to feature class constructors. */
const modelRegistry = new Map<string, FeatureClassConstructor>();
/**
* Decorator to register a feature class for a robot model.
* @param robotModelId Unique model identifier (e.g. 'roborock.vacuum.a70').
*/
export function RegisterModel(robotModelId: string) {
return function (constructor: FeatureClassConstructor) {
if (modelRegistry.has(robotModelId)) {
// Model already registered, overwriting.
}
modelRegistry.set(robotModelId, constructor);
};
}
// --- Zod Schemas (Base) ---
/**
* Base Zod schema for generic status properties.
*/
export const BaseStatusSchema = z.looseObject({
error_code: z.number().int().optional(),
// Add generic status fields if applicable
});
// --- Generic Base Class ---
/**
* Base class for device features. Handles init, feature application, and commands.
* Extended by specific types (e.g. V1VacuumFeatures).
*/
export abstract class BaseDeviceFeatures {
protected createdStates: Set<string> = new Set(); // Track created states to avoid redundant ensureState calls
protected runtimeDetectionComplete = false; // Initial runtime detection flag
protected readonly stateWriter: DeviceStateWriter;
protected deps: FeatureDependencies;
public commands: Record<string, CommandSpec | any>; // Command definitions for this device
public extraCommandGroups: Record<string, Record<string, CommandSpec | any>>;
protected duid: string;
protected robotModel: string;
public protocolVersion: string | null = null;
protected config: DeviceModelConfig; // Static feature config from model class
protected appliedFeatures = new Set<Feature>(); // Tracks applied features
protected pendingFeatures = new Set<Feature>(); // Tracks features currently being applied (Race Condition Guard)
protected commandsCreated = false; // Command objects created flag
// --- Constants (Generic) ---
protected static readonly CONSTANTS = {
// Generic constants for all Roborock devices
baseCommands: {},
// Generic error codes (subset)
errorCodes: {
0: "No error",
255: "Internal error",
"-1": "Unknown Error",
// Add more if generic across all devices
},
};
// --- Metadata Key for Feature Registry ---
// Unique symbol for registry on prototype
public static readonly FEATURE_METADATA_KEY = Symbol.for("roborock.featureRegistry");
/**
* Decorator to register a feature handler method.
* @param feature The Feature enum key.
*/
public static DeviceFeature(feature: Feature) {
return function (target: any, propertyKey: string) {
// 'target' is the prototype
let registry: Map<Feature, string> = target[BaseDeviceFeatures.FEATURE_METADATA_KEY];
if (!registry) {
registry = new Map();
// Store on prototype
target[BaseDeviceFeatures.FEATURE_METADATA_KEY] = registry;
}
registry.set(feature, propertyKey);
};
}
// --- Feature Registry (Instance Based via Metadata) ---
/**
* Base feature handler constructor.
* @param dependencies Injected dependencies.
* @param duid Device unique identifier.
* @param robotModel Robot model string.
* @param config Static feature config.
*/
constructor(dependencies: FeatureDependencies, duid: string, robotModel: string, config: DeviceModelConfig) {
this.deps = dependencies;
this.duid = duid;
this.robotModel = robotModel;
this.config = config;
this.stateWriter = new DeviceStateWriter(dependencies, duid);
// Initialize empty commands map. Actual commands will be populated during setupProtocolFeatures.
this.commands = {};
this.extraCommandGroups = {};
}
/**
* Applies a feature if not already applied. Looks up implementation in registry.
* @param feature Feature enum key.
* @returns `true` if applied now.
*/
protected async applyFeature(feature: Feature): Promise<boolean> {
// Validate input feature
if (!feature || !Object.values(Feature).includes(feature)) {
this.deps.log.warn(`[${this.duid}] Attempted to apply invalid feature value: ${feature}`);
return false;
}
// Check if already applied or pending
if (this.appliedFeatures.has(feature) || this.pendingFeatures.has(feature)) {
return false;
}
// Get registry from instance metadata (prototype chain)
const registry: Map<Feature, string> | undefined = (this as any)[BaseDeviceFeatures.FEATURE_METADATA_KEY];
if (registry && registry.has(feature)) {
const methodName = registry.get(feature)!;
this.pendingFeatures.add(feature); // Lock
try {
const applyMethod = (this as unknown as Record<string, () => Promise<void>>)[methodName];
if (typeof applyMethod !== "function") throw new Error(`Feature ${String(feature)}: missing method ${methodName}`);
await applyMethod.call(this);
this.appliedFeatures.add(feature); // Mark applied after success
return true;
} catch (e: unknown) {
const stack = e instanceof Error ? e.stack : "";
this.deps.log.error(`[FeatureApply|${this.robotModel}|${this.duid}] Error applying feature '${feature}': ${this.deps.adapter.errorMessage(e)} ${stack}`);
return false;
} finally {
this.pendingFeatures.delete(feature); // Unlock
}
} else {
return false;
}
}
// --- Abstract / Overridable Methods ---
/**
* Detects features via device-specific mechanisms (bitfields, fw info).
* Implemented by subclasses.
* @returns Set of detected `Feature` enum keys.
*/
protected abstract getDynamicFeatures(): Set<Feature>;
/**
* Applies static features from config.
* Override for pre-runtime model logic.
* @param _statusData Optional initial status data.
* @param _fwFeatures Optional initial firmware features.
*/
public async applyModelSpecifics(): Promise<void> {
const promises = this.config.staticFeatures.map((feature) => this.applyFeature(feature));
await Promise.all(promises);
}
/**
* Performs runtime feature detection using status data.
* Implemented by subclasses.
* @param statusData Validated status data.
* @param fwFeatures Optional firmware features.
* @returns `true` if features/commands changed.
*/
public abstract detectAndApplyRuntimeFeatures(_statusData: Readonly<Record<string, any>>): Promise<boolean>;
// --- Core Initialization Logic ---
/**
* Initializes features: Model Specifics -> Runtime Detection -> Dock Processing -> Command Objects.
* @param initialStatus Optional initial status.
* @param initialFwFeatures Optional initial firmware features.
*/
public async initialize(online: boolean = false): Promise<void> {
// Flow: Protocol -> Model Specifics -> Runtime Detection -> Dock Processing -> Command Objects
// 0. Setup Protocol Features (Command Sets)
try {
await this.setupProtocolFeatures();
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error setting up protocol features: ${this.deps.adapter.errorMessage(e)}`, "error");
}
// 1. Apply Model Specifics
try {
await this.applyModelSpecifics();
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error applying model specifics: ${this.deps.adapter.errorMessage(e)}`, "error");
}
// 2. Create/Update ioBroker Objects (Commands)
// Must be done BEFORE fetching data, as data updates might sync to command states.
try {
await this.createCommandObjects();
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error creating command objects: ${this.deps.adapter.errorMessage(e)}`, "error");
}
// 3. Fetch initial data if online
if (online) {
try {
await this.initializeDeviceData();
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error initializing device data: ${this.deps.adapter.errorMessage(e)}`, "error");
}
}
}
/**
* Fetches initial runtime data (status, consumables, map).
*/
public async initializeDeviceData(): Promise<void> {
// Default implementation: update status if online
await this.updateStatus();
await this.updateFirmwareFeatures();
await this.updateMap();
}
public async setupProtocolFeatures(): Promise<void> {
// Initialize with generic base commands
this.commands = JSON.parse(JSON.stringify(BaseDeviceFeatures.CONSTANTS.baseCommands));
this.extraCommandGroups = {};
}
/**
* Logs summary of applied features and commands. Call after init.
*/
public printSummary(): void {
}
// --- Core Helper Methods ---
/**
* Maps dynamic feature keys (e.g. 'is...') to action keys (e.g. 'MopWash').
* @param detectedFeature Detected Feature enum key.
* @returns Mapped action Feature key, detected key if actionable, or null.
*/
protected mapFeature(detectedFeature: Feature): Feature | null {
// Get registry from instance metadata
const registry: Map<Feature, string> | undefined = (this as any)[BaseDeviceFeatures.FEATURE_METADATA_KEY];
// Check if 'is...' key value exists as enum key
const potentialActionName = Feature[detectedFeature as keyof typeof Feature];
// Find enum key for string value, excluding original key
const mappedActionKey = (Object.keys(Feature) as Array<keyof typeof Feature>).find((key) => Feature[key] === potentialActionName && key !== detectedFeature);
if (mappedActionKey) {
const actionFeatureEnum = Feature[mappedActionKey];
// Check if mapped action has registered implementation
if (registry && registry.has(actionFeatureEnum)) {
this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Mapping dynamic feature '${detectedFeature}' to action '${actionFeatureEnum}'`, "debug");
return actionFeatureEnum;
} else {
this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Dynamic feature '${detectedFeature}' mapped to '${actionFeatureEnum}', but no action registered.`, "debug");
return null;
}
}
// Check if detected feature has registered action
if (registry && registry.has(detectedFeature)) {
this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Using dynamic feature '${detectedFeature}' directly.`, "debug");
return detectedFeature;
}
// No mapping or action found
this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Dynamic feature '${detectedFeature}' detected but has no registered action or mapping.`, "debug");
return null;
}
/**
* Creates/updates ioBroker command objects from this.commands.
*/
public async createCommandObjects(): Promise<void> {
const commandGroups: Record<string, Record<string, CommandSpec | any>> = {
commands: this.commands,
...this.extraCommandGroups
};
const promises: Promise<void>[] = [];
for (const [folderName, groupCommands] of Object.entries(commandGroups)) {
const folderPath = `Devices.${this.duid}.${folderName}`;
try {
await this.deps.ensureFolder(folderPath);
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Failed to ensure commands folder ${folderPath}: ${this.deps.adapter.errorMessage(e)}`, "error");
return;
}
for (const [command, commonCommand] of Object.entries(groupCommands)) {
promises.push(this.processCommand(folderPath, command, commonCommand));
}
}
try {
await Promise.all(promises); // Wait for all operations
this.commandsCreated = true; // Done
} catch (e: unknown) {
// Catch Promise.all errors (rare)
this.deps.log.error(`[${this.duid}] Critical error during parallel command object creation: ${this.deps.adapter.errorMessage(e)}`);
}
}
/**
* Process a single command object creation.
*/
protected async processCommand(folderPath: string, cmd: string, spec: CommandSpec | any): Promise<void> {
try {
const options: Partial<ioBroker.StateCommon> = {
...(spec as Partial<ioBroker.StateCommon>),
name: spec.name || this.deps.adapter.translations[cmd] || cmd, // Add name generation
write: true, // Writable
};
const originalType = spec.type; // Store original type
// Determine Role
if (!options.role) {
if (originalType === "boolean" && !options.states) options.role = "button";
else if (originalType === "number" && options.states) options.role = "value.list";
else if (originalType === "number") options.role = "level";
else if (originalType === "json" && options.states) options.role = "value.list";
else if (originalType === "json") options.role = "json";
else options.role = "state";
}
// Enforce default value if missing (User requirement: no null defaults)
if (options.def === undefined || options.def === null) {
if (options.type === "boolean" || options.role === "button") {
options.def = false;
} else if (options.type === "number") {
options.def = options.min ?? 0;
} else if (options.type === "string") {
options.def = "";
}
}
// Adjust type
if (originalType === "json") {
options.type = "string";
}
// Type validation and default
const validTypes: ioBroker.CommonType[] = ["string", "number", "boolean", "object", "array", "mixed"];
if (!options.type || typeof options.type !== "string" || !validTypes.includes(options.type as ioBroker.CommonType)) {
if (originalType !== "json") {
// Skip log if setting to string
this.deps.log.warn(`[${this.duid}] Invalid or missing type '${spec.type}' for command '${cmd}', defaulting to 'string'.`);
}
options.type = "string";
}
const path = `${folderPath}.${cmd}`;
// Create/Update Object
const existingObj = await this.deps.adapter.getObjectAsync(path);
if (existingObj) {
// Extend if common differs. Stringify is good enough for now.
if (JSON.stringify(existingObj.common) !== JSON.stringify(options)) {
await this.deps.adapter.extendObject(path, { common: options as ioBroker.StateCommon });
}
} else {
await this.deps.ensureState(path, options as ioBroker.StateCommon);
}
// Reset button states
if (options.role === "button" && options.type === "boolean") {
const currentState = await this.deps.adapter.getStateAsync(path);
// Reset to false if needed
if (!currentState || currentState.val !== false) {
await this.deps.adapter.setState(path, false, true);
}
}
} catch (e: unknown) {
this.deps.log.error(`[${this.duid}] Error processing command object '${cmd}': ${this.deps.adapter.errorMessage(e)}`);
}
}
// --- Helper Methods ---
/**
* Adds/updates command definition. Merges states to preserve specifics.
* @param name Command name.
* @param spec CommandSpec definition.
*/
protected addCommand(name: string, spec: CommandSpec | any, group = "commands"): void {
if (!name || typeof name !== "string") {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `addCommand: Invalid command name provided: ${name}`, "error");
return;
}
try {
let targetGroup = this.commands;
if (group !== "commands") {
if (!this.extraCommandGroups[group]) {
this.extraCommandGroups[group] = {};
}
targetGroup = this.extraCommandGroups[group];
}
// Merge states if new spec has fewer states.
if (targetGroup[name]?.states && spec.states) {
const existingStatesJson = JSON.stringify(targetGroup[name].states);
const newStatesJson = JSON.stringify(spec.states);
if (existingStatesJson !== newStatesJson) {
// Merge: New states overwrite/add
spec.states = { ...targetGroup[name].states, ...spec.states };
} else {
// Preserve existing spec if states identical
spec = { ...targetGroup[name], ...spec, states: targetGroup[name].states };
}
} else if (targetGroup[name]?.states && !spec.states) {
// Keep existing states if new one has none
spec.states = targetGroup[name].states;
}
targetGroup[name] = spec;
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error in addCommand for '${name}': ${this.deps.adapter.errorMessage(e)}`, "error");
}
}
public getCommandFolders(): string[] {
return ["commands", ...Object.keys(this.extraCommandGroups)];
}
public hasCommandFolder(folder: string): boolean {
return folder === "commands" || Object.prototype.hasOwnProperty.call(this.extraCommandGroups, folder);
}
public getCommandSpec(folder: string, command: string): CommandSpec | any | undefined {
if (folder === "commands") {
return this.commands[command];
}
return this.extraCommandGroups[folder]?.[command];
}
/**
* Calls injected ensureState with correct path.
* @param subfolder Subfolder name.
* @param stateName State name.
* @param commonOptions State options.
* @param native Optional native options.
*/
protected async ensureState(subfolder: string, stateName: string, commonOptions: Partial<ioBroker.StateCommon>, native: Record<string, any> = {}): Promise<void> {
const path = `Devices.${this.duid}.${subfolder}.${stateName}`;
try {
// Validate type before ensureState
const validTypes: ioBroker.CommonType[] = ["string", "number", "boolean", "object", "array", "mixed"];
if (commonOptions.type && !validTypes.includes(commonOptions.type as ioBroker.CommonType)) {
this.deps.adapter.rLog("System", this.duid, "Warn", undefined, undefined, `Invalid type '${commonOptions.type}' in ensureState for ${path}, defaulting to 'string'.`, "warn");
commonOptions.type = "string";
}
// Check if object exists and needs update
const existingObj = await this.deps.adapter.getObjectAsync(path);
if (existingObj && existingObj.common && this.hasStatesChanged(commonOptions.states, existingObj.common.states)) {
this.deps.log.debug(`[${this.duid}] Updating object definition for ${path} (states mapping changed)`);
await this.deps.adapter.extendObject(path, {
common: commonOptions as ioBroker.StateCommon,
native: native
});
return;
}
// Standard ensure (creates if not exists)
await this.deps.ensureState(path, commonOptions as ioBroker.StateCommon, native); // Cast after validation
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error in ensureState for ${path}: ${this.deps.adapter.errorMessage(e)}`, "error");
}
}
// --- Static Methods ---
/**
* Get registered feature class for model.
* @param modelId Robot model identifier.
* @returns Constructor or undefined.
*/
public static getRegisteredModelClass(modelId: string): FeatureClassConstructor | undefined {
return modelRegistry.get(modelId);
}
/**
* Get all registered model IDs.
*/
public static getRegisteredModels(): string[] {
return Array.from(modelRegistry.keys());
}
/**
* Check if static feature is defined.
* @param feature Feature enum key.
*/
public hasStaticFeature(feature: Feature): boolean {
return this.config.staticFeatures.includes(feature);
}
public hasFeature(feature: Feature): boolean {
return this.appliedFeatures.has(feature) || this.config.staticFeatures.includes(feature);
}
/**
* Helper to safely access dynamic feature methods.
* Encapsulates type casting for readability.
*/
protected getFeatureMethod(name: string): Function {
// Safe access using keyof assertion
const method = this[name as keyof this];
if (typeof method === "function") {
return method as Function;
}
throw new Error(`Feature method '${name}' not found or is not a function.`);
}
// --- Command Parameter Interception ---
/**
* Allows feature handlers to provide/modify parameters for a command before sending.
* Override this to implement logic like 'app_segment_clean' gathering segments from states.
* @param method Command method name.
* @param params Existing parameters passed from caller.
*/
public async getCommandParams(method: string, params?: unknown, id?: string): Promise<unknown> {
void method;
void id;
return params;
}
public async onCommandResult(requestedMethod: string, finalMethod: string, response: unknown, params?: unknown): Promise<void> {
void requestedMethod;
void finalMethod;
void response;
void params;
}
// --- Data Update Methods (Unified Data Handling) ---
/**
* Fetch data and store in folder.
* @param method API method.
* @param params API parameters.
* @param folder Target folder.
* @param mapper Optional data mapper.
*/
protected async requestAndProcess(method: string, params: any[], folder: string, mapper?: (data: any) => Record<string, any> | Promise<Record<string, any>>): Promise<void> {
try {
const result = await this.deps.adapter.requestsHandler.sendRequest(this.duid, method, params);
let resultObj: Record<string, unknown> | undefined;
// Recursively unwrap single-element arrays (common in B01/Tuya responses)
let unwrapped = result;
while (Array.isArray(unwrapped) && unwrapped.length === 1) {
unwrapped = unwrapped[0];
}
if (typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped)) {
resultObj = unwrapped as Record<string, unknown>;
}
if (resultObj) {
// Apply mapper
if (mapper) {
resultObj = await mapper(resultObj);
}
await this.deps.ensureFolder(`Devices.${this.duid}.${folder}`);
for (const key in resultObj) {
await this.processResultKey(folder, key, resultObj[key]);
}
}
} catch (e: unknown) {
this.deps.adapter.rLog("System", this.duid, "Warn", undefined, undefined, `Failed to update ${folder} (method: ${method}): ${this.deps.adapter.errorMessage(e)}`, "warn");
}
}
/**
* Process a single key from API result.
*/
protected async processResultKey(folder: string, key: string, val: unknown): Promise<void> {
// Determine common options (type, role, unit)
let common: Partial<ioBroker.StateCommon> | undefined;
if (folder === "deviceStatus") {
common = this.getCommonDeviceStates(key);
} else if (folder === "cleaningInfo") {
common = this.getCommonCleaningInfo(key);
} else if (folder === "cleaningRecords" || folder.includes("records")) {
common = this.getCommonCleaningRecords(key);
}
if (!common) {
common = { name: key, type: typeof val as ioBroker.CommonType, read: true, write: false };
}
// Handle Objects/Arrays by stringifying them so they don't crash the state
if (typeof val === "object" && val !== null) {
val = JSON.stringify(val);
}
// Formatting for timestamp keys only (clean_finish is 0/1 flag, not a timestamp)
if ((key === "last_clean_t" || key === "begin" || key === "end") && typeof (val as any) === "number") {
val = new Date((val as number) * 1000).toLocaleString();
common.type = "string"; // Update type to match new value
}
// Enforce type matching to keep the log clean
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;
}
const fullPath = `Devices.${this.duid}.${folder}.${key}`;
if (!this.createdStates.has(fullPath)) {
await this.deps.ensureState(fullPath, common);
this.createdStates.add(fullPath);
}
await this.deps.adapter.setStateChanged(fullPath, { val: val as ioBroker.StateValue, ack: true });
}
// --- Helper Methods ---
private hasStatesChanged(
newStates: Record<string, string> | string | string[] | undefined,
oldStates: Record<string, string> | string | string[] | undefined
): boolean {
if (!!newStates !== !!oldStates) return true; // One is defined, one is not
if (!newStates || !oldStates) return false; // Both undefined
return JSON.stringify(newStates) !== JSON.stringify(oldStates);
}
public async updateStatus(): Promise<void> {
// Default for vacuums
await this.requestAndProcess("get_prop", ["get_status"], "deviceStatus");
}
public async updateConsumables(): Promise<void> {
if (!this.hasFeature(Feature.Consumables)) return;
await this.requestAndProcess("get_consumable", [], "consumables");
}
public async updateNetworkInfo(): Promise<void> {
// No feature guard: get_network_info is supported on all devices (V1/MQTT); B01 overrides and uses service.get_net_info.
await this.requestAndProcess("get_network_info", [], "networkInfo");
}
public async updateTimers(): Promise<void> {
if (!this.hasFeature(Feature.Timers)) return;
await this.requestAndProcess("get_timer", [], "timers");
await this.requestAndProcess("get_server_timer", [], "timers");
}
public async updateFirmwareFeatures(): Promise<void> {
if (!this.hasFeature(Feature.FirmwareInfo)) return;
await this.requestAndProcess("get_fw_features", [], "firmwareFeatures");
}
public async updateMultiMapsList(): Promise<void> {
if (!this.hasFeature(Feature.MultiMap)) return;
await this.requestAndProcess("get_multi_maps_list", [], "map");
}
public async updateRoomMapping(): Promise<void> {
if (!this.hasFeature(Feature.RoomMapping)) return;
await this.requestAndProcess("get_room_mapping", [], "map");
}
// Complex updates (override in subclasses)
public async updateCleanSummary(): Promise<void> {
// Default: no-op
}
public async updateMap(): Promise<void> {
// Default: no-op
}
public async updateExtraStatus(): Promise<void> {
// Default: no-op. Override for model-specifics.
}
public getCurrentMapIndex(): number {
return 0;
}
public async getPhoto(imgId: string, type: number): Promise<any> {
if (!this.hasFeature(Feature.GetPhoto)) {
throw new Error("getPhoto feature not enabled for this device");
}
try {
const res = (await this.deps.adapter.requestsHandler.sendRequest(
this.duid,
"get_photo",
{
data_filter: {
img_id: imgId,
type: type,
},
}
)) as any;
// PhotoManager handles the async 300/301 packets and resolves the promise with the final image.
// The data returned here is the result of that resolution.
// If the robot supports encryption (Cipher 1), PhotoManager now automatically handles RSA/AES decryption.
const responseData = (res as any).buffer ? res : (res as any).data || res;
return responseData;
} catch (e: unknown) {
this.deps.adapter.rLog("Requests", this.duid, "Error", this.protocolVersion || undefined, undefined, `[getPhoto] Failed: ${this.deps.adapter.errorMessage(e)}`, "error");
throw e;
}
}
// --- Instance Getters for Constants (Abstract Declarations) ---
// Implemented by subclasses to provide constants.
public abstract getCommonConsumable(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
public abstract isResetableConsumable(consumable: string): boolean;
public abstract getCommonDeviceStates(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
public abstract getCommonCleaningRecords(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
public abstract getFirmwareFeatureName(featureID: string | number): string;
public abstract getCommonCleaningInfo(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
}