1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
use std::{fs, net::IpAddr, path::PathBuf};

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use log::{error, trace, warn};
use serde::Deserialize;

use crate::io::live_data::{check_age, AgeType, CachedPrevious};

use super::{
    hub::{IpWiserHub, WiserHub, WiserRoomData},
    WiserManager,
};

pub struct FileAndHub {
    file: PathBuf,
    last_data: CachedPrevious<WiserFileData>,
    hub: IpWiserHub,
}

impl FileAndHub {
    pub fn new(file: PathBuf, ip: IpAddr, secret: String) -> Self {
        Self {
            file,
            hub: IpWiserHub::new(ip, secret),
            last_data: CachedPrevious::none(),
        }
    }

    fn retrieve_data(&self) -> Result<WiserFileData, String> {
        let data = fs::read_to_string(&self.file)
            .map_err(|e| format!("Error reading {:?}: {}", self.file, e))?;

        serde_json::from_str(&data)
            .map_err(|e| format!("Error deserializing {:?}: {}\n{}", self.file, e, data))
    }
}

/// How long before we reject the file for being too outdated.
/// If this is too old then our data collection is broken.
const MAX_FILE_AGE_SECONDS: i64 = 2 * 60;
/// How long before we reject the data within the file for being too outdated
/// i.e. how long wiser we allow wiser to not respond for before taking action
const MAX_WISER_AGE_SECONDS: i64 = 10 * 60;

#[async_trait]
impl WiserManager for FileAndHub {
    async fn get_heating_turn_off_time(&self) -> Option<DateTime<Utc>> {
        let data = self.hub.get_room_data().await;
        if let Err(e) = data {
            error!("Error retrieving hub data: {:?}", e);
            return None;
        }
        let data = data.unwrap();
        get_turn_off_time(&data)
    }

    async fn get_heating_on(&self) -> Result<bool, ()> {
        let wiser_file_data = match self.retrieve_data() {
            Ok(data) => {
                self.last_data.update(data.clone());
                data
            }
            Err(e) => match self.last_data.get() {
                Some(data) => {
                    warn!("Failed to get current wiser data: {}, using previous", e);
                    data
                }
                None => {
                    error!(
                        "Failed to get current wiser data: {}, and no previous available.",
                        e
                    );
                    return Err(());
                }
            },
        };

        let file_age = check_age(wiser_file_data.timestamp, MAX_FILE_AGE_SECONDS);
        match file_age.age_type() {
            AgeType::Good => {
                trace!("{:?} {}", self.file, file_age)
            }
            AgeType::GettingOld => warn!("{:?}: {}", self.file, file_age),
            AgeType::TooOld => {
                error!("{:?}: {} - file is not up to date", self.file, file_age);
                return Err(());
            }
        };

        let wiser_heating_age = check_age(
            wiser_file_data.wiser.heating.timestamp,
            MAX_WISER_AGE_SECONDS,
        );
        match wiser_heating_age.age_type() {
            AgeType::Good => {
                trace!("heating on in: {:?}: {}", self.file, wiser_heating_age);
            }
            AgeType::GettingOld => warn!("heating on in: {:?}: {}", self.file, wiser_heating_age),
            AgeType::TooOld => {
                error!("heating on in {:?} {}", self.file, wiser_heating_age);
                return Err(());
            }
        }
        return Ok(wiser_file_data.wiser.heating.on);
    }

    fn get_wiser_hub(&self) -> &dyn WiserHub {
        &self.hub
    }
}

fn get_turn_off_time(data: &[WiserRoomData]) -> Option<DateTime<Utc>> {
    data.iter()
        .filter_map(|room| room.get_override_timeout())
        .max()
}

#[derive(Deserialize, Debug, PartialEq, Clone)]
struct WiserFileData {
    pub wiser: WiserData,
    pub timestamp: DateTime<Utc>,
}

#[derive(Deserialize, Debug, PartialEq, Clone)]
struct WiserData {
    pub heating: TimestampedOnValue,
    //pub away_mode: TimestampedOnValue,
}

#[derive(Deserialize, Debug, PartialEq, Clone)]
struct TimestampedOnValue {
    pub on: bool,
    pub timestamp: DateTime<Utc>,
}

#[cfg(test)]
mod test {
    use chrono::TimeZone;

    use crate::time_util::test_utils::{date, time};

    use super::*;

    const EXAMPLE_DATA: &str = r#"
    {
        "timestamp": "2024-01-03T15:35:32Z",
        "wiser": {
            "away_mode": {
                "on": false,
                "timestamp": "2024-01-03T15:35:29Z"
            },
            "heating": {
                "on": false,
                "timestamp": "2024-01-03T15:35:29Z"
            }
        }
    }
    "#;

    #[test]
    fn test_deserialize() {
        let actual: WiserFileData = serde_json::from_str(EXAMPLE_DATA).unwrap();

        let main_timestamp = Utc.from_utc_datetime(&date(2024, 1, 3).and_time(time(15, 35, 32)));
        let heating_timestamp = Utc.from_utc_datetime(&date(2024, 1, 3).and_time(time(15, 35, 29)));

        let expected = WiserFileData {
            timestamp: main_timestamp,
            wiser: WiserData {
                heating: TimestampedOnValue {
                    on: false,
                    timestamp: heating_timestamp,
                },
            },
        };

        assert_eq!(actual, expected);
    }
}