Skip to main content

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}