Files
homeassistant_espscreen/tests/test_state_colours.py
T
Max Gramser 9391d1b0ac Grey out what Home Assistant calls off and colour the rest like Home Assistant, so an airco that is off no longer looks on (0.2.85)
The airco tile stayed orange while the airco was off. The firmware kept its own
short list of what turns grey and gave an off climate Home Assistant's orange,
but Home Assistant has no colour for a climate that is off: its cards fall back
to the inactive grey. Other tiles had the same fault: an off fan, a closed
blind, a docked robot, a player in standby, an idle script, a paused timer and
a camera that isn't streaming all kept their colour.

Tile::active() is now a port of the frontend's stateActive() for the domains a
tile can show, and every tile that is not active is grey. An airco that is off
reads Off, with the room's temperature when the device reports one, as Home
Assistant's tile does. tile_controls::accent() follows stateColorCss(): alarm
binary sensors red, a battery green, orange or red by its charge, a running
script or timer and a streaming camera amber, the weather per condition, the
sun amber and indigo, someone in a zone blue. Scenes, selects, numbers and
sensors keep their own colour per kind. A full-page tile lights up only for
tiles that can be off (Tile::lights_up), and a closed blind's slider keeps its
colour. The top bar's alarm classes are Home Assistant's eleven.

Checked with tools/check.sh (471 Python tests, 19/19 C++ with 58 state cases
and every colour rule) and compiled for both boards: CYD 1,677,488 B (91.4 %,
+576 B), Guition 2,054,640 B (25.3 %).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 23:31:10 +02:00

75 lines
4.1 KiB
Python

"""Tiles take Home Assistant's state colours and its rule for what is off (app 0.2.85 / firmware 0.2.71).
Home Assistant's frontend decides with stateActive() (src/common/entity/state_active.ts) whether a card shows an
entity in its colour or grey, and stateColorCss() (src/common/entity/state_color.ts) picks the colour from the
--state-*-color variables of its theme. The screen ports both: Tile::active() and tile_controls::accent(). These
checks keep the tables in step with each other and with Home Assistant's palette; tests/test_runtime_model.cpp and
tests/test_tile_controls.cpp check the rules state by state.
"""
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
import header_bar # noqa: E402
import tile_icons # noqa: E402
COMPONENT = ROOT / 'components/smart_display'
CONTROLS = (COMPONENT / 'tile_controls.h').read_text()
TILES = (COMPONENT / 'runtime_tiles.h').read_text()
MODEL = (COMPONENT / 'runtime_model.h').read_text()
THEME = (COMPONENT / 'theme.h').read_text()
# Home Assistant's palette (frontend src/resources/theme/color/color.globals.ts), as its 2026.9 frontend serves it.
HA_PALETTE = {'RED': 'F44336', 'PURPLE': '926BC7', 'DEEP_PURPLE': '6E41AB', 'INDIGO': '3F51B5', 'BLUE': '2196F3',
'LIGHT_BLUE': '03A9F4', 'CYAN': '00BCD4', 'TEAL': '009688', 'GREEN': '4CAF50', 'LIME': 'CDDC39',
'YELLOW': 'FFEB3B', 'AMBER': 'FFC107', 'ORANGE': 'FF9800', 'DEEP_ORANGE': 'FF6F22', 'LIGHT_GREY': 'BDBDBD',
'GREY': '9E9E9E', 'BLUE_GREY': '607D8B', 'ICE': 'C0E0FF'}
# The binary sensor classes with a red --state-binary_sensor-<class>-on-color in that theme.
HA_ALARM_CLASSES = {'battery', 'carbon_monoxide', 'gas', 'heat', 'lock', 'moisture', 'problem', 'safety', 'smoke', 'sound',
'tamper'}
def function(source, signature):
return source.split(signature, 1)[1].split('\n}\n', 1)[0]
class StateColours(unittest.TestCase):
def test_the_theme_holds_home_assistants_palette(self):
ha = THEME.split('namespace ha {', 1)[1].split('} // namespace ha', 1)[0]
values = dict(re.findall(r'\b([A-Z_]+) = 0x([0-9A-F]{6})', ha))
for name, value in HA_PALETTE.items():
self.assertEqual(values.get(name), value, name)
def test_alarms_are_red_on_the_tile_and_in_the_top_bar(self):
firmware = set(re.findall(r'"(\w+)"', function(CONTROLS, 'inline bool alarm_class(const std::string &device_class) {')))
self.assertEqual(firmware, HA_ALARM_CLASSES)
self.assertEqual(set(header_bar.ALARM_CLASSES), HA_ALARM_CLASSES)
for device_class in sorted(HA_ALARM_CLASSES):
state = {'state': 'on', 'attributes': {'device_class': device_class}}
self.assertEqual(header_bar.accent('binary_sensor.a', state), header_bar.RED, device_class)
self.assertEqual(header_bar.accent('binary_sensor.a', {'state': 'on', 'attributes': {'device_class': 'door'}}),
header_bar.AMBER)
def test_every_weather_condition_has_its_colour(self):
body = function(CONTROLS, 'inline uint32_t weather_color(const std::string &condition) {')
self.assertEqual(set(re.findall(r'condition == "([\w-]+)"', body)), set(tile_icons.WEATHER))
def test_the_tile_greys_and_lights_up_by_home_assistants_rule(self):
self.assertIn('uint32_t accent=tile_controls::accent(t);', TILES)
self.assertIn('bool lit=w.full && on && t.lights_up() && available && !t.transparent;', TILES)
self.assertIn('const uint32_t fill=slider_on?accent:theme::STATE_OFF;', TILES)
self.assertNotIn('domain_accent', TILES)
self.assertIn('frontend src/common/entity/state_active.ts', MODEL)
def test_an_airco_that_is_off_says_so(self):
self.assertIn('else if (d == "climate" && t.state == "off") { value = tile_controls::climate_mode_text(t.state);', TILES)
off = TILES.index('d == "climate" && t.state == "off"')
self.assertLess(off, TILES.index('d == "climate" && std::isfinite(t.target)'), 'before the setpoint line')
if __name__ == '__main__':
unittest.main()