Skip to main content

amber_api/
models.rs

1//! # Amber Electric API Models
2//!
3//! This module contains all the data structures and types used to interact with
4//! the [Amber Electric Public API](https://api.amber.com.au/v1).
5//!
6//! ## Core Configuration
7//!
8//! - [`State`] - Australian states for renewable energy data (NSW, VIC, QLD,
9//!   SA)
10//! - [`Resolution`] - Interval resolution options (5-minute, 30-minute)
11//!
12//! ## Sites and Channels
13//!
14//! - [`Site`] - Information about electricity sites linked to your account
15//! - [`Channel`] - Power meter channels (General, Controlled Load, Feed In)
16//! - [`ChannelType`] - Types of meter channels
17//! - [`SiteStatus`] - Status of sites (Pending, Active, Closed)
18//!
19//! ## Pricing Data
20//!
21//! - [`Interval`] - Electricity pricing intervals (Actual, Forecast, Current)
22//! - [`BaseInterval`] - Common fields for all interval types
23//! - [`ActualInterval`] - Confirmed historical pricing data
24//! - [`ForecastInterval`] - Predicted future pricing data
25//! - [`CurrentInterval`] - Real-time pricing data
26//! - [`PriceDescriptor`] - Price categories (extremely low, low, neutral, high,
27//!   spike)
28//! - [`SpikeStatus`] - Spike warning indicators
29//! - [`Range`] - Price ranges when volatile
30//! - [`AdvancedPrice`] - Advanced price prediction with confidence bands
31//!
32//! ## Usage Data
33//!
34//! - [`Usage`] - Historical electricity consumption and generation data
35//! - [`UsageQuality`] - Data quality indicators (Estimated, Billable)
36//!
37//! ## Renewable Energy
38//!
39//! - [`Renewable`] - Renewable energy data (Actual, Forecast, Current)
40//! - [`BaseRenewable`] - Common fields for renewable data
41//! - [`ActualRenewable`] - Confirmed historical renewable values
42//! - [`ForecastRenewable`] - Predicted future renewable values
43//! - [`CurrentRenewable`] - Real-time renewable values
44//! - [`RenewableDescriptor`] - Renewable energy levels (best, great, ok,
45//!   not great, worst)
46//!
47//! ## Tariff Information
48//!
49//! - [`TariffInformation`] - Time-of-use and demand tariff details
50//! - [`TariffPeriod`] - Time periods (off peak, shoulder, solar sponge, peak)
51//! - [`TariffSeason`] - Seasonal variations (Summer, Winter, etc.)
52//!
53//! ## Date and Time Handling
54//!
55//! All datetime fields use the [`jiff`] crate for robust datetime handling:
56//! - [`jiff::civil::Date`] for date-only fields (ISO 8601 dates)
57//! - [`jiff::Timestamp`] for datetime fields (ISO 8601 timestamps)
58
59#![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/// Valid Australian states for renewable energy data.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum State {
74    /// New South Wales.
75    Nsw,
76    /// Victoria.
77    Vic,
78    /// Queensland.
79    Qld,
80    /// South Australia.
81    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/// Valid interval resolution options.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum Resolution {
100    /// 5-minute intervals.
101    FiveMinute = 5,
102    /// 30-minute intervals.
103    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/// Meter channel type.
127#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
128#[serde(rename_all = "camelCase")]
129#[non_exhaustive]
130pub enum ChannelType {
131    /// General channel provides continuous power - all of your appliances and
132    /// lights are attached to this channel.
133    General,
134    /// Controlled load channels are only on for a limited time during the day
135    /// (usually when the load on the network is low, or generation is high) -
136    /// you may have your hot water system attached to this channel.
137    ControlledLoad,
138    /// Feed in channel sends power back to the grid - you will have these types
139    /// of channels if you have solar or batteries.
140    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/// Describes a power meter channel.
155///
156/// The General channel provides continuous power - it's the channel all of your
157/// appliances and lights are attached to.
158///
159/// Controlled loads are only on for a limited time during the day (usually when
160/// the load on the network is low, or generation is high) - you may have your
161/// hot water system attached to this channel.
162///
163/// The feed in channel sends power back to the grid - you will have these types
164/// of channels if you have solar or batteries.
165#[derive(Debug, Clone, PartialEq, Deserialize)]
166#[serde(rename_all = "camelCase")]
167#[non_exhaustive]
168pub struct Channel {
169    /// Identifier of the channel.
170    pub identifier: String,
171    /// Channel type.
172    #[serde(rename = "type")]
173    pub channel_type: ChannelType,
174    /// The tariff code of the channel.
175    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/// Site status.
190///
191/// Pending sites are still in the process of being transferred.
192///
193/// Note: Amber only includes sites that have correct address details. If you
194/// expect to see a site, but don't, you may need to contact info@amber.com.au
195/// to check that the address is correct.
196///
197/// Active sites are ones that Amber actively supplies electricity to.
198///
199/// Closed sites are old sites that Amber no longer supplies.
200#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
201#[serde(rename_all = "camelCase")]
202#[non_exhaustive]
203pub enum SiteStatus {
204    /// Site is still in the process of being transferred.
205    ///
206    /// Note: Amber only includes sites that have correct address details. If
207    /// you expect to see a site, but don't, you may need to contact
208    /// info@amber.com.au to check that the address is correct.
209    Pending,
210    /// Site is actively supplied with electricity by Amber.
211    Active,
212    /// Old site that Amber no longer supplies.
213    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/// Site information.
228#[derive(Debug, Clone, PartialEq, Deserialize)]
229#[serde(rename_all = "camelCase")]
230#[non_exhaustive]
231pub struct Site {
232    /// Unique Site Identifier.
233    pub id: String,
234    /// National Metering Identifier (NMI) for the site.
235    pub nmi: String,
236    /// List of channels that are readable from your meter.
237    pub channels: Vec<Channel>,
238    /// The name of the site's network.
239    pub network: String,
240    /// Site status.
241    pub status: SiteStatus,
242    /// Date the site became active. This date will be in the future for pending
243    /// sites. It may also be undefined, though if it is, contact
244    /// info@amber.com.au as there may be an issue with your address.
245    pub active_from: Option<Date>,
246    /// Date the site closed. Undefined if the site is pending or active.
247    pub closed_on: Option<Date>,
248    /// Length of interval that you will be billed on. 5 or 30 minutes.
249    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/// Spike status.
264///
265/// Indicates whether this interval will potentially spike, or is currently in a
266/// spike state.
267#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
268#[serde(rename_all = "camelCase")]
269#[non_exhaustive]
270pub enum SpikeStatus {
271    /// No spike expected or occurring.
272    None,
273    /// Spike may potentially occur during this interval.
274    Potential,
275    /// Spike is currently occurring during this interval.
276    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/// Describes the current price.
291///
292/// Gives you an indication of how cheap the price is in relation to the average
293/// VMO and DMO. Note: Negative is no longer used. It has been replaced with
294/// extremelyLow.
295#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
296#[serde(rename_all = "camelCase")]
297#[non_exhaustive]
298pub enum PriceDescriptor {
299    /// Negative pricing (deprecated - replaced with `ExtremelyLow`).
300    #[deprecated(note = "Negative pricing is no longer used. Use `ExtremelyLow` instead.")]
301    Negative,
302    /// Extremely low pricing - significant cost savings opportunity.
303    ExtremelyLow,
304    /// Very low pricing - good cost savings opportunity.
305    VeryLow,
306    /// Low pricing - some cost savings available.
307    Low,
308    /// Neutral pricing - average market conditions.
309    Neutral,
310    /// High pricing - costs above average.
311    High,
312    /// Spike pricing - very high costs, avoid high usage.
313    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/// Describes the state of renewables.
332///
333/// Gives you an indication of how green power is right now.
334#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
335#[serde(rename_all = "camelCase")]
336#[non_exhaustive]
337pub enum RenewableDescriptor {
338    /// Best renewable conditions - highest percentage of green energy.
339    Best,
340    /// Great renewable conditions - high percentage of green energy.
341    Great,
342    /// Ok renewable conditions - moderate percentage of green energy.
343    Ok,
344    /// Not great renewable conditions - low percentage of green energy.
345    NotGreat,
346    /// Worst renewable conditions - lowest percentage of green energy.
347    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/// When prices are particularly volatile, the API may return a range of NEM
364/// spot prices (c/kWh) that are possible.
365#[derive(Debug, Clone, PartialEq, Deserialize)]
366#[serde(rename_all = "camelCase")]
367#[non_exhaustive]
368pub struct Range {
369    /// Estimated minimum price (c/kWh).
370    pub min: f64,
371    /// Estimated maximum price (c/kWh).
372    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/// Advanced price prediction.
383///
384/// Amber has created an advanced forecast system, that represents Amber's
385/// confidence in the AEMO forecast. The range indicates where Amber thinks the
386/// price will land for a given interval.
387#[derive(Debug, Clone, PartialEq, Deserialize)]
388#[serde(rename_all = "camelCase")]
389#[non_exhaustive]
390pub struct AdvancedPrice {
391    /// The lower bound of Amber's prediction band. Price includes network and
392    /// market fees. (c/kWh).
393    pub low: f64,
394    /// The predicted price. Use this if you need a single number to forecast
395    /// against. Price includes network and market fees. (c/kWh).
396    pub predicted: f64,
397    /// The upper bound of Amber's prediction band. Price includes network and
398    /// market fees. (c/kWh).
399    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/// Information about how your tariff affects an interval.
414#[derive(Debug, Clone, PartialEq, Deserialize)]
415#[serde(rename_all = "camelCase")]
416#[non_exhaustive]
417pub struct TariffInformation {
418    /// The Time of Use period that is currently active.
419    ///
420    /// Only available if the site in on a time of use tariff.
421    pub period: Option<TariffPeriod>,
422    /// The Time of Use season that is currently active.
423    ///
424    /// Only available if the site in on a time of use tariff.
425    pub season: Option<TariffSeason>,
426    /// The block that is currently active.
427    ///
428    /// Only available in the site in on a block tariff.
429    pub block: Option<u32>,
430    /// Is this interval currently in the demand window?
431    ///
432    /// Only available if the site in on a demand tariff.
433    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/// Time of Use period.
463#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
464#[serde(rename_all = "camelCase")]
465#[non_exhaustive]
466pub enum TariffPeriod {
467    /// Off-peak period with lowest electricity rates.
468    OffPeak,
469    /// Shoulder period with moderate electricity rates.
470    Shoulder,
471    /// Solar sponge period designed to encourage consumption when solar
472    /// generation is high.
473    SolarSponge,
474    /// Peak period with highest electricity rates.
475    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/// Time of Use season.
491#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
492#[serde(rename_all = "camelCase")]
493#[non_exhaustive]
494pub enum TariffSeason {
495    /// Default tariff season.
496    Default,
497    /// Summer tariff season with typically higher rates due to increased
498    /// demand.
499    Summer,
500    /// Autumn tariff season with moderate rates.
501    Autumn,
502    /// Winter tariff season with higher rates due to heating demand.
503    Winter,
504    /// Spring tariff season with moderate rates.
505    Spring,
506    /// Non-summer tariff season (autumn, winter, spring combined).
507    NonSummer,
508    /// Holiday tariff period with special rates.
509    Holiday,
510    /// Weekend tariff period with typically lower rates.
511    Weekend,
512    /// Combined weekend and holiday tariff period.
513    WeekendHoliday,
514    /// Weekday tariff period with standard rates.
515    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/// Base interval structure containing common fields.
537#[derive(Debug, Clone, PartialEq, Deserialize)]
538#[serde(rename_all = "camelCase")]
539#[non_exhaustive]
540pub struct BaseInterval {
541    /// Length of the interval in minutes.
542    pub duration: u32,
543    /// NEM spot price (c/kWh).
544    ///
545    /// This is the price generators get paid to generate electricity, and what
546    /// drives the variable component of your perKwh price - includes GST.
547    pub spot_per_kwh: f64,
548    /// Number of cents you will pay per kilowatt-hour (c/kWh) - includes GST.
549    pub per_kwh: f64,
550    /// Date the interval belongs to (in NEM time).
551    ///
552    /// This may be different to the date component of nemTime, as the last
553    /// interval of the day ends at 12:00 the following day.
554    pub date: Date,
555    /// The interval's NEM time.
556    ///
557    /// This represents the time at the end of the interval UTC+10.
558    pub nem_time: Timestamp,
559    /// Start time of the interval in UTC.
560    pub start_time: Timestamp,
561    /// End time of the interval in UTC.
562    pub end_time: Timestamp,
563    /// Percentage of renewables in the grid.
564    pub renewables: f64,
565    /// Channel type.
566    pub channel_type: ChannelType,
567    /// Tariff information.
568    pub tariff_information: Option<TariffInformation>,
569    /// Spike status.
570    pub spike_status: SpikeStatus,
571    /// Price descriptor.
572    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/// Actual interval with confirmed pricing.
602#[derive(Debug, Clone, PartialEq, Deserialize)]
603#[serde(rename_all = "camelCase")]
604#[non_exhaustive]
605pub struct ActualInterval {
606    /// Base interval data with confirmed pricing.
607    #[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/// Forecast interval with predicted pricing.
619#[derive(Debug, Clone, PartialEq, Deserialize)]
620#[serde(rename_all = "camelCase")]
621#[non_exhaustive]
622pub struct ForecastInterval {
623    /// Base interval data with predicted pricing.
624    #[serde(flatten)]
625    pub base: BaseInterval,
626    /// Price range when volatile.
627    pub range: Option<Range>,
628    /// Advanced price prediction.
629    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/// Current interval with real-time pricing.
647#[derive(Debug, Clone, PartialEq, Deserialize)]
648#[serde(rename_all = "camelCase")]
649#[non_exhaustive]
650pub struct CurrentInterval {
651    /// Base interval data with real-time pricing.
652    #[serde(flatten)]
653    pub base: BaseInterval,
654    /// Price range when volatile.
655    pub range: Option<Range>,
656    /// Shows true the current price is an estimate. Shows false is the price
657    /// has been locked in.
658    pub estimate: bool,
659    /// Advanced price prediction.
660    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/// Interval enum that can be any of the interval types.
681#[derive(Debug, Clone, PartialEq, Deserialize)]
682#[serde(tag = "type")]
683#[non_exhaustive]
684pub enum Interval {
685    /// Actual interval with confirmed historical pricing data.
686    ActualInterval(ActualInterval),
687    /// Forecast interval with predicted future pricing data.
688    ForecastInterval(ForecastInterval),
689    /// Current interval with real-time pricing data.
690    CurrentInterval(CurrentInterval),
691}
692
693impl Interval {
694    /// Returns `true` if the interval is [`ActualInterval`].
695    ///
696    /// [`ActualInterval`]: Interval::ActualInterval
697    #[must_use]
698    #[inline]
699    pub fn is_actual_interval(&self) -> bool {
700        matches!(self, Self::ActualInterval(..))
701    }
702
703    /// Returns `true` if the interval is [`ForecastInterval`].
704    ///
705    /// [`ForecastInterval`]: Interval::ForecastInterval
706    #[must_use]
707    #[inline]
708    pub fn is_forecast_interval(&self) -> bool {
709        matches!(self, Self::ForecastInterval(..))
710    }
711
712    /// Returns `true` if the interval is [`CurrentInterval`].
713    ///
714    /// [`CurrentInterval`]: Interval::CurrentInterval
715    #[inline]
716    #[must_use]
717    pub fn is_current_interval(&self) -> bool {
718        matches!(self, Self::CurrentInterval(..))
719    }
720
721    /// Return a reference to the [`ActualInterval`] variant if it exists.
722    ///
723    /// [`ActualInterval`]: Interval::ActualInterval
724    #[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    /// Return a reference to the [`ForecastInterval`] variant if it exists.
735    ///
736    /// [`ForecastInterval`]: Interval::ForecastInterval
737    #[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    /// Return a reference to the [`CurrentInterval`] variant if it exists.
748    ///
749    /// [`CurrentInterval`]: Interval::CurrentInterval
750    #[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    /// Returns the base interval if it exists.
761    #[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(&current.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/// Usage data for a specific interval.
784#[derive(Debug, Clone, PartialEq, Deserialize)]
785#[serde(rename_all = "camelCase")]
786#[non_exhaustive]
787pub struct Usage {
788    /// Base interval data for usage reporting.
789    #[serde(flatten)]
790    pub base: BaseInterval,
791    /// Meter channel identifier.
792    pub channel_identifier: String,
793    /// Number of kWh you consumed or generated.
794    ///
795    /// Generated numbers will be negative.
796    pub kwh: f64,
797    /// Data quality indicator.
798    pub quality: UsageQuality,
799    /// The total cost of your consumption or generation for this period -
800    /// includes GST.
801    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/// Usage data quality.
816#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
817#[serde(rename_all = "camelCase")]
818#[non_exhaustive]
819pub enum UsageQuality {
820    /// Estimated by the metering company.
821    Estimated,
822    /// Actual billable data.
823    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/// Base renewable data structure.
837#[derive(Debug, Clone, PartialEq, Deserialize)]
838#[serde(rename_all = "camelCase")]
839#[non_exhaustive]
840pub struct BaseRenewable {
841    /// Length of the interval in minutes.
842    pub duration: u32,
843    /// Date the interval belongs to (in NEM time).
844    ///
845    /// This may be different to the date component of nemTime, as the last
846    /// interval of the day ends at 12:00 the following day.
847    pub date: Date,
848    /// The interval's NEM time.
849    ///
850    /// This represents the time at the end of the interval UTC+10.
851    pub nem_time: Timestamp,
852    /// Start time of the interval in UTC.
853    pub start_time: Timestamp,
854    /// End time of the interval in UTC.
855    pub end_time: Timestamp,
856    /// Percentage of renewables in the grid.
857    pub renewables: f64,
858    /// Renewable descriptor.
859    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/// Actual renewable data.
874#[derive(Debug, Clone, PartialEq, Deserialize)]
875#[serde(rename_all = "camelCase")]
876#[non_exhaustive]
877pub struct ActualRenewable {
878    /// Base renewable data with confirmed historical values.
879    #[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/// Forecast renewable data.
891#[derive(Debug, Clone, PartialEq, Deserialize)]
892#[serde(rename_all = "camelCase")]
893#[non_exhaustive]
894pub struct ForecastRenewable {
895    /// Base renewable data with predicted future values.
896    #[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/// Current renewable data.
908#[derive(Debug, Clone, PartialEq, Deserialize)]
909#[serde(rename_all = "camelCase")]
910#[non_exhaustive]
911pub struct CurrentRenewable {
912    /// Base renewable data with current real-time values.
913    #[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/// Renewable enum that can be any of the renewable types.
925#[derive(Debug, Clone, PartialEq, Deserialize)]
926#[serde(tag = "type")]
927#[non_exhaustive]
928pub enum Renewable {
929    /// Actual renewable data with confirmed historical values.
930    ActualRenewable(ActualRenewable),
931    /// Forecast renewable data with predicted future values.
932    ForecastRenewable(ForecastRenewable),
933    /// Current renewable data with real-time values.
934    CurrentRenewable(CurrentRenewable),
935}
936
937impl Renewable {
938    /// Returns `true` if the renewable is [`ActualRenewable`].
939    ///
940    /// [`ActualRenewable`]: Renewable::ActualRenewable
941    #[must_use]
942    #[inline]
943    pub fn is_actual_renewable(&self) -> bool {
944        matches!(self, Self::ActualRenewable(..))
945    }
946
947    /// Returns `true` if the renewable is [`ForecastRenewable`].
948    ///
949    /// [`ForecastRenewable`]: Renewable::ForecastRenewable
950    #[must_use]
951    #[inline]
952    pub fn is_forecast_renewable(&self) -> bool {
953        matches!(self, Self::ForecastRenewable(..))
954    }
955
956    /// Returns `true` if the renewable is [`CurrentRenewable`].
957    ///
958    /// [`CurrentRenewable`]: Renewable::CurrentRenewable
959    #[must_use]
960    #[inline]
961    pub fn is_current_renewable(&self) -> bool {
962        matches!(self, Self::CurrentRenewable(..))
963    }
964
965    /// Return a reference to the [`ActualRenewable`] variant if it exists.
966    ///
967    /// [`ActualRenewable`]: Renewable::ActualRenewable
968    #[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    /// Return a reference to the [`ForecastRenewable`] variant if it exists.
979    ///
980    /// [`ForecastRenewable`]: Renewable::ForecastRenewable
981    #[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    /// Return a reference to the [`CurrentRenewable`] variant if it exists.
992    ///
993    /// [`CurrentRenewable`]: Renewable::CurrentRenewable
994    #[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    /// Returns the base renewable data.
1005    #[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) => &current.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 Site deserialization
1180    #[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 Interval deserialization (prices endpoint)
1235    #[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        // Test ActualInterval
1312        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        // Test CurrentInterval
1326        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        // Test ForecastInterval
1349        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 Current Prices endpoint (same as prices but for current endpoint)
1363    #[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        // Verify we can deserialize all three types in the current prices endpoint
1436        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 Usage deserialization
1448    #[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 individual components with edge cases
1495    #[test]
1496    fn channel_types_deserialisation() -> Result<()> {
1497        // Test all channel types
1498        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        // Test all site statuses
1521        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    // Display trait tests using insta snapshots
1573    #[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        // Test with no information
1698        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        // Test with all information
1707        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        // Test with partial information
1716        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        // Use parse instead of constant for complex timestamps
1729        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        // Test basic case with no spike status and no tariff information
1740        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        // Test with spike status potential
1757        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        // Test with spike status spike
1774        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        // Test with tariff information only
1791        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        // Test with both spike status and tariff information
1814        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}