substitutions: vacuum_entity: "vacuum.roborock_qrevo_s" start_icon: "\U0000e90a" play_icon: "\U0000e90a" pause_icon: "\U0000e90b" stop_icon: "\U0000e91c" locate_icon: "\U0000e923" docked_icon: "\U0000e924" fan_icon: "\U0000e926" room_plan_icon: "\U0000e927" battery_0_icon: "\U0000e928" battery_20_icon: "\U0000e929" battery_40_icon: "\U0000e92a" battery_60_icon: "\U0000e92b" battery_80_icon: "\U0000e92c" battery_100_icon: "\U0000e92d" globals: - id: vacuum_fan_speeds type: std::vector restore_value: false - id: vacuum_selected_fan_speed_index type: int restore_value: false sensor: # Battery level - platform: homeassistant id: vacuum_battery_level_sensor entity_id: "${vacuum_entity}" attribute: battery_level on_value: - lvgl.label.update: id: vacuum_battery_level_label text: !lambda |- if (isnan(x)) return " "; char buffer[32]; snprintf(buffer, sizeof(buffer), "%.0f%%", x); return std::string(buffer); - if: condition: lambda: 'return id(vacuum_battery_level_sensor).state >= 80;' then: - lvgl.image.update: id: vacuum_battery_level_img src: battery_very_high_img else: - if: condition: lambda: 'return id(vacuum_battery_level_sensor).state >= 60;' then: - lvgl.image.update: id: vacuum_battery_level_img src: battery_high_img else: - if: condition: lambda: 'return id(vacuum_battery_level_sensor).state >= 40;' then: - lvgl.image.update: id: vacuum_battery_level_img src: battery_middle_img else: - if: condition: lambda: 'return id(vacuum_battery_level_sensor).state >= 20;' then: - lvgl.image.update: id: vacuum_battery_level_img src: battery_low_img else: - if: condition: lambda: 'return id(vacuum_battery_level_sensor).state >= 1;' then: - lvgl.image.update: id: vacuum_battery_level_img src: battery_very_low_img else: - lvgl.image.update: id: vacuum_battery_level_img src: battery_empty_img # Cleaned area - platform: homeassistant id: vacuum_cleaned_area_sensor entity_id: "${vacuum_entity}" attribute: cleaned_area on_value: - lvgl.label.update: id: vacuum_cleaned_area_label text: !lambda |- if (isnan(x)) return " "; char buffer[32]; snprintf(buffer, sizeof(buffer), "%.2f m²", x); return std::string(buffer); text_sensor: - platform: homeassistant id: vacuum_fan_speed_list_sensor entity_id: "${vacuum_entity}" attribute: fan_speed_list on_value: - lambda: |- ESP_LOGD("vacuum", "Fan speed list received: %s", x.c_str()); std::vector options; std::string input = x; if (!input.empty() && input != "unknown") { input.erase(std::remove(input.begin(), input.end(), '['), input.end()); input.erase(std::remove(input.begin(), input.end(), ']'), input.end()); input.erase(std::remove(input.begin(), input.end(), '\''), input.end()); input.erase(std::remove(input.begin(), input.end(), '"'), input.end()); std::stringstream ss(input); std::string item; while (std::getline(ss, item, ',')) { item.erase(0, item.find_first_not_of(" \t")); item.erase(item.find_last_not_of(" \t") + 1); if (!item.empty()) { options.push_back(item); } } } if (options.empty()) { options = {"Silent", "Standard", "Medium", "High"}; } id(vacuum_fan_speeds) = options; auto dropdown = id(vacuum_fan_speed_dropdown); if (dropdown != nullptr) { dropdown->set_options(options); } - platform: homeassistant id: vacuum_fan_speed_sensor entity_id: "${vacuum_entity}" attribute: fan_speed on_value: - lambda: |- auto& speeds = id(vacuum_fan_speeds); if (!speeds.empty()) { for (int i = 0; i < (int)speeds.size(); i++) { if (speeds[i] == x) { id(vacuum_selected_fan_speed_index) = i; break; } } } - lvgl.dropdown.update: id: vacuum_fan_speed_dropdown selected_index: !lambda return id(vacuum_selected_fan_speed_index); - platform: homeassistant id: vacuum_name_sensor entity_id: "${vacuum_entity}" attribute: friendly_name on_value: - lvgl.label.update: id: vacuum_label_name text: !lambda return x; - platform: homeassistant id: vacuum_state_sensor entity_id: "${vacuum_entity}" on_value: then: - script.execute: id: vacuum_get_and_set_translated_state state_str: !lambda 'return id(vacuum_state_sensor).state;' - lvgl.widget.hide: vacuum_battery_level_img - lvgl.widget.hide: vacuum_charging_animation - lvgl.widget.hide: vacuum_cleaning_animation - lvgl.widget.hide: vacuum_go_home_animation - lvgl.widget.hide: vacuum_not_animation - lvgl.widget.hide: vacuum_bg_controls_cleaning - lvgl.widget.hide: vacuum_bg_controls_paused - lvgl.widget.hide: vacuum_bg_controls_docked - lvgl.widget.hide: vacuum_bg_controls_idle - lvgl.widget.hide: vacuum_bg_controls_returning - if: condition: lambda: 'return x == "cleaning";' then: - lvgl.widget.show: vacuum_cleaning_animation - lvgl.widget.show: vacuum_bg_controls_cleaning - lvgl.widget.show: vacuum_battery_level_img - if: condition: lambda: 'return x == "returning";' then: - lvgl.widget.show: vacuum_go_home_animation - lvgl.widget.show: vacuum_bg_controls_returning - lvgl.widget.show: vacuum_battery_level_img - if: condition: lambda: 'return x == "docked";' then: - lvgl.widget.show: vacuum_not_animation - lvgl.widget.show: vacuum_bg_controls_docked - if: condition: lambda: 'return id(vacuum_battery_level_sensor).state != 100;' then: - lvgl.widget.show: vacuum_charging_animation else: - lvgl.widget.show: vacuum_battery_level_img - if: condition: lambda: 'return x == "paused";' then: - lvgl.widget.show: vacuum_not_animation - lvgl.widget.show: vacuum_bg_controls_paused - lvgl.widget.show: vacuum_battery_level_img - if: condition: lambda: 'return x == "idle";' then: - lvgl.widget.show: vacuum_not_animation - lvgl.widget.show: vacuum_bg_controls_idle - lvgl.widget.show: vacuum_battery_level_img lvgl: pages: - id: vacuum_page bg_color: color_slate_blue_gray widgets: # main - obj: id: vacuum_bg_main y: 20 width: 440 height: 240 align: TOP_MID pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP border_width: 0 shadow_opa: TRANSP radius: 10 widgets: - obj: x: 10 y: 0 width: 140 height: 50 align: TOP_LEFT pad_all: 0 bg_opa: TRANSP shadow_opa: TRANSP border_opa: TRANSP border_width: 0 widgets: - label: align: LEFT_MID text_font: icons_28 text_color: color_misty_blue text: "${fan_icon}" - dropdown: id: vacuum_fan_speed_dropdown x: 30 width: 80 height: 40 align: LEFT_MID options: [] bg_opa: TRANSP shadow_opa: TRANSP border_opa: TRANSP border_width: 0 text_font: nunito_16 text_color: color_misty_blue dropdown_list: bg_opa: TRANSP shadow_opa: TRANSP border_opa: TRANSP border_width: 0 text_font: nunito_16 text_color: color_misty_blue text_line_space: 5 pad_all: 8 max_height: 200 selected: checked: bg_color: color_steel_blue bg_opa: 20% text_color: color_misty_blue pressed: bg_opa: TRANSP indicator: text_opa: TRANSP on_value: - homeassistant.action: action: vacuum.set_fan_speed data: entity_id: "${vacuum_entity}" fan_speed: !lambda |- auto& speeds = id(vacuum_fan_speeds); if (x >= 0 && x < (int)speeds.size()) { return speeds[x]; } return std::string(""); # battery level label - obj: x: 10 y: 0 width: 140 height: 50 align: BOTTOM_LEFT pad_all: 0 bg_opa: TRANSP shadow_opa: TRANSP border_opa: TRANSP border_width: 0 widgets: # state - image: align: LEFT_MID id: vacuum_battery_level_img src: battery_very_high_img # charging animation - animimg: id: vacuum_charging_animation align: LEFT_MID hidden: true src: [ battery_0_img, battery_20_img, battery_40_img, battery_60_img, battery_80_img, battery_very_high_img, ] duration: 1500ms - label: id: vacuum_battery_level_label x: 20 align: LEFT_MID text_font: nunito_16 text_color: color_misty_blue text: " " # state label - obj: x: -10 y: 0 width: 340 height: 50 align: TOP_RIGHT pad_all: 0 bg_opa: TRANSP shadow_opa: TRANSP border_opa: TRANSP border_width: 0 widgets: - label: align: RIGHT_MID id: vacuum_state_label text_font: nunito_16 text_color: color_misty_blue text: "" # cleaned area label - obj: x: -10 y: 0 width: 140 height: 50 align: BOTTOM_RIGHT pad_all: 0 bg_opa: TRANSP shadow_opa: TRANSP border_opa: TRANSP border_width: 0 widgets: - label: align: RIGHT_MID text_font: icons_28 text_color: color_misty_blue text: "${room_plan_icon}" - label: x: -40 align: RIGHT_MID id: vacuum_cleaned_area_label text_font: nunito_16 text_color: color_misty_blue text: "" # without animation - image: id: vacuum_not_animation y: 10 align: CENTER src: vacuum_img # return to base animation - animimg: id: vacuum_go_home_animation hidden: true y: 10 align: CENTER src: [ vacuum_img, vacuum_5_img, vacuum_10_img, vacuum_15_img, vacuum_10_img, vacuum_5_img, vacuum_img, vacuum_355_img, vacuum_350_img, vacuum_345_img, vacuum_350_img, vacuum_355_img, ] duration: 2000ms # cleaning animation - animimg: id: vacuum_cleaning_animation hidden: true y: 0 align: CENTER src: [ vacuum_img, vacuum_0_10_img, vacuum_0_20_img, vacuum_0_30_img, vacuum_0_20_img, vacuum_0_10_img, vacuum_img, vacuum_5_img, vacuum_10_img, vacuum_15_img, vacuum_20_img, vacuum_20_10_img, vacuum_20_20_img, vacuum_20_30_img, vacuum_20_20_img, vacuum_20_10_img, vacuum_20_img, vacuum_15_img, vacuum_10_img, vacuum_5_img, vacuum_img, vacuum_0_10_img, vacuum_0_20_img, vacuum_0_30_img, vacuum_0_20_img, vacuum_0_10_img, vacuum_img, vacuum_355_img, vacuum_350_img, vacuum_345_img, vacuum_340_img, vacuum_340_10_img, vacuum_340_20_img, vacuum_340_30_img, vacuum_340_20_img, vacuum_340_10_img, vacuum_340_img, vacuum_345_img, vacuum_350_img, vacuum_355_img, ] duration: 6000ms # docked state controls - obj: id: vacuum_bg_controls_docked x: 20 y: 280 width: 440 height: 100 pad_all: 10 align: TOP_LEFT bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP border_width: 0 shadow_opa: TRANSP radius: 10 layout: type: FLEX flex_align_main: SPACE_AROUND flex_align_cross: CENTER widgets: - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${play_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "docked";' then: - homeassistant.action: action: vacuum.start data: entity_id: "${vacuum_entity}" - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${locate_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "docked";' then: - homeassistant.action: action: vacuum.locate data: entity_id: "${vacuum_entity}" # cleaning state controls - obj: id: vacuum_bg_controls_cleaning hidden: true x: 20 y: 280 width: 440 height: 100 pad_all: 10 align: TOP_LEFT bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP border_width: 0 shadow_opa: TRANSP radius: 10 layout: type: FLEX flex_align_main: SPACE_AROUND flex_align_cross: CENTER widgets: - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${pause_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "cleaning";' then: - homeassistant.action: action: vacuum.pause data: entity_id: "${vacuum_entity}" - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${stop_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "cleaning";' then: - homeassistant.action: action: vacuum.stop data: entity_id: "${vacuum_entity}" - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${docked_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "cleaning";' then: - homeassistant.action: action: vacuum.return_to_base data: entity_id: "${vacuum_entity}" # paused state controls - obj: id: vacuum_bg_controls_paused hidden: true x: 20 y: 280 width: 440 height: 100 pad_all: 10 align: TOP_LEFT bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP border_width: 0 shadow_opa: TRANSP radius: 10 layout: type: FLEX flex_align_main: SPACE_AROUND flex_align_cross: CENTER widgets: - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${play_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "paused";' then: - homeassistant.action: action: vacuum.start data: entity_id: "${vacuum_entity}" - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${docked_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "paused";' then: - homeassistant.action: action: vacuum.return_to_base data: entity_id: "${vacuum_entity}" # idle state controls - obj: id: vacuum_bg_controls_idle hidden: true x: 20 y: 280 width: 440 height: 100 pad_all: 10 align: TOP_LEFT bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP border_width: 0 shadow_opa: TRANSP radius: 10 layout: type: FLEX flex_align_main: SPACE_AROUND flex_align_cross: CENTER widgets: - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${play_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "idle";' then: - homeassistant.action: action: vacuum.start data: entity_id: "${vacuum_entity}" - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${locate_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "idle";' then: - homeassistant.action: action: vacuum.locate data: entity_id: "${vacuum_entity}" - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${docked_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "idle";' then: - homeassistant.action: action: vacuum.return_to_base data: entity_id: "${vacuum_entity}" # returning state controls - obj: id: vacuum_bg_controls_returning hidden: true x: 20 y: 280 width: 440 height: 100 pad_all: 10 align: TOP_LEFT bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP border_width: 0 shadow_opa: TRANSP radius: 10 layout: type: FLEX flex_align_main: SPACE_AROUND flex_align_cross: CENTER widgets: - button: width: 80 height: 80 bg_opa: TRANSP shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_misty_blue text_font: icons_38 text: "${play_icon}" on_press: - if: condition: lambda: 'return id(vacuum_state_sensor).state == "returning";' then: - homeassistant.action: action: vacuum.start data: entity_id: "${vacuum_entity}" # Return - button: x: 20 y: 400 width: 60 height: 60 align: TOP_LEFT bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP shadow_opa: TRANSP radius: 10 widgets: - label: align: CENTER text_font: icons_28 text_color: color_misty_blue text: "${exit_icon}" on_press: - lvgl.page.show: devices_page - lvgl.widget.show: menu_controls_main # vacuum entity friendly name - obj: id: vacuum_bg_name x: -20 y: 400 width: 360 height: 60 align: TOP_RIGHT bg_color: color_steel_blue bg_opa: 20% border_opa: TRANSP border_width: 0 shadow_opa: TRANSP radius: 10 widgets: - label: id: vacuum_label_name align: CENTER text_font: nunito_16 text_color: color_misty_blue text: "friendly name" script: - id: vacuum_get_and_set_translated_state parameters: state_str: string then: - lambda: |- std::string state = state_str; auto it = id(vacuum_translations).find(state); std::string translated_state = (it != id(vacuum_translations).end()) ? it->second : state; lv_label_set_text(id(vacuum_state_label), translated_state.c_str()); select: - platform: lvgl widget: language_dropdown id: vacuum_select_language on_value: then: - delay: 300ms - script.execute: id: vacuum_get_and_set_translated_state state_str: !lambda 'return id(vacuum_state_sensor).state;' esphome: on_boot: priority: -100 then: - if: condition: lambda: 'return id(vacuum_state_sensor).has_state();' then: - script.execute: id: vacuum_get_and_set_translated_state state_str: !lambda 'return id(vacuum_state_sensor).state;' api: on_client_connected: - if: condition: lambda: 'return id(vacuum_state_sensor).has_state();' then: - script.execute: id: vacuum_get_and_set_translated_state state_str: !lambda 'return id(vacuum_state_sensor).state;'