Show learning activity with Learning Progress and Last Sample sensors
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>
This commit is contained in:
1 parent
3c949333bd
commit
d2bd398a0f
9 files changed
+179
-11
No files matched your search
@@ -24,7 +24,7 @@ async def test_setup_reload_unload(hass, entry, inputs):
|
||||
assert coordinator.config_entry is entry
|
||||
registry = er.async_get(hass)
|
||||
entities = er.async_entries_for_config_entry(registry, entry.entry_id)
|
||||
assert len(entities) == 9 # 5 core + 4 forecast-error sensors
|
||||
assert len(entities) == 11 # 7 core + 4 forecast-error sensors
|
||||
ids = {e.unique_id: e.entity_id for e in entities}
|
||||
assert coordinator.data["status"] == "learning"
|
||||
status = hass.states.get(ids[f"{entry.entry_id}_status"])
|
||||
@@ -55,7 +55,7 @@ async def test_options_reload_removes_only_old_room(hass, entry, inputs):
|
||||
assert hass.data[DOMAIN][entry.entry_id].rooms == ["sensor.new"]
|
||||
assert registry.async_get(old) is None
|
||||
assert registry.async_get_entity_id("sensor", DOMAIN, f"{entry.entry_id}_sensor.new_predicted_60min")
|
||||
assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 9
|
||||
assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 11
|
||||
|
||||
|
||||
async def test_storage_restore_and_remove(hass, entry, inputs):
|
||||
@@ -238,3 +238,31 @@ async def test_forecast_error_sensors_score_scheduled_polls(hass, entry, inputs,
|
||||
assert state.attributes["unit_of_measurement"] == "°C"
|
||||
assert "rooms" in state.state_info["unrecorded_attributes"]
|
||||
assert hass.states.get(ids[30]).state == "unknown"
|
||||
|
||||
|
||||
async def test_learning_progress_and_last_sample_sensors(hass, entry, inputs, freezer):
|
||||
await setup(hass, entry)
|
||||
registry = er.async_get(hass)
|
||||
progress_id = registry.async_get_entity_id("sensor", DOMAIN, f"{entry.entry_id}_learning_progress")
|
||||
last_id = registry.async_get_entity_id("sensor", DOMAIN, f"{entry.entry_id}_last_sample")
|
||||
assert hass.states.get(progress_id).state == "0.0"
|
||||
last = hass.states.get(last_id)
|
||||
assert last.state == "first_sample"
|
||||
assert "lag_uncertain" in last.attributes["options"]
|
||||
future = dt_util.utcnow() + timedelta(minutes=5, seconds=1)
|
||||
freezer.move_to(future)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(last_id).state == "learned"
|
||||
# One OFF sample: (1/200 + 0/30 + 1/30) / 3
|
||||
assert float(hass.states.get(progress_id).state) == 1.3
|
||||
progress = hass.states.get(progress_id).attributes
|
||||
assert (progress["samples"], progress["off_samples"], progress["required"]["samples"]) == (1, 1, 200)
|
||||
hass.states.async_set("switch.boiler", "unavailable")
|
||||
future += timedelta(minutes=5)
|
||||
freezer.move_to(future)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
last = hass.states.get(last_id)
|
||||
assert last.state == "waiting_for_inputs"
|
||||
assert last.attributes["counts_since_start"] == {"first_sample": 1, "learned": 1, "waiting_for_inputs": 1}
|
||||
@@ -686,3 +686,14 @@ class BoundaryTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(data['forecast_accuracy'][15]['samples'], 2)
|
||||
self.fresh('sensor.room', 'sensor.outside')
|
||||
self.assertEqual((await self.sample())['forecast_accuracy'][15]['samples'], 3)
|
||||
|
||||
async def test_learning_progress_and_waiting_result(self):
|
||||
data = await self.sample()
|
||||
self.assertEqual((data['sample_result'], data['learning_progress']), ('first_sample', 0))
|
||||
self.ready()
|
||||
self.fresh('sensor.room', 'sensor.outside')
|
||||
self.assertEqual((await self.sample())['learning_progress'], 100)
|
||||
self.states['sensor.outside'].state = 'unavailable'
|
||||
data = await self.sample()
|
||||
self.assertEqual(data['sample_result'], 'waiting_for_inputs')
|
||||
self.assertEqual(data['sample_results_since_start']['waiting_for_inputs'], 1)
|
||||
@@ -9,6 +9,9 @@ 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):
|
||||
@@ -52,3 +55,6 @@ class FreshnessTests(unittest.TestCase):
|
||||
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))
|
||||
Reference in new issue
Block a user