1#![expect(
60 deprecated,
61 reason = "Defining deprecated variant for backwards compatibility"
62)]
63
64use core::fmt;
65
66use jiff::{Timestamp, civil::Date};
67use serde::Deserialize;
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[non_exhaustive]
72pub enum State {
73 Nsw,
75 Vic,
77 Qld,
79 Sa,
81}
82
83impl fmt::Display for State {
84 #[inline]
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 State::Nsw => write!(f, "nsw"),
88 State::Vic => write!(f, "vic"),
89 State::Qld => write!(f, "qld"),
90 State::Sa => write!(f, "sa"),
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum Resolution {
99 FiveMinute = 5,
101 ThirtyMinute = 30,
103}
104
105impl fmt::Display for Resolution {
106 #[inline]
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Resolution::FiveMinute => write!(f, "5"),
110 Resolution::ThirtyMinute => write!(f, "30"),
111 }
112 }
113}
114
115impl From<Resolution> for u32 {
116 #[inline]
117 fn from(value: Resolution) -> Self {
118 match value {
119 Resolution::FiveMinute => 5,
120 Resolution::ThirtyMinute => 30,
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
127#[serde(rename_all = "camelCase")]
128#[non_exhaustive]
129pub enum ChannelType {
130 General,
133 ControlledLoad,
137 FeedIn,
140}
141
142impl fmt::Display for ChannelType {
143 #[inline]
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self {
146 ChannelType::General => write!(f, "general"),
147 ChannelType::ControlledLoad => write!(f, "controlled load"),
148 ChannelType::FeedIn => write!(f, "feed-in"),
149 }
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Deserialize)]
165#[serde(rename_all = "camelCase")]
166#[non_exhaustive]
167pub struct Channel {
168 pub identifier: String,
170 #[serde(rename = "type")]
172 pub channel_type: ChannelType,
173 pub tariff: String,
175}
176
177impl fmt::Display for Channel {
178 #[inline]
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 write!(
181 f,
182 "{} ({}): {}",
183 self.identifier, self.channel_type, self.tariff
184 )
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
200#[serde(rename_all = "camelCase")]
201#[non_exhaustive]
202pub enum SiteStatus {
203 Pending,
209 Active,
211 Closed,
213}
214
215impl fmt::Display for SiteStatus {
216 #[inline]
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 match self {
219 SiteStatus::Pending => write!(f, "pending"),
220 SiteStatus::Active => write!(f, "active"),
221 SiteStatus::Closed => write!(f, "closed"),
222 }
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Deserialize)]
228#[serde(rename_all = "camelCase")]
229#[non_exhaustive]
230pub struct Site {
231 pub id: String,
233 pub nmi: String,
235 pub channels: Vec<Channel>,
237 pub network: String,
239 pub status: SiteStatus,
241 pub active_from: Option<Date>,
245 pub closed_on: Option<Date>,
247 pub interval_length: u32,
249}
250
251impl fmt::Display for Site {
252 #[inline]
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 write!(
255 f,
256 "Site {} (NMI: {}) - {} on {} network",
257 self.id, self.nmi, self.status, self.network
258 )
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
267#[serde(rename_all = "camelCase")]
268#[non_exhaustive]
269pub enum SpikeStatus {
270 None,
272 Potential,
274 Spike,
276}
277
278impl fmt::Display for SpikeStatus {
279 #[inline]
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 match self {
282 SpikeStatus::None => write!(f, "none"),
283 SpikeStatus::Potential => write!(f, "potential"),
284 SpikeStatus::Spike => write!(f, "spike"),
285 }
286 }
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
295#[serde(rename_all = "camelCase")]
296#[non_exhaustive]
297pub enum PriceDescriptor {
298 #[deprecated(note = "Negative pricing is no longer used. Use `ExtremelyLow` instead.")]
300 Negative,
301 ExtremelyLow,
303 VeryLow,
305 Low,
307 Neutral,
309 High,
311 Spike,
313}
314
315impl fmt::Display for PriceDescriptor {
316 #[inline]
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 match self {
319 PriceDescriptor::Negative => write!(f, "negative"),
320 PriceDescriptor::ExtremelyLow => write!(f, "extremely low"),
321 PriceDescriptor::VeryLow => write!(f, "very low"),
322 PriceDescriptor::Low => write!(f, "low"),
323 PriceDescriptor::Neutral => write!(f, "neutral"),
324 PriceDescriptor::High => write!(f, "high"),
325 PriceDescriptor::Spike => write!(f, "spike"),
326 }
327 }
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
334#[serde(rename_all = "camelCase")]
335#[non_exhaustive]
336pub enum RenewableDescriptor {
337 Best,
339 Great,
341 Ok,
343 NotGreat,
345 Worst,
347}
348
349impl fmt::Display for RenewableDescriptor {
350 #[inline]
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 match self {
353 RenewableDescriptor::Best => write!(f, "best"),
354 RenewableDescriptor::Great => write!(f, "great"),
355 RenewableDescriptor::Ok => write!(f, "ok"),
356 RenewableDescriptor::NotGreat => write!(f, "not great"),
357 RenewableDescriptor::Worst => write!(f, "worst"),
358 }
359 }
360}
361
362#[derive(Debug, Clone, PartialEq, Deserialize)]
365#[serde(rename_all = "camelCase")]
366#[non_exhaustive]
367pub struct Range {
368 pub min: f64,
370 pub max: f64,
372}
373
374impl fmt::Display for Range {
375 #[inline]
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 write!(f, "{:.2}-{:.2}c/kWh", self.min, self.max)
378 }
379}
380
381#[derive(Debug, Clone, PartialEq, Deserialize)]
387#[serde(rename_all = "camelCase")]
388#[non_exhaustive]
389pub struct AdvancedPrice {
390 pub low: f64,
393 pub predicted: f64,
396 pub high: f64,
399}
400
401impl fmt::Display for AdvancedPrice {
402 #[inline]
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 write!(
405 f,
406 "L:{:.2} H:{:.2} P:{:.2} c/kWh",
407 self.low, self.predicted, self.high
408 )
409 }
410}
411
412#[derive(Debug, Clone, PartialEq, Deserialize)]
414#[serde(rename_all = "camelCase")]
415#[non_exhaustive]
416pub struct TariffInformation {
417 pub period: Option<TariffPeriod>,
421 pub season: Option<TariffSeason>,
425 pub block: Option<u32>,
429 pub demand_window: Option<bool>,
433}
434
435impl fmt::Display for TariffInformation {
436 #[inline]
437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438 let mut parts = Vec::new();
439
440 if let Some(ref period) = self.period {
441 parts.push(format!("period:{period}"));
442 }
443 if let Some(ref season) = self.season {
444 parts.push(format!("season:{season}"));
445 }
446 if let Some(block) = self.block {
447 parts.push(format!("block:{block}"));
448 }
449 if let Some(demand_window) = self.demand_window {
450 parts.push(format!("demand window:{demand_window}"));
451 }
452
453 if parts.is_empty() {
454 write!(f, "No tariff information")
455 } else {
456 write!(f, "{}", parts.join(", "))
457 }
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
463#[serde(rename_all = "camelCase")]
464#[non_exhaustive]
465pub enum TariffPeriod {
466 OffPeak,
468 Shoulder,
470 SolarSponge,
473 Peak,
475}
476
477impl fmt::Display for TariffPeriod {
478 #[inline]
479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480 match self {
481 TariffPeriod::OffPeak => write!(f, "off peak"),
482 TariffPeriod::Shoulder => write!(f, "shoulder"),
483 TariffPeriod::SolarSponge => write!(f, "solar sponge"),
484 TariffPeriod::Peak => write!(f, "peak"),
485 }
486 }
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
491#[serde(rename_all = "camelCase")]
492#[non_exhaustive]
493pub enum TariffSeason {
494 Default,
496 Summer,
498 Autumn,
500 Winter,
502 Spring,
504 NonSummer,
506 Holiday,
508 Weekend,
510 WeekendHoliday,
512 Weekday,
514}
515
516impl fmt::Display for TariffSeason {
517 #[inline]
518 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519 match self {
520 TariffSeason::Default => write!(f, "default"),
521 TariffSeason::Summer => write!(f, "summer"),
522 TariffSeason::Autumn => write!(f, "autumn"),
523 TariffSeason::Winter => write!(f, "winter"),
524 TariffSeason::Spring => write!(f, "spring"),
525 TariffSeason::NonSummer => write!(f, "non summer"),
526 TariffSeason::Holiday => write!(f, "holiday"),
527 TariffSeason::Weekend => write!(f, "weekend"),
528 TariffSeason::WeekendHoliday => write!(f, "weekend holiday"),
529 TariffSeason::Weekday => write!(f, "weekday"),
530 }
531 }
532}
533
534#[derive(Debug, Clone, PartialEq, Deserialize)]
536#[serde(rename_all = "camelCase")]
537#[non_exhaustive]
538pub struct BaseInterval {
539 pub duration: u32,
541 pub spot_per_kwh: f64,
546 pub per_kwh: f64,
548 pub date: Date,
553 pub nem_time: Timestamp,
557 pub start_time: Timestamp,
559 pub end_time: Timestamp,
561 pub renewables: f64,
563 pub channel_type: ChannelType,
565 pub tariff_information: Option<TariffInformation>,
567 pub spike_status: SpikeStatus,
569 pub descriptor: PriceDescriptor,
571}
572
573impl fmt::Display for BaseInterval {
574 #[inline]
575 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576 write!(
577 f,
578 "{} {} {:.2}c/kWh (spot: {:.2}c/kWh) ({}) {}% renewable",
579 self.date,
580 self.channel_type,
581 self.per_kwh,
582 self.spot_per_kwh,
583 self.descriptor,
584 self.renewables
585 )?;
586
587 if self.spike_status != SpikeStatus::None {
588 write!(f, " spike: {}", self.spike_status)?;
589 }
590
591 if let Some(ref tariff) = self.tariff_information {
592 write!(f, " [{tariff}]")?;
593 }
594
595 Ok(())
596 }
597}
598
599#[derive(Debug, Clone, PartialEq, Deserialize)]
601#[serde(rename_all = "camelCase")]
602#[non_exhaustive]
603pub struct ActualInterval {
604 #[serde(flatten)]
606 pub base: BaseInterval,
607}
608
609impl fmt::Display for ActualInterval {
610 #[inline]
611 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612 write!(f, "Actual: {}", self.base)
613 }
614}
615
616#[derive(Debug, Clone, PartialEq, Deserialize)]
618#[serde(rename_all = "camelCase")]
619#[non_exhaustive]
620pub struct ForecastInterval {
621 #[serde(flatten)]
623 pub base: BaseInterval,
624 pub range: Option<Range>,
626 pub advanced_price: Option<AdvancedPrice>,
628}
629
630impl fmt::Display for ForecastInterval {
631 #[inline]
632 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633 write!(f, "Forecast: {}", self.base)?;
634 if let Some(ref range) = self.range {
635 write!(f, " Range: {range}")?;
636 }
637 if let Some(ref adv_price) = self.advanced_price {
638 write!(f, " Advanced: {adv_price}")?;
639 }
640 Ok(())
641 }
642}
643
644#[derive(Debug, Clone, PartialEq, Deserialize)]
646#[serde(rename_all = "camelCase")]
647#[non_exhaustive]
648pub struct CurrentInterval {
649 #[serde(flatten)]
651 pub base: BaseInterval,
652 pub range: Option<Range>,
654 pub estimate: bool,
657 pub advanced_price: Option<AdvancedPrice>,
659}
660
661impl fmt::Display for CurrentInterval {
662 #[inline]
663 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
664 write!(f, "Current: {}", self.base)?;
665 if self.estimate {
666 write!(f, " (estimate)")?;
667 }
668 if let Some(ref range) = self.range {
669 write!(f, " Range: {range}")?;
670 }
671 if let Some(ref adv_price) = self.advanced_price {
672 write!(f, " Advanced: {adv_price}")?;
673 }
674 Ok(())
675 }
676}
677
678#[derive(Debug, Clone, PartialEq, Deserialize)]
680#[serde(tag = "type")]
681#[non_exhaustive]
682pub enum Interval {
683 ActualInterval(ActualInterval),
685 ForecastInterval(ForecastInterval),
687 CurrentInterval(CurrentInterval),
689}
690
691impl Interval {
692 #[must_use]
696 #[inline]
697 pub fn is_actual_interval(&self) -> bool {
698 matches!(self, Self::ActualInterval(..))
699 }
700
701 #[must_use]
705 #[inline]
706 pub fn is_forecast_interval(&self) -> bool {
707 matches!(self, Self::ForecastInterval(..))
708 }
709
710 #[inline]
714 #[must_use]
715 pub fn is_current_interval(&self) -> bool {
716 matches!(self, Self::CurrentInterval(..))
717 }
718
719 #[inline]
723 #[must_use]
724 pub fn as_actual_interval(&self) -> Option<&ActualInterval> {
725 if let Self::ActualInterval(v) = self {
726 Some(v)
727 } else {
728 None
729 }
730 }
731
732 #[inline]
736 #[must_use]
737 pub fn as_forecast_interval(&self) -> Option<&ForecastInterval> {
738 if let Self::ForecastInterval(v) = self {
739 Some(v)
740 } else {
741 None
742 }
743 }
744
745 #[inline]
749 #[must_use]
750 pub fn as_current_interval(&self) -> Option<&CurrentInterval> {
751 if let Self::CurrentInterval(v) = self {
752 Some(v)
753 } else {
754 None
755 }
756 }
757
758 #[inline]
760 #[must_use]
761 pub fn as_base_interval(&self) -> Option<&BaseInterval> {
762 match self {
763 Interval::ActualInterval(actual) => Some(&actual.base),
764 Interval::ForecastInterval(forecast) => Some(&forecast.base),
765 Interval::CurrentInterval(current) => Some(¤t.base),
766 }
767 }
768}
769
770impl fmt::Display for Interval {
771 #[inline]
772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773 match self {
774 Interval::ActualInterval(actual) => write!(f, "{actual}"),
775 Interval::ForecastInterval(forecast) => write!(f, "{forecast}"),
776 Interval::CurrentInterval(current) => write!(f, "{current}"),
777 }
778 }
779}
780
781#[derive(Debug, Clone, PartialEq, Deserialize)]
783#[serde(rename_all = "camelCase")]
784#[non_exhaustive]
785pub struct Usage {
786 #[serde(flatten)]
788 pub base: BaseInterval,
789 pub channel_identifier: String,
791 pub kwh: f64,
795 pub quality: UsageQuality,
797 pub cost: f64,
800}
801
802impl fmt::Display for Usage {
803 #[inline]
804 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
805 write!(
806 f,
807 "Usage {} {:.2}kWh ${:.2} ({})",
808 self.channel_identifier, self.kwh, self.cost, self.quality
809 )
810 }
811}
812
813#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
815#[serde(rename_all = "camelCase")]
816#[non_exhaustive]
817pub enum UsageQuality {
818 Estimated,
820 Billable,
822}
823
824impl fmt::Display for UsageQuality {
825 #[inline]
826 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
827 match self {
828 UsageQuality::Estimated => write!(f, "estimated"),
829 UsageQuality::Billable => write!(f, "billable"),
830 }
831 }
832}
833
834#[derive(Debug, Clone, PartialEq, Deserialize)]
836#[serde(rename_all = "camelCase")]
837#[non_exhaustive]
838pub struct BaseRenewable {
839 pub duration: u32,
841 pub date: Date,
846 pub nem_time: Timestamp,
850 pub start_time: Timestamp,
852 pub end_time: Timestamp,
854 pub renewables: f64,
856 pub descriptor: RenewableDescriptor,
858}
859
860impl fmt::Display for BaseRenewable {
861 #[inline]
862 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
863 write!(
864 f,
865 "{} {}% renewable ({})",
866 self.date, self.renewables, self.descriptor
867 )
868 }
869}
870
871#[derive(Debug, Clone, PartialEq, Deserialize)]
873#[serde(rename_all = "camelCase")]
874#[non_exhaustive]
875pub struct ActualRenewable {
876 #[serde(flatten)]
878 pub base: BaseRenewable,
879}
880
881impl fmt::Display for ActualRenewable {
882 #[inline]
883 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
884 write!(f, "Actual: {}", self.base)
885 }
886}
887
888#[derive(Debug, Clone, PartialEq, Deserialize)]
890#[serde(rename_all = "camelCase")]
891#[non_exhaustive]
892pub struct ForecastRenewable {
893 #[serde(flatten)]
895 pub base: BaseRenewable,
896}
897
898impl fmt::Display for ForecastRenewable {
899 #[inline]
900 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
901 write!(f, "Forecast: {}", self.base)
902 }
903}
904
905#[derive(Debug, Clone, PartialEq, Deserialize)]
907#[serde(rename_all = "camelCase")]
908#[non_exhaustive]
909pub struct CurrentRenewable {
910 #[serde(flatten)]
912 pub base: BaseRenewable,
913}
914
915impl fmt::Display for CurrentRenewable {
916 #[inline]
917 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
918 write!(f, "Current: {}", self.base)
919 }
920}
921
922#[derive(Debug, Clone, PartialEq, Deserialize)]
924#[serde(tag = "type")]
925#[non_exhaustive]
926pub enum Renewable {
927 ActualRenewable(ActualRenewable),
929 ForecastRenewable(ForecastRenewable),
931 CurrentRenewable(CurrentRenewable),
933}
934
935impl Renewable {
936 #[must_use]
940 #[inline]
941 pub fn is_actual_renewable(&self) -> bool {
942 matches!(self, Self::ActualRenewable(..))
943 }
944
945 #[must_use]
949 #[inline]
950 pub fn is_forecast_renewable(&self) -> bool {
951 matches!(self, Self::ForecastRenewable(..))
952 }
953
954 #[must_use]
958 #[inline]
959 pub fn is_current_renewable(&self) -> bool {
960 matches!(self, Self::CurrentRenewable(..))
961 }
962
963 #[must_use]
967 #[inline]
968 pub fn as_actual_renewable(&self) -> Option<&ActualRenewable> {
969 if let Self::ActualRenewable(v) = self {
970 Some(v)
971 } else {
972 None
973 }
974 }
975
976 #[must_use]
980 #[inline]
981 pub fn as_forecast_renewable(&self) -> Option<&ForecastRenewable> {
982 if let Self::ForecastRenewable(v) = self {
983 Some(v)
984 } else {
985 None
986 }
987 }
988
989 #[must_use]
993 #[inline]
994 pub fn as_current_renewable(&self) -> Option<&CurrentRenewable> {
995 if let Self::CurrentRenewable(v) = self {
996 Some(v)
997 } else {
998 None
999 }
1000 }
1001
1002 #[must_use]
1004 #[inline]
1005 pub fn as_base_renewable(&self) -> &BaseRenewable {
1006 match self {
1007 Self::ActualRenewable(actual) => &actual.base,
1008 Self::ForecastRenewable(forecast) => &forecast.base,
1009 Self::CurrentRenewable(current) => ¤t.base,
1010 }
1011 }
1012}
1013
1014impl fmt::Display for Renewable {
1015 #[inline]
1016 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1017 match self {
1018 Renewable::ActualRenewable(actual) => write!(f, "{actual}"),
1019 Renewable::ForecastRenewable(forecast) => write!(f, "{forecast}"),
1020 Renewable::CurrentRenewable(current) => write!(f, "{current}"),
1021 }
1022 }
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028 use anyhow::Result;
1029 use pretty_assertions::assert_eq;
1030
1031 #[test]
1032 fn actual_renewable_deserialisation_strict() -> Result<()> {
1033 let json = r#"{
1034 "type": "ActualRenewable",
1035 "duration": 5,
1036 "date": "2021-05-05",
1037 "nemTime": "2021-05-06T12:30:00+10:00",
1038 "startTime": "2021-05-05T02:00:01Z",
1039 "endTime": "2021-05-05T02:30:00Z",
1040 "renewables": 45,
1041 "descriptor": "best"
1042 }"#;
1043
1044 let actual: ActualRenewable = serde_json::from_str(json)?;
1045 assert_eq!(actual.base.duration, 5);
1046 assert_eq!(actual.base.date.to_string(), "2021-05-05");
1047 assert!(44.0_f64 < actual.base.renewables && actual.base.renewables < 46.0_f64);
1048 assert_eq!(actual.base.descriptor, RenewableDescriptor::Best);
1049
1050 Ok(())
1051 }
1052
1053 #[test]
1054 fn actual_renewable_deserialisation() -> Result<()> {
1055 let json = r#"{
1056 "type": "ActualRenewable",
1057 "duration": 5,
1058 "date": "2021-05-05",
1059 "nemTime": "2021-05-06T12:30:00+10:00",
1060 "startTime": "2021-05-05T02:00:01Z",
1061 "endTime": "2021-05-05T02:30:00Z",
1062 "renewables": 45,
1063 "descriptor": "best"
1064 }"#;
1065
1066 let renewable: Renewable = serde_json::from_str(json)?;
1067 if let Renewable::ActualRenewable(actual) = renewable {
1068 assert_eq!(actual.base.duration, 5);
1069 assert_eq!(actual.base.date.to_string(), "2021-05-05");
1070 assert!(44.0_f64 < actual.base.renewables && actual.base.renewables < 46.0_f64);
1071 assert_eq!(actual.base.descriptor, RenewableDescriptor::Best);
1072 } else {
1073 panic!("Expected ActualRenewable variant");
1074 }
1075
1076 Ok(())
1077 }
1078
1079 #[test]
1080 fn current_renewable_deserialisation_strict() -> Result<()> {
1081 let json = r#"{
1082 "type": "CurrentRenewable",
1083 "duration": 5,
1084 "date": "2021-05-05",
1085 "nemTime": "2021-05-06T12:30:00+10:00",
1086 "startTime": "2021-05-05T02:00:01Z",
1087 "endTime": "2021-05-05T02:30:00Z",
1088 "renewables": 45,
1089 "descriptor": "best"
1090 }"#;
1091
1092 let current: CurrentRenewable = serde_json::from_str(json)?;
1093 assert_eq!(current.base.duration, 5);
1094 assert_eq!(current.base.date.to_string(), "2021-05-05");
1095 assert!(44.0_f64 < current.base.renewables && current.base.renewables < 46.0_f64);
1096 assert_eq!(current.base.descriptor, RenewableDescriptor::Best);
1097
1098 Ok(())
1099 }
1100
1101 #[test]
1102 fn current_renewable_deserialisation() -> Result<()> {
1103 let json = r#"{
1104 "type": "CurrentRenewable",
1105 "duration": 5,
1106 "date": "2021-05-05",
1107 "nemTime": "2021-05-06T12:30:00+10:00",
1108 "startTime": "2021-05-05T02:00:01Z",
1109 "endTime": "2021-05-05T02:30:00Z",
1110 "renewables": 45,
1111 "descriptor": "best"
1112 }"#;
1113
1114 let renewable: Renewable = serde_json::from_str(json)?;
1115 if let Renewable::CurrentRenewable(current) = renewable {
1116 assert_eq!(current.base.duration, 5);
1117 assert_eq!(current.base.date.to_string(), "2021-05-05");
1118 assert!(44.0_f64 < current.base.renewables && current.base.renewables < 46.0_f64);
1119 assert_eq!(current.base.descriptor, RenewableDescriptor::Best);
1120 } else {
1121 panic!("Expected CurrentRenewable variant");
1122 }
1123
1124 Ok(())
1125 }
1126
1127 #[test]
1128 fn forecast_renewable_deserialisation_strict() -> Result<()> {
1129 let json = r#"{
1130 "type": "ForecastRenewable",
1131 "duration": 5,
1132 "date": "2021-05-05",
1133 "nemTime": "2021-05-06T12:30:00+10:00",
1134 "startTime": "2021-05-05T02:00:01Z",
1135 "endTime": "2021-05-05T02:30:00Z",
1136 "renewables": 45,
1137 "descriptor": "best"
1138 }"#;
1139
1140 let forecast: ForecastRenewable = serde_json::from_str(json)?;
1141 assert_eq!(forecast.base.duration, 5);
1142 assert_eq!(forecast.base.date.to_string(), "2021-05-05");
1143 assert!(44.0_f64 < forecast.base.renewables && forecast.base.renewables < 46.0_f64);
1144 assert_eq!(forecast.base.descriptor, RenewableDescriptor::Best);
1145
1146 Ok(())
1147 }
1148
1149 #[test]
1150 fn forecast_renewable_deserialisation() -> Result<()> {
1151 let json = r#"{
1152 "type": "ForecastRenewable",
1153 "duration": 5,
1154 "date": "2021-05-05",
1155 "nemTime": "2021-05-06T12:30:00+10:00",
1156 "startTime": "2021-05-05T02:00:01Z",
1157 "endTime": "2021-05-05T02:30:00Z",
1158 "renewables": 45,
1159 "descriptor": "best"
1160 }"#;
1161
1162 let renewable: Renewable = serde_json::from_str(json)?;
1163 if let Renewable::ForecastRenewable(forecast) = renewable {
1164 assert_eq!(forecast.base.duration, 5);
1165 assert_eq!(forecast.base.date.to_string(), "2021-05-05");
1166 assert!(44.0_f64 < forecast.base.renewables && forecast.base.renewables < 46.0_f64);
1167 assert_eq!(forecast.base.descriptor, RenewableDescriptor::Best);
1168 } else {
1169 panic!("Expected ForecastRenewable variant");
1170 }
1171
1172 Ok(())
1173 }
1174
1175 #[test]
1177 fn site_deserialisation() -> Result<()> {
1178 let json = r#"[
1179 {
1180 "id": "01F5A5CRKMZ5BCX9P1S4V990AM",
1181 "nmi": "3052282872",
1182 "channels": [
1183 {
1184 "identifier": "E1",
1185 "type": "general",
1186 "tariff": "A100"
1187 }
1188 ],
1189 "network": "Jemena",
1190 "status": "closed",
1191 "activeFrom": "2022-01-01",
1192 "closedOn": "2022-05-01",
1193 "intervalLength": 30
1194 }
1195 ]"#;
1196
1197 let sites: Vec<Site> = serde_json::from_str(json)?;
1198 assert_eq!(sites.len(), 1);
1199
1200 let site = sites.first().expect("Expected at least one site");
1201 assert_eq!(site.id, "01F5A5CRKMZ5BCX9P1S4V990AM");
1202 assert_eq!(site.nmi, "3052282872");
1203 assert_eq!(site.channels.len(), 1);
1204
1205 let channel = site
1206 .channels
1207 .first()
1208 .expect("Expected at least one channel");
1209 assert_eq!(channel.identifier, "E1");
1210 assert_eq!(channel.channel_type, ChannelType::General);
1211 assert_eq!(channel.tariff, "A100");
1212
1213 assert_eq!(site.network, "Jemena");
1214 assert_eq!(site.status, SiteStatus::Closed);
1215 assert_eq!(
1216 site.active_from
1217 .expect("Expected active_from date")
1218 .to_string(),
1219 "2022-01-01"
1220 );
1221 assert_eq!(
1222 site.closed_on.expect("Expected closed_on date").to_string(),
1223 "2022-05-01"
1224 );
1225 assert_eq!(site.interval_length, 30);
1226
1227 Ok(())
1228 }
1229
1230 #[test]
1232 #[expect(
1233 clippy::too_many_lines,
1234 reason = "Comprehensive test for all interval types"
1235 )]
1236 fn prices_interval_deserialisation() -> Result<()> {
1237 let json = r#"[
1238 {
1239 "type": "ActualInterval",
1240 "duration": 5,
1241 "spotPerKwh": 6.12,
1242 "perKwh": 24.33,
1243 "date": "2021-05-05",
1244 "nemTime": "2021-05-06T12:30:00+10:00",
1245 "startTime": "2021-05-05T02:00:01Z",
1246 "endTime": "2021-05-05T02:30:00Z",
1247 "renewables": 45,
1248 "channelType": "general",
1249 "tariffInformation": null,
1250 "spikeStatus": "none",
1251 "descriptor": "negative"
1252 },
1253 {
1254 "type": "CurrentInterval",
1255 "duration": 5,
1256 "spotPerKwh": 6.12,
1257 "perKwh": 24.33,
1258 "date": "2021-05-05",
1259 "nemTime": "2021-05-06T12:30:00+10:00",
1260 "startTime": "2021-05-05T02:00:01Z",
1261 "endTime": "2021-05-05T02:30:00Z",
1262 "renewables": 45,
1263 "channelType": "general",
1264 "tariffInformation": null,
1265 "spikeStatus": "none",
1266 "descriptor": "negative",
1267 "range": {
1268 "min": 0,
1269 "max": 0
1270 },
1271 "estimate": true,
1272 "advancedPrice": {
1273 "low": 1,
1274 "predicted": 3,
1275 "high": 10
1276 }
1277 },
1278 {
1279 "type": "ForecastInterval",
1280 "duration": 5,
1281 "spotPerKwh": 6.12,
1282 "perKwh": 24.33,
1283 "date": "2021-05-05",
1284 "nemTime": "2021-05-06T12:30:00+10:00",
1285 "startTime": "2021-05-05T02:00:01Z",
1286 "endTime": "2021-05-05T02:30:00Z",
1287 "renewables": 45,
1288 "channelType": "general",
1289 "tariffInformation": null,
1290 "spikeStatus": "none",
1291 "descriptor": "negative",
1292 "range": {
1293 "min": 0,
1294 "max": 0
1295 },
1296 "advancedPrice": {
1297 "low": 1,
1298 "predicted": 3,
1299 "high": 10
1300 }
1301 }
1302 ]"#;
1303
1304 let intervals: Vec<Interval> = serde_json::from_str(json)?;
1305 assert_eq!(intervals.len(), 3);
1306
1307 if let Some(Interval::ActualInterval(actual)) = intervals.first() {
1309 assert_eq!(actual.base.duration, 5);
1310 assert!((actual.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1311 assert!((actual.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1312 assert_eq!(actual.base.date.to_string(), "2021-05-05");
1313 assert!((actual.base.renewables - 45.0_f64).abs() < f64::EPSILON);
1314 assert_eq!(actual.base.channel_type, ChannelType::General);
1315 assert_eq!(actual.base.spike_status, SpikeStatus::None);
1316 assert_eq!(actual.base.descriptor, PriceDescriptor::Negative);
1317 } else {
1318 panic!("Expected ActualInterval at index 0");
1319 }
1320
1321 if let Some(Interval::CurrentInterval(current)) = intervals.get(1) {
1323 assert_eq!(current.base.duration, 5);
1324 assert!((current.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1325 assert!((current.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1326 assert_eq!(current.estimate, true);
1327 assert!(current.range.is_some());
1328 assert!(current.advanced_price.is_some());
1329
1330 if let Some(ref range) = current.range {
1331 assert!((range.min - 0.0_f64).abs() < f64::EPSILON);
1332 assert!((range.max - 0.0_f64).abs() < f64::EPSILON);
1333 }
1334
1335 if let Some(ref adv_price) = current.advanced_price {
1336 assert!((adv_price.low - 1.0_f64).abs() < f64::EPSILON);
1337 assert!((adv_price.predicted - 3.0_f64).abs() < f64::EPSILON);
1338 assert!((adv_price.high - 10.0_f64).abs() < f64::EPSILON);
1339 }
1340 } else {
1341 panic!("Expected CurrentInterval at index 1");
1342 }
1343
1344 if let Some(Interval::ForecastInterval(forecast)) = intervals.get(2) {
1346 assert_eq!(forecast.base.duration, 5);
1347 assert!((forecast.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1348 assert!((forecast.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1349 assert!(forecast.range.is_some());
1350 assert!(forecast.advanced_price.is_some());
1351 } else {
1352 panic!("Expected ForecastInterval at index 2");
1353 }
1354
1355 Ok(())
1356 }
1357
1358 #[test]
1360 fn current_prices_interval_deserialisation() -> Result<()> {
1361 let json = r#"[
1362 {
1363 "type": "ActualInterval",
1364 "duration": 5,
1365 "spotPerKwh": 6.12,
1366 "perKwh": 24.33,
1367 "date": "2021-05-05",
1368 "nemTime": "2021-05-06T12:30:00+10:00",
1369 "startTime": "2021-05-05T02:00:01Z",
1370 "endTime": "2021-05-05T02:30:00Z",
1371 "renewables": 45,
1372 "channelType": "general",
1373 "tariffInformation": null,
1374 "spikeStatus": "none",
1375 "descriptor": "negative"
1376 },
1377 {
1378 "type": "CurrentInterval",
1379 "duration": 5,
1380 "spotPerKwh": 6.12,
1381 "perKwh": 24.33,
1382 "date": "2021-05-05",
1383 "nemTime": "2021-05-06T12:30:00+10:00",
1384 "startTime": "2021-05-05T02:00:01Z",
1385 "endTime": "2021-05-05T02:30:00Z",
1386 "renewables": 45,
1387 "channelType": "general",
1388 "tariffInformation": null,
1389 "spikeStatus": "none",
1390 "descriptor": "negative",
1391 "range": {
1392 "min": 0,
1393 "max": 0
1394 },
1395 "estimate": true,
1396 "advancedPrice": {
1397 "low": 1,
1398 "predicted": 3,
1399 "high": 10
1400 }
1401 },
1402 {
1403 "type": "ForecastInterval",
1404 "duration": 5,
1405 "spotPerKwh": 6.12,
1406 "perKwh": 24.33,
1407 "date": "2021-05-05",
1408 "nemTime": "2021-05-06T12:30:00+10:00",
1409 "startTime": "2021-05-05T02:00:01Z",
1410 "endTime": "2021-05-05T02:30:00Z",
1411 "renewables": 45,
1412 "channelType": "general",
1413 "tariffInformation": null,
1414 "spikeStatus": "none",
1415 "descriptor": "negative",
1416 "range": {
1417 "min": 0,
1418 "max": 0
1419 },
1420 "advancedPrice": {
1421 "low": 1,
1422 "predicted": 3,
1423 "high": 10
1424 }
1425 }
1426 ]"#;
1427
1428 let intervals: Vec<Interval> = serde_json::from_str(json)?;
1429 assert_eq!(intervals.len(), 3);
1430
1431 let first_interval = intervals.first().expect("Expected at least one interval");
1433 let second_interval = intervals.get(1).expect("Expected at least two intervals");
1434 let third_interval = intervals.get(2).expect("Expected at least three intervals");
1435
1436 assert!(matches!(first_interval, Interval::ActualInterval(_)));
1437 assert!(matches!(second_interval, Interval::CurrentInterval(_)));
1438 assert!(matches!(third_interval, Interval::ForecastInterval(_)));
1439
1440 Ok(())
1441 }
1442
1443 #[test]
1445 fn usage_deserialisation() -> Result<()> {
1446 let json = r#"[
1447 {
1448 "type": "Usage",
1449 "duration": 5,
1450 "spotPerKwh": 6.12,
1451 "perKwh": 24.33,
1452 "date": "2021-05-05",
1453 "nemTime": "2021-05-06T12:30:00+10:00",
1454 "startTime": "2021-05-05T02:00:01Z",
1455 "endTime": "2021-05-05T02:30:00Z",
1456 "renewables": 45,
1457 "channelType": "general",
1458 "tariffInformation": null,
1459 "spikeStatus": "none",
1460 "descriptor": "negative",
1461 "channelIdentifier": "E1",
1462 "kwh": 0,
1463 "quality": "estimated",
1464 "cost": 0
1465 }
1466 ]"#;
1467
1468 let usage_data: Vec<Usage> = serde_json::from_str(json)?;
1469 assert_eq!(usage_data.len(), 1);
1470
1471 let usage = usage_data
1472 .first()
1473 .expect("Expected at least one usage entry");
1474 assert_eq!(usage.base.duration, 5);
1475 assert!((usage.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1476 assert!((usage.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1477 assert_eq!(usage.base.date.to_string(), "2021-05-05");
1478 assert!((usage.base.renewables - 45.0_f64).abs() < f64::EPSILON);
1479 assert_eq!(usage.base.channel_type, ChannelType::General);
1480 assert_eq!(usage.base.spike_status, SpikeStatus::None);
1481 assert_eq!(usage.base.descriptor, PriceDescriptor::Negative);
1482 assert_eq!(usage.channel_identifier, "E1");
1483 assert!((usage.kwh - 0.0_f64).abs() < f64::EPSILON);
1484 assert_eq!(usage.quality, UsageQuality::Estimated);
1485 assert!((usage.cost - 0.0_f64).abs() < f64::EPSILON);
1486
1487 Ok(())
1488 }
1489
1490 #[test]
1492 fn channel_types_deserialisation() -> Result<()> {
1493 let general_json = r#"{"identifier": "E1", "type": "general", "tariff": "A100"}"#;
1495 let controlled_json = r#"{"identifier": "E2", "type": "controlledLoad", "tariff": "A200"}"#;
1496 let feedin_json = r#"{"identifier": "E3", "type": "feedIn", "tariff": "A300"}"#;
1497
1498 let general: Channel = serde_json::from_str(general_json)?;
1499 let controlled: Channel = serde_json::from_str(controlled_json)?;
1500 let feedin: Channel = serde_json::from_str(feedin_json)?;
1501
1502 assert_eq!(general.channel_type, ChannelType::General);
1503 assert_eq!(controlled.channel_type, ChannelType::ControlledLoad);
1504 assert_eq!(feedin.channel_type, ChannelType::FeedIn);
1505
1506 Ok(())
1507 }
1508
1509 #[test]
1510 fn site_status_deserialisation() -> Result<()> {
1511 #[derive(Deserialize)]
1512 struct TestSiteStatus {
1513 status: SiteStatus,
1514 }
1515
1516 let pending_json = r#"{"status": "pending"}"#;
1518 let active_json = r#"{"status": "active"}"#;
1519 let closed_json = r#"{"status": "closed"}"#;
1520
1521 let pending: TestSiteStatus = serde_json::from_str(pending_json)?;
1522 let active: TestSiteStatus = serde_json::from_str(active_json)?;
1523 let closed: TestSiteStatus = serde_json::from_str(closed_json)?;
1524
1525 assert_eq!(pending.status, SiteStatus::Pending);
1526 assert_eq!(active.status, SiteStatus::Active);
1527 assert_eq!(closed.status, SiteStatus::Closed);
1528
1529 Ok(())
1530 }
1531
1532 #[test]
1533 fn range_and_advanced_price_deserialisation() -> Result<()> {
1534 let range_json = r#"{"min": 0, "max": 100}"#;
1535 let advanced_price_json = r#"{"low": 1, "predicted": 3, "high": 10}"#;
1536
1537 let range: Range = serde_json::from_str(range_json)?;
1538 let advanced_price: AdvancedPrice = serde_json::from_str(advanced_price_json)?;
1539
1540 assert!((range.min - 0.0_f64).abs() < f64::EPSILON);
1541 assert!((range.max - 100.0_f64).abs() < f64::EPSILON);
1542 assert!((advanced_price.low - 1.0_f64).abs() < f64::EPSILON);
1543 assert!((advanced_price.predicted - 3.0_f64).abs() < f64::EPSILON);
1544 assert!((advanced_price.high - 10.0_f64).abs() < f64::EPSILON);
1545
1546 Ok(())
1547 }
1548
1549 #[test]
1550 fn usage_quality_deserialisation() -> Result<()> {
1551 #[derive(Deserialize)]
1552 struct TestUsageQuality {
1553 quality: UsageQuality,
1554 }
1555
1556 let estimated_json = r#"{"quality": "estimated"}"#;
1557 let billable_json = r#"{"quality": "billable"}"#;
1558
1559 let estimated: TestUsageQuality = serde_json::from_str(estimated_json)?;
1560 let billable: TestUsageQuality = serde_json::from_str(billable_json)?;
1561
1562 assert_eq!(estimated.quality, UsageQuality::Estimated);
1563 assert_eq!(billable.quality, UsageQuality::Billable);
1564
1565 Ok(())
1566 }
1567
1568 #[test]
1570 fn display_state() {
1571 insta::assert_snapshot!(State::Nsw.to_string(), @"nsw");
1572 insta::assert_snapshot!(State::Vic.to_string(), @"vic");
1573 insta::assert_snapshot!(State::Qld.to_string(), @"qld");
1574 insta::assert_snapshot!(State::Sa.to_string(), @"sa");
1575 }
1576
1577 #[test]
1578 fn display_resolution() {
1579 insta::assert_snapshot!(Resolution::FiveMinute.to_string(), @"5");
1580 insta::assert_snapshot!(Resolution::ThirtyMinute.to_string(), @"30");
1581 }
1582
1583 #[test]
1584 fn display_channel_type() {
1585 insta::assert_snapshot!(ChannelType::General.to_string(), @"general");
1586 insta::assert_snapshot!(ChannelType::ControlledLoad.to_string(), @"controlled load");
1587 insta::assert_snapshot!(ChannelType::FeedIn.to_string(), @"feed-in");
1588 }
1589
1590 #[test]
1591 fn display_channel() {
1592 let channel = Channel {
1593 identifier: "E1".to_owned(),
1594 channel_type: ChannelType::General,
1595 tariff: "A100".to_owned(),
1596 };
1597 insta::assert_snapshot!(channel.to_string(), @"E1 (general): A100");
1598 }
1599
1600 #[test]
1601 fn display_site_status() {
1602 insta::assert_snapshot!(SiteStatus::Pending.to_string(), @"pending");
1603 insta::assert_snapshot!(SiteStatus::Active.to_string(), @"active");
1604 insta::assert_snapshot!(SiteStatus::Closed.to_string(), @"closed");
1605 }
1606
1607 #[test]
1608 fn display_site() {
1609 use jiff::civil::Date;
1610 let site = Site {
1611 id: "01F5A5CRKMZ5BCX9P1S4V990AM".to_owned(),
1612 nmi: "3052282872".to_owned(),
1613 channels: vec![],
1614 network: "Jemena".to_owned(),
1615 status: SiteStatus::Active,
1616 active_from: Some(Date::constant(2022, 1, 1)),
1617 closed_on: None,
1618 interval_length: 30,
1619 };
1620 insta::assert_snapshot!(site.to_string(), @"Site 01F5A5CRKMZ5BCX9P1S4V990AM (NMI: 3052282872) - active on Jemena network");
1621 }
1622
1623 #[test]
1624 fn display_spike_status() {
1625 insta::assert_snapshot!(SpikeStatus::None.to_string(), @"none");
1626 insta::assert_snapshot!(SpikeStatus::Potential.to_string(), @"potential");
1627 insta::assert_snapshot!(SpikeStatus::Spike.to_string(), @"spike");
1628 }
1629
1630 #[test]
1631 fn display_price_descriptor() {
1632 insta::assert_snapshot!(PriceDescriptor::Negative.to_string(), @"negative");
1633 insta::assert_snapshot!(PriceDescriptor::ExtremelyLow.to_string(), @"extremely low");
1634 insta::assert_snapshot!(PriceDescriptor::VeryLow.to_string(), @"very low");
1635 insta::assert_snapshot!(PriceDescriptor::Low.to_string(), @"low");
1636 insta::assert_snapshot!(PriceDescriptor::Neutral.to_string(), @"neutral");
1637 insta::assert_snapshot!(PriceDescriptor::High.to_string(), @"high");
1638 insta::assert_snapshot!(PriceDescriptor::Spike.to_string(), @"spike");
1639 }
1640
1641 #[test]
1642 fn display_renewable_descriptor() {
1643 insta::assert_snapshot!(RenewableDescriptor::Best.to_string(), @"best");
1644 insta::assert_snapshot!(RenewableDescriptor::Great.to_string(), @"great");
1645 insta::assert_snapshot!(RenewableDescriptor::Ok.to_string(), @"ok");
1646 insta::assert_snapshot!(RenewableDescriptor::NotGreat.to_string(), @"not great");
1647 insta::assert_snapshot!(RenewableDescriptor::Worst.to_string(), @"worst");
1648 }
1649
1650 #[test]
1651 fn display_range() {
1652 let range = Range {
1653 min: 12.34,
1654 max: 56.78,
1655 };
1656 insta::assert_snapshot!(range.to_string(), @"12.34-56.78c/kWh");
1657 }
1658
1659 #[test]
1660 fn display_advanced_price() {
1661 let advanced_price = AdvancedPrice {
1662 low: 1.23,
1663 predicted: 4.56,
1664 high: 7.89,
1665 };
1666 insta::assert_snapshot!(advanced_price.to_string(), @"L:1.23 H:4.56 P:7.89 c/kWh");
1667 }
1668
1669 #[test]
1670 fn display_tariff_period() {
1671 insta::assert_snapshot!(TariffPeriod::OffPeak.to_string(), @"off peak");
1672 insta::assert_snapshot!(TariffPeriod::Shoulder.to_string(), @"shoulder");
1673 insta::assert_snapshot!(TariffPeriod::SolarSponge.to_string(), @"solar sponge");
1674 insta::assert_snapshot!(TariffPeriod::Peak.to_string(), @"peak");
1675 }
1676
1677 #[test]
1678 fn display_tariff_season() {
1679 insta::assert_snapshot!(TariffSeason::Default.to_string(), @"default");
1680 insta::assert_snapshot!(TariffSeason::Summer.to_string(), @"summer");
1681 insta::assert_snapshot!(TariffSeason::Autumn.to_string(), @"autumn");
1682 insta::assert_snapshot!(TariffSeason::Winter.to_string(), @"winter");
1683 insta::assert_snapshot!(TariffSeason::Spring.to_string(), @"spring");
1684 insta::assert_snapshot!(TariffSeason::NonSummer.to_string(), @"non summer");
1685 insta::assert_snapshot!(TariffSeason::Holiday.to_string(), @"holiday");
1686 insta::assert_snapshot!(TariffSeason::Weekend.to_string(), @"weekend");
1687 insta::assert_snapshot!(TariffSeason::WeekendHoliday.to_string(), @"weekend holiday");
1688 insta::assert_snapshot!(TariffSeason::Weekday.to_string(), @"weekday");
1689 }
1690
1691 #[test]
1692 fn display_tariff_information() {
1693 let empty_tariff = TariffInformation {
1695 period: None,
1696 season: None,
1697 block: None,
1698 demand_window: None,
1699 };
1700 insta::assert_snapshot!(empty_tariff.to_string(), @"No tariff information");
1701
1702 let full_tariff = TariffInformation {
1704 period: Some(TariffPeriod::Peak),
1705 season: Some(TariffSeason::Summer),
1706 block: Some(2),
1707 demand_window: Some(true),
1708 };
1709 insta::assert_snapshot!(full_tariff.to_string(), @"period:peak, season:summer, block:2, demand window:true");
1710
1711 let partial_tariff = TariffInformation {
1713 period: Some(TariffPeriod::OffPeak),
1714 season: None,
1715 block: Some(1),
1716 demand_window: Some(false),
1717 };
1718 insta::assert_snapshot!(partial_tariff.to_string(), @"period:off peak, block:1, demand window:false");
1719 }
1720
1721 #[test]
1722 fn display_base_interval() {
1723 use jiff::{Timestamp, civil::Date};
1724 let nem_time = "2021-05-06T12:30:00+10:00"
1726 .parse::<Timestamp>()
1727 .expect("valid timestamp");
1728 let start_time = "2021-05-05T02:00:01Z"
1729 .parse::<Timestamp>()
1730 .expect("valid timestamp");
1731 let end_time = "2021-05-05T02:30:00Z"
1732 .parse::<Timestamp>()
1733 .expect("valid timestamp");
1734
1735 let base_interval_basic = BaseInterval {
1737 duration: 5,
1738 spot_per_kwh: 6.12,
1739 per_kwh: 24.33,
1740 date: Date::constant(2021, 5, 5),
1741 nem_time,
1742 start_time,
1743 end_time,
1744 renewables: 45.5,
1745 channel_type: ChannelType::General,
1746 tariff_information: None,
1747 spike_status: SpikeStatus::None,
1748 descriptor: PriceDescriptor::Low,
1749 };
1750 insta::assert_snapshot!(base_interval_basic.to_string(), @"2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (low) 45.5% renewable");
1751
1752 let base_interval_potential_spike = BaseInterval {
1754 duration: 5,
1755 spot_per_kwh: 6.12,
1756 per_kwh: 24.33,
1757 date: Date::constant(2021, 5, 5),
1758 nem_time,
1759 start_time,
1760 end_time,
1761 renewables: 45.5,
1762 channel_type: ChannelType::General,
1763 tariff_information: None,
1764 spike_status: SpikeStatus::Potential,
1765 descriptor: PriceDescriptor::High,
1766 };
1767 insta::assert_snapshot!(base_interval_potential_spike.to_string(), @"2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (high) 45.5% renewable spike: potential");
1768
1769 let base_interval_spike = BaseInterval {
1771 duration: 5,
1772 spot_per_kwh: 100.50,
1773 per_kwh: 120.75,
1774 date: Date::constant(2021, 5, 5),
1775 nem_time,
1776 start_time,
1777 end_time,
1778 renewables: 25.0,
1779 channel_type: ChannelType::General,
1780 tariff_information: None,
1781 spike_status: SpikeStatus::Spike,
1782 descriptor: PriceDescriptor::Spike,
1783 };
1784 insta::assert_snapshot!(base_interval_spike.to_string(), @"2021-05-05 general 120.75c/kWh (spot: 100.50c/kWh) (spike) 25% renewable spike: spike");
1785
1786 let tariff_info = TariffInformation {
1788 period: Some(TariffPeriod::Peak),
1789 season: Some(TariffSeason::Summer),
1790 block: Some(2),
1791 demand_window: Some(true),
1792 };
1793 let base_interval_tariff = BaseInterval {
1794 duration: 30,
1795 spot_per_kwh: 15.20,
1796 per_kwh: 35.40,
1797 date: Date::constant(2021, 7, 15),
1798 nem_time,
1799 start_time,
1800 end_time,
1801 renewables: 30.2,
1802 channel_type: ChannelType::ControlledLoad,
1803 tariff_information: Some(tariff_info),
1804 spike_status: SpikeStatus::None,
1805 descriptor: PriceDescriptor::Neutral,
1806 };
1807 insta::assert_snapshot!(base_interval_tariff.to_string(), @"2021-07-15 controlled load 35.40c/kWh (spot: 15.20c/kWh) (neutral) 30.2% renewable [period:peak, season:summer, block:2, demand window:true]");
1808
1809 let tariff_info_combined = TariffInformation {
1811 period: Some(TariffPeriod::OffPeak),
1812 season: None,
1813 block: None,
1814 demand_window: Some(false),
1815 };
1816 let base_interval_combined = BaseInterval {
1817 duration: 5,
1818 spot_per_kwh: 8.75,
1819 per_kwh: 28.90,
1820 date: Date::constant(2021, 12, 25),
1821 nem_time,
1822 start_time,
1823 end_time,
1824 renewables: 60.8,
1825 channel_type: ChannelType::FeedIn,
1826 tariff_information: Some(tariff_info_combined),
1827 spike_status: SpikeStatus::Potential,
1828 descriptor: PriceDescriptor::VeryLow,
1829 };
1830 insta::assert_snapshot!(base_interval_combined.to_string(), @"2021-12-25 feed-in 28.90c/kWh (spot: 8.75c/kWh) (very low) 60.8% renewable spike: potential [period:off peak, demand window:false]");
1831 }
1832
1833 #[test]
1834 fn display_actual_interval() {
1835 use jiff::{Timestamp, civil::Date};
1836 let nem_time = "2021-05-06T12:30:00+10:00"
1837 .parse::<Timestamp>()
1838 .expect("valid timestamp");
1839 let start_time = "2021-05-05T02:00:01Z"
1840 .parse::<Timestamp>()
1841 .expect("valid timestamp");
1842 let end_time = "2021-05-05T02:30:00Z"
1843 .parse::<Timestamp>()
1844 .expect("valid timestamp");
1845
1846 let actual_interval = ActualInterval {
1847 base: BaseInterval {
1848 duration: 5,
1849 spot_per_kwh: 6.12,
1850 per_kwh: 24.33,
1851 date: Date::constant(2021, 5, 5),
1852 nem_time,
1853 start_time,
1854 end_time,
1855 renewables: 45.5,
1856 channel_type: ChannelType::General,
1857 tariff_information: None,
1858 spike_status: SpikeStatus::None,
1859 descriptor: PriceDescriptor::Low,
1860 },
1861 };
1862 insta::assert_snapshot!(actual_interval.to_string(), @"Actual: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (low) 45.5% renewable");
1863 }
1864
1865 #[test]
1866 fn display_forecast_interval() {
1867 use jiff::{Timestamp, civil::Date};
1868 let nem_time = "2021-05-06T12:30:00+10:00"
1869 .parse::<Timestamp>()
1870 .expect("valid timestamp");
1871 let start_time = "2021-05-05T02:00:01Z"
1872 .parse::<Timestamp>()
1873 .expect("valid timestamp");
1874 let end_time = "2021-05-05T02:30:00Z"
1875 .parse::<Timestamp>()
1876 .expect("valid timestamp");
1877
1878 let forecast_interval = ForecastInterval {
1879 base: BaseInterval {
1880 duration: 5,
1881 spot_per_kwh: 6.12,
1882 per_kwh: 24.33,
1883 date: Date::constant(2021, 5, 5),
1884 nem_time,
1885 start_time,
1886 end_time,
1887 renewables: 45.5,
1888 channel_type: ChannelType::General,
1889 tariff_information: None,
1890 spike_status: SpikeStatus::Potential,
1891 descriptor: PriceDescriptor::High,
1892 },
1893 range: Some(Range {
1894 min: 10.0,
1895 max: 30.0,
1896 }),
1897 advanced_price: Some(AdvancedPrice {
1898 low: 15.0,
1899 predicted: 20.0,
1900 high: 25.0,
1901 }),
1902 };
1903 insta::assert_snapshot!(forecast_interval.to_string(), @"Forecast: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (high) 45.5% renewable spike: potential Range: 10.00-30.00c/kWh Advanced: L:15.00 H:20.00 P:25.00 c/kWh");
1904 }
1905
1906 #[test]
1907 fn display_current_interval() {
1908 use jiff::{Timestamp, civil::Date};
1909 let nem_time = "2021-05-06T12:30:00+10:00"
1910 .parse::<Timestamp>()
1911 .expect("valid timestamp");
1912 let start_time = "2021-05-05T02:00:01Z"
1913 .parse::<Timestamp>()
1914 .expect("valid timestamp");
1915 let end_time = "2021-05-05T02:30:00Z"
1916 .parse::<Timestamp>()
1917 .expect("valid timestamp");
1918
1919 let current_interval = CurrentInterval {
1920 base: BaseInterval {
1921 duration: 5,
1922 spot_per_kwh: 6.12,
1923 per_kwh: 24.33,
1924 date: Date::constant(2021, 5, 5),
1925 nem_time,
1926 start_time,
1927 end_time,
1928 renewables: 45.5,
1929 channel_type: ChannelType::FeedIn,
1930 tariff_information: None,
1931 spike_status: SpikeStatus::Spike,
1932 descriptor: PriceDescriptor::Spike,
1933 },
1934 range: Some(Range {
1935 min: 50.0,
1936 max: 100.0,
1937 }),
1938 estimate: true,
1939 advanced_price: Some(AdvancedPrice {
1940 low: 60.0,
1941 predicted: 75.0,
1942 high: 90.0,
1943 }),
1944 };
1945 insta::assert_snapshot!(current_interval.to_string(), @"Current: 2021-05-05 feed-in 24.33c/kWh (spot: 6.12c/kWh) (spike) 45.5% renewable spike: spike (estimate) Range: 50.00-100.00c/kWh Advanced: L:60.00 H:75.00 P:90.00 c/kWh");
1946 }
1947
1948 #[test]
1949 fn display_interval_enum() {
1950 use jiff::{Timestamp, civil::Date};
1951 let nem_time = "2021-05-06T12:30:00+10:00"
1952 .parse::<Timestamp>()
1953 .expect("valid timestamp");
1954 let start_time = "2021-05-05T02:00:01Z"
1955 .parse::<Timestamp>()
1956 .expect("valid timestamp");
1957 let end_time = "2021-05-05T02:30:00Z"
1958 .parse::<Timestamp>()
1959 .expect("valid timestamp");
1960
1961 let base = BaseInterval {
1962 duration: 5,
1963 spot_per_kwh: 6.12,
1964 per_kwh: 24.33,
1965 date: Date::constant(2021, 5, 5),
1966 nem_time,
1967 start_time,
1968 end_time,
1969 renewables: 45.5,
1970 channel_type: ChannelType::General,
1971 tariff_information: None,
1972 spike_status: SpikeStatus::None,
1973 descriptor: PriceDescriptor::Neutral,
1974 };
1975
1976 let actual_interval = Interval::ActualInterval(ActualInterval { base: base.clone() });
1977 let forecast_interval = Interval::ForecastInterval(ForecastInterval {
1978 base: base.clone(),
1979 range: None,
1980 advanced_price: None,
1981 });
1982 let current_interval = Interval::CurrentInterval(CurrentInterval {
1983 base,
1984 range: None,
1985 estimate: false,
1986 advanced_price: None,
1987 });
1988
1989 insta::assert_snapshot!(actual_interval.to_string(), @"Actual: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (neutral) 45.5% renewable");
1990 insta::assert_snapshot!(forecast_interval.to_string(), @"Forecast: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (neutral) 45.5% renewable");
1991 insta::assert_snapshot!(current_interval.to_string(), @"Current: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (neutral) 45.5% renewable");
1992 }
1993
1994 #[test]
1995 fn display_usage_quality() {
1996 insta::assert_snapshot!(UsageQuality::Estimated.to_string(), @"estimated");
1997 insta::assert_snapshot!(UsageQuality::Billable.to_string(), @"billable");
1998 }
1999
2000 #[test]
2001 fn display_usage() {
2002 use jiff::{Timestamp, civil::Date};
2003 let nem_time = "2021-05-06T12:30:00+10:00"
2004 .parse::<Timestamp>()
2005 .expect("valid timestamp");
2006 let start_time = "2021-05-05T02:00:01Z"
2007 .parse::<Timestamp>()
2008 .expect("valid timestamp");
2009 let end_time = "2021-05-05T02:30:00Z"
2010 .parse::<Timestamp>()
2011 .expect("valid timestamp");
2012
2013 let usage = Usage {
2014 base: BaseInterval {
2015 duration: 5,
2016 spot_per_kwh: 6.12,
2017 per_kwh: 24.33,
2018 date: Date::constant(2021, 5, 5),
2019 nem_time,
2020 start_time,
2021 end_time,
2022 renewables: 45.5,
2023 channel_type: ChannelType::General,
2024 tariff_information: None,
2025 spike_status: SpikeStatus::None,
2026 descriptor: PriceDescriptor::Low,
2027 },
2028 channel_identifier: "E1".to_owned(),
2029 kwh: 1.25,
2030 quality: UsageQuality::Billable,
2031 cost: 30.41,
2032 };
2033 insta::assert_snapshot!(usage.to_string(), @"Usage E1 1.25kWh $30.41 (billable)");
2034 }
2035
2036 #[test]
2037 fn display_base_renewable() {
2038 use jiff::{Timestamp, civil::Date};
2039 let nem_time = "2021-05-06T12:30:00+10:00"
2040 .parse::<Timestamp>()
2041 .expect("valid timestamp");
2042 let start_time = "2021-05-05T02:00:01Z"
2043 .parse::<Timestamp>()
2044 .expect("valid timestamp");
2045 let end_time = "2021-05-05T02:30:00Z"
2046 .parse::<Timestamp>()
2047 .expect("valid timestamp");
2048
2049 let base_renewable = BaseRenewable {
2050 duration: 5,
2051 date: Date::constant(2021, 5, 5),
2052 nem_time,
2053 start_time,
2054 end_time,
2055 renewables: 78.5,
2056 descriptor: RenewableDescriptor::Great,
2057 };
2058 insta::assert_snapshot!(base_renewable.to_string(), @"2021-05-05 78.5% renewable (great)");
2059 }
2060
2061 #[test]
2062 fn display_actual_renewable() {
2063 use jiff::{Timestamp, civil::Date};
2064 let nem_time = "2021-05-06T12:30:00+10:00"
2065 .parse::<Timestamp>()
2066 .expect("valid timestamp");
2067 let start_time = "2021-05-05T02:00:01Z"
2068 .parse::<Timestamp>()
2069 .expect("valid timestamp");
2070 let end_time = "2021-05-05T02:30:00Z"
2071 .parse::<Timestamp>()
2072 .expect("valid timestamp");
2073
2074 let actual_renewable = ActualRenewable {
2075 base: BaseRenewable {
2076 duration: 5,
2077 date: Date::constant(2021, 5, 5),
2078 nem_time,
2079 start_time,
2080 end_time,
2081 renewables: 78.5,
2082 descriptor: RenewableDescriptor::Great,
2083 },
2084 };
2085 insta::assert_snapshot!(actual_renewable.to_string(), @"Actual: 2021-05-05 78.5% renewable (great)");
2086 }
2087
2088 #[test]
2089 fn display_forecast_renewable() {
2090 use jiff::{Timestamp, civil::Date};
2091 let nem_time = "2021-05-06T12:30:00+10:00"
2092 .parse::<Timestamp>()
2093 .expect("valid timestamp");
2094 let start_time = "2021-05-05T02:00:01Z"
2095 .parse::<Timestamp>()
2096 .expect("valid timestamp");
2097 let end_time = "2021-05-05T02:30:00Z"
2098 .parse::<Timestamp>()
2099 .expect("valid timestamp");
2100
2101 let forecast_renewable = ForecastRenewable {
2102 base: BaseRenewable {
2103 duration: 5,
2104 date: Date::constant(2021, 5, 5),
2105 nem_time,
2106 start_time,
2107 end_time,
2108 renewables: 78.5,
2109 descriptor: RenewableDescriptor::Great,
2110 },
2111 };
2112 insta::assert_snapshot!(forecast_renewable.to_string(), @"Forecast: 2021-05-05 78.5% renewable (great)");
2113 }
2114
2115 #[test]
2116 fn display_current_renewable() {
2117 use jiff::{Timestamp, civil::Date};
2118 let nem_time = "2021-05-06T12:30:00+10:00"
2119 .parse::<Timestamp>()
2120 .expect("valid timestamp");
2121 let start_time = "2021-05-05T02:00:01Z"
2122 .parse::<Timestamp>()
2123 .expect("valid timestamp");
2124 let end_time = "2021-05-05T02:30:00Z"
2125 .parse::<Timestamp>()
2126 .expect("valid timestamp");
2127
2128 let current_renewable = CurrentRenewable {
2129 base: BaseRenewable {
2130 duration: 5,
2131 date: Date::constant(2021, 5, 5),
2132 nem_time,
2133 start_time,
2134 end_time,
2135 renewables: 78.5,
2136 descriptor: RenewableDescriptor::Great,
2137 },
2138 };
2139 insta::assert_snapshot!(current_renewable.to_string(), @"Current: 2021-05-05 78.5% renewable (great)");
2140 }
2141
2142 #[test]
2143 fn display_renewable_enum() {
2144 use jiff::{Timestamp, civil::Date};
2145 let nem_time = "2021-05-06T12:30:00+10:00"
2146 .parse::<Timestamp>()
2147 .expect("valid timestamp");
2148 let start_time = "2021-05-05T02:00:01Z"
2149 .parse::<Timestamp>()
2150 .expect("valid timestamp");
2151 let end_time = "2021-05-05T02:30:00Z"
2152 .parse::<Timestamp>()
2153 .expect("valid timestamp");
2154
2155 let base = BaseRenewable {
2156 duration: 5,
2157 date: Date::constant(2021, 5, 5),
2158 nem_time,
2159 start_time,
2160 end_time,
2161 renewables: 78.5,
2162 descriptor: RenewableDescriptor::Great,
2163 };
2164
2165 let actual_renewable = Renewable::ActualRenewable(ActualRenewable { base: base.clone() });
2166 let forecast_renewable =
2167 Renewable::ForecastRenewable(ForecastRenewable { base: base.clone() });
2168 let current_renewable = Renewable::CurrentRenewable(CurrentRenewable { base });
2169
2170 insta::assert_snapshot!(actual_renewable.to_string(), @"Actual: 2021-05-05 78.5% renewable (great)");
2171 insta::assert_snapshot!(forecast_renewable.to_string(), @"Forecast: 2021-05-05 78.5% renewable (great)");
2172 insta::assert_snapshot!(current_renewable.to_string(), @"Current: 2021-05-05 78.5% renewable (great)");
2173 }
2174}