1use std::{fmt::Display, str::FromStr, sync::LazyLock};
2
3use base64::{Engine, prelude::BASE64_STANDARD};
4use chrono::FixedOffset;
5use palette::Hsv;
6use regex::Regex;
7use serde::{Deserialize, Serialize};
8use serde_with::{DeserializeFromStr, SerializeDisplay};
9
10use crate::error::HabRsError;
11
12#[derive(Debug, Clone, PartialEq)]
14#[non_exhaustive]
15#[allow(clippy::large_enum_variant)]
16pub enum Event {
17 Message(Message),
19 Alive,
21 Unknown(UnknownEvent),
23}
24
25impl FromStr for Event {
26 type Err = HabRsError;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 match s.lines().collect::<Vec<_>>().as_slice() {
30 [first_line, second_line]
31 if first_line.starts_with("event: ") && second_line.starts_with("data: ") =>
32 {
33 let event_type = first_line
34 .split_once(':')
35 .expect("First line does not contain ':'")
36 .1
37 .trim();
38 let data = second_line
39 .split_once(':')
40 .expect("First line does not contain ':'")
41 .1
42 .trim();
43
44 match event_type {
45 "message" => Ok(Self::Message(serde_json::from_str(data)?)),
46 "alive" => Ok(Self::Alive),
47 _ => Ok(Self::Unknown(UnknownEvent {
48 event_type: event_type.to_string(),
49 data: data.to_string(),
50 })),
51 }
52 }
53 _ => Err(HabRsError::Parse(s.to_string())),
54 }
55 }
56}
57
58#[allow(missing_docs)]
60#[derive(Debug, Clone, PartialEq)]
61pub struct UnknownEvent {
62 pub event_type: String,
63 pub data: String,
64}
65
66#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
70pub struct Message {
71 pub topic: Topic,
73 #[serde(flatten)]
75 pub message_type: MessageType,
76}
77
78impl Message {
79 pub fn get_message_type_for_entity(&self, entity: &str) -> Option<&MessageType> {
81 if self.topic.entity == entity {
82 Some(&self.message_type)
83 } else {
84 None
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
93#[non_exhaustive]
94#[serde(tag = "type", content = "payload")]
95pub enum MessageType {
96 #[serde(with = "serde_nested_json")]
98 ItemStateEvent(StateUpdatedEvent),
99 #[serde(with = "serde_nested_json")]
101 ItemStateChangedEvent(StateChangedEvent),
102 #[serde(with = "serde_nested_json")]
104 GroupItemStateChangedEvent(StateChangedEvent),
105 #[serde(with = "serde_nested_json")]
107 ItemStateUpdatedEvent(StateUpdatedEvent),
108 #[serde(with = "serde_nested_json")]
110 ItemStatePredictedEvent(StatePredictedEvent),
111 #[serde(with = "serde_nested_json")]
113 GroupStateUpdatedEvent(StateUpdatedEvent),
114 #[serde(with = "serde_nested_json")]
116 ItemCommandEvent(StateUpdatedEvent),
117 #[serde(with = "serde_nested_json")]
119 RuleStatusInfoEvent(StatusInfoEvent),
120 #[serde(with = "serde_nested_json")]
122 ThingStatusInfoEvent(StatusInfoEvent),
123 #[serde(with = "serde_nested_json")]
125 ThingStatusInfoChangedEvent([StatusInfoEvent; 2]),
126 #[serde(with = "serde_nested_json")]
128 ChannelTriggeredEvent(ChannelTriggeredEvent),
129 #[serde(other)]
131 Unknown,
132}
133
134#[allow(missing_docs)]
136#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
137#[non_exhaustive]
138#[serde(rename_all = "camelCase")]
139pub struct StatusInfoEvent {
140 pub status: String,
141 pub status_detail: String,
142 pub description: Option<String>,
143}
144
145#[allow(missing_docs)]
147#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
148#[non_exhaustive]
149pub struct StateChangedEvent {
150 #[serde(flatten)]
151 pub value: TypedValue,
152 #[serde(flatten)]
153 pub old_value: TypedOldValue,
154}
155
156#[allow(missing_docs)]
158#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
159#[non_exhaustive]
160#[serde(rename_all = "camelCase")]
161pub struct StateUpdatedEvent {
162 #[serde(flatten)]
163 pub value: TypedValue,
164}
165
166#[allow(missing_docs)]
168#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
169#[non_exhaustive]
170#[serde(rename_all = "camelCase")]
171pub struct StatePredictedEvent {
172 #[serde(flatten)]
173 pub value: TypedPredictedValue,
174 pub is_confirmation: bool,
175}
176
177#[allow(missing_docs)]
179#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
180#[non_exhaustive]
181#[serde(rename_all = "camelCase")]
182pub struct ChannelTriggeredEvent {
183 pub event: String,
184 pub channel: String,
185}
186
187macro_rules! typed_values {
188 ($([$name:ident, $value_name:literal, $value_type_name:literal]),*) => {
189 $(
190 #[allow(missing_docs)]
195 #[derive(Debug, PartialEq, Deserialize, Serialize, Clone, Default)]
196 #[non_exhaustive]
197 #[serde(tag = $value_type_name, content = $value_name)]
198 pub enum $name {
199 Decimal(Decimal),
200 Percent(Decimal),
201 Quantity(Quantity),
202 DateTime(DateTime),
203 OnOff(OnOff),
204 PlayPause(PlayPause),
205 RewindFastforward(RewindFastforward),
206 StopMove(StopMove),
207 OpenClosed(OpenClosed),
208 IncreaseDecrease(IncreaseDecrease),
209 UpDown(UpDown),
210 NextPrevious(NextPrevious),
211 #[serde(rename = "HSB")]
212 Hsb(Hsb),
213 Point(Point),
214 String(String),
215 StringList(StringList),
216 UnDef(String),
217 Raw(Raw),
218 Unknown(String),
219 #[serde(other)]
220 #[default]
221 Unimplemented,
222 }
223
224 impl Display for $name {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 $name::Decimal(decimal) => decimal.fmt(f),
228 $name::Percent(decimal) => decimal.fmt(f),
229 $name::Quantity(quantity) => quantity.fmt(f),
230 $name::IncreaseDecrease(increase_decrease) => increase_decrease.fmt(f),
231 $name::UpDown(up_down) => up_down.fmt(f),
232 $name::NextPrevious(next_previous) => next_previous.fmt(f),
233 $name::Hsb(color) => color.fmt(f),
234 $name::Point(point) => point.fmt(f),
235 $name::DateTime(date_time) => date_time.fmt(f),
236 $name::OnOff(on_off) => on_off.fmt(f),
237 $name::PlayPause(play_pause) => play_pause.fmt(f),
238 $name::RewindFastforward(rewind_fastforward) => rewind_fastforward.fmt(f),
239 $name::StopMove(stop_move) => stop_move.fmt(f),
240 $name::OpenClosed(open_closed) => open_closed.fmt(f),
241 $name::String(string) => string.fmt(f),
242 $name::StringList(string_list) => string_list.fmt(f),
243 $name::Raw(raw) => raw.fmt(f),
244 $name::UnDef(string) => string.fmt(f),
245 $name::Unknown(string) => string.fmt(f),
246 $name::Unimplemented => write!(f, "Unimplemented"),
247 }
248 }
249 }
250 )*
251 };
252}
253
254typed_values!(
255 [TypedValue, "value", "type"],
256 [TypedOldValue, "oldValue", "oldType"],
257 [TypedPredictedValue, "predictedValue", "predictedType"]
258);
259
260macro_rules! from_typed_values {
261 ([$($name:ident),*]) => {
262 $(
263 impl From<$name> for TypedValue {
264 fn from(value: $name) -> Self {
265 match value {
266 $name::Decimal(decimal) => Self::Decimal(decimal),
267 $name::Percent(decimal) => Self::Percent(decimal),
268 $name::Quantity(quantity) => Self::Quantity(quantity),
269 $name::IncreaseDecrease(increase_decrease) => Self::IncreaseDecrease(increase_decrease),
270 $name::UpDown(up_down) => Self::UpDown(up_down),
271 $name::NextPrevious(next_previous) => Self::NextPrevious(next_previous),
272 $name::Hsb(color) => Self::Hsb(color),
273 $name::Point(point) => Self::Point(point),
274 $name::DateTime(date_time) => Self::DateTime(date_time),
275 $name::OnOff(on_off) => Self::OnOff(on_off),
276 $name::PlayPause(play_pause) => Self::PlayPause(play_pause),
277 $name::RewindFastforward(rewind_fastforward) => Self::RewindFastforward(rewind_fastforward),
278 $name::StopMove(stop_move) => Self::StopMove(stop_move),
279 $name::OpenClosed(open_closed) => Self::OpenClosed(open_closed),
280 $name::String(string) => Self::String(string),
281 $name::StringList(string_list) => Self::StringList(string_list),
282 $name::Raw(raw) => Self::Raw(raw),
283 $name::UnDef(string) => Self::UnDef(string),
284 $name::Unknown(string) => Self::Unknown(string),
285 $name::Unimplemented => Self::Unimplemented,
286 }
287 }
288 }
289 )*
290 };
291}
292
293from_typed_values!([TypedOldValue, TypedPredictedValue]);
294
295#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
297pub struct Decimal(pub f64);
298
299impl FromStr for Decimal {
300 type Err = HabRsError;
301
302 fn from_str(s: &str) -> Result<Self, Self::Err> {
303 Ok(Self(f64::from_str(s)?))
304 }
305}
306
307impl Display for Decimal {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 write!(f, "{}", self.0)
310 }
311}
312
313#[allow(missing_docs)]
315#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
316pub struct Quantity {
317 pub value: f64,
318 pub unit: String,
319}
320
321impl FromStr for Quantity {
322 type Err = HabRsError;
323
324 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 match s.split(' ').collect::<Vec<_>>().as_slice() {
326 [value, unit] => Ok(Self {
327 value: f64::from_str(value)?,
328 unit: (*unit).to_string(),
329 }),
330 _ => Err(HabRsError::Parse(s.to_string())),
331 }
332 }
333}
334
335impl Display for Quantity {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 write!(f, "{} {}", self.value, self.unit)
338 }
339}
340
341#[allow(missing_docs)]
343#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
344pub struct Point {
345 pub latitude: f64,
346 pub longitude: f64,
347 pub altitude: Option<f64>,
348}
349
350impl FromStr for Point {
351 type Err = HabRsError;
352
353 fn from_str(s: &str) -> Result<Self, Self::Err> {
354 match s.split(',').collect::<Vec<_>>().as_slice() {
355 [latitude, longitude] => Ok(Self {
356 latitude: f64::from_str(latitude)?,
357 longitude: f64::from_str(longitude)?,
358 altitude: None,
359 }),
360 [latitude, longitude, altitude] => Ok(Self {
361 latitude: f64::from_str(latitude)?,
362 longitude: f64::from_str(longitude)?,
363 altitude: Some(f64::from_str(altitude)?),
364 }),
365 _ => Err(HabRsError::Parse(s.to_string())),
366 }
367 }
368}
369
370impl Display for Point {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 write!(f, "{},{}", self.latitude, self.longitude)?;
373 if let Some(altitude) = &self.altitude {
374 write!(f, ",{altitude}")?;
375 }
376 Ok(())
377 }
378}
379
380#[allow(missing_docs)]
381#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
382pub struct Raw {
383 pub mime_type: String,
384 pub data: Vec<u8>,
385}
386
387impl FromStr for Raw {
388 type Err = HabRsError;
389
390 fn from_str(s: &str) -> Result<Self, Self::Err> {
391 match s.split(';').collect::<Vec<_>>().as_slice() {
392 [mime_type, data] if mime_type.starts_with("data:") && data.starts_with("base64,") => {
393 Ok(Self {
394 mime_type: mime_type
395 .split_once(':')
396 .ok_or_else(|| HabRsError::Parse(s.to_string()))?
397 .1
398 .to_string(),
399 data: BASE64_STANDARD.decode(
400 data.split_once(',')
401 .ok_or_else(|| HabRsError::Parse(s.to_string()))?
402 .1,
403 )?,
404 })
405 }
406 _ => Err(HabRsError::Parse(s.to_string())),
407 }
408 }
409}
410
411impl Display for Raw {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 write!(
414 f,
415 "data:{};base64,{}",
416 self.mime_type,
417 BASE64_STANDARD.encode(&self.data)
418 )
419 }
420}
421
422static DELIMITER_RE: LazyLock<Regex> =
423 LazyLock::new(|| Regex::new(r"[^\\],").expect("Invalid regex"));
424
425#[allow(missing_docs)]
426#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
427pub struct StringList(Vec<String>);
428
429impl FromStr for StringList {
430 type Err = HabRsError;
431
432 fn from_str(s: &str) -> Result<Self, Self::Err> {
433 let delim_matches: Vec<_> = DELIMITER_RE.find_iter(s).map(|m| m.start() + 1).collect();
434 let mut strings = Vec::with_capacity(delim_matches.len() + 1);
435
436 for i in 0..=delim_matches.len() {
437 let start = if i == 0 { 0 } else { delim_matches[i - 1] + 1 };
438 let end = if i == delim_matches.len() {
439 s.len()
440 } else {
441 delim_matches[i]
442 };
443 strings.push(s[start..end].replace("\\,", ","));
444 }
445
446 Ok(Self(strings))
447 }
448}
449
450impl Display for StringList {
451 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452 write!(
453 f,
454 "{}",
455 self.0
456 .iter()
457 .map(|s| s.replace(',', "\\,"))
458 .collect::<Vec<_>>()
459 .join(",")
460 )
461 }
462}
463
464#[allow(missing_docs)]
466#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
467pub enum IncreaseDecrease {
468 #[default]
469 Increase,
470 Decrease,
471}
472
473impl FromStr for IncreaseDecrease {
474 type Err = HabRsError;
475
476 fn from_str(s: &str) -> Result<Self, Self::Err> {
477 match s {
478 "INCREASE" => Ok(Self::Increase),
479 "DECREASE" => Ok(Self::Decrease),
480 _ => Err(HabRsError::Parse(s.to_string())),
481 }
482 }
483}
484
485impl Display for IncreaseDecrease {
486 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
487 match self {
488 Self::Increase => write!(f, "INCREASE"),
489 Self::Decrease => write!(f, "DECREASE"),
490 }
491 }
492}
493
494#[allow(missing_docs)]
496#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
497pub enum NextPrevious {
498 #[default]
499 Next,
500 Previous,
501}
502
503impl FromStr for NextPrevious {
504 type Err = HabRsError;
505
506 fn from_str(s: &str) -> Result<Self, Self::Err> {
507 match s {
508 "NEXT" => Ok(Self::Next),
509 "PREVIOUS" => Ok(Self::Previous),
510 _ => Err(HabRsError::Parse(s.to_string())),
511 }
512 }
513}
514
515impl Display for NextPrevious {
516 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517 match self {
518 Self::Next => write!(f, "NEXT"),
519 Self::Previous => write!(f, "PREVIOUS"),
520 }
521 }
522}
523
524#[allow(missing_docs)]
526#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
527pub enum PlayPause {
528 #[default]
529 Play,
530 Pause,
531}
532
533impl FromStr for PlayPause {
534 type Err = HabRsError;
535
536 fn from_str(s: &str) -> Result<Self, Self::Err> {
537 match s {
538 "PLAY" => Ok(Self::Play),
539 "PAUSE" => Ok(Self::Pause),
540 _ => Err(HabRsError::Parse(s.to_string())),
541 }
542 }
543}
544
545impl Display for PlayPause {
546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547 match self {
548 Self::Play => write!(f, "PLAY"),
549 Self::Pause => write!(f, "PAUSE"),
550 }
551 }
552}
553
554#[allow(missing_docs)]
556#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
557pub enum RewindFastforward {
558 #[default]
559 Rewind,
560 Fastforward,
561}
562
563impl FromStr for RewindFastforward {
564 type Err = HabRsError;
565
566 fn from_str(s: &str) -> Result<Self, Self::Err> {
567 match s {
568 "REWIND" => Ok(Self::Rewind),
569 "FASTFORWARD" => Ok(Self::Fastforward),
570 _ => Err(HabRsError::Parse(s.to_string())),
571 }
572 }
573}
574
575impl Display for RewindFastforward {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 match self {
578 Self::Rewind => write!(f, "REWIND"),
579 Self::Fastforward => write!(f, "FASTFORWARD"),
580 }
581 }
582}
583
584#[allow(missing_docs)]
586#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
587pub enum StopMove {
588 #[default]
589 Stop,
590 Move,
591}
592
593impl FromStr for StopMove {
594 type Err = HabRsError;
595
596 fn from_str(s: &str) -> Result<Self, Self::Err> {
597 match s {
598 "STOP" => Ok(Self::Stop),
599 "MOVE" => Ok(Self::Move),
600 _ => Err(HabRsError::Parse(s.to_string())),
601 }
602 }
603}
604
605impl Display for StopMove {
606 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607 match self {
608 Self::Stop => write!(f, "STOP"),
609 Self::Move => write!(f, "MOVE"),
610 }
611 }
612}
613
614#[allow(missing_docs)]
616#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
617pub enum UpDown {
618 #[default]
619 Up,
620 Down,
621}
622
623impl FromStr for UpDown {
624 type Err = HabRsError;
625
626 fn from_str(s: &str) -> Result<Self, Self::Err> {
627 match s {
628 "UP" => Ok(Self::Up),
629 "DOWN" => Ok(Self::Down),
630 _ => Err(HabRsError::Parse(s.to_string())),
631 }
632 }
633}
634
635impl Display for UpDown {
636 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637 match self {
638 Self::Up => write!(f, "UP"),
639 Self::Down => write!(f, "DOWN"),
640 }
641 }
642}
643
644#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
646pub struct Hsb(pub Hsv);
647
648impl FromStr for Hsb {
649 type Err = HabRsError;
650
651 fn from_str(s: &str) -> Result<Self, Self::Err> {
652 match s.split(',').collect::<Vec<_>>().as_slice() {
653 [h, s, b] => Ok(Self(Hsv::new_srgb(
654 f32::from_str(h)?,
655 f32::from_str(s)? / 100.0,
656 f32::from_str(b)? / 100.0,
657 ))),
658 _ => Err(HabRsError::Parse(s.to_string())),
659 }
660 }
661}
662
663impl Display for Hsb {
664 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665 write!(
666 f,
667 "{},{},{}",
668 self.0.hue.into_positive_degrees(),
669 self.0.saturation * 100.0,
670 self.0.value * 100.0
671 )
672 }
673}
674
675#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
677pub struct DateTime(pub chrono::DateTime<FixedOffset>);
678
679impl FromStr for DateTime {
680 type Err = HabRsError;
681
682 fn from_str(s: &str) -> Result<Self, Self::Err> {
683 Ok(Self(chrono::DateTime::from_str(s)?))
684 }
685}
686
687impl Display for DateTime {
688 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
689 write!(f, "{}", self.0.format("%Y-%m-%dT%H:%M:%S%.3f%z"))
690 }
691}
692
693#[allow(missing_docs)]
695#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
696pub enum OnOff {
697 #[default]
698 On,
699 Off,
700}
701
702impl FromStr for OnOff {
703 type Err = HabRsError;
704
705 fn from_str(s: &str) -> Result<Self, Self::Err> {
706 match s {
707 "ON" => Ok(Self::On),
708 "OFF" => Ok(Self::Off),
709 _ => Err(HabRsError::Parse(s.to_string())),
710 }
711 }
712}
713
714impl Display for OnOff {
715 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
716 match self {
717 Self::On => write!(f, "ON"),
718 Self::Off => write!(f, "OFF"),
719 }
720 }
721}
722
723#[allow(missing_docs)]
725#[derive(Debug, Copy, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
726pub enum OpenClosed {
727 #[default]
728 Open,
729 Closed,
730}
731
732impl FromStr for OpenClosed {
733 type Err = HabRsError;
734
735 fn from_str(s: &str) -> Result<Self, Self::Err> {
736 match s {
737 "OPEN" => Ok(Self::Open),
738 "CLOSED" => Ok(Self::Closed),
739 _ => Err(HabRsError::Parse(s.to_string())),
740 }
741 }
742}
743
744impl Display for OpenClosed {
745 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746 match self {
747 Self::Open => write!(f, "OPEN"),
748 Self::Closed => write!(f, "CLOSED"),
749 }
750 }
751}
752
753#[allow(missing_docs)]
757#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay, Default)]
758pub struct Topic {
759 pub namespace: String,
760 pub entity_type: String,
761 pub entity: String,
762 pub sub_entity: Option<String>,
764 pub action: String,
765}
766
767impl FromStr for Topic {
768 type Err = HabRsError;
769
770 fn from_str(s: &str) -> Result<Self, Self::Err> {
771 if s.chars().any(char::is_whitespace) {
772 return Err(HabRsError::Parse(s.to_string()));
773 }
774 match s.split('/').collect::<Vec<_>>().as_slice() {
775 [namespace, entity_type, entity, action] => Ok(Self {
776 namespace: (*namespace).to_string(),
777 entity_type: (*entity_type).to_string(),
778 entity: (*entity).to_string(),
779 sub_entity: None,
780 action: (*action).to_string(),
781 }),
782 [namespace, entity_type, entity, sub_entity, action] => Ok(Self {
783 namespace: (*namespace).to_string(),
784 entity_type: (*entity_type).to_string(),
785 entity: (*entity).to_string(),
786 sub_entity: Some((*sub_entity).to_string()),
787 action: (*action).to_string(),
788 }),
789 _ => Err(HabRsError::Parse(s.to_string())),
790 }
791 }
792}
793
794impl Display for Topic {
795 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796 write!(
797 f,
798 "{}/{}/{}/{}",
799 self.namespace, self.entity_type, self.entity, self.action
800 )
801 }
802}
803
804#[cfg(test)]
805mod tests {
806 use paste::paste;
807 use rstest::rstest;
808
809 use super::*;
810
811 #[test]
812 fn test_parse_event() {
813 let event_str = r#"event: message
814data: {"topic":"openhab/things/jeelink:lacrosse:40/status","payload":"{\"status\":\"ONLINE\",\"statusDetail\":\"NONE\"}","type":"ThingStatusInfoEvent"}"#;
815
816 let event = event_str.parse::<Event>().unwrap();
817 assert_eq!(
818 event,
819 Event::Message(Message {
820 topic: Topic {
821 namespace: "openhab".to_string(),
822 entity_type: "things".to_string(),
823 entity: "jeelink:lacrosse:40".to_string(),
824 sub_entity: None,
825 action: "status".to_string(),
826 },
827 message_type: MessageType::ThingStatusInfoEvent(StatusInfoEvent {
828 status: "ONLINE".to_string(),
829 status_detail: "NONE".to_string(),
830 description: None,
831 }),
832 })
833 );
834 }
835
836 #[test]
837 fn test_state_changed_event() {
838 let message_data = r#"{"topic":"openhab/items/Arbeit_Steck_P_power/statechanged","payload":"{\"type\":\"Decimal\",\"value\":\"222.23\",\"oldType\":\"Decimal\",\"oldValue\":\"225.99\"}","type":"ItemStateChangedEvent"}"#;
839 let message: Message = serde_json::from_str(message_data).unwrap();
840 assert_eq!(
841 message.message_type,
842 MessageType::ItemStateChangedEvent(StateChangedEvent {
843 value: TypedValue::Decimal(Decimal(222.23)),
844 old_value: TypedOldValue::Decimal(Decimal(225.99))
845 })
846 );
847 }
848
849 #[test]
850 fn test_state_event() {
851 let message_data = r#"{"topic":"openhab/items/Arbeit_Steck_3DD_EnergyTotal/state","payload":"{\"type\":\"Decimal\",\"value\":\"31.325\"}","type":"ItemStateEvent"}"#;
852 let message: Message = serde_json::from_str(message_data).unwrap();
853 assert_eq!(
854 message.message_type,
855 MessageType::ItemStateEvent(StateUpdatedEvent {
856 value: TypedValue::Decimal(Decimal(31.325)),
857 })
858 );
859 }
860
861 #[test]
862 fn test_string_list() {
863 let s = r"FirstString,Second\,String\,,ThirdString";
864 let string_list = StringList::from_str(s).unwrap();
865 assert_eq!(
866 string_list,
867 StringList(vec![
868 "FirstString".to_string(),
869 "Second,String,".to_string(),
870 "ThirdString".to_string()
871 ])
872 );
873 }
874
875 macro_rules! enum_test {
876 ($first:ident, $second:ident) => {
877 paste! {
878 #[rstest]
879 #[case::[<value _ $first:lower>]([<$first $second>]::$first, stringify!([<$first:upper>]))]
880 #[case::[<value _ $second:lower>]([<$first $second>]::$second, stringify!([<$second:upper>]))]
881 fn [<test_ $first:lower _ $second:lower>](#[case] val: [<$first $second>], #[case] exp_str: &str) {
882 let str = val.to_string();
883 assert_eq!(str, exp_str);
884 let val_from_str: [<$first $second>] = str.parse().unwrap();
885 assert_eq!(val_from_str, val);
886 }
887 }
888 };
889 }
890
891 enum_test!(Increase, Decrease);
892 enum_test!(Up, Down);
893 enum_test!(Next, Previous);
894 enum_test!(On, Off);
895 enum_test!(Play, Pause);
896 enum_test!(Rewind, Fastforward);
897 enum_test!(Stop, Move);
898 enum_test!(Open, Closed);
899}