Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\supercube2015\base.py: 0%

# Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\supercube2015\base.py : 0%

## 148 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 Base classes for the Supercube device.

5"""

6

7 import logging

8 from time import sleep

9

10 from opcua import Node

11

12 from hvl_ccb import configdataclass

13 from hvl_ccb.comm import OpcUaSubHandler, OpcUaCommunicationConfig, OpcUaCommunication

14 from . import constants

15 from ..base import SingleCommDevice

16

17

18 class InvalidSupercubeStatusError(Exception):

19 """

20 Exception raised when supercube has invalid status.

21 """

22

23 pass

24

25

26 class SupercubeSubscriptionHandler(OpcUaSubHandler):

27 """

28 OPC Subscription handler for datachange events and normal events specifically

29 implemented for the Supercube devices.

30 """

31

32 def datachange_notification(self, node: Node, val, data):

33 """

34 In addition to the standard operation (debug logging entry of the datachange),

35 alarms are logged at INFO level using the alarm text.

36

37 :param node: the node object that triggered the datachange event

38 :param val: the new value

39 :param data:

40 """

41

42 super().datachange_notification(node, val, data)

43

44 # assume an alarm datachange

45 if node.nodeid.Identifier == constants.Errors.stop_number:

46 alarm_text = constants.AlarmText.get(val)

47 logging.getLogger(__name__).info(alarm_text)

48

49

50@configdataclass

51 class SupercubeConfiguration:

52 """

53 Configuration dataclass for the Supercube devices.

54 """

55

56 #: Namespace of the OPC variables, typically this is 3 (coming from Siemens)

57 namespace_index: int = 7

58

59

60@configdataclass

61 class SupercubeOpcUaCommunicationConfig(OpcUaCommunicationConfig):

62 """

63 Communication protocol configuration for OPC UA, specifications for the Supercube

64 devices.

65 """

66

67 #: Subscription handler for data change events

68 sub_handler: OpcUaSubHandler = SupercubeSubscriptionHandler()

69

70 port: int = 4845

71

72

73 class SupercubeOpcUaCommunication(OpcUaCommunication):

74 """

75 Communication protocol specification for Supercube devices.

76 """

77

78 @staticmethod

79 def config_cls():

80 return SupercubeOpcUaCommunicationConfig

81

82

83 class Supercube2015Base(SingleCommDevice):

84 """

85 Base class for Supercube variants.

86 """

87

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

89 """

90 Constructor for Supercube base class.

91

92 :param com: the communication protocol or its configuration

93 :param dev_config: the device configuration

94 """

95

96 super().__init__(com, dev_config)

97

98 self.logger = logging.getLogger(__name__)

99

100 @staticmethod

101 def default_com_cls():

102 return SupercubeOpcUaCommunication

103

104 def start(self) -> None:

105 """

106 Starts the device. Sets the root node for all OPC read and write commands to

107 the Siemens PLC object node which holds all our relevant objects and variables.

108 """

109

110 self.logger.info("Starting Supercube Base device")

111 super().start()

112

113 self.logger.debug("Add monitoring nodes")

114 self.com.init_monitored_nodes(

115 map( # type: ignore

116 str, constants.GeneralSockets

117 ),

118 self.config.namespace_index,

119 )

120 self.com.init_monitored_nodes(

121 map( # type: ignore

122 str, constants.GeneralSupport

123 ),

124 self.config.namespace_index,

125 )

126 self.com.init_monitored_nodes(

127 map( # type: ignore

128 str, constants.Safety

129 ),

130 self.config.namespace_index,

131 )

132

133 self.com.init_monitored_nodes(

134 str(constants.Errors.stop_number), self.config.namespace_index

135 )

136

137 self.com.init_monitored_nodes(

138 map( # type: ignore

139 str, constants.EarthingStick

140 ),

141 self.config.namespace_index,

142 )

143

144 self.logger.debug("Finished starting")

145

146 def stop(self) -> None:

147 """

148 Stop the Supercube device. Deactivates the remote control and closes the

149 communication protocol.

150 """

151

152 super().stop()

153

154 @staticmethod

155 def config_cls():

156 return SupercubeConfiguration

157

158 def read(self, node_id: str):

159 """

160 Local wrapper for the OPC UA communication protocol read method.

161

162 :param node_id: the id of the node to read.

163 :return: the value of the variable

164 """

165

166 result = self.com.read(str(node_id), self.config.namespace_index)

167 self.logger.debug(f"Read from node ID {node_id}: {result}")

168 return result

169

170 def write(self, node_id, value) -> None:

171 """

172 Local wrapper for the OPC UA communication protocol write method.

173

174 :param node_id: the id of the node to read

175 :param value: the value to write to the variable

176 """

177

178 self.logger.debug(f"Write to node ID {node_id}: {value}")

179 self.com.write(str(node_id), self.config.namespace_index, value)

180

181 def set_remote_control(self, state: bool) -> None:

182 """

183 Enable or disable remote control for the Supercube. This will effectively

184 display a message on the touchscreen HMI.

185

186 :param state: desired remote control state

187 """

188

189 raise NotImplementedError("Function does not exist in Supercube 2015.")

190

191 def get_support_input(self, port: int, contact: int) -> bool:

192 """

193 Get the state of a support socket input.

194

195 :param port: is the socket number (1..6)

196 :param contact: is the contact on the socket (1..2)

197 :return: digital input read state

198 """

199

200 return bool(self.read(constants.GeneralSupport.input(port, contact)))

201

202 def get_support_output(self, port: int, contact: int) -> bool:

203 """

204 Get the state of a support socket output.

205

206 :param port: is the socket number (1..6)

207 :param contact: is the contact on the socket (1..2)

208 :return: digital output read state

209 """

210

211 return bool(self.read(constants.GeneralSupport.output(port, contact)))

212

213 def set_support_output(self, port: int, contact: int, state: bool) -> None:

214 """

215 Set the state of a support output socket.

216

217 :param port: is the socket number (1..6)

218 :param contact: is the contact on the socket (1..2)

219 :param state: is the desired state of the support output

220 """

221

222 self.write(constants.GeneralSupport.output(port, contact), bool(state))

223

224 def set_support_output_impulse(

225 self, port: int, contact: int, duration: float = 0.2, pos_pulse: bool = True

226 ) -> None:

227 """

228 Issue an impulse of a certain duration on a support output contact. The polarity

229 of the pulse (On-wait-Off or Off-wait-On) is specified by the pos_pulse

230 argument.

231

232 This function is blocking.

233

234 :param port: is the socket number (1..6)

235 :param contact: is the contact on the socket (1..2)

236 :param duration: is the length of the impulse in seconds

237 :param pos_pulse: is True, if the pulse shall be HIGH, False if it shall be LOW

238 """

239

240 self.set_support_output(port, contact, pos_pulse)

241 sleep(duration)

242 self.set_support_output(port, contact, not pos_pulse)

243

244 def get_t13_socket(self, port: int) -> bool:

245 """

246 Read the state of a SEV T13 power socket.

247

248 :param port: is the socket number, one of `constants.T13_SOCKET_PORTS`

249 :return: on-state of the power socket

250 """

251

252 if port not in constants.T13_SOCKET_PORTS:

253 raise ValueError(f"port not in {constants.T13_SOCKET_PORTS}: {port}")

254

255 return bool(self.read(getattr(constants.GeneralSockets, f"t13_{port}")))

256

257 def set_t13_socket(self, port: int, state: bool) -> None:

258 """

259 Set the state of a SEV T13 power socket.

260

261 :param port: is the socket number, one of `constants.T13_SOCKET_PORTS`

262 :param state: is the desired on-state of the socket

263 """

264

265 if not isinstance(state, bool):

266 raise ValueError(f"state is not <bool>: {state}")

267

268 if port not in constants.T13_SOCKET_PORTS:

269 raise ValueError(f"port not in {constants.T13_SOCKET_PORTS}: {port}")

270

271 self.write(getattr(constants.GeneralSockets, f"t13_{port}"), state)

272

273 def get_cee16_socket(self) -> bool:

274 """

275 Read the on-state of the IEC CEE16 three-phase power socket.

276

277 :return: the on-state of the CEE16 power socket

278 """

279

280 return bool(self.read(constants.GeneralSockets.cee16))

281

282 def set_cee16_socket(self, state: bool) -> None:

283 """

284 Switch the IEC CEE16 three-phase power socket on or off.

285

286 :param state: desired on-state of the power socket

287 :raises ValueError: if state is not of type bool

288 """

289

290 if not isinstance(state, bool):

291 raise ValueError(f"state is not <bool>: {state}")

292

293 self.write(constants.GeneralSockets.cee16, state)

294

295 def get_status(self) -> int:

296 """

297 Get the safety circuit status of the Supercube.

298

299 :return: the safety status of the supercube's state machine;

300 see `constants.SafetyStatus`.

301 """

302

303 ready_for_red = self.read(constants.Safety.status_ready_for_red)

304 red = self.read(constants.Safety.status_red)

305 green = self.read(constants.Safety.status_green)

306 operate = self.read(constants.Safety.switchto_operate)

307 error = self.read(constants.Safety.status_error)

308 triggered = self.read(constants.BreakdownDetection.triggered)

309 fso_active = self.read(constants.BreakdownDetection.activated)

310

311 if error:

312 return constants.SafetyStatus.Error

313

314 if triggered and fso_active:

315 return constants.SafetyStatus.QuickStop

316

317 if not ready_for_red and not red and green:

318 return constants.SafetyStatus.GreenNotReady

319

320 if ready_for_red and not red and not operate:

321 return constants.SafetyStatus.GreenReady

322

323 if red and not green and not operate:

324 return constants.SafetyStatus.RedReady

325

326 if red and not green and operate:

327 return constants.SafetyStatus.RedOperate

328

329 raise InvalidSupercubeStatusError(

330 f"ready_for_red: {ready_for_red}, red: {red}, green: {green}, "

331 f"operate: {operate}, triggered: {triggered}, fso_active: {fso_active}"

332 )

333

334 def ready(self, state: bool) -> None:

335 """

336 Set ready state. Ready means locket safety circuit, red lamps, but high voltage

337 still off.

338

339 :param state: set ready state

340 """

341

342 if state:

343 if self.get_status() is constants.SafetyStatus.GreenReady:

344 self.write(constants.Safety.switchto_ready, True)

345 sleep(0.02)

346 self.write(constants.Safety.switchto_ready, False)

347 else:

348 self.write(constants.Safety.switchto_green, True)

349 sleep(0.02)

350 self.write(constants.Safety.switchto_green, False)

351

352 def operate(self, state: bool) -> None:

353 """

354 Set operate state. If the state is RedReady, this will turn on the high

355 voltage and close the safety switches.

356

357 :param state: set operate state

358 """

359

360 if state:

361 if self.get_status() is constants.SafetyStatus.RedReady:

362 self.write(constants.Safety.switchto_operate, True)

363 else:

364 self.write(constants.Safety.switchto_operate, False)

365

366 def get_measurement_ratio(self, channel: int) -> float:

367 """

368 Get the set measurement ratio of an AC/DC analog input channel. Every input

369 channel has a divider ratio assigned during setup of the Supercube system.

370 This ratio can be read out.

371

372 **Attention:** Supercube 2015 does not have a separate ratio for every analog

373 input. Therefore there is only one ratio for ``channel = 1``.

374

375 :param channel: number of the input channel (1..4)

376 :return: the ratio

377 """

378

379 return float(self.read(constants.MeasurementsDividerRatio.get(channel)))

380

381 def get_measurement_voltage(self, channel: int) -> float:

382 """

383 Get the measured voltage of an analog input channel. The voltage read out

384 here is already scaled by the configured divider ratio.

385

386 **Attention:** In contrast to the *new* Supercube, the old one returns here

387 the input voltage read at the ADC. It is not scaled by a factor.

388

389 :param channel: number of the input channel (1..4)

390 :return: measured voltage

391 """

392

393 return float(self.read(constants.MeasurementsScaledInput.get(channel)))

394

395 def get_earthing_status(self, number: int) -> int:

396 """

397 Get the status of an earthing stick, whether it is closed, open or undefined

398 (moving).

399

400 :param number: number of the earthing stick (1..6)

401 :return: earthing stick status; see constants.EarthingStickStatus

402 """

403

404 connected = self.read(constants.EarthingStick.status_connected(number))

405 open_ = self.read(constants.EarthingStick.status_open(number))

406 closed = self.read(constants.EarthingStick.status_closed(number))

407

408 if not connected:

409 return constants.EarthingStickStatus.inactive

410

411 if open_ and not closed:

412 return constants.EarthingStickStatus.open

413

414 if not open_ and closed:

415 return constants.EarthingStickStatus.closed

416

417 return constants.EarthingStickStatus.error

418

419 def get_earthing_manual(self, number: int) -> bool:

420 """

421 Get the manual status of an earthing stick. If an earthing stick is set to

422 manual, it is closed even if the system is in states RedReady or RedOperate.

423

424 :param number: number of the earthing stick (1..6)

425 :return: earthing stick manual status

426 """

427

428 return bool(self.read(constants.EarthingStick.manual(number)))

429

430 def set_earthing_manual(self, number: int, manual: bool) -> None:

431 """

432 Set the manual status of an earthing stick. If an earthing stick is set to

433 manual, it is closed even if the system is in states RedReady or RedOperate.

434

435 :param number: number of the earthing stick (1..6)

436 :param manual: earthing stick manual status (True or False)

437 """

438

439 if self.get_status() is constants.SafetyStatus.RedOperate:

440 raise RuntimeError("Status is Red Operate, should not move earthing.")

441 self.write(constants.EarthingStick.manual(number), manual)

442

443 def quit_error(self) -> None:

444 """

445 Quits errors that are active on the Supercube.

446 """

447

448 self.write(constants.Errors.quit, True)

449 sleep(0.1)

450 self.write(constants.Errors.quit, False)

451

452 def horn(self, state: bool) -> None:

453 """

454 Turns acoustic horn on or off.

455

456 :param state: Turns horn on (True) or off (False)

457 """

458 self.write(constants.Safety.horn, state)

459

460 def get_door_status(self, door: int) -> constants.DoorStatus:

461 """

462 Get the status of a safety fence door. See :class:`constants.DoorStatus` for

463 possible returned door statuses.

464

465 :param door: the door number (1..3)

466 :return: the door status

467 """

468

469 raise NotImplementedError(

470 "Door status not supported in old Supercube 2015 version."

471 )