Skip to main content

openleadr_wire/
report.rs

1//! Types used for the `report/` endpoint
2
3use crate::{
4    event::EventId,
5    interval::{Interval, IntervalPeriod},
6    target::Target,
7    ClientId, Identifier, IdentifierError, Unit,
8};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use serde_with::skip_serializing_none;
12use std::{
13    fmt::{Display, Formatter},
14    str::FromStr,
15};
16use validator::{Validate, ValidateRange};
17
18/// report object.
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
20#[serde(rename_all = "camelCase")]
21pub struct Report {
22    /// URL safe VTN assigned object ID.
23    pub id: ReportId,
24    /// datetime in ISO 8601 format
25    #[serde(with = "crate::serde_rfc3339")]
26    pub created_date_time: DateTime<Utc>,
27    /// datetime in ISO 8601 format
28    #[serde(with = "crate::serde_rfc3339")]
29    pub modification_date_time: DateTime<Utc>,
30    #[serde(flatten)]
31    #[validate(nested)]
32    pub content: ReportRequest,
33    #[serde(rename = "clientID")]
34    pub client_id: ClientId,
35}
36
37#[skip_serializing_none]
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
39#[serde(rename_all = "camelCase", tag = "objectType", rename = "REPORT")]
40pub struct ReportRequest {
41    /// ID attribute of the event object this report is associated with.
42    #[serde(rename = "eventID")]
43    pub event_id: EventId,
44    /// User generated identifier; may be VEN ID provisioned during program enrollment.
45    #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
46    pub client_name: String,
47    /// User defined string for use in debugging or User Interface.
48    pub report_name: Option<String>,
49    /// A list of reportPayloadDescriptors.
50    ///
51    /// An optional list of objects that provide context to payload types.
52    #[validate(nested)]
53    pub payload_descriptors: Option<Vec<ReportPayloadDescriptor>>,
54    /// A list of objects containing report data for a set of resources.
55    pub resources: Vec<ReportResource>,
56}
57
58impl ReportRequest {
59    pub fn with_client_name(mut self, client_name: &str) -> Self {
60        self.client_name = client_name.to_string();
61        self
62    }
63
64    pub fn with_name(mut self, name: &str) -> Self {
65        self.report_name = Some(name.to_string());
66        self
67    }
68
69    pub fn with_payload_descriptors(mut self, descriptors: Vec<ReportPayloadDescriptor>) -> Self {
70        self.payload_descriptors = Some(descriptors);
71        self
72    }
73
74    pub fn with_resources(mut self, resources: Vec<ReportResource>) -> Self {
75        self.resources = resources;
76        self
77    }
78}
79
80/// URL safe VTN assigned object ID
81#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)]
82pub struct ReportId(pub(crate) Identifier);
83
84impl ReportId {
85    pub fn as_str(&self) -> &str {
86        self.0.as_str()
87    }
88}
89
90impl Display for ReportId {
91    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
92        write!(f, "{}", self.0)
93    }
94}
95
96impl FromStr for ReportId {
97    type Err = IdentifierError;
98
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        Ok(Self(s.parse()?))
101    }
102}
103
104/// Report data associated with a resource.
105#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct ReportResource {
108    /// User generated identifier. A value of AGGREGATED_REPORT indicates an aggregation of more
109    /// that one resource's data
110    pub resource_name: ResourceName,
111    /// Defines default start and durations of intervals.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub interval_period: Option<IntervalPeriod>,
114    /// A list of interval objects.
115    pub intervals: Vec<Interval>,
116}
117
118/// An object that may be used to request a report from a VEN.
119// TODO: replace "-1 means" with proper enum
120#[skip_serializing_none]
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct ReportDescriptor {
124    /// Represents the nature of values.
125    ///
126    /// See enumerations in Definitions for defined string values, or use privately defined strings
127    pub payload_type: ReportType,
128    /// Enumerated or private string signifying the type of reading.
129    pub reading_type: Option<ReadingType>,
130    /// Units of measure.
131    pub units: Option<Unit>,
132    /// A list of targets.
133    pub targets: Option<Vec<Target>>,
134    /// True if report should aggregate results from all targeted resources. False if report includes results for each resource.
135    #[serde(default = "bool_false")]
136    pub aggregate: bool,
137    /// The interval on which to generate a report. -1 indicates generate report at end of last interval.
138    #[serde(default = "neg_one")]
139    pub start_interval: i32,
140    /// The number of intervals to include in a report. -1 indicates that all intervals are to be included.
141    #[serde(default = "neg_one")]
142    pub num_intervals: i32,
143    /// True indicates report on intervals preceding startInterval. False indicates report on intervals following startInterval (e.g. forecast).
144    #[serde(default = "bool_true")]
145    pub historical: bool,
146    /// Number of intervals that elapse between reports. -1 indicates same as numIntervals.
147    #[serde(default = "neg_one")]
148    pub frequency: i32,
149    /// Number of times to repeat report. 1 indicates generate one report. -1 indicates repeat indefinitely.
150    #[serde(default = "pos_one")]
151    pub repeat: i32,
152    /// Indicates VEN report interval options. See User Guide.
153    #[serde(default)]
154    pub report_intervals: ReportIntervals,
155}
156
157impl ReportDescriptor {
158    /// An object that may be used to request a report from a VEN. See OpenADR REST User Guide for detailed description of how configure a report request.
159    pub fn new(payload_type: ReportType) -> Self {
160        Self {
161            payload_type,
162            reading_type: None,
163            units: None,
164            targets: None,
165            aggregate: false,
166            start_interval: -1,
167            num_intervals: -1,
168            historical: true,
169            frequency: -1,
170            repeat: 1,
171            report_intervals: Default::default(),
172        }
173    }
174}
175
176#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
177#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
178pub enum ReportIntervals {
179    #[default]
180    Intervals,
181    SubIntervals,
182    OpenIntervals,
183}
184
185fn bool_false() -> bool {
186    false
187}
188
189fn bool_true() -> bool {
190    true
191}
192
193fn neg_one() -> i32 {
194    -1
195}
196
197fn pos_one() -> i32 {
198    1
199}
200
201/// Contextual information used to interpret report payload values. E.g. a USAGE payload simply
202/// contains a usage value, an associated descriptor provides necessary context such as units and
203/// data quality.
204#[skip_serializing_none]
205#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
206#[serde(rename_all = "camelCase")]
207pub struct ReportPayloadDescriptor {
208    /// Represents the nature of values.
209    ///
210    /// See enumerations in Definitions for defined string values, or use privately defined strings
211    pub payload_type: ReportType,
212    /// Enumerated or private string signifying the type of reading.
213    #[serde(skip_serializing_if = "ReadingType::is_default", default)]
214    pub reading_type: ReadingType,
215    /// Units of measure.
216    pub units: Option<Unit>,
217    /// A quantification of the accuracy of a set of payload values.
218    pub accuracy: Option<f32>,
219    /// A quantification of the confidence in a set of payload values.
220    #[validate(range(min = Confidence(0), max = Confidence(100)))]
221    pub confidence: Option<Confidence>,
222}
223
224impl ReportPayloadDescriptor {
225    pub fn new(payload_type: ReportType) -> Self {
226        Self {
227            payload_type,
228            reading_type: Default::default(),
229            units: None,
230            accuracy: None,
231            confidence: None,
232        }
233    }
234}
235
236#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, PartialOrd)]
237pub struct Confidence(u8);
238
239impl ValidateRange<Confidence> for Confidence {
240    fn greater_than(&self, _: Confidence) -> Option<bool> {
241        None
242    }
243
244    fn less_than(&self, _: Confidence) -> Option<bool> {
245        None
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use crate::{
252        values_map::{Value, ValueType, ValuesMap},
253        Duration,
254    };
255
256    use super::*;
257
258    #[test]
259    fn test_report_type_serialization() {
260        assert_eq!(
261            serde_json::to_string(&ReportType::Baseline).unwrap(),
262            r#""BASELINE""#
263        );
264        assert_eq!(
265            serde_json::to_string(&ReportType::RegulationSetpoint).unwrap(),
266            r#""REGULATION_SETPOINT""#
267        );
268        assert_eq!(
269            serde_json::to_string(&ReportType::Private(String::from("something else"))).unwrap(),
270            r#""something else""#
271        );
272        assert_eq!(
273            serde_json::from_str::<ReportType>(r#""DEMAND""#).unwrap(),
274            ReportType::Demand
275        );
276        assert_eq!(
277            serde_json::from_str::<ReportType>(r#""EXPORT_RESERVATION_FEE""#).unwrap(),
278            ReportType::ExportReservationFee
279        );
280        assert_eq!(
281            serde_json::from_str::<ReportType>(r#""something else""#).unwrap(),
282            ReportType::Private(String::from("something else"))
283        );
284
285        assert!(serde_json::from_str::<ReportType>(r#""""#).is_err());
286        assert!(serde_json::from_str::<ReportType>(&format!("\"{}\"", "x".repeat(129))).is_err());
287    }
288
289    #[test]
290    fn test_reading_type_serialization() {
291        assert_eq!(
292            serde_json::to_string(&ReadingType::DirectRead).unwrap(),
293            r#""DIRECT_READ""#
294        );
295        assert_eq!(
296            serde_json::to_string(&ReadingType::Private(String::from("something else"))).unwrap(),
297            r#""something else""#
298        );
299        assert_eq!(
300            serde_json::from_str::<ReadingType>(r#""AVERAGE""#).unwrap(),
301            ReadingType::Average
302        );
303        assert_eq!(
304            serde_json::from_str::<ReadingType>(r#""something else""#).unwrap(),
305            ReadingType::Private(String::from("something else"))
306        );
307    }
308
309    #[test]
310    fn descriptor_parses_minimal() {
311        let json = r#"{"payloadType":"hello"}"#;
312        let expected = ReportDescriptor::new(ReportType::Private("hello".into()));
313
314        assert_eq!(
315            serde_json::from_str::<ReportDescriptor>(json).unwrap(),
316            expected
317        );
318    }
319
320    #[test]
321    fn parses_minimal_report() {
322        let example = r#"{"eventID":"e1","clientName":"c","resources":[]}"#;
323        let expected = ReportRequest {
324            event_id: EventId("e1".parse().unwrap()),
325            client_name: "c".to_string(),
326            report_name: None,
327            payload_descriptors: None,
328            resources: vec![],
329        };
330
331        assert_eq!(
332            serde_json::from_str::<ReportRequest>(example).unwrap(),
333            expected
334        );
335    }
336
337    #[test]
338    fn test_resource_name_serialization() {
339        assert_eq!(
340            serde_json::to_string(&ResourceName::AggregatedReport).unwrap(),
341            r#""AGGREGATED_REPORT""#
342        );
343        assert_eq!(
344            serde_json::to_string(&ResourceName::Private(String::from("something else"))).unwrap(),
345            r#""something else""#
346        );
347        assert_eq!(
348            serde_json::from_str::<ResourceName>(r#""AGGREGATED_REPORT""#).unwrap(),
349            ResourceName::AggregatedReport
350        );
351        assert_eq!(
352            serde_json::from_str::<ResourceName>(r#""something else""#).unwrap(),
353            ResourceName::Private(String::from("something else"))
354        );
355
356        assert!(serde_json::from_str::<ResourceName>(r#""""#).is_err());
357        assert!(serde_json::from_str::<ResourceName>(&format!("\"{}\"", "x".repeat(129))).is_err());
358    }
359
360    #[test]
361    fn parses_example() {
362        let example = r#"[{
363            "id": "object-999",
364            "createdDateTime": "2023-06-15T09:30:00Z",
365            "modificationDateTime": "2023-06-15T09:30:00Z",
366            "objectType": "REPORT",
367            "programID": "object-999",
368            "eventID": "object-999",
369            "clientName": "VEN-999",
370            "reportName": "Battery_usage_04112023",
371            "payloadDescriptors": null,
372            "resources": [
373              {
374                "resourceName": "RESOURCE-999",
375                "intervalPeriod": {
376                  "start": "2023-06-15T09:30:00Z",
377                  "duration": "PT1H",
378                  "randomizeStart": "PT1H"
379                },
380                "intervals": [
381                  {
382                    "id": 0,
383                    "intervalPeriod": {
384                      "start": "2023-06-15T09:30:00Z",
385                      "duration": "PT1H",
386                      "randomizeStart": "PT1H"
387                    },
388                    "payloads": [
389                      {
390                        "type": "PRICE",
391                        "values": [0.17]
392                      }
393                    ]
394                  }
395                ]
396              }
397            ],
398            "clientID": "249rj49jiej"
399          }]"#;
400
401        let expected = Report {
402            id: ReportId("object-999".parse().unwrap()),
403            created_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
404            modification_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
405            content: ReportRequest {
406                event_id: EventId("object-999".parse().unwrap()),
407                client_name: "VEN-999".into(),
408                report_name: Some("Battery_usage_04112023".into()),
409                payload_descriptors: None,
410                resources: vec![ReportResource {
411                    resource_name: ResourceName::Private("RESOURCE-999".into()),
412                    interval_period: Some(IntervalPeriod {
413                        start: "2023-06-15T09:30:00Z".parse().unwrap(),
414                        duration: Some(Duration::PT1H),
415                        randomize_start: Some(Duration::PT1H),
416                    }),
417                    intervals: vec![Interval {
418                        id: 0,
419                        interval_period: Some(IntervalPeriod {
420                            start: "2023-06-15T09:30:00Z".parse().unwrap(),
421                            duration: Some(Duration::PT1H),
422                            randomize_start: Some(Duration::PT1H),
423                        }),
424                        payloads: vec![ValuesMap {
425                            value_type: ValueType("PRICE".into()),
426                            values: vec![Value::Number(0.17)],
427                        }],
428                    }],
429                }],
430            },
431            client_id: ClientId::new("249rj49jiej").unwrap(),
432        };
433
434        assert_eq!(
435            serde_json::from_str::<Vec<Report>>(example).unwrap()[0],
436            expected
437        );
438    }
439}
440
441#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
442#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
443pub enum ReportType {
444    Reading,
445    Usage,
446    Demand,
447    Setpoint,
448    DeltaUsage,
449    Baseline,
450    OperatingState,
451    UpRegulationAvailable,
452    DownRegulationAvailable,
453    RegulationSetpoint,
454    StorageUsableCapacity,
455    StorageChargeLevel,
456    StorageMaxDischargePower,
457    StorageMaxChargePower,
458    SimpleLevel,
459    UsageForecast,
460    StorageDispatchForecast,
461    LoadShedDeltaAvailable,
462    GenerationDeltaAvailable,
463    DataQuality,
464    ImportReservationCapacity,
465    ImportReservationFee,
466    ExportReservationCapacity,
467    ExportReservationFee,
468    #[serde(untagged)]
469    Private(
470        #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] String,
471    ),
472}
473
474#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
475#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
476pub enum ReadingType {
477    #[default]
478    DirectRead,
479    Estimated,
480    Summed,
481    Mean,
482    Peak,
483    Forecast,
484    Average,
485    #[serde(untagged)]
486    Private(String),
487}
488
489impl ReadingType {
490    fn is_default(&self) -> bool {
491        *self == Self::default()
492    }
493}
494
495#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
496#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
497pub enum ResourceName {
498    AggregatedReport,
499    #[serde(untagged)]
500    Private(
501        #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] String,
502    ),
503}