1use core::fmt;
36use core::iter::Sum;
37use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
38
39use crate::error::UnitError;
40
41macro_rules! scalar_unit {
42 (
43 $(#[$meta:meta])*
44 $name:ident, $unit:literal, $si:literal
45 ) => {
46 $(#[$meta])*
47 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
48 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49 #[cfg_attr(feature = "serde", serde(transparent))]
50 pub struct $name(f64);
51
52 impl $name {
53 #[doc = concat!("Zero ", $si, ".")]
54 pub const ZERO: Self = Self(0.0);
55
56 #[doc = concat!("A value in ", $si, " (`", $unit, "`).")]
57 #[must_use]
58 pub fn new(value: f64) -> Self {
59 debug_assert!(value.is_finite(), concat!(stringify!($name), " must be finite"));
60 Self(value)
61 }
62
63 #[doc = concat!("A compile-time constant in ", $si, ", unchecked.")]
64 #[must_use]
65 pub const fn new_const(value: f64) -> Self {
66 Self(value)
67 }
68
69 #[doc = concat!("The value in ", $si, ".")]
70 #[must_use]
71 pub const fn get(self) -> f64 {
72 self.0
73 }
74
75 #[must_use]
78 pub fn is_finite(self) -> bool {
79 self.0.is_finite()
80 }
81
82 #[must_use]
84 pub fn max(self, other: Self) -> Self {
85 if self.0.is_nan() || other.0.is_nan() {
86 Self(f64::NAN)
87 } else if self.0 >= other.0 {
88 self
89 } else {
90 other
91 }
92 }
93
94 #[must_use]
96 pub fn min(self, other: Self) -> Self {
97 if self.0.is_nan() || other.0.is_nan() {
98 Self(f64::NAN)
99 } else if self.0 <= other.0 {
100 self
101 } else {
102 other
103 }
104 }
105
106 #[must_use]
112 pub fn clamp(self, lo: Self, hi: Self) -> Self {
113 assert!(lo.0 <= hi.0, "clamp range inverted: {lo:?} > {hi:?}");
114 self.max(lo).min(hi)
115 }
116
117 #[must_use]
119 pub fn abs(self) -> Self {
120 Self(self.0.abs())
121 }
122
123 #[must_use]
125 pub fn inflow(self) -> Self {
126 Self(self.0.max(0.0))
127 }
128
129 #[must_use]
131 pub fn outflow(self) -> Self {
132 Self((-self.0).max(0.0))
133 }
134 }
135
136 impl Add for $name {
137 type Output = Self;
138 fn add(self, rhs: Self) -> Self { Self(self.0 + rhs.0) }
139 }
140 impl AddAssign for $name {
141 fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0; }
142 }
143 impl Sub for $name {
144 type Output = Self;
145 fn sub(self, rhs: Self) -> Self { Self(self.0 - rhs.0) }
146 }
147 impl SubAssign for $name {
148 fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0; }
149 }
150 impl Neg for $name {
151 type Output = Self;
152 fn neg(self) -> Self { Self(-self.0) }
153 }
154 impl Mul<f64> for $name {
155 type Output = Self;
156 fn mul(self, rhs: f64) -> Self { Self(self.0 * rhs) }
157 }
158 impl Mul<$name> for f64 {
159 type Output = $name;
160 fn mul(self, rhs: $name) -> $name { $name(self * rhs.0) }
161 }
162 impl Div<f64> for $name {
163 type Output = Self;
164 fn div(self, rhs: f64) -> Self { Self(self.0 / rhs) }
165 }
166 impl Div for $name {
167 type Output = f64;
168 fn div(self, rhs: Self) -> f64 { self.0 / rhs.0 }
169 }
170 impl Sum for $name {
171 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
172 iter.fold(Self::ZERO, Add::add)
173 }
174 }
175 impl<'a> Sum<&'a $name> for $name {
176 fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
177 iter.fold(Self::ZERO, |acc, v| acc + *v)
178 }
179 }
180 impl fmt::Display for $name {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "{:.1} {}", self.0, $unit)
183 }
184 }
185 };
186}
187
188scalar_unit!(
189 Power, "W", "watts"
191);
192scalar_unit!(
193 Energy, "Wh", "watt-hours"
195);
196scalar_unit!(
197 ApparentPower, "VA", "volt-amperes"
200);
201scalar_unit!(
202 Current, "A", "amperes"
204);
205scalar_unit!(
206 Voltage, "V", "volts"
208);
209
210impl Power {
211 #[must_use]
214 pub fn from_kw(kw: f64) -> Self {
215 Self::new(kw * 1000.0)
216 }
217
218 #[must_use]
220 pub fn kw(self) -> f64 {
221 self.0 / 1000.0
222 }
223
224 #[must_use]
226 pub fn over(self, duration: time::Duration) -> Energy {
227 Energy::new(self.0 * duration.as_seconds_f64() / 3600.0)
228 }
229
230 #[must_use]
232 pub fn to_current_1p(self, voltage: Voltage) -> Current {
233 Current::new(self.0 / voltage.get())
234 }
235
236 #[must_use]
238 pub fn to_current_3p(self, voltage: Voltage) -> Current {
239 Current::new(self.0 / (3.0 * voltage.get()))
240 }
241}
242
243impl Energy {
244 #[must_use]
246 pub fn from_kwh(kwh: f64) -> Self {
247 Self::new(kwh * 1000.0)
248 }
249
250 #[must_use]
252 pub fn kwh(self) -> f64 {
253 self.0 / 1000.0
254 }
255
256 #[must_use]
258 pub fn over(self, duration: time::Duration) -> Power {
259 Power::new(self.0 * 3600.0 / duration.as_seconds_f64())
260 }
261}
262
263impl Current {
264 #[must_use]
266 pub fn to_power_1p(self, voltage: Voltage) -> Power {
267 Power::new(self.0 * voltage.get())
268 }
269
270 #[must_use]
272 pub fn to_power_3p(self, voltage: Voltage) -> Power {
273 Power::new(3.0 * self.0 * voltage.get())
274 }
275}
276
277pub const NOMINAL_VOLTAGE: Voltage = Voltage::new_const(230.0);
279
280#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
288#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
289#[cfg_attr(feature = "serde", serde(try_from = "f64", into = "f64"))]
290pub struct Soc(f64);
291
292impl Soc {
293 pub const EMPTY: Self = Self(0.0);
295 pub const ZERO_RESERVE: Self = Self(0.0);
297 pub const FULL: Self = Self(1.0);
299
300 pub fn new(fraction: f64) -> Result<Self, UnitError> {
305 if fraction.is_finite() && (0.0..=1.0).contains(&fraction) {
306 Ok(Self(fraction))
307 } else {
308 Err(UnitError::SocOutOfRange(fraction))
309 }
310 }
311
312 pub fn from_percent(percent: f64) -> Result<Self, UnitError> {
317 Self::new(percent / 100.0).map_err(|_| UnitError::SocOutOfRange(percent))
318 }
319
320 #[must_use]
324 pub fn clamped(fraction: f64) -> Self {
325 if fraction.is_finite() {
326 Self(fraction.clamp(0.0, 1.0))
327 } else {
328 Self::EMPTY
329 }
330 }
331
332 #[must_use]
334 pub const fn fraction(self) -> f64 {
335 self.0
336 }
337
338 #[must_use]
340 pub fn percent(self) -> f64 {
341 self.0 * 100.0
342 }
343
344 #[must_use]
346 pub fn energy_in(self, capacity: Energy) -> Energy {
347 capacity * self.0
348 }
349}
350
351impl TryFrom<f64> for Soc {
352 type Error = UnitError;
353 fn try_from(value: f64) -> Result<Self, Self::Error> {
354 Self::new(value)
355 }
356}
357
358impl From<Soc> for f64 {
359 fn from(value: Soc) -> Self {
360 value.0
361 }
362}
363
364impl fmt::Display for Soc {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 write!(f, "{:.1} %", self.percent())
367 }
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
374#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
375#[cfg_attr(feature = "serde", serde(rename_all = "UPPERCASE"))]
376pub enum Phase {
377 L1,
379 L2,
381 L3,
383}
384
385impl Phase {
386 pub const ALL: [Phase; 3] = [Phase::L1, Phase::L2, Phase::L3];
388
389 #[must_use]
391 pub const fn index(self) -> usize {
392 match self {
393 Phase::L1 => 0,
394 Phase::L2 => 1,
395 Phase::L3 => 2,
396 }
397 }
398}
399
400impl fmt::Display for Phase {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 match self {
403 Phase::L1 => f.write_str("L1"),
404 Phase::L2 => f.write_str("L2"),
405 Phase::L3 => f.write_str("L3"),
406 }
407 }
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Default)]
416#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
417pub struct PerPhase<T> {
418 pub l1: T,
420 pub l2: T,
422 pub l3: T,
424}
425
426impl<T: Copy> PerPhase<T> {
427 pub const fn splat(value: T) -> Self {
429 Self {
430 l1: value,
431 l2: value,
432 l3: value,
433 }
434 }
435
436 #[must_use]
438 pub const fn get(&self, phase: Phase) -> T {
439 match phase {
440 Phase::L1 => self.l1,
441 Phase::L2 => self.l2,
442 Phase::L3 => self.l3,
443 }
444 }
445
446 pub const fn set(&mut self, phase: Phase, value: T) {
448 match phase {
449 Phase::L1 => self.l1 = value,
450 Phase::L2 => self.l2 = value,
451 Phase::L3 => self.l3 = value,
452 }
453 }
454
455 #[must_use]
457 pub const fn as_array(&self) -> [T; 3] {
458 [self.l1, self.l2, self.l3]
459 }
460
461 #[must_use]
463 pub fn map<U: Copy>(&self, mut f: impl FnMut(T) -> U) -> PerPhase<U> {
464 PerPhase {
465 l1: f(self.l1),
466 l2: f(self.l2),
467 l3: f(self.l3),
468 }
469 }
470
471 #[must_use]
473 pub fn zip_with<U: Copy, V: Copy>(
474 &self,
475 other: &PerPhase<U>,
476 mut f: impl FnMut(T, U) -> V,
477 ) -> PerPhase<V> {
478 PerPhase {
479 l1: f(self.l1, other.l1),
480 l2: f(self.l2, other.l2),
481 l3: f(self.l3, other.l3),
482 }
483 }
484
485 pub fn iter(&self) -> impl Iterator<Item = (Phase, T)> + '_ {
487 Phase::ALL.into_iter().map(move |p| (p, self.get(p)))
488 }
489}
490
491impl PerPhase<Power> {
492 pub const ZERO: Self = Self::splat(Power::ZERO);
494
495 #[must_use]
497 pub fn total(&self) -> Power {
498 self.l1 + self.l2 + self.l3
499 }
500
501 #[must_use]
521 pub fn unbalance(&self) -> ApparentPower {
522 let [a, b, c] = self.as_array().map(Power::get);
523 let max = a.max(b).max(c);
524 let min = a.min(b).min(c);
525 ApparentPower::new(max - min)
526 }
527}
528
529impl Add for PerPhase<Power> {
530 type Output = Self;
531 fn add(self, rhs: Self) -> Self {
532 self.zip_with(&rhs, |a, b| a + b)
533 }
534}
535
536impl Sub for PerPhase<Power> {
537 type Output = Self;
538 fn sub(self, rhs: Self) -> Self {
539 self.zip_with(&rhs, |a, b| a - b)
540 }
541}
542
543impl Sum for PerPhase<Power> {
544 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
545 iter.fold(Self::ZERO, Add::add)
546 }
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
556#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
557#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
558pub enum PhaseMode {
559 Single,
563 #[default]
565 Three,
566}
567
568impl PhaseMode {
569 #[must_use]
571 pub const fn count(self) -> u8 {
572 match self {
573 PhaseMode::Single => 1,
574 PhaseMode::Three => 3,
575 }
576 }
577
578 #[must_use]
580 pub const fn other(self) -> Self {
581 match self {
582 PhaseMode::Single => PhaseMode::Three,
583 PhaseMode::Three => PhaseMode::Single,
584 }
585 }
586}
587
588impl fmt::Display for PhaseMode {
589 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590 match self {
591 PhaseMode::Single => f.write_str("1p"),
592 PhaseMode::Three => f.write_str("3p"),
593 }
594 }
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
600#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "kind"))]
601pub enum PhaseConnection {
602 Single {
607 phase: Phase,
609 },
610 Three,
612 Switchable {
615 phase: Phase,
617 },
618}
619
620impl PhaseConnection {
621 #[must_use]
623 pub const fn is_switchable(&self) -> bool {
624 matches!(self, PhaseConnection::Switchable { .. })
625 }
626
627 #[must_use]
632 pub const fn default_mode(&self) -> PhaseMode {
633 match self {
634 PhaseConnection::Single { .. } => PhaseMode::Single,
635 PhaseConnection::Three | PhaseConnection::Switchable { .. } => PhaseMode::Three,
636 }
637 }
638
639 #[must_use]
641 pub const fn supports(&self, mode: PhaseMode) -> bool {
642 match self {
643 PhaseConnection::Single { .. } => matches!(mode, PhaseMode::Single),
644 PhaseConnection::Three => matches!(mode, PhaseMode::Three),
645 PhaseConnection::Switchable { .. } => true,
646 }
647 }
648
649 #[must_use]
651 pub const fn clamp_mode(&self, mode: PhaseMode) -> PhaseMode {
652 if self.supports(mode) {
653 mode
654 } else {
655 self.default_mode()
656 }
657 }
658
659 #[must_use]
661 pub const fn single_phase_conductor(&self) -> Option<Phase> {
662 match self {
663 PhaseConnection::Single { phase } | PhaseConnection::Switchable { phase } => {
664 Some(*phase)
665 }
666 PhaseConnection::Three => None,
667 }
668 }
669
670 #[must_use]
672 pub fn distribute(&self, total: Power, mode: PhaseMode) -> PerPhase<Power> {
673 match (self.clamp_mode(mode), self.single_phase_conductor()) {
674 (PhaseMode::Single, Some(phase)) => {
675 let mut p = PerPhase::ZERO;
676 p.set(phase, total);
677 p
678 }
679 _ => PerPhase::splat(total / 3.0),
680 }
681 }
682
683 #[must_use]
685 pub const fn count(&self, mode: PhaseMode) -> u8 {
686 self.clamp_mode(mode).count()
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn power_round_trips_through_kilowatts() {
696 assert!((Power::from_kw(4.2).kw() - 4.2).abs() < 1e-12);
697 assert_eq!(Power::from_kw(4.2), Power::new(4200.0));
698 }
699
700 #[test]
701 fn inflow_and_outflow_split_the_sign() {
702 let importing = Power::from_kw(3.0);
703 let exporting = Power::from_kw(-3.0);
704 assert_eq!(importing.inflow(), Power::from_kw(3.0));
705 assert_eq!(importing.outflow(), Power::ZERO);
706 assert_eq!(exporting.inflow(), Power::ZERO);
707 assert_eq!(exporting.outflow(), Power::from_kw(3.0));
708 }
709
710 #[test]
711 fn energy_and_power_are_inverse_over_a_quarter_hour() {
712 let quarter = time::Duration::minutes(15);
713 let e = Power::from_kw(4.0).over(quarter);
714 assert!((e.kwh() - 1.0).abs() < 1e-12);
715 assert!((e.over(quarter).kw() - 4.0).abs() < 1e-12);
716 }
717
718 #[test]
719 fn soc_rejects_out_of_range_and_clamps_on_request() {
720 assert!(Soc::new(1.004).is_err());
721 assert!(Soc::from_percent(100.4).is_err());
722 assert_eq!(Soc::clamped(1.004), Soc::FULL);
723 assert_eq!(Soc::clamped(f64::NAN), Soc::EMPTY);
724 }
725
726 #[test]
727 fn unbalance_is_the_spread_between_conductors() {
728 let p = PerPhase {
730 l1: Power::from_kw(4.6),
731 l2: Power::ZERO,
732 l3: Power::ZERO,
733 };
734 assert!((p.unbalance().get() - 4600.0).abs() < 1e-9);
735 assert_eq!(
737 PerPhase::splat(Power::from_kw(11.0)).unbalance(),
738 ApparentPower::ZERO
739 );
740 }
741
742 #[test]
743 fn switchable_connection_moves_between_one_and_three_conductors() {
744 let c = PhaseConnection::Switchable { phase: Phase::L2 };
745 let single = c.distribute(Power::from_kw(3.6), PhaseMode::Single);
746 assert_eq!(single.l2, Power::from_kw(3.6));
747 assert_eq!(single.l1, Power::ZERO);
748 let three = c.distribute(Power::from_kw(11.0), PhaseMode::Three);
749 assert!((three.l1.kw() - 11.0 / 3.0).abs() < 1e-12);
750 assert_eq!(three.total(), Power::from_kw(11.0));
751 }
752
753 #[test]
754 fn a_wiring_that_cannot_switch_ignores_a_mode_it_does_not_have() {
755 let fixed = PhaseConnection::Three;
759 assert!(!fixed.is_switchable());
760 assert_eq!(fixed.clamp_mode(PhaseMode::Single), PhaseMode::Three);
761 assert_eq!(fixed.count(PhaseMode::Single), 3);
762 assert_eq!(
763 fixed.distribute(Power::from_kw(3.0), PhaseMode::Single).l1,
764 Power::from_kw(1.0)
765 );
766
767 let fixed_single = PhaseConnection::Single { phase: Phase::L1 };
768 assert_eq!(fixed_single.clamp_mode(PhaseMode::Three), PhaseMode::Single);
769 assert_eq!(fixed_single.count(PhaseMode::Three), 1);
770 }
771
772 #[test]
773 fn a_switchable_wallbox_starts_three_phase() {
774 let c = PhaseConnection::Switchable { phase: Phase::L1 };
775 assert_eq!(c.default_mode(), PhaseMode::Three);
776 assert!(c.supports(PhaseMode::Single) && c.supports(PhaseMode::Three));
777 assert_eq!(c.single_phase_conductor(), Some(Phase::L1));
778 }
779
780 #[test]
781 fn nan_propagates_through_min_and_max_instead_of_being_swallowed() {
782 let nan = Power::new_const(f64::NAN);
783 assert!(!nan.max(Power::ZERO).is_finite());
784 assert!(!nan.min(Power::ZERO).is_finite());
785 }
786}