0
|
1 #!/usr/bin/env python
|
|
2
|
|
3 import struct
|
|
4 import usb
|
|
5
|
|
6 WH1080_VENDOR = 0x1941
|
|
7 WH1080_DEVICE = 0x8021
|
|
8
|
|
9 WH1080_TIMEOUT = 100
|
|
10
|
|
11 WH1080_RECORD_SIZE = 32
|
|
12 WH1080_PAGE_SIZE = 32
|
|
13 WH1080_BASE = 0x100
|
|
14
|
|
15 WH1080_PAGE0_MAGIC1A = 0xffffffffffffaa55
|
|
16 WH1080_PAGE0_MAGIC1B = 0xffffffffffaaaa55
|
|
17 WH1080_PAGE0_MAGIC2 = 0xffffffffffffffff
|
|
18
|
|
19 WH1080_WIND_DIRECTIONS = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
|
|
20 "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
|
|
21
|
|
22 def apparent_temp(Ta, rh, ws, Q = None):
|
|
23 """Compute apparent temperature. Obtained from the Australian BOM at http://www.bom.gov.au/info/thermal_stress/
|
|
24 Ta = dry bulb temperature (Celcius)
|
|
25 rh = relative humidity (percentage)
|
|
26 ws = Wind speed (m/s)
|
|
27 Q = net radiation absorbed by body (W/m2) (optional)"""
|
|
28 e = rh / 100 * 6.105 * exp(17.27 * Ta / (237.7 + Ta))
|
|
29
|
|
30 if Q == None:
|
|
31 return(Ta + 0.33 * e - 0.70 * ws - 4.00)
|
|
32 else:
|
|
33 return(Ta + 0.33 * e - 0.70 * ws + 0.70 * Q/(ws + 10) - 4.25)
|
|
34
|
|
35 def list2uintN(l, size):
|
|
36 res = 0
|
|
37 for i in xrange(size):
|
|
38 res += l[i] << 8 * i
|
|
39 return res
|
|
40
|
|
41 class WH1080(object):
|
|
42 def __init__(self):
|
|
43
|
|
44 busses = usb.busses()
|
|
45
|
|
46 #
|
|
47 # Search for the device we want
|
|
48 #
|
|
49 self.handle = None
|
|
50 for bus in busses:
|
|
51 for dev in bus.devices:
|
|
52 #print "Looking at 0x%04x 0x%04x" % (dev.idVendor, dev.idProduct)
|
|
53 if dev.idVendor == WH1080_VENDOR and dev.idProduct == WH1080_DEVICE:
|
|
54 # Open the device and claim the USB interface that supports the spec
|
|
55 self.handle = dev.open()
|
|
56 self.dev = dev
|
|
57 break
|
|
58
|
|
59 if self.handle == None:
|
|
60 raise "Could not find a suitable USB device"
|
|
61 self.handle.claimInterface(0)
|
|
62
|
|
63 def get_current_record(self):
|
|
64 page0 = Page0(self.read_page(0))
|
|
65 return(page0.current_record)
|
|
66
|
|
67 def read_current_record(self):
|
|
68 return self.read_record(self.get_current_record())
|
|
69
|
|
70 def read_record(self, record):
|
|
71 """Read the nominated record from the device"""
|
|
72
|
|
73 # Calculate offset for this record
|
|
74 ofs = (record * WH1080_PAGE_SIZE) + WH1080_BASE
|
|
75 # 32 bytes covers 2 records.
|
|
76 # We don't want to read past the end of memory so we start at
|
|
77 # the even page then pick what we need.
|
|
78 ofs &= ~WH1080_RECORD_SIZE
|
|
79
|
|
80 data = self.read_page(ofs)
|
|
81 #print "Reading record %d => 0x%04x" % (record, ofs)
|
|
82 if record % 2:
|
|
83 data = data[0:16]
|
|
84 else:
|
|
85 data = data[16:32]
|
|
86
|
|
87 return Reading(data)
|
|
88
|
|
89 def read_page(self, ofs):
|
|
90 """Read a page from the device at ofs
|
|
91 Due to apparent hardware bugs / race conditions we read a few times until we have 2 identical reads"""
|
|
92 for t in xrange(10):
|
|
93 pageA = self._read_page(ofs)
|
|
94 pageB = self._read_page(ofs)
|
|
95 if pageA == pageB:
|
|
96 break
|
|
97 else:
|
|
98 raise IOError("Could not read page cleanly")
|
|
99
|
|
100 return pageA
|
|
101
|
|
102 def _read_page(self, ofs):
|
|
103 """Read a page from from the device at ofs (no retries)"""
|
|
104 msb = (ofs >> 8) & 0xff
|
|
105 lsb = ofs & 0xff
|
|
106
|
|
107 req = [0xa1, msb, lsb, 0x20] * 2
|
|
108 if self.handle.controlMsg(usb.TYPE_CLASS | usb.RECIP_INTERFACE, 0x9, req, value = 0x200, timeout = WH1080_TIMEOUT) != 8:
|
|
109 raise IOError("Unable to send control message")
|
|
110
|
|
111 data = self.handle.interruptRead(usb.ENDPOINT_IN | usb.RECIP_INTERFACE, WH1080_PAGE_SIZE, WH1080_TIMEOUT)
|
|
112 if len(data) != WH1080_PAGE_SIZE:
|
|
113 raise IOError("Unable to read from endpoint expected %d bytes got %d" %
|
|
114 (WH1080_PAGE_SIZE, len(data)))
|
|
115
|
|
116 data = map(chr, data)
|
|
117 return reduce(lambda a, b: a + b, data)
|
|
118
|
|
119 class Page0(object):
|
|
120 """Decode page 0, which contains a pointer to the current page"""
|
|
121 def __init__(self, data):
|
|
122 (magic1, magic2, current_offset) = struct.unpack('< Q Q 14x H', data)
|
|
123 if (magic1 != WH1080_PAGE0_MAGIC1A and magic1 != WH1080_PAGE0_MAGIC1B) or magic2 != WH1080_PAGE0_MAGIC2:
|
|
124 raise ValueError("page0 magic not valid")
|
|
125
|
|
126 self.current_record = (current_offset - WH1080_BASE) / WH1080_PAGE_SIZE
|
|
127 #print "Offset 0x%04x => %d" % (current_offset, self.current_record)
|
|
128
|
|
129 class Reading(object):
|
|
130 def __init__(self, data):
|
|
131 (self.last_save_mins, self.inside_humidity, self.inside_temp,
|
|
132 self.outside_humidity, self.outside_temp, self.pressure,
|
|
133 self.wind_speed, self.wind_gust, foo, self.wind_direction,
|
|
134 self.rain, bar) = struct.unpack('< B B h B h H B B B B H B', data)
|
|
135
|
|
136 #print "foo = 0x%02x, bar = 0x%02x" % (foo, bar)
|
|
137 self.inside_temp /= 10.0
|
|
138 self.outside_temp /= 10.0
|
|
139 self.wind_speed /= 10.0
|
|
140 self.pressure /= 10.0
|
|
141 if self.wind_direction == 0x80 or self.wind_direction == 0xff:
|
|
142 self.wind_direction = None
|
|
143 else:
|
|
144 self.wind_direction = WH1080_WIND_DIRECTIONS[self.wind_direction]
|
|
145
|
|
146 def __str__(self):
|
|
147 return "%2d %4.1f %3d %4.1f %3d %6.1f %5.1f %5.1f %s %4.1f" % (
|
|
148 self.last_save_mins, self.inside_temp, self.inside_humidity, self.outside_temp,
|
|
149 self.outside_humidity, self.pressure, self.wind_speed, self.wind_gust,
|
|
150 self.wind_direction, self.rain)
|