-
Notifications
You must be signed in to change notification settings - Fork 0
/
tftvantage.py
1861 lines (1542 loc) · 81.3 KB
/
tftvantage.py
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
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# Portions Copyright (c) 2009-2016 Tom Keffer <[email protected]>
#
# See the file LICENSE.txt for your full rights.
#
"""Classes and functions for interfacing with a Davis VantagePro, VantagePro2,
or VantageVue weather station"""
import datetime
import math
import struct
import time
DRIVER_NAME = 'Vantage'
MAX_RETRIES = 2
VANTAGE_CONFIG = {"type": "serial", "port": "/dev/ttyUSB0", "baudrate": "19200", "wait_before_retry": 1.2,
"command_delay": 1, "timeout": 6}
# ===============================================================================
# Constants
# ===============================================================================
_ack = chr(0x06)
_resend = chr(0x15) # NB: The Davis documentation gives this code as 0x21, but it's actually decimal 21
# ===============================================================================
# Exception Classes
# ===============================================================================
class DeviceIOError(IOError):
"""Base class of exceptions thrown when encountering an input/output error
with the hardware."""
class WakeupError(DeviceIOError):
"""Exception thrown when unable to wake up or initially connect with the
hardware."""
class CRCError(DeviceIOError):
"""Exception thrown when unable to pass a CRC check."""
class RetriesExceeded(DeviceIOError):
"""Exception thrown when max retries exceeded."""
class HardwareError(StandardError):
"""Exception thrown when an error is detected in the hardware."""
class UnknownArchiveType(HardwareError):
"""Exception thrown after reading an unrecognized archive type."""
class UnsupportedFeature(StandardError):
"""Exception thrown when attempting to access a feature that is not
supported (yet)."""
# ===============================================================================
# Utility Functions
# ===============================================================================
# ===============================================================================
# Vantage CRC Data
# ===============================================================================
_vantageCrc = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, # 0x00
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, # 0x08
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, # 0x10
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, # 0x18
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, # 0x20
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, # 0x28
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, # 0x30
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, # 0x38
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, # 0x40
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, # 0x48
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, # 0x50
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, # 0x58
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, # 0x60
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, # 0x68
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, # 0x70
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, # 0x78
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, # 0x80
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, # 0x88
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, # 0x90
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, # 0x98
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, # 0xA0
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, # 0xA8
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, # 0xB0
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, # 0xB8
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, # 0xC0
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, # 0xC8
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, # 0xD0
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, # 0xD8
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, # 0xE0
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, # 0xE8
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, # 0xF0
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 # 0xF8
]
# ==============================================================================
# Conversion Routines
# ==============================================================================
def crc16(string, crc_start=0):
""" Calculate CRC16 sum"""
crc_sum = reduce(lambda crc, ch: (_vantageCrc[(crc >> 8) ^ ord(ch)] ^ (crc << 8)) & 0xffff, string, crc_start)
return crc_sum
def to_int(x):
if isinstance(x, basestring) and x.lower() == 'none':
x = None
return int(x) if x is not None else None
def c_to_f(x):
return x * 1.8 + 32.0
def f_to_c(x):
return (x - 32.0) * 5.0 / 9.0
def dewpoint_f(t, r):
if t is None or r is None:
return None
td_c = dewpoint_c(f_to_c(t), r)
return c_to_f(td_c) if td_c is not None else None
def dewpoint_c(t, r):
if t is None or r is None:
return None
r = r / 100.0
try:
_gamma = 17.27 * t / (237.7 + t) + math.log(r)
td_c = 237.7 * _gamma / (17.27 - _gamma)
except (ValueError, OverflowError):
td_c = None
return td_c
def windchill_f(t_f, v_mph):
if t_f is None or v_mph is None:
return None
# only valid for temperatures below 50F and wind speeds over 3.0 mph
if t_f >= 50.0 or v_mph <= 3.0:
return t_f
wc_f = 35.74 + 0.6215 * t_f + (-35.75 + 0.4275 * t_f) * math.pow(v_mph, 0.16)
return wc_f
def heatindex_f(t, r):
if t is None or r is None:
return None
# Formula only valid for temperatures over 80F:
if t < 80.0 or r < 40.0:
return t
hi_f = -42.379 + 2.04901523 * t + 10.14333127 * r - 0.22475541 * t * r - 6.83783e-3 * t ** 2 \
- 5.481717e-2 * r ** 2 + 1.22874e-3 * t ** 2 * r + 8.5282e-4 * t * r ** 2 - 1.99e-6 * t ** 2 * r ** 2
if hi_f < t:
hi_f = t
return hi_f
def start_of_day(time_ts):
_time_tt = time.localtime(time_ts)
_bod_ts = time.mktime((_time_tt.tm_year,
_time_tt.tm_mon,
_time_tt.tm_mday,
0, 0, 0, 0, 0, -1))
return int(_bod_ts)
# ==============================================================================
# class ValueTuple
# ==============================================================================
""" A value, along with the unit it is in, can be represented by a 3-way tuple
called a value tuple. All routines can accept a simple unadorned
3-way tuple as a value tuple, but they return the type ValueTuple. It is
useful because its contents can be accessed using named attributes.
Item attribute Meaning
0 value The datum value (eg, 20.2)
1 unit The unit it is in ('degree_C")
2 group The unit group ("group_temperature")
It is valid to have a datum value of None.
It is also valid to have a unit type of None (meaning there is no information
about the unit the value is in). In this case, you won't be able to convert
it to another unit. """
class ValueTuple(tuple):
def __new__(cls, *args):
return tuple.__new__(cls)
@property
def value(self):
return self[0]
@property
def unit(self):
return self[1]
@property
def group(self):
return self[2]
# ValueTuples have some modest math abilities: subtraction and addition.
def __sub__(self, other):
if self[1] != other[1] or self[2] != other[2]:
raise TypeError("unsupported operand error for subtraction: %s and %s" % (self[1], other[1]))
return ValueTuple(self[0] - other[0], self[1], self[2])
def __add__(self, other):
if self[1] != other[1] or self[2] != other[2]:
raise TypeError("unsupported operand error for addition: %s and %s" % (self[1], other[1]))
return ValueTuple(self[0] + other[0], self[1], self[2])
# ===============================================================================
# class BaseWrapper
# ===============================================================================
class CommunicationBase(object):
"""Base class for (Serial|Ethernet)Wrapper"""
def __init__(self, wait_before_retry, command_delay):
self.wait_before_retry = wait_before_retry
self.command_delay = command_delay
def read(self, chars=1):
raise NotImplementedError()
def write(self, data):
raise NotImplementedError()
def flush_input(self):
raise NotImplementedError()
def queued_bytes(self):
raise NotImplementedError()
def wakeup_console(self, max_tries=MAX_RETRIES):
"""Wake up a Davis Vantage console.
This call has three purposes:
1. Wake up a sleeping console;
2. Cancel pending LOOP data (if any);
3. Flush the input buffer
Note: a flushed buffer is important before sending a command; we want to make sure
the next received character is the expected ACK.
If unsuccessful, an exception of type WakeupError is thrown"""
for count in xrange(max_tries):
try:
# Wake up console and cancel pending LOOP data.
# First try a gentle wake up
self.write('\n')
_resp = self.read(2)
if _resp == '\n\r': # LF, CR = 0x0a, 0x0d
# We're done; the console accepted our cancel LOOP command; nothing to flush
return
# That didn't work. Try a rude wake up.
# Flush any pending LOOP packets
self.flush_input()
# Look for the acknowledgment of the sent '\n'
_resp = self.read(2)
if _resp == '\n\r':
return
# print "Unable to wake up console... sleeping"
time.sleep(self.wait_before_retry)
# print "Unable to wake up console... retrying"
except DeviceIOError:
pass
raise WakeupError("Unable to wake up Vantage console")
def send_data(self, data):
"""Send data to the Davis console, waiting for an acknowledging <ACK>
If the <ACK> is not received, no retry is attempted. Instead, an exception
of type DeviceIOError is raised
data: The data to send, as a string"""
self.write(data)
# Look for the acknowledging ACK character
_resp = self.read()
if _resp != _ack:
raise DeviceIOError("No <ACK> received from Vantage console")
def send_data_with_crc16(self, data, max_tries=MAX_RETRIES):
"""Send data to the Davis console along with a CRC check, waiting for an acknowledging <ack>.
If none received, resend up to max_tries times.
data: The data to send, as a string"""
# Calculate the crc for the data:
_crc = crc16(data)
# ...and pack that on to the end of the data in big-endian order:
_data_with_crc = data + struct.pack(">H", _crc)
# Retry up to max_tries times:
for count in xrange(max_tries):
try:
self.write(_data_with_crc)
# Look for the acknowledgment.
_resp = self.read()
if _resp == _ack:
return
except DeviceIOError:
pass
raise CRCError("Unable to pass CRC16 check while sending data to Vantage console")
def send_command(self, command, max_tries=MAX_RETRIES):
"""Send a command to the console, then look for the string 'OK' in the response.
Any response from the console is split on \n\r characters and returned as a list."""
for count in xrange(max_tries):
try:
self.wakeup_console(max_tries=max_tries)
self.write(command)
# Takes some time for the Vantage to react and fill up the buffer. Sleep for a bit:
time.sleep(self.command_delay)
# Can't use function serial.readline() because the VP responds with \n\r, not just \n.
# So, instead find how many bytes are waiting and fetch them all
nc = self.queued_bytes()
_buffer = self.read(nc)
# Split the buffer on the newlines
_buffer_list = _buffer.strip().split('\n\r')
# The first member should be the 'OK' in the VP response
if _buffer_list[0] == 'OK':
# Return the rest:
return _buffer_list[1:]
except DeviceIOError:
# Caught an error. Keep trying...
pass
raise RetriesExceeded("Max retries exceeded while sending command %s" % command)
def get_data_with_crc16(self, nbytes, prompt=None, max_tries=MAX_RETRIES):
"""Get a packet of data and do a CRC16 check on it, asking for retransmit if necessary.
It is guaranteed that the length of the returned data will be of the requested length.
An exception of type CRCError will be thrown if the data cannot pass the CRC test
in the requested number of retries.
nbytes: The number of bytes (including the 2 byte CRC) to get.
prompt: Any string to be sent before requesting the data. Default=None
max_tries: Number of tries before giving up. Default=3
returns: the packet data as a string. The last 2 bytes will be the CRC"""
if prompt:
self.write(prompt)
first_time = True
_buffer = ''
for count in xrange(max_tries):
try:
if not first_time:
self.write(_resend)
_buffer = self.read(nbytes)
if crc16(_buffer) == 0:
return _buffer
except DeviceIOError:
pass
first_time = False
if _buffer:
raise CRCError("Unable to pass CRC16 check while getting data")
else:
raise DeviceIOError("Time out in get_data_with_crc16")
# ===============================================================================
# class Serial Wrapper
# ===============================================================================
def guard_termios(fn):
"""Decorator function that converts termios exceptions into our exceptions."""
# Some functions in the module 'serial' can raise undocumented termios
# exceptions. This catches them and converts them to our exceptions.
try:
import termios
def guarded_fn(*args, **kwargs):
try:
return fn(*args, **kwargs)
except termios.error, e:
raise DeviceIOError(e)
except ImportError:
import termios
def guarded_fn(*args, **kwargs):
return fn(*args, **kwargs)
return guarded_fn
class SerialWrapper(CommunicationBase):
"""Wraps a serial connection returned from package serial"""
def __init__(self, port, baudrate, timeout, wait_before_retry, command_delay):
super(SerialWrapper, self).__init__(wait_before_retry=wait_before_retry, command_delay=command_delay)
self.port = port
self.baudrate = baudrate
self.timeout = timeout
self.serial_port = None
@guard_termios
def flush_input(self):
self.serial_port.flushInput()
@guard_termios
def flush_output(self):
self.serial_port.flushOutput()
@guard_termios
def queued_bytes(self):
return self.serial_port.inWaiting()
def read(self, chars=1):
import serial
try:
_buffer = self.serial_port.read(chars)
except serial.SerialException, e:
# Re-raise as an error I/O error:
raise DeviceIOError(e)
n = len(_buffer)
if n != chars:
raise DeviceIOError("Expected to read %d chars; got %d instead" % (chars, n))
return _buffer
def write(self, data):
import serial
try:
n = self.serial_port.write(data)
except serial.SerialException, e:
# Re-raise as an error I/O error:
raise DeviceIOError(e)
# Python version 2.5 and earlier returns 'None', so it cannot be used to test for completion.
if n is not None and n != len(data):
raise DeviceIOError("Expected to write %d chars; sent %d instead" % (len(data), n))
def open_port(self):
import serial
# Open up the port and store it
self.serial_port = serial.Serial(self.port, self.baudrate, timeout=self.timeout)
def close_port(self):
try:
# This will cancel any pending loop:
self.write('\n')
except (HardwareError, UnsupportedFeature, DeviceIOError):
pass
self.serial_port.close()
# ===============================================================================
# class EthernetWrapper
# ===============================================================================
class EthernetWrapper(CommunicationBase):
"""Wrap a socket"""
def __init__(self, host, port, timeout, tcp_send_delay, wait_before_retry, command_delay):
super(EthernetWrapper, self).__init__(wait_before_retry=wait_before_retry, command_delay=command_delay)
self.host = host
self.port = port
self.timeout = timeout
self.tcp_send_delay = tcp_send_delay
self.socket = None
def open_port(self):
import socket
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(self.timeout)
self.socket.connect((self.host, self.port))
except (socket.error, socket.timeout, socket.herror), ex:
# Re-raise as an I/O error:
raise DeviceIOError(ex)
except StandardError:
raise
def close_port(self):
import socket
try:
# This will cancel any pending loop:
self.write('\n')
except (HardwareError, UnsupportedFeature, DeviceIOError):
pass
self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
def flush_input(self):
"""Flush the input buffer from WeatherLinkIP"""
import socket
try:
# This is a bit of a hack, but there is no analogue to pyserial's flushInput()
# Set socket timeout to 0 to get immediate result
self.socket.settimeout(0)
self.socket.recv(4096)
except (socket.timeout, socket.error):
pass
finally:
# set socket timeout back to original value
self.socket.settimeout(self.timeout)
def flush_output(self):
"""Flush the output buffer to WeatherLinkIP
This function does nothing as there should never be anything left in
the buffer when using socket.sendall()"""
pass
def queued_bytes(self):
"""Determine how many bytes are in the buffer"""
import socket
length = 0
try:
self.socket.settimeout(0)
length = len(self.socket.recv(8192, socket.MSG_PEEK))
except socket.error:
pass
finally:
self.socket.settimeout(self.timeout)
return length
def read(self, chars=1):
"""Read bytes from WeatherLinkIP"""
import socket
_buffer = ''
_remaining = chars
while _remaining:
_num = min(4096, _remaining)
try:
_recv = self.socket.recv(_num)
except (socket.timeout, socket.error), ex:
# Re-raise as an I/O error:
raise DeviceIOError(ex)
_nread = len(_recv)
if _nread == 0:
raise DeviceIOError("vantage: Expected %d characters; got zero instead" % (_num,))
_buffer += _recv
_remaining -= _nread
return _buffer
def write(self, data):
"""Write to a WeatherLinkIP"""
import socket
try:
self.socket.sendall(data)
# A delay of 0.0 gives socket write error; 0.01 gives no ack error; 0.05 is OK for our program
# Note: a delay of 0.5 s is required for wee_device --logger=logger_info
time.sleep(self.tcp_send_delay)
except (socket.timeout, socket.error), ex:
# Re-raise as an I/O error:
raise DeviceIOError(ex)
# ===============================================================================
# class Vantage
# ===============================================================================
class Vantage:
"""Class that represents a connection to a Davis Vantage console.
The connection to the console will be open after initialization"""
# Various codes used internally by the VP2:
barometer_unit_dict = {0: 'inHg', 1: 'mmHg', 2: 'hPa', 3: 'mbar'}
temperature_unit_dict = {0: 'degree_F', 1: 'degree_10F', 2: 'degree_C', 3: 'degree_10C'}
altitude_unit_dict = {0: 'foot', 1: 'meter'}
rain_unit_dict = {0: 'inch', 1: 'mm'}
wind_unit_dict = {0: 'mile_per_hour', 1: 'meter_per_second', 2: 'km_per_hour', 3: 'knot'}
wind_cup_dict = {0: 'small', 1: 'large'}
rain_bucket_dict = {0: "0.01 inches", 1: "0.2 MM", 2: "0.1 MM"}
transmitter_type_dict = {0: 'iss', 1: 'temp', 2: 'hum', 3: 'temp_hum', 4: 'wind',
5: 'rain', 6: 'leaf', 7: 'soil', 8: 'leaf_soil',
9: 'sensorlink', 10: 'none'}
hardware_type = None
def __init__(self, **vp_dict):
"""Initialize an object of type Vantage.
NAMED ARGUMENTS:
connection_type: The type of connection (serial|ethernet) [Required]
port: The serial port of the VP. [Required if serial/USB communication]
host: The Vantage network host [Required if Ethernet communication]
baudrate: Baudrate of the port. [Optional. Default 19200]
tcp_port: TCP port to connect to [Optional. Default 22222]
tcp_send_delay: After sending data to WeatherLinkIP how long to process the command [Optional. Default is 0.5]
timeout: How long to wait before giving up on a response from the serial port. [Optional. Default is 4]
wait_before_retry: How long to wait before retrying. [Optional. Default is 1.2 seconds]
command_delay: How long to wait after sending a command before looking for ack. [Optional. Default= 0.5]
max_tries: How many times to try again before giving up. [Optional. Default is 4]
iss_id: The station number of the ISS [Optional. Default is 1]
model_type: Vantage Pro model type. 1 := Vantage Pro; 2 := Vantage Pro2 [Optional. Default is 2]
"""
self.hardware_type = None
# These come from the configuration dictionary:
self.max_tries = int(vp_dict.get('max_tries', 4))
self.iss_id = to_int(vp_dict.get('iss_id'))
self.model_type = int(vp_dict.get('model_type', 2))
if self.model_type not in range(1, 3):
raise UnsupportedFeature("Unknown model_type (%d)" % self.model_type)
self.save_monthRain = None
self.max_dst_jump = 7200
# Get an appropriate port, depending on the connection type:
self.port = Vantage._port_factory(vp_dict)
# Open it up:
self.port.open_port()
# Read the EEPROM and fill in properties in this instance
self._setup()
def __del__(self):
self.close_port()
def open_port(self):
"""Open up the connection to the console"""
self.port.open_port()
def close_port(self):
"""Close the connection to the console. """
self.port.close_port()
def get_weather_loop(self, num_loops=1):
try:
loop_gen = self.gen_davis_loop_packets(num_loops)
list_loop = []
for loop_item in loop_gen:
list_loop.append(loop_item)
except (HardwareError, UnsupportedFeature, DeviceIOError):
list_loop = None
return list_loop
def get_latest_weather_loop(self):
try:
loop_gen = self.gen_davis_loop_packets(1)
loop_item = next(loop_gen)
except (HardwareError, UnsupportedFeature, DeviceIOError):
loop_item = None
return loop_item
def gen_loop_packets(self):
"""Generator function that returns loop packets"""
for count in range(self.max_tries):
while True:
try:
# Get LOOP packets in big batches This is necessary because there is
# an undocumented limit to how many LOOP records you can request
# on the VP (somewhere around 220).
for _loop_packet in self.gen_davis_loop_packets(200):
yield _loop_packet
except DeviceIOError:
break
raise RetriesExceeded("Max tries exceeded while getting LOOP data.")
def gen_davis_loop_packets(self, n=1):
"""Generator function to return n loop packets from a Vantage console
n: The number of packets to generate [default is 1]
yields: up to n loop packets (could be less in the event of a read or CRC error).
"""
self.port.wakeup_console(self.max_tries)
# Request n packets:
self.port.send_data("LOOP %d\n" % n)
for loop in range(n): # @UnusedVariable
# Fetch a packet...
_buffer = self.port.read(99)
# ... see if it passes the CRC test ...
if crc16(_buffer):
raise CRCError("LOOP buffer failed CRC check")
# ... decode it ...
loop_packet = self._unpack_loop_packet(_buffer[:95])
# .. then yield it
yield loop_packet
def gen_archive_records(self, since_ts):
"""A generator function to return archive packets from a Davis Vantage station.
since_ts: A timestamp. All data since (but not including) this time will be returned.
Pass in None for all data
yields: a sequence of dictionaries containing the data
"""
count = 0
while count < self.max_tries:
try:
for _record in self.gen_davis_archive_records(since_ts):
# Successfully retrieved record. Set count back to zero.
count = 0
since_ts = _record['dateTime']
yield _record
# The generator loop exited. We're done.
return
except DeviceIOError:
# Problem. Increment retry count
count += 1
raise RetriesExceeded("Max tries exceeded while getting archive data.")
def gen_davis_archive_records(self, since_ts):
"""A generator function to return archive records from a Davis Vantage station.
This version does not catch any exceptions."""
if since_ts:
since_tt = time.localtime(since_ts)
# NB: note that some of the Davis documentation gives the year offset as 1900.
# From experimentation, 2000 seems to be right, at least for the newer models:
_vantageDateStamp = since_tt[2] + (since_tt[1] << 5) + ((since_tt[0] - 2000) << 9)
_vantageTimeStamp = since_tt[3] * 100 + since_tt[4]
else:
_vantageDateStamp = _vantageTimeStamp = 0
# Pack the date and time into a string, little-endian order
_datestr = struct.pack("<HH", _vantageDateStamp, _vantageTimeStamp)
# Save the last good time:
_last_good_ts = since_ts if since_ts else 0
# Get the starting page and index. First, wake up the console...
self.port.wakeup_console(self.max_tries)
# ... request a dump...
self.port.send_data('DMPAFT\n')
# ... from the designated date (allow only one try because that's all the console allows):
self.port.send_data_with_crc16(_datestr, max_tries=1)
# Get the response with how many pages and starting index and decode it. Again, allow only one try:
_buffer = self.port.get_data_with_crc16(6, max_tries=1)
(_npages, _start_index) = struct.unpack("<HH", _buffer[:4])
# Cycle through the pages...
for ipage in xrange(_npages):
# ... get a page of archive data
_page = self.port.get_data_with_crc16(267, prompt=_ack, max_tries=1)
# Now extract each record from the page
for _index in xrange(_start_index, 5):
# Get the record string buffer for this index:
_record_string = _page[1 + 52 * _index:53 + 52 * _index]
# If the console has been recently initialized, there will
# be unused records, which are filled with 0xff. Detect this
# by looking at the first 4 bytes (the date and time):
if _record_string[0:4] == 4 * chr(0xff) or _record_string[0:4] == 4 * chr(0x00):
# This record has never been used. We're done.
return
# Unpack the archive packet from the string buffer:
_record = self._unpack_archive_packet(_record_string)
# Check to see if the time stamps are declining, which would
# signal that we are done.
if _record['dateTime'] is None or _record['dateTime'] <= _last_good_ts - self.max_dst_jump:
# The time stamp is declining. We're done.
return
# Set the last time to the current time, and yield the packet
_last_good_ts = _record['dateTime']
yield _record
# The starting index for pages other than the first is always zero
_start_index = 0
def gen_archive_dump(self):
"""A generator function to return all archive packets in the memory of a Davis Vantage station.
yields: a sequence of dictionaries containing the data
"""
# Wake up the console...
self.port.wakeup_console(self.max_tries)
# ... request a dump...
self.port.send_data('DMP\n')
# Cycle through the pages...
for ipage in xrange(512):
# ... get a page of archive data
_page = self.port.get_data_with_crc16(267, prompt=_ack, max_tries=self.max_tries)
# Now extract each record from the page
for _index in xrange(5):
# Get the record string buffer for this index:
_record_string = _page[1 + 52 * _index:53 + 52 * _index]
# If the console has been recently initialized, there will
# be unused records, which are filled with 0xff. Detect this
# by looking at the first 4 bytes (the date and time):
if _record_string[0:4] == 4 * chr(0xff) or _record_string[0:4] == 4 * chr(0x00):
# This record has never been used. Skip it
continue
# Unpack the raw archive packet:
_record = self._unpack_archive_packet(_record_string)
# Because the dump command does not go through the normal
# engine pipeline, we have to add these important software derived
# variables here.
try:
t = _record['outTemp']
r = _record['outHumidity']
w = _record['windSpeed']
_record['dewpoint'] = dewpoint_f(t, r)
_record['heatindex'] = heatindex_f(t, r)
_record['windchill'] = windchill_f(t, w)
except KeyError:
pass
yield _record
def gen_logger_summary(self):
"""A generator function to return a summary of each page in the logger.
yields: A 8-way tuple containing (page, index, year, month, day, hour, minute, timestamp)
"""
# Wake up the console...
self.port.wakeup_console(self.max_tries)
# ... request a dump...
self.port.send_data('DMP\n')
# Cycle through the pages...
for _ipage in xrange(512):
# ... get a page of archive data
_page = self.port.get_data_with_crc16(267, prompt=_ack, max_tries=self.max_tries)
# Now extract each record from the page
for _index in xrange(5):
# Get the record string buffer for this index:
_record_string = _page[1 + 52 * _index:53 + 52 * _index]
# If the console has been recently initialized, there will
# be unused records, which are filled with 0xff. Detect this
# by looking at the first 4 bytes (the date and time):
if _record_string[0:4] == 4 * chr(0xff) or _record_string[0:4] == 4 * chr(0x00):
# This record has never been used.
y = mo = d = h = mn = time_ts = None
else:
# Extract the date and time from the raw buffer:
datestamp, timestamp = struct.unpack("<HH", _record_string[0:4])
time_ts = _archive_datetime(datestamp, timestamp)
y = (0xfe00 & datestamp) >> 9 # year
mo = (0x01e0 & datestamp) >> 5 # month
d = (0x001f & datestamp) # day
h = timestamp // 100 # hour
mn = timestamp % 100 # minute
yield (_ipage, _index, y, mo, d, h, mn, time_ts)
def get_time(self):
"""Get the current time from the console, returning it as timestamp"""
time_dt = self.get_console_time()
return time.mktime(time_dt.timetuple())
def get_console_time(self):
"""Return the raw time on the console, uncorrected for DST or timezone."""
# Try up to max_tries times:
for unused_count in xrange(self.max_tries):
try:
# Wake up the console...
self.port.wakeup_console(max_tries=self.max_tries)
# ... request the time...
self.port.send_data('GETTIME\n')
# ... get the binary data. No prompt, only one try:
_buffer = self.port.get_data_with_crc16(8, max_tries=1)
(sec, minute, hr, day, mon, yr, unused_crc) = struct.unpack("<bbbbbbH", _buffer)
return datetime.datetime(yr + 1900, mon, day, hr, minute, sec)
except DeviceIOError:
# Caught an error. Keep retrying...
continue
raise RetriesExceeded("While getting console time")
def get_rx(self):
"""Returns reception statistics from the console.
Returns a tuple with 5 values: (# of packets, # of missed packets,
# of resynchronizations, the max # of packets received w/o an error,
the # of CRC errors detected.)"""
rx_list = self.port.send_command('RXCHECK\n')
# The following is a list of the reception statistics, but the elements are strings
rx_list_str = rx_list[0].split()
# Convert to numbers and return as a tuple:
rx_list = tuple(int(x) for x in rx_list_str)
return rx_list
def get_bar_data(self):
"""Gets barometer calibration data. Returns as a 9 element list."""
_bardata = self.port.send_command("BARDATA\n")
_barometer = float(_bardata[0].split()[1]) / 1000.0
_altitude = float(_bardata[1].split()[1])
_dewpoint = float(_bardata[2].split()[2])
_virt_temp = float(_bardata[3].split()[2])
_c = float(_bardata[4].split()[1])
_r = float(_bardata[5].split()[1]) / 1000.0
_barcal = float(_bardata[6].split()[1]) / 1000.0
_gain = float(_bardata[7].split()[1])
_offset = float(_bardata[8].split()[1])
return _barometer, _altitude, _dewpoint, _virt_temp, _c, _r, _barcal, _gain, _offset
def get_firmware_date(self):
"""Return the firmware date as a string. """
return self.port.send_command('VER\n')[0]
def get_firmware_version(self):
"""Return the firmware version as a string."""
return self.port.send_command('NVER\n')[0]
def get_station_info(self):
"""Return lat / lon, time zone, etc."""
(stnlat, stnlon) = self._get_eeprom_value(0x0B, "<2h")
stnlat /= 10.0
stnlon /= 10.0
man_or_auto = "MANUAL" if self._get_eeprom_value(0x12)[0] else "AUTO"
dst = "ON" if self._get_eeprom_value(0x13)[0] else "OFF"
gmt_or_zone = "GMT_OFFSET" if self._get_eeprom_value(0x16)[0] else "ZONE_CODE"
zone_code = self._get_eeprom_value(0x11)[0]
gmt_offset = self._get_eeprom_value(0x14, "<h")[0] / 100.0
return stnlat, stnlon, man_or_auto, dst, gmt_or_zone, zone_code, gmt_offset
def get_station_transmitters(self):
""" Get the types of transmitters on the eight channels."""
transmitters = []
use_tx = self._get_eeprom_value(0x17)[0]
transmitter_data = self._get_eeprom_value(0x19, "16B")
for transmitter_id in range(8):
transmitter_type = Vantage.transmitter_type_dict[transmitter_data[transmitter_id * 2] & 0x0F]
transmitter = {"transmitter_type": transmitter_type, "listen": (use_tx >> transmitter_id) & 1}
if transmitter_type in ['temp', 'temp_hum']:
# Extra temperature is origin 0.
transmitter['temp'] = (transmitter_data[transmitter_id * 2 + 1] & 0xF) + 1
if transmitter_type in ['hum', 'temp_hum']:
# Extra humidity is origin 1.
transmitter['hum'] = transmitter_data[transmitter_id * 2 + 1] >> 4
transmitters.append(transmitter)
return transmitters
def get_station_calibration(self):
""" Get the temperature/humidity/wind calibrations built into the console. """