================================================================================
BrightMeasurementsController - Summary & Response Structures
File: SonWinCommonAPI/Controllers/BrightMeasurementsController.cs
================================================================================
--------------------------------------------------------------------------------
WHAT THE CONTROLLER DOES
--------------------------------------------------------------------------------
BrightMeasurementsController exposes a customer's energy consumption readings
for a single service (metering point) over a date range, aggregated to a chosen
time resolution, shaped into the "Bright" measurement contract.
Any thrown exception, goes through logs the error (internal trace file + SonWin BLOG)
and returns HTTP 500 with the exception message in the body.
The controller has FOUR endpoints (monthly, daily, hourly, 15-minute). They are
identical except for ONE thing: each one calls the SAME service method but passes a different.MeasurementsResolution value:
The resolution decides ONLY how the raw 15-minute readings are bucketed/summed
before being returned; every endpoint returns the SAME BrightMeasurementReading
shape.
IMPORTANT - HOW AGGREGATION WORKS:
The repository ALWAYS reads quarter-hour (15-minute) rows from the database. The service then:
Daily/Monthly bucket boundaries are computed in Europe/Copenhagen local time but the timestamps on each returned bucket are still emitted in UTC (the first reading of the bucket keeps its original UTC Date).
--------------------------------------------------------------------------------
METHODS
--------------------------------------------------------------------------------
All four methods share the same parameters, validation, error handling and
response type. They differ only in route and the resolution passed to the
service (see WHAT THE CONTROLLER DOES). Common signature:
Params:
All passed via [FromQuery].
Returns:
Methods:
1) GetMeasurements
Route: GET /bright/measurements
Resolution passed: MeasurementsResolution.Monthly -> Resolution = "month"
What it does:
Returns the customer's readings for the service, summed into local-month
buckets across [DateFrom, DateTo).
2) GetMeasurementDays
Route: GET /bright/measurementDays
Resolution passed: MeasurementsResolution.Daily -> Resolution = "day"
What it does:
Returns the readings summed into local-day buckets.
3) GetMeasurementHours
Route: GET /bright/measurementHours
Resolution passed: MeasurementsResolution.Hourly -> Resolution = "hour"
What it does:
Returns the readings summed into hour buckets.
4) GetMeasurements15min
Route: GET /bright/measurements15min
Resolution passed: MeasurementsResolution.Quarterly -> Resolution = "15min"
What it does:
Returns the raw 15-minute readings (gap-filled), no further aggregation.
Validation that affects the result (ALL surface as HTTP 500 with a message;
each message is wrapped as "The operation [GetMeasurements] could not be
completed due to the following validation errors: ..."):
(thrown by DateTimeHelper when building
the response StartDate/EndDate; model
binding normally yields Unspecified kind,
which is allowed.)
--------------------------------------------------------------------------------
RESPONSE STRUCTURE (BrightMeasurementReading)
--------------------------------------------------------------------------------
Returned as a single JSON OBJECT (BrightMeasurementReading), not an array.
{
"StartDate": <value> // Derived: the request DateFrom converted to a UTC DateTimeOffset
// (DateTimeHelper.DateTimeToDateTimeOffset_UTC). Echoes the input range start,
// NOT the first reading. Throws if DateFrom.Kind == Local.
"EndDate": <value> // Derived: the request DateTo converted to a UTC DateTimeOffset.
// Echoes the input range end, NOT the last reading. Throws if Kind == Local.
"Resolution": <value> // Derived: the resolution enum's DISPLAY name (GetDisplayName):
// "month" / "day" / "hour" / "15min" depending on the endpoint.
// (Uses the [Display(Name=...)] value, not the enum name.)
"ComparisonProperties": [] // Hardcoded: never populated by the mapper. Always an empty array.
// (BrightComparisonProperty.Key/Unit are unused today.)
"Values": [ // Data/Derived: one element per bucket at the chosen resolution, ordered by date.
// Gap-filled: every slot in the range is present (missing readings -> KWh 0).
{
"Date": <value> // Data: bucket timestamp (TimeSeriesValues.DATOTID), as a UTC DateTimeOffset.
// The DB stores UTC; Dapper's local offset is stripped and re-stamped to +00:00.
// For aggregated buckets this is the timestamp of the FIRST reading in the bucket.
"KWh": <value> // Data/Derived: kilowatt-hours (TimeSeriesValues.KWH). For Quarterly it is the raw
// 15-min value (0 if the slot was missing); for Hour/Day/Month it is the SUM of the
// 15-min KWh values in that bucket. Null KWh from the DB is treated as 0.
"M3": null // Hardcoded null: water volume. Commented out in the mapper ("r.M3") - not implemented.
"Kg": null // Hardcoded null: mass. Commented out in the mapper ("r.Kg") - not implemented.
"Cost": null // Hardcoded null: cost. Commented out in the mapper ("r.Cost") - not implemented.
"PeakWatt": <value|null> // Derived: an object is created ONLY if the DB row's PeakWattValue is not null;
// otherwise null. In practice the current SQL never selects PeakWattValue, so this
// is effectively ALWAYS null today.
// When present, the object's fields are:
{ // (only emitted when PeakWatt is non-null)
"Value": null // Hardcoded null: commented out in the mapper ("r.PeakWattValue") - not implemented.
"Cost": null // Hardcoded null: commented out in the mapper ("r.PeakWattCost") - not implemented.
"Peaks": [] // Hardcoded: never populated ("TODO: map Peaks once DB schema is known"). Empty array.
}
"Parts": [] // Hardcoded: never populated ("TODO: map Parts ... once DB schema is known"). Empty array.
"Compare": [] // Hardcoded: never populated ("TODO: map ... Compare once DB schema is known"). Empty array.
}
]
} |