sdm72_lib/
tokio_async.rs

1//! This module provides an asynchronous client for the SDM72 energy meter.
2//!
3//! The [`SDM72`] struct is the main entry point for interacting with the meter. It
4//! wraps an asynchronous `tokio-modbus` context and provides high-level methods
5//! for reading and writing meter data.
6//!
7//! This client is suitable for applications that require non-blocking I/O and
8//! is designed to be used within a `tokio` runtime. For applications that do
9//! not use `tokio`, the synchronous client in the [`crate::tokio_sync`]
10//! module may be more suitable.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use sdm72_lib::{
16//!     protocol::Address,
17//!     tokio_async::SDM72,
18//! };
19//! use tokio_modbus::client::tcp;
20//! use tokio_modbus::Slave;
21//! use std::time::Duration;
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
25//!     let socket_addr = "192.168.1.100:502".parse()?;
26//!     let mut ctx = tcp::connect_slave(socket_addr, Slave(*Address::default())).await?;
27//!
28//!     let values = SDM72::read_all(&mut ctx, &Duration::from_millis(100)).await?;
29//!
30//!     println!("Successfully read values: {:#?}", values);
31//!
32//!     Ok(())
33//! }
34//! ```
35
36use crate::{
37    protocol::{self as proto, ModbusParam},
38    tokio_common::{AllSettings, AllValues, Result},
39};
40use tokio_modbus::prelude::{Reader, Writer};
41
42/// An asynchronous client for the SDM72 energy meter.
43///
44/// This struct provides a high-level interface for interacting with the SDM72
45/// energy meter. It uses an asynchronous `tokio-modbus` context for communication.
46/// An instance of this client can be created using the [`new`](#method.new) method.
47pub struct SDM72;
48
49/// A macro to generate an async function for reading a holding register.
50macro_rules! read_holding {
51    ($func_name:expr, $ty:ident) => {
52        paste::item! {
53            #[doc = "Reads the [`proto::" $ty "`] value from the Modbus holding register."]
54            pub async fn $func_name(ctx: &mut tokio_modbus::client::Context) -> Result<proto::$ty> {
55                let rsp = ctx
56                    .read_holding_registers(<proto::$ty>::ADDRESS, <proto::$ty>::QUANTITY).await??;
57                Ok(<proto::$ty>::decode_from_holding_registers(&rsp)?)
58            }
59        }
60    };
61}
62
63/// A macro to generate an async function for writing a holding register.
64macro_rules! write_holding {
65    ($func_name:expr, $ty:ident) => {
66        paste::item! {
67            #[doc = "Writes the [`proto::" $ty "`] value to the Modbus holding register."]
68            pub async fn [< set_ $func_name >](ctx: &mut tokio_modbus::client::Context, value: proto::$ty) -> Result<()> {
69                Ok(ctx.write_multiple_registers(
70                    <proto::$ty>::ADDRESS,
71                    &value.encode_for_write_registers(),
72                ).await??)
73            }
74        }
75    };
76}
77
78impl SDM72 {
79    read_holding!(system_type, SystemType);
80    write_holding!(system_type, SystemType);
81    read_holding!(pulse_width, PulseWidth);
82    write_holding!(pulse_width, PulseWidth);
83    read_holding!(kppa, KPPA);
84    /// Sets the Key Parameter Programming Authorization (KPPA).
85    ///
86    /// This is required to change settings on the meter.
87    pub async fn set_kppa(
88        ctx: &mut tokio_modbus::client::Context,
89        password: proto::Password,
90    ) -> Result<()> {
91        Ok(ctx
92            .write_multiple_registers(
93                proto::KPPA::ADDRESS,
94                &proto::KPPA::encode_for_write_registers(password),
95            )
96            .await??)
97    }
98    read_holding!(parity_and_stop_bit, ParityAndStopBit);
99    write_holding!(parity_and_stop_bit, ParityAndStopBit);
100    read_holding!(address, Address);
101    write_holding!(address, Address);
102    read_holding!(pulse_constant, PulseConstant);
103    write_holding!(pulse_constant, PulseConstant);
104    read_holding!(password, Password);
105    write_holding!(password, Password);
106    read_holding!(baud_rate, BaudRate);
107    write_holding!(baud_rate, BaudRate);
108    read_holding!(auto_scroll_time, AutoScrollTime);
109    write_holding!(auto_scroll_time, AutoScrollTime);
110    read_holding!(backlight_time, BacklightTime);
111    write_holding!(backlight_time, BacklightTime);
112    read_holding!(pulse_energy_type, PulseEnergyType);
113    write_holding!(pulse_energy_type, PulseEnergyType);
114    /// Resets the historical data on the meter.
115    ///
116    /// This requires KPPA authorization.
117    pub async fn reset_historical_data(ctx: &mut tokio_modbus::client::Context) -> Result<()> {
118        Ok(ctx
119            .write_multiple_registers(
120                proto::ResetHistoricalData::ADDRESS,
121                &proto::ResetHistoricalData::encode_for_write_registers(),
122            )
123            .await??)
124    }
125    read_holding!(serial_number, SerialNumber);
126    read_holding!(meter_code, MeterCode);
127    read_holding!(software_version, SoftwareVersion);
128
129    /// Reads all settings from the meter in a single batch operation.
130    ///
131    /// This method is more efficient than reading each setting individually because
132    /// it minimizes the number of Modbus requests by batching them. The SDM72
133    /// meter has a limit of 30 parameters per request, so this function splits
134    /// the reads into multiple batches.
135    ///
136    /// # Arguments
137    ///
138    /// * `delay` - The delay to be inserted between Modbus requests. This is
139    ///   necessary for some Modbus devices, which may need a short pause to
140    ///   process a request before they are ready to accept the next one. A
141    ///   typical value is 100 milliseconds, but this may vary depending on the
142    ///   device and network conditions.
143    pub async fn read_all_settings(
144        ctx: &mut tokio_modbus::client::Context,
145        delay: &std::time::Duration,
146    ) -> Result<AllSettings> {
147        let offset1 = proto::SystemType::ADDRESS;
148        let quantity =
149            { proto::PulseEnergyType::ADDRESS - offset1 + proto::PulseEnergyType::QUANTITY };
150        let rsp1 = ctx.read_holding_registers(offset1, quantity).await??;
151
152        tokio::time::sleep(*delay).await;
153        let serial_number = Self::serial_number(ctx).await?;
154        tokio::time::sleep(*delay).await;
155        let meter_code = Self::meter_code(ctx).await?;
156        tokio::time::sleep(*delay).await;
157        let software_version = Self::software_version(ctx).await?;
158
159        Ok(AllSettings {
160            system_type: crate::decode_subset_item_from_holding_register!(
161                offset1,
162                proto::SystemType,
163                &rsp1
164            )?,
165            pulse_width: crate::decode_subset_item_from_holding_register!(
166                offset1,
167                proto::PulseWidth,
168                &rsp1
169            )?,
170            kppa: crate::decode_subset_item_from_holding_register!(offset1, proto::KPPA, &rsp1)?,
171            parity_and_stop_bit: crate::decode_subset_item_from_holding_register!(
172                offset1,
173                proto::ParityAndStopBit,
174                &rsp1
175            )?,
176            address: crate::decode_subset_item_from_holding_register!(
177                offset1,
178                proto::Address,
179                &rsp1
180            )?,
181            pulse_constant: crate::decode_subset_item_from_holding_register!(
182                offset1,
183                proto::PulseConstant,
184                &rsp1
185            )?,
186            password: crate::decode_subset_item_from_holding_register!(
187                offset1,
188                proto::Password,
189                &rsp1
190            )?,
191            baud_rate: crate::decode_subset_item_from_holding_register!(
192                offset1,
193                proto::BaudRate,
194                &rsp1
195            )?,
196            auto_scroll_time: crate::decode_subset_item_from_holding_register!(
197                offset1,
198                proto::AutoScrollTime,
199                &rsp1
200            )?,
201            backlight_time: crate::decode_subset_item_from_holding_register!(
202                offset1,
203                proto::BacklightTime,
204                &rsp1
205            )?,
206            pulse_energy_type: crate::decode_subset_item_from_holding_register!(
207                offset1,
208                proto::PulseEnergyType,
209                &rsp1
210            )?,
211            serial_number,
212            meter_code,
213            software_version,
214        })
215    }
216
217    /// Reads all measurement values from the meter in a single batch operation.
218    ///
219    /// This method is more efficient than reading each value individually because
220    /// it minimizes the number of Modbus requests by batching them. The SDM72
221    /// meter has a limit of 30 parameters per request, so this function splits
222    /// the reads into multiple batches.
223    ///
224    /// # Arguments
225    ///
226    /// * `delay` - The delay to be inserted between Modbus requests. This is
227    ///   necessary for some Modbus devices, which may need a short pause to
228    ///   process a request before they are ready to accept the next one. A
229    ///   typical value is 100 milliseconds, but this may vary depending on the
230    ///   device and network conditions.
231    pub async fn read_all(
232        ctx: &mut tokio_modbus::client::Context,
233        delay: &std::time::Duration,
234    ) -> Result<AllValues> {
235        let offset1 = proto::L1Voltage::ADDRESS;
236        let quantity =
237            { proto::ExportEnergyActive::ADDRESS - offset1 + proto::ExportEnergyActive::QUANTITY };
238        let rsp1 = ctx.read_input_registers(offset1, quantity).await??;
239
240        tokio::time::sleep(*delay).await;
241
242        let offset2 = proto::L1ToL2Voltage::ADDRESS;
243        let quantity =
244            { proto::NeutralCurrent::ADDRESS - offset2 + proto::NeutralCurrent::QUANTITY };
245        let rsp2 = ctx.read_input_registers(offset2, quantity).await??;
246
247        tokio::time::sleep(*delay).await;
248
249        let offset3 = proto::TotalEnergyActive::ADDRESS;
250        let quantity = { proto::NetKwh::ADDRESS - offset3 + proto::NetKwh::QUANTITY };
251        let rsp3 = ctx.read_input_registers(offset3, quantity).await??;
252
253        tokio::time::sleep(*delay).await;
254
255        let offset4 = proto::ImportTotalPowerActive::ADDRESS;
256        let quantity = {
257            proto::ExportTotalPowerActive::ADDRESS - offset4
258                + proto::ExportTotalPowerActive::QUANTITY
259        };
260        let rsp4 = ctx.read_input_registers(offset4, quantity).await??;
261
262        Ok(AllValues {
263            l1_voltage: crate::decode_subset_item_from_input_register!(
264                offset1,
265                proto::L1Voltage,
266                &rsp1
267            )?,
268            l2_voltage: crate::decode_subset_item_from_input_register!(
269                offset1,
270                proto::L2Voltage,
271                &rsp1
272            )?,
273            l3_voltage: crate::decode_subset_item_from_input_register!(
274                offset1,
275                proto::L3Voltage,
276                &rsp1
277            )?,
278            l1_current: crate::decode_subset_item_from_input_register!(
279                offset1,
280                proto::L1Current,
281                &rsp1
282            )?,
283            l2_current: crate::decode_subset_item_from_input_register!(
284                offset1,
285                proto::L2Current,
286                &rsp1
287            )?,
288            l3_current: crate::decode_subset_item_from_input_register!(
289                offset1,
290                proto::L3Current,
291                &rsp1
292            )?,
293            l1_power_active: crate::decode_subset_item_from_input_register!(
294                offset1,
295                proto::L1PowerActive,
296                &rsp1
297            )?,
298            l2_power_active: crate::decode_subset_item_from_input_register!(
299                offset1,
300                proto::L2PowerActive,
301                &rsp1
302            )?,
303            l3_power_active: crate::decode_subset_item_from_input_register!(
304                offset1,
305                proto::L3PowerActive,
306                &rsp1
307            )?,
308            l1_power_apparent: crate::decode_subset_item_from_input_register!(
309                offset1,
310                proto::L1PowerApparent,
311                &rsp1
312            )?,
313            l2_power_apparent: crate::decode_subset_item_from_input_register!(
314                offset1,
315                proto::L2PowerApparent,
316                &rsp1
317            )?,
318            l3_power_apparent: crate::decode_subset_item_from_input_register!(
319                offset1,
320                proto::L3PowerApparent,
321                &rsp1
322            )?,
323            l1_power_reactive: crate::decode_subset_item_from_input_register!(
324                offset1,
325                proto::L1PowerReactive,
326                &rsp1
327            )?,
328            l2_power_reactive: crate::decode_subset_item_from_input_register!(
329                offset1,
330                proto::L2PowerReactive,
331                &rsp1
332            )?,
333            l3_power_reactive: crate::decode_subset_item_from_input_register!(
334                offset1,
335                proto::L3PowerReactive,
336                &rsp1
337            )?,
338            l1_power_factor: crate::decode_subset_item_from_input_register!(
339                offset1,
340                proto::L1PowerFactor,
341                &rsp1
342            )?,
343            l2_power_factor: crate::decode_subset_item_from_input_register!(
344                offset1,
345                proto::L2PowerFactor,
346                &rsp1
347            )?,
348            l3_power_factor: crate::decode_subset_item_from_input_register!(
349                offset1,
350                proto::L3PowerFactor,
351                &rsp1
352            )?,
353            ln_average_voltage: crate::decode_subset_item_from_input_register!(
354                offset1,
355                proto::LtoNAverageVoltage,
356                &rsp1
357            )?,
358            ln_average_current: crate::decode_subset_item_from_input_register!(
359                offset1,
360                proto::LtoNAverageCurrent,
361                &rsp1
362            )?,
363            total_line_current: crate::decode_subset_item_from_input_register!(
364                offset1,
365                proto::TotalLineCurrent,
366                &rsp1
367            )?,
368            total_power: crate::decode_subset_item_from_input_register!(
369                offset1,
370                proto::TotalPower,
371                &rsp1
372            )?,
373            total_power_apparent: crate::decode_subset_item_from_input_register!(
374                offset1,
375                proto::TotalPowerApparent,
376                &rsp1
377            )?,
378            total_power_reactive: crate::decode_subset_item_from_input_register!(
379                offset1,
380                proto::TotalPowerReactive,
381                &rsp1
382            )?,
383            total_power_factor: crate::decode_subset_item_from_input_register!(
384                offset1,
385                proto::TotalPowerFactor,
386                &rsp1
387            )?,
388            frequency: crate::decode_subset_item_from_input_register!(
389                offset1,
390                proto::Frequency,
391                &rsp1
392            )?,
393            import_energy_active: crate::decode_subset_item_from_input_register!(
394                offset1,
395                proto::ImportEnergyActive,
396                &rsp1
397            )?,
398            export_energy_active: crate::decode_subset_item_from_input_register!(
399                offset1,
400                proto::ExportEnergyActive,
401                &rsp1
402            )?,
403
404            l1l2_voltage: crate::decode_subset_item_from_input_register!(
405                offset2,
406                proto::L1ToL2Voltage,
407                &rsp2
408            )?,
409            l2l3_voltage: crate::decode_subset_item_from_input_register!(
410                offset2,
411                proto::L2ToL3Voltage,
412                &rsp2
413            )?,
414            l3l1_voltage: crate::decode_subset_item_from_input_register!(
415                offset2,
416                proto::L3ToL1Voltage,
417                &rsp2
418            )?,
419            ll_average_voltage: crate::decode_subset_item_from_input_register!(
420                offset2,
421                proto::LtoLAverageVoltage,
422                &rsp2
423            )?,
424            neutral_current: crate::decode_subset_item_from_input_register!(
425                offset2,
426                proto::NeutralCurrent,
427                &rsp2
428            )?,
429
430            total_energy_active: crate::decode_subset_item_from_input_register!(
431                offset3,
432                proto::TotalEnergyActive,
433                &rsp3
434            )?,
435            total_energy_reactive: crate::decode_subset_item_from_input_register!(
436                offset3,
437                proto::TotalEnergyReactive,
438                &rsp3
439            )?,
440            resettable_total_energy_active: crate::decode_subset_item_from_input_register!(
441                offset3,
442                proto::ResettableTotalEnergyActive,
443                &rsp3
444            )?,
445            resettable_total_energy_reactive: crate::decode_subset_item_from_input_register!(
446                offset3,
447                proto::ResettableTotalEnergyReactive,
448                &rsp3
449            )?,
450            resettable_import_energy_active: crate::decode_subset_item_from_input_register!(
451                offset3,
452                proto::ResettableImportEnergyActive,
453                &rsp3
454            )?,
455            resettable_export_energy_active: crate::decode_subset_item_from_input_register!(
456                offset3,
457                proto::ResettableExportEnergyActive,
458                &rsp3
459            )?,
460            net_kwh: crate::decode_subset_item_from_input_register!(offset3, proto::NetKwh, &rsp3)?,
461
462            import_total_energy_active: crate::decode_subset_item_from_input_register!(
463                offset4,
464                proto::ImportTotalPowerActive,
465                &rsp4
466            )?,
467            export_total_energy_active: crate::decode_subset_item_from_input_register!(
468                offset4,
469                proto::ExportTotalPowerActive,
470                &rsp4
471            )?,
472        })
473    }
474}