1use haystack_core::data::HDict;
4use haystack_core::kinds::{HRef, Kind, Number};
5
6pub fn demo_entities() -> Vec<HDict> {
15 let mut entities = Vec::new();
16
17 let site_id = "demo-site";
19 entities.push(make_site(site_id));
20
21 let floor_ids: Vec<String> = (1..=3).map(|n| format!("demo-floor-{n}")).collect();
23 for (i, floor_id) in floor_ids.iter().enumerate() {
24 entities.push(make_floor(floor_id, i + 1, site_id));
25 }
26
27 let ahu_ids: Vec<String> = (1..=2).map(|n| format!("demo-ahu-{n}")).collect();
29 for (i, ahu_id) in ahu_ids.iter().enumerate() {
30 entities.push(make_ahu(ahu_id, i + 1, site_id, &floor_ids[0]));
31 }
32
33 for (ahu_idx, ahu_id) in ahu_ids.iter().enumerate() {
37 let ahu_num = ahu_idx + 1;
38 for vav_num in 1..=3 {
39 let vav_id = format!("demo-vav-{ahu_num}-{vav_num:02}");
40 let floor_id = &floor_ids[vav_num - 1];
41 entities.push(make_vav(
42 &vav_id, ahu_num, vav_num, site_id, floor_id, ahu_id,
43 ));
44
45 entities.push(make_zone_air_temp_sensor(
47 &vav_id, site_id, floor_id, ahu_id,
48 ));
49 entities.push(make_zone_air_temp_sp(&vav_id, site_id, floor_id, ahu_id));
50 entities.push(make_damper_cmd(&vav_id, site_id, floor_id, ahu_id));
51 entities.push(make_occ_sensor(&vav_id, site_id, floor_id, ahu_id));
52 }
53 }
54
55 entities
56}
57
58fn make_site(id: &str) -> HDict {
59 let mut d = HDict::new();
60 d.set("id", Kind::Ref(HRef::new(id, Some("Demo Building".into()))));
61 d.set("site", Kind::Marker);
62 d.set("dis", Kind::Str("Demo Building".into()));
63 d.set(
64 "area",
65 Kind::Number(Number::new(50000.0, Some("ft\u{00b2}".into()))),
66 );
67 d.set("geoCity", Kind::Str("Richmond".into()));
68 d.set("geoState", Kind::Str("VA".into()));
69 d.set("tz", Kind::Str("New_York".into()));
70 d
71}
72
73fn make_floor(id: &str, num: usize, site_id: &str) -> HDict {
74 let mut d = HDict::new();
75 let dis = format!("Floor {num}");
76 d.set("id", Kind::Ref(HRef::new(id, Some(dis.clone()))));
77 d.set("floor", Kind::Marker);
78 d.set("dis", Kind::Str(dis));
79 d.set("siteRef", Kind::Ref(HRef::from_val(site_id)));
80 d
81}
82
83fn make_ahu(id: &str, num: usize, site_id: &str, floor_id: &str) -> HDict {
84 let mut d = HDict::new();
85 let dis = format!("AHU-{num}");
86 d.set("id", Kind::Ref(HRef::new(id, Some(dis.clone()))));
87 d.set("ahu", Kind::Marker);
88 d.set("equip", Kind::Marker);
89 d.set("dis", Kind::Str(dis));
90 d.set("siteRef", Kind::Ref(HRef::from_val(site_id)));
91 d.set("floorRef", Kind::Ref(HRef::from_val(floor_id)));
92 d
93}
94
95fn make_vav(
96 id: &str,
97 ahu_num: usize,
98 vav_num: usize,
99 site_id: &str,
100 floor_id: &str,
101 ahu_id: &str,
102) -> HDict {
103 let mut d = HDict::new();
104 let dis = format!("VAV-{ahu_num}-{vav_num:02}");
105 d.set("id", Kind::Ref(HRef::new(id, Some(dis.clone()))));
106 d.set("vav", Kind::Marker);
107 d.set("equip", Kind::Marker);
108 d.set("dis", Kind::Str(dis));
109 d.set("siteRef", Kind::Ref(HRef::from_val(site_id)));
110 d.set("floorRef", Kind::Ref(HRef::from_val(floor_id)));
111 d.set("equipRef", Kind::Ref(HRef::from_val(ahu_id)));
112 d
113}
114
115fn make_zone_air_temp_sensor(vav_id: &str, site_id: &str, floor_id: &str, _ahu_id: &str) -> HDict {
116 let mut d = HDict::new();
117 let pt_id = format!("{vav_id}-zat");
118 let dis = format!("{} Zone Air Temp", vav_dis(vav_id));
119 d.set("id", Kind::Ref(HRef::new(&pt_id, Some(dis.clone()))));
120 d.set("point", Kind::Marker);
121 d.set("sensor", Kind::Marker);
122 d.set("zone", Kind::Marker);
123 d.set("air", Kind::Marker);
124 d.set("temp", Kind::Marker);
125 d.set("his", Kind::Marker);
126 d.set("kind", Kind::Str("Number".into()));
127 d.set("unit", Kind::Str("\u{00b0}F".into()));
128 d.set(
129 "curVal",
130 Kind::Number(Number::new(72.0, Some("\u{00b0}F".into()))),
131 );
132 d.set("dis", Kind::Str(dis));
133 d.set("siteRef", Kind::Ref(HRef::from_val(site_id)));
134 d.set("floorRef", Kind::Ref(HRef::from_val(floor_id)));
135 d.set("equipRef", Kind::Ref(HRef::from_val(vav_id)));
136 d
137}
138
139fn make_zone_air_temp_sp(vav_id: &str, site_id: &str, floor_id: &str, _ahu_id: &str) -> HDict {
140 let mut d = HDict::new();
141 let pt_id = format!("{vav_id}-zatsp");
142 let dis = format!("{} Zone Air Temp SP", vav_dis(vav_id));
143 d.set("id", Kind::Ref(HRef::new(&pt_id, Some(dis.clone()))));
144 d.set("point", Kind::Marker);
145 d.set("sp", Kind::Marker);
146 d.set("zone", Kind::Marker);
147 d.set("air", Kind::Marker);
148 d.set("temp", Kind::Marker);
149 d.set("kind", Kind::Str("Number".into()));
150 d.set("unit", Kind::Str("\u{00b0}F".into()));
151 d.set(
152 "curVal",
153 Kind::Number(Number::new(72.0, Some("\u{00b0}F".into()))),
154 );
155 d.set("dis", Kind::Str(dis));
156 d.set("siteRef", Kind::Ref(HRef::from_val(site_id)));
157 d.set("floorRef", Kind::Ref(HRef::from_val(floor_id)));
158 d.set("equipRef", Kind::Ref(HRef::from_val(vav_id)));
159 d
160}
161
162fn make_damper_cmd(vav_id: &str, site_id: &str, floor_id: &str, _ahu_id: &str) -> HDict {
163 let mut d = HDict::new();
164 let pt_id = format!("{vav_id}-dmpr");
165 let dis = format!("{} Damper Cmd", vav_dis(vav_id));
166 d.set("id", Kind::Ref(HRef::new(&pt_id, Some(dis.clone()))));
167 d.set("point", Kind::Marker);
168 d.set("cmd", Kind::Marker);
169 d.set("damper", Kind::Marker);
170 d.set("writable", Kind::Marker);
171 d.set("kind", Kind::Str("Number".into()));
172 d.set("unit", Kind::Str("%".into()));
173 d.set("curVal", Kind::Number(Number::new(85.0, Some("%".into()))));
174 d.set("dis", Kind::Str(dis));
175 d.set("siteRef", Kind::Ref(HRef::from_val(site_id)));
176 d.set("floorRef", Kind::Ref(HRef::from_val(floor_id)));
177 d.set("equipRef", Kind::Ref(HRef::from_val(vav_id)));
178 d
179}
180
181fn make_occ_sensor(vav_id: &str, site_id: &str, floor_id: &str, _ahu_id: &str) -> HDict {
182 let mut d = HDict::new();
183 let pt_id = format!("{vav_id}-occ");
184 let dis = format!("{} Occ", vav_dis(vav_id));
185 d.set("id", Kind::Ref(HRef::new(&pt_id, Some(dis.clone()))));
186 d.set("point", Kind::Marker);
187 d.set("sensor", Kind::Marker);
188 d.set("occ", Kind::Marker);
189 d.set("kind", Kind::Str("Bool".into()));
190 d.set("curVal", Kind::Bool(true));
191 d.set("dis", Kind::Str(dis));
192 d.set("siteRef", Kind::Ref(HRef::from_val(site_id)));
193 d.set("floorRef", Kind::Ref(HRef::from_val(floor_id)));
194 d.set("equipRef", Kind::Ref(HRef::from_val(vav_id)));
195 d
196}
197
198fn vav_dis(vav_id: &str) -> String {
200 vav_id
202 .strip_prefix("demo-")
203 .unwrap_or(vav_id)
204 .to_uppercase()
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use std::collections::HashSet;
211
212 #[test]
213 fn demo_has_expected_count() {
214 let entities = demo_entities();
215 assert_eq!(entities.len(), 36);
217 }
218
219 #[test]
220 fn demo_site_has_required_tags() {
221 let entities = demo_entities();
222 let site = entities
223 .iter()
224 .find(|e| e.has("site"))
225 .expect("should have a site entity");
226 assert!(site.has("site"));
227 assert!(site.has("dis"));
228 assert!(site.has("area"));
229 assert_eq!(site.get("dis"), Some(&Kind::Str("Demo Building".into())));
230 assert_eq!(
231 site.get("area"),
232 Some(&Kind::Number(Number::new(
233 50000.0,
234 Some("ft\u{00b2}".into())
235 )))
236 );
237 }
238
239 #[test]
240 fn demo_refs_are_valid() {
241 let entities = demo_entities();
242
243 let ids: HashSet<String> = entities
245 .iter()
246 .filter_map(|e| e.id().map(|r| r.val.clone()))
247 .collect();
248
249 for entity in &entities {
251 for ref_tag in &["siteRef", "floorRef", "equipRef"] {
252 if let Some(Kind::Ref(r)) = entity.get(ref_tag) {
253 assert!(
254 ids.contains(&r.val),
255 "entity {:?} has {} = @{} which is not a valid entity ID",
256 entity.id().map(|r| &r.val),
257 ref_tag,
258 r.val
259 );
260 }
261 }
262 }
263 }
264
265 #[test]
266 fn demo_points_have_kind() {
267 let entities = demo_entities();
268 let points: Vec<&HDict> = entities.iter().filter(|e| e.has("point")).collect();
269
270 assert_eq!(points.len(), 24);
272
273 for pt in &points {
274 assert!(
275 pt.has("kind"),
276 "point {:?} is missing 'kind' tag",
277 pt.id().map(|r| &r.val)
278 );
279 match pt.get("kind") {
280 Some(Kind::Str(s)) => {
281 assert!(s == "Number" || s == "Bool", "unexpected kind value: {s}");
282 }
283 other => panic!("expected kind to be a Str, got {:?}", other),
284 }
285 }
286 }
287}