amber_api/
models.rs

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