ve_direct 0.1.7

Victron Energy Direct protocol parser and units converter
Documentation
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
// #![allow(clippy::upper_case_acronyms)]
// #![allow(non_camel_case_types)]
// #![allow(non_snake_case)]

use serde::{Deserialize, Serialize};
use std::io::{Error, ErrorKind, Result};
use std::str::FromStr;
use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, FromRepr};

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum StateOfOperation {
    Off = 0,
    LowPower = 1,
    Fault = 2,
    Bulk = 3,
    Absorption = 4,
    Float = 5,
    Storage = 6,
    Equalize = 7,
    Inverting = 9,
    PowerSupply = 11,
    StartingUp = 245,
    RepeatedAbsorption = 246,
    AutoEqualize = 247,
    BatterySafe = 248,
    ExternalControl = 252,
}

impl Default for StateOfOperation {
    fn default() -> Self {
        StateOfOperation::Off
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum ErrorCode {
    None = 0,
    BatteryVoltageTooHigh = 2,
    ChargerTemperatureTooHigh = 17,
    ChargerOverCurrent = 18,
    ChargerCurrentReversed = 19,
    BulkTimeLimitExceeded = 20,
    CurrentSensorIssue = 21,
    TerminalsOverheated = 26,
    ConverterIssue = 28,
    InputVoltageTooHigh = 33,
    InputCurrentTooHigh = 34,
    InputShutdownBatVoltage = 38,
    InputShutdownCurrentFlow = 39,
    LostComWithDevices = 65,
    SynchronisedChargingIssue = 66,
    BMSConnectionLost = 67,
    NetworkMisconfigured = 68,
    FactoryCalibrationDataLost = 116,
    InvalidFirmware = 117,
    UserSettingsInvalid = 119,
}

impl Default for ErrorCode {
    fn default() -> Self {
        ErrorCode::None
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr, EnumIter, Copy)]
pub enum OffReason {
    None = 0,
    NoInputPower = 1,
    SwitchedOffPowerSwitch = 2,
    SwitchedOffDMR = 4,
    RemoteInput = 8,
    ProtectionActive = 16,
    Paygo = 32,
    BMS = 64,
    EngineShutdownDetection = 128,
    AnalysingInputVoltage = 256,
}

impl Default for OffReason {
    fn default() -> Self {
        OffReason::None
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr, EnumIter, Copy)]
pub enum AlarmReason {
    None = 0,
    LowVoltage = 1,
    HighVoltage = 2,
    LowSOC = 4,
    LowStarterVoltage = 8,
    HighStarterVoltage = 16,
    LowTemperature = 32,
    HighTemperature = 64,
    MidVoltage = 128,
    Overload = 256,
    DCripple = 512,
    LowVACout = 1024,
    HighVACout = 2048,
}

impl Default for AlarmReason {
    fn default() -> Self {
        AlarmReason::None
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr, EnumIter, Copy)]
pub enum WarningReason {
    None = 0,
    LowVoltage = 1,
    HighVoltage = 2,
    LowSOC = 4,
    LowStarterVoltage = 8,
    HighStarterVoltage = 16,
    LowTemperature = 32,
    HighTemperature = 64,
    MidVoltage = 128,
    Overload = 256,
    DCripple = 512,
    LowVACout = 1024,
    HighVACout = 2048,
}

impl Default for WarningReason {
    fn default() -> Self {
        WarningReason::None
    }
}

#[allow(non_camel_case_types)]
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum DeviceMode {
    None = 0,
    VE_REG_MODE_INVERTER = 2,
    VE_REG_MODE_OFF = 4,
    VE_REG_MODE_ECO = 5,
}

impl Default for DeviceMode {
    fn default() -> Self {
        DeviceMode::None
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum BluetoothStatus {
    Off = 0,
    On = 1,
}
impl Default for BluetoothStatus {
    fn default() -> Self {
        BluetoothStatus::Off
    }
}

#[allow(non_camel_case_types)]
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum BluetoothCapBle {
    None = 0,
    BLE_Supports_Switching_Off = 1,
    BLE_Switching_Off_Is_Permanent = 2,
}
impl Default for BluetoothCapBle {
    fn default() -> Self {
        BluetoothCapBle::None
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum Load {
    Off = 0,
    On = 1,
}

impl Default for Load {
    fn default() -> Self {
        Load::Off
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum Alarm {
    Off = 0,
    On = 1,
}
impl Default for Alarm {
    fn default() -> Self {
        Alarm::Off
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, FromRepr)]
pub enum Relay {
    Off = 0,
    On = 1,
}
impl Default for Relay {
    fn default() -> Self {
        Relay::Off
    }
}

#[allow(non_camel_case_types)]
#[derive(Display)]
pub enum Labels {
    V,     //                           [mV] Main (battery) voltage
    VS,    //                           [mV] Auxiliary (starter) voltage
    VM,    //                           [mV] Mid-point voltage of the battery bank
    DM,    //                           [‰] Mid-point deviation of the battery bank
    VPV,   //                           [mV] Panel voltage
    PPV,   //                           [W] Panel power
    I,     //                           [mA] Battery current
    IL,    //                           [mA] Load current
    LOAD,  //                           Load output state (ON/OFF)
    T,     //                           [°C] Battery temperature
    P,     //                           [W] Instantaneous power
    CE,    //                           [mAh] Consumed Amp Hours
    SOC,   //                           [‰] State-of-charge
    TTG,   //                           [Minutes] Time-to-go
    Alarm, //                           Alarm condition active
    Relay, //                           Relay state
    AR,    //                           Alarm reason
    OR,    //                           Off reason
    H1,    //                           [mAh] Depth of the deepest discharge
    H2,    //                           [mAh] Depth of the last discharge
    H3,    //                           [mAh] Depth of the average discharge
    H4,    //                           Number of charge cycles
    H5,    //                           Number of full discharges
    H6,    //                           [mAh] Cumulative Amp Hours drawn
    H7,    //                           [mV] Minimum main (battery) voltage
    H8,    //                           [mV] Maximum main (battery) voltage
    H9,    //                           [Seconds] Number of seconds since last full charge
    H10,   //                           Number of automatic synchronizations
    H11,   //                           Number of low main voltage alarms
    H12,   //                           Number of high main voltage alarms
    H13,   //                           Number of low auxiliary voltage alarms
    H14,   //                           Number of high auxiliary voltage alarms
    H15,   //                           [mV] Minimum auxiliary (battery) voltage
    H16,   //                           [mV] Maximum auxiliary (battery) voltage
    H17,   //                           [0.01 kWh] Amount of discharged energy
    H18,   //                           [0.01 kWh] Amount of charged energy
    H19,   //                           [0.01 kWh] Yield total (user resettable counter)
    H20,   //                           [0.01 kWh] Yield today
    H21,   //                           [W] Maximum power today
    H22,   //                           [0.01 kWh] Yield yesterday
    H23,   //                           [W] Maximum power yesterday
    ERR,   //                           Error code
    CS,    //                           State of operation
    BMV,   //                           Model description (deprecated)
    FW,    //                           Firmware version
    FWE,   //                           Firmware version
    PID,   //                           Product ID
    #[strum(serialize = "SER#")]
    SER, //                             Serial number
    HSDS,  //                           Day sequence number (0..364)
    MODE,  //                           Device mode
    AC_OUT_V, //                        [0.01 V] AC output voltage
    AC_OUT_I, //                        [0.1 A] AC output current
    AC_OUT_S, //                        [W] AC output power
    WARN,  //                           Warning reason
    BLE,   //                           Bluetooth status
    CAP_BLE, //                         Bloetooth capabilities
    Checksum, //                        Checksum from packet
    Calc_sum, //                        Checksum calculated by parser
    Unknown, //                         Unknown labels vector
    Time,  //                           Timestamp
}

#[allow(non_snake_case)]
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
pub struct VEDirectData {
    pub V: Option<f64>,
    pub VS: Option<f64>,
    pub VM: Option<f64>,
    pub DM: Option<f64>,
    pub VPV: Option<f64>,
    pub PPV: Option<f64>,
    pub I: Option<f64>,
    pub IL: Option<f64>,
    pub LOAD: Option<Load>,
    pub T: Option<f64>,
    pub P: Option<f64>,
    pub CE: Option<f64>,
    pub SOC: Option<f64>,
    pub TTG: Option<f64>,
    pub Alarm: Option<Alarm>,
    pub Relay: Option<Relay>,
    pub AR: Option<Vec<AlarmReason>>,
    pub OR: Option<Vec<OffReason>>,
    pub H1: Option<f64>,
    pub H2: Option<f64>,
    pub H3: Option<f64>,
    pub H4: Option<f64>,
    pub H5: Option<f64>,
    pub H6: Option<f64>,
    pub H7: Option<f64>,
    pub H8: Option<f64>,
    pub H9: Option<f64>,
    pub H10: Option<f64>,
    pub H11: Option<f64>,
    pub H12: Option<f64>,
    pub H13: Option<f64>,
    pub H14: Option<f64>,
    pub H15: Option<f64>,
    pub H16: Option<f64>,
    pub H17: Option<f64>,
    pub H18: Option<f64>,
    pub H19: Option<f64>,
    pub H20: Option<f64>,
    pub H21: Option<f64>,
    pub H22: Option<f64>,
    pub H23: Option<f64>,
    pub ERR: Option<ErrorCode>,
    pub CS: Option<StateOfOperation>,
    pub BMV: Option<String>,
    pub FW: Option<String>,
    pub FWE: Option<String>,
    pub PID: Option<String>,
    pub SER: Option<String>,
    pub HSDS: Option<f64>,
    pub MODE: Option<DeviceMode>,
    pub AC_OUT_V: Option<f64>,
    pub AC_OUT_I: Option<f64>,
    pub AC_OUT_S: Option<f64>,
    pub WARN: Option<Vec<WarningReason>>,
    pub Calc_sum: Option<u8>,
    pub Checksum: Option<u8>,
    pub BLE: Option<BluetoothStatus>,
    pub CAP_BLE: Option<BluetoothCapBle>,
    pub Time: Option<i64>,
    pub Unknown: Option<Vec<String>>,
}

pub type V = f64; // volt
pub type A = f64; // ampere
pub type Ah = f64; // ampere hour
pub type Wh = f64; // watt hour

#[allow(non_camel_case_types)]
pub type mV = f64; // mili volt
#[allow(non_camel_case_types)]
pub type cV = f64; // centy volt
#[allow(non_camel_case_types)]
pub type mA = f64; // mili ampere
#[allow(non_camel_case_types)]
pub type dA = f64; // deci ampere
#[allow(non_camel_case_types)]
pub type mAh = f64; // mili ampere hour
#[allow(non_camel_case_types)]
pub type daWh = f64; // deca watt hour
#[allow(non_camel_case_types)]
pub type kWh = f64; // kilo watt hour

pub fn convert_mv(str: String) -> Result<V> {
    let val = str.parse::<mV>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val / 1000_f64)
}

pub fn convert_ma(str: String) -> Result<A> {
    let val = str.parse::<mA>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val / 1000_f64)
}

pub fn convert_mah(str: String) -> Result<Ah> {
    let val = str.parse::<mAh>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val / 1000_f64)
}

pub fn convert_kwh(str: String) -> Result<Wh> {
    let val = str.parse::<kWh>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val * 1000_f64)
}

pub fn convert_dawh(str: String) -> Result<Wh> {
    let val = str.parse::<daWh>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val * 10_f64)
}

pub fn convert_cv(str: String) -> Result<V> {
    let val = str.parse::<cV>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val / 100_f64)
}

pub fn convert_da(str: String) -> Result<A> {
    let val = str.parse::<dA>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val / 10_f64)
}

pub fn convert_none<T>(val: T) -> Result<T> {
    Ok(val)
}

pub fn convert_parse<T>(str: String) -> Result<T>
where
    T: FromStr,
{
    str.parse::<T>().map_err(|_| Error::from(ErrorKind::InvalidData))
}

pub fn convert_minutes(str: String) -> Result<f64> {
    let val = str.parse::<f64>().map_err(|err| Error::new(ErrorKind::InvalidData, err))?;
    Ok(val * 60_f64)
}

pub fn convert_state_of_operation(field: String) -> Result<StateOfOperation> {
    match StateOfOperation::from_repr(field.parse::<usize>().map_err(|_| Error::from(ErrorKind::InvalidData))?) {
        Some(v) => Ok(v),
        None => Err(Error::from(ErrorKind::InvalidData)),
    }
}

pub fn convert_error_code(field: String) -> Result<ErrorCode> {
    match ErrorCode::from_repr(field.parse::<usize>().map_err(|_| Error::from(ErrorKind::InvalidData))?) {
        Some(v) => Ok(v),
        None => Err(Error::from(ErrorKind::InvalidData)),
    }
}

pub fn convert_alarm_reason(field: String) -> Result<Vec<AlarmReason>> {
    let val = field.parse::<usize>().map_err(|_| Error::from(ErrorKind::InvalidData))?;
    let mut ret = Vec::<AlarmReason>::new();

    if val == 0 {
        ret.push(AlarmReason::None);
        return Ok(ret);
    }

    for reason in AlarmReason::iter() {
        if val & (reason as usize) > 0 {
            ret.push(reason)
        }
    }

    if !ret.is_empty() {
        Ok(ret)
    } else {
        Err(Error::from(ErrorKind::InvalidData))
    }
}

pub fn convert_warning_reason(field: String) -> Result<Vec<WarningReason>> {
    let val = field.parse::<usize>().map_err(|_| Error::from(ErrorKind::InvalidData))?;
    let mut ret = Vec::<WarningReason>::new();

    if val == 0 {
        ret.push(WarningReason::None);
        return Ok(ret);
    }

    for reason in WarningReason::iter() {
        if val & (reason as usize) > 0 {
            ret.push(reason)
        }
    }

    if !ret.is_empty() {
        Ok(ret)
    } else {
        Err(Error::from(ErrorKind::InvalidData))
    }
}

pub fn convert_device_mode(field: String) -> Result<DeviceMode> {
    match DeviceMode::from_repr(field.parse::<usize>().map_err(|_| Error::from(ErrorKind::InvalidData))?) {
        Some(v) => Ok(v),
        None => Err(Error::from(ErrorKind::InvalidData)),
    }
}

pub fn convert_off_reason(field: String) -> Result<Vec<OffReason>> {
    let without_prefix = field.trim_start_matches("0x");
    let val = usize::from_str_radix(without_prefix, 16).map_err(|_| Error::from(ErrorKind::InvalidData))?;
    let mut ret = Vec::<OffReason>::new();

    if val == 0 {
        ret.push(OffReason::None);
        return Ok(ret);
    }

    for reason in OffReason::iter() {
        if val & (reason as usize) > 0 {
            ret.push(reason)
        }
    }

    if !ret.is_empty() {
        Ok(ret)
    } else {
        Err(Error::from(ErrorKind::InvalidData))
    }
}

pub fn convert_ble(field: String) -> Result<BluetoothStatus> {
    let without_prefix = field.trim_start_matches("0x");
    let val = usize::from_str_radix(without_prefix, 16).map_err(|_| Error::from(ErrorKind::InvalidData))?;
    match BluetoothStatus::from_repr(val) {
        Some(v) => Ok(v),
        None => Err(Error::from(ErrorKind::InvalidData)),
    }
}

pub fn convert_capble(field: String) -> Result<BluetoothCapBle> {
    let without_prefix = field.trim_start_matches("0x");
    let val = usize::from_str_radix(without_prefix, 16).map_err(|_| Error::from(ErrorKind::InvalidData))?;
    match BluetoothCapBle::from_repr(val) {
        Some(v) => Ok(v),
        None => Err(Error::from(ErrorKind::InvalidData)),
    }
}

pub fn convert_alarm(field: String) -> Result<Alarm> {
    let lower = field.to_lowercase();
    if lower == "on" {
        Ok(Alarm::On)
    } else if lower == "off" {
        Ok(Alarm::Off)
    } else {
        Err(Error::from(ErrorKind::InvalidData))
    }
}

pub fn convert_relay(field: String) -> Result<Relay> {
    let lower = field.to_lowercase();
    if lower == "on" {
        Ok(Relay::On)
    } else if lower == "off" {
        Ok(Relay::Off)
    } else {
        Err(Error::from(ErrorKind::InvalidData))
    }
}

pub fn convert_load(field: String) -> Result<Load> {
    let lower = field.to_lowercase();
    if lower == "on" {
        Ok(Load::On)
    } else if lower == "off" {
        Ok(Load::Off)
    } else {
        Err(Error::from(ErrorKind::InvalidData))
    }
}