Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

All notable changes to this project will be documented in this file.

## [1.2.7] - 2026-03-16

### Added
- **`usgs_earthquakes_feed_new_events` HA event**: The sensor now fires this event on the HA event bus every time new earthquake events are detected. Automations can use `trigger: platform: event / event_type: usgs_earthquakes_feed_new_events` to react instantly. The event payload contains `entry_id`, `count` (number of new events), and `events` (the list of new event dicts).
- **`EVENT_NEW_QUAKES` constant** added to `const.py` to hold the event name.

### Fixed
- **README**: Updated sensor description to reflect the `latest_events` delta semantics introduced in v1.2.6 (removed references to the old `events` attribute and the "last 50 events" cap). Added full documentation for the new HA event and an example automation.


### Fixed
- **Conceptual error in v1.2.5**: `latest_events` was incorrectly made a cumulative list that grew on every update, always containing all historical events. The correct behaviour is:
- `latest_events` contains **only the new events** detected in the current update cycle (events whose IDs have not been seen before).
- On the **first run** (or after HA restarts), all events returned by the feed are considered new, so `latest_events` is populated with all of them.
- On **subsequent runs** where no new earthquakes have been reported, `latest_events` is empty (`[]`), the sensor state does not change, and automations that trigger on state change are not fired.
- When a **new earthquake** is detected, `latest_events` contains only that event (or those events), the sensor state updates to the most recent event's timestamp, and the automation is triggered.

### Changed
- Replaced the cumulative `_latest_events` accumulator with an internal `_seen_ids: set[str]` that tracks which event IDs have already been reported. This is not exposed as a sensor attribute.
- Removed the now-unused `MAX_EVENTS` constant from `sensor.py`.

## [1.2.5] - 2026-03-16

### Fixed
- **`latest_events` was always empty after the first update cycle**: The sensor previously maintained two separate lists — `events` (cumulative) and `latest_events` (new events only per cycle). Because `latest_events` was reset to only the newly-detected IDs on every update, it became empty whenever no brand-new earthquakes arrived, causing the `format_events` service to return an empty result.

### Changed
- **Removed `events` attribute from the sensor**: Seismic events are now exposed solely through the `latest_events` attribute, which accumulates all events (up to 50) ordered from most recent to oldest — mirroring the previous behaviour of the `events` attribute.
- **Diagnostics now report `latest_events`**: The diagnostics payload has been updated to expose `latest_events` instead of the removed `events` key.

## [1.2.4] - 2026-03-16

### Fixed
Expand Down
59 changes: 51 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
- **Maximum Distance** from your location (Radius)
- Creates `geo_location` entities for each event.
- Includes a special sensor `sensor.usgs_earthquakes_feed_latest` that:
- Stores the last **50** new earthquake events (based on their unique `id`)
- State is the timestamp of the most recent event
- Exposes the full list of stored events via the `events` attribute
- State is the timestamp of the most recent **new** earthquake event
- Exposes new events via the `latest_events` attribute (only events detected in the current update cycle; empty when no new earthquakes have arrived)
- Fires a `usgs_earthquakes_feed_new_events` event on the HA event bus whenever new earthquakes are detected
- Includes a `format_events` action that returns earthquake events as formatted text via a response variable

---
Expand Down Expand Up @@ -112,8 +112,50 @@ Full list: [USGS GeoJSON Feed Documentation](https://earthquake.usgs.gov/earthqu

This sensor exposes:

- `state`: Timestamp of the latest event
- `events`: List of the last 50 new earthquakes (stored across restarts)
- `state`: Timestamp of the most recent **new** earthquake event (only changes when new earthquakes are detected)
- `latest_events`: List of new earthquakes detected in the current update cycle, ordered from newest to oldest. Empty when no new earthquakes have arrived since the last update.

> **How it works:**
> - **First run** (or after HA restart): `latest_events` contains all earthquakes that match your filter criteria.
> - **Subsequent updates with no new earthquakes**: `latest_events` is empty (`[]`) and the sensor state does not change.
> - **New earthquake detected**: `latest_events` contains only the new event(s), the sensor state updates to the newest event's timestamp.

---

## 📣 Event: `usgs_earthquakes_feed_new_events`

Every time new earthquakes are detected the integration fires this event on the HA event bus. You can use it as an automation trigger:

```yaml
trigger:
- platform: event
event_type: usgs_earthquakes_feed_new_events
```

The event data contains:

| Field | Description |
|---|---|
| `entry_id` | Config-entry ID of the integration instance |
| `count` | Number of new events detected |
| `events` | List of new earthquake event dicts |

### Example automation using the event:

```yaml
automation:
- alias: "Notify on new earthquake"
trigger:
- platform: event
event_type: usgs_earthquakes_feed_new_events
action:
- service: notify.mobile_app_my_phone
data:
title: "🌍 New Earthquake"
message: >
{{ trigger.event.data.count }} new earthquake(s) detected.
Latest: {{ trigger.event.data.events[0].title }}
```

---

Expand Down Expand Up @@ -168,9 +210,10 @@ The variable `quake_report.formatted_events` will contain a multiline string wit

## 📓 Notes

- On first setup, **all events** matching the filters are included.
- On updates, only **new events** (based on USGS `id`) are added.
- Sensor shows events in reverse chronological order (newest first).
- On first setup (or after HA restart), **all events** matching the filters are treated as new and included in `latest_events`.
- On subsequent updates, only **new events** (based on USGS `id`) are included in `latest_events`.
- When no new earthquakes are detected, `latest_events` is empty (`[]`) and the sensor state does not change.
- Events in `latest_events` are ordered from newest to oldest.
- All magnitude and distance values follow standard units (Mw, km).

---
Expand Down
3 changes: 3 additions & 0 deletions custom_components/usgs_earthquakes_feed/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
DOMAIN = "usgs_earthquakes_feed"

# Event fired on the HA event bus whenever new earthquake events are detected
EVENT_NEW_QUAKES = f"{DOMAIN}_new_events"

CONF_RADIUS = "radius"
CONF_MINIMUM_MAGNITUDE = "minimum_magnitude"
CONF_FEED_TYPE = "feed_type"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/usgs_earthquakes_feed/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ async def async_get_config_entry_diagnostics(
diagnostics = {
"config": entry.data,
"options": entry.options,
"events": data.get("events", []),
"latest_events": data.get("latest_events", []),
}
return diagnostics
2 changes: 1 addition & 1 deletion custom_components/usgs_earthquakes_feed/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"aio-geojson-usgs-earthquakes==0.3",
"aio-geojson-client==0.12"
],
"version": "1.2.4"
"version": "1.2.7"
}
54 changes: 23 additions & 31 deletions custom_components/usgs_earthquakes_feed/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import as_local

from .const import DOMAIN
from .const import DOMAIN, EVENT_NEW_QUAKES
from .helpers import parse_event_time

import logging
Expand All @@ -26,8 +26,6 @@

SIGNAL_EVENTS_UPDATED = f"{DOMAIN}_events_updated_{{}}"

MAX_EVENTS = 50 # Máximo de eventos a almacenar


class UsgsQuakesLatestSensor(SensorEntity):
"""Sensor to store the latest USGS quake events."""
Expand All @@ -44,7 +42,7 @@ def __init__(self, hass: HomeAssistant, entry_id: str, device_info: DeviceInfo)
self.hass = hass
self._entry_id = entry_id
self._attr_device_info = device_info
self._events: list[dict[str, Any]] = []
self._seen_ids: set[str] = set()
self._latest_events: list[dict[str, Any]] = []
self._unsub_dispatcher: Any = None
self._attr_native_value: datetime | None = None
Expand All @@ -66,56 +64,50 @@ async def _async_update_events(self) -> None:
"""Update sensor state from the shared event list."""
new_events = self.hass.data[DOMAIN][self._entry_id].get("events", [])

# Crear conjunto con las ids ya almacenadas
existing_ids = {e["id"] for e in self._events}

# Determinar si es primera ejecución (sin eventos guardados)
if not self._events:
filtered_events = new_events
else:
filtered_events = [e for e in new_events if e["id"] not in existing_ids]
# Determinar qué eventos son nuevos (no vistos antes)
filtered_events = [e for e in new_events if e["id"] not in self._seen_ids]

# Agregar nuevos eventos y reordenar
self._events.extend(filtered_events)
self._events = sorted(
self._events, key=parse_event_time, reverse=True
)[:MAX_EVENTS]
# Registrar los nuevos IDs como vistos
self._seen_ids.update(e["id"] for e in filtered_events)

# latest_events: eventos nuevos de esta actualización, ordenados del más reciente al más antiguo
self._latest_events = sorted(
filtered_events, key=parse_event_time, reverse=True
)
# latest_events: solo los eventos nuevos de este ciclo, del más reciente al más antiguo
self._latest_events = sorted(filtered_events, key=parse_event_time, reverse=True)

# Publicar latest_events en hass.data para que el servicio format_events pueda leerlos
entry_data = self.hass.data[DOMAIN].setdefault(self._entry_id, {})
entry_data["latest_events"] = self._latest_events

# Actualizar valor del sensor (fecha del más reciente)
if self._events:
# Actualizar el estado del sensor solo cuando lleguen eventos nuevos
if self._latest_events:
try:
time_val = self._events[0]["time"]
time_val = self._latest_events[0]["time"]
if isinstance(time_val, datetime):
dt = time_val if time_val.tzinfo else time_val.replace(tzinfo=timezone.utc)
else:
dt = datetime.fromisoformat(str(time_val).replace("Z", "+00:00"))
self._attr_native_value = as_local(dt)
except (ValueError, AttributeError):
_LOGGER.debug("Could not parse native value from event time: %s", self._events[0].get("time"))
self._attr_native_value = None
else:
self._attr_native_value = None
_LOGGER.debug("Could not parse native value from event time: %s", self._latest_events[0].get("time"))

# Disparar evento en el bus de HA para que las automatizaciones puedan reaccionar
self.hass.bus.async_fire(
EVENT_NEW_QUAKES,
{
"entry_id": self._entry_id,
"count": len(self._latest_events),
"events": self._latest_events,
},
)

_LOGGER.debug(
"USGS Quakes Sensor actualizado. Nuevos eventos: %d. Total almacenados: %d.",
"USGS Quakes Sensor actualizado. Nuevos eventos: %d.",
len(filtered_events),
len(self._events),
)
self.async_write_ha_state()

@property
def extra_state_attributes(self) -> dict[str, Any]:
return {
"events": self._events,
"latest_events": self._latest_events,
}

Expand Down
Loading