Regenerate child instances after presence service writes
Presence pattern and override handlers now call regenerate_for_child. Definition edit/activate pass HA-local today and configured horizon.
This commit is contained in:
@@ -270,6 +270,15 @@ class PresenceOverridesDao:
|
|||||||
)
|
)
|
||||||
return [_override_from_row(row) for row in rows]
|
return [_override_from_row(row) for row in rows]
|
||||||
|
|
||||||
|
async def get(self, override_id: int) -> PresenceOverrideRecord | None:
|
||||||
|
"""Return one override by id, or None when it does not exist."""
|
||||||
|
row = await self._database.fetch_one(
|
||||||
|
f"SELECT {_OVERRIDE_COLUMNS} FROM presence_overrides "
|
||||||
|
"WHERE id = ?",
|
||||||
|
(override_id,),
|
||||||
|
)
|
||||||
|
return _override_from_row(row) if row is not None else None
|
||||||
|
|
||||||
async def delete(self, override_id: int) -> bool:
|
async def delete(self, override_id: int) -> bool:
|
||||||
"""Remove one override by id; True if a row was removed."""
|
"""Remove one override by id; True if a row was removed."""
|
||||||
result = await self._database.execute(
|
result = await self._database.execute(
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ the Feature 07 handler stays the one in ``__init__``.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -22,6 +24,8 @@ from . import children as children_layer
|
|||||||
from . import completion as completion_layer
|
from . import completion as completion_layer
|
||||||
from . import quest_definitions as quest_definitions_layer
|
from . import quest_definitions as quest_definitions_layer
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_HORIZON_DAYS,
|
||||||
|
DEFAULT_HORIZON_DAYS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DOMAIN_SERVICES,
|
DOMAIN_SERVICES,
|
||||||
QUEST_WINDOWS,
|
QUEST_WINDOWS,
|
||||||
@@ -38,6 +42,7 @@ from .const import (
|
|||||||
SERVICE_UPDATE_QUEST_DEFINITION,
|
SERVICE_UPDATE_QUEST_DEFINITION,
|
||||||
)
|
)
|
||||||
from .dao_presence import PresenceOverridesDao, PresenceSchedulesDao
|
from .dao_presence import PresenceOverridesDao, PresenceSchedulesDao
|
||||||
|
from .materialize import regenerate_for_child
|
||||||
from .presence import PresenceSchedule
|
from .presence import PresenceSchedule
|
||||||
from .recurrence import ScheduleRule
|
from .recurrence import ScheduleRule
|
||||||
|
|
||||||
@@ -225,13 +230,24 @@ SCHEMA_MANAGE_CHILD = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _require_database(hass: HomeAssistant, find_runtime: FindRuntime):
|
def _require_runtime(hass: HomeAssistant, find_runtime: FindRuntime):
|
||||||
runtime = find_runtime(hass)
|
runtime = find_runtime(hass)
|
||||||
if runtime is None:
|
if runtime is None:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"NestQuest has no live database; cannot run this service"
|
"NestQuest has no live database; cannot run this service"
|
||||||
)
|
)
|
||||||
return runtime.database
|
return runtime
|
||||||
|
|
||||||
|
|
||||||
|
def _generation_kwargs(hass: HomeAssistant, runtime: Any) -> dict[str, Any]:
|
||||||
|
time_zone = ZoneInfo(hass.config.time_zone)
|
||||||
|
today = datetime.datetime.now(time_zone).date()
|
||||||
|
value = runtime.options.get(CONF_HORIZON_DAYS, DEFAULT_HORIZON_DAYS)
|
||||||
|
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
||||||
|
horizon_days = DEFAULT_HORIZON_DAYS
|
||||||
|
else:
|
||||||
|
horizon_days = value
|
||||||
|
return {"today": today, "horizon_days": horizon_days}
|
||||||
|
|
||||||
|
|
||||||
def _actor_kwargs(call: Any) -> dict[str, Any]:
|
def _actor_kwargs(call: Any) -> dict[str, Any]:
|
||||||
@@ -274,7 +290,7 @@ def _complete_quest(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
database = _require_runtime(hass, find_runtime).database
|
||||||
await completion_layer.complete_instance(
|
await completion_layer.complete_instance(
|
||||||
database,
|
database,
|
||||||
call.data["instance_id"],
|
call.data["instance_id"],
|
||||||
@@ -288,7 +304,7 @@ def _uncomplete_quest(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
database = _require_runtime(hass, find_runtime).database
|
||||||
await completion_layer.uncomplete_instance(
|
await completion_layer.uncomplete_instance(
|
||||||
database,
|
database,
|
||||||
call.data["instance_id"],
|
call.data["instance_id"],
|
||||||
@@ -302,7 +318,7 @@ def _create_quest_definition(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
database = _require_runtime(hass, find_runtime).database
|
||||||
data = call.data
|
data = call.data
|
||||||
await quest_definitions_layer.create_quest_definition(
|
await quest_definitions_layer.create_quest_definition(
|
||||||
database,
|
database,
|
||||||
@@ -321,9 +337,9 @@ def _update_quest_definition(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
runtime = _require_runtime(hass, find_runtime)
|
||||||
data = call.data
|
data = call.data
|
||||||
kwargs: dict[str, Any] = {}
|
kwargs: dict[str, Any] = _generation_kwargs(hass, runtime)
|
||||||
if "title" in data:
|
if "title" in data:
|
||||||
kwargs["title"] = data["title"]
|
kwargs["title"] = data["title"]
|
||||||
if "description" in data:
|
if "description" in data:
|
||||||
@@ -335,7 +351,7 @@ def _update_quest_definition(
|
|||||||
if "windows" in data:
|
if "windows" in data:
|
||||||
kwargs["windows"] = data["windows"]
|
kwargs["windows"] = data["windows"]
|
||||||
await quest_definitions_layer.edit_quest_definition(
|
await quest_definitions_layer.edit_quest_definition(
|
||||||
database, data["definition_id"], **kwargs
|
runtime.database, data["definition_id"], **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
return _with_errors(_handler)
|
return _with_errors(_handler)
|
||||||
@@ -345,11 +361,12 @@ def _set_quest_definition_active(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
runtime = _require_runtime(hass, find_runtime)
|
||||||
await quest_definitions_layer.set_quest_definition_active(
|
await quest_definitions_layer.set_quest_definition_active(
|
||||||
database,
|
runtime.database,
|
||||||
call.data["definition_id"],
|
call.data["definition_id"],
|
||||||
call.data["is_active"],
|
call.data["is_active"],
|
||||||
|
**_generation_kwargs(hass, runtime),
|
||||||
)
|
)
|
||||||
|
|
||||||
return _with_errors(_handler)
|
return _with_errors(_handler)
|
||||||
@@ -359,7 +376,7 @@ def _set_presence_pattern(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
runtime = _require_runtime(hass, find_runtime)
|
||||||
data = call.data
|
data = call.data
|
||||||
schedule = PresenceSchedule(
|
schedule = PresenceSchedule(
|
||||||
data["child_id"],
|
data["child_id"],
|
||||||
@@ -367,12 +384,17 @@ def _set_presence_pattern(
|
|||||||
data["anchor_date"],
|
data["anchor_date"],
|
||||||
data["pattern"],
|
data["pattern"],
|
||||||
)
|
)
|
||||||
await PresenceSchedulesDao(database).upsert_by_child(
|
await PresenceSchedulesDao(runtime.database).upsert_by_child(
|
||||||
schedule.child_id,
|
schedule.child_id,
|
||||||
schedule.cycle_length_weeks,
|
schedule.cycle_length_weeks,
|
||||||
schedule.anchor_date.isoformat(),
|
schedule.anchor_date.isoformat(),
|
||||||
schedule.encode(),
|
schedule.encode(),
|
||||||
)
|
)
|
||||||
|
await regenerate_for_child(
|
||||||
|
runtime.database,
|
||||||
|
schedule.child_id,
|
||||||
|
**_generation_kwargs(hass, runtime),
|
||||||
|
)
|
||||||
|
|
||||||
return _with_errors(_handler)
|
return _with_errors(_handler)
|
||||||
|
|
||||||
@@ -381,15 +403,20 @@ def _create_presence_override(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
runtime = _require_runtime(hass, find_runtime)
|
||||||
data = call.data
|
data = call.data
|
||||||
await PresenceOverridesDao(database).create(
|
await PresenceOverridesDao(runtime.database).create(
|
||||||
data["child_id"],
|
data["child_id"],
|
||||||
data["start_date"],
|
data["start_date"],
|
||||||
data["end_date"],
|
data["end_date"],
|
||||||
data["is_present"],
|
data["is_present"],
|
||||||
note=data.get("note"),
|
note=data.get("note"),
|
||||||
)
|
)
|
||||||
|
await regenerate_for_child(
|
||||||
|
runtime.database,
|
||||||
|
data["child_id"],
|
||||||
|
**_generation_kwargs(hass, runtime),
|
||||||
|
)
|
||||||
|
|
||||||
return _with_errors(_handler)
|
return _with_errors(_handler)
|
||||||
|
|
||||||
@@ -398,8 +425,20 @@ def _delete_presence_override(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
runtime = _require_runtime(hass, find_runtime)
|
||||||
await PresenceOverridesDao(database).delete(call.data["override_id"])
|
dao = PresenceOverridesDao(runtime.database)
|
||||||
|
override_id = call.data["override_id"]
|
||||||
|
override = await dao.get(override_id)
|
||||||
|
if override is None:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"override_id: presence override {override_id} does not exist"
|
||||||
|
)
|
||||||
|
await dao.delete(override_id)
|
||||||
|
await regenerate_for_child(
|
||||||
|
runtime.database,
|
||||||
|
override.child_id,
|
||||||
|
**_generation_kwargs(hass, runtime),
|
||||||
|
)
|
||||||
|
|
||||||
return _with_errors(_handler)
|
return _with_errors(_handler)
|
||||||
|
|
||||||
@@ -414,7 +453,7 @@ def _manage_child(
|
|||||||
hass: HomeAssistant, find_runtime: FindRuntime
|
hass: HomeAssistant, find_runtime: FindRuntime
|
||||||
) -> ServiceHandler:
|
) -> ServiceHandler:
|
||||||
async def _handler(call: Any) -> None:
|
async def _handler(call: Any) -> None:
|
||||||
database = _require_database(hass, find_runtime)
|
database = _require_runtime(hass, find_runtime).database
|
||||||
data = call.data
|
data = call.data
|
||||||
action = data["action"]
|
action = data["action"]
|
||||||
if action == MANAGE_CHILD_CREATE:
|
if action == MANAGE_CHILD_CREATE:
|
||||||
|
|||||||
@@ -455,6 +455,20 @@ def test_override_list_unknown_child_returns_empty(tmp_path) -> None:
|
|||||||
assert _with_db(tmp_path, "override-list-unknown.db")(_body) == []
|
assert _with_db(tmp_path, "override-list-unknown.db")(_body) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_get_round_trip(tmp_path) -> None:
|
||||||
|
async def _body(database, schedules, overrides, children, child):
|
||||||
|
record = await overrides.create(
|
||||||
|
child.id, "2026-09-10", "2026-09-11", True, note="camp"
|
||||||
|
)
|
||||||
|
found = await overrides.get(record.id)
|
||||||
|
assert found == record
|
||||||
|
assert await overrides.get(999) is None
|
||||||
|
return found
|
||||||
|
|
||||||
|
found = _with_db(tmp_path, "override-get.db")(_body)
|
||||||
|
assert found.note == "camp"
|
||||||
|
|
||||||
|
|
||||||
def test_override_delete_round_trip(tmp_path) -> None:
|
def test_override_delete_round_trip(tmp_path) -> None:
|
||||||
async def _body(database, schedules, overrides, children, child):
|
async def _body(database, schedules, overrides, children, child):
|
||||||
record = await overrides.create(
|
record = await overrides.create(
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ from custom_components.nestquest.const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
DOMAIN_SERVICES,
|
DOMAIN_SERVICES,
|
||||||
SERVICE_COMPLETE_QUEST,
|
SERVICE_COMPLETE_QUEST,
|
||||||
|
SERVICE_CREATE_PRESENCE_OVERRIDE,
|
||||||
SERVICE_CREATE_QUEST_DEFINITION,
|
SERVICE_CREATE_QUEST_DEFINITION,
|
||||||
|
SERVICE_DELETE_PRESENCE_OVERRIDE,
|
||||||
SERVICE_EXPORT_HISTORY_CSV,
|
SERVICE_EXPORT_HISTORY_CSV,
|
||||||
SERVICE_MANAGE_CHILD,
|
SERVICE_MANAGE_CHILD,
|
||||||
SERVICE_REGENERATE,
|
SERVICE_REGENERATE,
|
||||||
|
SERVICE_SET_PRESENCE_PATTERN,
|
||||||
SERVICE_SET_QUEST_DEFINITION_ACTIVE,
|
SERVICE_SET_QUEST_DEFINITION_ACTIVE,
|
||||||
SERVICE_UNCOMPLETE_QUEST,
|
SERVICE_UNCOMPLETE_QUEST,
|
||||||
)
|
)
|
||||||
@@ -246,6 +249,123 @@ async def test_complete_and_uncomplete_quest_call_business_layer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_presence_pattern_regenerates_child_instances(
|
||||||
|
hass, make_entry
|
||||||
|
) -> None:
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from custom_components.nestquest.children import list_children
|
||||||
|
from custom_components.nestquest.const import DEFAULT_HORIZON_DAYS
|
||||||
|
from custom_components.nestquest.dao_instances import QuestInstancesDao
|
||||||
|
|
||||||
|
entry = _wire(make_entry(), hass.registry)
|
||||||
|
assert await async_setup_entry(hass, entry) is True
|
||||||
|
await hass.services.call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_MANAGE_CHILD,
|
||||||
|
{"action": "create", "display_name": "Ada"},
|
||||||
|
)
|
||||||
|
child = (await list_children(entry.runtime_data.database))[0]
|
||||||
|
await hass.services.call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CREATE_QUEST_DEFINITION,
|
||||||
|
{
|
||||||
|
"title": "Brush teeth",
|
||||||
|
"rule": {"rule_type": "daily"},
|
||||||
|
"assignee_child_ids": [child.id],
|
||||||
|
"windows": ["morning"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.services.call(DOMAIN, SERVICE_REGENERATE)
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
end = (
|
||||||
|
datetime.date.today() + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)
|
||||||
|
).isoformat()
|
||||||
|
dao = QuestInstancesDao(entry.runtime_data.database)
|
||||||
|
assert await dao.list_by_date_range(child.id, today, end)
|
||||||
|
await hass.services.call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_PRESENCE_PATTERN,
|
||||||
|
{
|
||||||
|
"child_id": child.id,
|
||||||
|
"cycle_length_weeks": 1,
|
||||||
|
"anchor_date": today,
|
||||||
|
"pattern": {0: []},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert await dao.list_by_date_range(child.id, today, end) == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_presence_override_create_and_delete_regenerate(
|
||||||
|
hass, make_entry
|
||||||
|
) -> None:
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from custom_components.nestquest.children import list_children
|
||||||
|
from custom_components.nestquest.const import DEFAULT_HORIZON_DAYS
|
||||||
|
from custom_components.nestquest.dao_instances import QuestInstancesDao
|
||||||
|
from custom_components.nestquest.dao_presence import PresenceOverridesDao
|
||||||
|
|
||||||
|
entry = _wire(make_entry(), hass.registry)
|
||||||
|
assert await async_setup_entry(hass, entry) is True
|
||||||
|
await hass.services.call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_MANAGE_CHILD,
|
||||||
|
{"action": "create", "display_name": "Ada"},
|
||||||
|
)
|
||||||
|
child = (await list_children(entry.runtime_data.database))[0]
|
||||||
|
await hass.services.call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CREATE_QUEST_DEFINITION,
|
||||||
|
{
|
||||||
|
"title": "Brush teeth",
|
||||||
|
"rule": {"rule_type": "daily"},
|
||||||
|
"assignee_child_ids": [child.id],
|
||||||
|
"windows": ["morning"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.services.call(DOMAIN, SERVICE_REGENERATE)
|
||||||
|
today = datetime.date.today()
|
||||||
|
end = today + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)
|
||||||
|
dao = QuestInstancesDao(entry.runtime_data.database)
|
||||||
|
before = await dao.list_by_date_range(
|
||||||
|
child.id, today.isoformat(), end.isoformat()
|
||||||
|
)
|
||||||
|
assert before
|
||||||
|
await hass.services.call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CREATE_PRESENCE_OVERRIDE,
|
||||||
|
{
|
||||||
|
"child_id": child.id,
|
||||||
|
"start_date": today.isoformat(),
|
||||||
|
"end_date": end.isoformat(),
|
||||||
|
"is_present": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
await dao.list_by_date_range(
|
||||||
|
child.id, today.isoformat(), end.isoformat()
|
||||||
|
)
|
||||||
|
== []
|
||||||
|
)
|
||||||
|
override = (
|
||||||
|
await PresenceOverridesDao(
|
||||||
|
entry.runtime_data.database
|
||||||
|
).list_by_child_and_range(
|
||||||
|
child.id, today.isoformat(), end.isoformat()
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
await hass.services.call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_DELETE_PRESENCE_OVERRIDE,
|
||||||
|
{"override_id": override.id},
|
||||||
|
)
|
||||||
|
after = await dao.list_by_date_range(
|
||||||
|
child.id, today.isoformat(), end.isoformat()
|
||||||
|
)
|
||||||
|
assert len(after) == len(before)
|
||||||
|
|
||||||
|
|
||||||
async def test_export_history_csv_raises_home_assistant_error(
|
async def test_export_history_csv_raises_home_assistant_error(
|
||||||
hass, make_entry
|
hass, make_entry
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user