Files
homeassistant_espscreen/tests/test_onboarding.py
T
Max Gramser 3e959b3df9 Put ESP Screens in English and keep layouts when entity IDs change (0.2.40)
Everything a user sees or reads is now English: both boards' labels,
status replies and logs, the ESP Screens editor and Alerts cheatsheet,
and the README and guides (docs/TILES.md, CALIBRATING.md, ACCEPTANCE.md
and TROUBLESHOOTING.md replace the Dutch file names). Labels follow
Home Assistant's own English names (Heat/Cool modes, Heating/Cooling
actions, Away, Returning to dock, weather conditions), dates use English
abbreviations and the top bar writes numbers the English way. Docs use
HA 2026.9's menu names (Settings -> Apps, App store, Settings -> Tools).
Unused legacy files (home-like.yaml, buttons.yaml, a.out) are removed.

Firmware 0.2.34 names its entities in English. Home Assistant registers
a renamed ESPHome entity under a new entity ID, so the inbox
text.<screen>_tegelinstellingen becomes text.<screen>_tile_settings and
the layout stored under the old ID would be lost. The manager now
recognises the screen by its device, during a run or after a restart,
and moves its layout and update history along; the Update button and
the nightly round follow the screen through the rename. Screens that
still run 0.2.33 or older keep working: both languages are matched.

Main is the only release branch from now on.

Tested on a Home Assistant Yellow (HA 2026.9.1) with both bench boards:
see docs/TEST_RESULTS_0240.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 02:08:43 +02:00

127 lines
5.8 KiB
Python

"""Offline regression tests for installation, calibration and safe export."""
import copy
from pathlib import Path
import re
import sys
import tempfile
import unittest
import zipfile
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import calibrate
import new_device
import export_bundle
def measurements():
points = []
for name, (x, y) in calibrate.TARGETS:
rx = round(280 + y * 3580 / 240)
ry = round(340 + x * 3520 / 320)
samples = [dict(raw_x=rx+d, raw_y=ry+d, native_x=239-y,
native_y=x, pressure=1500) for d in (-2, 0, 2)]
points.append(dict(name=name, screen=[x, y], samples=samples))
return dict(schema=1, screen=[320, 240], points=points)
class CalibrationTests(unittest.TestCase):
def test_identity_fit_and_independent_verification(self):
coefficients, report = calibrate.fit(measurements())
self.assertAlmostEqual(coefficients[0], 1, places=3)
self.assertAlmostEqual(coefficients[4], 1, places=3)
self.assertTrue(all(p['error_px'] < 1 for p in report))
self.assertEqual(len(calibrate.verify(measurements())), 5)
def test_affine_skew_is_corrected(self):
data = measurements()
for point in data['points']:
for s in point['samples']:
x, y = s['raw_x'], s['raw_y']
s['raw_x'] = round(.9*x - .03*y + 190)
s['raw_y'] = round(.04*x + .95*y + 50)
_, report = calibrate.fit(data)
self.assertTrue(all(p['error_px'] < 1 for p in report))
def test_center_is_not_used_to_hide_bad_fit(self):
data = measurements()
for s in data['points'][4]['samples']: s['raw_x'] += 600
with self.assertRaisesRegex(ValueError, 'center'): calibrate.fit(data)
def test_jitter_rejected(self):
data = measurements()
data['points'][0]['samples'][0]['raw_x'] += 700
with self.assertRaises(ValueError): calibrate.fit(data)
def test_repeated_corner_rejected(self):
data = measurements()
for p in data['points']: p['samples'] = copy.deepcopy(data['points'][0]['samples'])
with self.assertRaises(ValueError): calibrate.fit(data)
def test_wrong_firmware_coordinates_rejected(self):
data = measurements()
for s in data['points'][0]['samples']: s['native_y'] += 40
with self.assertRaises(ValueError): calibrate.verify(data)
def test_capture_requires_isolated_mode(self):
line = 'press native=20,30 raw=500,600 pressure=1500 calibration='
self.assertEqual(calibrate.parse_touch(line+'1')['raw_x'], 500)
with self.assertRaises(ValueError): calibrate.parse_touch(line+'0')
self.assertIsNone(calibrate.parse_touch('unrelated log'))
def test_wrong_screen_or_order_rejected(self):
for field in ('screen', 'order'):
data = measurements()
if field == 'screen': data['screen'] = [240,320]
else: data['points'].reverse()
with self.assertRaises(ValueError): calibrate.fit(data)
def test_output_requires_explicit_replace_and_preserves_backup(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp)/'calibration.yaml'
calibrate.write_file(path, 'old')
with self.assertRaises(ValueError): calibrate.write_file(path, 'new')
calibrate.write_file(path, 'new', replace=True)
self.assertEqual(path.read_text(), 'new')
self.assertEqual(next(Path(tmp).glob('*.bak-*')).read_text(), 'old')
class InstallationTests(unittest.TestCase):
def test_unique_secrets_and_no_overwrite(self):
with tempfile.TemporaryDirectory() as a, tempfile.TemporaryDirectory() as b:
new_device.create(a, 'display-a', 'Display A')
new_device.create(b, 'display-b', 'Display B')
original = (Path(a)/'secrets.yaml').read_text()
self.assertNotEqual(original, (Path(b)/'secrets.yaml').read_text())
self.assertNotIn('WILL_BE_UNIQUELY_GENERATED', original)
with self.assertRaises(ValueError): new_device.create(a, 'new', 'New')
self.assertEqual(original, (Path(a)/'secrets.yaml').read_text())
def test_invalid_device_name(self):
with tempfile.TemporaryDirectory() as tmp:
for name in ('../oops', 'UPPER', 'has space', 'trailing-'):
with self.assertRaises(ValueError): new_device.create(tmp, name, 'Test')
self.assertEqual(list(Path(tmp).iterdir()), [])
def test_export_is_neutral_and_has_required_sources(self):
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp)/'starter.zip'
export_bundle.export(ROOT, output)
with zipfile.ZipFile(output) as archive:
names = archive.namelist()
for forbidden in ('secrets.yaml', 'device.yaml', 'calibration.yaml', '.git/', '.esphome/', '.log', '.bin', '.json'):
self.assertFalse(any(('/'+forbidden in n if forbidden.endswith('/') else n.endswith(forbidden) if forbidden.startswith('.') else n.endswith('/'+forbidden)) for n in names), forbidden)
self.assertIn('cyd-display/.gitignore', names)
base = archive.read('cyd-display/home-like-2432s028.yaml').decode()
values = dict(re.findall(r'^ (\w+): "([^"]*)"', base, re.M))
self.assertEqual(values['TOUCH_AFFINE_XX'], '1.000000000')
self.assertEqual(values['DIRECT_ACTIONS'], 'false')
for i in range(1,11):
self.assertRegex(values[f'TILE{i}_ENTITY'], r'\.(example_|unused_)')
archive.extractall(tmp)
new_device.create(Path(tmp)/'cyd-display', 'fresh-board', 'Fresh board')
with self.assertRaises(FileExistsError): export_bundle.export(ROOT, output)
if __name__ == '__main__': unittest.main()