During a first test the Status entity only said "learning" for the first half hour (the radiator-lag wait after a restart), and the attributes explaining why were hard to find. Add two sensors: - Learning Progress (%): for the least trained room, the mean of its three readiness requirements (200 samples, 30 ON, 30 OFF), each capped at 100 %; 100 % means ready. Attributes show the counts. - Last Sample: an enum of the last poll's learning outcome, with English and Romanian state names (e.g. "Waiting for radiators to settle"). Its history shows when learning happens. Polls blocked by missing shared inputs now report waiting_for_inputs. Readiness thresholds move to const.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
3.1 KiB
Python
61 lines
3.1 KiB
Python
"""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_freshness_settings_translated(self):
|
|
for name in ('strings.json', 'translations/en.json', 'translations/ro.json'):
|
|
data = json.loads((ROOT / name).read_text())
|
|
for section, step in (('config', 'user'), ('options', 'init')):
|
|
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))
|