3e959b3df9
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>
127 lines
5.8 KiB
Python
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()
|