Skip to content

Add domain expiration sensor to Gatus integration - #178661

Open
TN-1 wants to merge 9 commits into
home-assistant:devfrom
TN-1:gatus-domain-expiry
Open

Add domain expiration sensor to Gatus integration#178661
TN-1 wants to merge 9 commits into
home-assistant:devfrom
TN-1:gatus-domain-expiry

Conversation

@TN-1

@TN-1 TN-1 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Proposed change

Adds sensor to each endpoint representing domain expiry time

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a diagnostic sensor showing each Gatus endpoint’s domain-expiration duration in days.

Changes:

  • Adds domain-expiration conversion and sensor metadata.
  • Adds translation, fixtures, snapshots, and missing-value coverage.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
homeassistant/components/gatus/sensor.py Defines the domain-expiration sensor.
homeassistant/components/gatus/strings.json Adds the sensor name.
tests/components/gatus/conftest.py Supplies domain-expiration fixture data.
tests/components/gatus/test_sensor.py Tests parsing and missing values.
tests/components/gatus/snapshots/test_sensor.ambr Captures the new entity and state.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@TN-1
TN-1 marked this pull request as draft August 10, 2026 12:28
Copilot AI review requested due to automatic review settings August 10, 2026 13:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/gatus/sensor.py:60

  • Anchor the expiration timestamp to the Gatus result instead of the time Home Assistant reads the property. domain_expiration is a remaining duration captured with the result, so polling the same result repeatedly makes this value move forward on every coordinator refresh; for example, a stale 300-day result will keep reporting 300 days from now rather than a fixed expiration date. Use the result/check timestamp if the client exposes it, or cache the derived timestamp until a genuinely new result arrives.
            dt_util.utcnow()
            + timedelta(seconds=result.domain_expiration / 1_000_000_000)

homeassistant/components/gatus/sensor.py:84

  • Create or dynamically add the domain-expiration entity when the value later becomes available. This setup-time filter permanently omits the entity when the initial check lacks certificate data (for example, during a transient failed check), even if subsequent coordinator updates contain domain_expiration; unlike the other sensors, it will not recover until the integration reloads.
        if description.key != "domain_expiration"
        or (
            (endpoint := coordinator.data.get(endpoint_key))
            and endpoint.results
            and endpoint.results[-1].domain_expiration is not None

@TN-1
TN-1 marked this pull request as ready for review August 10, 2026 13:40
Copilot AI review requested due to automatic review settings August 12, 2026 06:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/gatus/sensor.py:84

  • Create the expiration entity when the field first becomes available on any coordinator update. This setup-time-only filter permanently omits the entity when the latest result has no expiration during startup—even if a later successful check supplies it—because this platform has no listener that adds the entity afterward; track endpoint/description pairs and register a coordinator listener, as the binary-sensor platform does for dynamic endpoints.
        if description.key != "domain_expiration"
        or (
            (endpoint := coordinator.data.get(endpoint_key))
            and endpoint.results
            and endpoint.results[-1].domain_expiration is not None

homeassistant/components/gatus/sensor.py:60

  • Anchor the duration to the Gatus result timestamp instead of the current read time. domain_expiration is the remaining duration recorded when Gatus performed the check, so adding it to utcnow() makes stale results report a later expiry and causes the timestamp to drift whenever the same coordinator data is written again; parse/expose the result timestamp and add the duration to that fixed instant.
            dt_util.utcnow()
            + timedelta(seconds=result.domain_expiration / 1_000_000_000)

Comment thread homeassistant/components/gatus/sensor.py Outdated
@home-assistant
home-assistant Bot marked this pull request as draft August 12, 2026 11:30
@home-assistant

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copilot AI review requested due to automatic review settings August 12, 2026 14:37
@TN-1
TN-1 marked this pull request as ready for review August 12, 2026 14:38
@home-assistant
home-assistant Bot requested a review from joostlek August 12, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/gatus/sensor.py:71

  • Preserve the current second when deriving the expiration timestamp. Resetting second to zero shifts every reported expiration by up to 59 seconds whenever an update occurs outside the first second of a minute.
            dt_util.utcnow().replace(microsecond=0, second=0)

homeassistant/components/gatus/strings.json:36

  • Restore the last_event translation alongside the new entry. Removing it renames the existing entity to sensor.core_backend_service and drops its localized enum labels, as the updated snapshot demonstrates.
      "domain_expiration": {
        "name": "Domain expiration"
      },

Copilot AI review requested due to automatic review settings August 13, 2026 05:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

homeassistant/components/gatus/sensor.py:74

  • Preserve the current seconds when deriving the expiration timestamp. Resetting second and microsecond before adding this relative duration moves every reported expiration backward by up to almost a minute; add the complete duration to utcnow() instead.
            dt_util.utcnow().replace(microsecond=0, second=0)
            + timedelta(
                seconds=int(endpoint.results[-1].domain_expiration / 1_000_000_000)
            )

homeassistant/components/gatus/strings.json:36

  • Restore the last_event translation alongside the new entry. Removing it makes the existing enum sensor lose its name and state translations; as the updated snapshot shows, new installs now create sensor.core_backend_service instead of sensor.core_backend_service_last_event.
      "domain_expiration": {
        "name": "Domain expiration"
      },

Copilot AI review requested due to automatic review settings August 15, 2026 14:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

homeassistant/components/gatus/sensor.py:77

  • The timestamp is computed from dt_util.utcnow() at evaluation time, which can make the sensor drift and become inaccurate relative to when the API data was fetched (the API value appears to be a “time until expiration” at fetch time). Compute the absolute expiration timestamp once when the coordinator fetches data (using the fetch time, e.g. coordinator’s update timestamp) and store it, or base the calculation on the coordinator’s last update time to keep it consistent and correct. Also, avoid float rounding by using integer division: endpoint.results[-1].domain_expiration // 1_000_000_000 instead of int(x / 1_000_000_000).
        value_fn=lambda endpoint: (
            dt_util.utcnow().replace(microsecond=0, second=0)
            + timedelta(
                seconds=int(endpoint.results[-1].domain_expiration / 1_000_000_000)
            )
            if endpoint.results and endpoint.results[-1].domain_expiration is not None
            else None
        ),

homeassistant/components/gatus/sensor.py:95

  • Gating entity creation on the initial availability of domain_expiration means the entity will never appear if the value becomes available later (unless the config entry is reloaded and entities are re-added). Prefer always adding the entity and returning None/unknown when the field is missing, or implement explicit dynamic entity add/remove behavior when the coordinator data changes so entity lifecycle matches runtime data.
    async_add_entities(
        GatusEndpointSensor(coordinator, entry, endpoint_key, description)
        for endpoint_key, endpoint in coordinator.data.items()
        for description in SENSOR_TYPES
        if description.key != "domain_expiration"
        or (endpoint.results and endpoint.results[-1].domain_expiration is not None)
    )

homeassistant/components/gatus/sensor.py:114

  • _attr_translation_key is being set directly from description.translation_key. If some descriptions intentionally omit translation_key, forcing _attr_translation_key to None can change naming/translation behavior (snapshots suggest the “last event” label may have been lost). Consider either ensuring every description has an explicit translation_key (including last_event if it remains) or only setting _attr_translation_key when description.translation_key is not None to preserve existing entity naming/translation defaults.
        """Initialize the sensor."""
        super().__init__(coordinator, entry, endpoint_key)
        self.entity_description = description
        self._attr_translation_key = description.translation_key
        self._attr_unique_id = f"{entry.entry_id}_{endpoint_key}_{description.key}"

Comment thread homeassistant/components/gatus/strings.json
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 16, 2026 03:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (5)

homeassistant/components/gatus/sensor.py:77

  • The conversion from nanoseconds to seconds uses float division (/) and then casts to int, which can introduce precision loss for large nanosecond values. Use integer arithmetic (e.g., floor division by 1_000_000_000) to avoid float rounding. Also, replace(..., second=0) truncates seconds (rounding down to the minute), which may produce an incorrect expiration timestamp; consider only stripping microsecond (or not truncating at all) so the computed datetime remains accurate.
        value_fn=lambda endpoint: (
            dt_util.utcnow().replace(microsecond=0, second=0)
            + timedelta(
                seconds=int(endpoint.results[-1].domain_expiration / 1_000_000_000)
            )
            if endpoint.results and endpoint.results[-1].domain_expiration is not None
            else None
        ),

homeassistant/components/gatus/sensor.py:95

  • Gating entity creation on the initial coordinator data means the domain_expiration sensor will never appear for an endpoint that starts without domain_expiration but later begins reporting it (until the config entry is reloaded). If you want the entity to show up automatically when data becomes available, consider always creating the entity and returning None when missing (so it’s unknown/unavailable), or implement dynamic entity addition when the coordinator data changes.
    async_add_entities(
        GatusEndpointSensor(coordinator, entry, endpoint_key, description)
        for endpoint_key, endpoint in coordinator.data.items()
        for description in SENSOR_TYPES
        if description.key != "domain_expiration"
        or (endpoint.results and endpoint.results[-1].domain_expiration is not None)
    )

homeassistant/components/gatus/strings.json:38

  • This removes the last_event.state translations (healthy/resolved/start/unhealthy). If the last_event sensor still reports an enum-like state (as shown in snapshots/options), dropping these translations regresses UI/localization by showing raw state keys. Consider restoring last_event.state mappings (even if only for English) or changing the sensor to a non-enum representation if state labels are no longer relevant.
      "domain_expiration": {
        "name": "Domain expiration"
      },
      "last_event": {
        "name": "Last event"
      },

tests/components/gatus/test_sensor.py:178

  • This replaces the prior coverage for a missing events list producing STATE_UNKNOWN on the last_event sensor. If the last_event sensor and its missing-events behavior still exist, consider adding a separate test to keep that behavior covered while also testing the new domain-expiration behavior.
async def test_sensor_missing_domain_expiration(
    hass: HomeAssistant,
    mock_gatus_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test that an endpoint missing domain_expiration does not create the domain expiration sensor."""

tests/components/gatus/conftest.py:51

  • The domain_expiration=7776000000000000 literal is a hard-to-read magic number. Consider defining it as a named constant (e.g., NINETY_DAYS_NS = 90 * 24 * 60 * 60 * 1_000_000_000) or adding an inline comment indicating the unit (nanoseconds) and intended duration, to make the fixture easier to understand and maintain.
                    results=[
                        Result(
                            success=True,
                            status=200,
                            duration=23123100,
                            domain_expiration=7776000000000000,
                        )
                    ],

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants