Skip to main content

fennec_modbus/contrib/mq2200/
schedule.rs

1use core::fmt::{Display, Formatter};
2
3use bytes::{Buf, BufMut};
4
5use crate::{
6    Error,
7    contrib::{Percentage, Watts},
8    protocol::{
9        Address,
10        address,
11        codec::{BitSize, Decode, Encode},
12    },
13};
14
15/// Stride of schedule entry blocks.
16///
17/// There are [`Entry::N_TOTAL`] schedule entries starting from here.
18pub type BlockStride = address::Stride<48010, Block>;
19
20/// Number of entries per schedule block.
21///
22/// There are [`N_BLOCKS`] such blocks.
23pub const N_ENTRIES_PER_BLOCK: usize = 12;
24
25/// Number of schedule blocks, each consisting of [`N_ENTRIES_PER_BLOCK`] entries.
26pub const N_BLOCKS: usize = 8;
27
28/// Full schedule type alias.
29///
30/// Note that this is not encodable nor decodable as it doesn't fit the Modbus payload size.
31/// The type alias is provided solely for convenience.
32pub type Full = [Entry; Entry::N_TOTAL];
33
34/// Schedule block consisting of 12 entries.
35pub type Block = [Entry; N_ENTRIES_PER_BLOCK];
36
37/// Block index for batch-reading 12 schedule entries at a time.
38///
39/// There are 8 blocks (indices 0–7), covering all 96 entries.
40#[must_use]
41#[derive(Copy, Clone)]
42pub struct BlockIndex(pub u16);
43
44impl BlockIndex {
45    #[expect(clippy::cast_possible_truncation)]
46    pub const MAX: u16 = (N_BLOCKS - 1) as u16;
47}
48
49impl Address for BlockIndex {}
50
51impl Encode for BlockIndex {
52    fn encode(&self, to: &mut impl BufMut) {
53        BlockStride::from(self.0).encode(to);
54    }
55}
56
57#[derive(Copy, Clone, Debug, Eq, PartialEq)]
58#[repr(u16)]
59#[must_use]
60pub enum WorkingMode {
61    SelfUse = 1_u16,
62    FeedInPriority = 2_u16,
63    BackUp = 3_u16,
64    PeakShaving = 4_u16,
65    ForceCharge = 6_u16,
66    ForceDischarge = 7_u16,
67    Unknown(u16),
68}
69
70impl Encode for WorkingMode {
71    fn encode(&self, to: &mut impl BufMut) {
72        to.put_u16(match self {
73            Self::SelfUse => 1,
74            Self::FeedInPriority => 2,
75            Self::BackUp => 3,
76            Self::PeakShaving => 4,
77            Self::ForceCharge => 6,
78            Self::ForceDischarge => 7,
79            Self::Unknown(working_mode) => *working_mode,
80        });
81    }
82}
83
84impl Decode for WorkingMode {
85    fn decode(from: &mut impl Buf) -> Result<Self, Error> {
86        Ok(match from.try_get_u16()? {
87            1 => Self::SelfUse,
88            2 => Self::FeedInPriority,
89            3 => Self::BackUp,
90            4 => Self::PeakShaving,
91            6 => Self::ForceCharge,
92            7 => Self::ForceDischarge,
93            working_mode => Self::Unknown(working_mode),
94        })
95    }
96}
97
98/// Scheduler entry start or end time.
99#[derive(Copy, Clone, Debug, Eq, PartialEq)]
100#[must_use]
101pub struct NaiveTime {
102    pub hour: u8,
103    pub minute: u8,
104}
105
106impl Display for NaiveTime {
107    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
108        write!(f, "{:02}:{:02}", self.hour, self.minute)
109    }
110}
111
112impl NaiveTime {
113    /// The first minute of a day.
114    pub const MIN: Self = Self { hour: 0, minute: 0 };
115
116    /// The last minute of a day.
117    ///
118    /// Note that it is always _inclusive_.
119    pub const MAX: Self = Self { hour: 23, minute: 59 };
120}
121
122impl Encode for NaiveTime {
123    fn encode(&self, to: &mut impl BufMut) {
124        to.put_u8(self.hour);
125        to.put_u8(self.minute);
126    }
127}
128
129impl Decode for NaiveTime {
130    fn decode(from: &mut impl Buf) -> Result<Self, Error> {
131        Ok(Self { hour: from.try_get_u8()?, minute: from.try_get_u8()? })
132    }
133}
134
135/// Single schedule entry.
136#[derive(Copy, Clone, Debug, Eq, PartialEq)]
137#[must_use]
138pub struct Entry {
139    pub is_enabled: bool,
140
141    /// Time slot start time, inclusive.
142    pub start_time: NaiveTime,
143
144    /// Time slot end time, exclusive.
145    ///
146    /// Note that 23:59 is special as it is *inclusive*. 00:00 cannot be set as end time.
147    /// Confirmed with Fox ESS support that this the intended behaviour.
148    pub end_time: NaiveTime,
149
150    pub working_mode: WorkingMode,
151    pub maximum_state_of_charge: Percentage<u8>,
152    pub minimum_state_of_charge: Percentage<u8>,
153
154    /// This is called "feed SoC" or "fdSoC", but in reality, it is a target SoC
155    /// for charging or discharging.
156    #[allow(clippy::doc_markdown)]
157    pub target_state_of_charge: Percentage<u16>,
158
159    pub power: Watts<u16>,
160
161    /// Reserved, set to zero.
162    pub reserved_1: u16,
163
164    /// Reserved, set to zero.
165    pub reserved_2: u16,
166
167    /// Reserved, set to zero.
168    pub reserved_3: u16,
169}
170
171impl Entry {
172    /// Total number of schedule entries in the register space.
173    pub const N_TOTAL: usize = N_BLOCKS * N_ENTRIES_PER_BLOCK;
174}
175
176impl BitSize for Entry {
177    const N_BITS: u16 = 20 * 8;
178}
179
180impl Encode for Entry {
181    fn encode(&self, to: &mut impl BufMut) {
182        to.put_u16(u16::from(self.is_enabled));
183        self.start_time.encode(to);
184        self.end_time.encode(to);
185        self.working_mode.encode(to);
186        to.put_u8(self.maximum_state_of_charge.0);
187        to.put_u8(self.minimum_state_of_charge.0);
188        self.target_state_of_charge.encode(to);
189        self.power.encode(to);
190        self.reserved_1.encode(to);
191        self.reserved_2.encode(to);
192        self.reserved_3.encode(to);
193    }
194}
195
196impl Decode for Entry {
197    fn decode(from: &mut impl Buf) -> Result<Self, Error> {
198        Ok(Self {
199            is_enabled: from.try_get_u16()? != 0,
200            start_time: NaiveTime::decode(from)?,
201            end_time: NaiveTime::decode(from)?,
202            working_mode: WorkingMode::decode(from)?,
203            maximum_state_of_charge: Percentage(from.try_get_u8()?),
204            minimum_state_of_charge: Percentage(from.try_get_u8()?),
205            target_state_of_charge: Percentage::decode(from)?,
206            power: Watts::decode(from)?,
207            reserved_1: u16::decode(from)?,
208            reserved_2: u16::decode(from)?,
209            reserved_3: u16::decode(from)?,
210        })
211    }
212}