Skip to content

Add certificate expiration sensor to Gatus integration - #178660

Open
TN-1 wants to merge 14 commits into
home-assistant:devfrom
TN-1:gatus-cert-expiry
Open

Add certificate expiration sensor to Gatus integration#178660
TN-1 wants to merge 14 commits into
home-assistant:devfrom
TN-1:gatus-cert-expiry

Conversation

@TN-1

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

Copy link
Copy Markdown
Contributor

Proposed change

Adds sensor to each endpoint with SSL cert expiry.

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

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 certificate-expiration monitoring to Gatus endpoint sensors.

Changes:

  • Converts certificate lifetime from nanoseconds to days.
  • Adds translated entity metadata and snapshots.
  • Tests populated and missing expiration values.

Reviewed changes

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

Show a summary per file
File Description
homeassistant/components/gatus/sensor.py Defines the certificate-expiration sensor.
homeassistant/components/gatus/strings.json Adds its translated name.
tests/components/gatus/conftest.py Supplies certificate fixture data.
tests/components/gatus/test_sensor.py Tests certificate sensor behavior.
tests/components/gatus/snapshots/test_sensor.ambr Captures entity and state snapshots.

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

Comment thread homeassistant/components/gatus/sensor.py
Copilot AI review requested due to automatic review settings August 10, 2026 11:13

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)

tests/components/gatus/snapshots/test_sensor.ambr:2

  • Regenerate the Gatus translations before recording this snapshot. The fallback Duration name and entity ID show that the new translation was not loaded; this also makes the missing-certificate test ineffective because it checks sensor.backend_service_certificate_expiration while an accidentally created entity would currently be named sensor.backend_service_duration. Run the translation-generation command and refresh the snapshot so it records the certificate-expiration name and ID.
# name: test_sensor_setup_and_states[sensor.core_backend_service_duration-entry]

homeassistant/components/gatus/sensor.py:79

  • Add certificate sensors when certificate data first appears rather than filtering only during platform setup. async_setup_entry runs once, so an HTTPS endpoint whose latest result lacks certificate data at startup (for example, while unreachable) never gets this entity even after later coordinator updates provide it. Track endpoints with certificate data and register a coordinator listener, as binary_sensor.py:26-41 does, while preventing duplicates.
        if description.key != "certificate_expiration"
        or (endpoint.results and endpoint.results[-1].certificate_expiration is not None)

Copilot AI review requested due to automatic review settings August 10, 2026 11:23

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)

tests/components/gatus/snapshots/test_sensor.ambr:2

  • Regenerate the Gatus English translations before updating this snapshot. This expectation uses the duration device-class fallback (sensor.core_backend_service_duration) instead of the added certificate_expiration translation, so it does not verify the production entity name and will conflict with a freshly generated translation set; rerun python3 -m script.translations develop --integration gatus and regenerate the snapshot.
# name: test_sensor_setup_and_states[sensor.core_backend_service_duration-entry]

homeassistant/components/gatus/sensor.py:80

  • Discover certificate sensors on coordinator updates rather than only during initial setup. If Home Assistant starts while an HTTPS endpoint has a failed result without certificate_expiration, this filter permanently omits the sensor even after a later successful refresh provides the value; add a coordinator listener (tracking created endpoint/sensor pairs) so the entity is created when certificate data first appears.
        if description.key != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None

Copilot AI review requested due to automatic review settings August 10, 2026 11:36

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:80

  • Add certificate entities when certificate data first appears after setup. This filter runs only during platform setup, so an HTTPS endpoint whose first result lacks certificate data (for example, a transient failed check) never gets the sensor after a later coordinator refresh supplies certificate_expiration; register a coordinator listener that adds newly eligible certificate sensors and cover that transition in a test.
        if description.key != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None

tests/components/gatus/snapshots/test_sensor.ambr:16

  • Regenerate the integration translations before recording this snapshot so the entity is named “Certificate expiration.” The snapshot currently blesses the device-class fallback sensor.core_backend_service_duration (and “Duration” below) even though its translation key and unique ID are for certificate expiration, exposing the wrong entity name and failing to verify the new translation.
    'entity_id': 'sensor.core_backend_service_duration',

Copilot AI review requested due to automatic review settings August 10, 2026 11:42

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 (1)

homeassistant/components/gatus/sensor.py:80

  • Add certificate entities when certificate data first becomes available after setup. This filter examines only the initial coordinator payload, and unlike the binary-sensor platform this platform has no listener that can add an omitted entity later; if the latest startup check lacks certificate data (for example, during a transient TLS failure), the certificate entity remains missing even after later updates provide an expiration.
        if description.key != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None

@joostlek joostlek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of storing the certificate expiration as days, can we calculate the timestamp when it expires? that way the data is always correct. We can even remove variance to make sure it doesn't change it's date every update

@home-assistant
home-assistant Bot marked this pull request as draft August 10, 2026 12:26

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:83

  • Add the certificate entity when certificate data first becomes available instead of deciding only during platform setup. If the latest result lacks certificate data on the initial refresh (for example, during a transient TLS failure), this generator omits the entity permanently because coordinator updates do not rerun async_setup_entry, even when later results contain an expiration. Track additions from coordinator updates with deduplication, while still avoiding entities for endpoints that never report certificate data.
        if description.key != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None
        )

homeassistant/components/gatus/sensor.py:60

  • Base the expiration on the result's observation timestamp rather than the current Home Assistant time. certificate_expiration is a duration captured for that Gatus result, but the coordinator can read the same result repeatedly between Gatus checks; adding it to utcnow() therefore moves the reported expiry later on every refresh. Compute it from the result timestamp and cover a stale result in the test.
            dt_util.utcnow()
            + timedelta(seconds=result.certificate_expiration / 1_000_000_000)

@TN-1
TN-1 marked this pull request as ready for review August 10, 2026 13:32
@home-assistant
home-assistant Bot requested a review from joostlek August 10, 2026 13:32
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 (1)

homeassistant/components/gatus/sensor.py:83

  • Add certificate entities when certificate data first becomes available after setup. An endpoint can legitimately have empty results on the first refresh, but this filter permanently skips its certificate sensor because the sensor platform does not register a coordinator listener; mirror the dynamic discovery pattern in binary_sensor.py:26-41 and add the entity on a later update.
        if description.key != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None
        )

Comment thread homeassistant/components/gatus/sensor.py Outdated
@home-assistant
home-assistant Bot marked this pull request as draft August 12, 2026 11:29
Copilot AI review requested due to automatic review settings August 12, 2026 14:33

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 seconds when deriving the expiration timestamp. Because certificate_expiration is treated as a duration from now, resetting second and microsecond makes every value early by the current sub-minute offset; the test hides this by freezing exactly on a minute boundary.
            dt_util.utcnow().replace(microsecond=0, second=0)

homeassistant/components/gatus/strings.json:35

  • Restore the last_event translation block alongside the new certificate translation. Removing it leaves the existing enum without its name/state translations and changes new installations from sensor.<endpoint>_last_event to sensor.<endpoint>, as the updated snapshot demonstrates; restore the block and regenerate the snapshot.
      "certificate_expiration": {
        "name": "Certificate expiration"

@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 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 (3)

homeassistant/components/gatus/sensor.py:98

  • Add the certificate sensor when expiration data first becomes available rather than deciding only during setup. If the initial result lacks this field (for example, during a transient failed check), this filter permanently suppresses the entity even when later coordinator updates provide a certificate expiration; use a coordinator listener to add it on first availability, or keep an unknown entity from setup.
        if description.key != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None
        )

homeassistant/components/gatus/strings.json:36

  • Restore the last_event translations alongside the new certificate label. SENSOR_TYPES still exposes translation_key="last_event"; removing its entry drops the entity name and enum-state translations, as the updated snapshot’s rename to sensor.core_backend_service demonstrates.
      "certificate_expiration": {
        "name": "Certificate expiration"

homeassistant/components/gatus/sensor.py:71

  • Preserve the current second when calculating the expiration timestamp. Resetting second before adding the duration shifts every reported expiration backward by the current 0–59 second offset.
            dt_util.utcnow().replace(microsecond=0, second=0)

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 2 comments.

Suppressed comments (3)

homeassistant/components/gatus/sensor.py:98

  • Conditionally creating the certificate_expiration entity based on the current coordinator data at setup time means the entity will never exist unless the integration is reloaded (e.g., if Gatus starts returning certificate info later for an endpoint). Consider always creating the entity and returning None when unavailable, or basing entity creation on a stable capability signal rather than a single snapshot of 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 != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None
        )
    )

homeassistant/components/gatus/sensor.py:78

  • dt_util.utcnow().replace(microsecond=0, second=0) truncates seconds to the start of the minute, introducing up to a 59-second error in the computed expiration timestamp. If truncation is only intended to stabilize tests/snapshots, it’s better handled in tests (which you already do via freezer) while keeping production values accurate (e.g., only stripping microseconds, or not rounding at all).
        value_fn=lambda endpoint: (
            dt_util.utcnow().replace(microsecond=0, second=0)
            + timedelta(
                seconds=int(endpoint.results[-1].certificate_expiration / 1_000_000_000)
            )
            if endpoint.results
            and endpoint.results[-1].certificate_expiration is not None
            else None
        ),

tests/components/gatus/test_sensor.py:183

  • This change removes coverage for the existing last_event sensor’s “missing events” behavior (previously asserting STATE_UNKNOWN when events=[]). Since sensor.py still defines last_event, consider keeping a dedicated test for the missing-events case (in addition to the new certificate expiration test) to avoid losing regression coverage for that sensor.
async def test_sensor_missing_certificate_expiration(
    hass: HomeAssistant,
    mock_gatus_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test that a result missing certificate expiration creates no entity."""
    mock_gatus_client.get_endpoints_statuses.return_value = [
        EndpointStatus(
            key="backend_service",
            name="Backend Service",
            group=None,

Comment thread homeassistant/components/gatus/strings.json
Comment thread homeassistant/components/gatus/sensor.py Outdated
Copilot AI review requested due to automatic review settings August 15, 2026 14:49

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 (2)

homeassistant/components/gatus/strings.json:36

  • last_event translation strings were removed, but the sensor platform still appears to define/use a last_event sensor description (including state options). If the entity still exists, removing these strings will regress UI naming/state translations (state labels will fall back to raw values). Either restore the entity.sensor.last_event block (including state mappings) or remove/rename the last_event sensor in sensor.py and update snapshots/tests to match the intended new sensor key.
      "certificate_expiration": {
        "name": "Certificate expiration"
      },

homeassistant/components/gatus/sensor.py:98

  • Conditionally creating entities based on a value being present in the first fetched payload can lead to a permanently missing entity until the config entry is reloaded (e.g., certificate data becomes available later, but the entity was never created). In Home Assistant it’s typically better to create the entity consistently and let it report None / unknown (or use available to reflect support) when the data isn’t present, so the entity model doesn’t depend on transient runtime payload differences.
    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 != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None
        )
    )

Comment thread homeassistant/components/gatus/sensor.py Outdated
Copilot AI review requested due to automatic review settings August 16, 2026 03:00

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 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (6)

homeassistant/components/gatus/coordinator.py:31

  • Rounding last_update_time to the nearest minute can make the derived certificate_expiration timestamp incorrect by up to 59 seconds. Consider storing the full dt_util.utcnow() value (no .replace(...)) and, if test determinism is the goal, rely on freezing time in tests rather than truncating production timestamps.
        self.last_update_time = dt_util.utcnow().replace(second=0, microsecond=0)

homeassistant/components/gatus/coordinator.py:54

  • Rounding last_update_time to the nearest minute can make the derived certificate_expiration timestamp incorrect by up to 59 seconds. Consider storing the full dt_util.utcnow() value (no .replace(...)) and, if test determinism is the goal, rely on freezing time in tests rather than truncating production timestamps.
        self.last_update_time = dt_util.utcnow().replace(second=0, microsecond=0)

homeassistant/components/gatus/sensor.py:100

  • Conditionally creating the certificate_expiration entity based on the current data means the entity will never appear later if the endpoint starts reporting certificate_expiration after initial setup (until reload). A more reliable pattern is to always create the entity and return None/unknown from native_value when the field is absent, so entity availability can change with data without requiring a reload.
    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 != "certificate_expiration"
        or (
            endpoint.results and endpoint.results[-1].certificate_expiration is not None
        )
    )

homeassistant/components/gatus/sensor.py:75

  • Using integer division (//) truncates sub-second precision and makes the conversion logic slightly harder to read. Prefer a non-truncating conversion (e.g., divide by 1_000_000_000 as a float) and/or add a short comment documenting the unit (nanoseconds duration) expected from certificate_expiration to avoid future misinterpretation.
        value_fn=lambda coordinator, endpoint: (
            coordinator.last_update_time
            + timedelta(
                seconds=endpoint.results[-1].certificate_expiration // 1_000_000_000
            )

tests/components/gatus/test_sensor.py:173

  • This test appears to replace the prior coverage for the last_event sensor behavior when events=[]. If there isn’t another test in this file covering the last_event missing-events case, it would be good to add a separate test for it, and keep this new test focused on certificate-expiration behavior.
async def test_sensor_missing_certificate_expiration(

tests/components/gatus/test_sensor.py:178

  • This test appears to replace the prior coverage for the last_event sensor behavior when events=[]. If there isn’t another test in this file covering the last_event missing-events case, it would be good to add a separate test for it, and keep this new test focused on certificate-expiration behavior.
    """Test that a result missing certificate expiration creates no entity."""

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