Skip to main content

solaredge_modbus/
tcp_client.rs

1use modbus::{Client as _, tcp};
2
3use crate::error::Error;
4
5pub struct TcpClient {
6	client: tcp::Transport,
7}
8
9impl TcpClient {
10	pub fn new(address: &str, port: u16, device_id: u8) -> Result<Self, Error> {
11		Ok(TcpClient {
12			client: tcp::Transport::new_with_cfg(
13				address,
14				tcp::Config {
15					tcp_port: port,
16					modbus_uid: device_id,
17					..Default::default()
18				},
19			)?,
20		})
21	}
22
23	#[cfg(feature = "discover")]
24	pub fn new_from_host_info(host_info: &crate::SolaredgeHostInfo) -> Result<Self, Error> {
25		Self::new(&host_info.address(), host_info.port, host_info.modbus_id)
26	}
27
28	/// Value = "SunS" (0x53756e53). Uniquely identifies this as a SunSpec MODBUS Map
29	pub fn spec_id(&mut self) -> Result<String, Error> {
30		Ok(self.read_register(SunspecRegister::C_SunSpec_ID)?.string())
31	}
32
33	/// Value = 0x0001. Uniquely identifies this as a SunSpec Common Model Block
34	pub fn model_id(&mut self) -> Result<u16, Error> {
35		Ok(self.read_register(SunspecRegister::C_SunSpec_DID)?.u16())
36	}
37
38	/// Value Registered with SunSpec = "SolarEdge "
39	pub fn manufacturer(&mut self) -> Result<String, Error> {
40		Ok(self.read_register(SunspecRegister::C_Manufacturer)?.string())
41	}
42
43	/// SolarEdge Specific Value
44	pub fn model(&mut self) -> Result<String, Error> {
45		Ok(self.read_register(SunspecRegister::C_Model)?.string())
46	}
47
48	/// SolarEdge Specific Value
49	pub fn version(&mut self) -> Result<String, Error> {
50		Ok(self.read_register(SunspecRegister::C_Version)?.string())
51	}
52
53	/// SolarEdge Unique Value
54	pub fn serial_number(&mut self) -> Result<String, Error> {
55		Ok(self.read_register(SunspecRegister::C_SerialNumber)?.string())
56	}
57
58	/// MODBUS Unit ID
59	pub fn device_address(&mut self) -> Result<u16, Error> {
60		Ok(self.read_register(SunspecRegister::C_DeviceAddress)?.u16())
61	}
62
63	pub fn phase_count(&mut self) -> Result<PhaseCount, Error> {
64		let val = self.read_register(SunspecRegister::I_PhaseCount)?.u16();
65		PhaseCount::from_u16(val).ok_or(Error::InvalidPhaseCountValue(val))
66	}
67
68	/// Amps AC Total Current value
69	pub fn ac_current(&mut self) -> Result<f64, Error> {
70		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_Current, SunspecRegister::I_AC_Current_SF)?;
71		Ok(val.u16().scaled(scale))
72	}
73
74	/// Amps AC Phase A Current value
75	pub fn ac_current_a(&mut self) -> Result<f64, Error> {
76		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_CurrentA, SunspecRegister::I_AC_Current_SF)?;
77		Ok(val.u16().scaled(scale))
78	}
79
80	/// Amps AC Phase B Current value
81	pub fn ac_current_b(&mut self) -> Result<f64, Error> {
82		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_CurrentB, SunspecRegister::I_AC_Current_SF)?;
83		Ok(val.u16().scaled(scale))
84	}
85
86	/// Amps AC Phase C Current value
87	pub fn ac_current_c(&mut self) -> Result<f64, Error> {
88		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_CurrentC, SunspecRegister::I_AC_Current_SF)?;
89		Ok(val.u16().scaled(scale))
90	}
91
92	/// Volts AC Voltage Phase AB value
93	pub fn ac_voltage_ab(&mut self) -> Result<f64, Error> {
94		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VoltageAB, SunspecRegister::I_AC_Voltage_SF)?;
95		Ok(val.u16().scaled(scale))
96	}
97
98	/// Volts AC Voltage Phase BC value
99	pub fn ac_voltage_bc(&mut self) -> Result<f64, Error> {
100		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VoltageBC, SunspecRegister::I_AC_Voltage_SF)?;
101		Ok(val.u16().scaled(scale))
102	}
103
104	/// Volts AC Voltage Phase CA value
105	pub fn ac_voltage_ca(&mut self) -> Result<f64, Error> {
106		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VoltageCA, SunspecRegister::I_AC_Voltage_SF)?;
107		Ok(val.u16().scaled(scale))
108	}
109
110	/// Volts AC Voltage Phase A to N value
111	pub fn ac_voltage_an(&mut self) -> Result<f64, Error> {
112		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VoltageAN, SunspecRegister::I_AC_Voltage_SF)?;
113		Ok(val.u16().scaled(scale))
114	}
115
116	/// Volts AC Voltage Phase B to N value
117	pub fn ac_voltage_bn(&mut self) -> Result<f64, Error> {
118		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VoltageBN, SunspecRegister::I_AC_Voltage_SF)?;
119		Ok(val.u16().scaled(scale))
120	}
121
122	/// Volts AC Voltage Phase C to N value
123	pub fn ac_voltage_cn(&mut self) -> Result<f64, Error> {
124		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VoltageCN, SunspecRegister::I_AC_Voltage_SF)?;
125		Ok(val.u16().scaled(scale))
126	}
127
128	/// Watts AC Power value
129	pub fn ac_power(&mut self) -> Result<f64, Error> {
130		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_Power, SunspecRegister::I_AC_Power_SF)?;
131		Ok(val.i16().scaled(scale))
132	}
133
134	/// Hertz AC Frequency value
135	pub fn ac_frequency(&mut self) -> Result<f64, Error> {
136		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_Frequency, SunspecRegister::I_AC_Frequency_SF)?;
137		Ok(val.u16().scaled(scale))
138	}
139
140	/// VA Apparent Power
141	pub fn ac_va(&mut self) -> Result<f64, Error> {
142		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VA, SunspecRegister::I_AC_VA_SF)?;
143		Ok(val.i16().scaled(scale))
144	}
145
146	/// VAR Reactive Power
147	pub fn ac_var(&mut self) -> Result<f64, Error> {
148		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_VAR2, SunspecRegister::I_AC_VAR_SF2)?;
149		Ok(val.i16().scaled(scale))
150	}
151
152	/// Power Factor
153	pub fn power_factor(&mut self) -> Result<f64, Error> {
154		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_PF1, SunspecRegister::I_AC_PF_SF1)?;
155		Ok(val.i16().scaled(scale))
156	}
157
158	/// WattHours AC Lifetime Energy production
159	pub fn energy_wh(&mut self) -> Result<f64, Error> {
160		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_AC_Energy_WH, SunspecRegister::I_AC_Energy_WH_SF)?;
161		Ok(val.u32().scaled(scale))
162	}
163
164	/// Amps DC Current value
165	pub fn dc_current(&mut self) -> Result<f64, Error> {
166		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_DC_Current, SunspecRegister::I_DC_Current_SF)?;
167		Ok(val.u16().scaled(scale))
168	}
169
170	/// Volts DC Voltage value
171	pub fn dc_voltage(&mut self) -> Result<f64, Error> {
172		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_DC_Voltage, SunspecRegister::I_DC_Voltage_SF)?;
173		Ok(val.u16().scaled(scale))
174	}
175
176	/// Watts DC Power value
177	pub fn dc_power(&mut self) -> Result<f64, Error> {
178		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_DC_Power, SunspecRegister::I_DC_Power_SF)?;
179		Ok(val.i16().scaled(scale))
180	}
181
182	/// Degrees C Heat Sink Temperature
183	pub fn temp_sink(&mut self) -> Result<f64, Error> {
184		let (val, scale) = self.read_register_with_scale(SunspecRegister::I_Temp_Sink, SunspecRegister::I_Temp_SF)?;
185		Ok(val.i16().scaled(scale))
186	}
187
188	/// Operating State
189	pub fn status(&mut self) -> Result<InverterStatus, Error> {
190		let val = self.read_register(SunspecRegister::I_Status)?.u16();
191		Ok(InverterStatus::from_u16(val))
192	}
193
194	/// Vendor-defined operating state and error codes. For error description, meaning, and troubleshooting, refer to the SolarEdge
195	/// Installation Guide.
196	pub fn status_vendor(&mut self) -> Result<u16, Error> {
197		Ok(self.read_register(SunspecRegister::I_Status_Vendor)?.u16())
198	}
199
200	/// Vendor-defined operating state and error codes. For error description, meaning, and troubleshooting, refer to the SolarEdge
201	/// Installation Guide, 16MSB for controller type (3x, 8x, 18x) and 16 LSB for error code.
202	pub fn status_vendor4(&mut self) -> Result<u32, Error> {
203		Ok(self.read_register(SunspecRegister::I_Status_Vendor4)?.u32())
204	}
205
206	#[inline(always)]
207	fn read_register<R: Register>(&mut self, register: R) -> modbus::Result<Vec<u16>> {
208		self.client.read_holding_registers(register.address(), register.size())
209	}
210
211	/// Reads scale and the register in one call if they are adjacent.
212	///
213	/// This is an optimization to reduce the number of Modbus calls. If the register and scale are not adjacent, it falls back to reading them separately.
214	#[inline(always)]
215	fn read_register_with_scale<R: Register, RS: Register>(
216		&mut self,
217		register: R,
218		scale_register: RS,
219	) -> modbus::Result<(Vec<u16>, i16)> {
220		if register.address() + register.size() == scale_register.address() {
221			let mut combined = self
222				.client
223				.read_holding_registers(register.address(), register.size() + scale_register.size())?;
224			let scale = combined.pop().unwrap();
225			Ok((combined, scale as i16))
226		} else if scale_register.address() + scale_register.size() == register.address() {
227			let mut combined = self
228				.client
229				.read_holding_registers(scale_register.address(), scale_register.size() + register.size())?;
230			let scale = combined.remove(0);
231			Ok((combined, scale as i16))
232		} else {
233			Ok((self.read_register(register)?, self.read_register(scale_register)?.i16()))
234		}
235	}
236}
237
238#[derive(Debug, Clone, Copy)]
239pub enum PhaseCount {
240	SinglePhase,
241	SplitPhase,
242	ThreePhase,
243}
244
245impl PhaseCount {
246	pub fn from_u16(value: u16) -> Option<Self> {
247		match value {
248			101 => Some(PhaseCount::SinglePhase),
249			102 => Some(PhaseCount::SplitPhase),
250			103 => Some(PhaseCount::ThreePhase),
251			_ => None,
252		}
253	}
254}
255
256#[derive(Debug, Clone, Copy)]
257pub enum InverterStatus {
258	Off,
259	/// Sleeping (auto-shutdown) – Night mode
260	Sleeping,
261	/// Grid Monitoring/wake-up
262	Starting,
263	/// Inverter is ON and producing power
264	Mppt,
265	/// Production (curtailed)
266	Throttled,
267	ShuttingDown,
268	Fault,
269	/// Maintenance/setup
270	Standby,
271	/// Vendor-specific status, refer to SolarEdge Installation Guide
272	Vendor(u16),
273}
274
275impl InverterStatus {
276	pub fn from_u16(value: u16) -> Self {
277		match value {
278			1 => InverterStatus::Off,
279			2 => InverterStatus::Sleeping,
280			3 => InverterStatus::Starting,
281			4 => InverterStatus::Mppt,
282			5 => InverterStatus::Throttled,
283			6 => InverterStatus::ShuttingDown,
284			7 => InverterStatus::Fault,
285			8 => InverterStatus::Standby,
286			v => InverterStatus::Vendor(v),
287		}
288	}
289}
290
291trait Register {
292	fn address(&self) -> u16;
293	fn size(&self) -> u16;
294}
295
296#[derive(Debug, Clone, Copy)]
297#[allow(non_camel_case_types)]
298enum SunspecRegister {
299	/// Value = "SunS" (0x53756e53). Uniquely identifies this as a SunSpec MODBUS Map
300	C_SunSpec_ID = 40000,
301	/// Value = 0x0001. Uniquely identifies this as a SunSpec Common Model Block
302	C_SunSpec_DID = 40002,
303	/// Value Registered with SunSpec = "SolarEdge "
304	C_Manufacturer = 40004,
305	/// SolarEdge Specific Value
306	C_Model = 40020,
307	/// SolarEdge Specific Value
308	C_Version = 40044,
309	/// SolarEdge Unique Value
310	C_SerialNumber = 40052,
311	/// MODBUS Unit ID
312	C_DeviceAddress = 40068,
313	/// 101 = single phase
314	/// 102 = split phase
315	/// 103 = three phase
316	I_PhaseCount = 40069,
317	/// Amps AC Total Current value
318	I_AC_Current = 40071,
319	/// Amps AC Phase A Current value
320	I_AC_CurrentA = 40072,
321	/// Amps AC Phase B Current value
322	I_AC_CurrentB = 40073,
323	/// Amps AC Phase C Current value
324	I_AC_CurrentC = 40074,
325	/// AC Current scale factor
326	I_AC_Current_SF = 40075,
327	/// Volts AC Voltage Phase AB value
328	I_AC_VoltageAB = 40076,
329	/// Volts AC Voltage Phase BC value
330	I_AC_VoltageBC = 40077,
331	/// Volts AC Voltage Phase CA value
332	I_AC_VoltageCA = 40078,
333	/// Volts AC Voltage Phase A to N value
334	I_AC_VoltageAN = 40079,
335	/// Volts AC Voltage Phase B to N value
336	I_AC_VoltageBN = 40080,
337	/// Volts AC Voltage Phase C to N value
338	I_AC_VoltageCN = 40081,
339	/// AC Voltage scale factor
340	I_AC_Voltage_SF = 40082,
341	/// Watts AC Power value
342	I_AC_Power = 40083,
343	/// AC Power scale factor
344	I_AC_Power_SF = 40084,
345	/// Hertz AC Frequency value
346	I_AC_Frequency = 40085,
347	/// Scale factor
348	I_AC_Frequency_SF = 40086,
349	/// VA Apparent Power
350	I_AC_VA = 40087,
351	/// Scale factor
352	I_AC_VA_SF = 40088,
353	/// VAR Reactive Power
354	I_AC_VAR2 = 40089,
355	/// Scale factor
356	I_AC_VAR_SF2 = 40090,
357	/// Power Factor
358	I_AC_PF1 = 40091,
359	/// Scale factor
360	I_AC_PF_SF1 = 40092,
361	/// WattHours AC Lifetime Energy production
362	I_AC_Energy_WH = 40093,
363	/// Scale factor
364	I_AC_Energy_WH_SF = 40095,
365	/// Amps DC Current value
366	I_DC_Current = 40096,
367	/// Scale factor
368	I_DC_Current_SF = 40097,
369	/// Volts DC Voltage value
370	I_DC_Voltage = 40098,
371	/// Scale factor
372	I_DC_Voltage_SF = 40099,
373	/// Watts DC Power value
374	I_DC_Power = 40100,
375	/// Scale factor
376	I_DC_Power_SF = 40101,
377	/// Degrees C Heat Sink Temperature
378	I_Temp_Sink = 40103,
379	/// Scale factor
380	I_Temp_SF = 40106,
381	/// Operating State
382	I_Status = 40107,
383	/// Vendor-defined operating state and error codes. For error description, meaning, and troubleshooting, refer to the SolarEdge
384	/// Installation Guide.
385	I_Status_Vendor = 40108,
386	/// Vendor-defined operating state and error codes. For error description, meaning, and troubleshooting, refer to the SolarEdge
387	/// Installation Guide, 16MSB for controller type (3x, 8x, 18x) and 16 LSB for error code.
388	I_Status_Vendor4 = 40119,
389}
390
391impl Register for SunspecRegister {
392	#[inline(always)]
393	fn address(&self) -> u16 {
394		*self as u16
395	}
396
397	#[inline(always)]
398	fn size(&self) -> u16 {
399		match self {
400			SunspecRegister::C_SunSpec_ID => 2,
401			SunspecRegister::C_SunSpec_DID => 1,
402			SunspecRegister::C_Manufacturer => 16,
403			SunspecRegister::C_Model => 16,
404			SunspecRegister::C_Version => 8,
405			SunspecRegister::C_SerialNumber => 16,
406			SunspecRegister::C_DeviceAddress => 1,
407			SunspecRegister::I_PhaseCount => 1,
408			SunspecRegister::I_AC_Current => 1,
409			SunspecRegister::I_AC_CurrentA => 1,
410			SunspecRegister::I_AC_CurrentB => 1,
411			SunspecRegister::I_AC_CurrentC => 1,
412			SunspecRegister::I_AC_Current_SF => 1,
413			SunspecRegister::I_AC_VoltageAB => 1,
414			SunspecRegister::I_AC_VoltageBC => 1,
415			SunspecRegister::I_AC_VoltageCA => 1,
416			SunspecRegister::I_AC_VoltageAN => 1,
417			SunspecRegister::I_AC_VoltageBN => 1,
418			SunspecRegister::I_AC_VoltageCN => 1,
419			SunspecRegister::I_AC_Voltage_SF => 1,
420			SunspecRegister::I_AC_Power => 1,
421			SunspecRegister::I_AC_Power_SF => 1,
422			SunspecRegister::I_AC_Frequency => 1,
423			SunspecRegister::I_AC_Frequency_SF => 1,
424			SunspecRegister::I_AC_VA => 1,
425			SunspecRegister::I_AC_VA_SF => 1,
426			SunspecRegister::I_AC_VAR2 => 1,
427			SunspecRegister::I_AC_VAR_SF2 => 1,
428			SunspecRegister::I_AC_PF1 => 1,
429			SunspecRegister::I_AC_PF_SF1 => 1,
430			SunspecRegister::I_AC_Energy_WH => 2,
431			SunspecRegister::I_AC_Energy_WH_SF => 1,
432			SunspecRegister::I_DC_Current => 1,
433			SunspecRegister::I_DC_Current_SF => 1,
434			SunspecRegister::I_DC_Voltage => 1,
435			SunspecRegister::I_DC_Voltage_SF => 1,
436			SunspecRegister::I_DC_Power => 1,
437			SunspecRegister::I_DC_Power_SF => 1,
438			SunspecRegister::I_Temp_Sink => 1,
439			SunspecRegister::I_Temp_SF => 1,
440			SunspecRegister::I_Status => 1,
441			SunspecRegister::I_Status_Vendor => 1,
442			SunspecRegister::I_Status_Vendor4 => 2,
443		}
444	}
445}
446
447trait RegisterResponse {
448	fn string(self) -> String;
449	fn u16(self) -> u16;
450	fn i16(self) -> i16;
451	fn u32(self) -> u32;
452}
453
454impl RegisterResponse for Vec<u16> {
455	fn string(self) -> String {
456		self
457			.into_iter()
458			.flat_map(|s| s.to_be_bytes())
459			.take_while(|&b| b != 0)
460			.map(char::from)
461			.collect()
462	}
463
464	fn u16(self) -> u16 {
465		self[0]
466	}
467
468	fn i16(self) -> i16 {
469		self[0] as i16
470	}
471
472	fn u32(self) -> u32 {
473		((self[0] as u32) << 16) | self[1] as u32
474	}
475}
476
477trait Scaled {
478	fn scaled(self, scale: i16) -> f64;
479}
480
481impl Scaled for i16 {
482	fn scaled(self, scale: i16) -> f64 {
483		let value = self as f64;
484		value * 10f64.powi(i32::from(scale))
485	}
486}
487
488impl Scaled for u16 {
489	fn scaled(self, scale: i16) -> f64 {
490		let value = self as f64;
491		value * 10f64.powi(i32::from(scale))
492	}
493}
494
495impl Scaled for u32 {
496	fn scaled(self, scale: i16) -> f64 {
497		let value = self as f64;
498		value * 10f64.powi(i32::from(scale))
499	}
500}