Agriculture — precision farming
The problem. A farm is a patchwork of fields, each with soil sensors streaming moisture and temperature, and each periodically photographed from a drone or satellite to spot disease and growth variation. The agronomist needs the sensor trends and the imagery in one place, tied to the field they belong to, so a dry-soil reading and a stressed-crop image line up.
This scenario is the one where files matter as much as numbers: attach imagery and scouting reports to the field, alongside its sensor series.
Set up demo data
New workspace? Run this once (Python) to create the field's soil series with two days of readings that dry out toward the irrigation threshold. (The drone-scan upload in step 2 needs a local image file — any file works.) Safe to re-run.
import datahub_sdk, numpy as np, pandas as pd
client = datahub_sdk.DataHubClient.from_env()
for s, u in [("field_north_40_soil_moisture", "pct"), ("field_north_40_soil_temp_c", "deg_c")]:
client.timeseries.create([datahub_sdk.TimeSeries(external_id=s, name=s, unit=u)])
idx = pd.date_range(end=pd.Timestamp.now(tz="UTC"), periods=48, freq="1h")
moisture = 35 - np.linspace(0, 15, 48) + np.random.normal(0, 1, 48) # drying out
client.timeseries.insert_from_lists(timestamps=idx, values=moisture, ts="field_north_40_soil_moisture")
1. Model fields and stream soil sensors
A farm contains fields; each field's sensors are series.
- Java
- Python
- Rust
var moisture = Timeseries.of("field_north_40_soil_moisture").name("North 40 soil moisture");
moisture.setUnit("pct");
var soilTemp = Timeseries.of("field_north_40_soil_temp_c").name("North 40 soil temperature");
soilTemp.setUnit("deg_c");
client.timeseries().create(moisture, soilTemp);
client.timeseries().ingest(Map.of(
"field_north_40_soil_moisture", moistureReadings)); // List<Datapoint>
import datahub_sdk
client.timeseries.create([
datahub_sdk.TimeSeries(external_id="field_north_40_soil_moisture",
name="North 40 soil moisture", unit="pct"),
datahub_sdk.TimeSeries(external_id="field_north_40_soil_temp_c",
name="North 40 soil temperature", unit="deg_c")])
client.timeseries.insert_from_lists(
timestamps=timestamps, values=moisture, ts="field_north_40_soil_moisture")
use dataplatform_rust_sdk::timeseries::TimeSeries;
let mut moisture = TimeSeries::new("field_north_40_soil_moisture", "North 40 soil moisture");
moisture.unit = Some("pct".into());
api.time_series.create_one(&moisture).await?;
let mut soil_temp = TimeSeries::new("field_north_40_soil_temp_c", "North 40 soil temperature");
soil_temp.unit = Some("deg_c".into());
api.time_series.create_one(&soil_temp).await?;
api.time_series
.insert_datapoint(None, Some("field_north_40_soil_moisture".into()), ts, "21.5".into())
.await?;
2. Attach the drone imagery to the field
Upload each scan into the field's folder, tagged so it's discoverable next to the field's sensor data. See Attach files to assets for the upload details.
- Java
- Python
- Rust
byte[] scan = Files.readAllBytes(Path.of("north_40_2026_06_28.tif"));
client.files().upload(
FileUploadRequest.builder()
.path("fields/north_40/scans/2026_06_28.tif")
.content(scan)
.contentType("image/tiff")
.externalId("scan_north_40_2026_06_28")
.description("Drone NDVI scan — North 40")
.source("drone_survey")
.build());
client.files.upload_file(datahub_sdk.FileUpload(
path="north_40_2026_06_28.tif", # local file
destination_path="/fields/north_40/scans/",
external_id="scan_north_40_2026_06_28",
name="2026_06_28.tif"))
use dataplatform_rust_sdk::files::FileUpload;
let mut upload = FileUpload::new_with_destination_path(
"north_40_2026_06_28.tif", "/fields/north_40/scans/");
upload.external_id = "scan_north_40_2026_06_28".into();
upload.name = "2026_06_28.tif".into();
api.files.upload_file(upload).await?;
Later, list a field's folder to pull up its scan history alongside the soil trend:
- Java
- Python
- Rust
client.files().list("/fields/north_40/scans")
.getItems().forEach(node -> System.out.println(node.getName()));
for node in client.files.list_directory_by_path("/fields/north_40/scans"):
print(node.name)
let scans = api.files.list_directory_by_path("/fields/north_40/scans").await?;
for node in scans.get_items() { println!("{}", node.name); }
3. Trigger irrigation on a dry trend
When soil moisture stays below target through the heat of the day, raise an
irrigation_needed event the irrigation controller acts on.
See the result
Chart the soil moisture — the downward trend is what triggers irrigation:
import matplotlib.pyplot as plt
rf = datahub_sdk.RetrieveFilter(ts="field_north_40_soil_moisture",
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(days=2), end=pd.Timestamp.now(tz="UTC"))
s = pd.Series({pd.to_datetime(p.timestamp): float(p.value)
for p in client.timeseries.retrieve_datapoints(rf)[0].get_datapoints()}).sort_index()
s.plot(title="North 40 soil moisture (%)"); plt.show()
See also
- Attach files to assets — uploading and listing imagery.
- High-throughput ingestion — soil-sensor data.
- Turn readings into events — the irrigation rule.