diff --git a/custom_components/nestquest/blueprints/automation/afternoon_reminder.yaml b/custom_components/nestquest/blueprints/automation/afternoon_reminder.yaml index ffe0b73..34ea11c 100644 --- a/custom_components/nestquest/blueprints/automation/afternoon_reminder.yaml +++ b/custom_components/nestquest/blueprints/automation/afternoon_reminder.yaml @@ -69,21 +69,22 @@ conditions: actions: - action: !input notify_target - title: NestQuest afternoon reminder - message: | - {%- set ns = namespace(lines=[]) -%} - {%- for due in states.sensor - | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%} - {%- if due.attributes.get('present') -%} - {%- set remaining_values = states.sensor - | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$') - | selectattr('attributes.child_id', 'eq', due.attributes.child_id) - | map(attribute='state') - | list -%} - {%- if remaining_values and (remaining_values | first | int) > 0 -%} - {%- set line = due.attributes.child_name ~ ' still has ' ~ (remaining_values | first | int) ~ ' quest(s) open.' -%} - {%- set ns.lines = ns.lines + [line] -%} + data: + title: NestQuest afternoon reminder + message: | + {%- set ns = namespace(lines=[]) -%} + {%- for due in states.sensor + | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%} + {%- if due.attributes.get('present') -%} + {%- set remaining_values = states.sensor + | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$') + | selectattr('attributes.child_id', 'eq', due.attributes.child_id) + | map(attribute='state') + | list -%} + {%- if remaining_values and (remaining_values | first | int) > 0 -%} + {%- set line = due.attributes.child_name ~ ' still has ' ~ (remaining_values | first | int) ~ ' quest(s) open.' -%} + {%- set ns.lines = ns.lines + [line] -%} + {%- endif -%} {%- endif -%} - {%- endif -%} - {%- endfor -%} - {{- ns.lines | join('\n') -}} \ No newline at end of file + {%- endfor -%} + {{- ns.lines | join('\n') -}} \ No newline at end of file diff --git a/custom_components/nestquest/blueprints/automation/morning_summary.yaml b/custom_components/nestquest/blueprints/automation/morning_summary.yaml index 20f74b4..ea38e1d 100644 --- a/custom_components/nestquest/blueprints/automation/morning_summary.yaml +++ b/custom_components/nestquest/blueprints/automation/morning_summary.yaml @@ -46,32 +46,33 @@ conditions: actions: - action: !input notify_target - title: NestQuest morning summary - message: | - {%- set ns = namespace(lines=[]) -%} - {%- for due in states.sensor - | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%} - {%- if due.attributes.get('present') -%} - {%- set remaining_values = states.sensor - | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$') - | selectattr('attributes.child_id', 'eq', due.attributes.child_id) - | map(attribute='state') - | list -%} - {%- if remaining_values -%} - {%- set remaining = remaining_values | first | int -%} - {%- if remaining > 0 -%} - {%- set line = due.attributes.child_name ~ ' owes ' ~ remaining ~ ' quest(s) today.' -%} + data: + title: NestQuest morning summary + message: | + {%- set ns = namespace(lines=[]) -%} + {%- for due in states.sensor + | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%} + {%- if due.attributes.get('present') -%} + {%- set remaining_values = states.sensor + | selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$') + | selectattr('attributes.child_id', 'eq', due.attributes.child_id) + | map(attribute='state') + | list -%} + {%- if remaining_values -%} + {%- set remaining = remaining_values | first | int -%} + {%- if remaining > 0 -%} + {%- set line = due.attributes.child_name ~ ' owes ' ~ remaining ~ ' quest(s) today.' -%} + {%- else -%} + {%- set line = due.attributes.child_name ~ ' is all clear today (0 quests remaining).' -%} + {%- endif -%} {%- else -%} - {%- set line = due.attributes.child_name ~ ' is all clear today (0 quests remaining).' -%} + {%- set line = due.attributes.child_name ~ "'s quest count is unavailable today." -%} {%- endif -%} - {%- else -%} - {%- set line = due.attributes.child_name ~ "'s quest count is unavailable today." -%} + {%- set ns.lines = ns.lines + [line] -%} {%- endif -%} - {%- set ns.lines = ns.lines + [line] -%} + {%- endfor -%} + {%- if ns.lines -%} + {{- ns.lines | join('\n') -}} + {%- else -%} + No children present today. {%- endif -%} - {%- endfor -%} - {%- if ns.lines -%} - {{- ns.lines | join('\n') -}} - {%- else -%} - No children present today. - {%- endif -%} diff --git a/tests/blueprint_helpers.py b/tests/blueprint_helpers.py index 5752fbb..d4ec16e 100644 --- a/tests/blueprint_helpers.py +++ b/tests/blueprint_helpers.py @@ -82,6 +82,16 @@ def morning_action(bp: dict) -> dict: return single_action(bp) +def notify_data(bp: dict, index: int = 0) -> dict: + """Return a notify action's service ``data`` (title/message live + there in the modern HA action schema, not at the action's top level).""" + action = bp["actions"][index] + assert isinstance(action.get("data"), dict), ( + "notify payload fields must nest under data:" + ) + return action["data"] + + def iter_strings(node: object): """Yield every string value in a parsed YAML structure, recursively.""" if isinstance(node, str): diff --git a/tests/test_blueprint_afternoon.py b/tests/test_blueprint_afternoon.py index 1fe5458..7c13c77 100644 --- a/tests/test_blueprint_afternoon.py +++ b/tests/test_blueprint_afternoon.py @@ -17,6 +17,7 @@ import re from custom_components.nestquest.const import DEFAULT_AFTERNOON_REMINDER_TIME from tests.blueprint_helpers import ( + notify_data, BlueprintInput, blueprint, iter_strings, @@ -80,8 +81,9 @@ def test_afternoon_reminder_wires_inputs_not_literals() -> None: action = single_action(bp) assert action["action"] == BlueprintInput("notify_target") - assert action["title"] == AFTERNOON_TITLE - assert isinstance(action["message"], str) and action["message"].strip() + data = notify_data(afternoon_reminder()) + assert data["title"] == AFTERNOON_TITLE + assert isinstance(data["message"], str) and data["message"].strip() # --------------------------------------------------------------------------- @@ -125,7 +127,7 @@ def test_afternoon_reminder_lists_only_present_children_with_open_quests() -> No TemplateState("sensor.nestquest_household_quests_due_today", "7"), ], ) - rendered = render(single_action(afternoon_reminder())["message"], states=states) + rendered = render(notify_data(afternoon_reminder())["message"], states=states) assert rendered == "Alice still has 2 quest(s) open." assert "Carol" not in rendered assert "Bob" not in rendered @@ -218,7 +220,7 @@ def test_afternoon_reminder_collision_suffixed_duplicate_names() -> None: ), ], ) - rendered = render(single_action(afternoon_reminder())["message"], states=states) + rendered = render(notify_data(afternoon_reminder())["message"], states=states) assert rendered.split("\n") == [ "Ada still has 1 quest(s) open.", "Ada still has 3 quest(s) open.", diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index fbd49c3..ac8a991 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -26,6 +26,7 @@ from types import SimpleNamespace from custom_components.nestquest.const import DEFAULT_MORNING_SUMMARY_TIME from tests.blueprint_helpers import ( + notify_data, ALLOWED_ENTITY_SHAPES, BlueprintInput, _ENTITY_LITERAL_RE, @@ -88,8 +89,9 @@ def test_morning_summary_wires_inputs_not_literals() -> None: action = morning_action(bp) assert action["action"] == BlueprintInput("notify_target") - assert action["title"] == MORNING_TITLE - assert isinstance(action["message"], str) and action["message"].strip() + data = notify_data(bp) + assert data["title"] == MORNING_TITLE + assert isinstance(data["message"], str) and data["message"].strip() # --------------------------------------------------------------------------- @@ -139,7 +141,7 @@ def test_due_sensor_regex_selects_only_nestquest_due_sensors() -> None: including HA collision-suffixed duplicates (a second child with the same name gets entity ids ending _2) — and rejects everything else.""" - message = morning_action(morning_summary())["message"] + message = notify_data(morning_summary())["message"] match = re.search( r"\^(sensor\\\\\.nestquest_\.\*_quests_due_today\(_\\\\d\+\)\?\$)", message ) @@ -216,7 +218,7 @@ def morning_fixture() -> SimpleNamespace: def test_morning_summary_lists_present_child_and_omits_absent() -> None: """Present Alice appears with her count; absent Bob and decoys never do.""" - message = morning_action(morning_summary())["message"] + message = notify_data(morning_summary())["message"] rendered = render(message, states=morning_fixture()) assert rendered == "Alice owes 2 quest(s) today." assert "Bob" not in rendered @@ -250,7 +252,7 @@ def test_morning_summary_present_child_zero_remaining_is_all_clear() -> None: ), ], ) - rendered = render(morning_action(morning_summary())["message"], states=states) + rendered = render(notify_data(morning_summary())["message"], states=states) assert "Alice owes 2 quest(s) today." in rendered assert "Carol is all clear today (0 quests remaining)." in rendered @@ -267,7 +269,7 @@ def test_morning_summary_present_child_without_sensors_is_flagged() -> None: ), ], ) - rendered = render(morning_action(morning_summary())["message"], states=states) + rendered = render(notify_data(morning_summary())["message"], states=states) assert rendered == "Frances's quest count is unavailable today." @@ -299,7 +301,7 @@ def test_morning_summary_collision_suffixed_duplicate_names() -> None: ), ], ) - rendered = render(morning_action(morning_summary())["message"], states=states) + rendered = render(notify_data(morning_summary())["message"], states=states) lines = rendered.split("\n") assert lines == [ "Ada owes 1 quest(s) today.", @@ -323,7 +325,7 @@ def test_morning_summary_no_present_children() -> None: ), ], ) - rendered = render(morning_action(morning_summary())["message"], states=states) + rendered = render(notify_data(morning_summary())["message"], states=states) assert rendered == "No children present today."