Direct controls on wide cards, weather/climate cards, spinner busy state (0.2.23)
App 0.2.23 / firmware 0.2.19. - Wide cards of climate, switch, light, fan, vacuum, cover, media_player, number, select, timer, scene, script and button show Home Assistant style controls on the right (keys, -/+ pill with debounced send, slider with handle, toggle, run key); per-tile choice `options.controls` in the editor with mockup preview; the add-on sends only the set a card really shows. - Weather card: now + next hours and coming days with rain, from daily and hourly forecasts; scenes/scripts/buttons show when they last ran. - Climate card: power key, tap-to-toggle option, fan and swing rows in a redesigned mode picker (grey backdrop, white chips, accent selection). - Busy state as a spinner on a translucent sheet; render guards so only changed styles reach LVGL; dark theme removed from the runtime code. - supported_features above 1e6 reaches the screen; middle dot in text fonts; 18 control glyphs in the icon fonts; diagnostics services preview_climate_picker and preview_busy; demo layout --controls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,12 @@
|
||||
#include <limits>
|
||||
int main() {
|
||||
using cyd::quantize_temperature;
|
||||
// Fan and swing mode chips read the n-th name of a JSON list attribute.
|
||||
assert(cyd::list_item("[\"auto\",\"low\",\"high\"]", 0) == "auto");
|
||||
assert(cyd::list_item("[\"auto\",\"low\",\"high\"]", 2) == "high");
|
||||
assert(cyd::list_item("[\"auto\",\"low\",\"high\"]", 3).empty());
|
||||
assert(cyd::list_item("", 0).empty());
|
||||
assert(cyd::list_item("[\"broken", 0).empty());
|
||||
assert(quantize_temperature(223, 160, 300, 5) == 225);
|
||||
assert(quantize_temperature(221, 160, 300, 5) == 220);
|
||||
assert(quantize_temperature(-1, 160, 300, 5) == 160);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Weather card data (hours, rain) and last-run times for scenes, scripts and buttons."""
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
|
||||
from core import extras, packets, state_message
|
||||
import test_portal
|
||||
|
||||
TZ = ZoneInfo('Europe/Amsterdam')
|
||||
NOW = datetime(2026, 9, 13, 11, 40, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def hourly():
|
||||
return [{'datetime': f'2026-09-13T{h:02d}:00:00+00:00', 'condition': 'rainy' if h < 12 else 'partlycloudy', 'temperature': 18 + h / 10,
|
||||
'precipitation': 0.2 if h < 12 else 0.0, 'precipitation_probability': 60 if h < 12 else 5} for h in range(8, 30)]
|
||||
|
||||
|
||||
def daily():
|
||||
return [{'datetime': f'2026-09-{13 + i}T10:00:00+00:00', 'condition': 'cloudy', 'temperature': 21 + i, 'templow': 12.1, 'precipitation': 1.7 if i else 0.0}
|
||||
for i in range(6)]
|
||||
|
||||
|
||||
class WeatherExtras(unittest.TestCase):
|
||||
def test_hours_start_now_and_carry_rain(self):
|
||||
states = {'weather.huis': {'state': 'rainy', 'attributes': {'temperature': 18.4, 'humidity': 92, 'wind_speed': 12.2, 'wind_speed_unit': 'km/h'}}}
|
||||
extra = extras({'entity': 'weather.huis'}, states, daily(), TZ, hourly(), NOW)
|
||||
self.assertEqual([h['t'] for h in extra['hours']], ['13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00'], 'local time, from the current hour')
|
||||
self.assertEqual(extra['hours'][0], {'t': '13:00', 'c': 'rainy', 'h': 19.1, 'p': 60, 'r': 0.2})
|
||||
self.assertEqual(extra['hours'][1]['r'], 0.0)
|
||||
self.assertEqual(len(extra['days']), 5)
|
||||
self.assertEqual(extra['days'][1], {'d': 'ma', 'c': 'cloudy', 'h': 22, 'l': 12.1, 'r': 1.7}, 'no probability when the provider gives none')
|
||||
message = state_message(0, {'entity': 'weather.huis', 'name': 'Buiten', 'options': {'display': 'forecast'}}, states, extra)
|
||||
self.assertEqual(message['a']['wind_speed_unit'], 'km/h')
|
||||
self.assertEqual(message['a']['humidity'], 92)
|
||||
self.assertLessEqual(len(packets(message)), 8, 'a full weather card still fits comfortably')
|
||||
|
||||
def test_weather_without_forecasts_sends_nothing_extra(self):
|
||||
self.assertIsNone(extras({'entity': 'weather.huis'}, {}, None, TZ, None, NOW))
|
||||
self.assertEqual(extras({'entity': 'weather.huis'}, {}, daily(), TZ, [], NOW).keys(), {'days'})
|
||||
|
||||
|
||||
class LastRun(unittest.TestCase):
|
||||
def test_scripts_scenes_and_buttons_report_when_they_last_ran(self):
|
||||
states = {'script.tv': {'state': 'off', 'attributes': {'last_triggered': '2026-09-13T08:10:49.403953+00:00'}},
|
||||
'scene.avond': {'state': '2026-09-12T16:33:37.040291+00:00', 'attributes': {}},
|
||||
'button.bel': {'state': '2026-09-13T09:00:00+00:00', 'attributes': {}},
|
||||
'script.nieuw': {'state': 'off', 'attributes': {'last_triggered': None}},
|
||||
'scene.nooit': {'state': 'unknown', 'attributes': {}}}
|
||||
self.assertEqual(extras({'entity': 'script.tv'}, states, None, TZ), {'last': 1789287049})
|
||||
self.assertEqual(extras({'entity': 'scene.avond'}, states, None, TZ), {'last': 1789230817})
|
||||
self.assertEqual(extras({'entity': 'button.bel'}, states, None, TZ), {'last': 1789290000})
|
||||
self.assertIsNone(extras({'entity': 'script.nieuw'}, states, None, TZ))
|
||||
self.assertIsNone(extras({'entity': 'scene.nooit'}, states, None, TZ))
|
||||
message = state_message(0, {'entity': 'script.tv', 'name': ''}, states, extras({'entity': 'script.tv'}, states, None, TZ))
|
||||
self.assertEqual(message['x'], {'last': 1789287049})
|
||||
self.assertNotIn('last_triggered', message['a'], 'the raw timestamp stays behind; the firmware gets the epoch')
|
||||
|
||||
|
||||
class HourlySync(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_manager_asks_for_daily_and_hourly_forecasts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m = test_portal.ManagerTests().setup_manager(Path(tmp) / 'screens.json')
|
||||
m.ha.states['weather.huis'] = {'state': 'rainy', 'attributes': {'friendly_name': 'Huis', 'temperature': 18.4}}
|
||||
asked = []
|
||||
async def forecast(entity, kind='daily'):
|
||||
asked.append(kind)
|
||||
return daily() if kind == 'daily' else hourly()
|
||||
m.ha.forecast = forecast
|
||||
m.ha.registry.append({'entity_id': 'sensor.fw', 'platform': 'esphome', 'original_name': 'Schermfirmware'})
|
||||
m.ha.states['sensor.fw'] = {'state': '0.2.19'}
|
||||
m.save('text.screen', {'title': 'Thuis', 'tiles': [{'entity': 'weather.huis', 'name': '', 'options': {'display': 'forecast'}}]})
|
||||
await m.sync_one('text.screen', m.layouts['text.screen'])
|
||||
self.assertEqual(sorted(asked), ['daily', 'hourly'])
|
||||
sent = m.ha.messages[1][1]
|
||||
self.assertEqual(len(sent['x']['days']), 5)
|
||||
self.assertTrue(sent['x']['hours'])
|
||||
await m.sync_one('text.screen', m.layouts['text.screen'])
|
||||
self.assertEqual(len(asked), 2, 'both forecasts are cached')
|
||||
@@ -0,0 +1,104 @@
|
||||
// c++ -std=c++17 -Wall -Wextra -pedantic tests/test_tile_controls.cpp -o /tmp/test_tile_controls && /tmp/test_tile_controls
|
||||
#include "../components/smart_display/tile_controls.h"
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
using namespace tile_controls;
|
||||
using runtime_tiles::Tile;
|
||||
|
||||
static Tile make(const char *entity, const char *state, uint32_t supported = 0) {
|
||||
Tile t; t.entity = entity; t.state = state; t.supported = supported; t.received = true; return t;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// -/+ steps snap to the entity's grid and stay inside its range.
|
||||
assert(step_value(20.0f, 0.5f, 16, 32, 1) == 20.5f);
|
||||
assert(step_value(20.3f, 0.5f, 16, 32, -1) == 20.0f);
|
||||
assert(step_value(31.8f, 1.0f, 16, 32, 1) == 32.0f);
|
||||
assert(step_value(16.0f, 1.0f, 16, 32, -1) == 16.0f);
|
||||
assert(step_value(NAN, 1.0f, 16, 32, 1) == 17.0f);
|
||||
assert(step_value(5.0f, 0.0f, NAN, NAN, 1) == 6.0f);
|
||||
assert(format_value(20.0f, 1.0f, "°") == "20°");
|
||||
assert(format_value(20.5f, 0.5f, "°") == "20.5°");
|
||||
assert(format_value(NAN, 0.5f, "°") == "--");
|
||||
|
||||
// Curtain: horizontal arrows, open disabled when fully open, stop only when supported.
|
||||
Tile curtain = make("cover.gordijnen", "open", 15); curtain.device_class = "curtain"; curtain.position = 100; curtain.controls = "buttons";
|
||||
std::array<Key, 3> keys;
|
||||
assert(keys_for(curtain, keys) == 3);
|
||||
assert(!strcmp(keys[0].icon, glyph::EXPAND) && keys[0].command == COVER_OPEN && keys[0].disabled);
|
||||
assert(keys[1].command == COVER_STOP && !keys[1].disabled);
|
||||
assert(!strcmp(keys[2].icon, glyph::COLLAPSE) && keys[2].command == COVER_CLOSE && !keys[2].disabled);
|
||||
assert(key_action(curtain, COVER_CLOSE).service == "cover.close_cover");
|
||||
Tile shutter = make("cover.rolluik", "closed", 3); shutter.controls = "buttons";
|
||||
assert(keys_for(shutter, keys) == 2 && !strcmp(keys[0].icon, glyph::UP) && keys[1].disabled);
|
||||
assert(status_text(curtain) == "Open · 100%");
|
||||
assert(status_text(shutter) == "Dicht");
|
||||
|
||||
// Vacuum: play while docked, pause while cleaning, dock disabled in the dock.
|
||||
Tile robot = make("vacuum.s8", "docked", 30524); robot.controls = "buttons";
|
||||
assert(keys_for(robot, keys) == 3);
|
||||
assert(!strcmp(keys[0].icon, glyph::PLAY) && keys[0].command == VACUUM_START && !keys[0].disabled);
|
||||
assert(keys[1].command == VACUUM_STOP && keys[1].disabled);
|
||||
assert(keys[2].command == VACUUM_DOCK && keys[2].disabled);
|
||||
assert(key_action(robot, VACUUM_START).service == "vacuum.start");
|
||||
robot.state = "cleaning";
|
||||
assert(keys_for(robot, keys) == 3 && keys[0].command == VACUUM_PAUSE && !keys[1].disabled && !keys[2].disabled);
|
||||
Tile old_robot = make("vacuum.old", "docked", feature::VACUUM_TURN_ON | feature::VACUUM_RETURN); old_robot.controls = "buttons";
|
||||
assert(keys_for(old_robot, keys) == 2 && key_action(old_robot, VACUUM_START).service == "vacuum.turn_on");
|
||||
|
||||
// Media: playback keys follow supported_features; mute flips is_volume_muted.
|
||||
Tile sonos = make("media_player.sonos", "playing", 8321599); sonos.volume = 0.17f; sonos.media_title = "TV"; sonos.controls = "playback";
|
||||
assert(keys_for(sonos, keys) == 3 && !strcmp(keys[1].icon, glyph::PAUSE) && keys[1].command == MEDIA_PLAY_PAUSE);
|
||||
assert(status_text(sonos) == "TV · 17%");
|
||||
Action mute = key_action(sonos, MEDIA_MUTE);
|
||||
assert(mute.service == "media_player.volume_mute" && mute.key == "is_volume_muted" && mute.value == "true");
|
||||
sonos.muted = true; assert(key_action(sonos, MEDIA_MUTE).value == "false");
|
||||
Tile radio = make("media_player.radio", "idle", feature::MEDIA_PLAY | feature::MEDIA_PAUSE); radio.controls = "playback";
|
||||
assert(keys_for(radio, keys) == 1 && !strcmp(keys[0].icon, glyph::PLAY));
|
||||
assert(status_text(radio) == "Inactief");
|
||||
|
||||
// Climate: mode keys in priority order, at most three, the active one checked.
|
||||
Tile ac = make("climate.airco", "cool"); ac.hvac_modes = "[\"off\",\"heat_cool\",\"cool\",\"heat\",\"fan_only\",\"dry\"]"; ac.controls = "mode"; ac.current = 21.5f; ac.target = 20; ac.step = 1;
|
||||
assert(keys_for(ac, keys) == 3);
|
||||
assert(keys[0].arg == "off" && keys[1].arg == "heat" && keys[2].arg == "cool" && keys[2].checked && !keys[1].checked);
|
||||
Action mode = key_action(ac, HVAC_MODE, keys[1].arg);
|
||||
assert(mode.service == "climate.set_hvac_mode" && mode.key == "hvac_mode" && mode.value == "heat");
|
||||
assert(status_text(ac) == "Koelen · 21.5°");
|
||||
ac.hvac_action = "cooling"; assert(status_text(ac) == "Koelt · 21.5°");
|
||||
assert(edit_target(ac) == 20 && edit_step(ac) == 1);
|
||||
Action set = edit_action(ac, step_value(edit_target(ac), edit_step(ac), ac.minimum, ac.maximum, 1));
|
||||
assert(set.service == "climate.set_temperature" && set.key == "temperature" && set.value == "21");
|
||||
assert(edit_action(ac, 20.5f).value == "20.5");
|
||||
|
||||
// Numbers edit their own state; selects step through their options with wrap-around.
|
||||
Tile number = make("number.doel", "55"); number.minimum = 0; number.maximum = 100; number.step = 5;
|
||||
assert(edit_target(number) == 55 && edit_action(number, step_value(55, 5, 0, 100, -1)).value == "50");
|
||||
assert(edit_action(number, 50).service == "number.set_value");
|
||||
Tile select = make("select.stand", "Comfort"); select.options = {"Eco", "Comfort", "Boost"}; select.option_count = 3; select.controls = "stepper";
|
||||
assert(panel_kind(select) == "chevrons");
|
||||
assert(keys_for(select, keys) == 2 && !keys[0].disabled);
|
||||
assert(key_action(select, SELECT_NEXT).value == "Boost" && key_action(select, SELECT_PREVIOUS).value == "Eco");
|
||||
select.state = "Boost"; assert(key_action(select, SELECT_NEXT).value == "Eco");
|
||||
select.option_count = 1; assert(keys_for(select, keys) == 2 && keys[0].disabled);
|
||||
|
||||
// Timer, run buttons and toggles.
|
||||
Tile timer = make("timer.eieren", "active"); timer.controls = "buttons";
|
||||
assert(keys_for(timer, keys) == 2 && keys[0].command == TIMER_PAUSE && !keys[1].disabled);
|
||||
timer.state = "idle"; assert(keys_for(timer, keys) == 2 && keys[0].command == TIMER_START && keys[1].disabled);
|
||||
assert(key_action(make("scene.avond", "unknown"), RUN).service == "scene.turn_on");
|
||||
assert(key_action(make("script.alles", "off"), RUN).service == "script.turn_on");
|
||||
assert(key_action(make("input_button.bel", "unknown"), RUN).service == "input_button.press");
|
||||
assert(!strcmp(run_label("scene"), "Activeren") && !strcmp(run_label("button"), "Indrukken"));
|
||||
assert(key_action(make("switch.bureau", "on"), TOGGLE).service == "switch.turn_off");
|
||||
assert(key_action(make("light.lamp", "off"), TOGGLE).service == "light.turn_on");
|
||||
assert(!key_action(make("sensor.x", "1"), TOGGLE).valid());
|
||||
assert(!key_action(make("sensor.x", "1"), RUN).valid());
|
||||
|
||||
// Panel kinds.
|
||||
Tile lamp = make("light.lamp", "on"); lamp.controls = "brightness";
|
||||
assert(is_slider(panel_kind(lamp)) && !is_key_row(panel_kind(lamp)));
|
||||
assert(is_key_row("playback") && is_key_row("mode") && !is_key_row("setpoint"));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Direct controls on wide cards: editor choices, validation and what goes on the wire."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
|
||||
from core import CONTROLS, controls_catalogue, min_firmware, packets, resolve_controls, screen_options, state_message, validate_layout
|
||||
import tile_icons
|
||||
import test_portal
|
||||
|
||||
HAS_AIOHTTP = importlib.util.find_spec('aiohttp') is not None
|
||||
|
||||
|
||||
class ControlChoices(unittest.TestCase):
|
||||
def test_catalogue_offers_each_domain_its_sets_plus_none(self):
|
||||
catalogue = controls_catalogue()
|
||||
self.assertEqual(catalogue['media_player']['default'], 'volume')
|
||||
self.assertEqual([c['key'] for c in catalogue['media_player']['choices']], ['volume', 'playback', 'none'])
|
||||
self.assertEqual([c['key'] for c in catalogue['climate']['choices']], ['setpoint', 'mode', 'none'])
|
||||
for domain in ('climate', 'switch', 'vacuum', 'cover', 'media_player'):
|
||||
self.assertIn(domain, catalogue)
|
||||
self.assertNotIn('sensor', catalogue)
|
||||
|
||||
def test_validation_accepts_own_sets_and_rejects_others(self):
|
||||
layout = validate_layout({'title': 'T', 'tiles': [{'entity': 'cover.gordijn', 'options': {'size': 'wide', 'controls': 'position'}}]})
|
||||
self.assertEqual(layout['tiles'][0]['options']['controls'], 'position')
|
||||
validate_layout({'title': 'T', 'tiles': [{'entity': 'light.a', 'options': {'controls': 'none'}}]})
|
||||
for entity, choice in (('cover.a', 'volume'), ('sensor.a', 'toggle'), ('switch.a', 'brightness'), ('climate.a', 42), ('light.a', 'run')):
|
||||
with self.assertRaises(ValueError):
|
||||
validate_layout({'title': 'T', 'tiles': [{'entity': entity, 'options': {'size': 'wide', 'controls': choice}}]})
|
||||
|
||||
def test_wide_cards_get_the_default_set_only_in_the_standard_layout(self):
|
||||
self.assertEqual(resolve_controls({'entity': 'climate.a', 'options': {'size': 'wide'}}), 'setpoint')
|
||||
self.assertEqual(resolve_controls({'entity': 'media_player.a', 'options': {'size': 'wide', 'controls': 'playback'}}), 'playback')
|
||||
self.assertIsNone(resolve_controls({'entity': 'media_player.a', 'options': {'size': 'wide', 'controls': 'none'}}))
|
||||
self.assertIsNone(resolve_controls({'entity': 'climate.a'}), 'single cards stay as they are')
|
||||
self.assertIsNone(resolve_controls({'entity': 'climate.a', 'options': {'size': 'wide', 'display': 'watch'}}))
|
||||
self.assertIsNone(resolve_controls({'entity': 'light.a', 'options': {'size': 'wide', 'inline': 'slider'}}))
|
||||
self.assertIsNone(resolve_controls({'entity': 'sensor.a', 'options': {'size': 'wide'}}))
|
||||
|
||||
def test_wire_carries_only_the_shown_set_and_the_attributes_the_panels_need(self):
|
||||
states = {'media_player.sonos': {'state': 'playing', 'attributes': {'volume_level': 0.17, 'is_volume_muted': False, 'media_title': 'TV', 'supported_features': 8321599, 'assumed_state': True}},
|
||||
'cover.gordijn': {'state': 'open', 'attributes': {'current_position': 80, 'device_class': 'curtain', 'supported_features': 15}},
|
||||
'climate.airco': {'state': 'cool', 'attributes': {'hvac_action': 'cooling', 'hvac_modes': ['off', 'cool', 'heat'], 'temperature': 20, 'target_temp_step': 1.0}}}
|
||||
msg = state_message(0, {'entity': 'media_player.sonos', 'name': '', 'options': {'size': 'wide'}}, states)
|
||||
self.assertEqual(msg['o'], {'size': 'wide', 'controls': 'volume'})
|
||||
self.assertIs(msg['a']['is_volume_muted'], False)
|
||||
self.assertNotIn('assumed_state', msg['a'])
|
||||
msg = state_message(1, {'entity': 'cover.gordijn', 'name': '', 'options': {'size': 'wide', 'controls': 'none', 'background': 'blue'}}, states)
|
||||
self.assertEqual(msg['o'], {'size': 'wide', 'background': 'blue'}, 'an explicit none is not sent; the stored choice stays')
|
||||
self.assertEqual(msg['a']['device_class'], 'curtain')
|
||||
msg = state_message(2, {'entity': 'climate.airco', 'name': '', 'options': {'size': 'wide', 'controls': 'mode'}}, states)
|
||||
self.assertEqual(msg['o'], {'size': 'wide', 'controls': 'mode'})
|
||||
self.assertEqual(msg['a']['hvac_action'], 'cooling')
|
||||
self.assertTrue(all(len(p) <= 255 for p in packets(msg)))
|
||||
msg = state_message(3, {'entity': 'climate.airco', 'name': '', 'options': {'controls': 'mode'}}, states)
|
||||
self.assertEqual(msg['o'], {}, 'a single card never shows a panel')
|
||||
self.assertIsNone(min_firmware({'tiles': [{'entity': 'climate.airco', 'options': {'size': 'wide', 'controls': 'mode'}}]}), 'older firmware ignores the field')
|
||||
|
||||
def test_screen_options_keeps_the_choice_out_of_the_wire_but_in_storage(self):
|
||||
tile = {'entity': 'switch.bureau', 'name': '', 'options': {'size': 'wide', 'controls': 'toggle', 'icon': 'auto'}}
|
||||
self.assertEqual(screen_options(tile, {}), {'size': 'wide', 'controls': 'toggle'})
|
||||
self.assertEqual(validate_layout({'title': 'T', 'tiles': [tile]})['tiles'][0]['options'], tile['options'])
|
||||
|
||||
def test_control_glyphs_exist_in_the_icon_fonts_and_the_editor_font(self):
|
||||
header = (ROOT / 'components/smart_display/tile_controls.h').read_text()
|
||||
used = {code.upper() for code in re.findall(r'\\U000(F[0-9A-F]{4})', header)}
|
||||
carried = {code for code in tile_icons.GLYPHS.values()}
|
||||
self.assertTrue(used <= carried, used - carried)
|
||||
self.assertTrue(set(tile_icons.CONTROL_GLYPHS) <= set(tile_icons.GLYPHS))
|
||||
self.assertEqual(set(tile_icons.editor()['controls']), set(tile_icons.CONTROL_GLYPHS))
|
||||
for name in ('guition-4848s040.yaml', 'home-like-2432s028.yaml', 'packages/guition.yaml', 'packages/cyd.yaml'):
|
||||
text = (ROOT / name).read_text()
|
||||
for code in used:
|
||||
self.assertIn(f'"\\U000{code}"', text, (name, code))
|
||||
self.assertIn('runtime_tiles::control_font = id(sublabel_big)->get_lv_font();', text, name)
|
||||
|
||||
|
||||
@unittest.skipUnless(HAS_AIOHTTP, 'Run using .venv-portal/bin/python for server tests')
|
||||
class ControlInventory(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_inventory_lists_the_catalogue_and_a_saved_choice_survives_an_old_editor(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m = test_portal.ManagerTests().setup_manager(Path(tmp) / 'screens.json')
|
||||
self.assertEqual(m.inventory()[0][0]['id'], 'text.screen')
|
||||
m.save('text.screen', {'title': 'Thuis', 'tiles': [{'entity': 'light.a', 'options': {'size': 'wide', 'controls': 'brightness'}}]})
|
||||
m.save('text.screen', {'title': 'Thuis', 'tiles': [{'entity': 'light.a', 'options': {'size': 'wide'}}]})
|
||||
self.assertEqual(m.layouts['text.screen']['tiles'][0]['options'], {'size': 'wide', 'controls': 'brightness'})
|
||||
await m.sync_one('text.screen', m.layouts['text.screen'])
|
||||
self.assertEqual(m.ha.messages[1][1]['o'], {'size': 'wide', 'controls': 'brightness'})
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
from server import create_app
|
||||
async with TestClient(TestServer(create_app(m, development=True))) as client:
|
||||
data = await (await client.get('/api/inventory')).json()
|
||||
self.assertEqual(data['controls']['cover']['default'], 'buttons')
|
||||
self.assertIn('controls', data['icons'])
|
||||
light = await (await client.get('/api/inventory?light=1')).json()
|
||||
self.assertNotIn('controls', light)
|
||||
Reference in New Issue
Block a user