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