Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\mbw973.py: 41%

# Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\mbw973.py : 41%

## 111 statements

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET

2#

3"""

4 Device class for controlling a MBW 973 SF6 Analyzer over a serial connection.

5

6 The MBW 973 is a gas analyzer designed for gas insulated switchgear and measures

7 humidity, SF6 purity and SO2 contamination in one go.

8 Manufacturer homepage: https://www.mbw.ch/products/sf6-gas-analysis/973-sf6-analyzer/

9"""

10

11 import logging

12 from typing import Dict, Type, Union

13

14 from .base import SingleCommDevice

15 from .utils import Poller

16 from ..comm import SerialCommunication, SerialCommunicationConfig

17 from ..comm.serial import (

18 SerialCommunicationParity,

19 SerialCommunicationStopbits,

20 SerialCommunicationBytesize,

21)

22 from ..configuration import configdataclass

23 from ..utils.typing import Number

24

25

26 class MBW973Error(Exception):

27 """

28 General error with the MBW973 dew point mirror device.

29 """

30

31 pass

32

33

34 class MBW973ControlRunningException(MBW973Error):

35 """

36 Error indicating there is still a measurement running, and a new one cannot be

37 started.

38 """

39

40 pass

41

42

43 class MBW973PumpRunningException(MBW973Error):

44 """

45 Error indicating the pump of the dew point mirror is still recovering gas,

46 unable to start a new measurement.

47 """

48

49 pass

50

51

52@configdataclass

53 class MBW973SerialCommunicationConfig(SerialCommunicationConfig):

54 #: Baudrate for MBW973 is 9600 baud

55 baudrate: int = 9600

56

57 #: MBW973 does not use parity

58 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE

59

60 #: MBW973 does use one stop bit

61 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE

62

63 #: One byte is eight bits long

64 bytesize: Union[

65 int, SerialCommunicationBytesize

66 ] = SerialCommunicationBytesize.EIGHTBITS

67

68 #: The terminator is only CR

69 terminator: bytes = b"\r"

70

71 #: use 3 seconds timeout as default

72 timeout: Number = 3

73

74

75 class MBW973SerialCommunication(SerialCommunication):

76 """

77 Specific communication protocol implementation for the MBW973 dew point mirror.

78 Already predefines device-specific protocol parameters in config.

79 """

80

81 @staticmethod

82 def config_cls():

83 return MBW973SerialCommunicationConfig

84

85

86@configdataclass

87 class MBW973Config:

88 """

89 Device configuration dataclass for MBW973.

90 """

91

92 #: Polling period for `is_done` status queries [in seconds].

93 polling_interval: Number = 2

94

95 def clean_values(self):

96 if self.polling_interval <= 0:

97 raise ValueError("Polling interval needs to be positive.")

98

99

100 class MBW973(SingleCommDevice):

101 """

102 MBW 973 dew point mirror device class.

103 """

104

105 def __init__(self, com, dev_config=None):

106

107 # Call superclass constructor

108 super().__init__(com, dev_config)

109

110 # polling status

111 self.status_poller = Poller(

112 self.is_done,

113 polling_delay_sec=self.config.polling_interval,

114 polling_interval_sec=self.config.polling_interval,

115 )

116

117 # is done with dew point = True, new measurement sample required and

118 # not ready yet = False

119 self.is_done_with_measurements = True

120

121 # dict telling what measurement options are selected

122 self.measurement_options = {

123 "dewpoint": True,

124 "SF6_Vol": False,

125 }

126

127 self.last_measurement_values = {}

128

129 @staticmethod

130 def default_com_cls():

131 return MBW973SerialCommunication

132

133 @staticmethod

134 def config_cls():

135 return MBW973Config

136

137 def start(self) -> None:

138 """

139 Start this device. Opens the communication protocol and retrieves the

140 set measurement options from the device.

141

142 :raises SerialCommunicationIOError: when communication port cannot be opened.

143 """

144

145 logging.info("Starting device " + str(self))

146 super().start()

147

148 # check test options

149 self.write("HumidityTest?")

150 self.measurement_options["dewpoint"] = bool(self.read_int())

151

152 self.write("SF6PurityTest?")

153 self.measurement_options["SF6_Vol"] = bool(self.read_int())

154

155 def stop(self) -> None:

156 """

157 Stop the device. Closes also the communication protocol.

158 """

159

160 logging.info("Stopping device " + str(self))

161 super().stop()

162

163 def write(self, value) -> None:

164 """

165 Send `value` to `self.com`.

166

167 :param value: Value to send, converted to `str`.

168 :raises SerialCommunicationIOError: when communication port is not opened

169 """

170 self.com.write_text(str(value))

171

172 def read(self, cast_type: Type = str):

173 """

174 Read value from `self.com` and cast to `cast_type`.

175 Raises `ValueError` if read text (`str`) is not convertible to `cast_type`,

176 e.g. to `float` or to `int`.

177

178 :return: Read value of `cast_type` type.

179 """

180 return cast_type(self.com.read_text())

181

182 def read_float(self) -> float:

183 """

184 Convenience wrapper for `self.read()`, with typing hint for return value.

185

186 :return: Read `float` value.

187 """

188 return self.read(float)

189

190 def read_int(self) -> int:

191 """

192 Convenience wrapper for `self.read()`, with typing hint for return value.

193

194 :return: Read `int` value.

195 """

196 return self.read(int)

197

198 def is_done(self) -> bool:

199 """

200 Poll status of the dew point mirror and return True, if all

201 measurements are done.

202

203 :return: True, if all measurements are done; False otherwise.

204 :raises SerialCommunicationIOError: when communication port is not opened

205 """

206

207 # assume everything is done

208 done = True

209

210 if self.measurement_options["dewpoint"]:

211 # ask if done with DP

212 self.write("DoneWithDP?")

213 done = done and bool(self.read_int())

214

215 if self.measurement_options["SF6_Vol"]:

216 # ask if done with SF6 volume measurement

217 self.write("SF6VolHold?")

218 done = done and bool(self.read_int())

219

220 self.is_done_with_measurements = done

221

222 if self.is_done_with_measurements:

223 self.status_poller.stop_polling()

224 self.read_measurements()

225

226 return self.is_done_with_measurements

227

228 def start_control(self) -> None:

229 """

230 Start dew point control to acquire a new value set.

231

232 :raises SerialCommunicationIOError: when communication port is not opened

233 """

234

235 # send control?

236 self.write("control?")

237

238 if self.read_float():

239 raise MBW973ControlRunningException

240

241 # send Pump.on? to check, whether gas is still being pumped back

242 self.write("Pump.on?")

243

244 if self.read_float():

245 raise MBW973PumpRunningException

246

247 # start control of device

248 self.write("control=1")

249 logging.info("Starting dew point control")

250 self.is_done_with_measurements = False

251 self.status_poller.start_polling()

252

253 def read_measurements(self) -> Dict[str, float]:

254 """

255 Read out measurement values and return them as a dictionary.

256

257 :return: Dictionary with values.

258 :raises SerialCommunicationIOError: when communication port is not opened

259 """

260

261 self.write("Fp?")

262 frostpoint = self.read_float()

263

264 self.write("Fp1?")

265 frostpoint_ambient = self.read_float()

266

267 self.write("Px?")

268 pressure = self.read_float()

269

270 self.write("PPMv?")

271 ppmv = self.read_float()

272

273 self.write("PPMw?")

274 ppmw = self.read_float()

275

276 self.write("SF6Vol?")

277 sf6_vol = self.read_float()

278

279 values = {

280 "frostpoint": frostpoint,

281 "frostpoint_ambient": frostpoint_ambient,

282 "pressure": pressure,

283 "ppmv": ppmv,

284 "ppmw": ppmw,

285 "sf6_vol": sf6_vol,

286 }

287

288 logging.info("Read out values")

289 logging.info(values)

290

291 self.last_measurement_values = values

292

293 return values

294

295 def set_measuring_options(

296 self, humidity: bool = True, sf6_purity: bool = False

297 ) -> None:

298 """

299 Send measuring options to the dew point mirror.

300

301 :param humidity: Perform humidity test or not?

302 :param sf6_purity: Perform SF6 purity test or not?

303 :raises SerialCommunicationIOError: when communication port is not opened

304 """

305

306 self.write(f"HumidityTest={1 if humidity else 0}")

307 self.write(f"SF6PurityTest={1 if sf6_purity else 0}")

308

309 self.measurement_options["dewpoint"] = humidity

310 self.measurement_options["SF6_Vol"] = sf6_purity