# HG changeset patch # User Daniel O'Connor # Date 1266047382 -37800 # Node ID de9fe8d301475d315f3a30ce203435ace12b55de Initial commit. Seems to work for the basics. diff -r 000000000000 -r de9fe8d30147 wh1080.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wh1080.py Sat Feb 13 18:19:42 2010 +1030 @@ -0,0 +1,150 @@ +#!/usr/bin/env python + +import struct +import usb + +WH1080_VENDOR = 0x1941 +WH1080_DEVICE = 0x8021 + +WH1080_TIMEOUT = 100 + +WH1080_RECORD_SIZE = 32 +WH1080_PAGE_SIZE = 32 +WH1080_BASE = 0x100 + +WH1080_PAGE0_MAGIC1A = 0xffffffffffffaa55 +WH1080_PAGE0_MAGIC1B = 0xffffffffffaaaa55 +WH1080_PAGE0_MAGIC2 = 0xffffffffffffffff + +WH1080_WIND_DIRECTIONS = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", + "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"] + +def apparent_temp(Ta, rh, ws, Q = None): + """Compute apparent temperature. Obtained from the Australian BOM at http://www.bom.gov.au/info/thermal_stress/ +Ta = dry bulb temperature (Celcius) +rh = relative humidity (percentage) +ws = Wind speed (m/s) +Q = net radiation absorbed by body (W/m2) (optional)""" + e = rh / 100 * 6.105 * exp(17.27 * Ta / (237.7 + Ta)) + + if Q == None: + return(Ta + 0.33 * e - 0.70 * ws - 4.00) + else: + return(Ta + 0.33 * e - 0.70 * ws + 0.70 * Q/(ws + 10) - 4.25) + +def list2uintN(l, size): + res = 0 + for i in xrange(size): + res += l[i] << 8 * i + return res + +class WH1080(object): + def __init__(self): + + busses = usb.busses() + + # + # Search for the device we want + # + self.handle = None + for bus in busses: + for dev in bus.devices: + #print "Looking at 0x%04x 0x%04x" % (dev.idVendor, dev.idProduct) + if dev.idVendor == WH1080_VENDOR and dev.idProduct == WH1080_DEVICE: + # Open the device and claim the USB interface that supports the spec + self.handle = dev.open() + self.dev = dev + break + + if self.handle == None: + raise "Could not find a suitable USB device" + self.handle.claimInterface(0) + + def get_current_record(self): + page0 = Page0(self.read_page(0)) + return(page0.current_record) + + def read_current_record(self): + return self.read_record(self.get_current_record()) + + def read_record(self, record): + """Read the nominated record from the device""" + + # Calculate offset for this record + ofs = (record * WH1080_PAGE_SIZE) + WH1080_BASE + # 32 bytes covers 2 records. + # We don't want to read past the end of memory so we start at + # the even page then pick what we need. + ofs &= ~WH1080_RECORD_SIZE + + data = self.read_page(ofs) + #print "Reading record %d => 0x%04x" % (record, ofs) + if record % 2: + data = data[0:16] + else: + data = data[16:32] + + return Reading(data) + + def read_page(self, ofs): + """Read a page from the device at ofs +Due to apparent hardware bugs / race conditions we read a few times until we have 2 identical reads""" + for t in xrange(10): + pageA = self._read_page(ofs) + pageB = self._read_page(ofs) + if pageA == pageB: + break + else: + raise IOError("Could not read page cleanly") + + return pageA + + def _read_page(self, ofs): + """Read a page from from the device at ofs (no retries)""" + msb = (ofs >> 8) & 0xff + lsb = ofs & 0xff + + req = [0xa1, msb, lsb, 0x20] * 2 + if self.handle.controlMsg(usb.TYPE_CLASS | usb.RECIP_INTERFACE, 0x9, req, value = 0x200, timeout = WH1080_TIMEOUT) != 8: + raise IOError("Unable to send control message") + + data = self.handle.interruptRead(usb.ENDPOINT_IN | usb.RECIP_INTERFACE, WH1080_PAGE_SIZE, WH1080_TIMEOUT) + if len(data) != WH1080_PAGE_SIZE: + raise IOError("Unable to read from endpoint expected %d bytes got %d" % + (WH1080_PAGE_SIZE, len(data))) + + data = map(chr, data) + return reduce(lambda a, b: a + b, data) + +class Page0(object): + """Decode page 0, which contains a pointer to the current page""" + def __init__(self, data): + (magic1, magic2, current_offset) = struct.unpack('< Q Q 14x H', data) + if (magic1 != WH1080_PAGE0_MAGIC1A and magic1 != WH1080_PAGE0_MAGIC1B) or magic2 != WH1080_PAGE0_MAGIC2: + raise ValueError("page0 magic not valid") + + self.current_record = (current_offset - WH1080_BASE) / WH1080_PAGE_SIZE + #print "Offset 0x%04x => %d" % (current_offset, self.current_record) + +class Reading(object): + def __init__(self, data): + (self.last_save_mins, self.inside_humidity, self.inside_temp, + self.outside_humidity, self.outside_temp, self.pressure, + self.wind_speed, self.wind_gust, foo, self.wind_direction, + self.rain, bar) = struct.unpack('< B B h B h H B B B B H B', data) + + #print "foo = 0x%02x, bar = 0x%02x" % (foo, bar) + self.inside_temp /= 10.0 + self.outside_temp /= 10.0 + self.wind_speed /= 10.0 + self.pressure /= 10.0 + if self.wind_direction == 0x80 or self.wind_direction == 0xff: + self.wind_direction = None + else: + self.wind_direction = WH1080_WIND_DIRECTIONS[self.wind_direction] + + def __str__(self): + return "%2d %4.1f %3d %4.1f %3d %6.1f %5.1f %5.1f %s %4.1f" % ( + self.last_save_mins, self.inside_temp, self.inside_humidity, self.outside_temp, + self.outside_humidity, self.pressure, self.wind_speed, self.wind_gust, + self.wind_direction, self.rain)