redispatch_xml/documents/unavailability.rs
1//! `Unavailability_MarketDocument` — planned and forced unavailability declarations for generation resources.
2use serde::{Deserialize, Serialize};
3
4use crate::documents::activation::EicCodingScheme;
5use crate::documents::kaskade::ParticipantMrid;
6use crate::types::{Decimal3, Mrid, RevisionNumber, SimpleContent, UtcDateTime, UtcMinuteDateTime};
7
8// ── Namespace ─────────────────────────────────────────────────────────────────
9
10/// Expected XML namespace for `Unavailability_MarketDocument`.
11pub const NAMESPACE: &str = "urn:iec62325.351:tc57wg16:451-6:outagedocument:3:0";
12
13// ── Enumerations ──────────────────────────────────────────────────────────────
14
15/// Document type codes for `Unavailability_MarketDocument`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum UnavailabilityDocType {
18 /// Planned unavailability.
19 #[serde(rename = "A67")]
20 PlannedUnavailability,
21 /// Forced (unplanned) unavailability.
22 #[serde(rename = "A76")]
23 ForcedUnavailability,
24 /// Production unavailability.
25 #[serde(rename = "A80")]
26 ProductionUnavailability,
27}
28
29/// Process type for `Unavailability_MarketDocument`.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub enum UnavailabilityProcessType {
32 /// Day-ahead / intraday forecast.
33 #[serde(rename = "A14")]
34 Forecast,
35 /// Outage information.
36 #[serde(rename = "A26")]
37 OutageInfo,
38}
39
40/// Business type for `Unavailability_MarketDocument` time series.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum UnavailabilityBusinessType {
43 /// Production.
44 #[serde(rename = "A01")]
45 Production,
46 /// Planned maintenance.
47 #[serde(rename = "A53")]
48 PlannedMaintenance,
49 /// Unplanned outage.
50 #[serde(rename = "A54")]
51 UnplannedOutage,
52}
53
54/// Sender role for `Unavailability_MarketDocument`.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub enum UnavailabilitySenderRole {
57 /// Resource provider.
58 #[serde(rename = "A27")]
59 ResourceProvider,
60 /// Data provider.
61 #[serde(rename = "A39")]
62 DataProvider,
63}
64
65/// Receiver role for `Unavailability_MarketDocument`.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub enum UnavailabilityReceiverRole {
68 /// Grid operator.
69 #[serde(rename = "A18")]
70 GridOperator,
71 /// Data provider.
72 #[serde(rename = "A39")]
73 DataProvider,
74}
75
76// ── Market participant helpers ────────────────────────────────────────────────
77
78/// Market role type for `Unavailability_MarketDocument`.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
80pub enum UnavailabilityMarketRoleType {
81 /// Grid operator.
82 #[serde(rename = "A18")]
83 GridOperator,
84 /// Resource provider.
85 #[serde(rename = "A27")]
86 ResourceProvider,
87 /// Data provider.
88 #[serde(rename = "A39")]
89 DataProvider,
90}
91
92// The sender and receiver are **flat, dotted** elements on the wire —
93// `<sender_MarketParticipant.mRID>` and
94// `<sender_MarketParticipant.marketRole.type>` — not a nested
95// `<sender_MarketParticipant>` container. That is the ENTSO-E CIM convention
96// the BDEW XSD follows, and the difference is not cosmetic: a nested document
97// fails XSD validation at the counterparty, and an inbound flat one loses the
98// sender entirely, because `serde` skips elements the model does not declare.
99// `original_sender_MarketParticipant.mRID` in the TimeSeries already had the
100// right shape, which is what made the mismatch easy to miss.
101
102/// One quarter-hour point of an unavailability curve.
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct UnavailabilityPoint {
105 /// 1-based position within the `Available_Period`.
106 pub position: u32,
107 /// Available capacity in that interval (MW).
108 pub quantity: Decimal3,
109}
110
111/// The asset this unavailability applies to.
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct AssetRegisteredResource {
114 /// Asset identifier.
115 #[serde(rename = "mRID")]
116 pub m_rid: ParticipantMrid,
117}
118
119// ── UnavailabilityTimeInterval ────────────────────────────────────────────────
120
121/// A UTC time interval expressed as separate `start` and `end` sub-elements
122/// (minute precision).
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub struct UnavailabilityTimeInterval {
125 /// Start of the unavailability period (UTC, minute precision).
126 pub start: UtcMinuteDateTime,
127 /// End of the unavailability period (UTC, minute precision).
128 pub end: UtcMinuteDateTime,
129}
130
131/// The interval-resolved availability curve.
132///
133/// This is the document's actual payload: `start_DateAndOrTime` and
134/// `end_DateAndOrTime` say *when* the resource is affected, and these points
135/// say *how much* capacity remains in each interval. A model without them
136/// reduces an unavailability to a date range, and the Ausfallarbeit of
137/// `BilAReM` Kap. 3.2.2.1 is bounded by exactly this figure — `P_bean`, „die
138/// beanspruchbare Leistung der TR … die sich aus Subtraktion der
139/// Nichtbeanspruchbarkeit von der installierten Leistung ergibt".
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct UnavailabilityAvailablePeriod {
142 /// Interval the curve covers.
143 #[serde(rename = "timeInterval")]
144 pub time_interval: UnavailabilityTimeInterval,
145 /// Resolution of the points (ISO 8601 duration, e.g. `PT15M`).
146 pub resolution: String,
147 /// The points (at least one).
148 #[serde(rename = "Point")]
149 pub points: Vec<UnavailabilityPoint>,
150}
151
152// ── docStatus ─────────────────────────────────────────────────────────────────
153
154/// Document withdrawal status (used instead of `TimeSeries` for withdrawals).
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct DocStatus {
157 /// Always `"A13"` (withdrawn).
158 pub value: String,
159}
160
161// ── TimeSeries ────────────────────────────────────────────────────────────────
162
163/// Bidding zone domain reference in `Unavailability_MarketDocument`.
164pub type UnavailabilityBiddingZone = SimpleContent<String, EicCodingScheme>;
165
166// `biddingZone_Domain.mRID` and `quantity_Measure_Unit.name` are likewise flat
167// dotted elements, not containers.
168
169/// A single unavailability time series.
170///
171/// Each `TimeSeries` covers one calendar day and one business type.
172/// Instead of quarter-hour `Period/Interval` data, this uses separate
173/// `start_DateAndOrTime.date` / `time` and `end_DateAndOrTime.date` / `time`
174/// fields per IEC 62325.
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct UnavailabilityTimeSeries {
177 /// Unique time-series identifier within this document.
178 #[serde(rename = "mRID")]
179 pub m_rid: Mrid,
180 /// Original sender mRID when forwarded via data provider (optional).
181 #[serde(
182 rename = "original_sender_MarketParticipant.mRID",
183 default,
184 skip_serializing_if = "Option::is_none"
185 )]
186 pub original_sender_m_rid: Option<ParticipantMrid>,
187 /// Original document mRID when forwarded (optional).
188 #[serde(
189 rename = "original_document_mRID",
190 default,
191 skip_serializing_if = "Option::is_none"
192 )]
193 pub original_document_m_rid: Option<Mrid>,
194 /// Original revision number when forwarded (optional).
195 #[serde(
196 rename = "original_revisionNumber",
197 default,
198 skip_serializing_if = "Option::is_none"
199 )]
200 pub original_revision_number: Option<RevisionNumber>,
201 /// Original creation timestamp when forwarded (optional).
202 #[serde(
203 rename = "original_createdDateTime",
204 default,
205 skip_serializing_if = "Option::is_none"
206 )]
207 pub original_created_date_time: Option<UtcDateTime>,
208 /// Original time-series mRID when forwarded (optional).
209 #[serde(
210 rename = "original_timeseries_mRID",
211 default,
212 skip_serializing_if = "Option::is_none"
213 )]
214 pub original_timeseries_m_rid: Option<Mrid>,
215 /// Business type: production, planned maintenance, or unplanned outage.
216 #[serde(rename = "businessType")]
217 pub business_type: UnavailabilityBusinessType,
218 /// Control zone of the resource.
219 #[serde(rename = "biddingZone_Domain.mRID")]
220 pub bidding_zone_domain_m_rid: UnavailabilityBiddingZone,
221 /// The production resource this unavailability applies to.
222 #[serde(
223 rename = "production_RegisteredResource.mRID",
224 default,
225 skip_serializing_if = "Option::is_none"
226 )]
227 pub production_registered_resource_m_rid: Option<ParticipantMrid>,
228 /// The power-system resource the production resource belongs to.
229 #[serde(
230 rename = "production_RegisteredResource.pSRType.powerSystemResources.mRID",
231 default,
232 skip_serializing_if = "Option::is_none"
233 )]
234 pub production_registered_resource_psr_type_m_rid: Option<ParticipantMrid>,
235 /// Start date of the unavailability period (ISO date `yyyy-mm-dd`).
236 #[serde(rename = "start_DateAndOrTime.date")]
237 pub start_date: String,
238 /// Start time of the unavailability period (`hh:mm:ssZ`).
239 #[serde(rename = "start_DateAndOrTime.time")]
240 pub start_time: String,
241 /// End date of the unavailability period (ISO date `yyyy-mm-dd`).
242 #[serde(rename = "end_DateAndOrTime.date")]
243 pub end_date: String,
244 /// End time of the unavailability period (`hh:mm:ssZ`).
245 #[serde(rename = "end_DateAndOrTime.time")]
246 pub end_time: String,
247 /// Power unit of the availability curve (always `MAW`).
248 #[serde(rename = "quantity_Measure_Unit.name")]
249 pub quantity_measure_unit_name: String,
250 /// Curve type.
251 #[serde(rename = "curveType")]
252 pub curve_type: String,
253 /// The asset this unavailability applies to.
254 #[serde(
255 rename = "Asset_RegisteredResource",
256 default,
257 skip_serializing_if = "Option::is_none"
258 )]
259 pub asset_registered_resource: Option<AssetRegisteredResource>,
260 /// The interval-resolved availability curve.
261 #[serde(rename = "Available_Period")]
262 pub available_period: UnavailabilityAvailablePeriod,
263}
264
265// ── Unavailability_MarketDocument ─────────────────────────────────────────────
266
267/// `Unavailability_MarketDocument` — planned or forced unavailability of a
268/// generation resource.
269///
270/// XSD version: 1.1b (Fehlerkorrektur 2025-04-16)
271/// Namespace: `urn:iec62325.351:tc57wg16:451-6:outagedocument:3:0`
272///
273/// Each time series covers one complete calendar day. If the document carries
274/// a `docStatus` (withdrawal), no `TimeSeries` elements are present.
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276#[serde(rename = "Unavailability_MarketDocument")]
277pub struct UnavailabilityMarketDocument {
278 /// Unique message identifier (max 35 chars).
279 #[serde(rename = "mRID")]
280 pub m_rid: Mrid,
281 /// Revision number (1–999).
282 #[serde(rename = "revisionNumber")]
283 pub revision_number: RevisionNumber,
284 /// Document type.
285 #[serde(rename = "type")]
286 pub doc_type: UnavailabilityDocType,
287 /// Process type.
288 #[serde(rename = "process.processType")]
289 pub process_type: UnavailabilityProcessType,
290 /// Document creation timestamp (UTC, second precision).
291 #[serde(rename = "createdDateTime")]
292 pub created_date_time: UtcDateTime,
293 /// Sender market participant identifier.
294 #[serde(rename = "sender_MarketParticipant.mRID")]
295 pub sender_m_rid: ParticipantMrid,
296 /// Sender market role.
297 #[serde(rename = "sender_MarketParticipant.marketRole.type")]
298 pub sender_market_role: UnavailabilityMarketRoleType,
299 /// Receiver market participant identifier.
300 #[serde(rename = "receiver_MarketParticipant.mRID")]
301 pub receiver_m_rid: ParticipantMrid,
302 /// Receiver market role.
303 #[serde(rename = "receiver_MarketParticipant.marketRole.type")]
304 pub receiver_market_role: UnavailabilityMarketRoleType,
305 /// The overall unavailability period (one calendar day).
306 ///
307 /// One flat dotted element on the wire, not a
308 /// `<unavailability_Time_Period>` container with a `<timeInterval>` child.
309 #[serde(rename = "unavailability_Time_Period.timeInterval")]
310 pub unavailability_time_interval: UnavailabilityTimeInterval,
311 /// Document withdrawal status (mutually exclusive with `time_series`).
312 #[serde(rename = "docStatus", default, skip_serializing_if = "Option::is_none")]
313 pub doc_status: Option<DocStatus>,
314 /// Unavailability time series (0–30; absent when `doc_status` is set).
315 #[serde(rename = "TimeSeries", default, skip_serializing_if = "Vec::is_empty")]
316 pub time_series: Vec<UnavailabilityTimeSeries>,
317}