Synthetic sensor time series: 72 hours across two fields

144 reproducible hourly CSV/JSON records with explicit missing/suspect flags. Includes standard-library checks for full-record equivalence, UTC intervals and exact quality counts.

License: cc-by-4.0

synthetic, sensor, timeseries, csv, quality-flags

Overview

# Synthetic sensor time series: 72 hours across two fields

## Goal, files and requirements

Test a parser, chart and export pipeline without farm data. Download `sensor-observations.csv`, `sensor-observations.json`, `data-dictionary-v2.json` and `license.txt` to one folder. Run the following with Python 3's standard library from that folder; no package installation or credentials are needed. There are 144 rows excluding the CSV header, two fictional fields/devices and 72 hourly timestamps per field starting at the arbitrary fixture time 2026-01-01T00:00:00Z.

## Meaning and procedure

`record_id` is the primary key; `field_id` joins companion fixtures. `soil_vwc_pct` is synthetic volumetric water content in percent; `air_temp_c` is °C. Empty CSV VWC equals JSON null, never zero. The dictionary gives the complete formula: field offset A=0/B=2; `VWC=round(26+offset+3*sin(h*pi/12),2)`, `temperature=round(18+offset+5*sin((h-6)*pi/12),2)`. Hours 17/49 are missing; hour 35 is intentionally suspect at 99 for each field. These are test cases, not irrigation thresholds.
Save the code as `check_sensor.py`, run `python3 check_sensor.py`, then chart the same normalized values with missing gaps and visible suspect markers.

```python
import csv, json
from collections import Counter
from datetime import datetime
from pathlib import Path

root = Path(".")
def boolean(value):
    assert value in ("true", "false"), value
    return value == "true"

with (root / "sensor-observations.csv").open(newline="", encoding="utf-8") as f:
    raw = list(csv.DictReader(f))
rows = [{**r, "soil_vwc_pct": None if r["soil_vwc_pct"] == "" else float(r["soil_vwc_pct"]),
         "air_temp_c": float(r["air_temp_c"]), "is_synthetic": boolean(r["is_synthetic"])} for r in raw]
other = json.loads((root / "sensor-observations.json").read_text(encoding="utf-8"))
left = {r["record_id"]: r for r in rows}
right = {r["record_id"]: r for r in other}
assert len(rows) == len(left) == len(other) == len(right) == 144
assert left == right
quality = Counter(r["quality_flag"] for r in rows)
assert quality == {"ok": 138, "missing": 4, "suspect": 2}
assert all(r["is_synthetic"] is True for r in rows)
assert all((r["soil_vwc_pct"] is None) == (r["quality_flag"] == "missing") for r in rows)
assert all(r["soil_vwc_pct"] == 99 for r in rows if r["quality_flag"] == "suspect")
for field in ("sandbox-field-a", "sandbox-field-b"):
    times = sorted(datetime.fromisoformat(r["timestamp_utc"].replace("Z", "+00:00"))
                   for r in rows if r["field_id"] == field)
    assert len(times) == 72
    assert all((b - a).total_seconds() == 3600 for a, b in zip(times, times[1:]))
assert json.loads((root / "data-dictionary-v2.json").read_text())["row_count"] == 144
print("rows=144; ok=138; missing=4; suspect=2; hours_per_field=72")
```

## Expected output and worked example

`rows=144; ok=138; missing=4; suspect=2; hours_per_field=72`
Any assertion failure means the downloaded/parser result differs from this fixture; investigate before using the output. `sensor-a-017` must remain null, while `sensor-a-035` retains value 99 and the suspect flag. Sorting or exporting must preserve IDs and flags, not silently delete records.

## Next action, privacy, provenance and license

Use [sandbox boundaries](/hub/agridigi/sandbox-field-boundaries) for the executable field-ID join and [irrigation monitoring](/hub/agridigi/irrigation-monitoring-7-day) for a separately approved real workflow. AgriDigi generated these deterministic values; `data-dictionary-v2.json` states the rules. No personal records, real device IDs or real locations are included. Original fixture/text is [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/): credit AgriDigi and link source/license; mark changes. This toy signal validates interoperability only, not device calibration, physical control, predictive accuracy or water savings.

Dataset

Rows: 144

Columns: record_id, field_id, sensor_id, timestamp_utc, soil_vwc_pct, air_temp_c, quality_flag, is_synthetic

  • license.txt · text/plain · 558 bytes
  • sensor-observations.csv · text/csv · 13396 bytes
  • sensor-observations.json · application/json · 38377 bytes
  • data-dictionary-v2.json · application/json · 9651 bytes
  • readme-v3-en.txt · text/plain · 3908 bytes
  • readme-v3-tr.txt · text/plain · 3981 bytes
  • readme-v3-de.txt · text/plain · 3668 bytes
  • readme-v2-es.txt · text/plain · 3921 bytes
  • readme-v2-fr.txt · text/plain · 3889 bytes
  • readme-v2-it.txt · text/plain · 3712 bytes
  • readme-v3-az.txt · text/plain · 3618 bytes
  • readme-v3-ru.txt · text/plain · 4314 bytes
  • readme-v2-sr.txt · text/plain · 3615 bytes
  • readme-v2-ar.txt · text/plain · 4438 bytes
  • validate-v2.txt · text/plain · 1579 bytes
  • quality-summary-v2.csv · text/csv · 116 bytes
  • expected-checks-v2.json · application/json · 194 bytes

View publisher @agridigi

Explore AgriDigi Hub