1
- use std:: str:: FromStr ;
1
+ use bytes:: Bytes ;
2
+ use futures:: SinkExt as _;
3
+
4
+ use serde:: de;
2
5
use tokio:: net:: TcpStream ;
3
6
use tokio:: time:: Interval ;
7
+ use tokio_util:: codec:: { Framed , LengthDelimitedCodec } ;
4
8
5
9
use super :: { BatteryDevice , BatteryInfo , BatteryStatus , DeviceName } ;
6
10
use crate :: blocks:: prelude:: * ;
7
11
8
- #[ derive( Debug , Default ) ]
9
- struct PropertyMap ( HashMap < String , String > ) ;
10
-
11
12
make_log_macro ! ( debug, "battery[apc_ups]" ) ;
12
13
13
- impl PropertyMap {
14
- fn insert ( & mut self , k : String , v : String ) -> Option < String > {
15
- self . 0 . insert ( k, v)
16
- }
17
-
18
- fn get ( & self , k : & str ) -> Option < & str > {
19
- self . 0 . get ( k) . map ( |v| v. as_str ( ) )
20
- }
14
+ #[ derive( Debug , SmartDefault ) ]
15
+ enum Value {
16
+ String ( String ) ,
17
+ // The value is a percentage (0-100)
18
+ Percent ( f64 ) ,
19
+ Watts ( f64 ) ,
20
+ Seconds ( f64 ) ,
21
+ #[ default]
22
+ None ,
23
+ }
21
24
22
- fn get_property < T : FromStr + Send + Sync > (
23
- & self ,
24
- property_name : & str ,
25
- required_unit : & str ,
26
- ) -> Result < T > {
27
- let stat = self
28
- . get ( property_name)
29
- . or_error ( || format ! ( "{property_name} not in apc ups data" ) ) ?;
30
- let ( value, unit) = stat
31
- . split_once ( ' ' )
32
- . or_error ( || format ! ( "could not split {property_name}" ) ) ?;
33
- if unit == required_unit {
34
- value
35
- . parse :: < T > ( )
36
- . map_err ( |_| Error :: new ( "Could not parse data" ) )
37
- } else {
38
- Err ( Error :: new ( format ! (
39
- "Expected unit for {property_name} are {required_unit}, but got {unit}"
40
- ) ) )
25
+ impl < ' de > Deserialize < ' de > for Value {
26
+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
27
+ where
28
+ D : de:: Deserializer < ' de > ,
29
+ {
30
+ let s = String :: deserialize ( deserializer) ?;
31
+ for unit in [ "Percent" , "Watts" , "Seconds" , "Minutes" , "Hours" ] {
32
+ if let Some ( stripped) = s. strip_suffix ( unit) {
33
+ let value = stripped. trim ( ) . parse :: < f64 > ( ) . map_err ( de:: Error :: custom) ?;
34
+ return Ok ( match unit {
35
+ "Percent" => Value :: Percent ( value) ,
36
+ "Watts" => Value :: Watts ( value) ,
37
+ "Seconds" => Value :: Seconds ( value) ,
38
+ "Minutes" => Value :: Seconds ( value * 60.0 ) ,
39
+ "Hours" => Value :: Seconds ( value * 3600.0 ) ,
40
+ _ => unreachable ! ( ) ,
41
+ } ) ;
42
+ }
41
43
}
44
+ Ok ( Value :: String ( s) )
42
45
}
43
46
}
44
47
45
- #[ derive( Debug ) ]
46
- struct ApcConnection ( TcpStream ) ;
47
-
48
- impl ApcConnection {
49
- async fn connect ( addr : & str ) -> Result < Self > {
50
- Ok ( Self (
51
- TcpStream :: connect ( addr)
52
- . await
53
- . error ( "Failed to connect to socket" ) ?,
54
- ) )
55
- }
56
-
57
- async fn write ( & mut self , msg : & [ u8 ] ) -> Result < ( ) > {
58
- let msg_len = u16:: try_from ( msg. len ( ) )
59
- . error ( "msg is too long, it must be less than 2^16 characters long" ) ?;
60
-
61
- self . 0
62
- . write_u16 ( msg_len)
63
- . await
64
- . error ( "Could not write message length to socket" ) ?;
65
- self . 0
66
- . write_all ( msg)
67
- . await
68
- . error ( "Could not write message to socket" ) ?;
69
- Ok ( ( ) )
70
- }
71
-
72
- async fn read_line < ' a > ( & ' _ mut self , buf : & ' a mut Vec < u8 > ) -> Result < Option < & ' a str > > {
73
- let read_size = self
74
- . 0
75
- . read_u16 ( )
76
- . await
77
- . error ( "Could not read response length from socket" ) ?
78
- . into ( ) ;
79
- if read_size == 0 {
80
- return Ok ( None ) ;
81
- }
82
-
83
- buf. resize ( read_size, 0 ) ;
84
- self . 0
85
- . read_exact ( buf)
86
- . await
87
- . error ( "Could not read from socket" ) ?;
88
-
89
- std:: str:: from_utf8 ( buf) . error ( "invalid UTF8" ) . map ( Some )
90
- }
48
+ #[ derive( Debug , Deserialize , Default ) ]
49
+ #[ serde( rename_all = "UPPERCASE" , default ) ]
50
+ struct Properties {
51
+ status : Value ,
52
+ bcharge : Value ,
53
+ nompower : Value ,
54
+ loadpct : Value ,
55
+ timeleft : Value ,
91
56
}
92
57
93
58
pub ( super ) struct Device {
@@ -104,21 +69,40 @@ impl Device {
104
69
} )
105
70
}
106
71
107
- async fn get_status ( & mut self ) -> Result < PropertyMap > {
108
- let mut conn = ApcConnection :: connect ( & self . addr ) . await ?;
72
+ async fn get_status ( & mut self ) -> Result < Properties > {
73
+ let mut conn = Framed :: new (
74
+ TcpStream :: connect ( & self . addr )
75
+ . await
76
+ . error ( "Failed to connect to socket" ) ?,
77
+ LengthDelimitedCodec :: builder ( )
78
+ . length_field_type :: < u16 > ( )
79
+ . new_codec ( ) ,
80
+ ) ;
109
81
110
- conn. write ( b"status" ) . await ?;
82
+ conn. send ( Bytes :: from_static ( b"status" ) )
83
+ . await
84
+ . error ( "Could not send message to socket" ) ?;
85
+ conn. close ( ) . await . error ( "Could not close socket sink" ) ?;
111
86
112
- let mut buf = vec ! [ ] ;
113
- let mut property_map = PropertyMap :: default ( ) ;
87
+ let mut map = serde_json:: Map :: new ( ) ;
114
88
115
- while let Some ( line) = conn. read_line ( & mut buf) . await ? {
116
- if let Some ( ( key, value) ) = line. split_once ( ':' ) {
117
- property_map. insert ( key. trim ( ) . to_string ( ) , value. trim ( ) . to_string ( ) ) ;
89
+ while let Some ( frame) = conn. next ( ) . await {
90
+ let frame = frame. error ( "Failed to read from socket" ) ?;
91
+ if frame. is_empty ( ) {
92
+ continue ;
118
93
}
94
+ let line = std:: str:: from_utf8 ( & frame) . error ( "Failed to convert to UTF-8" ) ?;
95
+ let Some ( ( key, value) ) = line. split_once ( ':' ) else {
96
+ debug ! ( "Invalid field format: {line:?}" ) ;
97
+ continue ;
98
+ } ;
99
+ map. insert (
100
+ key. trim ( ) . to_uppercase ( ) ,
101
+ serde_json:: Value :: String ( value. trim ( ) . to_string ( ) ) ,
102
+ ) ;
119
103
}
120
104
121
- Ok ( property_map )
105
+ serde_json :: from_value ( serde_json :: Value :: Object ( map ) ) . error ( "Failed to deserialize" )
122
106
}
123
107
}
124
108
@@ -134,48 +118,33 @@ impl BatteryDevice for Device {
134
118
} )
135
119
. unwrap_or_default ( ) ;
136
120
137
- let status_str = status_data. get ( "STATUS" ) . unwrap_or ( "COMMLOST" ) ;
121
+ let Value :: String ( status_str) = status_data. status else {
122
+ return Ok ( None ) ;
123
+ } ;
124
+
125
+ let status = match & * status_str {
126
+ "ONBATT" => BatteryStatus :: Discharging ,
127
+ "ONLINE" => BatteryStatus :: Charging ,
128
+ _ => BatteryStatus :: Unknown ,
129
+ } ;
138
130
139
131
// Even if the connection is valid, in the first few seconds
140
132
// after apcupsd starts BCHARGE may not be present
141
- let capacity = status_data
142
- . get_property :: < f64 > ( "BCHARGE" , "Percent" )
143
- . unwrap_or ( f64:: MIN ) ;
144
-
145
- if status_str == "COMMLOST" || capacity == f64:: MIN {
133
+ let Value :: Percent ( capacity) = status_data. bcharge else {
146
134
return Ok ( None ) ;
147
- }
135
+ } ;
148
136
149
- let status = if status_str == "ONBATT" {
150
- if capacity == 0.0 {
151
- BatteryStatus :: Empty
152
- } else {
153
- BatteryStatus :: Discharging
137
+ let power = match ( status_data. nompower , status_data. loadpct ) {
138
+ ( Value :: Watts ( nominal_power) , Value :: Percent ( load_percent) ) => {
139
+ Some ( nominal_power * load_percent / 100.0 )
154
140
}
155
- } else if status_str == "ONLINE" {
156
- if capacity == 100.0 {
157
- BatteryStatus :: Full
158
- } else {
159
- BatteryStatus :: Charging
160
- }
161
- } else {
162
- BatteryStatus :: Unknown
141
+ _ => None ,
163
142
} ;
164
143
165
- let power = status_data
166
- . get_property :: < f64 > ( "NOMPOWER" , "Watts" )
167
- . ok ( )
168
- . and_then ( |nominal_power| {
169
- status_data
170
- . get_property :: < f64 > ( "LOADPCT" , "Percent" )
171
- . ok ( )
172
- . map ( |load_percent| nominal_power * load_percent / 100.0 )
173
- } ) ;
174
-
175
- let time_remaining = status_data
176
- . get_property :: < f64 > ( "TIMELEFT" , "Minutes" )
177
- . ok ( )
178
- . map ( |e| e * 60_f64 ) ;
144
+ let time_remaining = match status_data. timeleft {
145
+ Value :: Seconds ( time_left) => Some ( time_left) ,
146
+ _ => None ,
147
+ } ;
179
148
180
149
Ok ( Some ( BatteryInfo {
181
150
status,
0 commit comments