iobroker.roborock
Version:
122 lines (105 loc) • 4.26 kB
text/typescript
import { CardSpecData, ProductV5Response } from "./apiTypes";
import { Feature } from "./features/features.enum";
export class ProductHelper {
/**
* Retrieves the CardSpec for a given product model.
* @param productInfo The full V5 product response
* @param model The model identifier (e.g. "roborock.vacuum.a70")
*/
public static getCardSpec(productInfo: ProductV5Response, model: string): CardSpecData | null {
if (!productInfo?.data?.categoryDetailList) return null;
// Try to find matching category by code
const categoryDetail = productInfo.data.categoryDetailList.find(c => c.category.code === model);
if (categoryDetail && categoryDetail.category.cardspec) {
try {
const parsed = JSON.parse(categoryDetail.category.cardspec) as CardSpecData;
return parsed;
} catch (e) {
console.error(`Failed to parse cardspec for ${model}:`, e);
return null;
}
}
return null;
}
/**
* Deduces features based on Product Tags and CardSpec.
*/
public static deduceFeatures(productInfo: ProductV5Response, model: string): Set<Feature> {
const features = new Set<Feature>();
// 1. Tag-based deduction
let productItem;
if (productInfo.data.categoryDetailList) {
for (const cat of productInfo.data.categoryDetailList) {
if (cat.productList) {
productItem = cat.productList.find(p => p.model === model);
if (productItem) break;
}
}
}
if (productItem && productItem.productTags) {
for (const tag of productItem.productTags) {
switch (tag.name) {
case "OfflineMap":
// features.add(Feature.OfflineMap); // If exists
break;
case "camera_landing":
features.add(Feature.Camera);
break;
// Add more tag mappings as discovered
}
}
}
// 2. CardSpec-based deduction
const cardSpec = ProductHelper.getCardSpec(productInfo, model);
if (cardSpec && cardSpec.data) {
// Check for Mop Wash (Look for 'wash_status' or description in state)
// Strategy: Check if specific states exist or if specific values exist in 'state' (121)
// MopWash: Check for state 121 value 23 ("Washing the mop")
if (ProductHelper.hasStateValue(cardSpec, "state", 23) || ProductHelper.hasStateDescription(cardSpec, "state", "Washing the mop")) {
features.add(Feature.MopWash);
}
// AutoEmpty: Check for state 121 value 22 ("Emptying")
if (ProductHelper.hasStateValue(cardSpec, "state", 22) || ProductHelper.hasStateDescription(cardSpec, "state", "Emptying")) {
features.add(Feature.AutoEmptyDock);
}
// MopDry: Check for 'dry_status' (136) or state 121 value 26 ("Drying")
if (ProductHelper.hasStateValue(cardSpec, "state", 26) || cardSpec.data["dry_status"]) {
features.add(Feature.MopDry);
}
// WaterBox
if (cardSpec.data["water_box_mode"] || cardSpec.data["water_box_custom_mode"]) {
features.add(Feature.WaterBox);
}
}
return features;
}
private static hasStateValue(cardSpec: CardSpecData, stateName: string, valueToCheck: number): boolean {
const item = cardSpec.data[stateName];
if (!item?.value) return false;
return item.value.some(v => v.value.includes(valueToCheck));
}
private static hasStateDescription(cardSpec: CardSpecData, stateName: string, descriptionSnippet: string): boolean {
const item = cardSpec.data[stateName];
if (!item?.value) return false;
// Check English description
return item.value.some(v => v.desc?.en?.toLowerCase().includes(descriptionSnippet.toLowerCase()));
}
/**
* Returns a map of state values to their translated labels for a given state (e.g. 'fan_power').
* Returns an empty record if not found.
*/
public static getStateDefinitions(productInfo: ProductV5Response, model: string, stateName: string, lang: string = "en"): Record<number, string> {
const cardSpec = ProductHelper.getCardSpec(productInfo, model);
if (!cardSpec || !cardSpec.data[stateName]) return {};
const item = cardSpec.data[stateName];
if (!item?.value) return {};
const result: Record<number, string> = {};
for (const v of item.value) {
const label = v.desc?.[lang] || v.desc?.["en"] || "Unknown";
for (const val of v.value) {
result[val] = label;
}
}
return result;
}
}