Currently we don't have a delay trigger for geofences.
Standard Traccar doesn't have a "wait until continuously inside for X seconds" option for geofence enter — that's why you couldn't find it. And geofenceCrossed won't help here: it's the opposite tool. It's enabled with event.geofence.segmentCrossing=true and fires when the straight line between two consecutive positions crosses a geofence even if no position landed inside (so visits aren't missed with long reporting intervals). That makes detection more aggressive, so it would fire on more drive-bys, not fewer.
Two things that solve it:
1) Reshape the geofence tighter around the station apron/parking so it doesn't overlap the road. This alone removes most of the spurious clipping and needs no code.
2) Add a small "dwell" event handler if you build from source. It starts a timer when a unit enters and only raises an event once it's been continuously inside for a configured number of seconds; a unit passing by never reaches the limit, so nothing fires. It resets when the unit leaves, so it re-arms for the next real arrival.
Steps:
a. Add the event type in src/main/java/org/traccar/model/Event.java:
public static final String TYPE_GEOFENCE_DWELL = "geofenceDwell";
b. Create src/main/java/org/traccar/handler/events/GeofenceDwellHandler.java:
package org.traccar.handler.events;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.traccar.model.Event;
import org.traccar.model.Geofence;
import org.traccar.model.Position;
import org.traccar.session.cache.CacheManager;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Singleton
public class GeofenceDwellHandler extends BasePositionEventHandler {
public static final String ATTRIBUTE_DWELL_LIMIT = "dwellLimit"; // seconds, set on the geofence
public static final String ATTRIBUTE_DWELL_TIME = "dwellTime"; // reported on the event
// per device: geofenceId -> entry time (+ whether we already fired for this stay)
private final Map<Long, Map<Long, DwellEntry>> insideStates = new ConcurrentHashMap<>();
private static final class DwellEntry {
private final Date startTime;
private boolean notified;
private DwellEntry(Date startTime) {
this.startTime = startTime;
}
}
@Inject
public GeofenceDwellHandler(CacheManager cacheManager) {
super(cacheManager);
}
@Override
protected void onPosition(Position position, Position lastPosition, Callback callback) {
Set<Long> current = new HashSet<>();
if (position.getGeofenceIds() != null) {
current.addAll(position.getGeofenceIds());
}
Map<Long, DwellEntry> deviceState =
insideStates.computeIfAbsent(position.getDeviceId(), key -> new ConcurrentHashMap<>());
// reset timers for geofences we're no longer inside (re-arms for the next visit)
deviceState.keySet().removeIf(geofenceId -> !current.contains(geofenceId));
for (long geofenceId : current) {
Geofence geofence = cacheManager.getObject(Geofence.class, geofenceId);
if (geofence == null) {
continue;
}
long limit = geofence.getLong(ATTRIBUTE_DWELL_LIMIT); // seconds
if (limit <= 0) {
continue; // no dwell configured on this geofence
}
DwellEntry entry = deviceState.get(geofenceId);
if (entry == null) {
deviceState.put(geofenceId, new DwellEntry(position.getFixTime())); // start timer
continue;
}
if (entry.notified) {
continue; // already fired for this continuous stay
}
long elapsed = position.getFixTime().getTime() - entry.startTime.getTime();
if (elapsed >= limit * 1000L) {
Event event = new Event(Event.TYPE_GEOFENCE_DWELL, position);
event.setGeofenceId(geofenceId);
event.set(ATTRIBUTE_DWELL_TIME, elapsed / 1000L);
callback.eventDetected(event);
entry.notified = true;
}
}
}
}
c. Register it in src/main/java/org/traccar/ProcessingHandler.java, in the eventHandlers list, right after GeofenceEventHandler.class:
GeofenceEventHandler.class,
GeofenceDwellHandler.class,
d. (optional, for a nice label in the web app) add to the web translations, e.g. en.json:
"eventGeofenceDwell": "Geofence dwell",
e. On the station geofence, add an attribute dwellLimit with the number of seconds a unit must stay before it counts as "in quarters" (e.g. 20). Then create a Notification for the geofenceDwell event type.
Now a unit that just drives past (inside for a couple of seconds) never triggers, while one that actually pulls into quarters and stays does. It uses the timestamps on the positions, fires once per stay, and re-arms after the unit leaves.
If you also want a "left quarters" event that ignores brief clips, the same pattern works in reverse: start a timer on a real exit and fire only after the unit has been continuously outside for X seconds.
Our geofence for one of our ambulance stations sits right on one of our main roads. I am trying to take advantage of geofence alerts for enter and exit to denote units entering/leaving quarters but when units pass by to/from calls the events are inadvertently firing. I looked for a way to delay a geofence enter alert until continuously inside the fence for x seconds but could not find. Also wonder how geofence crossed event can help here.
Any suggestions would be appreciated.