Add domain expiration sensor to Gatus integration - #178661
Conversation
There was a problem hiding this comment.
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.
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:60
- Anchor the expiration timestamp to the Gatus result instead of the time Home Assistant reads the property.
domain_expirationis 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
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: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_expirationis the remaining duration recorded when Gatus performed the check, so adding it toutcnow()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)
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
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 second when deriving the expiration timestamp. Resetting
secondto 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_eventtranslation alongside the new entry. Removing it renames the existing entity tosensor.core_backend_serviceand drops its localized enum labels, as the updated snapshot demonstrates.
"domain_expiration": {
"name": "Domain 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 (2)
homeassistant/components/gatus/sensor.py:74
- Preserve the current seconds when deriving the expiration timestamp. Resetting
secondandmicrosecondbefore adding this relative duration moves every reported expiration backward by up to almost a minute; add the complete duration toutcnow()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_eventtranslation 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 createsensor.core_backend_serviceinstead ofsensor.core_backend_service_last_event.
"domain_expiration": {
"name": "Domain 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 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_000instead ofint(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_expirationmeans 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 returningNone/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_keyis being set directly fromdescription.translation_key. If some descriptions intentionally omittranslation_key, forcing_attr_translation_keytoNonecan change naming/translation behavior (snapshots suggest the “last event” label may have been lost). Consider either ensuring every description has an explicittranslation_key(includinglast_eventif it remains) or only setting_attr_translation_keywhendescription.translation_keyis notNoneto 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}"
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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 (5)
homeassistant/components/gatus/sensor.py:77
- The conversion from nanoseconds to seconds uses float division (
/) and then casts toint, 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 strippingmicrosecond(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_expirationsensor will never appear for an endpoint that starts withoutdomain_expirationbut 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 returningNonewhen missing (so it’sunknown/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.statetranslations (healthy/resolved/start/unhealthy). If thelast_eventsensor still reports an enum-like state (as shown in snapshots/options), dropping these translations regresses UI/localization by showing raw state keys. Consider restoringlast_event.statemappings (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
eventslist producingSTATE_UNKNOWNon thelast_eventsensor. If thelast_eventsensor 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=7776000000000000literal 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,
)
],
Proposed change
Adds sensor to each endpoint representing domain expiry time
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: