Add certificate expiration sensor to Gatus integration - #178660
Conversation
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Durationname and entity ID show that the new translation was not loaded; this also makes the missing-certificate test ineffective because it checkssensor.backend_service_certificate_expirationwhile an accidentally created entity would currently be namedsensor.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_entryruns 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, asbinary_sensor.py:26-41does, while preventing duplicates.
if description.key != "certificate_expiration"
or (endpoint.results and endpoint.results[-1].certificate_expiration is not None)
There was a problem hiding this comment.
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 addedcertificate_expirationtranslation, so it does not verify the production entity name and will conflict with a freshly generated translation set; rerunpython3 -m script.translations develop --integration gatusand 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
There was a problem hiding this comment.
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',
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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_expirationis a duration captured for that Gatus result, but the coordinator can read the same result repeatedly between Gatus checks; adding it toutcnow()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)
There was a problem hiding this comment.
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-41and add the entity on a later update.
if description.key != "certificate_expiration"
or (
endpoint.results and endpoint.results[-1].certificate_expiration is not None
)
There was a problem hiding this comment.
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_expirationis treated as a duration from now, resettingsecondandmicrosecondmakes 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_eventtranslation block alongside the new certificate translation. Removing it leaves the existing enum without its name/state translations and changes new installations fromsensor.<endpoint>_last_eventtosensor.<endpoint>, as the updated snapshot demonstrates; restore the block and regenerate the snapshot.
"certificate_expiration": {
"name": "Certificate expiration"
There was a problem hiding this comment.
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_eventtranslations alongside the new certificate label.SENSOR_TYPESstill exposestranslation_key="last_event"; removing its entry drops the entity name and enum-state translations, as the updated snapshot’s rename tosensor.core_backend_servicedemonstrates.
"certificate_expiration": {
"name": "Certificate expiration"
homeassistant/components/gatus/sensor.py:71
- Preserve the current second when calculating the expiration timestamp. Resetting
secondbefore adding the duration shifts every reported expiration backward by the current 0–59 second offset.
dt_util.utcnow().replace(microsecond=0, second=0)
There was a problem hiding this comment.
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_expirationentity 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 returningNonewhen 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 viafreezer) 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_eventsensor’s “missing events” behavior (previously assertingSTATE_UNKNOWNwhenevents=[]). Sincesensor.pystill defineslast_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,
There was a problem hiding this comment.
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_eventtranslation strings were removed, but the sensor platform still appears to define/use alast_eventsensor 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 theentity.sensor.last_eventblock (includingstatemappings) or remove/rename thelast_eventsensor insensor.pyand 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 useavailableto 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
)
)
There was a problem hiding this comment.
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_timeto the nearest minute can make the derivedcertificate_expirationtimestamp incorrect by up to 59 seconds. Consider storing the fulldt_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_timeto the nearest minute can make the derivedcertificate_expirationtimestamp incorrect by up to 59 seconds. Consider storing the fulldt_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_expirationentity based on the current data means the entity will never appear later if the endpoint starts reportingcertificate_expirationafter initial setup (until reload). A more reliable pattern is to always create the entity and returnNone/unknown fromnative_valuewhen 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 by1_000_000_000as a float) and/or add a short comment documenting the unit (nanoseconds duration) expected fromcertificate_expirationto 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_eventsensor behavior whenevents=[]. If there isn’t another test in this file covering thelast_eventmissing-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_eventsensor behavior whenevents=[]. If there isn’t another test in this file covering thelast_eventmissing-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."""
Proposed change
Adds sensor to each endpoint with SSL cert expiry.
Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: