1use crate::error::SiteError;
15use crate::ids::{AssetId, CircuitId};
16use crate::units::{Current, Power};
17
18#[derive(Debug, Clone, PartialEq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct Circuit {
22 pub id: CircuitId,
24 #[cfg_attr(feature = "serde", serde(default))]
26 pub parent: Option<CircuitId>,
27 #[cfg_attr(feature = "serde", serde(default))]
29 pub label: String,
30 #[cfg_attr(feature = "serde", serde(default))]
32 pub fuse_current: Option<Current>,
33 #[cfg_attr(feature = "serde", serde(default))]
36 pub power_limit: Option<Power>,
37}
38
39impl Circuit {
40 #[must_use]
42 pub fn new(id: CircuitId, parent: Option<CircuitId>, fuse_current: Current) -> Self {
43 Self {
44 label: id.to_string(),
45 id,
46 parent,
47 fuse_current: Some(fuse_current),
48 power_limit: None,
49 }
50 }
51
52 #[must_use]
60 pub fn symmetric_power_limit(&self, voltage: crate::units::Voltage) -> Option<Power> {
61 let from_fuse = self.fuse_current.map(|i| i.to_power_3p(voltage));
62 match (from_fuse, self.power_limit) {
63 (Some(a), Some(b)) => Some(a.min(b)),
64 (a, b) => a.or(b),
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Default)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72#[cfg_attr(feature = "serde", serde(transparent))]
73pub struct Circuits {
74 circuits: Vec<Circuit>,
75}
76
77impl Circuits {
78 pub fn new(circuits: Vec<Circuit>) -> Result<Self, SiteError> {
85 for (i, c) in circuits.iter().enumerate() {
86 if circuits[..i].iter().any(|o| o.id == c.id) {
87 return Err(SiteError::DuplicateId {
88 kind: "circuit",
89 id: c.id.to_string(),
90 });
91 }
92 }
93 for c in &circuits {
94 if let Some(parent) = &c.parent
95 && !circuits.iter().any(|o| &o.id == parent)
96 {
97 return Err(SiteError::UnknownParent {
98 circuit: c.id.to_string(),
99 parent: parent.to_string(),
100 });
101 }
102 }
103 let this = Self { circuits };
104 for c in &this.circuits {
107 let mut cursor = c;
108 for _ in 0..=this.circuits.len() {
109 match cursor.parent.as_ref().and_then(|p| this.get(p)) {
110 Some(parent) => cursor = parent,
111 None => break,
112 }
113 if cursor.id == c.id {
114 return Err(SiteError::CircuitCycle {
115 circuit: c.id.to_string(),
116 });
117 }
118 }
119 }
120 Ok(this)
121 }
122
123 #[must_use]
125 pub fn get(&self, id: &CircuitId) -> Option<&Circuit> {
126 self.circuits.iter().find(|c| &c.id == id)
127 }
128
129 #[must_use]
131 pub fn all(&self) -> &[Circuit] {
132 &self.circuits
133 }
134
135 #[must_use]
139 pub fn path_to_root(&self, id: &CircuitId) -> Vec<&Circuit> {
140 let mut path = Vec::new();
141 let mut cursor = self.get(id);
142 for _ in 0..=self.circuits.len() {
145 let Some(c) = cursor else { break };
146 path.push(c);
147 cursor = c.parent.as_ref().and_then(|p| self.get(p));
148 }
149 path
150 }
151
152 #[must_use]
154 pub fn path_for_asset(&self, asset: &crate::asset::Asset) -> Vec<&Circuit> {
155 self.path_to_root(&asset.meta().circuit)
156 }
157
158 #[must_use]
160 pub fn assets_below<'a>(
161 &self,
162 circuit: &CircuitId,
163 assets: &'a [crate::asset::Asset],
164 ) -> Vec<&'a AssetId> {
165 assets
166 .iter()
167 .filter(|a| {
168 self.path_to_root(&a.meta().circuit)
169 .iter()
170 .any(|c| &c.id == circuit)
171 })
172 .map(crate::asset::Asset::id)
173 .collect()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 fn cid(s: &str) -> CircuitId {
182 CircuitId::new(s).unwrap()
183 }
184
185 fn tree() -> Circuits {
186 Circuits::new(vec![
187 Circuit::new(cid("main"), None, Current::new(63.0)),
188 Circuit::new(cid("garage"), Some(cid("main")), Current::new(20.0)),
189 ])
190 .unwrap()
191 }
192
193 #[test]
194 fn a_path_collects_every_limit_up_to_the_root() {
195 let ids: Vec<_> = tree()
196 .path_to_root(&cid("garage"))
197 .iter()
198 .map(|c| c.id.to_string())
199 .collect();
200 assert_eq!(ids, ["garage", "main"]);
201 }
202
203 #[test]
204 fn a_dangling_parent_is_refused() {
205 let err = Circuits::new(vec![Circuit::new(
206 cid("garage"),
207 Some(cid("nope")),
208 Current::new(20.0),
209 )])
210 .unwrap_err();
211 assert!(matches!(err, SiteError::UnknownParent { .. }));
212 }
213
214 #[test]
215 fn a_cycle_is_refused_rather_than_hanging_the_arbiter() {
216 let err = Circuits::new(vec![
217 Circuit::new(cid("a"), Some(cid("b")), Current::new(20.0)),
218 Circuit::new(cid("b"), Some(cid("a")), Current::new(20.0)),
219 ])
220 .unwrap_err();
221 assert!(matches!(err, SiteError::CircuitCycle { .. }));
222 }
223
224 #[test]
225 fn a_duplicate_name_is_refused() {
226 let err = Circuits::new(vec![
227 Circuit::new(cid("main"), None, Current::new(63.0)),
228 Circuit::new(cid("main"), None, Current::new(35.0)),
229 ])
230 .unwrap_err();
231 assert!(matches!(
232 err,
233 SiteError::DuplicateId {
234 kind: "circuit",
235 ..
236 }
237 ));
238 }
239
240 #[test]
241 fn a_fuse_becomes_a_symmetric_power_ceiling() {
242 let c = Circuit::new(cid("garage"), None, Current::new(20.0));
243 let limit = c
245 .symmetric_power_limit(crate::units::NOMINAL_VOLTAGE)
246 .unwrap();
247 assert!((limit.kw() - 13.8).abs() < 1e-9);
248 }
249}