A flood forecast is only as good as the observations fed into it. This chapter walks through the four observation objects of WIA-ENE-033 PHASE 1 — PrecipitationData, RiverLevelData, DamStatusData and DrainageData — field by field, explains the hydrology each field stands for, and points out where the published schema leaves room for misinterpretation that an implementer has to close explicitly.
Chapter 1 followed water from the cloud to the floodplain: rain falls, part of it runs off, the runoff is routed down the channel, the stage rises at a gauge, and eventually the water leaves the channel. Each link in that chain is observed by a different kind of instrument owned by a different kind of organisation. Rain gauges and weather radar belong to meteorological services; stage gauges belong to river authorities; reservoirs are operated by dam owners; pump stations and storm sewers are run by municipalities. WIA-ENE-033 gives each of these sources its own object so that the forecasting node described in Chapter 5 can combine them without first reverse-engineering every provider's private format. The prediction request and result objects that consume these observations are the subject of Chapter 3; here we stay with the observations themselves.
PHASE 1 (Data Format) states that its canonical structures are expressed in JSON Schema 2020-12 and remain stable for the lifetime of the phase. In the published material, however, the structures appear as TypeScript interfaces — both in the PHASE 1 document itself (§5.3 to §5.6) and in the reference SDK file api/typescript/src/types.ts. No machine-readable JSON Schema file is published alongside v1.0. In practice this means that an implementer who wants to validate payloads has to derive the schema from the interfaces. That is not difficult, but it must be done deliberately, because a TypeScript interface says nothing about value ranges, units or time zones; those live only in comments.
The phase lists four references that frame the model:
ENE-033 does not reproduce the full ISO 19156 model. Its objects are deliberately flattened "state envelopes": one JSON document carries the latest value, the thresholds, a short forecast and a derived status for a single station. This is convenient for dashboards and alert logic, but it means the concepts that ISO 19156 keeps separate — the feature (the river reach), the sampling point (the gauge), the observed property (stage), the procedure (the sensor), and the result time — are merged into one record. Section 2.7 shows how to pull them apart again when data has to be exchanged as WaterML 2.0.
Every quantitative field in the PHASE 1 interfaces carries a unit in its comment: metres for water level, cubic metres per second for flow, millimetres for rainfall depth, millimetres per hour for rainfall intensity, millions of cubic metres for reservoir storage, percent for ratios. The units are therefore fixed by the standard and are not transmitted in the payload. The consequence is simple and strict: a producer must convert to the ENE-033 unit before emitting a record. There is no unit attribute to rescue a feed that reports discharge in cubic metres per hour or storage in thousands of cubic metres.
Because the unit is part of the contract, a derived JSON Schema should record it, for example with a description or a custom annotation keyword. The fragment below is an example of how an implementer might express the current block of RiverLevelData; it is not a file published by the standard.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.org/schemas/ene033/river-current.json",
"title": "RiverLevelData.current (derived example)",
"type": "object",
"required": ["waterLevel", "flowRate", "velocity", "timestamp"],
"properties": {
"waterLevel": { "type": "number", "description": "Stage in metres, same datum as levels.*" },
"flowRate": { "type": "number", "minimum": 0, "description": "Discharge in m3/s" },
"velocity": { "type": "number", "description": "Mean velocity in m/s" },
"timestamp": { "type": "string", "format": "date-time" }
},
"additionalProperties": false
}
Two choices in this fragment deserve comment. First, minimum: 0 on discharge assumes a non-tidal reach; near an estuary the flow can reverse on the flood tide, and a producer there must drop the constraint and say in its profile that negative values mean upstream flow. Second, additionalProperties: false is a local choice: Annex E of PHASE 1 treats new optional fields as non-breaking, so a strict consumer that rejects unknown fields will break on the first additive revision. Consumers should usually allow unknown properties and ignore them.
The four objects map one-to-one onto the four families of instruments. A basin forecasting node therefore receives many RiverLevelData records (one per gauge), several DamStatusData records (one per reservoir), a set of DrainageData records (one per facility) and precipitation data covering the forecast area. When these are combined into a prediction, Chapter 3 shows that FloodPredictionResult.contributingFactors holds them as arrays for rivers and dams but as a single object for precipitation and drainage — a detail worth keeping in mind when designing storage.
Rainfall is the driving input of almost every flood model. PrecipitationData has four parts: what has been measured (observed), what is expected (forecast), an optional pointer to weather-radar imagery (radar), and a statistical characterisation of the event (statistics).
| Field | Type | Unit | Meaning |
|---|---|---|---|
observed.current | number | mm/h | Current rainfall intensity |
observed.last1h … last24h | number | mm | Accumulated depth over the past 1, 3, 6 and 24 hours |
forecast.next1h … next24h | number | not stated (read as mm) | Expected rainfall over the next 1, 3, 6, 12 and 24 hours |
forecast.hourly | number[] | mm/h | Hour-by-hour forecast intensity |
radar.imageUrl | string | — | Link to a radar image product |
radar.coverage | number | km | Coverage extent of the radar product |
radar.lastUpdate | Timestamp | — | Time of the radar product |
statistics.returnPeriod | number | years | Return period of the observed rainfall |
statistics.percentile | number | % | Percentile of the observed value in the climatology |
statistics.isExtreme | boolean | — | Flag set by the producer for an extreme event |
The single most common error with rainfall data is mixing up intensity and accumulation. Intensity is a rate (millimetres per hour); accumulation is a depth (millimetres). A millimetre of rain is one litre per square metre, so both numbers can be turned into volumes, but they answer different questions. Intensity tells a drainage engineer whether a storm sewer designed for a given rate will surcharge in the next few minutes. Accumulation tells a river forecaster how much water the catchment has received and, together with soil moisture, how much of it will run off.
The two are related but not interchangeable. A storm that rains at a steady 30 mm/h for three hours delivers 90 mm, so last3h would read 90 while current reads 30. A convective cell that produces 60 mm/h for ten minutes and then stops leaves current near zero and last1h at about 10 mm; a system that reads the intensity field alone would conclude that nothing is happening at the very moment the sewer network is still draining the burst. Implementations should therefore validate the two fields for plausibility against each other: no accumulation can be negative, and each longer window must be at least as large as the shorter one it contains (last1h <= last3h <= last6h <= last24h).
Read against the observation model of Section 2.1, PrecipitationData is missing three things that every other observation object has in some form:
monitorRainfall(regionId) method is typed to return any, which confirms that the monitoring shape is not settled. Producers that publish rainfall per gauge or per radar pixel should wrap each record with at least an identifier, a Location and a timestamp, and document the wrapper in their interoperability profile.last1h to last24h end at some instant that the object does not state. Without it, a consumer cannot tell a fresh record from one that has been sitting in a queue for an hour.hourly; the next1h … next24h fields have no unit comment. Read by analogy with last*, they are accumulations in millimetres, and this book uses that reading. The first element of hourly is likewise not defined; the natural reading is "the hour starting at the observation time", and a producer should state it.Chapter 1 introduced the return period as the reciprocal of the annual exceedance probability. In statistics.returnPeriod the producer expresses how rare the observed rainfall is. One important piece of context is not in the schema: which duration the return period refers to. A storm can be a 100-year event for its one-hour maximum and an unremarkable event for its 24-hour total, or the reverse. Producers should compute the return period for the duration most relevant to the catchment (short durations for urban drainage, longer ones for large rivers) and say which one they used. percentile has the same ambiguity and additionally depends on the reference period of the climatology. isExtreme is a producer-side judgement; consumers should not assume that two producers use the same threshold for it.
A river gauge measures stage, the height of the water surface, directly. Discharge — the volume of water passing per second — is usually not measured continuously; it is computed from stage with a rating curve, a site-specific relationship calibrated from occasional flow measurements. RiverLevelData.current carries both (waterLevel in metres and flowRate in m³/s) plus a mean velocity in m/s. The schema does not say whether discharge was measured or derived, and rating curves are least reliable exactly during floods, when the channel may overtop its banks or its bed may move. A consumer should treat flowRate as an estimate unless the producer's profile says otherwise.
| Field | Type | Unit | Meaning |
|---|---|---|---|
riverId, name | string | — | River identifier (e.g. HAN-001) and display name |
stationId | string | — | Gauge identifier |
location | Coordinates | degrees | Position of the gauge |
current.waterLevel | number | m | Current stage |
current.flowRate | number | m³/s | Current discharge |
current.velocity | number | m/s | Mean flow velocity |
current.timestamp | Timestamp | — | Time of the current reading |
levels.normal / attention / warning / danger | number | m | Threshold ladder for this gauge |
levels.bankHeight | number | m | Height of the bank or levee crest |
forecast.peak, peakTime | number, Timestamp | m | Expected maximum stage and its time |
forecast.hourly | number[] | m | Hour-by-hour forecast stage |
trend.rateOfRise | number | m/h | Current rate of change of stage (negative when falling) |
trend.direction | enum | — | rising | stable | falling |
trend.acceleration | number | m/h² | Change of the rate of rise |
status.condition | enum | — | normal | attention | warning | danger |
status.overflowRisk | number | % | Producer's probability of overflow, 0–100 |
status.estimatedTimeToOverflow | number (optional) | h | Hours until the overflow condition is reached |
The levels block is the part of the record that turns a number into a decision. It holds four ascending thresholds and the physical crest: normal, attention, warning, danger, bank height. Each gauge has its own ladder; a stage of 5 m can be harmless at one site and catastrophic at another. For this reason the ladder travels with the record rather than being looked up elsewhere — a consumer that receives a single RiverLevelData can determine the gauge's condition without a second query.
Three rules follow from this design. The thresholds must be strictly ascending (normal < attention < warning < danger <= bankHeight); a record that violates this is corrupt. The thresholds and the current stage must share the same vertical datum (Section 2.7.1). And status.condition should be the highest threshold that the current stage has reached, so that consumers can recompute and check it. Note that the condition enumeration stops at danger: there is no separate value for "above bank height". Overtopping must be detected by comparing current.waterLevel with levels.bankHeight directly.
These gauge-level names are not the same scale as the five-step risk levels LEVEL-0 to LEVEL-4 used by prediction results, and neither is a synonym of the official flood forecast terms issued by Korea's flood control offices. How the scales relate is a mapping decision covered in Chapter 3; within the gauge record, the four names only mean "which threshold of this gauge has been crossed".
The trend block summarises how fast the situation is changing. rateOfRise is the first derivative of stage in metres per hour, acceleration the second derivative in m/h², and direction a categorical reading of the sign. A rate of rise of 0.3 m/h at a gauge that is 0.6 m below its danger threshold is a very different situation from the same stage with a falling trend, and many operational warning rules are expressed in terms of rate of rise as much as absolute level.
status.estimatedTimeToOverflow is optional and, in the schema, undefined in one important respect: it does not say which level counts as "overflow". The reference SDK's helper estimateTimeToOverflow(currentLevel, dangerLevel, rateOfRise) divides the distance to the danger threshold by the current rate of rise and returns nothing when the river is not rising or is already above danger. A producer that uses the bank height instead, or that reads the crossing time from its hydraulic forecast, will publish a different number for the same situation. Chapter 5 discusses the calculation; here the requirement is only that each producer state its definition in its profile, and that consumers not compare this field across producers without checking.
The following record is an illustrative example; the station, values and time are invented to be internally consistent and do not describe a real gauge.
{
"riverId": "HAN-001",
"name": "Example River (illustrative)",
"stationId": "HAN-001-ST03",
"location": { "latitude": 37.5170, "longitude": 126.9580 },
"current": {
"waterLevel": 4.10,
"flowRate": 2850,
"velocity": 2.1,
"timestamp": "2026-07-15T09:00:00Z"
},
"levels": {
"normal": 2.00,
"attention": 3.00,
"warning": 4.00,
"danger": 5.50,
"bankHeight": 6.80
},
"forecast": {
"peak": 5.80,
"peakTime": "2026-07-15T17:00:00Z",
"hourly": [4.36, 4.63, 4.91, 5.20, 5.46, 5.66, 5.78, 5.80, 5.72, 5.58]
},
"trend": {
"rateOfRise": 0.25,
"direction": "rising",
"acceleration": 0.01
},
"status": {
"condition": "warning",
"overflowRisk": 70,
"estimatedTimeToOverflow": 5.6
}
}
Checking the record against the rules above shows how the fields hang together. The current stage of 4.10 m is above the warning threshold (4.00 m) and below danger (5.50 m), so condition is warning. The forecast peak of 5.80 m is the maximum of the hourly array, and it sits in the eighth element; if the first element is the hour ending at 10:00Z, the eighth ends at 17:00Z, matching peakTime. The time to overflow of 5.6 hours is the SDK's linear estimate, (5.50 − 4.10) / 0.25. The hourly forecast, which includes the slight positive acceleration, crosses 5.50 m between its fifth and sixth elements, slightly earlier. Both numbers are defensible; the record is only unambiguous if the producer's profile says which method estimatedTimeToOverflow uses. The peak stays a full metre below the bank height, so the forecast describes a danger-level flood that is not expected to overtop. overflowRisk is the producer's own probability and cannot be checked from the other fields.
A reservoir is both a flood defence and a flood source. Before and during a storm it can store part of the inflow and reduce the peak downstream. When storage approaches its limit, the operator has to release water through the spillway, and that release travels down the river and adds to whatever the downstream tributaries are already delivering. For a gauge below a large dam, the planned release is often the single most important piece of information for the next several hours. That is why DamStatusData carries not only the current state but also a dischargePlan.
| Field | Type | Unit | Meaning |
|---|---|---|---|
damId, name | string | — | Dam identifier and display name |
type | enum | — | multi-purpose | flood-control | hydropower |
waterLevel.current | number | m | Current reservoir level |
waterLevel.normal | number | m | Normal high water level |
waterLevel.flood | number | m | Flood-season restricted level |
waterLevel.design | number | m | Design flood level |
storage.current / total / effective | number | million m³ | Current, total and effective storage |
storage.percentage | number | % | Fill ratio |
flow.inflow | number | m³/s | Inflow to the reservoir |
flow.outflow | number | m³/s | Total release |
flow.discharge | number | m³/s | Spillway release |
flow.powerGeneration | number | m³/s | Release through the turbines |
dischargePlan.scheduled | boolean | — | Whether a release is planned |
dischargePlan.startTime, duration, maxDischarge | Timestamp, number, number | —, h, m³/s | Timing and size of the planned release |
dischargePlan.affectedArea | string[] | — | Areas downstream affected by the release |
operational.gateStatus | enum | — | open | closed | partial |
operational.gateOpening | number | % | Gate opening ratio |
operational.emergencyMode | boolean | — | Whether emergency operation is in effect |
The three reference levels describe the operating envelope of the reservoir. The normal high water level is the upper limit of routine storage. The flood-season restricted level is kept lower during the flood season so that the space between it and the higher levels is available to absorb a flood. The design flood level is the level the structure is designed to reach during its design flood. How close waterLevel.current is to flood during the flood season is a direct measure of the remaining buffer.
Storage and flow are linked by a simple balance: over an interval, the change in storage equals inflow minus outflow multiplied by the interval length (ignoring evaporation and seepage, which are small at flood time scales). A net inflow of 1,000 m³/s sustained for one hour adds 3.6 million m³, or 3.6 in ENE-033's storage unit. Consumers can use this to sanity-check a sequence of records: if storage rises by 20 million m³ in an hour while inflow and outflow are nearly equal, one of the fields is wrong or has the wrong unit. The million-cubic-metre unit is a frequent source of errors because some operators publish storage in thousands of cubic metres or as a volume of hundreds of millions; the conversion must be done before the record is emitted.
Two aspects of the flow and storage blocks are not defined by the schema and should be stated in a producer profile. The relation between the flow fields is implied but not written down: a natural reading is that outflow is the total release, including discharge through the spillway, powerGeneration through the turbines and any other outlets, so that outflow is at least the sum of the other two. The denominator of storage.percentage may be total or effective storage. A consumer should check such relations as warnings rather than rejecting the record.
The dischargePlan is the forward-looking part of the dam record. When scheduled is true, the optional fields describe when the release starts, how long it lasts and its maximum rate. For a downstream forecasting node, these values are boundary conditions: the release is routed down the channel and arrives at each downstream gauge after a travel time that depends on the distance and the flow. affectedArea is a free list of strings; to make it machine-usable, producers should fill it with the same region identifiers used in Location.regionId (Section 2.6) rather than place names.
The operational block reports the gate state. gateOpening is a percentage, but the schema describes a single gate state for the whole dam, whereas many dams have several spillway gates operated independently. Where this matters, the percentage should be defined in the profile, for example as the fraction of total gate capacity currently open. emergencyMode is a flag that downstream consumers should surface prominently, because emergency operation generally means that normal release rules have been suspended.
In cities, many floods do not come from the river overtopping its banks at all. When a heavy downpour exceeds the capacity of the storm sewers, or when a high river stage forces the gates on outfalls to close so that the river cannot back up into the city, rain that falls inside the protected area has nowhere to go. It must be pumped out or held in detention basins until the river falls. This is inland flooding (the urban flood type of Chapter 1), and DrainageData is the object that describes the facilities that fight it. The type enumeration covers three kinds of facility: pump-station, sewer and detention-basin.
| Field | Type | Unit | Meaning |
|---|---|---|---|
facilityId, name, type | string, string, enum | — | Identity and kind of facility |
capacity.design | number | m³/s | Design drainage capacity |
capacity.current | number | m³/s | Capacity available now |
capacity.utilization | number | % | Share of capacity in use |
pumps.total / active / standby / failed | number | count | Pump counts (pump stations only) |
pumps.efficiency | number | % | Pump efficiency |
waterLevel.current / capacity / available | number | m | Basin level, maximum level and remaining margin (detention basins) |
operational.status | enum | — | active | standby | maintenance | failure |
operational.lastMaintenance, nextMaintenance | Timestamp | — | Maintenance schedule |
operational.alerts | string[] | — | Active facility alarms |
performance.flowRate | number | m³/s | Actual drained flow |
performance.energyUsage | number | kWh | Energy used |
performance.reliability | number | % | Availability of the facility |
The capacity block distinguishes what a facility was designed to do from what it can do now. A pump station designed for a certain flow may have one pump under repair and another on standby, so its current capacity is lower. That difference — not the design figure — is what matters for a flood forecast, because the inundation model needs to know how much water the protected area can actually evacuate. The pumps counts support the same reasoning: in a consistent record, active + standby + failed equals total, and a rising failed count is an early sign that current capacity will fall further.
Two quantities are easy to confuse. capacity.utilization is the share of capacity in use; performance.flowRate is the flow actually drained. The schema does not say whether utilization is measured against the design or the current capacity. A facility running at its full current capacity with half its pumps failed shows 100 % against current capacity but only about 50 % against design; the two readings lead to opposite conclusions about spare capacity. Producers should state which denominator they use.
For detention basins, the optional waterLevel block expresses storage as heights: current level, maximum level and remaining margin, where the margin is naturally capacity − current. This is not the same as the volume still available, which depends on the basin's shape; if an inundation model needs volume, it must convert with the basin's level–storage relationship. energyUsage in kilowatt-hours has no stated accumulation period; operators typically report it per day or since the start of an event, and the profile should say which.
The operational.status enumeration has four values in both PHASE 1 and the SDK: active, standby, maintenance and failure. The short data-source summary in the README uses a three-value pumpStatus without maintenance and expresses load as a percentage; that summary predates the PHASE 1 structure and should not be used as a schema. A facility in maintenance during the flood season is operationally equivalent to reduced capacity, and consumers should treat it that way rather than as a neutral state.
All four observation objects locate themselves with Coordinates, an object with named latitude and longitude fields in decimal degrees. The standard does not name a coordinate reference system; since GPS positions and web maps normally use WGS 84, and GeoJSON (RFC 7946) requires it, producers should use WGS 84 and convert any national grid coordinates before publishing. Prediction requests and results use the richer Location type, which adds regionId, name, an optional address and an optional boundary geometry.
A frequent bug arises at the border between the two representations. Coordinates names its fields, so their order does not matter. GeoJSON positions are bare arrays in the order longitude, latitude. Code that copies {latitude, longitude} into a GeoJSON array in the order it reads them produces a position whose "latitude" is 126.9° for a gauge near Seoul. That particular mistake is caught by a range check, because latitudes cannot exceed 90°, but for sites where both values are below 90 — much of Europe, Africa and South America — a swapped point is perfectly legal and simply lands in the wrong place. A unit test with a known station is the only reliable guard. The SDK's GeoJSON type is also simplified: it is a geometry with an optional properties member, whereas in RFC 7946 properties belong to a Feature that wraps a geometry. Producers that want standard tools to read their boundaries should emit proper Feature or FeatureCollection objects.
The Timestamp type is defined only as an ISO 8601 string. ISO 8601 permits local times without an offset, which are ambiguous as soon as data crosses a time zone or a daylight-saving boundary. The safe convention, used in all examples in this book, is to emit UTC with the Z designator (for example 2026-07-15T09:00:00Z) and to convert to local time only for display. Consumers should reject timestamps without an offset rather than guess.
The examples in the README and the CLI show a pattern for identifiers: region IDs such as REG-SEOUL-001 and river IDs such as HAN-001. Station, dam and facility identifiers have no published pattern. The standard does not operate a registry for any of them, so two agencies can independently assign the same string. For data that crosses organisational boundaries, identifiers should be made globally unique, for example by prefixing them with the issuing organisation's namespace, and they must be stable: a gauge that is relocated to a new cross-section should receive a new stationId, because its thresholds and rating curve change.
| Identifier | Used in | Example pattern | Recommendation |
|---|---|---|---|
regionId | Location, alert and dashboard objects | REG-SEOUL-001 | Reuse in dischargePlan.affectedArea |
riverId | RiverLevelData | HAN-001 | One per river or reach, shared by its gauges |
stationId | RiverLevelData | not published | Namespace-prefixed, new ID on relocation |
damId | DamStatusData | not published | Namespace-prefixed |
facilityId | DrainageData | not published | Namespace-prefixed, one per facility |
A water level is a height above something. Stage gauges often report height above the gauge's own zero, while dam levels and embankment crests are usually expressed as elevations above a national height datum related to mean sea level. The difference between the two can be several metres or more. ENE-033 does not carry a datum field, so the only protection is internal consistency: within one record, current.waterLevel, all levels thresholds and forecast values must use the same datum. When records from several gauges are combined — for example, to draw a longitudinal water-surface profile or to feed a hydraulic model — they must be converted to a common elevation datum first. Producers should publish the datum and the gauge-zero elevation of each station in their profile.
Flood warnings are time-critical, and a forecast that combines observations from dozens of sources is only as coherent as their clocks. Annex E of PHASE 1 notes that NTPv4 against stratum-2 servers is sufficient for most deadlines expressed in the phase and recommends PTP where deterministic interlocks are required. For observation data this has a practical corollary: loggers in remote gauge huts should be synchronised and their clock status monitored, because a logger that drifts by several minutes silently shifts the computed rate of rise and the apparent arrival time of a flood wave.
Every numeric field in the four objects is required in the interfaces except the optional blocks. A producer whose rain gauge is offline is therefore tempted to emit 0. For rainfall this is dangerous: a zero reads as "dry" and lowers the forecast at the very moment an outage may be caused by the storm. The rule should be that an unmeasured value is never replaced with a number. If the whole record is unavailable, it should not be sent, and the monitoring dashboard's sensor status (Chapter 4) should count the station as offline. If only some fields are unavailable, the producer can add an optional quality block as an extension; Annex E treats additive optional fields as non-breaking. Such an extension is a producer's choice and must be declared in the interoperability profile, because ENE-033 v1.0 defines no quality flags.
Unit errors rarely appear on day one. They appear when a sensor is replaced, a logger is reconfigured or a data provider changes a feed. The best defence is a set of plausibility checks at ingest that encode the relationships described in this chapter. The function below is an illustrative example in TypeScript; it uses only fields defined in PHASE 1 and returns warnings rather than throwing, so that a questionable record can be quarantined instead of lost.
import { RiverLevelData, TrendDirection } from '@wia/ene-033';
// Illustrative ingest checks for a single gauge record.
export function checkRiverRecord(r: RiverLevelData): string[] {
const w: string[] = [];
const L = r.levels;
if (!(L.normal < L.attention && L.attention < L.warning &&
L.warning < L.danger && L.danger <= L.bankHeight)) {
w.push('threshold ladder is not ascending');
}
if (r.current.flowRate < 0) {
w.push('negative discharge');
}
if (!/(Z|[+-]\d\d:\d\d)$/.test(r.current.timestamp)) {
w.push('timestamp has no UTC offset');
}
const h = r.current.waterLevel;
const expected =
h >= L.danger ? 'danger' :
h >= L.warning ? 'warning' :
h >= L.attention ? 'attention' : 'normal';
if (r.status.condition !== expected) {
w.push(`condition ${r.status.condition} but stage implies ${expected}`);
}
if (h >= L.bankHeight) {
w.push('stage at or above bank height (no enum value for this)');
}
const dir = r.trend.rateOfRise > 0 ? TrendDirection.RISING
: r.trend.rateOfRise < 0 ? TrendDirection.FALLING
: TrendDirection.STABLE;
if (Math.abs(r.trend.rateOfRise) > 0.01 && r.trend.direction !== dir) {
w.push('trend direction contradicts rateOfRise');
}
if (r.forecast.hourly.length > 0 &&
Math.max(...r.forecast.hourly) > r.forecast.peak + 1e-6) {
w.push('hourly forecast exceeds stated peak');
}
return w;
}
Running this function against the example in Section 2.3.5 returns an empty list. The small dead band on the direction check (0.01 m/h) is itself an implementation choice: a gauge whose stage is effectively flat will report a tiny non-zero rate of rise from sensor noise, and stable is the honest description.
The PHASE 1 document writes enumerations as inline string literals (for example 'rising' | 'stable' | 'falling'); the SDK declares TypeScript enums — RiverLevelStatus, TrendDirection, DamType, GateStatus, DrainageFacilityType and OperationalStatus. The string values of these enums are identical to the literals in the specification, so the two are wire-compatible: a JSON document produced from either passes the other. The field names and nesting of the four observation objects are also identical in both sources. The observable differences are elsewhere: the SDK types the rainfall monitoring response as any, and the README's summary DataSources block uses different names (for example warningLevel and dangerLevel) and should be read as an informal overview, not as the data model.
National hydrological services and international data-exchange programmes commonly publish time series in OGC WaterML 2.0. An ENE-033 producer that also has to supply WaterML, or a consumer that ingests WaterML and emits ENE-033, needs a mapping between the flattened envelope and the observation model. The table below gives the correspondences for the gauge record; the same approach applies to rainfall and reservoir data.
| WaterML 2.0 / observation concept | ENE-033 source | Notes |
|---|---|---|
| Monitoring point (sampling feature) | stationId, name, location | One monitoring point per gauge |
| Sampled feature (the water body) | riverId | The river or reach the gauge samples |
| Observed property | implicit in the field name | Separate series for stage (waterLevel), discharge (flowRate) and velocity |
| Observation with a single time–value result | current.* + current.timestamp | Phenomenon time = current.timestamp |
| Measurement time series (forecast) | forecast.hourly | Needs an explicit start time and one-hour step, which ENE-033 leaves implicit; result time = forecast issue time |
| Unit of measure | fixed by ENE-033 | Emit explicitly, e.g. m, m3/s, mm |
| Interpolation type | not carried | Stage is instantaneous; rainfall accumulations are totals over the preceding interval |
| Quality / qualifier | not carried | Use the producer's quality extension, if any |
| Threshold levels | levels.* | No dedicated element in the core time-series model; publish as monitoring-point metadata or a companion document |
| Derived status | trend.*, status.* | Not observations; do not export as measured series |
The last row reflects the underlying design difference. WaterML carries what was measured or forecast; ENE-033's gauge record also carries interpretation — condition, overflow risk, time to overflow — computed by the producer. When converting in the other direction, from WaterML to ENE-033, those derived fields must be computed by the converter, and the converter becomes responsible for their definitions. The mapping also makes clear which information must be supplied from outside the ENE-033 record to produce valid WaterML: the forecast series' start time, the interpolation type and the datum. Those are exactly the gaps this chapter has recommended closing in the producer's interoperability profile.
With the observation objects in place, the next chapter turns to the objects that consume them: the prediction request, the prediction result with its risk level and inundation estimate, and the alert message that carries a decision to the public.