Skip to main content

openleadr_wire/
report.rs

1//! Types used for the `report/` endpoint
2
3use crate::{
4    ClientId, Identifier, IdentifierError, Unit,
5    event::EventId,
6    interval::{Interval, IntervalPeriod},
7    target::Target,
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        Duration,
253        values_map::{Value, ValueType, ValuesMap},
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            "eventID": "object-999",
368            "clientName": "VEN-999",
369            "reportName": "Battery_usage_04112023",
370            "payloadDescriptors": null,
371            "resources": [
372              {
373                "resourceName": "RESOURCE-999",
374                "intervalPeriod": {
375                  "start": "2023-06-15T09:30:00Z",
376                  "duration": "PT1H",
377                  "randomizeStart": "PT1H"
378                },
379                "intervals": [
380                  {
381                    "id": 0,
382                    "intervalPeriod": {
383                      "start": "2023-06-15T09:30:00Z",
384                      "duration": "PT1H",
385                      "randomizeStart": "PT1H"
386                    },
387                    "payloads": [
388                      {
389                        "type": "PRICE",
390                        "values": [0.17]
391                      }
392                    ]
393                  }
394                ]
395              }
396            ],
397            "clientID": "249rj49jiej"
398          }]"#;
399
400        let expected = Report {
401            id: ReportId("object-999".parse().unwrap()),
402            created_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
403            modification_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
404            content: ReportRequest {
405                event_id: EventId("object-999".parse().unwrap()),
406                client_name: "VEN-999".into(),
407                report_name: Some("Battery_usage_04112023".into()),
408                payload_descriptors: None,
409                resources: vec![ReportResource {
410                    resource_name: ResourceName::Private("RESOURCE-999".into()),
411                    interval_period: Some(IntervalPeriod {
412                        start: "2023-06-15T09:30:00Z".parse().unwrap(),
413                        duration: Some(Duration::PT1H),
414                        randomize_start: Some(Duration::PT1H),
415                    }),
416                    intervals: vec![Interval {
417                        id: 0,
418                        interval_period: Some(IntervalPeriod {
419                            start: "2023-06-15T09:30:00Z".parse().unwrap(),
420                            duration: Some(Duration::PT1H),
421                            randomize_start: Some(Duration::PT1H),
422                        }),
423                        payloads: vec![ValuesMap {
424                            value_type: ValueType("PRICE".into()),
425                            values: vec![Value::Number(0.17)],
426                        }],
427                    }],
428                }],
429            },
430            client_id: ClientId::new("249rj49jiej").unwrap(),
431        };
432
433        assert_eq!(
434            serde_json::from_str::<Vec<Report>>(example).unwrap()[0],
435            expected
436        );
437    }
438}
439
440#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
441#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
442pub enum ReportType {
443    Reading,
444    Usage,
445    Demand,
446    Setpoint,
447    DeltaUsage,
448    Baseline,
449    OperatingState,
450    UpRegulationAvailable,
451    DownRegulationAvailable,
452    RegulationSetpoint,
453    StorageUsableCapacity,
454    StorageChargeLevel,
455    StorageMaxDischargePower,
456    StorageMaxChargePower,
457    SimpleLevel,
458    UsageForecast,
459    StorageDispatchForecast,
460    LoadShedDeltaAvailable,
461    GenerationDeltaAvailable,
462    DataQuality,
463    ImportReservationCapacity,
464    ImportReservationFee,
465    ExportReservationCapacity,
466    ExportReservationFee,
467    #[serde(untagged)]
468    Private(
469        #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] String,
470    ),
471}
472
473#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
474#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
475pub enum ReadingType {
476    #[default]
477    DirectRead,
478    Estimated,
479    Summed,
480    Mean,
481    Peak,
482    Forecast,
483    Average,
484    #[serde(untagged)]
485    Private(String),
486}
487
488impl ReadingType {
489    fn is_default(&self) -> bool {
490        *self == Self::default()
491    }
492}
493
494#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
495#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
496pub enum ResourceName {
497    AggregatedReport,
498    #[serde(untagged)]
499    Private(
500        #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] String,
501    ),
502}