Coverage for /usr/lib/python3/dist-packages/gpiozero/internal_devices.py: 28%

214 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-02-10 12:38 +0000

1# vim: set fileencoding=utf-8: 

2# 

3# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins 

4# 

5# Copyright (c) 2016-2021 Dave Jones <dave@waveform.org.uk> 

6# Copyright (c) 2017-2021 Ben Nuttall <ben@bennuttall.com> 

7# Copyright (c) 2019 Jeevan M R <14.jeevan@gmail.com> 

8# Copyright (c) 2019 Andrew Scheller <github@loowis.durge.org> 

9# 

10# SPDX-License-Identifier: BSD-3-Clause 

11 

12from __future__ import ( 

13 unicode_literals, 

14 print_function, 

15 absolute_import, 

16 division, 

17) 

18str = type('') 

19 

20 

21import os 

22import io 

23import subprocess 

24from datetime import datetime, time 

25import warnings 

26 

27from .devices import Device 

28from .mixins import EventsMixin, event 

29from .threads import GPIOThread 

30from .exc import ThresholdOutOfRange, DeviceClosed 

31 

32 

33class InternalDevice(EventsMixin, Device): 

34 """ 

35 Extends :class:`Device` to provide a basis for devices which have no 

36 specific hardware representation. These are effectively pseudo-devices and 

37 usually represent operating system services like the internal clock, file 

38 systems or network facilities. 

39 """ 

40 def __init__(self, pin_factory=None): 

41 self._closed = False 

42 super(InternalDevice, self).__init__(pin_factory=pin_factory) 

43 

44 def close(self): 

45 self._closed = True 

46 super(InternalDevice, self).close() 

47 

48 @property 

49 def closed(self): 

50 return self._closed 

51 

52 def __repr__(self): 

53 try: 

54 self._check_open() 

55 return "<gpiozero.%s object>" % self.__class__.__name__ 

56 except DeviceClosed: 

57 return "<gpiozero.%s object closed>" % self.__class__.__name__ 

58 

59 

60class PolledInternalDevice(InternalDevice): 

61 """ 

62 Extends :class:`InternalDevice` to provide a background thread to poll 

63 internal devices that lack any other mechanism to inform the instance of 

64 changes. 

65 """ 

66 def __init__(self, event_delay=1.0, pin_factory=None): 

67 self._event_thread = None 

68 self._event_delay = event_delay 

69 super(PolledInternalDevice, self).__init__(pin_factory=pin_factory) 

70 

71 def close(self): 

72 try: 

73 self._start_stop_events(False) 

74 except AttributeError: 

75 pass # pragma: no cover 

76 super(PolledInternalDevice, self).close() 

77 

78 @property 

79 def event_delay(self): 

80 """ 

81 The delay between sampling the device's value for the purposes of 

82 firing events. 

83 

84 Note that this only applies to events assigned to attributes like 

85 :attr:`~EventsMixin.when_activated` and 

86 :attr:`~EventsMixin.when_deactivated`. When using the 

87 :attr:`~SourceMixin.source` and :attr:`~ValuesMixin.values` properties, 

88 the sampling rate is controlled by the 

89 :attr:`~SourceMixin.source_delay` property. 

90 """ 

91 return self._event_delay 

92 

93 @event_delay.setter 

94 def event_delay(self, value): 

95 self._event_delay = float(value) 

96 

97 def wait_for_active(self, timeout=None): 

98 self._start_stop_events(True) 

99 try: 

100 return super(PolledInternalDevice, self).wait_for_active(timeout) 

101 finally: 

102 self._start_stop_events( 

103 self.when_activated or self.when_deactivated) 

104 

105 def wait_for_inactive(self, timeout=None): 

106 self._start_stop_events(True) 

107 try: 

108 return super(PolledInternalDevice, self).wait_for_inactive(timeout) 

109 finally: 

110 self._start_stop_events( 

111 self.when_activated or self.when_deactivated) 

112 

113 def _watch_value(self): 

114 while not self._event_thread.stopping.wait(self._event_delay): 

115 self._fire_events(self.pin_factory.ticks(), self.is_active) 

116 

117 def _start_stop_events(self, enabled): 

118 if self._event_thread and not enabled: 

119 self._event_thread.stop() 

120 self._event_thread = None 

121 elif not self._event_thread and enabled: 

122 self._event_thread = GPIOThread(self._watch_value) 

123 self._event_thread.start() 

124 

125 

126class PingServer(PolledInternalDevice): 

127 """ 

128 Extends :class:`PolledInternalDevice` to provide a device which is active 

129 when a *host* (domain name or IP address) can be pinged. 

130 

131 The following example lights an LED while ``google.com`` is reachable:: 

132 

133 from gpiozero import PingServer, LED 

134 from signal import pause 

135 

136 google = PingServer('google.com') 

137 led = LED(4) 

138 

139 google.when_activated = led.on 

140 google.when_deactivated = led.off 

141 

142 pause() 

143 

144 :param str host: 

145 The hostname or IP address to attempt to ping. 

146 

147 :type event_delay: float 

148 :param event_delay: 

149 The number of seconds between pings (defaults to 10 seconds). 

150 

151 :type pin_factory: Factory or None 

152 :param pin_factory: 

153 See :doc:`api_pins` for more information (this is an advanced feature 

154 which most users can ignore). 

155 """ 

156 def __init__(self, host, event_delay=10.0, pin_factory=None): 

157 self._host = host 

158 super(PingServer, self).__init__( 

159 event_delay=event_delay, pin_factory=pin_factory) 

160 self._fire_events(self.pin_factory.ticks(), self.is_active) 

161 

162 def __repr__(self): 

163 try: 

164 self._check_open() 

165 return '<gpiozero.PingServer object host="%s">' % self.host 

166 except DeviceClosed: 

167 return super(PingServer, self).__repr__() 

168 

169 @property 

170 def host(self): 

171 """ 

172 The hostname or IP address to test whenever :attr:`value` is queried. 

173 """ 

174 return self._host 

175 

176 @property 

177 def value(self): 

178 """ 

179 Returns :data:`1` if the host returned a single ping, and :data:`0` 

180 otherwise. 

181 """ 

182 # XXX This is doing a DNS lookup every time it's queried; should we 

183 # call gethostbyname in the constructor and ping that instead (good 

184 # for consistency, but what if the user *expects* the host to change 

185 # address?) 

186 with io.open(os.devnull, 'wb') as devnull: 

187 try: 

188 subprocess.check_call( 

189 ['ping', '-c1', self.host], 

190 stdout=devnull, stderr=devnull) 

191 except subprocess.CalledProcessError: 

192 return 0 

193 else: 

194 return 1 

195 

196 when_activated = event( 

197 """ 

198 The function to run when the device changes state from inactive 

199 (host unresponsive) to active (host responsive). 

200 

201 This can be set to a function which accepts no (mandatory) 

202 parameters, or a Python function which accepts a single mandatory 

203 parameter (with as many optional parameters as you like). If the 

204 function accepts a single mandatory parameter, the device that 

205 activated it will be passed as that parameter. 

206 

207 Set this property to ``None`` (the default) to disable the event. 

208 """) 

209 

210 when_deactivated = event( 

211 """ 

212 The function to run when the device changes state from inactive 

213 (host responsive) to active (host unresponsive). 

214 

215 This can be set to a function which accepts no (mandatory) 

216 parameters, or a Python function which accepts a single mandatory 

217 parameter (with as many optional parameters as you like). If the 

218 function accepts a single mandatory parameter, the device that 

219 activated it will be passed as that parameter. 

220 

221 Set this property to ``None`` (the default) to disable the event. 

222 """) 

223 

224 

225class CPUTemperature(PolledInternalDevice): 

226 """ 

227 Extends :class:`PolledInternalDevice` to provide a device which is active 

228 when the CPU temperature exceeds the *threshold* value. 

229 

230 The following example plots the CPU's temperature on an LED bar graph:: 

231 

232 from gpiozero import LEDBarGraph, CPUTemperature 

233 from signal import pause 

234 

235 # Use minimums and maximums that are closer to "normal" usage so the 

236 # bar graph is a bit more "lively" 

237 cpu = CPUTemperature(min_temp=50, max_temp=90) 

238 

239 print('Initial temperature: {}C'.format(cpu.temperature)) 

240 

241 graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True) 

242 graph.source = cpu 

243 

244 pause() 

245 

246 :param str sensor_file: 

247 The file from which to read the temperature. This defaults to the 

248 sysfs file :file:`/sys/class/thermal/thermal_zone0/temp`. Whatever 

249 file is specified is expected to contain a single line containing the 

250 temperature in milli-degrees celsius. 

251 

252 :param float min_temp: 

253 The temperature at which :attr:`value` will read 0.0. This defaults to 

254 0.0. 

255 

256 :param float max_temp: 

257 The temperature at which :attr:`value` will read 1.0. This defaults to 

258 100.0. 

259 

260 :param float threshold: 

261 The temperature above which the device will be considered "active". 

262 (see :attr:`is_active`). This defaults to 80.0. 

263 

264 :type event_delay: float 

265 :param event_delay: 

266 The number of seconds between file reads (defaults to 5 seconds). 

267 

268 :type pin_factory: Factory or None 

269 :param pin_factory: 

270 See :doc:`api_pins` for more information (this is an advanced feature 

271 which most users can ignore). 

272 """ 

273 def __init__(self, sensor_file='/sys/class/thermal/thermal_zone0/temp', 

274 min_temp=0.0, max_temp=100.0, threshold=80.0, event_delay=5.0, 

275 pin_factory=None): 

276 self.sensor_file = sensor_file 

277 super(CPUTemperature, self).__init__( 

278 event_delay=event_delay, pin_factory=pin_factory) 

279 try: 

280 if min_temp >= max_temp: 

281 raise ValueError('max_temp must be greater than min_temp') 

282 self.min_temp = min_temp 

283 self.max_temp = max_temp 

284 if not min_temp <= threshold <= max_temp: 

285 warnings.warn(ThresholdOutOfRange( 

286 'threshold is outside of the range (min_temp, max_temp)')) 

287 self.threshold = threshold 

288 self._fire_events(self.pin_factory.ticks(), self.is_active) 

289 except: 

290 self.close() 

291 raise 

292 

293 def __repr__(self): 

294 try: 

295 self._check_open() 

296 return '<gpiozero.CPUTemperature object temperature=%.2f>' % self.temperature 

297 except DeviceClosed: 

298 return super(CPUTemperature, self).__repr__() 

299 

300 @property 

301 def temperature(self): 

302 """ 

303 Returns the current CPU temperature in degrees celsius. 

304 """ 

305 with io.open(self.sensor_file, 'r') as f: 

306 return float(f.read().strip()) / 1000 

307 

308 @property 

309 def value(self): 

310 """ 

311 Returns the current CPU temperature as a value between 0.0 

312 (representing the *min_temp* value) and 1.0 (representing the 

313 *max_temp* value). These default to 0.0 and 100.0 respectively, hence 

314 :attr:`value` is :attr:`temperature` divided by 100 by default. 

315 """ 

316 temp_range = self.max_temp - self.min_temp 

317 return (self.temperature - self.min_temp) / temp_range 

318 

319 @property 

320 def is_active(self): 

321 """ 

322 Returns :data:`True` when the CPU :attr:`temperature` exceeds the 

323 *threshold*. 

324 """ 

325 return self.temperature > self.threshold 

326 

327 when_activated = event( 

328 """ 

329 The function to run when the device changes state from inactive to 

330 active (temperature reaches *threshold*). 

331 

332 This can be set to a function which accepts no (mandatory) 

333 parameters, or a Python function which accepts a single mandatory 

334 parameter (with as many optional parameters as you like). If the 

335 function accepts a single mandatory parameter, the device that 

336 activated it will be passed as that parameter. 

337 

338 Set this property to ``None`` (the default) to disable the event. 

339 """) 

340 

341 when_deactivated = event( 

342 """ 

343 The function to run when the device changes state from active to 

344 inactive (temperature drops below *threshold*). 

345 

346 This can be set to a function which accepts no (mandatory) 

347 parameters, or a Python function which accepts a single mandatory 

348 parameter (with as many optional parameters as you like). If the 

349 function accepts a single mandatory parameter, the device that 

350 activated it will be passed as that parameter. 

351 

352 Set this property to ``None`` (the default) to disable the event. 

353 """) 

354 

355 

356class LoadAverage(PolledInternalDevice): 

357 """ 

358 Extends :class:`PolledInternalDevice` to provide a device which is active 

359 when the CPU load average exceeds the *threshold* value. 

360 

361 The following example plots the load average on an LED bar graph:: 

362 

363 from gpiozero import LEDBarGraph, LoadAverage 

364 from signal import pause 

365 

366 la = LoadAverage(min_load_average=0, max_load_average=2) 

367 graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True) 

368 

369 graph.source = la 

370 

371 pause() 

372 

373 :param str load_average_file: 

374 The file from which to read the load average. This defaults to the 

375 proc file :file:`/proc/loadavg`. Whatever file is specified is expected 

376 to contain three space-separated load averages at the beginning of the 

377 file, representing 1 minute, 5 minute and 15 minute averages 

378 respectively. 

379 

380 :param float min_load_average: 

381 The load average at which :attr:`value` will read 0.0. This defaults to 

382 0.0. 

383 

384 :param float max_load_average: 

385 The load average at which :attr:`value` will read 1.0. This defaults to 

386 1.0. 

387 

388 :param float threshold: 

389 The load average above which the device will be considered "active". 

390 (see :attr:`is_active`). This defaults to 0.8. 

391 

392 :param int minutes: 

393 The number of minutes over which to average the load. Must be 1, 5 or 

394 15. This defaults to 5. 

395 

396 :type event_delay: float 

397 :param event_delay: 

398 The number of seconds between file reads (defaults to 10 seconds). 

399 

400 :type pin_factory: Factory or None 

401 :param pin_factory: 

402 See :doc:`api_pins` for more information (this is an advanced feature 

403 which most users can ignore). 

404 """ 

405 def __init__(self, load_average_file='/proc/loadavg', min_load_average=0.0, 

406 max_load_average=1.0, threshold=0.8, minutes=5, event_delay=10.0, 

407 pin_factory=None): 

408 if min_load_average >= max_load_average: 

409 raise ValueError( 

410 'max_load_average must be greater than min_load_average') 

411 self.load_average_file = load_average_file 

412 self.min_load_average = min_load_average 

413 self.max_load_average = max_load_average 

414 if not min_load_average <= threshold <= max_load_average: 

415 warnings.warn(ThresholdOutOfRange( 

416 'threshold is outside of the range (min_load_average, ' 

417 'max_load_average)')) 

418 self.threshold = threshold 

419 if minutes not in (1, 5, 15): 

420 raise ValueError('minutes must be 1, 5 or 15') 

421 self._load_average_file_column = { 

422 1: 0, 

423 5: 1, 

424 15: 2, 

425 }[minutes] 

426 super(LoadAverage, self).__init__( 

427 event_delay=event_delay, pin_factory=pin_factory) 

428 self._fire_events(self.pin_factory.ticks(), None) 

429 

430 def __repr__(self): 

431 try: 

432 self._check_open() 

433 return '<gpiozero.LoadAverage object load average=%.2f>' % self.load_average 

434 except DeviceClosed: 

435 return super(LoadAverage, self).__repr__() 

436 

437 @property 

438 def load_average(self): 

439 """ 

440 Returns the current load average. 

441 """ 

442 with io.open(self.load_average_file, 'r') as f: 

443 print(repr(f)) 

444 file_columns = f.read().strip().split() 

445 return float(file_columns[self._load_average_file_column]) 

446 

447 @property 

448 def value(self): 

449 """ 

450 Returns the current load average as a value between 0.0 (representing 

451 the *min_load_average* value) and 1.0 (representing the 

452 *max_load_average* value). These default to 0.0 and 1.0 respectively. 

453 """ 

454 load_average_range = self.max_load_average - self.min_load_average 

455 return (self.load_average - self.min_load_average) / load_average_range 

456 

457 @property 

458 def is_active(self): 

459 """ 

460 Returns :data:`True` when the :attr:`load_average` exceeds the 

461 *threshold*. 

462 """ 

463 return self.load_average > self.threshold 

464 

465 when_activated = event( 

466 """ 

467 The function to run when the device changes state from inactive to 

468 active (load average reaches *threshold*). 

469 

470 This can be set to a function which accepts no (mandatory) 

471 parameters, or a Python function which accepts a single mandatory 

472 parameter (with as many optional parameters as you like). If the 

473 function accepts a single mandatory parameter, the device that 

474 activated it will be passed as that parameter. 

475 

476 Set this property to ``None`` (the default) to disable the event. 

477 """) 

478 

479 when_deactivated = event( 

480 """ 

481 The function to run when the device changes state from active to 

482 inactive (load average drops below *threshold*). 

483 

484 This can be set to a function which accepts no (mandatory) 

485 parameters, or a Python function which accepts a single mandatory 

486 parameter (with as many optional parameters as you like). If the 

487 function accepts a single mandatory parameter, the device that 

488 activated it will be passed as that parameter. 

489 

490 Set this property to ``None`` (the default) to disable the event. 

491 """) 

492 

493 

494class TimeOfDay(PolledInternalDevice): 

495 """ 

496 Extends :class:`PolledInternalDevice` to provide a device which is active 

497 when the computer's clock indicates that the current time is between 

498 *start_time* and *end_time* (inclusive) which are :class:`~datetime.time` 

499 instances. 

500 

501 The following example turns on a lamp attached to an :class:`Energenie` 

502 plug between 07:00AM and 08:00AM:: 

503 

504 from gpiozero import TimeOfDay, Energenie 

505 from datetime import time 

506 from signal import pause 

507 

508 lamp = Energenie(1) 

509 morning = TimeOfDay(time(7), time(8)) 

510 

511 morning.when_activated = lamp.on 

512 morning.when_deactivated = lamp.off 

513 

514 pause() 

515 

516 Note that *start_time* may be greater than *end_time*, indicating a time 

517 period which crosses midnight. 

518 

519 :param ~datetime.time start_time: 

520 The time from which the device will be considered active. 

521 

522 :param ~datetime.time end_time: 

523 The time after which the device will be considered inactive. 

524 

525 :param bool utc: 

526 If :data:`True` (the default), a naive UTC time will be used for the 

527 comparison rather than a local time-zone reading. 

528 

529 :type event_delay: float 

530 :param event_delay: 

531 The number of seconds between file reads (defaults to 10 seconds). 

532 

533 :type pin_factory: Factory or None 

534 :param pin_factory: 

535 See :doc:`api_pins` for more information (this is an advanced feature 

536 which most users can ignore). 

537 """ 

538 def __init__(self, start_time, end_time, utc=True, event_delay=5.0, 

539 pin_factory=None): 

540 self._start_time = None 

541 self._end_time = None 

542 self._utc = True 

543 super(TimeOfDay, self).__init__( 

544 event_delay=event_delay, pin_factory=pin_factory) 

545 try: 

546 self._start_time = self._validate_time(start_time) 

547 self._end_time = self._validate_time(end_time) 

548 if self.start_time == self.end_time: 

549 raise ValueError('end_time cannot equal start_time') 

550 self._utc = utc 

551 self._fire_events(self.pin_factory.ticks(), self.is_active) 

552 except: 

553 self.close() 

554 raise 

555 

556 def __repr__(self): 

557 try: 

558 self._check_open() 

559 return '<gpiozero.TimeOfDay object active between %s and %s %s>' % ( 

560 self.start_time, self.end_time, ('local', 'UTC')[self.utc]) 

561 except DeviceClosed: 

562 return super(TimeOfDay, self).__repr__() 

563 

564 def _validate_time(self, value): 

565 if isinstance(value, datetime): 

566 value = value.time() 

567 if not isinstance(value, time): 

568 raise ValueError( 

569 'start_time and end_time must be a datetime, or time instance') 

570 return value 

571 

572 @property 

573 def start_time(self): 

574 """ 

575 The time of day after which the device will be considered active. 

576 """ 

577 return self._start_time 

578 

579 @property 

580 def end_time(self): 

581 """ 

582 The time of day after which the device will be considered inactive. 

583 """ 

584 return self._end_time 

585 

586 @property 

587 def utc(self): 

588 """ 

589 If :data:`True`, use a naive UTC time reading for comparison instead of 

590 a local timezone reading. 

591 """ 

592 return self._utc 

593 

594 @property 

595 def value(self): 

596 """ 

597 Returns :data:`1` when the system clock reads between :attr:`start_time` 

598 and :attr:`end_time`, and :data:`0` otherwise. If :attr:`start_time` is 

599 greater than :attr:`end_time` (indicating a period that crosses 

600 midnight), then this returns :data:`1` when the current time is 

601 greater than :attr:`start_time` or less than :attr:`end_time`. 

602 """ 

603 now = datetime.utcnow().time() if self.utc else datetime.now().time() 

604 if self.start_time < self.end_time: 

605 return int(self.start_time <= now <= self.end_time) 

606 else: 

607 return int(not self.end_time < now < self.start_time) 

608 

609 when_activated = event( 

610 """ 

611 The function to run when the device changes state from inactive to 

612 active (time reaches *start_time*). 

613 

614 This can be set to a function which accepts no (mandatory) 

615 parameters, or a Python function which accepts a single mandatory 

616 parameter (with as many optional parameters as you like). If the 

617 function accepts a single mandatory parameter, the device that 

618 activated it will be passed as that parameter. 

619 

620 Set this property to ``None`` (the default) to disable the event. 

621 """) 

622 

623 when_deactivated = event( 

624 """ 

625 The function to run when the device changes state from active to 

626 inactive (time reaches *end_time*). 

627 

628 This can be set to a function which accepts no (mandatory) 

629 parameters, or a Python function which accepts a single mandatory 

630 parameter (with as many optional parameters as you like). If the 

631 function accepts a single mandatory parameter, the device that 

632 activated it will be passed as that parameter. 

633 

634 Set this property to ``None`` (the default) to disable the event. 

635 """) 

636 

637 

638class DiskUsage(PolledInternalDevice): 

639 """ 

640 Extends :class:`PolledInternalDevice` to provide a device which is active 

641 when the disk space used exceeds the *threshold* value. 

642 

643 The following example plots the disk usage on an LED bar graph:: 

644 

645 from gpiozero import LEDBarGraph, DiskUsage 

646 from signal import pause 

647 

648 disk = DiskUsage() 

649 

650 print('Current disk usage: {}%'.format(disk.usage)) 

651 

652 graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True) 

653 graph.source = disk 

654 

655 pause() 

656 

657 :param str filesystem: 

658 A path within the filesystem for which the disk usage needs to be 

659 computed. This defaults to :file:`/`, which is the root filesystem. 

660 

661 :param float threshold: 

662 The disk usage percentage above which the device will be considered 

663 "active" (see :attr:`is_active`). This defaults to 90.0. 

664 

665 :type event_delay: float 

666 :param event_delay: 

667 The number of seconds between file reads (defaults to 30 seconds). 

668 

669 :type pin_factory: Factory or None 

670 :param pin_factory: 

671 See :doc:`api_pins` for more information (this is an advanced feature 

672 which most users can ignore). 

673 """ 

674 def __init__(self, filesystem='/', threshold=90.0, event_delay=30.0, 

675 pin_factory=None): 

676 super(DiskUsage, self).__init__( 

677 event_delay=event_delay, pin_factory=pin_factory) 

678 os.statvfs(filesystem) 

679 if not 0 <= threshold <= 100: 

680 warnings.warn(ThresholdOutOfRange( 

681 'threshold is outside of the range (0, 100)')) 

682 self.filesystem = filesystem 

683 self.threshold = threshold 

684 self._fire_events(self.pin_factory.ticks(), None) 

685 

686 def __repr__(self): 

687 try: 

688 self._check_open() 

689 return '<gpiozero.DiskUsage object usage=%.2f>' % self.usage 

690 except DeviceClosed: 

691 return super(DiskUsage, self).__repr__() 

692 

693 @property 

694 def usage(self): 

695 """ 

696 Returns the current disk usage in percentage. 

697 """ 

698 return self.value * 100 

699 

700 @property 

701 def value(self): 

702 """ 

703 Returns the current disk usage as a value between 0.0 and 1.0 by 

704 dividing :attr:`usage` by 100. 

705 """ 

706 # This slightly convoluted calculation is equivalent to df's "Use%"; 

707 # it calculates the percentage of FS usage as a proportion of the 

708 # space available to *non-root users*. Technically this means it can 

709 # exceed 100% (when FS is filled to the point that only root can write 

710 # to it), hence the clamp. 

711 vfs = os.statvfs(self.filesystem) 

712 used = vfs.f_blocks - vfs.f_bfree 

713 total = used + vfs.f_bavail 

714 return min(1.0, used / total) 

715 

716 @property 

717 def is_active(self): 

718 """ 

719 Returns :data:`True` when the disk :attr:`usage` exceeds the 

720 *threshold*. 

721 """ 

722 return self.usage > self.threshold 

723 

724 when_activated = event( 

725 """ 

726 The function to run when the device changes state from inactive to 

727 active (disk usage reaches *threshold*). 

728 

729 This can be set to a function which accepts no (mandatory) 

730 parameters, or a Python function which accepts a single mandatory 

731 parameter (with as many optional parameters as you like). If the 

732 function accepts a single mandatory parameter, the device that 

733 activated it will be passed as that parameter. 

734 

735 Set this property to ``None`` (the default) to disable the event. 

736 """) 

737 

738 when_deactivated = event( 

739 """ 

740 The function to run when the device changes state from active to 

741 inactive (disk usage drops below *threshold*). 

742 

743 This can be set to a function which accepts no (mandatory) 

744 parameters, or a Python function which accepts a single mandatory 

745 parameter (with as many optional parameters as you like). If the 

746 function accepts a single mandatory parameter, the device that 

747 activated it will be passed as that parameter. 

748 

749 Set this property to ``None`` (the default) to disable the event. 

750 """)