From aa2137f3f856ef859ffed186ac1863762a745c13 Mon Sep 17 00:00:00 2001
From: Max Gramser
Date: Tue, 15 Sep 2026 15:22:28 +0200
Subject: [PATCH] Choose vacuum, vac & mop or mop on the vacuum card, and a
climate card for the CYD (0.2.46)
App 0.2.46 / firmware 0.2.39:
- The vacuum's state message carries extras from its own device: the cleaning mode and water
selects (found by translation key, then entity id ending, so localized ids work), the suction
speeds to offer, the battery sensor (HA 2026.8 dropped battery_level), charging and the current
room. Speeds and water levels a mode covers move into the mode row; Custom only while in use.
The manager watches these entities and resends only the vacuum's message when one changes.
- Vacuum card redesign on both boards: robot with state, battery, charging bolt and room; Start
(Pause, Resume) and Dock; then segmented rows for mode, suction and water, showing only the rows
the chosen mode uses. Taps show at once and wait for Home Assistant; Locate is a round button in
the Guition hero.
- CYD climate card without the arc: big setpoint between large -/+ keys (debounced, hold to
repeat) and one row of mode keys in HA's order and colours, with a more key for fan and swing.
- Both profiles clear climate_edit_pending when the climate card is closed, instead of running
ui_refresh every second while the flag stayed set.
Tested with the owner's Roborock S8 on HA 2026.9.1 (live data, OTA-flashed bench boards) and with
host renders of both boards; see docs/TEST_RESULTS_0246.md.
Co-Authored-By: Claude Opus 5
---
README.md | 11 +-
TILE_CONFIGURATION.md | 17 +-
components/smart_display/runtime_model.h | 17 +
components/smart_display/runtime_tiles.h | 308 +++++++--
components/smart_display/tile_controls.h | 63 ++
components/smart_display/tile_icon_names.h | 1 +
docs/RELEASING.md | 35 +
docs/TEST_RESULTS_0246.md | 64 ++
docs/images/cyd-climate.png | Bin 0 -> 14607 bytes
docs/images/cyd-vacuum.png | Bin 0 -> 18891 bytes
docs/images/guition-vacuum.png | Bin 31548 -> 37230 bytes
guition-4848s040.yaml | 14 +-
home-like-2432s028.yaml | 730 +++++++++++++--------
packages/cyd.yaml | 730 +++++++++++++--------
packages/guition.yaml | 14 +-
screen_manager/CHANGELOG.md | 11 +
screen_manager/app/core.py | 110 +++-
screen_manager/app/server.py | 26 +-
screen_manager/app/static/tile-icons.woff | Bin 15484 -> 15516 bytes
screen_manager/app/tile_icons.py | 2 +
screen_manager/config.yaml | 2 +-
tests/test_tile_controls.cpp | 38 ++
tests/test_vacuum_card.py | 137 ++++
23 files changed, 1675 insertions(+), 655 deletions(-)
create mode 100644 docs/TEST_RESULTS_0246.md
create mode 100644 docs/images/cyd-climate.png
create mode 100644 docs/images/cyd-vacuum.png
create mode 100644 tests/test_vacuum_card.py
diff --git a/README.md b/README.md
index 88f41bd..55b7711 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,11 @@ The firmware and built-in CLI are tested with **ESPHome 2026.6.2**.
per tile; **None** keeps the regular card.
- **Weather card** with current weather, the coming hours and days including chance of
rain or millimeters; **climate card** with an on/off button, mode, fan, and swing settings.
+ On the CYD the climate card has big − / + keys and one row of mode keys instead of a dial.
+- **Vacuum card** with the state, battery and charging, start and dock, and how the robot
+ cleans: **vacuum, vacuum and mop, or mop only** for robots that offer a cleaning mode in
+ Home Assistant (such as Roborock), then suction and water. Only the rows the chosen mode
+ uses are shown (app 0.2.46 / firmware 0.2.39).
Scenes, scripts, and buttons show when they last ran. A tile that's waiting on
Home Assistant shows a small spinner on a light overlay.
- **Top bar per screen** (firmware 0.2.32+): the name on the left, up to six
@@ -99,10 +104,14 @@ The firmware and built-in CLI are tested with **ESPHome 2026.6.2**.
-
+
+
+
+
+
Features depend on the capabilities Home Assistant reports for an entity.
The app must keep running to keep the screens supplied with current data.
diff --git a/TILE_CONFIGURATION.md b/TILE_CONFIGURATION.md
index c0f99da..5098ed5 100644
--- a/TILE_CONFIGURATION.md
+++ b/TILE_CONFIGURATION.md
@@ -47,8 +47,9 @@ vertically, so the grid can be taller than the screen.
The background is a two-stop vertical gradient (`BG_TOP_COLOR` ->
`BG_BOTTOM_COLOR`), not an image. That saves ~150 KB of flash and removes an
image blit from every redraw, which is what makes scrolling usable on this
-board. The same gradient is reused as the backdrop of the slider, color and
-thermostat overlays through the `style_bg_grad` style.
+board. The same gradient is reused as the backdrop of the slider and color
+overlays through the `style_bg_grad` style; the climate card uses the plain
+light background of the other cards.
## Accent colors are automatic
@@ -167,9 +168,9 @@ Only relevant when `TILE*_TYPE` is set to `"climate"`.
| Key | Default | Description |
|-----|---------|-------------|
-| `TILE*_CLIMATE_MIN_TEMP` | `"5.0"` | Minimum target temperature shown in the thermostat overlay. |
-| `TILE*_CLIMATE_MAX_TEMP` | `"30.0"` | Maximum target temperature shown in the thermostat overlay. |
-| `TILE*_CLIMATE_STEP` | `"0.5"` | Step size for the thermostat ring and +/- buttons. Values below `0.5` are rounded up to `0.5`, so the UI always adjusts in whole or half-degree steps. |
+| `TILE*_CLIMATE_MIN_TEMP` | `"5.0"` | Minimum target temperature on the climate card. |
+| `TILE*_CLIMATE_MAX_TEMP` | `"30.0"` | Maximum target temperature on the climate card. |
+| `TILE*_CLIMATE_STEP` | `"0.5"` | Step size of the − / + keys. Values below `0.5` are rounded up to `0.5`, so the UI always adjusts in whole or half-degree steps. |
| `TILE*_CLIMATE_CURRENT_TEMP_ATTRIBUTE` | `"current_temperature"` | Home Assistant attribute used for the current room temperature. |
| `TILE*_CLIMATE_TARGET_TEMP_ATTRIBUTE` | `"temperature"` | Home Assistant attribute used for the target temperature. |
| `TILE*_CLIMATE_HUMIDITY_ATTRIBUTE` | `"current_humidity"` | Home Assistant attribute used for humidity. Leave the default if the entity does not provide humidity. |
@@ -210,7 +211,7 @@ In all modes, the event `tileN_long_press` is always published to `sensor.smartd
For light tiles in `slider` mode, the brightness overlay automatically shows a color-ring button when `DIRECT_ACTIONS` is `"true"` and Home Assistant reports a supported color mode (`hs`, `rgb`, `rgbw`, `rgbww`, `xy`) or color temperature mode (`color_temp`). The color detail view can send `light.turn_on` with `color_name`, `color_temp_kelvin`, and the current brightness percentage. If the light only supports color or only supports color temperature, the Color/Temp tab switch is hidden and only the supported controls are shown. No extra tile substitutions are required.
-For climate tiles in `slider` mode, the thermostat overlay sends `climate.set_temperature` with the configured target temperature. The min/max range, step size, and Home Assistant attribute names are configured through the `TILE*_CLIMATE_*` substitutions. The mode pill cycles through the climate entity's supported `hvac_modes` and sends `climate.set_hvac_mode` when `DIRECT_ACTIONS` is `"true"`.
+For climate tiles in `slider` mode, the climate card shows the target temperature between big − / + keys: the number follows every tap at once, holding a key keeps stepping, and one `climate.set_temperature` goes out once the taps stop (0.7 s). The min/max range, step size, and Home Assistant attribute names are configured through the `TILE*_CLIMATE_*` substitutions. Below it, one key per supported `hvac_mode` (in Home Assistant's order, off is the power key) sends `climate.set_hvac_mode` when `DIRECT_ACTIONS` is `"true"`; the ··· key opens the picker with fan and swing modes.
---
@@ -390,7 +391,7 @@ TILE4_LONGPRESS_ACTION_SERVICE: ""
---
-### Climate — thermostat ring on long press
+### Climate — climate card on long press
```yaml
TILE5_ENTITY: "climate.living_room"
@@ -399,7 +400,7 @@ TILE5_TITLE: "Living"
TILE5_ICON: "\U000F0438" # mdi:radiator
TILE5_TYPE: "climate"
TILE5_TAP_ACTION: "auto" # no default tap action; use TAP_SERVICE if desired
-TILE5_LONGPRESS: "slider" # long press opens thermostat ring
+TILE5_LONGPRESS: "slider" # long press opens the climate card
TILE5_VALUE_MODE: "auto" # shows target temperature, or current temperature as fallback
TILE5_LABEL_OFF: "Off"
TILE5_LABEL_ON: "Heat"
diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h
index 2271542..1cfb64b 100644
--- a/components/smart_display/runtime_model.h
+++ b/components/smart_display/runtime_model.h
@@ -31,6 +31,16 @@ inline std::string state_revision(const std::string &state, const std::string &a
}
struct Forecast { std::string day, condition; float high = NAN, low = NAN, rain = NAN, mm = NAN; };
struct Hour { std::string time, condition; float temp = NAN, rain = NAN, mm = NAN; };
+// One row of choices on a vacuum card (firmware 0.2.39+), found by the manager on the robot's device:
+// kind 'm' a cleaning mode select (Roborock: vacuum, mop or both), 'w' a water or mop intensity
+// select, 's' the suction speeds of the vacuum itself. `values` go to Home Assistant, `labels` are
+// shown; a cleaning mode carries one role letter per value (v vacuum only, m mop only, b both,
+// a automatic: the robot or the app decides suction and water). `sent` is the value just tapped.
+struct Choice {
+ char kind = 0;
+ std::string entity, current, roles, sent;
+ std::vector values, labels;
+};
struct Tile {
std::string entity, name, state, unit, modes, hvac_modes, fan_modes, swing_modes, fan_mode, swing_mode;
std::array fan_speeds;
@@ -66,6 +76,13 @@ struct Tile {
std::string sunrise, sunset, duration, remaining;
uint32_t timer_end = 0;
float battery = NAN, volume = NAN;
+ // Vacuum only: mode, water and suction rows (see Choice); an empty vector costs nothing elsewhere.
+ // `room` is where the robot is and `charging` whether it charges, both from sensors of its device.
+ std::vector choices;
+ std::string room;
+ bool charging = false;
+ Choice *choice(char kind) { for (auto &c : choices) if (c.kind == kind) return &c; return nullptr; }
+ const Choice *choice(char kind) const { for (auto &c : choices) if (c.kind == kind) return &c; return nullptr; }
uint32_t supported = 0, background = 0;
bool transparent = false; // "Background: none": card fill and border hidden, contents unchanged.
std::string icon; // UTF-8 glyph of a chosen icon the icon fonts contain; empty keeps the domain icon.
diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h
index 0ed4179..ee9a336 100644
--- a/components/smart_display/runtime_tiles.h
+++ b/components/smart_display/runtime_tiles.h
@@ -278,6 +278,8 @@ inline std::string receive(const std::string &payload) {
auto a = root["a"].as();
// ArduinoJson clears its destination string: serialize attributes first, then add state.
std::string attributes; serializeJson(a, attributes);
+ // The extras count too: a vacuum's mode or water select changes only there.
+ if (!root["x"].isNull()) { std::string more; serializeJson(root["x"], more); attributes += more; }
std::string revision = state_revision(string(root["state"]), attributes);
bool was_confirmed=tile.confirmed;
tile.observe(revision);
@@ -369,6 +371,30 @@ inline std::string receive(const std::string &payload) {
if (tile.fan_speed_count == 4) break;
tile.fan_speeds[tile.fan_speed_count++] = string(speed, 48);
}
+ // Vacuum rows (app 0.2.46+): the cleaning mode and water selects of the robot's device and the
+ // suction speeds to offer, each at most six; the battery sensor when the vacuum has no attribute.
+ // A chip just tapped keeps its choice while Home Assistant is still busy with it, so another update
+ // of the robot (its battery, say) does not flip the row back for a moment.
+ std::vector> tapped;
+ if (tile.loading(esphome::millis())) for (auto &c : tile.choices) if (!c.sent.empty()) tapped.emplace_back(c.kind, c.sent);
+ tile.choices.clear();
+ auto choice = [&](const char *key, char kind) {
+ auto c = extra[key];
+ if (!c["o"].is()) return;
+ Choice row; row.kind = kind;
+ row.entity = string(c["e"], 120); row.current = string(c["s"], 48); row.roles = string(c["r"], 6);
+ if (kind != 's' && !valid_entity(row.entity)) return;
+ for (JsonVariant value : c["o"].as()) { if (row.values.size() == 6) break; row.values.push_back(string(value, 48)); }
+ if (c["l"].is()) for (JsonVariant label : c["l"].as()) {
+ if (row.labels.size() == row.values.size()) break; row.labels.push_back(string(label, 24)); }
+ while (row.labels.size() < row.values.size()) row.labels.push_back(row.values[row.labels.size()]);
+ if (!row.values.empty()) tile.choices.push_back(std::move(row));
+ };
+ if (tile.domain() == "vacuum") { choice("mode", 'm'); choice("water", 'w'); choice("fan", 's'); tile_controls::settle_suction(tile); }
+ for (auto &[kind, value] : tapped) if (auto *c = tile.choice(kind)) if (c->current != value) c->sent = value;
+ if (!std::isfinite(tile.battery)) tile.battery = number(extra["bat"]);
+ tile.charging = extra["chg"].is() && extra["chg"].as() == 1;
+ tile.room = string(extra["room"], 32);
// Home Assistant reports the edited value: the -/+ pill follows its state again.
if(std::isfinite(tile.edit_value) && tile.edit_sent && std::fabs(tile_controls::edit_target(tile)-tile.edit_value)<0.051f)tile.edit_value=NAN;
tile.received = true;
@@ -411,7 +437,7 @@ namespace runtime_tiles {
inline lv_obj_t *detail_root=nullptr;
inline unsigned detail_index=0;
inline const lv_font_t *detail_font=nullptr;
-inline lv_obj_t *detail_actions[16]{};
+inline lv_obj_t *detail_actions[32]{};
inline unsigned detail_action_count=0;
inline lv_obj_t *detail_status=nullptr;
inline lv_obj_t *detail_badge_status=nullptr;
@@ -477,6 +503,17 @@ inline void slider_event(lv_event_t *e){
if(changed && cyd::touch_guard.accept_slider(esphome::millis(),200+index))commit_slider(index,lv_slider_get_value(slider));
}
}
+inline void show_detail(unsigned index);
+// A chip on the vacuum card: the service call goes out, the chip shows the choice at once, and the card
+// waits for Home Assistant like after its other buttons. The card is drawn again once this tap's event
+// has finished, because a chosen mode can add or remove the suction and water rows.
+inline void choose(Tile &t,Choice &row,const std::string &value){
+ if(value==row.current)return;
+ auto a=tile_controls::choice_action(t,row.kind,value);if(!a.valid())return;
+ action(a.service,row.kind=='s'?t.entity:row.entity,a.key,a.value);
+ row.sent=value;t.begin(esphome::millis());
+ lv_async_call([](void *){if(detail_root && !lv_obj_has_flag(detail_root,LV_OBJ_FLAG_HIDDEN))show_detail(detail_index);},nullptr);
+}
inline lv_obj_t *detail_button(const char *text,int x,int y,int width,int height,int command){
auto *button=lv_obj_create(detail_root);lv_obj_remove_style_all(button);lv_obj_set_pos(button,x,y);lv_obj_set_size(button,width,height);
lv_obj_set_style_bg_color(button,lv_color_hex(command==0?0x009FE3:0xD9E6F0),0);lv_obj_set_style_bg_opa(button,LV_OPA_COVER,0);lv_obj_set_style_radius(button,12,0);lv_obj_add_flag(button,LV_OBJ_FLAG_CLICKABLE);
@@ -486,7 +523,10 @@ inline lv_obj_t *detail_button(const char *text,int x,int y,int width,int height
if(!fresh()||detail_index>=model.count || !allowed(esphome::millis(),300+cmd,"card button "+model.tiles[detail_index].entity))return;
auto &t=model.tiles[detail_index];if(!t.available()||t.loading(esphome::millis()))return;
if(cmd<4){const char *services[]={"vacuum.start","vacuum.pause","vacuum.return_to_base","vacuum.locate"};action(services[cmd],t.entity);}
- if(cmd>=10 && cmd<14 && cmd-10<(int)t.fan_speed_count)action("vacuum.set_fan_speed",t.entity,"fan_speed",t.fan_speeds[cmd-10]);
+ // Vacuum rows: 10-15 suction, 50-55 cleaning mode, 60-65 water.
+ static const std::pair rows[]={{10,'s'},{50,'m'},{60,'w'}};
+ for(const auto &[first,kind]:rows)
+ if(cmd>=first && cmdvalues.size())choose(t,*row,row->values[cmd-first]);}
if(cmd==20)action("media_player.media_play_pause",t.entity);
if(cmd==21)action("media_player.media_previous_track",t.entity);
if(cmd==22)action("media_player.media_next_track",t.entity);
@@ -497,7 +537,7 @@ inline lv_obj_t *detail_button(const char *text,int x,int y,int width,int height
lv_obj_set_style_bg_color(button,lv_color_hex(0x0075B0),LV_STATE_PRESSED);
lv_obj_set_style_transform_width(button,-2,LV_STATE_PRESSED);lv_obj_set_style_transform_height(button,-2,LV_STATE_PRESSED);
lv_obj_set_style_opa(button,LV_OPA_50,LV_STATE_DISABLED);
- if(command>=0 && detail_action_count<16)detail_actions[detail_action_count++]=button;
+ if(command>=0 && detail_action_count<32)detail_actions[detail_action_count++]=button;
return button;
}
@@ -600,6 +640,195 @@ inline void render_weather_detail(const Tile &t,bool large,int width,int height,
detail_text(days,std::isfinite(f.low)?degrees(f.low):"",temps_x+high_w,scy,low_w,small,LV_TEXT_ALIGN_RIGHT,muted);
}
}
+// ---- Vacuum card ----
+// A robot with its state and battery, the two commands used most, and one block that says how it cleans:
+// the cleaning mode, then suction and water. Only the rows the chosen mode uses are shown, and they sit
+// below the mode, so a tap never moves the control under the finger. Native shapes and labels only.
+// Native shapes keep the robot crisp without image buffers or extra layers.
+inline lv_obj_t *detail_shape(lv_obj_t *parent,int x,int y,int w,int h,uint32_t color,int radius){
+ auto *o=lv_obj_create(parent);lv_obj_remove_style_all(o);lv_obj_set_pos(o,x,y);lv_obj_set_size(o,w,h);
+ lv_obj_set_style_radius(o,radius,0);lv_obj_set_style_bg_color(o,lv_color_hex(color),0);lv_obj_set_style_bg_opa(o,LV_OPA_COVER,0);
+ lv_obj_remove_flag(o,LV_OBJ_FLAG_CLICKABLE);lv_obj_remove_flag(o,LV_OBJ_FLAG_SCROLLABLE);return o;
+}
+inline int text_width(const std::string &text,const lv_font_t *font){
+ lv_point_t size;lv_text_get_size(&size,text.c_str(),font,0,0,LV_COORD_MAX,LV_TEXT_FLAG_NONE);return size.x;
+}
+// The state colour, as Home Assistant colours a vacuum: teal while cleaning, blue on the way back, amber
+// when paused, red on an error; a docked or idle robot keeps the card's own blue. `tint` is the halo.
+struct VacuumLook { uint32_t accent, tint; };
+inline VacuumLook vacuum_look(const std::string &state){
+ if(state=="cleaning")return {0x009688,0xDCF0EE};
+ if(state=="returning")return {0x2196F3,0xE1EFFD};
+ if(state=="paused")return {0xFF9800,0xFFF0DA};
+ if(state=="error")return {0xF44336,0xFDE4E2};
+ return {0x00A6ED,0xEAF5FC};
+}
+// A robot seen from above on a halo: a white body with a rim, the laser turret, a light bar in the state colour.
+inline void vacuum_robot(lv_obj_t *parent,int x,int y,int size,const VacuumLook &look){
+ auto *halo=detail_shape(parent,x,y,size,size,look.tint,size/2);
+ int r=size*72/100,rim=std::max(1,size/44);
+ auto *body=detail_shape(halo,(size-r)/2,(size-r)/2,r,r,0xFFFFFF,r/2);
+ lv_obj_set_style_border_width(body,rim,0);lv_obj_set_style_border_color(body,lv_color_hex(0xCEDDE6),0);
+ int c=r-2*rim;auto s=[&](int v){return std::max(1,v*c/100);};
+ detail_shape(body,s(32),s(10),s(36),s(36),0xE3EBEF,s(18));
+ detail_shape(body,s(41),s(19),s(18),s(18),0xA8BCC8,s(9));
+ detail_shape(body,s(31),s(70),s(38),std::max(3,s(7)),look.accent,s(4));
+}
+// A battery like a phone's: outline, level fill (red below 20 %) and a small cap.
+inline void vacuum_battery(lv_obj_t *parent,int x,int y,int w,int h,float level){
+ int line=std::max(1,h/7),fill_w=w-4*line,fill=std::clamp((int)std::lround(fill_w*level/100.0f),0,fill_w);
+ auto *shell=detail_shape(parent,x,y,w,h,0xFFFFFF,std::max(2,h/4));
+ lv_obj_set_style_bg_opa(shell,LV_OPA_TRANSP,0);
+ lv_obj_set_style_border_width(shell,line,0);lv_obj_set_style_border_color(shell,lv_color_hex(0x7D858D),0);
+ if(fill)detail_shape(shell,line,line,fill,h-4*line,level<20?0xE53935:0x46525E,std::max(1,h/9));
+ detail_shape(parent,x+w,y+h*3/10,std::max(2,line+1),h-2*(h*3/10),0x7D858D,1);
+}
+// Battery meter, level and a green bolt while charging, in one row from `x`; returns the row's width.
+// Without a parent it only measures.
+inline int vacuum_power(lv_obj_t *parent,const Tile &t,int x,int y,const lv_font_t *font,bool large){
+ const lv_font_t *bolt_font=large && watch_icon_font?watch_icon_font:mini_icon_font;
+ std::string percent=std::to_string((int)std::lround(t.battery))+"%";
+ int h=lv_font_get_line_height(font),meter_w=large?30:22,meter_h=large?15:11,gap=large?10:6,words=text_width(percent,font);
+ int bolt_w=t.charging && bolt_font?text_width("\U000F0241",bolt_font):0;
+ int width=meter_w+3+gap+words+(bolt_w?gap/2+bolt_w:0);
+ if(!parent)return width;
+ vacuum_battery(parent,x,y+(h-meter_h)/2,meter_w,meter_h,t.battery);
+ int px=x+meter_w+3+gap;
+ detail_text(parent,percent,px,y,words+2,font,LV_TEXT_ALIGN_LEFT,0x5F6368);
+ if(bolt_w)detail_text(parent,"\U000F0241",px+words+gap/2,y+(h-lv_font_get_line_height(bolt_font))/2,bolt_w+2,bolt_font,LV_TEXT_ALIGN_LEFT,0x43A047);
+ return width;
+}
+// Put a detail_button's words in `font`, sized to the words and centred (or at `x` when given).
+inline lv_obj_t *button_words(lv_obj_t *button,const lv_font_t *font,uint32_t color,int available,int x=-1){
+ auto *label=lv_obj_get_child(button,0);
+ lv_obj_set_style_text_font(label,font,0);lv_obj_set_style_text_color(label,lv_color_hex(color),0);
+ lv_obj_set_size(label,std::min(available,text_width(lv_label_get_text(label),font)+2),lv_font_get_line_height(font));
+ if(x<0)lv_obj_center(label);else lv_obj_align(label,LV_ALIGN_LEFT_MID,x,0);
+ return label;
+}
+// A command button with its icon before the words, the two centred as one group.
+inline lv_obj_t *vacuum_command(const char *icon,const char *text,int x,int y,int w,int h,int command,bool primary,
+ const lv_font_t *font,const lv_font_t *icon_font,int radius){
+ auto *button=detail_button(text,x,y,w,h,command);
+ uint32_t ink=primary?0xFFFFFF:0x1B1B1B;
+ lv_obj_set_style_radius(button,radius,0);
+ lv_obj_set_style_bg_color(button,lv_color_hex(primary?0x009FE3:0xFFFFFF),0);
+ if(!primary){
+ lv_obj_set_style_border_width(button,1,0);lv_obj_set_style_border_color(button,lv_color_hex(0xDDDDDD),0);
+ lv_obj_set_style_bg_color(button,lv_color_hex(0xEEEEEE),LV_STATE_PRESSED);
+ }
+ int gap=std::max(6,h/7),icon_w=text_width(icon,icon_font),words=text_width(text,font);
+ int group=icon_w+gap+words,left=std::max(4,(w-group)/2);
+ auto *glyph=lv_label_create(button);lv_label_set_text(glyph,icon);lv_obj_remove_flag(glyph,LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_set_style_text_font(glyph,icon_font,0);lv_obj_set_style_text_color(glyph,lv_color_hex(ink),0);
+ lv_obj_align(glyph,LV_ALIGN_LEFT_MID,left,0);
+ button_words(button,font,ink,w-left-icon_w-gap-4,left+icon_w+gap);
+ return button;
+}
+// A segmented control: one rounded track, each option as wide as its words plus an equal share of the
+// room left, the chosen one filled in blue. Commands first..first+5.
+inline void vacuum_segments(const Tile &t,const Choice &row,int first,int x,int y,int w,int h,uint32_t track,bool border,const lv_font_t *font){
+ int n=std::min((int)row.values.size(),6);if(!n)return;
+ auto *bar=detail_shape(detail_root,x,y,w,h,track,h/2);
+ if(border){lv_obj_set_style_border_width(bar,1,0);lv_obj_set_style_border_color(bar,lv_color_hex(0xDDDDDD),0);}
+ int inset=std::max(3,h/12),room=w-2*inset,words=0;int widths[6];
+ for(int i=0;i24)
+ detail_text(detail_root," · "+t.room,pad+dot+10+state_w,y,words-state_w,text,LV_TEXT_ALIGN_LEFT,0x5F6368);
+ if(battery)vacuum_power(detail_root,t,pad+inner-power_w,y,text,false);
+ y+=line+(gap+4);
+ }
+ // Start (or pause, or resume) as the one blue button, dock beside it.
+ int action_h=large?54:(small_hero?40:36),start_w=large?(inner-gap)*2/3:(inner-gap)/2;
+ vacuum_command(cleaning?"\U000F03E4":"\U000F040A",cleaning?(large?"Pause cleaning":"Pause"):paused?"Resume":(large?"Start cleaning":"Clean"),
+ pad,y,start_w,action_h,cleaning?1:0,true,text,icons,large?radius:action_h/2);
+ vacuum_command("\U000F05F8","Dock",pad+start_w+gap,y,inner-start_w-gap,action_h,2,false,text,icons,large?radius:action_h/2);
+ y+=action_h+gap;
+ if(!mode && !suction && !water){
+ auto *note=detail_text(detail_root,"Automatic suction power",pad,y+gap,inner,text,LV_TEXT_ALIGN_CENTER,0x6B6B6B);(void)note;
+ return;
+ }
+ // How it cleans. The Guition groups the rows on one white card; the small screen has no room for a card.
+ int mode_h=large?48:34,row_h=large?44:32,step=large?10:6,edge=large?14:0,icon_w=large?40:26;
+ int note_h=lv_font_get_line_height(text);
+ int block=(mode?mode_h:0)+(suction?(mode?step:0)+row_h:0)+(water?((mode||suction)?step:0)+row_h:0)+(automatic?step+note_h+step:0);
+ uint32_t track=large?0xF1F1F1:0xFFFFFF;
+ if(large)detail_card(pad,y,inner,block+2*edge);
+ int x=pad+edge,w=inner-2*edge;
+ y+=edge;
+ if(mode){vacuum_segments(t,*mode,50,x,y,w,mode_h,track,!large,text);y+=mode_h+step;}
+ auto level=[&](const Choice &row,int first,const char *icon){
+ detail_text(detail_root,icon,x,y+(row_h-lv_font_get_line_height(icons))/2,icon_w,icons,LV_TEXT_ALIGN_LEFT,0x6B6B6B);
+ vacuum_segments(t,row,first,x+icon_w,y,w-icon_w,row_h,track,!large,text);
+ y+=row_h+step;
+ };
+ if(suction)level(*suction,10,"\U000F0210");
+ if(water)level(*water,60,"\U000F058C");
+ if(automatic){
+ bool smart=tile_controls::shown_value(t,*mode,now).find("smart")!=std::string::npos;
+ detail_text(detail_root,smart?"The robot chooses suction and water":"Suction and water as set per room",x,y,w,text,LV_TEXT_ALIGN_CENTER,0x6B6B6B);
+ }
+}
inline void show_detail(unsigned index){
if(index>=model.count)return;detail_index=index;auto &t=model.tiles[index];
if(!detail_font)detail_font=lv_obj_get_style_text_font(widgets[0].title,LV_PART_MAIN);
@@ -617,8 +846,8 @@ inline void show_detail(unsigned index){
lv_obj_set_style_text_font(heading,title_font,0);lv_obj_set_height(heading,lv_font_get_line_height(title_font));lv_obj_set_style_text_align(heading,LV_TEXT_ALIGN_CENTER,0);
std::string state=detail_state(t);
auto d=t.domain();
- // The large vacuum card carries its state in the badge of the hero; a second line would crowd it.
- if(!(d=="vacuum" && large)){detail_status=detail_label(detail_root,state+(t.unit.empty()?"":" "+t.unit),pad,large?80:50,width-2*pad);lv_obj_set_style_text_align(detail_status,LV_TEXT_ALIGN_CENTER,0);lv_obj_set_style_text_color(detail_status,lv_color_hex(0x616161),0);}
+ // The vacuum card draws its own state (hero or status row) with the battery beside it.
+ if(d!="vacuum"){detail_status=detail_label(detail_root,state+(t.unit.empty()?"":" "+t.unit),pad,large?80:50,width-2*pad);lv_obj_set_style_text_align(detail_status,LV_TEXT_ALIGN_CENTER,0);lv_obj_set_style_text_color(detail_status,lv_color_hex(0x616161),0);}
if(t.is_switch()){
detail_label(detail_root,"Tap to toggle",pad,top,width-2*pad);
detail_switch=lv_switch_create(detail_root);
@@ -641,50 +870,7 @@ inline void show_detail(unsigned index){
},LV_EVENT_VALUE_CHANGED,nullptr);
detail_actions[detail_action_count++]=detail_switch;
}else if(d=="vacuum"){
- // Native shapes keep the robot crisp without image buffers or extra layers.
- auto shape=[&](lv_obj_t *parent,int x,int y,int w,int h,uint32_t color,int radius){
- auto *o=lv_obj_create(parent);lv_obj_remove_style_all(o);lv_obj_set_pos(o,x,y);lv_obj_set_size(o,w,h);
- lv_obj_set_style_radius(o,radius,0);lv_obj_set_style_bg_color(o,lv_color_hex(color),0);lv_obj_set_style_bg_opa(o,LV_OPA_COVER,0);
- lv_obj_remove_flag(o,LV_OBJ_FLAG_CLICKABLE);lv_obj_remove_flag(o,LV_OBJ_FLAG_SCROLLABLE);return o;
- };
- uint32_t surface=0xFFFFFF, muted=0xEAF5FC;
- if(large){
- auto *hero=shape(detail_root,pad,100,width-2*pad,148,surface,24);
- shape(hero,18,14,120,120,muted,60);
- auto *robot=shape(hero,35,27,86,86,0xFFFFFF,43);
- lv_obj_set_style_border_width(robot,2,0);lv_obj_set_style_border_color(robot,lv_color_hex(0xCEDDE6),0);
- shape(robot,27,10,30,30,0xE3EBEF,15);shape(robot,35,18,14,14,0xA8BCC8,7);
- shape(robot,29,59,26,5,0x00A6ED,3);
- detail_label(hero,t.state=="cleaning"?"Working":t.state=="returning"?"Charging":t.state=="paused"?"Paused":"Ready for your home",154,24,260);
- auto *badge=shape(hero,154,59,240,32,muted,16);
- auto *status=detail_label(badge,t.awaiting_action(esphome::millis())?"Command sent...":state,12,5,218);
- detail_badge_status=status;
- lv_obj_set_style_text_color(status,lv_color_hex(0x087BA8),0);
- detail_label(hero,std::isfinite(t.battery)?"Battery "+std::to_string((int)t.battery)+"%":"Connected via Home Assistant",154,107,260);
- detail_button(t.state=="cleaning"?"Pause cleaning":"Start cleaning",pad,260,width-2*pad,58,t.state=="cleaning"?1:0);
- detail_button("Return to dock",pad,330,cw,48,2);
- detail_button("Find my robot",pad+cw+gap,330,cw,48,3);
- detail_label(detail_root,"Suction power",pad,394,width-2*pad);
- top=424;
- }else{
- auto *robot=shape(detail_root,pad,64,46,46,muted,23);
- shape(robot,16,8,14,14,0xA8BCC8,7);shape(robot,15,32,16,3,0x00A6ED,2);
- detail_label(detail_root,std::isfinite(t.battery)?"Battery "+std::to_string((int)t.battery)+"%":"Robot vacuum",pad+58,66,width-2*pad-58);
- detail_label(detail_root,"Choose an action",pad+58,87,width-2*pad-58);
- detail_button(t.state=="cleaning"?"Pause":"Clean",pad,120,cw,38,t.state=="cleaning"?1:0);
- detail_button("To dock",pad+cw+gap,120,cw,38,2);
- detail_label(detail_root,"Suction power",pad,168,width-2*pad);top=194;
- }
- if(!t.fan_speed_count)detail_label(detail_root,"Automatic suction power",pad,top,width-2*pad);
- int count=std::max(1,(int)t.fan_speed_count),sw=(width-2*pad-gap*(count-1))/count;
- for(unsigned i=0;ivalues.size() && i < mode->roles.size(); ++i)
+ if (mode->values[i] == value) return mode->roles[i];
+ return 'b';
+}
+// Rows under the mode: suction unless the robot only mops, water (when the robot has a water select)
+// unless it only vacuums; an automatic mode leaves both to the robot or its app.
+struct VacuumRows { bool suction = false, water = false; };
+inline VacuumRows vacuum_rows(const Tile &t, uint32_t now) {
+ char role = vacuum_role(t, now);
+ VacuumRows rows;
+ rows.suction = role != 'm' && role != 'a' && t.choice('s');
+ rows.water = role != 'v' && role != 'a' && t.choice('w');
+ return rows;
+}
+// Labels for a manager that sends no suction row (app before 0.2.46): the vacuum's own speed names.
+inline std::string speed_label(const std::string &speed) {
+ if (speed == "quiet") return "Quiet";
+ if (speed == "balanced") return "Normal";
+ if (speed == "turbo") return "Turbo";
+ if (speed == "max") return "Max";
+ return speed;
+}
+// The suction row a vacuum card shows: the manager's (filtered, labelled) speeds, else the first four
+// of the vacuum's own list. Its value is always the vacuum's fan_speed.
+inline void settle_suction(Tile &t) {
+ auto *row = t.choice('s');
+ if (!row && t.fan_speed_count) {
+ runtime_tiles::Choice legacy; legacy.kind = 's';
+ for (unsigned i = 0; i < t.fan_speed_count; ++i) { legacy.values.push_back(t.fan_speeds[i]); legacy.labels.push_back(speed_label(t.fan_speeds[i])); }
+ t.choices.push_back(std::move(legacy));
+ row = &t.choices.back();
+ }
+ if (row) row->current = t.fan_speed;
+}
+// The service call behind a chip: a select option on the device, or the vacuum's own fan speed.
+inline Action choice_action(const Tile &t, char kind, const std::string &value) {
+ if (kind == 's') return {"vacuum.set_fan_speed", "fan_speed", value};
+ const auto *row = t.choice(kind);
+ if (!row || row->entity.empty()) return {};
+ return {"select.select_option", "option", value};
+}
+
// The debounced -/+ edit lands as one service call.
inline Action edit_action(const Tile &t, float value) {
if (!std::isfinite(value)) return {};
diff --git a/components/smart_display/tile_icon_names.h b/components/smart_display/tile_icon_names.h
index 4892c25..e8931e3 100644
--- a/components/smart_display/tile_icon_names.h
+++ b/components/smart_display/tile_icon_names.h
@@ -185,6 +185,7 @@ inline constexpr Named NAMES[] = {
{"weather-pouring", 0xF0596},
{"weather-snowy-rainy", 0xF067F},
{"weather-sunset-down", 0xF059B},
+ {"dots-horizontal", 0xF01D8},
};
inline constexpr size_t NAME_COUNT = sizeof(NAMES) / sizeof(NAMES[0]);
// Codepoint of a name the fonts carry; 0 for any other name.
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index a8f29f4..2e19f76 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -176,6 +176,41 @@ icons sit off-center in the browser); run `generate_packages.py` afterward.
The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual
profiles must come from the set. No changed preferences or keys.
+### Compatibility 0.2.46 / firmware 0.2.39
+
+Storage version, tile protocol (`v: 1`), preferences and keys are unchanged. Additive wire fields only.
+
+- A vacuum's state message carries new extras in `x` (`core.vacuum_extras`): `mode` and `water`
+ (`e` select entity, `s` state, `o` options, `l` labels, `r` one role letter per mode: v/m/b/a),
+ `fan` (the suction speeds to offer, `o`/`l`), `bat` (0..100), `chg` (1 while charging) and
+ `room`. Options are capped at six, labels at 24 bytes. With a mode select, speeds and water
+ levels that a mode covers (`off`, `custom`, `smart_mode`, …) leave the rows, and `custom` leaves
+ the mode row unless it is the current mode. `a.fan_speed_list` stays capped at four, so firmware
+ before 0.2.39 draws its old suction row; firmware 0.2.39 with an older app does the same.
+- `core.vacuum_related` finds the selects, the battery sensor (`device_class: battery`), the
+ charging binary sensor (`battery_charging`) and the room sensor on the vacuum's device, by
+ translation key first (entity ids follow the HA language) and entity id ending second; disabled
+ entities are skipped. `Manager.device_entries` indexes the registry per device (rebuilt with the
+ registry object), `related_entities` adds these ids to `watched_entities` and to the `sync_one`
+ reuse check, so a select change resends the vacuum's message.
+- Firmware: `Tile::choices` (`Choice`: kind m/w/s, entity, current, roles, values, labels, `sent`),
+ `room`, `charging`; the state revision now includes `x`, so a select change confirms a pending
+ command. Chips send `select.select_option` (or `vacuum.set_fan_speed`), mark the vacuum tile
+ pending and redraw the card through `lv_async_call` after the event. `tile_controls::vacuum_rows`
+ / `vacuum_role` / `shown_value` / `settle_suction` / `choice_action` are covered by
+ tests/test_tile_controls.cpp. Detail commands: 10-15 suction, 50-55 mode, 60-65 water;
+ `detail_actions` holds 32 buttons. `tick()` redraws an open vacuum card when its state text changes.
+- CYD climate card (home-like-2432s028.yaml): the arc is gone. New widgets `climate_setpoint_card`,
+ `climate_mode_keys`, `cmkey_*`, font `setpoint_digits` (Roboto 400, 40 px, digits . - °), globals
+ `active_climate_current`, `active_climate_action`, `active_climate_target_known`, scripts
+ `climate_card_refresh` and `climate_target_send` (restart mode, 700 ms, entity and value as
+ parameters); `climate_target_preview`/`climate_target_commit` are removed. `tile_controls::mode_color`
+ holds HA's mode colours (also used by `domain_accent`). Glyph `dots-horizontal` (F01D8) joins
+ `tile_icons.FIXED`.
+- Both profiles: the 1 s interval clears `climate_edit_pending` when the climate card is closed and,
+ on runtime tiles, replays the held-back state through `detail_update`, instead of running
+ `ui_refresh` every second while the flag stayed set.
+
### Compatibility 0.2.45 (firmware stays 0.2.38)
App only: storage version, tile protocol, preferences and keys are unchanged.
diff --git a/docs/TEST_RESULTS_0246.md b/docs/TEST_RESULTS_0246.md
new file mode 100644
index 0000000..6c6d800
--- /dev/null
+++ b/docs/TEST_RESULTS_0246.md
@@ -0,0 +1,64 @@
+# Test results app 0.2.46 / firmware 0.2.39 (2026-09-15)
+
+Cleaning mode, suction and water on the vacuum card, the vacuum card redesign on both boards, the
+new CYD climate card and the cleared climate edit flag. See CHANGELOG 0.2.46 and RELEASING
+"Compatibility 0.2.46".
+
+## Automated
+
+- Python (`.venv-portal`): 170 tests OK.
+ - New: `tests/test_vacuum_card.py` covers the extras on the owner's Roborock data (Dutch entity
+ ids), a robot without a mode select, a plain vacuum, entity id endings, disabled entities and
+ unknown modes (an Ecovacs-style `work_mode`), bounded options, and that a select change marks
+ the vacuum dirty and resends only its message.
+- C++ (`clang++ -std=c++17 -Wall -Wextra -Werror -I . -I components/smart_display`): 13/13 PASS;
+ `test_tile_controls.cpp` adds `mode_color`, `vacuum_rows`, `vacuum_role`, `shown_value`,
+ `settle_suction` and `choice_action`.
+- `generate_packages.py --check` green, `generate_icons.py --check` verified (150 pickable icons,
+ 179 glyphs).
+- ESPHome 2026.6.2 compile SUCCESS, one after another: `easy-cyd-device.yaml` (RAM 31.9%, flash
+ 78.5%), `easy-guition-device.yaml` (RAM 32.8%, flash 18.8%), `guition-device.yaml` (RAM 32.9%,
+ flash 19.8%), `device.yaml` (RAM 32.0%, flash 83.2%). Packages carry `SCREEN_FIRMWARE_VERSION` 0.2.39.
+
+## Home Assistant facts (owner's HA 2026.9.1, read only)
+
+- Roborock S8 (`roborock.vacuum.a51`): `vacuum.s8` has no `battery_level`; its device holds
+ `select.woonkamer_s8_schoonmaakmodus` (translation key `cleaning_mode`: vacuum, vac_and_mop,
+ mop, custom), `select.s8_intensiteit_van_dweilen` (`mop_intensity`: off, mild, standard, intense,
+ custom), `sensor.s8_batterij` (battery), `binary_sensor.s8_opladen` (battery_charging) and
+ `sensor.s8_huidige_kamer` (`current_room`). Entity ids follow the UI language, so matching goes
+ by translation key.
+- The Roborock cleaning mode select exists in HA 2026.8.0 and 2026.9.1 (`RoborockCleaningModeSelectEntity`).
+- `core.extras` on this data: mode vacuum / vac_and_mop / mop (roles `vbm`), water mild / standard /
+ intense, suction quiet … max_plus, battery, charging and room; the message is 612 bytes.
+
+## Bench boards (OTA)
+
+- Both Easy Setup builds uploaded over the network; `device_info` reports project 0.2.39, compiled
+ 2026-09-15 15:11:48 (CYD) and 15:12:20 (Guition), matching the build logs.
+- The real `vacuum.s8` state with the new extras, built by the app code from live HA data, was sent
+ through `screen_message` and the card opened with `preview_runtime_card` on both boards. The
+ Guition capture (`capture_ui.py`) shows Docked, 83 % with the charging bolt, Start cleaning and
+ Dock, Vac & mop, Max and Intense. The owner looked at both screens.
+- The owner tapped Vacuum on the CYD: Home Assistant switched `select.woonkamer_s8_schoonmaakmodus`
+ to `vacuum` (15:16:12) and the robot set its water to off and suction to balanced. The production
+ app was still 0.2.45 and resent the vacuum without the new extras, so the card fell back to
+ suction only, as described for mixed versions in the CHANGELOG.
+
+## Renders (ESPHome host + SDL, both boards, this release's code)
+
+- Vacuum card with the owner's data in ten states: docked and charging, cleaning with the room,
+ mop only, vacuum only, Custom (note instead of rows), returning, error, paused at 12 % (red
+ battery), a vacuum without selects (small hero on the CYD) and an Ecovacs-style robot with long
+ labels (Vac, then mop; Ultra high). No label is cut off; the mode row keeps its place.
+- CYD climate card: heating with humidity, an air conditioner with five modes plus ···, and off
+ (grey setpoint). The README images `guition-vacuum.png`, `cyd-vacuum.png` and `cyd-climate.png`
+ come from these renders.
+
+## Not tested / for the owner
+
+- The vacuum rows with app 0.2.46 on the Yellow after the store update (live updates of mode,
+ water, battery and room while the robot cleans).
+- The CYD climate card by hand with the real air conditioner (− / + taps, holding, mode keys, ···).
+- Other brands only through the tests with their documented entity names; no Dreame or Ecovacs
+ robot was available.
diff --git a/docs/images/cyd-climate.png b/docs/images/cyd-climate.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3b264885cbb0202b0f796769b552090f351369f
GIT binary patch
literal 14607
zcmbt*c{tSV_y2pWspyG97>coEmo@5}E^p6By?KY#p|>vwfsU9K_i_qoqG_v`gK=iKkmJ6fvLrZ(DF9h!UWZ(6E3{8RW4)C&_^NO7G7DVy^GvCuMuIS^pfidoLpSl
zgvU+iZdhB?r{bFVhl`^#o2r{$e!KH)T3wom&g=Vu_jEeu+t=1oR@;5jR%eN;ve~)0
zSL0Ez)@8PM+8hjc~p*lU8jeCwW*fvK$r1S@EN@V;CYa9P_|D1~{3Wl(c=%>95KL@n6oZE|H+ly1r
zHpEKu9&f*;iPwnKx%f9_uKe4i
z2dB3JlAKqiKi@*Zt|nXN>)W5DaHc3HB}xfa%1Dr~!29>&G_rNqD?j6JPPV4ZUxA~P
z1Zh8KiG=L#HydM!YN)K6?&U6R##JI7TfZhLf--3iz|DO#W|Crh+R!{u=(|@_m0jnz
zNutNpiqAt*G9mO_m$<=I(h@>LMMkq5a2!(khS&V8n
z5Q3;rH|G>n>|I&9nbcORhJuBT;K&3en!SSt7+3G9`)w`?K#bM1dxA)lqm!xl!M{Dd
ztnWg?qK2B{Y&dYs)l&z5sby|I8GCFOPoME;s$#Sr?*f0EI{XqNY2o7|WAwIejK?(?
zT#{n*7FnF(1FZ|Thdhed?i3;|%D8@B33UDiPyG1lrLgmJgfEJn_yd6i0_pI;yc8>6
z+;=#p$f$sXWtdHf)n2@%5e1v>Cs{uTDv>ohtketAWSV?x_i#Q7kI3!pZ=s8H{cQUQ
zXizISkW!CJsi!{c^61ilwP*|s&crmVe)sO(GcC^4sCzN(%I_GWWA@h7VZkvMnyp|F
zDA*&HkLy1n?s1{blimR%cNQ0Zyw@`(GtZYE_D;!-)?N{jHQ=AIINDEE;iXF=%bA%&
zU%Vr57VJE5T2k#jYv$R?!4
z-fc1ldU}W3DA@Wpf2(h9k4IH9HpK(^*e>fC(SFWRHo>@5$`@L6lidN`@&-_OCm}7Z
z&WdAWVP>|6lbEXBL1m$n1tsnFRV1h8JXCM`Ja7;NCeNq>Z}L0d0P0`}e`)}kAx;mn
z81uT7o>Wy<4qVePK*2l~U9!`*1R^vqOl*ddl~x2MB>
z#p__08Iz`j1Okg1Bcmzf$dZm$wICTtFf3KReK5osTv8fZ`jiv?ZovWnjw`4PPVymk
zPSKh&`Oo*%6s%imlDFz+e*JRrK*74J8&$`!+nH^pPgQ@>?^Ij8q{@K7X}OT57tzio
z6~y9jm`h+FD^fRi>P8HVI@K<~+r_~dZ{hX0ERcnFm2xhclTHy#yqHWfVeD@BAf{0;
z>J|%w4Y>v?q}$}-yN6vP77Q&{qBM$Z$YpqAVel_x#d+wUn8%@X1mb2_>}!QHde`a}@#K<0)q&9Z
zZN~?XOdF(}GWBjBFYHO%kk4f_lji5t))Fnh!(*sPQb4XJ>42|?+H$!qyXpj{W$hXA
zNBjPTbs15yKi(4iEh=-YXW2L3m4yDu_x`&}FBD9Er*mHoqF2vM%vnB@irvY^IA`PW
z1(On=`H%nP3)w4wt{^VtyK|*(&UNehfMDI4E-<#g=ta7;UK9j12*fVCAuuCdXNvr%
z`5;pMTf#ulZbi_Uh8Bf`<#@v%%#u!IB2E5Fo03i!`kv5}e+$LfF-
z*XBz=Ey0v%w6eOqo8tEUDin%aBShl@+5{?6)_mB4_Lez=x|Vtp>MtPei|Xi+h+Q;7
zW|r_9yNccon5fO_YGKB2HOH?WJQwnZAiDw<-MQr!XFW51zCN3iZtS>{aImwsx@M1c
zDT+!y$*w6M%2$t9R69M%3{6bR?tPqLHw@33V>?`C3*MkIm$nEaBJ$K|IYh#ycu`G4
zfy?2FqQUBz`lF4JCJ|^tyz6f#O9*UlJB2=re2A*vl7n+Tz7mM{uJrIPg=q5l(03NMl?9IXhf{bAOP
zmr40*w(KQ78-q5%boRJ+D*ULorCsthGG4zKOn`<$(;B0LedmK;&PeOqMm5i_$+Nvv
z5ik{H6n|2uPC~5PG#hVvMo6+Rw8(7|-S^8vAo}j8^f1kE*iqN4`=_Qw4?APP1VMJ2
zO>BLzKiez**7YE8(<=z4AWyAXL0jz!Hy=9&3NXXZqZF$3=xm?lu>1ArnUSX$oMA*;
z)_vT!N@S|ux*j8}JaJ)Tevnj*?)`L{P^!152F!kTs%Vs8f8Xj-k&v9m^{Ak}A0CA5
zp^opSi~dR^j6)rQQ7{oRVyw3OEL-xkqhO$fArA_uQpJ(9_G#2C(J_Du`#>I<3!2^H
zjEF{M@A}^F71nQUb@e%^CR(t7m;q%sdJLi>O`DcyYsOPQb)C6*6TPRFVB=r(D4ZfTnf|nA&Ya;2){Xw!$ra-gLJZujVP(nfdf!F|v`0BDnWr>ueK`+s*^B<6uy!o)
zev_EE39@;#O4f)NgE(mww3x(icV
ziaF&h+`eM6E_z&W)H5pc`%3RFQYBDb*5JzG3hJ5r8pSEXCm|mN6|(ev>?`4spUjhc
z$Q$Y8EQ@ORghsZ6PwLJtFE!{Sg8bA;@}9wOq|msc6ZXn_0l&9bOO?^biP-wWfV3uI
z3`NX8aKjqfKsuneK2?{7ifMj~>~4AMkt|vwXs(al9~KJ&gdwkCaNRN5hO&~9lDG2X
z?(Xj8(r^^hFXrC!Q4x-;bCbmug=u>QRwY6TT}L!bWdrGrP8AAqeDP|ljuWnX7iLzn
z`n7oBFItQj2BmP!Ho}ms=u&eA1_H-uOpImMPAI;&E|3#$C$v#pAc*y%Fl0>RStH6+
zOZ9@EiyT=%`&1KvFFbRde|~|VDq>1#-%lNtwIQKT5O;mS%*G&a^jTonjpOfvRB!_p
zTwoFu;UTYgNkuQ>bH_GEhfh_qD43Nfjy{M
z#ugcM6N`w6GWT=4Xwn>IbqmjKkYnd2gO}~(N?7b=^{+|G9y|04nJ^u@js~~Wz7|=6
z#-1i<;#|5~-#ZCmRJj`24DX~O(Vi@`**Raj=J77D(XMbf3LVGR6*t;t6?S{IBPt;hAhj`psGgwf#FEVc7;Bq;k7%vgY=)
zslpf5;!6KnvZ_z8BwWsi6W+7*@|jbah>X{h!9KvEGCKPBG?N~I#dQD-0qXzfN6I6m
z!bHo#N|Jl%NgShY2sc|}2e`Po<#>*;2vi^-%AO`Uz>J%V3#i-}vOg1G)|=+@>k_aC5(^Nx)yPI7
zEH6(ZMmNq^RO{ACOixRr7++04lm)cqw;#04bjfX+0ApLtl7+MD`-AEGhN_4lkjBgW
z_mZRg}$42d10tPB36jHF(vpLV<&==tD+ifpLZBW3w;UXHrNTn8%hVKmt2RKip0?
z4%8|m;&VBXeh2h!{kg!Ur$BpWXZhalZOKux6a$%yhxJx1G#Q4Z
zjE4t1I1H;T7e6Hp3os4}mn(pC;A%3+_>n#0R?9nGQj=#h42NH9Ir%!S{^|fmf)4i;
zMbB&CtP3`3L!=aDvpDPJX^S358o^?hcY0>{O!+qJH|vO5Xfyob!KU%ht(qxrGv}Hq
z-cCHIJQHNi7!dvjMa|`qDR#voHr&4gIObvF8WM(JL2o>rty=L
zTQ%%)V$D7vIUivJ7gAUBZ79qh{f_=>nvm%v9okTb^n&;!on5-pOB|2FWe!biJkcY|
zs%{K$qYvW9wrx1M>E
zoOfzfCTI~1a0D!U*=Gpmm_>_G3jtln26SXOR2iln$**9#*e1_~fVOx6jf1^c2eP?S
zS5sw6!PCxah4uUp>JBrGLAKy;W9$y!Z3vnL^%)2z04jbji^6fx84l~WEx7EiTfP#W
z{w(RyxQjs*-K_td4YG4`Yj@f>pxF2rb*%}S`cY}~7?UQPG6^GGRr>SGDGu5gwFqBy
zAfgX=k5Glt>NLJSsGGzO?~g6-FZ-2#$>OB_(2osMzDb9EToN_XhB$DOPMoe9@rj&?
z&JsmUshP81)!_Dbm7m;_-T9p2>2hh@_uN*&HSn~^w%P*2$VD`u*&j#xu%9Ot4!!q|#{S!|BDpZlR%%f4-DugkE&M?2<8*zhoFi`RNmQ%C72
z9TPs;UqI%Py})ZKlbA-HCeJz>&2c4Sl?|UR?^NW|VGnH0+bEpj$2?Wd-+hzTpE}kn
zjcT(PKCgN#EvV=eLs_w%9!8}X{1Q}p+&_~u@Nav#su50
zM$KzPoD;os&H8H3!WBDej?nNd0aH?XxJ92!fyah(-|OZs4WiPQYpurB@i7wU7`}_8
z1{}*;UB7PQ`qryA;%MwCW{aC8t)HqEJgmXkSC`Sj`(D1sMV{3u9hgn~`cF{bIl9q?
z-tLRf#!-c*ihbcTTMmnlo?MM#Y^ru?3x#o^5}mq|Ocsc#V?Qt3&+u83x0uz=u0tmY
zJu<#ou{B`vQT^EQe&3QZYM`)Os9wSPY74$_D8Xwt(=!vN_YOUhe|u;icI%^lxZc8LzLbI_Eo6_3
zR@TBm38`(TO4Pv!Ly2fAU_K`I4^cBV+fPYc$O>iXKM!)JKp$dYV6emw%C~wZD3m-yHy5}gm~dQQ
zf?D?j*unBiNlhg;x3nZdwzk3nY=JRl!@z(++bI(iEcgBd^jlTcb>P>`4CGB>;v*jx
zHa5x(Jzlh|^5DUPjM5j!_JOjM4INkl
zV(gaV;HR&pV4w4}XK6_Tnc*d1uet?Ol6AHEM7{d)r1l>?FW
zUK&@lBRN@VK2+`C-LI}G@L|+>jFexucsgx;DZ);WK5#sLAUFQaDVk{2!!
z$-Z)Y1IG%RPSF>XrI&0{es;&h;6^v+pOt$N%`!Iq{%KxG(lxx9o*1hKW2zDEsvD7P
zZDRxZBVd=wgjEVZq}c6pSiGIq;$!t&XARn+aS9Afi|u!4PBoN`
zii+CsaJ~sHK#e6~Wmf$ueFXgM;Z)`$khM8ELhVbL^h(ljqwq4bqP5XnZ|Ji0qzKkE
zVkolmmgry{Y?paAc`TI7#n+eVci=U
z5@9HN!C`tWG4WDHRa8uT{jG4rMj*^ipIIb0Efm==9(+Pw4M$g}2$y*Cp2@L`wI
zX`{UJe90}TiKWjt2l3wyD2|^4wldDylUvqt5}S57qTELyEjzax%U_g%{usoY9~b|c
zfS(^XJweciTVg<_RO;Mk*c~uR|9Qk{B^_O&2^IdoKF+rs<+MRe-%9ylP&Mk247%fA
z&!qGd@a0w!#5oRCx@P)aSO0{G*{43C>Ls$V(2J{`n&3_z1U)^j2pAW^7Um`#)l_N%
zxUx>xMuD!lgCwP-0hYk_*+t}0e+@X&5PW>
z4bS|yYx6CYokcG2Rh%2nA2-9j`nsxe%^M%E>9M|z*_#$3?Z_e;<9D!kQZf15$wdDd
znFRk-ub)yRh)dx_>bNtM
zeyVY-N~*PGm{szuO8W7JlVyRYULj40uk`!E-+=>(`fD{uugg_m62{VN!yOxu>wA_P
z({7;}{_gwUkpFf(7GD
z+&hv2)7AJh%lS?77|~J68URP@!$h*zPx7_jtgq7?8Ef_DxMz4hSc&2n9qRv~Y}r?k
zRH8OD#EGo&BEDyaBg{6JvRgt2GUyr%W}+pi9@o?b4s-S`q;(yr{Nq_g{SwvmGQApu
z3MZ(tnWUiSgA8p#Q-L04gI4f6&E=RkH0rR@?!tEzqILC%npDM}g-^EdqW#FQ0sL`n
zEJ)sf&7L!9a%e{eA8o*DRPGvzky`rgyKyS>3c7**uqGkw^9UR?6j8HIk{MDm+Ida6
zs!&aN&iMon+RKazC=}5~>+`c@-Gc8@U$<@7wuE{tgRRJ0^t3Glxi=qs&@W}!9QJU9
z?%{&=?`KruoZD5Rm4xNmWIV}Q)lrXvICh!;fK@%%o);-YdFuAbJ(*pN;&?igS^pf0y|p
z26NI~5oX|60OuTov0jVzeS$y3jY=&F_TP080I;zdtmFPWiho1$6XwrM=2a#cm=Og^
zWwExB92^|1uO~-b8mSd`Q=wtinWUHSXQZrG)4axf1P)ozL11(#lEjl2?9NQCF3a9G
z^@!vC{7*zda5U2&iP!IS(gX
z#87))Q9YCGV&9GAB#`NMlbxhMI5L(G+*@vudsS{RK!-wELM^t!7`jhs^q<>9og|oU;I7cajX1KV1yw}9-hw#q8Bw_3X)^q
zlc;nBQ29eD!ISU6f8F!w2)KN`c%OfJ`vt4-DPhQ3^_Ag2*a{{YZ`e^yZ;R<|Nwi6$
zDodx%_xBlWGowhUbh?}?_kt5t`Ny5wQjn#6${*AiHiNaZTt-zriz5nSAn61a+!82~
z(A8lliQ-zPZ=T2ae5A)$W>pn-&i|0#4j&SZRe;l;Bs5QGEoV^4fpImqZOXYgaG4><
zwRQDVj~r5_zYr@l6bw#_j+r)tVrIUyi}iOGIMQg5qq^=k-T=u+gn_VBH%>0JltHaH
zU$z2D=DcZqFPm4MoGb{-+yPKs$=&3B`RIS3;0|pYmz>TjPH;ICDzAA!K|mJ;2)I~I
zFw6*eI#hv?!>ze{E)03}bRobBE$u8gCp;d}cgZl=CW!g#hK`u4t#6o4gKqY%XHNS^Zb~qgdy`g
zYG@PBXqAHM(W%iWs0@;B)0^|&s?AD8`DAiofB0bO=BO9`W11vw-LO*)wUh`|MAq_`
zGeTtMGo7i5qSrNXF3r`^UZWlipf}GfyFWZSk|$x|eAxFwe(j3+w#Veq0%U{_9Iw4q
zlgTZUsgJ2taU8d!!{!xMpGJ<>c#(EA#p}^}6ACKIWj{3=>DV9saW(9l4vEw?-Jsmn
z)zvidg38gUdo^{Y{m|6KnBjuD$5urAYNyOOTLl?!^t)d|w5tE1?P*>JIGj!s7HDkNs`pF7j#s$`VR_x%tpoh}^-I>zs618eKkI<~5*rSzTR=N_Spl#Y?c*I%28+%kS#oSU
znBh~qa$dx54;Iq@C5^%o0I<A`D0Po9|{2XL@(XPpH`^{y+9+UC{sb^QDUZ
zWpk4YH_~E%XB}XX_pf~TpveEF%q%stgtzej5O3ZSrz%XGszhac*K)k9uUFHBuhP%x
zd|k=a4mgcW-<#qFWLMio1=8`=9`4apOY=Q6vVWT{L%@|*l1
z!e?~-AZ%g5ImoZ=XfhgojAelLW)0pn2fw*PD54zymF!HB6#Dc(l28+vJ@`KMSw{3)
zRhs^eEL@{%bIcFoJr$!0&HzDb!$B(NKeYi{U59(x8HxN_T3RGasDxTFq-5<+>f@dv
zAbT`zP4OIhBtZ39=x{}d)aB=M29hNrn0Ub6KK^hY9(1r!fv0I`Z$JA(6L7BlNdc5Q
z4*zN``dAvkV6D@NB$D6HvZQq&|_0PBmtCH&Jm(QuC-
zlZr>e1VOO4<2x_zuG7X4YCJNi`h}M2O739b?34iZitORuYQmnsKfYI6p+h%xVi#<7
z#s@{GGk5K}D{Q)_i5=A=e=B;?Pfoqfk<7-6?Gv3hM$daym*C#X_}hwG>J#a-Wg#h*
zX6Di{XGHS63fO=8t>+=A#ca-NY)GCcomMhoRln8tmC
zCrLS`1pN1B?!tw9V3WO+Gx!qLU_>qbvCc&svHqoK?>k7gP({mtI<;TrPNSz&r0gqF
z-()irC{(B|V4lmhvqjNU#3Pxl!5OwJeXIRiRFN0=U;pentNv`t5&W3-{c3``W$Rv@
z3QkT=w)fE+J*xCGUvul!Bxo(NpC2i)q6EN^bUfwphUM-R=m%Rny4P|X4@6MkZc>l`
z{%zZqs$QX*Z&jNYq^a@)D26
z@wJow82di1vwj^05#z%hmSHlbIioLSvbMZ*#pC(*t6H?YC$;z%flTa|xZdO9Go-!m
zakJFN`+2!1tf(frDJJVSbqX*pcUZPfpI6GVy8;7|>yo^PQ<&?8MRM@MOO_@#ayjkr
z?X4HbjSq8yqdO{&c88WDeFUiXi7w4brZSH(-&`1`Y}i;BmYV}B^On$~9)WM2S>_Q8
zg7~_|P8G2pax}n@;sbBpTy0hs(F$9q_3QZ4e`rW6l(|3{ZfZ%Ea9rUBuXnwrEe+_M
zeD`No2VPzI;714c07zVx#&2X)ZDxoEgTt{ax!d7WLe0u`Y3ufXbcQ+N>nTZ7Iw?_6
z(gyG>172sr8%`IjUqlyG!}g92NXOvk#Vg>3
zzZp(25MADHZb0=WUYyu&=zAlEta);No*$s2qZ5dZIo15K8o_6yfy(-MQQUty>LDyp
zB;g8cOG}F}EhZlpbmHJQlxU-Zhh+7|#p8xxZ!O>=Ve_lZtCBHSv*Ry-J&Zxt5($-o
z*;Hk)o%GfW5T5Xot4-5=Rsa;1?7W+;;t`
zy`;8(AwfNxQ$dSO$m4fIETAdB!Ugf{I}dQ?
z1LhQRTeK5mzO#qJDPVkR9GeMma3aHr3ulfKpIYsaogMGhFG93lJgckgO%EL$<rY
zseoqCgDOkQuH7Br@Mz?7Q;VpmC~y>>V(^k!ybK#UX|K6(XVPl3Ke~7AAYNvwJjgs!
z=G(iuwM|6I2uZ`*^#mN7(BPxY73p6d?Vi@V;AjBM#y5bcm?ayu0LSV$Qh6S^PQa1g
z$<>psf#&Gv0W5O97m$rF&XE=u7YplA-5P2x8q>vaIKcw5jXZehja+vkr
z&n_G>f^9EvC$)Pb12fFVT=CSa;wVJdJUMONja!+($amYSYjwqpQO
zn4%$snpxJK)Me`1c>p|Jlw$U*D`^p_zBU08a|7B3jZH$gLdQo|gQ$>C?=SE+5vM)c
z32zqKH~!YE4uu#x;@g&;TSwP}AU0QHN(S$MgAr}xPUDpH_M+Eg1D~yiZSY1xb?&N~
z2j5&A#|`8@4{Tz)P%og$e|CCRXrb-omb!XxQd}
zh*by!&*uENLUm9^QOln!TN$c9L8kdGsDI{ri`|WdwC5zgE3MqdcFa4ewb@=o=ZvU91lNx*Z7KjPl!Ymk3;^
zkHuWMFmt`ruk2~#{gEk;{BRt4&Ew-J#t&TS>h8%gvj_YvJw54>pk#o8o$uDST6>H=
z4~}}ENRX>eBbS$#3*`@I+3It~GVJfsDIyOy8+Rtz>R(xWB-i}DJ~fr=r2wuSjjOPL
zllTQdnae%jACwCn`?i>(IKeWyrWW72CJh$I8(c*bdtQEBjjhh}*S4QpPqet;NP283
zy9M714bFN*^kYa%Mm(GiP#7~rO9(nO7jt4G$bnz!gaidiiXZj9k5|oqere9I5zGZU
zAC^mo#-{>?7K%tS+}09*Y61xw2O(J$uk-}WJ@)m1wED$1vld?$C`xNBh=)D3L
zHgKNoB^Dd)Pi2esH{AU&Ieeom+?Ja!mD*&DV1OBe|K6OV@~MfU)(Fb|T=pl$x_ill
z7i+FKE_Pfd?vVI~M6cuSVmNU2%6@uWf
z{wd(k$lW&)X!ce4km45R)xPHJ=`z;D`a(6Dm0Z7mW&8JCg$Ag?2wmm<1sD-m(qG71
z+(YOv!hinecYEEa$f|J+@v-8>_ubJ<4Q3rit#%8Didf7$9Er;A%Z*Q#{p2C+vjlxN
z$$a=Y?=ak~$ck^Y%?pjgIMs>9Y%+a8o3*Trb-rE>>^TP4wM+`(v6|*2O#N7EZ0S&l
ztq=j6uyk+U|F~MRlpORm|7}Qd28agnGFYvUHR*D!m*7x9&<9-hyDnNYf``Mvj>dY$
ze|DyB>1|J~eBO>+=Jq0XXWx=%?7Xof?Pwm6?gTv4*}MGqR8TW!pHbgb1pI6Id3W{$
zj&UCBZp8Es`YVrKRmv|=an6Qkppi$nKtOT@>-IG&)j93!GBRf>>(9%XQ7IIO({#(f
ztIO9z>mttO)=E)|;R5(-r%`o}QIc8W!6j~ijt|-ijC@6-b7diR1^9F?`?{!$*{K2*
zf>U<5*!hi3YU2}rY2gEBL>?ZoRnMRAj3paaIFW-y@ac|>@8cSJXLMJ>rcBE0>?7d#
zK8cE|>ixC-;e)+TJJ9x#7)ws%sQTPc|BAro9(gWAHXev_1kCYlqY4*rC1(AnY8YIc
z3$2tN*?j+-%+^XNgoV7sltTQx4dH=D-y;R!xCnt&nMH6_v3ovulJ^GroZZ
zCa_IqND68Rm|HBMB7kjg*L+KScKa-jYe+R)*RX%HE8UgGoJ8%0{Z=*SP;0SS3s&0L
z4UPWRcum8-09W#EGl9wBWRGjtAo=}vAQ|`LZ1VK;o9gO0Wyk
z8nc6B%00F>&75@?q%@yw^tz_!chHkW?1~x(dIg>NQqNU~Wb>d!T@Q|yiXUF%lz|K^rFP*C-|7os#~Si(lf2;H2JdCV
zt9p6_YopeG?Zj?EWj}dBnKwWfE&BtbU|wb$V16|OlUCJdKB{0%FAb8Rn^73LRnZ}4
zSI^}Y^!)9>&qgCmY&mI>)!KCG3k!|^M&c0TBQZ7GaxN)7ozlE5erLyoS;B$wyUZ`G
z*rxu)?nJcA^3j;GIOs4iqpDD+$YJ+4AHho^-2LlE@(bK~`+1QOkYeIa(26v+v99Y<
z&r}f}S7Kpr(^!Z<35zDMYw-^$b~U0GOA`C#q^*P+xzoH!K;afzzc;e{f>0W)z1`em
z#9R%(tgLzPr8J@T%#pkq{GSX*3--RF`{(=Y0Px24Xz3lr;|o-*A^Te6T6LxMG$Ti<
zhV0b@7t+1!kQ1OWL!T+K;vM>zoSPDdz|a#u?VdiJ?rD>QJjnF@c^gG{`r+Tde=8!o
z$`vsAfgeP@nqaR;I(DFWU11~Ot2yV^*1e&t($+F}&4v?Bgnhtyaq95!aKK@xj0$;I
zTduzpb&K*GE7|yux3_=@u+h>dakf3!H}Y$_w(0Z^@`7z0rUGwx*!1f&R>|kT7bC4J
zy~NC1+8n&se+`tZ5BatmVSmLcfVTJjPun%|gH7KDM6Lxcx~HG1B$2*kT(t+@Re99r
z6TTW-H5DxZAGg5|U6UDrf5SRb$pFtV`;DOtvki8R54nOpH0o?A_R`T@
zyvy?3QCq|1s5NWY`xjp~L&uTaF`s-I)PGH5Ka+pFCm;7
zZuDZ{mh)tDB#@85j2Yg^_b>11&Qv|V9RFj)@0uNBNCzmdNgA3!34eIUu0Q(_sYzi7
z@9~<)x2*
zkLRt|eERsCPEXs3Z%6Jp^tGz6OI@pDvrTuQ*0?z^OT1Ygr)Js1_8~gKpgW2kWcS=ao61cQKqYk_Y`cF
z{xIMtjh2+p@sS08cbc!+H##9Wfb|?$aK9}nIxG8Qs{wpJ1C=!qRx(EVa6DP@q74~X
z_3JEEvE3U_emH)9eAj>y#C4_5gv0uCvbq$%kTt=y_0(Vit8@c=O-Q`i-~H(Ir+B15
zw(cw@xFb?KURDdNgq~m8<7^0q!OckKg-9=sUmBkj!!v^;e_fF#ljvy4X8$-f0DST4
z5#5sM`13wIx2Nt)uzas7+UFxxAUgoB7Q8@a`H%R0p$yJrX2a!Ef}3I|I`4z8P0ckM
z6kQ-yMB1TX9B`CBbnCM46XPb3dZq_0tu!}3!{GI4uvWF#J8^drdJrdy113gtdQB4W
zrpPzrm5LvXZTlWp4)?Fik1$`2o1+Vig+X$_yS@}--CLV@=ZFg$STeAcAM+v!1EUCn
zqiQonLh^Ld*B}~a=PZ+4E_t`Oax}YH_1Q8Vd4=mP=4SViH*y>ei_{N+%U4pbELXsx_Mwex7lFfN%Mf?v)KGO-K3->
zBVV#NEW113T@|L*z!&N$8=N6Cl~zZteCj{EDKbt(!M59)FBd7Wq=kSV<;DEmb5k1U`gT&Jg?XpEm*{r}u0l8b7vt2n1i&J7iN!@crFcPsa_u
P=m@}-wUi2Pnm_qpWqm*{
literal 0
HcmV?d00001
diff --git a/docs/images/cyd-vacuum.png b/docs/images/cyd-vacuum.png
new file mode 100644
index 0000000000000000000000000000000000000000..226f2e8735b542a9e7a589ac566e3590abe87eb4
GIT binary patch
literal 18891
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
N002ovPDHLkV1n+0>EZwY
literal 0
HcmV?d00001
diff --git a/docs/images/guition-vacuum.png b/docs/images/guition-vacuum.png
index d59540475e02daa8a17fef01822cd40cbaa1a896..fecd26def0bb7e200df59c2d7e252a6fe3433680 100644
GIT binary patch
literal 37230
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%0DX!TIUlsbh&
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