Skip to content

Commit 669c24d

Browse files
danieldotnlclaude
andauthored
Fix hassfest validation: move URLs from translations to placeholders (#249)
* Fix hassfest validation: move URLs from translations to placeholders Home Assistant PR #154224 (merged Jan 27, 2026) introduced validation that prevents URLs from being directly embedded in translation strings. This commit implements the proper solution using description placeholders: - Created strings.json with URL placeholders for form steps - Added URL constants and placeholder functions in config_flow.py - Updated SchemaFlowFormStep instances to inject URLs via description_placeholders - Removed URLs from menu step descriptions (SchemaFlowMenuStep doesn't support placeholders) This keeps the URLs maintainable in code while allowing translators to work with clean, locale-agnostic strings. Fixes hassfest validation failures reported in issue #154226 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix unit tests: pass hass parameter to Template constructor Home Assistant now requires the hass parameter when creating Template objects (will be mandatory in 2025.10). Updated three failing tests to pass coordinator.hass to Template constructor. Fixes test_start_with_counter, test_start_with_source, and test_start_with_time. All 126 tests now passing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Remove translations/en.json in favor of strings.json Home Assistant now uses strings.json as the primary translation file. The old translations/en.json with embedded URLs is no longer needed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Use translations/en.json for custom integration (remove strings.json) strings.json is for core HA integrations. Custom integrations use translations/en.json at runtime. URLs are removed from translations and injected via description_placeholders in config_flow.py. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Add URL placeholders to translations The placeholders {buymeacoffee_url}, {device_class_url}, and {state_class_url} are now in the translation strings and will be replaced with actual URLs by the description_placeholders functions at runtime. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 1f7f0e8 commit 669c24d

4 files changed

Lines changed: 59 additions & 12 deletions

File tree

.devcontainer.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
{
22
"name": "danieldotnl/measureit",
33
"image": "mcr.microsoft.com/vscode/devcontainers/python:3.13",
4-
"postCreateCommand": "scripts/setup",
4+
"postCreateCommand": "scripts/setup && gh auth setup-git",
5+
"features": {
6+
"ghcr.io/devcontainers/features/node:1": {
7+
"version": "24"
8+
},
9+
"ghcr.io/devcontainers/features/github-cli:1.0.15": {},
10+
"ghcr.io/anthropics/devcontainer-features/claude-code:1": {}
11+
},
12+
"mounts": [
13+
"source=${env:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,consistency=cached"
14+
],
515
"forwardPorts": [8123],
616
"portsAttributes": {
717
"8123": {
@@ -43,6 +53,5 @@
4353
}
4454
}
4555
},
46-
"remoteUser": "vscode",
47-
"features": {}
56+
"remoteUser": "vscode"
4857
}

custom_components/measureit/config_flow.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
if TYPE_CHECKING:
6161
from collections.abc import Mapping
6262

63+
# URL constants for description placeholders
64+
_DEVICE_CLASS_URL = "https://www.home-assistant.io/integrations/sensor/#device-class"
65+
_STATE_CLASS_URL = "https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes"
66+
_BUYMEACOFFEE_URL = "https://www.buymeacoffee.com/danieldotnl"
67+
6368
PERIOD_OPTIONS = [
6469
selector.SelectOptionDict(value="hour", label="hour"),
6570
selector.SelectOptionDict(value="day", label="day"),
@@ -388,6 +393,35 @@ async def validate_sensor_edit(
388393
DATA_SCHEMA_THANK_YOU = vol.Schema({})
389394

390395

396+
async def get_sensors_step_placeholders(
397+
handler: SchemaCommonFlowHandler, # noqa: ARG001
398+
) -> dict[str, str]:
399+
"""Return description placeholders for sensors step."""
400+
return {
401+
"device_class_url": _DEVICE_CLASS_URL,
402+
"state_class_url": _STATE_CLASS_URL,
403+
}
404+
405+
406+
async def get_thank_you_placeholders(
407+
handler: SchemaCommonFlowHandler, # noqa: ARG001
408+
) -> dict[str, str]:
409+
"""Return description placeholders for thank you step."""
410+
return {
411+
"buymeacoffee_url": _BUYMEACOFFEE_URL,
412+
}
413+
414+
415+
async def get_add_sensors_placeholders(
416+
handler: SchemaCommonFlowHandler, # noqa: ARG001
417+
) -> dict[str, str]:
418+
"""Return description placeholders for add sensors step."""
419+
return {
420+
"device_class_url": _DEVICE_CLASS_URL,
421+
"state_class_url": _STATE_CLASS_URL,
422+
}
423+
424+
391425
CONFIG_FLOW = {
392426
"user": SchemaFlowMenuStep(["time", "source", "count"]),
393427
"time": SchemaFlowFormStep(
@@ -415,9 +449,11 @@ async def validate_sensor_edit(
415449
validate_user_input=validate_sensor_setup,
416450
suggested_values=get_add_sensor_suggested_values,
417451
next_step="thank_you",
452+
description_placeholders=get_sensors_step_placeholders,
418453
),
419454
"thank_you": SchemaFlowFormStep(
420455
DATA_SCHEMA_THANK_YOU,
456+
description_placeholders=get_thank_you_placeholders,
421457
),
422458
}
423459

@@ -434,6 +470,7 @@ async def validate_sensor_edit(
434470
suggested_values=get_add_sensor_suggested_values,
435471
validate_user_input=validate_sensor_setup,
436472
next_step="thank_you",
473+
description_placeholders=get_add_sensors_placeholders,
437474
),
438475
"select_edit_sensor": SchemaFlowFormStep(
439476
get_select_sensor_schema,
@@ -455,6 +492,7 @@ async def validate_sensor_edit(
455492
),
456493
"thank_you": SchemaFlowFormStep(
457494
DATA_SCHEMA_THANK_YOU,
495+
description_placeholders=get_thank_you_placeholders,
458496
),
459497
}
460498

custom_components/measureit/translations/en.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"step": {
44
"user": {
55
"title": "Choose what you want to measure (what)",
6-
"description": "Thank you for setting up MeasureIt!\nIf you need help with the configuration have a look at the [readme](https://github.com/danieldotnl/ha-measureit) or ask a question on the [community forum](https://community.home-assistant.io/t/measureit-measure-all-you-need-based-on-time-and-templates/660614).\n\nChoose what you want to measure:\n**Time:** Measure the elapsed time while conditions are met.\n**Source:** Measure the state changes of a source entity, while conditions are met.\n**Counter:** Measure the number of times something (described in a template) occurs, while conditions are met.",
6+
"description": "Thank you for setting up MeasureIt!\nIf you need help with the configuration, have a look at the readme or ask a question on the community forum.\n\nChoose what you want to measure:\n**Time:** Measure the elapsed time while conditions are met.\n**Source:** Measure the state changes of a source entity, while conditions are met.\n**Counter:** Measure the number of times something (described in a template) occurs, while conditions are met.",
77
"menu_options": {
88
"time": "Time",
99
"source": "Source",
@@ -45,7 +45,7 @@
4545
},
4646
"sensors": {
4747
"title": "Configure the sensors (how)",
48-
"description": "Configure the sensors. When in doubt, stick to the defaults. Individual sensor settings can be adjusted after this setup via 'configure'.\n\n**Reset periods:** Select a predefined period to measure (when the meter will reset). Alternatively, provide a custom cron expression. Each period becomes a separate sensor.\n**Value template:** A template that is applied on the output of the sensor. Use `value` to refer to the sensor state.\n**Unit of measurement:** The unit of what your are measuring. E.g.: m3\n**Device class:** Find more about device classes [here](https://www.home-assistant.io/integrations/sensor/#device-class).\n**State class**: Find more about state classes [here](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes).",
48+
"description": "Configure the sensors. When in doubt, stick to the defaults. Individual sensor settings can be adjusted after this setup via 'configure'.\n\n**Reset periods:** Select a predefined period to measure (when the meter will reset). Alternatively, provide a custom cron expression. Each period becomes a separate sensor.\n**Value template:** A template that is applied on the output of the sensor. Use `value` to refer to the sensor state.\n**Unit of measurement:** The unit of what your are measuring. E.g.: m3\n**Device class:** Find more about device classes [{device_class_url}].\n**State class**: Find more about state classes [{state_class_url}].",
4949
"data": {
5050
"periods": "Reset periods:",
5151
"unit_of_measurement": "Unit of measurement",
@@ -56,7 +56,7 @@
5656
},
5757
"thank_you": {
5858
"title": "Thank you for setting up MeasureIt!",
59-
"description": "Did you know that I'm incredibly motivated by coffee? ☕\nIf you like using MeasureIt, please consider buying me a coffee!\nhttps://www.buymeacoffee.com/danieldotnl 🙏"
59+
"description": "Did you know that I'm incredibly motivated by coffee? ☕\nIf you like using MeasureIt, please consider buying me a coffee!\n{buymeacoffee_url} 🙏"
6060
}
6161
},
6262
"error": {
@@ -77,7 +77,7 @@
7777
},
7878
"add_sensors": {
7979
"title": "Add sensor(s)",
80-
"description": "Add and configure one or more sensors. When in doubt, stick to the defaults.\n\n**Reset periods:** Select the periods you want to measure (when the meter will reset). Each period becomes a separate sensor.\n**Value template:** A template that is applied on the output of the sensor. Use `value` to refer to the sensor state.\n**Unit of measurement:** The unit of what your are measuring. E.g.: m3\n**Device class:** Find more about device classes [here](https://www.home-assistant.io/integrations/sensor/#device-class).\n**State class**: Find more about state classes [here](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes).",
80+
"description": "Add and configure one or more sensors. When in doubt, stick to the defaults.\n\n**Reset periods:** Select the periods you want to measure (when the meter will reset). Each period becomes a separate sensor.\n**Value template:** A template that is applied on the output of the sensor. Use `value` to refer to the sensor state.\n**Unit of measurement:** The unit of what your are measuring. E.g.: m3\n**Device class:** Find more about device classes [{device_class_url}].\n**State class**: Find more about state classes [{state_class_url}].",
8181
"data": {
8282
"period": "Reset periods:",
8383
"unit_of_measurement": "Unit of measurement",
@@ -97,7 +97,7 @@
9797
},
9898
"thank_you": {
9999
"title": "Thank you for setting up MeasureIt!",
100-
"description": "Did you know that I'm incredibly motivated by coffee? ☕\nIf you like using MeasureIt, please consider buying me a coffee!\nhttps://www.buymeacoffee.com/danieldotnl 🙏"
100+
"description": "Did you know that I'm incredibly motivated by coffee? ☕\nIf you like using MeasureIt, please consider buying me a coffee!\n{buymeacoffee_url} 🙏"
101101
},
102102
"edit_sensor": {
103103
"title": "Edit sensor",

tests/unit/test_coordinator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ def test_async_on_heartbeat(coordinator: MeasureItCoordinator) -> None:
186186

187187
def test_start_with_counter(coordinator: MeasureItCoordinator) -> None:
188188
"""Test start."""
189-
coordinator._condition_template = Template("{{ True }}")
190-
coordinator._counter_template = Template("{{ True }}")
189+
coordinator._condition_template = Template("{{ True }}", coordinator.hass)
190+
coordinator._counter_template = Template("{{ True }}", coordinator.hass)
191191
assert coordinator._condition_template_listener is None
192192
assert coordinator._time_window_listener is None
193193
assert coordinator._condition_template_listener is None
@@ -200,7 +200,7 @@ def test_start_with_counter(coordinator: MeasureItCoordinator) -> None:
200200
def test_start_with_source(coordinator: MeasureItCoordinator) -> None:
201201
"""Test start."""
202202
coordinator._meter_type = MeterType.SOURCE
203-
coordinator._condition_template = Template("{{ True }}")
203+
coordinator._condition_template = Template("{{ True }}", coordinator.hass)
204204
coordinator._source_entity = "sensor.test"
205205
coordinator._get_sensor_state = MagicMock(return_value=123)
206206
entity = MeasureItCoordinatorEntity()
@@ -221,7 +221,7 @@ def test_start_with_source(coordinator: MeasureItCoordinator) -> None:
221221
def test_start_with_time(coordinator: MeasureItCoordinator) -> None:
222222
"""Test start."""
223223
coordinator._meter_type = MeterType.TIME
224-
coordinator._condition_template = Template("{{ True }}")
224+
coordinator._condition_template = Template("{{ True }}", coordinator.hass)
225225
assert coordinator._condition_template_listener is None
226226
assert coordinator._time_window_listener is None
227227
assert coordinator._heartbeat_listener is None

0 commit comments

Comments
 (0)