hab_rs/
event.rs

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/// This struct represents an event coming from openHAB's event bus as fetched via REST-API.
13#[derive(Debug, Clone, PartialEq)]
14#[non_exhaustive]
15#[allow(clippy::large_enum_variant)]
16pub enum Event {
17    /// Message event
18    Message(Message),
19    /// Alive event
20    Alive,
21    /// Unknown event
22    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/// Wrapper around an event not known to hab-rs
59#[allow(missing_docs)]
60#[derive(Debug, Clone, PartialEq)]
61pub struct UnknownEvent {
62    pub event_type: String,
63    pub data: String,
64}
65
66/// This struct represents an event message coming from openHAB's event bus as fetched via REST-API.
67///
68/// See <https://www.openhab.org/docs/developer/utils/events.html>.
69#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
70pub struct Message {
71    /// Topic of the message
72    pub topic: Topic,
73    /// Type of the message
74    #[serde(flatten)]
75    pub message_type: MessageType,
76}
77
78impl Message {
79    /// Get the [`MessageType`] when this message matches the given entity name.
80    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/// Event type of a [Message].
90///
91/// See <https://www.openhab.org/docs/developer/utils/events.html#the-core-events>.
92#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
93#[non_exhaustive]
94#[serde(tag = "type", content = "payload")]
95pub enum MessageType {
96    /// The state of an item is updated.
97    #[serde(with = "serde_nested_json")]
98    ItemStateEvent(StateUpdatedEvent),
99    /// The state of an item has changed.
100    #[serde(with = "serde_nested_json")]
101    ItemStateChangedEvent(StateChangedEvent),
102    /// The state of a group item has changed through a member.
103    #[serde(with = "serde_nested_json")]
104    GroupItemStateChangedEvent(StateChangedEvent),
105    /// Description not present in the openHAB documentation.
106    #[serde(with = "serde_nested_json")]
107    ItemStateUpdatedEvent(StateUpdatedEvent),
108    /// The state of an item predicted to be updated.
109    #[serde(with = "serde_nested_json")]
110    ItemStatePredictedEvent(StatePredictedEvent),
111    /// Description not present in the openHAB documentation.
112    #[serde(with = "serde_nested_json")]
113    GroupStateUpdatedEvent(StateUpdatedEvent),
114    /// A command is sent to an item via a channel.
115    #[serde(with = "serde_nested_json")]
116    ItemCommandEvent(StateUpdatedEvent),
117    /// Description not present in the openHAB documentation.
118    #[serde(with = "serde_nested_json")]
119    RuleStatusInfoEvent(StatusInfoEvent),
120    /// The status of a thing is updated.
121    #[serde(with = "serde_nested_json")]
122    ThingStatusInfoEvent(StatusInfoEvent),
123    /// The status of a thing changed.
124    #[serde(with = "serde_nested_json")]
125    ThingStatusInfoChangedEvent([StatusInfoEvent; 2]),
126    /// A channel has been triggered.
127    #[serde(with = "serde_nested_json")]
128    ChannelTriggeredEvent(ChannelTriggeredEvent),
129    /// Unknown/not implemented
130    #[serde(other)]
131    Unknown,
132}
133
134/// The status of an entity is updated.
135#[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/// The state of an entity has changed.
146#[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/// An entity has been updated.
157#[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/// The state of an entity predicted to be updated.
167#[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/// A channel has been triggered.
178#[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            /// State and command types
191            ///
192            /// See
193            /// <https://www.openhab.org/docs/concepts/items.html#state-and-command-type-formatting>.
194            #[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/// <https://www.openhab.org/docs/concepts/items.html#decimaltype-percenttype>
296#[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/// <https://www.openhab.org/docs/concepts/items.html#quantitytype>
314#[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/// <https://www.openhab.org/docs/concepts/items.html#pointtype>
342#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
465#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
495#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
525#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
555#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
585#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
615#[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/// <https://www.openhab.org/docs/concepts/items.html#hsbtype>
645#[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/// <https://www.openhab.org/docs/concepts/items.html#datetimetype>
676#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
694#[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/// <https://www.openhab.org/docs/concepts/items.html#enum-types>
724#[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/// A topic clearly defines the target of the event and its structure is similar to a REST URI, except the last part, the action.
754///
755/// See <https://www.openhab.org/docs/developer/utils/events.html#the-core-events>.
756#[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    /// `GroupItemStateChangedEvent` has an additional topic part
763    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}