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