"""Timestamp boundaries and UI translation coverage without HA dependencies.""" import importlib.util import json import unittest from datetime import datetime, timedelta, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[1] / 'custom_components' / 'heating_mpc' spec = importlib.util.spec_from_file_location('quality', ROOT / 'data_quality.py') quality = importlib.util.module_from_spec(spec) spec.loader.exec_module(quality) spec = importlib.util.spec_from_file_location('heating_mpc_const', ROOT / 'const.py') const = importlib.util.module_from_spec(spec) spec.loader.exec_module(const) class FreshnessTests(unittest.TestCase): def test_age_boundaries(self): now = datetime(2026, 10, 26, tzinfo=timezone.utc) for seconds, expected in ((0, 'valid'), (1800, 'valid'), (1801, 'stale'), (-1, 'future_timestamp')): with self.subTest(seconds=seconds): result = quality.freshness(now - timedelta(seconds=seconds), now, 1800) self.assertEqual(result['status'], expected) self.assertEqual(result['age_seconds'], seconds) def test_missing_naive_and_malformed_times(self): now = datetime.now(timezone.utc) for timestamp in (None, 'bad', datetime.now()): self.assertEqual(quality.freshness(timestamp, now, 1800)['status'], 'invalid_timestamp') def test_invalid_limits_rejected(self): now = datetime.now(timezone.utc) for limit in (0, -1, float('inf'), float('nan')): with self.assertRaises(ValueError): quality.freshness(now, now, limit) def test_report_age_history_keeps_24_hour_maximum(self): history = quality.ReportAgeHistory() start = datetime(2026, 10, 26, tzinfo=timezone.utc) self.assertEqual(history.observe('sensor.a', start, 2400), 2400) self.assertEqual(history.observe('sensor.b', start, 60), 60) for minutes in range(5, 24 * 60, 5): result = history.observe('sensor.a', start + timedelta(minutes=minutes), 300) self.assertEqual(result, 2400) # still inside the 24-hour window later = start + timedelta(hours=24, minutes=5) self.assertEqual(history.observe('sensor.a', later, 300), 300) self.assertEqual(history.observe('sensor.b', later, 30), 30) def test_inputs_differ_ignores_noise_but_not_meaningful_changes(self): before = {('temperature', 'sensor.room'): 20.0, ('power', 'sensor.ups'): 210.0, ('wind_direction', 'sensor.dir'): 355.0, ('wind', 'sensor.wind'): 4.0, ('window', 'binary_sensor.w'): False, ('thermostat', 'climate.t'): ('heat', 22, None)} noise = {**before, ('temperature', 'sensor.room'): 20.1, ('power', 'sensor.ups'): 240.0, ('wind_direction', 'sensor.dir'): 5.0, ('wind', 'sensor.wind'): 4.6} self.assertEqual(quality.inputs_differ(before, noise), []) # 10° across north is small for key, value in ((('temperature', 'sensor.room'), 20.5), (('power', 'sensor.ups'), 610.0), (('wind_direction', 'sensor.dir'), 40.0), (('window', 'binary_sensor.w'), True), (('window', 'binary_sensor.w'), None), (('thermostat', 'climate.t'), ('heat', 21, None)), (('temperature', 'sensor.room'), None)): with self.subTest(key=key, value=value): self.assertEqual(quality.inputs_differ(before, {**before, key: value}), [key]) self.assertEqual(quality.inputs_differ(before, {**before, ('power', 'sensor.new'): 5.0}), [('power', 'sensor.new')]) def test_freshness_settings_translated(self): for name in ('strings.json', 'translations/en.json', 'translations/ro.json'): data = json.loads((ROOT / name).read_text()) # Configure opens a menu: settings, or resetting the learned model. options = data['options'] self.assertEqual(set(options['step']['init']['menu_options']), {'settings', 'reset_model'}) self.assertTrue(options['step']['reset_model']['data']['confirm_reset']) self.assertEqual(set(options['abort']), {'model_reset', 'model_not_reset'}) for section, step in (('config', 'user'), ('options', 'settings')): labels = data[section]['step'][step]['data'] for key in ('room_max_age_minutes', 'weather_max_age_minutes', 'room_stale_use_last_value'): self.assertTrue(labels[key]) help_text = data[section]['step'][step]['data_description'] self.assertIn('24', help_text['room_stale_use_last_value']) # Every poll outcome has a translated state name. states = data['entity']['sensor']['last_sample']['state'] self.assertEqual(set(states), set(const.SAMPLE_RESULTS))