Offline Mobile Edits & Conflict Reconciliation
An offline edit is a measurement taken at one moment against a copy of the network taken at another, submitted into a network that has moved on since both. That is not a flaw in offline working; it is what offline working is. The engineering question is what happens when the crew’s observation meets the office changes made while they were out — and the answer most estates default to, whichever side the tool favours, quietly discards whichever source it was configured against. Losing office corrections is annoying. Losing field observations is corrosive, because the crew learns that recording things is pointless. This guide sets out per-field reconciliation between field and office, what to do when both sides are observations, and when to stop deciding and dispatch a check. It applies the ownership discipline from field data capture and mobile sync to the specific case of an offline shift.
Environment Prerequisites
- A replica or offline map area with a recorded check-out moment, because the conflict set is everything that changed in the network since that moment.
- Per-field ownership declared between field and office, and stored as configuration rather than embedded in a synchronisation setting.
- Provenance on office changes. The reconciler needs to know whether an office value came from a human, a bulk import, or a telemetry feed, because a telemetry value is itself an observation.
- Timestamps that are comparable. Field capture time, office edit time and telemetry event time must share a clock, or recency comparisons are meaningless.
- A held-conflict queue with a service level, and a dispatch path for the checks it raises.
- Python 3.11 with
pandas>=2.0; this layer reasons about records and provenance rather than geometry, so it needs no spatial stack.
Schema-Aware Reconciliation Protocol — Run Before Applying Anything
- Establish the check-out moment and the conflict window. Everything the office changed between check-out and synchronisation is in scope; anything outside it is not a conflict no matter how similar it looks.
- Reduce each conflict to fields. A record-level conflict is almost always a small number of differing fields, and the ownership map only applies at field level.
- Attach provenance to the office side. A steward’s correction and a SCADA update are both “the office” and they resolve differently against a field observation.
- Normalise clocks before comparing recency. Mobile devices, the enterprise geodatabase and the telemetry historian frequently disagree by minutes; decide which is authoritative and convert.
- Identify contradictions, not just differences. A crew recording a valve as closed when the record says open is a contradiction about the physical world. A crew recording a condition grade the office never held is simply new information.
Minimal Reproducible Implementation
The reconciler below takes a batch of field edits, the office changes made during the window, and the ownership map, and produces a per-field decision with its reason. It applies nothing itself; it returns decisions for the applier to act on.
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from datetime import datetime
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
LOG = logging.getLogger("offline-reconcile")
FIELD_OWNED = {"position", "condition_grade", "operable", "operational_status"}
OFFICE_OWNED = {"asset_class", "asset_type", "network_association", "work_order_status"}
OBSERVED_SOURCES = {"SCADA", "FIELD_INSPECTION"} # office values that are measurements
@dataclass
class Change:
field_name: str
value: object
at: datetime
source: str # FIELD, STEWARD, SCADA, IMPORT, FIELD_INSPECTION
@dataclass
class Decision:
asset_id: str
field_name: str
outcome: str # TAKE_FIELD, TAKE_OFFICE, HOLD
reason: str
def reconcile_field(asset_id: str, field_change: Change,
office_change: Change | None) -> Decision:
"""Decide one field where a field edit may meet an office change.
The office side may be absent, in which case the field edit simply applies. Where
both exist, ownership decides — except when both sides are observations, where the
newer one wins, and except where the two contradict each other about something
physical, which is held for a check.
"""
name = field_change.field_name
if office_change is None:
return Decision(asset_id, name, "TAKE_FIELD", "no competing office change")
both_observed = office_change.source in OBSERVED_SOURCES
same_value = field_change.value == office_change.value
if same_value:
return Decision(asset_id, name, "TAKE_FIELD", "both sides agree")
if name in FIELD_OWNED:
if both_observed:
newer = "TAKE_FIELD" if field_change.at >= office_change.at else "TAKE_OFFICE"
return Decision(asset_id, name, newer,
f"both are observations — newer wins ({office_change.source})")
return Decision(asset_id, name, "TAKE_FIELD",
f"field-owned; office change from {office_change.source} logged")
if name in OFFICE_OWNED:
# A field edit that contradicts an office-owned value is information, not noise.
return Decision(asset_id, name, "HOLD",
f"field contradicts office-owned {name} — dispatch a check")
return Decision(asset_id, name, "HOLD", f"no ownership declared for {name}")
def reconcile_batch(field_changes: list[Change], office_changes: list[Change],
asset_id: str) -> list[Decision]:
"""Reconcile every field a crew touched on one asset."""
office_by_field = {c.field_name: c for c in office_changes}
decisions = [reconcile_field(asset_id, fc, office_by_field.get(fc.field_name))
for fc in field_changes]
held = sum(d.outcome == "HOLD" for d in decisions)
LOG.info("%s: %d field(s) reconciled, %d held", asset_id, len(decisions), held)
return decisions
The clause worth arguing about is the one that holds when a field observation contradicts an office-owned value. It would be simpler to let the office win — that is what ownership says — but a crew reporting that a valve is a different type than the record claims has produced evidence the catalogue cannot. Holding it costs one review; discarding it silently costs the observation and, eventually, the crew’s willingness to make another.
Production Deployment Pattern
- Reconcile per shift, not per edit. A crew’s submission is one unit of work, and a reconciliation report covering the shift is far more useful to them than fifty notifications.
- Log the losing side always. Every decision should record what the other side held, so a later dispute has both values rather than one and a memory.
- Give held conflicts a dispatch path. A hold with no way to raise a check becomes a queue entry that ages. The check is usually five minutes of a crew’s time on their next pass.
- Report contradiction rates by asset class. A class where field observations frequently contradict the record is telling you the record is wrong at scale, which is a data-quality programme rather than a series of individual conflicts.
- Never reconcile silently on both sides. If both the field edit and the office change are discarded in favour of a merge, nobody can reconstruct what either party asserted.
- Feed the outcome into the crew application. The disposition path is the same one the validator uses, and reusing it means one channel rather than two.
Conclusion
Offline reconciliation is where an estate decides whose knowledge it values. Per-field ownership resolves most of the volume without a person; treating telemetry as an observation rather than as office data resolves most of the rest; and holding genuine contradictions preserves the field evidence that a record-level rule would discard. What makes the whole arrangement work is that every decision is logged with both values, so a disagreement becomes something to investigate rather than something to argue about. The remaining source of field-versus-record disagreement is position, and that is governed by what the receiver was capable of — the subject of GNSS accuracy requirements.
Related
- Up to the parent topic: Field Data Capture & Mobile Sync
- Up to the section: Asset Lifecycle & Maintenance Automation
- Validating Field-Collected Assets Before Sync
- GNSS Accuracy Requirements for Utility Field Capture
- Resolving Branch Version Conflicts in Utility Networks
For authoritative reference, consult the Python datetime library and the pandas merge guide.