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,
499 Autumn,
501 Winter,
503 Spring,
505 NonSummer,
507 Holiday,
509 Weekend,
511 WeekendHoliday,
513 Weekday,
515}
516
517impl fmt::Display for TariffSeason {
518 #[inline]
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 match self {
521 TariffSeason::Default => write!(f, "default"),
522 TariffSeason::Summer => write!(f, "summer"),
523 TariffSeason::Autumn => write!(f, "autumn"),
524 TariffSeason::Winter => write!(f, "winter"),
525 TariffSeason::Spring => write!(f, "spring"),
526 TariffSeason::NonSummer => write!(f, "non summer"),
527 TariffSeason::Holiday => write!(f, "holiday"),
528 TariffSeason::Weekend => write!(f, "weekend"),
529 TariffSeason::WeekendHoliday => write!(f, "weekend holiday"),
530 TariffSeason::Weekday => write!(f, "weekday"),
531 }
532 }
533}
534
535#[derive(Debug, Clone, PartialEq, Deserialize)]
537#[serde(rename_all = "camelCase")]
538#[non_exhaustive]
539pub struct BaseInterval {
540 pub duration: u32,
542 pub spot_per_kwh: f64,
547 pub per_kwh: f64,
549 pub date: Date,
554 pub nem_time: Timestamp,
558 pub start_time: Timestamp,
560 pub end_time: Timestamp,
562 pub renewables: f64,
564 pub channel_type: ChannelType,
566 pub tariff_information: Option<TariffInformation>,
568 pub spike_status: SpikeStatus,
570 pub descriptor: PriceDescriptor,
572}
573
574impl fmt::Display for BaseInterval {
575 #[inline]
576 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577 write!(
578 f,
579 "{} {} {:.2}c/kWh (spot: {:.2}c/kWh) ({}) {}% renewable",
580 self.date,
581 self.channel_type,
582 self.per_kwh,
583 self.spot_per_kwh,
584 self.descriptor,
585 self.renewables
586 )?;
587
588 if self.spike_status != SpikeStatus::None {
589 write!(f, " spike: {}", self.spike_status)?;
590 }
591
592 if let Some(ref tariff) = self.tariff_information {
593 write!(f, " [{tariff}]")?;
594 }
595
596 Ok(())
597 }
598}
599
600#[derive(Debug, Clone, PartialEq, Deserialize)]
602#[serde(rename_all = "camelCase")]
603#[non_exhaustive]
604pub struct ActualInterval {
605 #[serde(flatten)]
607 pub base: BaseInterval,
608}
609
610impl fmt::Display for ActualInterval {
611 #[inline]
612 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613 write!(f, "Actual: {}", self.base)
614 }
615}
616
617#[derive(Debug, Clone, PartialEq, Deserialize)]
619#[serde(rename_all = "camelCase")]
620#[non_exhaustive]
621pub struct ForecastInterval {
622 #[serde(flatten)]
624 pub base: BaseInterval,
625 pub range: Option<Range>,
627 pub advanced_price: Option<AdvancedPrice>,
629}
630
631impl fmt::Display for ForecastInterval {
632 #[inline]
633 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
634 write!(f, "Forecast: {}", self.base)?;
635 if let Some(ref range) = self.range {
636 write!(f, " Range: {range}")?;
637 }
638 if let Some(ref adv_price) = self.advanced_price {
639 write!(f, " Advanced: {adv_price}")?;
640 }
641 Ok(())
642 }
643}
644
645#[derive(Debug, Clone, PartialEq, Deserialize)]
647#[serde(rename_all = "camelCase")]
648#[non_exhaustive]
649pub struct CurrentInterval {
650 #[serde(flatten)]
652 pub base: BaseInterval,
653 pub range: Option<Range>,
655 pub estimate: bool,
658 pub advanced_price: Option<AdvancedPrice>,
660}
661
662impl fmt::Display for CurrentInterval {
663 #[inline]
664 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665 write!(f, "Current: {}", self.base)?;
666 if self.estimate {
667 write!(f, " (estimate)")?;
668 }
669 if let Some(ref range) = self.range {
670 write!(f, " Range: {range}")?;
671 }
672 if let Some(ref adv_price) = self.advanced_price {
673 write!(f, " Advanced: {adv_price}")?;
674 }
675 Ok(())
676 }
677}
678
679#[derive(Debug, Clone, PartialEq, Deserialize)]
681#[serde(tag = "type")]
682#[non_exhaustive]
683pub enum Interval {
684 ActualInterval(ActualInterval),
686 ForecastInterval(ForecastInterval),
688 CurrentInterval(CurrentInterval),
690}
691
692impl Interval {
693 #[must_use]
697 #[inline]
698 pub fn is_actual_interval(&self) -> bool {
699 matches!(self, Self::ActualInterval(..))
700 }
701
702 #[must_use]
706 #[inline]
707 pub fn is_forecast_interval(&self) -> bool {
708 matches!(self, Self::ForecastInterval(..))
709 }
710
711 #[inline]
715 #[must_use]
716 pub fn is_current_interval(&self) -> bool {
717 matches!(self, Self::CurrentInterval(..))
718 }
719
720 #[inline]
724 #[must_use]
725 pub fn as_actual_interval(&self) -> Option<&ActualInterval> {
726 if let Self::ActualInterval(v) = self {
727 Some(v)
728 } else {
729 None
730 }
731 }
732
733 #[inline]
737 #[must_use]
738 pub fn as_forecast_interval(&self) -> Option<&ForecastInterval> {
739 if let Self::ForecastInterval(v) = self {
740 Some(v)
741 } else {
742 None
743 }
744 }
745
746 #[inline]
750 #[must_use]
751 pub fn as_current_interval(&self) -> Option<&CurrentInterval> {
752 if let Self::CurrentInterval(v) = self {
753 Some(v)
754 } else {
755 None
756 }
757 }
758
759 #[inline]
761 #[must_use]
762 pub fn as_base_interval(&self) -> Option<&BaseInterval> {
763 match self {
764 Interval::ActualInterval(actual) => Some(&actual.base),
765 Interval::ForecastInterval(forecast) => Some(&forecast.base),
766 Interval::CurrentInterval(current) => Some(¤t.base),
767 }
768 }
769}
770
771impl fmt::Display for Interval {
772 #[inline]
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 match self {
775 Interval::ActualInterval(actual) => write!(f, "{actual}"),
776 Interval::ForecastInterval(forecast) => write!(f, "{forecast}"),
777 Interval::CurrentInterval(current) => write!(f, "{current}"),
778 }
779 }
780}
781
782#[derive(Debug, Clone, PartialEq, Deserialize)]
784#[serde(rename_all = "camelCase")]
785#[non_exhaustive]
786pub struct Usage {
787 #[serde(flatten)]
789 pub base: BaseInterval,
790 pub channel_identifier: String,
792 pub kwh: f64,
796 pub quality: UsageQuality,
798 pub cost: f64,
801}
802
803impl fmt::Display for Usage {
804 #[inline]
805 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
806 write!(
807 f,
808 "Usage {} {:.2}kWh ${:.2} ({})",
809 self.channel_identifier, self.kwh, self.cost, self.quality
810 )
811 }
812}
813
814#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
816#[serde(rename_all = "camelCase")]
817#[non_exhaustive]
818pub enum UsageQuality {
819 Estimated,
821 Billable,
823}
824
825impl fmt::Display for UsageQuality {
826 #[inline]
827 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828 match self {
829 UsageQuality::Estimated => write!(f, "estimated"),
830 UsageQuality::Billable => write!(f, "billable"),
831 }
832 }
833}
834
835#[derive(Debug, Clone, PartialEq, Deserialize)]
837#[serde(rename_all = "camelCase")]
838#[non_exhaustive]
839pub struct BaseRenewable {
840 pub duration: u32,
842 pub date: Date,
847 pub nem_time: Timestamp,
851 pub start_time: Timestamp,
853 pub end_time: Timestamp,
855 pub renewables: f64,
857 pub descriptor: RenewableDescriptor,
859}
860
861impl fmt::Display for BaseRenewable {
862 #[inline]
863 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
864 write!(
865 f,
866 "{} {}% renewable ({})",
867 self.date, self.renewables, self.descriptor
868 )
869 }
870}
871
872#[derive(Debug, Clone, PartialEq, Deserialize)]
874#[serde(rename_all = "camelCase")]
875#[non_exhaustive]
876pub struct ActualRenewable {
877 #[serde(flatten)]
879 pub base: BaseRenewable,
880}
881
882impl fmt::Display for ActualRenewable {
883 #[inline]
884 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885 write!(f, "Actual: {}", self.base)
886 }
887}
888
889#[derive(Debug, Clone, PartialEq, Deserialize)]
891#[serde(rename_all = "camelCase")]
892#[non_exhaustive]
893pub struct ForecastRenewable {
894 #[serde(flatten)]
896 pub base: BaseRenewable,
897}
898
899impl fmt::Display for ForecastRenewable {
900 #[inline]
901 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
902 write!(f, "Forecast: {}", self.base)
903 }
904}
905
906#[derive(Debug, Clone, PartialEq, Deserialize)]
908#[serde(rename_all = "camelCase")]
909#[non_exhaustive]
910pub struct CurrentRenewable {
911 #[serde(flatten)]
913 pub base: BaseRenewable,
914}
915
916impl fmt::Display for CurrentRenewable {
917 #[inline]
918 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919 write!(f, "Current: {}", self.base)
920 }
921}
922
923#[derive(Debug, Clone, PartialEq, Deserialize)]
925#[serde(tag = "type")]
926#[non_exhaustive]
927pub enum Renewable {
928 ActualRenewable(ActualRenewable),
930 ForecastRenewable(ForecastRenewable),
932 CurrentRenewable(CurrentRenewable),
934}
935
936impl Renewable {
937 #[must_use]
941 #[inline]
942 pub fn is_actual_renewable(&self) -> bool {
943 matches!(self, Self::ActualRenewable(..))
944 }
945
946 #[must_use]
950 #[inline]
951 pub fn is_forecast_renewable(&self) -> bool {
952 matches!(self, Self::ForecastRenewable(..))
953 }
954
955 #[must_use]
959 #[inline]
960 pub fn is_current_renewable(&self) -> bool {
961 matches!(self, Self::CurrentRenewable(..))
962 }
963
964 #[must_use]
968 #[inline]
969 pub fn as_actual_renewable(&self) -> Option<&ActualRenewable> {
970 if let Self::ActualRenewable(v) = self {
971 Some(v)
972 } else {
973 None
974 }
975 }
976
977 #[must_use]
981 #[inline]
982 pub fn as_forecast_renewable(&self) -> Option<&ForecastRenewable> {
983 if let Self::ForecastRenewable(v) = self {
984 Some(v)
985 } else {
986 None
987 }
988 }
989
990 #[must_use]
994 #[inline]
995 pub fn as_current_renewable(&self) -> Option<&CurrentRenewable> {
996 if let Self::CurrentRenewable(v) = self {
997 Some(v)
998 } else {
999 None
1000 }
1001 }
1002
1003 #[must_use]
1005 #[inline]
1006 pub fn as_base_renewable(&self) -> &BaseRenewable {
1007 match self {
1008 Self::ActualRenewable(actual) => &actual.base,
1009 Self::ForecastRenewable(forecast) => &forecast.base,
1010 Self::CurrentRenewable(current) => ¤t.base,
1011 }
1012 }
1013}
1014
1015impl fmt::Display for Renewable {
1016 #[inline]
1017 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1018 match self {
1019 Renewable::ActualRenewable(actual) => write!(f, "{actual}"),
1020 Renewable::ForecastRenewable(forecast) => write!(f, "{forecast}"),
1021 Renewable::CurrentRenewable(current) => write!(f, "{current}"),
1022 }
1023 }
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028 use super::*;
1029 use anyhow::Result;
1030 use pretty_assertions::assert_eq;
1031
1032 #[test]
1033 fn actual_renewable_deserialisation_strict() -> Result<()> {
1034 let json = r#"{
1035 "type": "ActualRenewable",
1036 "duration": 5,
1037 "date": "2021-05-05",
1038 "nemTime": "2021-05-06T12:30:00+10:00",
1039 "startTime": "2021-05-05T02:00:01Z",
1040 "endTime": "2021-05-05T02:30:00Z",
1041 "renewables": 45,
1042 "descriptor": "best"
1043 }"#;
1044
1045 let actual: ActualRenewable = serde_json::from_str(json)?;
1046 assert_eq!(actual.base.duration, 5);
1047 assert_eq!(actual.base.date.to_string(), "2021-05-05");
1048 assert!(44.0_f64 < actual.base.renewables && actual.base.renewables < 46.0_f64);
1049 assert_eq!(actual.base.descriptor, RenewableDescriptor::Best);
1050
1051 Ok(())
1052 }
1053
1054 #[test]
1055 fn actual_renewable_deserialisation() -> Result<()> {
1056 let json = r#"{
1057 "type": "ActualRenewable",
1058 "duration": 5,
1059 "date": "2021-05-05",
1060 "nemTime": "2021-05-06T12:30:00+10:00",
1061 "startTime": "2021-05-05T02:00:01Z",
1062 "endTime": "2021-05-05T02:30:00Z",
1063 "renewables": 45,
1064 "descriptor": "best"
1065 }"#;
1066
1067 let renewable: Renewable = serde_json::from_str(json)?;
1068 if let Renewable::ActualRenewable(actual) = renewable {
1069 assert_eq!(actual.base.duration, 5);
1070 assert_eq!(actual.base.date.to_string(), "2021-05-05");
1071 assert!(44.0_f64 < actual.base.renewables && actual.base.renewables < 46.0_f64);
1072 assert_eq!(actual.base.descriptor, RenewableDescriptor::Best);
1073 } else {
1074 panic!("Expected ActualRenewable variant");
1075 }
1076
1077 Ok(())
1078 }
1079
1080 #[test]
1081 fn current_renewable_deserialisation_strict() -> Result<()> {
1082 let json = r#"{
1083 "type": "CurrentRenewable",
1084 "duration": 5,
1085 "date": "2021-05-05",
1086 "nemTime": "2021-05-06T12:30:00+10:00",
1087 "startTime": "2021-05-05T02:00:01Z",
1088 "endTime": "2021-05-05T02:30:00Z",
1089 "renewables": 45,
1090 "descriptor": "best"
1091 }"#;
1092
1093 let current: CurrentRenewable = serde_json::from_str(json)?;
1094 assert_eq!(current.base.duration, 5);
1095 assert_eq!(current.base.date.to_string(), "2021-05-05");
1096 assert!(44.0_f64 < current.base.renewables && current.base.renewables < 46.0_f64);
1097 assert_eq!(current.base.descriptor, RenewableDescriptor::Best);
1098
1099 Ok(())
1100 }
1101
1102 #[test]
1103 fn current_renewable_deserialisation() -> Result<()> {
1104 let json = r#"{
1105 "type": "CurrentRenewable",
1106 "duration": 5,
1107 "date": "2021-05-05",
1108 "nemTime": "2021-05-06T12:30:00+10:00",
1109 "startTime": "2021-05-05T02:00:01Z",
1110 "endTime": "2021-05-05T02:30:00Z",
1111 "renewables": 45,
1112 "descriptor": "best"
1113 }"#;
1114
1115 let renewable: Renewable = serde_json::from_str(json)?;
1116 if let Renewable::CurrentRenewable(current) = renewable {
1117 assert_eq!(current.base.duration, 5);
1118 assert_eq!(current.base.date.to_string(), "2021-05-05");
1119 assert!(44.0_f64 < current.base.renewables && current.base.renewables < 46.0_f64);
1120 assert_eq!(current.base.descriptor, RenewableDescriptor::Best);
1121 } else {
1122 panic!("Expected CurrentRenewable variant");
1123 }
1124
1125 Ok(())
1126 }
1127
1128 #[test]
1129 fn forecast_renewable_deserialisation_strict() -> Result<()> {
1130 let json = r#"{
1131 "type": "ForecastRenewable",
1132 "duration": 5,
1133 "date": "2021-05-05",
1134 "nemTime": "2021-05-06T12:30:00+10:00",
1135 "startTime": "2021-05-05T02:00:01Z",
1136 "endTime": "2021-05-05T02:30:00Z",
1137 "renewables": 45,
1138 "descriptor": "best"
1139 }"#;
1140
1141 let forecast: ForecastRenewable = serde_json::from_str(json)?;
1142 assert_eq!(forecast.base.duration, 5);
1143 assert_eq!(forecast.base.date.to_string(), "2021-05-05");
1144 assert!(44.0_f64 < forecast.base.renewables && forecast.base.renewables < 46.0_f64);
1145 assert_eq!(forecast.base.descriptor, RenewableDescriptor::Best);
1146
1147 Ok(())
1148 }
1149
1150 #[test]
1151 fn forecast_renewable_deserialisation() -> Result<()> {
1152 let json = r#"{
1153 "type": "ForecastRenewable",
1154 "duration": 5,
1155 "date": "2021-05-05",
1156 "nemTime": "2021-05-06T12:30:00+10:00",
1157 "startTime": "2021-05-05T02:00:01Z",
1158 "endTime": "2021-05-05T02:30:00Z",
1159 "renewables": 45,
1160 "descriptor": "best"
1161 }"#;
1162
1163 let renewable: Renewable = serde_json::from_str(json)?;
1164 if let Renewable::ForecastRenewable(forecast) = renewable {
1165 assert_eq!(forecast.base.duration, 5);
1166 assert_eq!(forecast.base.date.to_string(), "2021-05-05");
1167 assert!(44.0_f64 < forecast.base.renewables && forecast.base.renewables < 46.0_f64);
1168 assert_eq!(forecast.base.descriptor, RenewableDescriptor::Best);
1169 } else {
1170 panic!("Expected ForecastRenewable variant");
1171 }
1172
1173 Ok(())
1174 }
1175
1176 #[test]
1178 fn site_deserialisation() -> Result<()> {
1179 let json = r#"[
1180 {
1181 "id": "01F5A5CRKMZ5BCX9P1S4V990AM",
1182 "nmi": "3052282872",
1183 "channels": [
1184 {
1185 "identifier": "E1",
1186 "type": "general",
1187 "tariff": "A100"
1188 }
1189 ],
1190 "network": "Jemena",
1191 "status": "closed",
1192 "activeFrom": "2022-01-01",
1193 "closedOn": "2022-05-01",
1194 "intervalLength": 30
1195 }
1196 ]"#;
1197
1198 let sites: Vec<Site> = serde_json::from_str(json)?;
1199 assert_eq!(sites.len(), 1);
1200
1201 let site = sites.first().expect("Expected at least one site");
1202 assert_eq!(site.id, "01F5A5CRKMZ5BCX9P1S4V990AM");
1203 assert_eq!(site.nmi, "3052282872");
1204 assert_eq!(site.channels.len(), 1);
1205
1206 let channel = site
1207 .channels
1208 .first()
1209 .expect("Expected at least one channel");
1210 assert_eq!(channel.identifier, "E1");
1211 assert_eq!(channel.channel_type, ChannelType::General);
1212 assert_eq!(channel.tariff, "A100");
1213
1214 assert_eq!(site.network, "Jemena");
1215 assert_eq!(site.status, SiteStatus::Closed);
1216 assert_eq!(
1217 site.active_from
1218 .expect("Expected active_from date")
1219 .to_string(),
1220 "2022-01-01"
1221 );
1222 assert_eq!(
1223 site.closed_on.expect("Expected closed_on date").to_string(),
1224 "2022-05-01"
1225 );
1226 assert_eq!(site.interval_length, 30);
1227
1228 Ok(())
1229 }
1230
1231 #[test]
1233 #[expect(
1234 clippy::too_many_lines,
1235 reason = "Comprehensive test for all interval types"
1236 )]
1237 fn prices_interval_deserialisation() -> Result<()> {
1238 let json = r#"[
1239 {
1240 "type": "ActualInterval",
1241 "duration": 5,
1242 "spotPerKwh": 6.12,
1243 "perKwh": 24.33,
1244 "date": "2021-05-05",
1245 "nemTime": "2021-05-06T12:30:00+10:00",
1246 "startTime": "2021-05-05T02:00:01Z",
1247 "endTime": "2021-05-05T02:30:00Z",
1248 "renewables": 45,
1249 "channelType": "general",
1250 "tariffInformation": null,
1251 "spikeStatus": "none",
1252 "descriptor": "negative"
1253 },
1254 {
1255 "type": "CurrentInterval",
1256 "duration": 5,
1257 "spotPerKwh": 6.12,
1258 "perKwh": 24.33,
1259 "date": "2021-05-05",
1260 "nemTime": "2021-05-06T12:30:00+10:00",
1261 "startTime": "2021-05-05T02:00:01Z",
1262 "endTime": "2021-05-05T02:30:00Z",
1263 "renewables": 45,
1264 "channelType": "general",
1265 "tariffInformation": null,
1266 "spikeStatus": "none",
1267 "descriptor": "negative",
1268 "range": {
1269 "min": 0,
1270 "max": 0
1271 },
1272 "estimate": true,
1273 "advancedPrice": {
1274 "low": 1,
1275 "predicted": 3,
1276 "high": 10
1277 }
1278 },
1279 {
1280 "type": "ForecastInterval",
1281 "duration": 5,
1282 "spotPerKwh": 6.12,
1283 "perKwh": 24.33,
1284 "date": "2021-05-05",
1285 "nemTime": "2021-05-06T12:30:00+10:00",
1286 "startTime": "2021-05-05T02:00:01Z",
1287 "endTime": "2021-05-05T02:30:00Z",
1288 "renewables": 45,
1289 "channelType": "general",
1290 "tariffInformation": null,
1291 "spikeStatus": "none",
1292 "descriptor": "negative",
1293 "range": {
1294 "min": 0,
1295 "max": 0
1296 },
1297 "advancedPrice": {
1298 "low": 1,
1299 "predicted": 3,
1300 "high": 10
1301 }
1302 }
1303 ]"#;
1304
1305 let intervals: Vec<Interval> = serde_json::from_str(json)?;
1306 assert_eq!(intervals.len(), 3);
1307
1308 if let Some(Interval::ActualInterval(actual)) = intervals.first() {
1310 assert_eq!(actual.base.duration, 5);
1311 assert!((actual.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1312 assert!((actual.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1313 assert_eq!(actual.base.date.to_string(), "2021-05-05");
1314 assert!((actual.base.renewables - 45.0_f64).abs() < f64::EPSILON);
1315 assert_eq!(actual.base.channel_type, ChannelType::General);
1316 assert_eq!(actual.base.spike_status, SpikeStatus::None);
1317 assert_eq!(actual.base.descriptor, PriceDescriptor::Negative);
1318 } else {
1319 panic!("Expected ActualInterval at index 0");
1320 }
1321
1322 if let Some(Interval::CurrentInterval(current)) = intervals.get(1) {
1324 assert_eq!(current.base.duration, 5);
1325 assert!((current.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1326 assert!((current.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1327 assert_eq!(current.estimate, true);
1328 assert!(current.range.is_some());
1329 assert!(current.advanced_price.is_some());
1330
1331 if let Some(ref range) = current.range {
1332 assert!((range.min - 0.0_f64).abs() < f64::EPSILON);
1333 assert!((range.max - 0.0_f64).abs() < f64::EPSILON);
1334 }
1335
1336 if let Some(ref adv_price) = current.advanced_price {
1337 assert!((adv_price.low - 1.0_f64).abs() < f64::EPSILON);
1338 assert!((adv_price.predicted - 3.0_f64).abs() < f64::EPSILON);
1339 assert!((adv_price.high - 10.0_f64).abs() < f64::EPSILON);
1340 }
1341 } else {
1342 panic!("Expected CurrentInterval at index 1");
1343 }
1344
1345 if let Some(Interval::ForecastInterval(forecast)) = intervals.get(2) {
1347 assert_eq!(forecast.base.duration, 5);
1348 assert!((forecast.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1349 assert!((forecast.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1350 assert!(forecast.range.is_some());
1351 assert!(forecast.advanced_price.is_some());
1352 } else {
1353 panic!("Expected ForecastInterval at index 2");
1354 }
1355
1356 Ok(())
1357 }
1358
1359 #[test]
1361 fn current_prices_interval_deserialisation() -> Result<()> {
1362 let json = r#"[
1363 {
1364 "type": "ActualInterval",
1365 "duration": 5,
1366 "spotPerKwh": 6.12,
1367 "perKwh": 24.33,
1368 "date": "2021-05-05",
1369 "nemTime": "2021-05-06T12:30:00+10:00",
1370 "startTime": "2021-05-05T02:00:01Z",
1371 "endTime": "2021-05-05T02:30:00Z",
1372 "renewables": 45,
1373 "channelType": "general",
1374 "tariffInformation": null,
1375 "spikeStatus": "none",
1376 "descriptor": "negative"
1377 },
1378 {
1379 "type": "CurrentInterval",
1380 "duration": 5,
1381 "spotPerKwh": 6.12,
1382 "perKwh": 24.33,
1383 "date": "2021-05-05",
1384 "nemTime": "2021-05-06T12:30:00+10:00",
1385 "startTime": "2021-05-05T02:00:01Z",
1386 "endTime": "2021-05-05T02:30:00Z",
1387 "renewables": 45,
1388 "channelType": "general",
1389 "tariffInformation": null,
1390 "spikeStatus": "none",
1391 "descriptor": "negative",
1392 "range": {
1393 "min": 0,
1394 "max": 0
1395 },
1396 "estimate": true,
1397 "advancedPrice": {
1398 "low": 1,
1399 "predicted": 3,
1400 "high": 10
1401 }
1402 },
1403 {
1404 "type": "ForecastInterval",
1405 "duration": 5,
1406 "spotPerKwh": 6.12,
1407 "perKwh": 24.33,
1408 "date": "2021-05-05",
1409 "nemTime": "2021-05-06T12:30:00+10:00",
1410 "startTime": "2021-05-05T02:00:01Z",
1411 "endTime": "2021-05-05T02:30:00Z",
1412 "renewables": 45,
1413 "channelType": "general",
1414 "tariffInformation": null,
1415 "spikeStatus": "none",
1416 "descriptor": "negative",
1417 "range": {
1418 "min": 0,
1419 "max": 0
1420 },
1421 "advancedPrice": {
1422 "low": 1,
1423 "predicted": 3,
1424 "high": 10
1425 }
1426 }
1427 ]"#;
1428
1429 let intervals: Vec<Interval> = serde_json::from_str(json)?;
1430 assert_eq!(intervals.len(), 3);
1431
1432 let first_interval = intervals.first().expect("Expected at least one interval");
1434 let second_interval = intervals.get(1).expect("Expected at least two intervals");
1435 let third_interval = intervals.get(2).expect("Expected at least three intervals");
1436
1437 assert!(matches!(first_interval, Interval::ActualInterval(_)));
1438 assert!(matches!(second_interval, Interval::CurrentInterval(_)));
1439 assert!(matches!(third_interval, Interval::ForecastInterval(_)));
1440
1441 Ok(())
1442 }
1443
1444 #[test]
1446 fn usage_deserialisation() -> Result<()> {
1447 let json = r#"[
1448 {
1449 "type": "Usage",
1450 "duration": 5,
1451 "spotPerKwh": 6.12,
1452 "perKwh": 24.33,
1453 "date": "2021-05-05",
1454 "nemTime": "2021-05-06T12:30:00+10:00",
1455 "startTime": "2021-05-05T02:00:01Z",
1456 "endTime": "2021-05-05T02:30:00Z",
1457 "renewables": 45,
1458 "channelType": "general",
1459 "tariffInformation": null,
1460 "spikeStatus": "none",
1461 "descriptor": "negative",
1462 "channelIdentifier": "E1",
1463 "kwh": 0,
1464 "quality": "estimated",
1465 "cost": 0
1466 }
1467 ]"#;
1468
1469 let usage_data: Vec<Usage> = serde_json::from_str(json)?;
1470 assert_eq!(usage_data.len(), 1);
1471
1472 let usage = usage_data
1473 .first()
1474 .expect("Expected at least one usage entry");
1475 assert_eq!(usage.base.duration, 5);
1476 assert!((usage.base.spot_per_kwh - 6.12_f64).abs() < f64::EPSILON);
1477 assert!((usage.base.per_kwh - 24.33_f64).abs() < f64::EPSILON);
1478 assert_eq!(usage.base.date.to_string(), "2021-05-05");
1479 assert!((usage.base.renewables - 45.0_f64).abs() < f64::EPSILON);
1480 assert_eq!(usage.base.channel_type, ChannelType::General);
1481 assert_eq!(usage.base.spike_status, SpikeStatus::None);
1482 assert_eq!(usage.base.descriptor, PriceDescriptor::Negative);
1483 assert_eq!(usage.channel_identifier, "E1");
1484 assert!((usage.kwh - 0.0_f64).abs() < f64::EPSILON);
1485 assert_eq!(usage.quality, UsageQuality::Estimated);
1486 assert!((usage.cost - 0.0_f64).abs() < f64::EPSILON);
1487
1488 Ok(())
1489 }
1490
1491 #[test]
1493 fn channel_types_deserialisation() -> Result<()> {
1494 let general_json = r#"{"identifier": "E1", "type": "general", "tariff": "A100"}"#;
1496 let controlled_json = r#"{"identifier": "E2", "type": "controlledLoad", "tariff": "A200"}"#;
1497 let feedin_json = r#"{"identifier": "E3", "type": "feedIn", "tariff": "A300"}"#;
1498
1499 let general: Channel = serde_json::from_str(general_json)?;
1500 let controlled: Channel = serde_json::from_str(controlled_json)?;
1501 let feedin: Channel = serde_json::from_str(feedin_json)?;
1502
1503 assert_eq!(general.channel_type, ChannelType::General);
1504 assert_eq!(controlled.channel_type, ChannelType::ControlledLoad);
1505 assert_eq!(feedin.channel_type, ChannelType::FeedIn);
1506
1507 Ok(())
1508 }
1509
1510 #[test]
1511 fn site_status_deserialisation() -> Result<()> {
1512 #[derive(Deserialize)]
1513 struct TestSiteStatus {
1514 status: SiteStatus,
1515 }
1516
1517 let pending_json = r#"{"status": "pending"}"#;
1519 let active_json = r#"{"status": "active"}"#;
1520 let closed_json = r#"{"status": "closed"}"#;
1521
1522 let pending: TestSiteStatus = serde_json::from_str(pending_json)?;
1523 let active: TestSiteStatus = serde_json::from_str(active_json)?;
1524 let closed: TestSiteStatus = serde_json::from_str(closed_json)?;
1525
1526 assert_eq!(pending.status, SiteStatus::Pending);
1527 assert_eq!(active.status, SiteStatus::Active);
1528 assert_eq!(closed.status, SiteStatus::Closed);
1529
1530 Ok(())
1531 }
1532
1533 #[test]
1534 fn range_and_advanced_price_deserialisation() -> Result<()> {
1535 let range_json = r#"{"min": 0, "max": 100}"#;
1536 let advanced_price_json = r#"{"low": 1, "predicted": 3, "high": 10}"#;
1537
1538 let range: Range = serde_json::from_str(range_json)?;
1539 let advanced_price: AdvancedPrice = serde_json::from_str(advanced_price_json)?;
1540
1541 assert!((range.min - 0.0_f64).abs() < f64::EPSILON);
1542 assert!((range.max - 100.0_f64).abs() < f64::EPSILON);
1543 assert!((advanced_price.low - 1.0_f64).abs() < f64::EPSILON);
1544 assert!((advanced_price.predicted - 3.0_f64).abs() < f64::EPSILON);
1545 assert!((advanced_price.high - 10.0_f64).abs() < f64::EPSILON);
1546
1547 Ok(())
1548 }
1549
1550 #[test]
1551 fn usage_quality_deserialisation() -> Result<()> {
1552 #[derive(Deserialize)]
1553 struct TestUsageQuality {
1554 quality: UsageQuality,
1555 }
1556
1557 let estimated_json = r#"{"quality": "estimated"}"#;
1558 let billable_json = r#"{"quality": "billable"}"#;
1559
1560 let estimated: TestUsageQuality = serde_json::from_str(estimated_json)?;
1561 let billable: TestUsageQuality = serde_json::from_str(billable_json)?;
1562
1563 assert_eq!(estimated.quality, UsageQuality::Estimated);
1564 assert_eq!(billable.quality, UsageQuality::Billable);
1565
1566 Ok(())
1567 }
1568
1569 #[test]
1571 fn display_state() {
1572 insta::assert_snapshot!(State::Nsw.to_string(), @"nsw");
1573 insta::assert_snapshot!(State::Vic.to_string(), @"vic");
1574 insta::assert_snapshot!(State::Qld.to_string(), @"qld");
1575 insta::assert_snapshot!(State::Sa.to_string(), @"sa");
1576 }
1577
1578 #[test]
1579 fn display_resolution() {
1580 insta::assert_snapshot!(Resolution::FiveMinute.to_string(), @"5");
1581 insta::assert_snapshot!(Resolution::ThirtyMinute.to_string(), @"30");
1582 }
1583
1584 #[test]
1585 fn display_channel_type() {
1586 insta::assert_snapshot!(ChannelType::General.to_string(), @"general");
1587 insta::assert_snapshot!(ChannelType::ControlledLoad.to_string(), @"controlled load");
1588 insta::assert_snapshot!(ChannelType::FeedIn.to_string(), @"feed-in");
1589 }
1590
1591 #[test]
1592 fn display_channel() {
1593 let channel = Channel {
1594 identifier: "E1".to_owned(),
1595 channel_type: ChannelType::General,
1596 tariff: "A100".to_owned(),
1597 };
1598 insta::assert_snapshot!(channel.to_string(), @"E1 (general): A100");
1599 }
1600
1601 #[test]
1602 fn display_site_status() {
1603 insta::assert_snapshot!(SiteStatus::Pending.to_string(), @"pending");
1604 insta::assert_snapshot!(SiteStatus::Active.to_string(), @"active");
1605 insta::assert_snapshot!(SiteStatus::Closed.to_string(), @"closed");
1606 }
1607
1608 #[test]
1609 fn display_site() {
1610 use jiff::civil::Date;
1611 let site = Site {
1612 id: "01F5A5CRKMZ5BCX9P1S4V990AM".to_owned(),
1613 nmi: "3052282872".to_owned(),
1614 channels: vec![],
1615 network: "Jemena".to_owned(),
1616 status: SiteStatus::Active,
1617 active_from: Some(Date::constant(2022, 1, 1)),
1618 closed_on: None,
1619 interval_length: 30,
1620 };
1621 insta::assert_snapshot!(site.to_string(), @"Site 01F5A5CRKMZ5BCX9P1S4V990AM (NMI: 3052282872) - active on Jemena network");
1622 }
1623
1624 #[test]
1625 fn display_spike_status() {
1626 insta::assert_snapshot!(SpikeStatus::None.to_string(), @"none");
1627 insta::assert_snapshot!(SpikeStatus::Potential.to_string(), @"potential");
1628 insta::assert_snapshot!(SpikeStatus::Spike.to_string(), @"spike");
1629 }
1630
1631 #[test]
1632 fn display_price_descriptor() {
1633 insta::assert_snapshot!(PriceDescriptor::Negative.to_string(), @"negative");
1634 insta::assert_snapshot!(PriceDescriptor::ExtremelyLow.to_string(), @"extremely low");
1635 insta::assert_snapshot!(PriceDescriptor::VeryLow.to_string(), @"very low");
1636 insta::assert_snapshot!(PriceDescriptor::Low.to_string(), @"low");
1637 insta::assert_snapshot!(PriceDescriptor::Neutral.to_string(), @"neutral");
1638 insta::assert_snapshot!(PriceDescriptor::High.to_string(), @"high");
1639 insta::assert_snapshot!(PriceDescriptor::Spike.to_string(), @"spike");
1640 }
1641
1642 #[test]
1643 fn display_renewable_descriptor() {
1644 insta::assert_snapshot!(RenewableDescriptor::Best.to_string(), @"best");
1645 insta::assert_snapshot!(RenewableDescriptor::Great.to_string(), @"great");
1646 insta::assert_snapshot!(RenewableDescriptor::Ok.to_string(), @"ok");
1647 insta::assert_snapshot!(RenewableDescriptor::NotGreat.to_string(), @"not great");
1648 insta::assert_snapshot!(RenewableDescriptor::Worst.to_string(), @"worst");
1649 }
1650
1651 #[test]
1652 fn display_range() {
1653 let range = Range {
1654 min: 12.34,
1655 max: 56.78,
1656 };
1657 insta::assert_snapshot!(range.to_string(), @"12.34-56.78c/kWh");
1658 }
1659
1660 #[test]
1661 fn display_advanced_price() {
1662 let advanced_price = AdvancedPrice {
1663 low: 1.23,
1664 predicted: 4.56,
1665 high: 7.89,
1666 };
1667 insta::assert_snapshot!(advanced_price.to_string(), @"L:1.23 H:4.56 P:7.89 c/kWh");
1668 }
1669
1670 #[test]
1671 fn display_tariff_period() {
1672 insta::assert_snapshot!(TariffPeriod::OffPeak.to_string(), @"off peak");
1673 insta::assert_snapshot!(TariffPeriod::Shoulder.to_string(), @"shoulder");
1674 insta::assert_snapshot!(TariffPeriod::SolarSponge.to_string(), @"solar sponge");
1675 insta::assert_snapshot!(TariffPeriod::Peak.to_string(), @"peak");
1676 }
1677
1678 #[test]
1679 fn display_tariff_season() {
1680 insta::assert_snapshot!(TariffSeason::Default.to_string(), @"default");
1681 insta::assert_snapshot!(TariffSeason::Summer.to_string(), @"summer");
1682 insta::assert_snapshot!(TariffSeason::Autumn.to_string(), @"autumn");
1683 insta::assert_snapshot!(TariffSeason::Winter.to_string(), @"winter");
1684 insta::assert_snapshot!(TariffSeason::Spring.to_string(), @"spring");
1685 insta::assert_snapshot!(TariffSeason::NonSummer.to_string(), @"non summer");
1686 insta::assert_snapshot!(TariffSeason::Holiday.to_string(), @"holiday");
1687 insta::assert_snapshot!(TariffSeason::Weekend.to_string(), @"weekend");
1688 insta::assert_snapshot!(TariffSeason::WeekendHoliday.to_string(), @"weekend holiday");
1689 insta::assert_snapshot!(TariffSeason::Weekday.to_string(), @"weekday");
1690 }
1691
1692 #[test]
1693 fn display_tariff_information() {
1694 let empty_tariff = TariffInformation {
1696 period: None,
1697 season: None,
1698 block: None,
1699 demand_window: None,
1700 };
1701 insta::assert_snapshot!(empty_tariff.to_string(), @"No tariff information");
1702
1703 let full_tariff = TariffInformation {
1705 period: Some(TariffPeriod::Peak),
1706 season: Some(TariffSeason::Summer),
1707 block: Some(2),
1708 demand_window: Some(true),
1709 };
1710 insta::assert_snapshot!(full_tariff.to_string(), @"period:peak, season:summer, block:2, demand window:true");
1711
1712 let partial_tariff = TariffInformation {
1714 period: Some(TariffPeriod::OffPeak),
1715 season: None,
1716 block: Some(1),
1717 demand_window: Some(false),
1718 };
1719 insta::assert_snapshot!(partial_tariff.to_string(), @"period:off peak, block:1, demand window:false");
1720 }
1721
1722 #[test]
1723 fn display_base_interval() {
1724 use jiff::{Timestamp, civil::Date};
1725 let nem_time = "2021-05-06T12:30:00+10:00"
1727 .parse::<Timestamp>()
1728 .expect("valid timestamp");
1729 let start_time = "2021-05-05T02:00:01Z"
1730 .parse::<Timestamp>()
1731 .expect("valid timestamp");
1732 let end_time = "2021-05-05T02:30:00Z"
1733 .parse::<Timestamp>()
1734 .expect("valid timestamp");
1735
1736 let base_interval_basic = BaseInterval {
1738 duration: 5,
1739 spot_per_kwh: 6.12,
1740 per_kwh: 24.33,
1741 date: Date::constant(2021, 5, 5),
1742 nem_time,
1743 start_time,
1744 end_time,
1745 renewables: 45.5,
1746 channel_type: ChannelType::General,
1747 tariff_information: None,
1748 spike_status: SpikeStatus::None,
1749 descriptor: PriceDescriptor::Low,
1750 };
1751 insta::assert_snapshot!(base_interval_basic.to_string(), @"2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (low) 45.5% renewable");
1752
1753 let base_interval_potential_spike = BaseInterval {
1755 duration: 5,
1756 spot_per_kwh: 6.12,
1757 per_kwh: 24.33,
1758 date: Date::constant(2021, 5, 5),
1759 nem_time,
1760 start_time,
1761 end_time,
1762 renewables: 45.5,
1763 channel_type: ChannelType::General,
1764 tariff_information: None,
1765 spike_status: SpikeStatus::Potential,
1766 descriptor: PriceDescriptor::High,
1767 };
1768 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");
1769
1770 let base_interval_spike = BaseInterval {
1772 duration: 5,
1773 spot_per_kwh: 100.50,
1774 per_kwh: 120.75,
1775 date: Date::constant(2021, 5, 5),
1776 nem_time,
1777 start_time,
1778 end_time,
1779 renewables: 25.0,
1780 channel_type: ChannelType::General,
1781 tariff_information: None,
1782 spike_status: SpikeStatus::Spike,
1783 descriptor: PriceDescriptor::Spike,
1784 };
1785 insta::assert_snapshot!(base_interval_spike.to_string(), @"2021-05-05 general 120.75c/kWh (spot: 100.50c/kWh) (spike) 25% renewable spike: spike");
1786
1787 let tariff_info = TariffInformation {
1789 period: Some(TariffPeriod::Peak),
1790 season: Some(TariffSeason::Summer),
1791 block: Some(2),
1792 demand_window: Some(true),
1793 };
1794 let base_interval_tariff = BaseInterval {
1795 duration: 30,
1796 spot_per_kwh: 15.20,
1797 per_kwh: 35.40,
1798 date: Date::constant(2021, 7, 15),
1799 nem_time,
1800 start_time,
1801 end_time,
1802 renewables: 30.2,
1803 channel_type: ChannelType::ControlledLoad,
1804 tariff_information: Some(tariff_info),
1805 spike_status: SpikeStatus::None,
1806 descriptor: PriceDescriptor::Neutral,
1807 };
1808 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]");
1809
1810 let tariff_info_combined = TariffInformation {
1812 period: Some(TariffPeriod::OffPeak),
1813 season: None,
1814 block: None,
1815 demand_window: Some(false),
1816 };
1817 let base_interval_combined = BaseInterval {
1818 duration: 5,
1819 spot_per_kwh: 8.75,
1820 per_kwh: 28.90,
1821 date: Date::constant(2021, 12, 25),
1822 nem_time,
1823 start_time,
1824 end_time,
1825 renewables: 60.8,
1826 channel_type: ChannelType::FeedIn,
1827 tariff_information: Some(tariff_info_combined),
1828 spike_status: SpikeStatus::Potential,
1829 descriptor: PriceDescriptor::VeryLow,
1830 };
1831 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]");
1832 }
1833
1834 #[test]
1835 fn display_actual_interval() {
1836 use jiff::{Timestamp, civil::Date};
1837 let nem_time = "2021-05-06T12:30:00+10:00"
1838 .parse::<Timestamp>()
1839 .expect("valid timestamp");
1840 let start_time = "2021-05-05T02:00:01Z"
1841 .parse::<Timestamp>()
1842 .expect("valid timestamp");
1843 let end_time = "2021-05-05T02:30:00Z"
1844 .parse::<Timestamp>()
1845 .expect("valid timestamp");
1846
1847 let actual_interval = ActualInterval {
1848 base: BaseInterval {
1849 duration: 5,
1850 spot_per_kwh: 6.12,
1851 per_kwh: 24.33,
1852 date: Date::constant(2021, 5, 5),
1853 nem_time,
1854 start_time,
1855 end_time,
1856 renewables: 45.5,
1857 channel_type: ChannelType::General,
1858 tariff_information: None,
1859 spike_status: SpikeStatus::None,
1860 descriptor: PriceDescriptor::Low,
1861 },
1862 };
1863 insta::assert_snapshot!(actual_interval.to_string(), @"Actual: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (low) 45.5% renewable");
1864 }
1865
1866 #[test]
1867 fn display_forecast_interval() {
1868 use jiff::{Timestamp, civil::Date};
1869 let nem_time = "2021-05-06T12:30:00+10:00"
1870 .parse::<Timestamp>()
1871 .expect("valid timestamp");
1872 let start_time = "2021-05-05T02:00:01Z"
1873 .parse::<Timestamp>()
1874 .expect("valid timestamp");
1875 let end_time = "2021-05-05T02:30:00Z"
1876 .parse::<Timestamp>()
1877 .expect("valid timestamp");
1878
1879 let forecast_interval = ForecastInterval {
1880 base: BaseInterval {
1881 duration: 5,
1882 spot_per_kwh: 6.12,
1883 per_kwh: 24.33,
1884 date: Date::constant(2021, 5, 5),
1885 nem_time,
1886 start_time,
1887 end_time,
1888 renewables: 45.5,
1889 channel_type: ChannelType::General,
1890 tariff_information: None,
1891 spike_status: SpikeStatus::Potential,
1892 descriptor: PriceDescriptor::High,
1893 },
1894 range: Some(Range {
1895 min: 10.0,
1896 max: 30.0,
1897 }),
1898 advanced_price: Some(AdvancedPrice {
1899 low: 15.0,
1900 predicted: 20.0,
1901 high: 25.0,
1902 }),
1903 };
1904 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");
1905 }
1906
1907 #[test]
1908 fn display_current_interval() {
1909 use jiff::{Timestamp, civil::Date};
1910 let nem_time = "2021-05-06T12:30:00+10:00"
1911 .parse::<Timestamp>()
1912 .expect("valid timestamp");
1913 let start_time = "2021-05-05T02:00:01Z"
1914 .parse::<Timestamp>()
1915 .expect("valid timestamp");
1916 let end_time = "2021-05-05T02:30:00Z"
1917 .parse::<Timestamp>()
1918 .expect("valid timestamp");
1919
1920 let current_interval = CurrentInterval {
1921 base: BaseInterval {
1922 duration: 5,
1923 spot_per_kwh: 6.12,
1924 per_kwh: 24.33,
1925 date: Date::constant(2021, 5, 5),
1926 nem_time,
1927 start_time,
1928 end_time,
1929 renewables: 45.5,
1930 channel_type: ChannelType::FeedIn,
1931 tariff_information: None,
1932 spike_status: SpikeStatus::Spike,
1933 descriptor: PriceDescriptor::Spike,
1934 },
1935 range: Some(Range {
1936 min: 50.0,
1937 max: 100.0,
1938 }),
1939 estimate: true,
1940 advanced_price: Some(AdvancedPrice {
1941 low: 60.0,
1942 predicted: 75.0,
1943 high: 90.0,
1944 }),
1945 };
1946 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");
1947 }
1948
1949 #[test]
1950 fn display_interval_enum() {
1951 use jiff::{Timestamp, civil::Date};
1952 let nem_time = "2021-05-06T12:30:00+10:00"
1953 .parse::<Timestamp>()
1954 .expect("valid timestamp");
1955 let start_time = "2021-05-05T02:00:01Z"
1956 .parse::<Timestamp>()
1957 .expect("valid timestamp");
1958 let end_time = "2021-05-05T02:30:00Z"
1959 .parse::<Timestamp>()
1960 .expect("valid timestamp");
1961
1962 let base = BaseInterval {
1963 duration: 5,
1964 spot_per_kwh: 6.12,
1965 per_kwh: 24.33,
1966 date: Date::constant(2021, 5, 5),
1967 nem_time,
1968 start_time,
1969 end_time,
1970 renewables: 45.5,
1971 channel_type: ChannelType::General,
1972 tariff_information: None,
1973 spike_status: SpikeStatus::None,
1974 descriptor: PriceDescriptor::Neutral,
1975 };
1976
1977 let actual_interval = Interval::ActualInterval(ActualInterval { base: base.clone() });
1978 let forecast_interval = Interval::ForecastInterval(ForecastInterval {
1979 base: base.clone(),
1980 range: None,
1981 advanced_price: None,
1982 });
1983 let current_interval = Interval::CurrentInterval(CurrentInterval {
1984 base,
1985 range: None,
1986 estimate: false,
1987 advanced_price: None,
1988 });
1989
1990 insta::assert_snapshot!(actual_interval.to_string(), @"Actual: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (neutral) 45.5% renewable");
1991 insta::assert_snapshot!(forecast_interval.to_string(), @"Forecast: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (neutral) 45.5% renewable");
1992 insta::assert_snapshot!(current_interval.to_string(), @"Current: 2021-05-05 general 24.33c/kWh (spot: 6.12c/kWh) (neutral) 45.5% renewable");
1993 }
1994
1995 #[test]
1996 fn display_usage_quality() {
1997 insta::assert_snapshot!(UsageQuality::Estimated.to_string(), @"estimated");
1998 insta::assert_snapshot!(UsageQuality::Billable.to_string(), @"billable");
1999 }
2000
2001 #[test]
2002 fn display_usage() {
2003 use jiff::{Timestamp, civil::Date};
2004 let nem_time = "2021-05-06T12:30:00+10:00"
2005 .parse::<Timestamp>()
2006 .expect("valid timestamp");
2007 let start_time = "2021-05-05T02:00:01Z"
2008 .parse::<Timestamp>()
2009 .expect("valid timestamp");
2010 let end_time = "2021-05-05T02:30:00Z"
2011 .parse::<Timestamp>()
2012 .expect("valid timestamp");
2013
2014 let usage = Usage {
2015 base: BaseInterval {
2016 duration: 5,
2017 spot_per_kwh: 6.12,
2018 per_kwh: 24.33,
2019 date: Date::constant(2021, 5, 5),
2020 nem_time,
2021 start_time,
2022 end_time,
2023 renewables: 45.5,
2024 channel_type: ChannelType::General,
2025 tariff_information: None,
2026 spike_status: SpikeStatus::None,
2027 descriptor: PriceDescriptor::Low,
2028 },
2029 channel_identifier: "E1".to_owned(),
2030 kwh: 1.25,
2031 quality: UsageQuality::Billable,
2032 cost: 30.41,
2033 };
2034 insta::assert_snapshot!(usage.to_string(), @"Usage E1 1.25kWh $30.41 (billable)");
2035 }
2036
2037 #[test]
2038 fn display_base_renewable() {
2039 use jiff::{Timestamp, civil::Date};
2040 let nem_time = "2021-05-06T12:30:00+10:00"
2041 .parse::<Timestamp>()
2042 .expect("valid timestamp");
2043 let start_time = "2021-05-05T02:00:01Z"
2044 .parse::<Timestamp>()
2045 .expect("valid timestamp");
2046 let end_time = "2021-05-05T02:30:00Z"
2047 .parse::<Timestamp>()
2048 .expect("valid timestamp");
2049
2050 let base_renewable = BaseRenewable {
2051 duration: 5,
2052 date: Date::constant(2021, 5, 5),
2053 nem_time,
2054 start_time,
2055 end_time,
2056 renewables: 78.5,
2057 descriptor: RenewableDescriptor::Great,
2058 };
2059 insta::assert_snapshot!(base_renewable.to_string(), @"2021-05-05 78.5% renewable (great)");
2060 }
2061
2062 #[test]
2063 fn display_actual_renewable() {
2064 use jiff::{Timestamp, civil::Date};
2065 let nem_time = "2021-05-06T12:30:00+10:00"
2066 .parse::<Timestamp>()
2067 .expect("valid timestamp");
2068 let start_time = "2021-05-05T02:00:01Z"
2069 .parse::<Timestamp>()
2070 .expect("valid timestamp");
2071 let end_time = "2021-05-05T02:30:00Z"
2072 .parse::<Timestamp>()
2073 .expect("valid timestamp");
2074
2075 let actual_renewable = ActualRenewable {
2076 base: BaseRenewable {
2077 duration: 5,
2078 date: Date::constant(2021, 5, 5),
2079 nem_time,
2080 start_time,
2081 end_time,
2082 renewables: 78.5,
2083 descriptor: RenewableDescriptor::Great,
2084 },
2085 };
2086 insta::assert_snapshot!(actual_renewable.to_string(), @"Actual: 2021-05-05 78.5% renewable (great)");
2087 }
2088
2089 #[test]
2090 fn display_forecast_renewable() {
2091 use jiff::{Timestamp, civil::Date};
2092 let nem_time = "2021-05-06T12:30:00+10:00"
2093 .parse::<Timestamp>()
2094 .expect("valid timestamp");
2095 let start_time = "2021-05-05T02:00:01Z"
2096 .parse::<Timestamp>()
2097 .expect("valid timestamp");
2098 let end_time = "2021-05-05T02:30:00Z"
2099 .parse::<Timestamp>()
2100 .expect("valid timestamp");
2101
2102 let forecast_renewable = ForecastRenewable {
2103 base: BaseRenewable {
2104 duration: 5,
2105 date: Date::constant(2021, 5, 5),
2106 nem_time,
2107 start_time,
2108 end_time,
2109 renewables: 78.5,
2110 descriptor: RenewableDescriptor::Great,
2111 },
2112 };
2113 insta::assert_snapshot!(forecast_renewable.to_string(), @"Forecast: 2021-05-05 78.5% renewable (great)");
2114 }
2115
2116 #[test]
2117 fn display_current_renewable() {
2118 use jiff::{Timestamp, civil::Date};
2119 let nem_time = "2021-05-06T12:30:00+10:00"
2120 .parse::<Timestamp>()
2121 .expect("valid timestamp");
2122 let start_time = "2021-05-05T02:00:01Z"
2123 .parse::<Timestamp>()
2124 .expect("valid timestamp");
2125 let end_time = "2021-05-05T02:30:00Z"
2126 .parse::<Timestamp>()
2127 .expect("valid timestamp");
2128
2129 let current_renewable = CurrentRenewable {
2130 base: BaseRenewable {
2131 duration: 5,
2132 date: Date::constant(2021, 5, 5),
2133 nem_time,
2134 start_time,
2135 end_time,
2136 renewables: 78.5,
2137 descriptor: RenewableDescriptor::Great,
2138 },
2139 };
2140 insta::assert_snapshot!(current_renewable.to_string(), @"Current: 2021-05-05 78.5% renewable (great)");
2141 }
2142
2143 #[test]
2144 fn display_renewable_enum() {
2145 use jiff::{Timestamp, civil::Date};
2146 let nem_time = "2021-05-06T12:30:00+10:00"
2147 .parse::<Timestamp>()
2148 .expect("valid timestamp");
2149 let start_time = "2021-05-05T02:00:01Z"
2150 .parse::<Timestamp>()
2151 .expect("valid timestamp");
2152 let end_time = "2021-05-05T02:30:00Z"
2153 .parse::<Timestamp>()
2154 .expect("valid timestamp");
2155
2156 let base = BaseRenewable {
2157 duration: 5,
2158 date: Date::constant(2021, 5, 5),
2159 nem_time,
2160 start_time,
2161 end_time,
2162 renewables: 78.5,
2163 descriptor: RenewableDescriptor::Great,
2164 };
2165
2166 let actual_renewable = Renewable::ActualRenewable(ActualRenewable { base: base.clone() });
2167 let forecast_renewable =
2168 Renewable::ForecastRenewable(ForecastRenewable { base: base.clone() });
2169 let current_renewable = Renewable::CurrentRenewable(CurrentRenewable { base });
2170
2171 insta::assert_snapshot!(actual_renewable.to_string(), @"Actual: 2021-05-05 78.5% renewable (great)");
2172 insta::assert_snapshot!(forecast_renewable.to_string(), @"Forecast: 2021-05-05 78.5% renewable (great)");
2173 insta::assert_snapshot!(current_renewable.to_string(), @"Current: 2021-05-05 78.5% renewable (great)");
2174 }
2175}