From 0109bfd6d2a244a18baa03dde872085eebc4a371 Mon Sep 17 00:00:00 2001 From: Max Gramser Date: Sat, 12 Sep 2026 18:29:06 +0200 Subject: [PATCH] Add persistent native Guition rotation to screen settings --- components/smart_display/runtime_tiles.h | 17 ++++++++++++- docs/GUITION.md | 14 +++++++++++ docs/RELEASING.md | 10 ++++++++ docs/TEST_RESULTS_029.md | 23 +++++++++++++++++ guition-4848s040.yaml | 15 ++++++++++- home-like-2432s028.yaml | 2 +- packages/cyd.yaml | 2 +- packages/guition.yaml | 15 ++++++++++- screen_manager/CHANGELOG.md | 5 ++++ screen_manager/app/core.py | 5 ++++ screen_manager/app/server.py | 9 ++++++- screen_manager/app/static/app.js | 12 ++++++--- screen_manager/config.yaml | 2 +- tests/test_portal.py | 4 +-- tests/test_rotation.py | 32 ++++++++++++++++++++++++ 15 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 docs/TEST_RESULTS_029.md create mode 100644 tests/test_rotation.py diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index 0e0f1c3..6cffdf7 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -15,6 +15,9 @@ namespace runtime_tiles { inline bool enabled = false; inline bool light_theme = false; inline bool swipe_pages = false; +inline uint32_t rotation = 0; +inline bool rotation_supported = false; +inline esphome::ESPPreferenceObject rotation_preference; inline esphome::ESPPreferenceObject swipe_preference; inline Model model; inline std::string inbox; @@ -30,6 +33,11 @@ inline void load_settings() { swipe_preference = esphome::global_preferences->make_preference(0x53575031); uint32_t swipe_saved=0; if(swipe_preference.load(&swipe_saved))swipe_pages=swipe_saved==1; + if(rotation_supported){ + rotation_preference=esphome::global_preferences->make_preference(0x524F5431); + uint32_t saved=0; + if(rotation_preference.load(&saved) && saved<=270 && saved%90==0)rotation=saved; + } screen_settings::Settings saved; if (settings_preference.load(&saved) && saved.valid()) screen_settings::current = saved; } @@ -94,17 +102,24 @@ inline std::string receive(const std::string &payload) { entities.push_back(entity.as()); } if(!root["swipe_pages"].isNull() && !root["swipe_pages"].is())return false; + if(!root["rotation"].isNull() && (!root["rotation"].is() || + root["rotation"].as()>270 || root["rotation"].as()%90!=0))return false; inbox = string(root["inbox"], 160); bool changed = false; if (!model.set_layout(entities, string(root["title"], 96), changed)) return false; if(root["swipe_pages"].is() && swipe_pages!=root["swipe_pages"].as()){ swipe_pages=root["swipe_pages"].as();uint32_t saved=swipe_pages?1:0;swipe_preference.save(&saved); } + bool rotation_changed=false; + if(rotation_supported && root["rotation"].is() && rotation!=root["rotation"].as()){ + rotation=root["rotation"].as();rotation_preference.save(&rotation);rotation_changed=true; + } if (!(settings == screen_settings::current)) { screen_settings::current = settings; settings_preference.save(&settings); // ESPHome batches flash writes; no write on keepalive. - if (settings_changed) settings_changed(); + rotation_changed=true; } + if(rotation_changed && settings_changed)settings_changed(); if (changed) { active_index = -1; for (auto &w : widgets) w.cached_active = -1; if (dismiss) dismiss(); } last_received = esphome::millis(); if (layout_changed) layout_changed(); diff --git a/docs/GUITION.md b/docs/GUITION.md index 180233c..a7673a1 100644 --- a/docs/GUITION.md +++ b/docs/GUITION.md @@ -24,6 +24,20 @@ geen `calibration.yaml` of XPT2046-correctie over. ## Interface +### Montage en schermrotatie (runtime 0.2.9+) + +Installeer Guition-firmware 0.2.9 en ESP Screen Manager 0.2.9. Open het scherm +in de beheerpagina, ga naar **Scherminstellingen** en kies bij **Scherm draaien** +0°, 90°, 180° of 270°, met de klok mee. Klik op opslaan. Een firmwareflash is +daarna niet meer nodig om de hoek te wijzigen. De instelling blijft in de appdata +en op het scherm bewaard na een herstart. + +Dit gebruikt de native ESPHome/LVGL-rotatie voor beeld én touch, zonder wijzigingen +aan de paneelinitialisatie of GT911-spiegeling. Controleer na montage de vier +hoeken en de navigatie fysiek; een renderproef test geen aanrakingen. De optie is +alleen zichtbaar voor de Guition nadat zijn nieuwe firmware door HA is ontdekt. +De CYD blijft op zijn bestaande oriëntatie met zijn eigen kalibratie. + Zes tegels van **218 × 108 pixels** per pagina, 12px tussenruimte en een aparte navigatiestrook. Vanaf zeven tegels verschijnt pagina twee; tot zes verdwijnt de navigatie. Grotere letters, iconen, dimmers en kleurkeuzes; de diff --git a/docs/RELEASING.md b/docs/RELEASING.md index a17ce3f..ea9b49f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -101,3 +101,13 @@ breng de indeling eerst terug tot tien of herstel hun gegevensback-up bij rollba `settings.swipe_pages` is additief in opslagversie 1. Op de draad staat het buiten het ongewijzigde elfvelden-settingsobject. De bestaande preferences blijven gelijk; alleen swipe gebruikt een nieuwe eigen uint32-key `0x53575031`, standaard uit. + +### Compatibiliteit 0.2.9 + +Guition-rotatie is additief als `settings.rotation` in appdata, uitsluitend +0/90/180/270. Oude beheerpagina's die het veld weglaten behouden de opgeslagen +hoek. Op de draad staat `rotation` naast het ongewijzigde elfvelden-settingsobject. +De firmware bewaart de hoek als afzonderlijke uint32 op key `0x524F5431`; de +bestaande Settings-structuur en CYD-kalibratie blijven ongewijzigd. Alleen de +Guition activeert de rotatiecallback. De diagnostische entity `Guition schermtype` +maakt deze capability herkenbaar, ook als het scherm offline of hernoemd is. diff --git a/docs/TEST_RESULTS_029.md b/docs/TEST_RESULTS_029.md new file mode 100644 index 0000000..1021a92 --- /dev/null +++ b/docs/TEST_RESULTS_029.md @@ -0,0 +1,23 @@ +# 0.2.9 — Guition-rotatie + +De beheerpagina biedt kwartslagen voor de Guition. De native ESPHome 2026.6.2 +LVGL-component draait beeld en touch samen. De paneelinitialisatie, GT911-transform +en CYD-kalibratie zijn niet aangepast. De aparte opgeslagen hoek verandert de +bestaande settingsstructuur en preference-keys niet. + +- 48 Python-tests geslaagd, inclusief ongeldige hoeken, behoud na appherstart en + opslaan vanuit een oude browser, bordherkenning bij offline status en behoud + van het elfvelden-wireprotocol. +- Alle acht C++-testprogramma's geslaagd; JavaScript-syntax gecontroleerd. +- Beide runtime- en beide handmatige firmwareprofielen gebouwd. +- Guition-runtime op het geïdentificeerde apparaat via OTA geïnstalleerd. +- Renderdiagnose na opdrachten voor 0°, 90°, 180° en 270°: per stand tien + paginacontroles en vijftig overlaycycli geslaagd. Deze controle bedient geen + HA-apparaten en is geen fysieke touchmeting. + +De definitieve Guition-build bevat daarnaast een uitlezing van de gewenste én +werkelijk door LVGL gebruikte hoek via `ui_state`. Na de definitieve OTA-flash +en herstart meldden beide 270°: de voorkeur bleef behouden en werd toegepast. +Daarna is de testhoek weer naar 0° teruggezet. Fysieke touchacceptatie is +afzonderlijk nodig; de bestaande `tools/verify_gt911.py --rotation` ondersteunt +alle vier de hoeken. diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index 3900eff..276f59f 100644 --- a/guition-4848s040.yaml +++ b/guition-4848s040.yaml @@ -1,7 +1,7 @@ # Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911. # Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.8" + SCREEN_FIRMWARE_VERSION: "0.2.9" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "guition-wallbox" @@ -617,6 +617,7 @@ esphome: runtime_tiles::watch_font = id(headline)->get_lv_font(); runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true"; if (!runtime_tiles::enabled) return; + runtime_tiles::rotation_supported = true; runtime_tiles::load_settings(); runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); }; runtime_tiles::dismiss = []() { @@ -778,6 +779,7 @@ api: (unsigned)(millis() - id(last_touch_ms)), id(display_dimmed)); ESP_LOGI("ui_diag", "navigation page=%d runtime_detail=%d", id(tile_page), runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)); + ESP_LOGI("ui_diag", "rotation=%u actual=%u", (unsigned)runtime_tiles::rotation, (unsigned)id(screen_lvgl).get_rotation()); lv_area_t nav, grid; lv_obj_update_layout(id(home_page)->obj); lv_obj_get_coords(id(page_next), &nav); @@ -1334,6 +1336,11 @@ binary_sensor: # - Used by the UI to display fan presets when applicable # ---------------------------------------------------------- text_sensor: + - platform: template + name: "Guition schermtype" + entity_category: diagnostic + update_interval: never + lambda: 'return {"guition"};' - platform: template name: "Schermfirmware" entity_category: diagnostic @@ -1683,6 +1690,7 @@ display: lvgl: + id: screen_lvgl on_draw_start: - lambda: 'id(rendered_frames)++;' displays: @@ -4546,6 +4554,11 @@ script: then: - lambda: |- const auto &settings = screen_settings::current; + if(runtime_tiles::enabled && static_cast(id(screen_lvgl).get_rotation())!=runtime_tiles::rotation){ + cyd::touch_guard.consume(); + if(lv_indev_get_act())lv_indev_wait_release(lv_indev_get_act()); + id(screen_lvgl).set_rotation(static_cast(runtime_tiles::rotation)); + } if (id(display_dimmed) && !settings.standby_enabled) { id(display_dimmed) = false; } diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index e8994ac..845f24e 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -58,7 +58,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.8" + SCREEN_FIRMWARE_VERSION: "0.2.9" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "cyd-new" diff --git a/packages/cyd.yaml b/packages/cyd.yaml index 999d57a..15ce60c 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -59,7 +59,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.8" + SCREEN_FIRMWARE_VERSION: "0.2.9" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" diff --git a/packages/guition.yaml b/packages/guition.yaml index 007bd62..0503497 100644 --- a/packages/guition.yaml +++ b/packages/guition.yaml @@ -2,7 +2,7 @@ # Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911. # Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.8" + SCREEN_FIRMWARE_VERSION: "0.2.9" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "guition-new" @@ -621,6 +621,7 @@ esphome: runtime_tiles::watch_font = id(headline)->get_lv_font(); runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true"; if (!runtime_tiles::enabled) return; + runtime_tiles::rotation_supported = true; runtime_tiles::load_settings(); runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); }; runtime_tiles::dismiss = []() { @@ -782,6 +783,7 @@ api: (unsigned)(millis() - id(last_touch_ms)), id(display_dimmed)); ESP_LOGI("ui_diag", "navigation page=%d runtime_detail=%d", id(tile_page), runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)); + ESP_LOGI("ui_diag", "rotation=%u actual=%u", (unsigned)runtime_tiles::rotation, (unsigned)id(screen_lvgl).get_rotation()); lv_area_t nav, grid; lv_obj_update_layout(id(home_page)->obj); lv_obj_get_coords(id(page_next), &nav); @@ -1287,6 +1289,11 @@ binary_sensor: # - Used by the UI to display fan presets when applicable # ---------------------------------------------------------- text_sensor: + - platform: template + name: "Guition schermtype" + entity_category: diagnostic + update_interval: never + lambda: 'return {"guition"};' - platform: template name: "Schermfirmware" entity_category: diagnostic @@ -1616,6 +1623,7 @@ display: lvgl: + id: screen_lvgl on_draw_start: - lambda: 'id(rendered_frames)++;' displays: @@ -4479,6 +4487,11 @@ script: then: - lambda: |- const auto &settings = screen_settings::current; + if(runtime_tiles::enabled && static_cast(id(screen_lvgl).get_rotation())!=runtime_tiles::rotation){ + cyd::touch_guard.consume(); + if(lv_indev_get_act())lv_indev_wait_release(lv_indev_get_act()); + id(screen_lvgl).set_rotation(static_cast(runtime_tiles::rotation)); + } if (id(display_dimmed) && !settings.standby_enabled) { id(display_dimmed) = false; } diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 3053bef..c4a03db 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.9 + +- Guition: kies 0°, 90°, 180° of 270° onder Scherminstellingen. Na opslaan draait de interface direct mee, inclusief touch. De hoek blijft bewaard na herstart; een nieuwe flash per hoek is niet nodig. +- Installeer eerst Guition-firmware 0.2.9 voor deze optie. De CYD behoudt zijn vaste oriëntatie en kalibratie. + ## 0.2.8 - Firmware: vaste tekstbreedtes voor nette afkapping met puntjes. Ondertitels gebruiken de beschikbare ruimte, ook bij korte titels. diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index 0e2ad1a..1b743c4 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -24,6 +24,7 @@ SETTING_RULES = { 'clock_24h': (True, None, None), 'home_on_standby': (False, None, None), 'swipe_pages': (False, None, None), + 'rotation': (0, 0, 270), } def validate_settings(data): @@ -36,6 +37,7 @@ def validate_settings(data): valid = type(value) is bool else: valid = type(value) is int and minimum <= value <= maximum + if key == "rotation": valid = valid and value in (0, 90, 180, 270) if not valid: raise ValueError(f'Ongeldige waarde voor {key}.') clean[key] = value @@ -129,6 +131,8 @@ def discover(registry, states, devices, areas): area_map = {a['area_id']: a['name'] for a in areas} versions = {item.get('device_id'): states.get(item['entity_id'], {}).get('state', 'onbekend') for item in registry if item.get('platform') == 'esphome' and item.get('original_name') == 'Schermfirmware'} + boards = {item.get("device_id"): "guition" for item in registry + if item.get("platform") == "esphome" and item.get("original_name") == "Guition schermtype"} screens, entities = [], [] for item in registry: eid = item['entity_id'] @@ -138,6 +142,7 @@ def discover(registry, states, devices, areas): if item.get('platform') == 'esphome' and eid.startswith('text.') and item.get('original_name') == 'Tegelinstellingen' and not item.get('disabled_by'): screens.append({'id': eid, 'name': device.get('name_by_user') or device.get('name') or eid, 'firmware': versions.get(item.get('device_id'), 'onbekend'), + 'board': boards.get(item.get('device_id'), 'unknown'), 'area': area, 'online': state.get('state') not in (None, 'unknown', 'unavailable'), 'status': state.get('state', 'Niet verbonden')}) if not entity_id(eid) or item.get('disabled_by'): diff --git a/screen_manager/app/server.py b/screen_manager/app/server.py index 3ec8dac..db30be1 100644 --- a/screen_manager/app/server.py +++ b/screen_manager/app/server.py @@ -184,6 +184,11 @@ class Manager: layout['settings'] = self.layouts[inbox]['settings'].copy() if 'settings' in layout and 'swipe_pages' not in data.get('settings',{}): layout['settings']['swipe_pages']=self.layouts.get(inbox,{}).get('settings',{}).get('swipe_pages',False) + if 'settings' in layout and 'rotation' not in data.get('settings',{}): + layout['settings']['rotation']=self.layouts.get(inbox,{}).get('settings',{}).get('rotation',0) + screen=next(s for s in self.inventory()[0] if s['id']==inbox) + if layout.get('settings',{}).get('rotation',0) and screen.get('board')!='guition': + raise ValueError('Rotatie vereist een Guition met firmware 0.2.9 of nieuwer.') old_tiles = {t['entity']:t for t in self.layouts.get(inbox,{}).get('tiles',[])} for tile in layout['tiles']: if 'options' not in tile and 'options' in old_tiles.get(tile['entity'],{}): @@ -211,8 +216,10 @@ class Manager: return messages = [{'v': 1, 'op': 'layout', 'inbox': inbox, 'title': layout['title'], 'entities': [t['entity'] for t in layout['tiles']]}] if 'settings' in layout: - messages[0]['settings'] = {k:v for k,v in layout['settings'].items() if k!='swipe_pages'} + messages[0]['settings'] = {k:v for k,v in layout['settings'].items() if k not in ('swipe_pages','rotation')} messages[0]['swipe_pages'] = layout['settings'].get('swipe_pages',False) + if next((s.get('board') for s in self.inventory()[0] if s['id']==inbox),None)=='guition': + messages[0]['rotation'] = layout['settings'].get('rotation',0) for i,tile in enumerate(layout['tiles']): message=state_message(i,tile,self.ha.states) if tile['entity'].startswith('sensor.') and hasattr(self.ha,'history'): diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index 4fca4f2..7768cff 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -81,6 +81,7 @@ const settingDefinitions = [ ["show_clock", "Klok tonen", "check", true], ["clock_24h", "24-uursklok (uit = 12 uur)", "check", true], ["home_on_standby", "Na standby terug naar pagina 1", "check", false], + ["rotation", "Scherm draaien (met de klok mee)", "rotation", 0], ["swipe_pages", "Vegen tussen pagina’s (firmware 0.2.7+)", "check", false], ]; function renderSettings() { @@ -93,20 +94,25 @@ function renderSettings() { const container = $("#settings-fields"); container.replaceChildren(); for (const [key, title, kind, , min, max] of settingDefinitions) { + if(key === "rotation" && inventory.screens.find(s => s.id === selected)?.board !== "guition") continue; const label = node( "label", undefined, `setting ${kind === "check" ? "setting-check" : ""}`, ); const caption = node("span", title), - input = node("input"), + input = node(kind === "rotation" ? "select" : "input"), output = node("output"); input.id = `setting-${key}`; - input.type = + if(kind !== "rotation") input.type = kind === "check" ? "checkbox" : kind === "minutes" ? "number" : kind; input.setAttribute("aria-label", title); input.required = kind === "minutes" || kind === "time"; - if (kind === "check") input.checked = values[key]; + if(kind === "rotation") { + for(const angle of [0,90,180,270]) { const option=node("option", `${angle}°`); option.value=angle; input.append(option); } + input.value=values[key]; + } + else if (kind === "check") input.checked = values[key]; else if (kind === "time") input.value = `${String(Math.floor(values[key] / 60)).padStart(2, "0")}:${String(values[key] % 60).padStart(2, "0")}`; else { diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml index a3eb08c..e1063e8 100644 --- a/screen_manager/config.yaml +++ b/screen_manager/config.yaml @@ -1,5 +1,5 @@ name: ESP Screen Manager -version: "0.2.8" +version: "0.2.9" slug: esp_screen_manager description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen. url: https://github.com/MaxGramser/homeassistant_espscreen diff --git a/tests/test_portal.py b/tests/test_portal.py index e168eeb..029a171 100644 --- a/tests/test_portal.py +++ b/tests/test_portal.py @@ -122,11 +122,11 @@ class ManagerTests(unittest.IsolatedAsyncioTestCase): with self.assertRaises(ValueError): fresh.save('text.screen',{**saved,'settings':{'brightness':0}}) self.assertEqual(path.read_bytes(),before) await fresh.sync_one('text.screen',saved) - self.assertEqual(fresh.ha.messages[0][1]['settings'],{k:v for k,v in settings.items() if k!='swipe_pages'}) + self.assertEqual(fresh.ha.messages[0][1]['settings'],{k:v for k,v in settings.items() if k not in ('swipe_pages','rotation')}) await fresh.sync_one('text.screen',saved) self.assertEqual(len(fresh.ha.messages),2) await fresh.sync_one('text.screen',saved,True) - self.assertEqual(fresh.ha.messages[2][1]['settings'],{k:v for k,v in settings.items() if k!='swipe_pages'}) + self.assertEqual(fresh.ha.messages[2][1]['settings'],{k:v for k,v in settings.items() if k not in ('swipe_pages','rotation')}) async def test_reorder_aborts_old_batch(self): with tempfile.TemporaryDirectory() as temp: diff --git a/tests/test_rotation.py b/tests/test_rotation.py new file mode 100644 index 0000000..9aa033c --- /dev/null +++ b/tests/test_rotation.py @@ -0,0 +1,32 @@ +import tempfile +import unittest +from pathlib import Path +import test_portal +from core import validate_settings + +class RotationTests(unittest.IsolatedAsyncioTestCase): + def test_only_quarter_turns(self): + for angle in (0,90,180,270): + self.assertEqual(validate_settings({'rotation':angle})['rotation'],angle) + for angle in (-90,45,360,True,90.0,'90'): + with self.assertRaises(ValueError):validate_settings({'rotation':angle}) + + async def test_legacy_wire_and_saved_rotation_survive_old_browser(self): + with tempfile.TemporaryDirectory() as tmp: + m=test_portal.ManagerTests().setup_manager(Path(tmp)/'screens.json') + layout={'title':'Thuis','tiles':[{'entity':'light.a'}],'settings':{'rotation':90}} + with self.assertRaises(ValueError):m.save('text.screen',layout) + m.ha.registry[0]['device_id']='guition' + m.ha.registry.append({'entity_id':'sensor.board','device_id':'guition','platform':'esphome','original_name':'Guition schermtype'}) + # Capability stays discoverable with the panel offline or renamed. + m.ha.states['sensor.board']={'state':'unavailable'} + m.save('text.screen',layout) + await m.sync_one('text.screen',m.layouts['text.screen']) + wire=m.ha.messages[0][1] + self.assertEqual(wire['rotation'],90) + self.assertEqual(len(wire['settings']),11) + self.assertNotIn('rotation',wire['settings']) + m.save('text.screen',{'title':'Anders','tiles':layout['tiles'],'settings':{'brightness':80}}) + self.assertEqual(m.layouts['text.screen']['settings']['rotation'],90) + fresh=test_portal.ManagerTests().setup_manager(m.path) + self.assertEqual(fresh.layouts['text.screen']['settings']['rotation'],90)