tom (and anyone else having problems with the wmr300 driver),
please try one of the following drivers, in this order, until you get one
that works:
wmr300-0.16b.py - this is the same as 0.12 that shipped in weewx 3.6.0, but
with patches
wmr300-0.16a.py - this is the same as 0.9 that shipped in weewx 3.5.0, but
with patches
wmr300-0.16c.py - this is the latest development version, and includes
additional diagnostics
let us know which one works. if one of them fails, please post the log
showing the failure. it would be helpful if you ran it two or three times,
and post the log corresponding to each failure.
please report the following information about your system:
- weewx version and how you installed it (setup.py versus deb versus rpm)
what was the last version of weewx you were running with no wmr300
failures?
- operating system
cat /etc/issue
- kernel
uname -a
- usb versions
dpkg -l | grep usb
- locale/lang
echo $LANG
the 'patches' referred to above include:
- fix for pressure (noticeable for stations at higher altitudes)
- fix for rain count
- new sensor mapping pattern
none of these patches affect the usb comms.
m
--
You received this message because you are subscribed to the Google Groups
"weewx-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.
#!/usr/bin/env python
# Copyright 2015 Matthew Wall
# See the file LICENSE.txt for your rights.
#
# Credits:
# Thanks to Benji for the identification and decoding of 7 packet types
#
# Thanks to Eric G for posting USB captures and providing hardware for testing
# https://groups.google.com/forum/#!topic/weewx-development/5R1ahy2NFsk
#
# Thanks to Zahlii
# https://bsweather.myworkbook.de/category/weather-software/
#
# No thanks to oregon scientific - repeated requests for hardware and/or
# specifications resulted in no response at all.
# TODO: battery level for each sensor
# TODO: signal strength for each sensor
# TODO: altitude
# TODO: archive interval
# FIXME: warn if altitude in pressure packet does not match weewx altitude
# FIXME: figure out unknown bytes in history packet
# FIXME: decode the 0xdb packets
# FIXME: figure out how to automatically reset the rain counter, otherwise
# rain count is not recorded once the counter hits maximum value.
# FIXME: the read/write logic is rather brittle. it appears that communication
# must be initiated with an interrupt write. after that, the station will
# spew data. this implementation starts with a read, which will fail with
# a 'No data available' usb error. that results in an empty buffer (instead
# of an exception popping up the stack) so that a heartbeat write is sent.
# the genLoopPacket and genStartupRecords logic should be refactored to make
# this behiavor explicit.
# FIXME: if the operating system is localized, the check for the string
# 'No data available' will probably fail. we should check for a code instead,
# but it is not clear whether such an element is available in a usb.USBError
# object, or whether it is available across different pyusb versions.
"""Driver for Oregon Scientific WMR300 weather stations.
Sensor data transmission frequencies:
wind: 2.5 to 3 seconds
TH: 10 to 12 seconds
rain: 20 to 24 seconds
The station supports 1 wind, 1 rain, 1 UV, and up to 8 temperature/humidity
sensors.
Sniffing USB traffic shows all communication is interrupt. The endpoint
descriptors for the device show this as well. Response timing is 1.
The station ships with "Weather OS PRO" software for windows. This was used
for the USB sniffing.
Internal observation names use the convention name_with_specifier. These are
mapped to the wview or other schema as needed with a configuration setting.
For example, for the wview schema, wind_speed maps to windSpeed, temperature_0
maps to inTemp, and humidity_1 maps to outHumidity.
Maximum value for rain counter is 40000 mm (10160 in) (0x9c 0x40). The counter
does not wrap; it must be reset when it hits maximum value otherwise rain data
will not be recorded.
Message types -----------------------------------------------------------------
packet types from station:
57 - station type/model; history count
41 - ACK
D2 - history; 128 bytes
D3 - temperature/humidity/dewpoint; 61 bytes
D4 - wind; 54 bytes
D5 - rain; 40 bytes
D6 - pressure; 46 bytes
DB - forecast; 32 bytes
DC - temperature/humidity ranges; 62 bytes
packet types from host:
A6 - heartbeat
41 - ACK
65 - ? each of these is ack-ed by the station
cd - ? start history request? last two bytes are one after most recent read
35 - ? finish history request? last two bytes are latest record count
72 - ?
73 - ?
notes:
WOP sends A6 message every 20 seconds
WOP requests history at startup, then again every 120 minutes
each A6 is followed by a 57 from the station
each data packet D* from the station is followed by an ack packet 41 from host
D2 (history) records are recorded every minute
D6 (pressure) packets seem to come every 15 minutes (900 seconds)
4,5 of 7x match 12,13 of 57
Message field decodings -------------------------------------------------------
Values are stored in 1 to 3 bytes in big endian order. Negative numbers are
stored as Two's Complement (if the first byte starts with F it is a negative
number).
no data:
7f ff
values for channel number:
0 - console sensor
1 - sensor 1
2 - sensor 2
...
8 - sensor 8
values for trend:
0 - steady
1 - rising
2 - falling
bitwise transformation for compass direction:
1000 0000 0000 0000 = NNW
0100 0000 0000 0000 = NW
0010 0000 0000 0000 = WNW
0001 0000 0000 0000 = W
0000 1000 0000 0000 = WSW
0000 0100 0000 0000 = SW
0000 0010 0000 0000 = SSW
0000 0001 0000 0000 = S
0000 0000 1000 0000 = SSE
0000 0000 0100 0000 = SE
0000 0000 0010 0000 = ESE
0000 0000 0001 0000 = E
0000 0000 0000 1000 = ENE
0000 0000 0000 0100 = NE
0000 0000 0000 0010 = NNE
0000 0000 0000 0001 = N
values for forecast:
0x08 - cloudy
0x0c - rainy
0x1e - partly cloudy
0x0e - partly cloudy at night
0x70 - sunny
0x00 - clear night
Message decodings -------------------------------------------------------------
message: ACK
byte hex dec description decoded value
0 41 A acknowledgement ACK
1 43 C
2 4b K
3 73
4 e5
5 0a
6 26
7 0e
8 c1
examples:
41 43 4b 73 e5 0a 26 0e c1
41 43 4b 65 19 e5 04
message: station info
byte hex dec description decoded value
0 57 W station type WMR300
1 4d M
2 52 R
3 33 3
4 30 0
5 30 0
6 2c ,
7 41 A station model A002
8 30 0
9 30 0
10 32 2
11 2c ,
12 0e
13 c1
14 00
15 00
16 2c ,
17 67 lastest history record 26391 (0x67*256 0x17)
18 17
19 2c ,
20 4b
21 2c ,
22 52
23 2c ,
examples:
57 4d 52 33 30 30 2c 41 30 30 32 2c 0e c1 00 00 2c 67 17 2c 4b 2c 52 2c
57 4d 52 33 30 30 2c 41 30 30 32 2c 88 8b 00 00 2c 2f b5 2c 4b 2c 52 2c
57 4d 52 33 30 30 2c 41 30 30 34 2c 0e c1 00 00 2c 7f e0 2c 4b 2c 49 2c
57 4d 52 33 30 30 2c 41 30 30 34 2c 88 8b 00 00 2c 7f e0 2c 4b 2c 49 2c
message: history
byte hex dec description decoded value
0 d2 packet type
1 80 128 packet length
2 31 count 12694
3 96
4 0f 15 year ee if not set
5 08 8 month ee if not set
6 0a 10 day ee if not set
7 06 6 hour
8 02 2 minute
9 00 temperature 0 21.7 C
10 d9
11 00 temperature 1 25.4 C
12 fe
13 7f temperature 2
14 ff
15 7f temperature 3
16 ff
17 7f temperature 4
18 ff
19 7f temperature 5
20 ff
21 7f temperature 6
22 ff
23 7f temperature 7
24 ff
25 7f temperature 8
26 ff (a*256 + b)/10
27 26 humidity 0 38 %
28 49 humidity 1 73 %
29 7f humidity 2
30 7f humidity 3
31 7f humidity 4
32 7f humidity 5
33 7f humidity 6
34 7f humidity 7
35 7f humidity 8
36 00 dewpoint 1 20.0 C
37 c8 (a*256 + b)/10
38 7f dewpoint 2
39 ff
40 7f dewpoint 3
41 ff
42 7f dewpoint 4
43 ff
44 7f dewpoint 5
45 ff
46 7f dewpoint 6
47 ff
48 7f dewpoint 7
49 ff
50 7f dewpoint 8
51 ff
52 7f heat index 1 C
53 fd (a*256 + b)/10
54 7f heat index 2
55 ff
56 7f heat index 3
57 ff
58 7f heat index 4
59 ff
60 7f heat index 5
61 ff
62 7f heat index 6
63 ff
64 7f heat index 7
65 ff
66 7f heat index 8
67 ff
68 7f wind chill C
69 fd (a*256 + b)/10
70 7f ?
71 ff ?
72 00 wind gust speed 0.0 m/s
73 00 (a*256 + b)/10
74 00 wind average speed 0.0 m/s
75 00 (a*256 + b)/10
76 01 wind gust direction 283 degrees
77 1b (a*256 + b)
78 01 wind average direction 283 degrees
78 1b (a*256 + b)
80 30 forecast
81 00 ?
82 00 ?
83 00 hourly rain hundredths_of_inch
84 00 (a*256 + b)
85 00 ?
86 00 accumulated rain hundredths_of_inch
87 03 (a*256 + b)
88 0f accumulated rain start year
89 07 accumulated rain start month
90 09 accumulated rain start day
91 13 accumulated rain start hour
92 09 accumulated rain start minute
93 00 rain rate hundredths_of_inch/hour
94 00 (a*256 + b)
95 26 pressure mbar
96 ab (a*256 + b)/10
97 01 pressure trend
98 7f ?
99 ff ?
100 7f ?
101 ff ?
102 7f ?
103 ff ?
104 7f ?
105 ff ?
106 7f ?
107 7f ?
108 7f ?
109 7f ?
110 7f ?
111 7f ?
112 7f ?
113 7f ?
114 ff ?
115 7f ?
116 ff ?
117 7f ?
118 ff ?
119 00 ?
120 00 ?
121 00 ?
122 00 ?
123 00 ?
124 00 ?
125 00 ?
126 f8 checksum
127 3b
message: temperature/humidity/dewpoint
byte hex dec description decoded value
0 D3 packet type
1 3D 61 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 12 hour
6 14 20 minute
7 01 1 channel number
8 00 temperature 19.5 C
9 C3
10 2D humidity 45 %
11 00 dewpoint 7.0 C
12 46
13 7F heat index? N/A
14 FD
15 00 temperature trend
16 00 humidity trend
17 0E 14 max_dewpoint_last_day year
18 05 5 month
19 09 9 day
20 0A 10 hour
21 24 36 minute
22 00 max_dewpoint_last_day 13.0 C
23 82
24 0E 14 min_dewpoint_last_day year
25 05 5 month
26 09 9 day
27 10 16 hour
28 1F 31 minute
29 00 min_dewpoint_last_day 6.0 C
30 3C
31 0E 14 max_dewpoint_last_month year
32 05 5 month
33 01 1 day
34 0F 15 hour
35 1B 27 minute
36 00 max_dewpoint_last_month 13.0 C
37 82
38 0E 14 min_dewpoint_last_month year
39 05 5 month
40 04 4 day
41 0B 11 hour
42 08 8 minute
43 FF min_dewpoint_last_month -1.0 C
44 F6
45 0E 14 max_heat_index? year
46 05 5 month
47 09 9 day
48 00 0 hour
49 00 0 minute
50 7F max_heat_index? N/A
51 FF
52 0E 14 min_heat_index? year
53 05 5 month
54 01 1 day
55 00 0 hour
56 00 0 minute
57 7F min_heat_index? N/A
58 FF
59 0B checksum
60 63
0 41 ACK
1 43
2 4B
3 D3 packet type
4 01 channel number
5 8B sometimes DF
examples:
41 43 4b d3 01 20
41 43 4b d3 00 20
message: wind
byte hex dec description decoded value
0 D4 packet type
1 36 54 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 18 hour
6 14 20 minute
7 01 1 channel number
8 00 gust speed 1.4 m/s
9 0E
10 00 gust direction 168 degrees
11 A8
12 00 average speed 2.9 m/s
13 1D
14 00 average direction 13 degrees
15 0D
16 00 compass direction 3 N/NNE
17 03
18 7F windchill 32765 N/A
19 FD
20 0E 14 gust today year
21 05 5 month
22 09 9 day
23 10 16 hour
24 3B 59 minute
25 00 gust today 10 m/s
26 64
27 00 gust direction today 39 degree
28 27
29 0E 14 gust this month year
30 05 5 month
31 09 9 day
32 10 16 hour
33 3B 59 minute
34 00 gust this month 10 m/s
35 64
36 00 gust direction this month 39 degree
37 27
38 0E 14 wind chill today year
39 05 5 month
40 09 9 day
41 00 0 hour
42 00 0 minute
43 7F windchill today N/A
44 FF
45 0E 14 windchill this month year
46 05 5 month
47 03 3 day
48 09 9 hour
49 04 4 minute
50 00 windchill this month 2.9 C
51 1D
52 07 checksum
53 6A
0 41 ACK
1 43
2 4B
3 D4 packet type
4 01 channel number
5 8B
examples:
41 43 4b d4 01 20
41 43 4b d4 01 16
message: rain
byte hex dec description decoded value
0 D5 packet type
1 28 40 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 18 hour
6 15 21 minute
7 01 1 channel number
8 00
9 00 rainfall this hour 0 inch
10 00
11 00
12 00 rainfall last 24 hours 0.12 inch
13 0C 12
14 00
15 00 rainfall accumulated 1.61 inch
16 A1 161
17 00 rainfall rate 0 inch/hr
18 00
19 0E 14 accumulated start year
20 04 4 month
21 1D 29 day
22 12 18 hour
23 00 0 minute
24 0E 14 max rate last 24 hours year
25 05 5 month
26 09 9 day
27 01 1 hour
28 0C 12 minute
29 00 0 max rate last 24 hours 0.11 inch/hr ((0x00<<8)+0x0b)/100.0
30 0B 11
31 0E 14 max rate last month year
32 05 5 month
33 02 2 day
34 04 4 hour
35 0C 12 minute
36 00 0 max rate last month 1.46 inch/hr ((0x00<<8)+0x92)/100.0
37 92 146
38 03 checksum 794 = (0x03<<8) + 0x1a
39 1A
0 41 ACK
1 43
2 4B
3 D5 packet type
4 01 channel number
5 8B
examples:
41 43 4b d5 01 20
41 43 4b d5 01 16
message: pressure
byte hex dec description decoded value
0 D6 packet type
1 2E 46 packet length
2 0E 14 year
3 05 5 month
4 0D 13 day
5 0E 14 hour
6 30 48 minute
7 00 1 channel number
8 26 station pressure 981.7 mbar ((0x26<<8)+0x59)/10.0
9 59
10 27 sea level pressure 1015.3 mbar ((0x27<<8)+0xa9)/10.0
11 A9
12 01 altitude meter 300 m (0x01<<8)+0x2c
13 2C
14 03 ?
15 00
16 0E 14 max pressure today year
17 05 5 max pressure today month
18 0D 13 max pressure today day
19 0C 12 max pressure today hour
20 33 51 max pressure today minute
21 27 max pressure today 1015.7 mbar
22 AD
23 0E 14 min pressure today year
24 05 5 min pressure today month
25 0D 13 min pressure today day
26 00 0 min pressure today hour
27 06 6 min pressure today minute
28 27 min pressure today 1014.1 mbar
29 9D
30 0E 14 max pressure month year
31 05 5 max pressure month month
32 04 4 max pressure month day
33 01 1 max pressure month hour
34 15 21 max pressure month minute
35 27 max pressure month 1022.5 mbar
36 F1
37 0E 14 min pressure month year
38 05 5 min pressure month month
39 0B 11 min pressure month day
40 00 0 min pressure month hour
41 06 6 min pressure month minute
42 27 min pressure month 1007.8 mbar
43 5E
44 06 checksum
45 EC
0 41 ACK
1 43
2 4B
3 D6 packet type
4 00 channel number
5 8B
examples:
41 43 4b d6 00 20
message: forecast
byte hex dec description decoded value
0 DB
1 20
2 0F 15 year
3 07 7 month
4 09 9 day
5 12 18 hour
6 23 35 minute
7 00
8 FA
9 79
10 FC
11 40
12 01
13 4A
14 06
15 17
16 14
17 23
18 06
19 01
20 00
21 00
22 01
23 01
24 01
25 00
26 00
27 00
28 FE
29 00
30 05 checksum
31 A5
0 41 ACK
1 43
2 4B
3 D6 packet type
4 00 channel number
5 20
examples:
41 43 4b db 00 20
message: temperature/humidity ranges
byte hex dec description decoded value
0 DC packet type
1 3E 62 packet length
2 0E 14 year
3 05 5 month
4 0D 13 day
5 0E 14 hour
6 30 48 minute
7 00 0 channel number
8 0E 14 max temp today year
9 05 5 month
10 0D 13 day
11 00 0 hour
12 00 0 minute
13 00 max temp today 20.8 C
14 D0
15 0E 14 min temp today year
16 05 5 month
17 0D 13 day
18 0B 11 hour
19 34 52 minute
20 00 min temp today 19.0 C
21 BE
22 0E 14 max temp month year
23 05 5 month
24 0A 10 day
25 0D 13 hour
26 19 25 minute
27 00 max temp month 21.4 C
28 D6
29 0E 14 min temp month year
30 05 5 month
31 04 4 day
32 03 3 hour
33 2A 42 minute
34 00 min temp month 18.1 C
35 B5
36 0E 14 max humidity today year
37 05 5 month
38 0D 13 day
39 05 5 hour
40 04 4 minute
41 45 max humidity today 69 %
42 0E 14 min numidity today year
43 05 5 month
44 0D 13 day
45 0B 11 hour
46 32 50 minute
47 41 min humidity today 65 %
48 0E 14 max humidity month year
49 05 5 month
50 0C 12 day
51 13 19 hour
52 32 50 minute
53 46 max humidity month 70 %
54 0E 14 min humidity month year
55 05 5 month
56 04 4 day
57 14 20 hour
58 0E 14 minute
59 39 min humidity month 57 %
60 07 checksum
61 BF
0 41 ACK
1 43
2 4B
3 DC packet type
4 00 0 channel number
5 8B
examples:
41 43 4b dc 01 20
41 43 4b dc 00 20
41 43 4b dc 01 16
41 43 4b dc 00 16
"""
from __future__ import with_statement
import syslog
import time
import usb
import weewx.drivers
import weewx.wxformulas
from weeutil.weeutil import timestamp_to_string
DRIVER_NAME = 'WMR300'
DRIVER_VERSION = '0.16a' # 0.9/3.5.0 plus patches
DEBUG_COMM = 0
DEBUG_PACKET = 0
DEBUG_COUNTS = 0
DEBUG_DECODE = 0
DEBUG_HISTORY = 0
DEBUG_RAIN = 1
def loader(config_dict, _):
return WMR300Driver(**config_dict[DRIVER_NAME])
def confeditor_loader():
return WMR300ConfEditor()
def logmsg(level, msg):
syslog.syslog(level, 'wmr300: %s' % msg)
def logdbg(msg):
logmsg(syslog.LOG_DEBUG, msg)
def loginf(msg):
logmsg(syslog.LOG_INFO, msg)
def logerr(msg):
logmsg(syslog.LOG_ERR, msg)
def logcrt(msg):
logmsg(syslog.LOG_CRIT, msg)
def _fmt_bytes(data):
return ' '.join(['%02x' % x for x in data])
def _lo(x):
return x - 256 * (x >> 8)
def _hi(x):
return x >> 8
class WMR300Driver(weewx.drivers.AbstractDevice):
"""weewx driver that communicates with a WMR300 weather station."""
# the default map is for the wview schema
DEFAULT_MAP = {
'pressure': 'pressure',
'barometer': 'barometer',
'windSpeed': 'wind_avg',
'windDir': 'wind_dir',
'windGust': 'wind_gust',
'windGustDir': 'wind_gust_dir',
'inTemp': 'temperature_0',
'outTemp': 'temperature_1',
'extraTemp1': 'temperature_2',
'extraTemp2': 'temperature_3',
'extraTemp3': 'temperature_4',
'extraTemp4': 'temperature_5',
'extraTemp5': 'temperature_6',
'extraTemp6': 'temperature_7',
'extraTemp7': 'temperature_8',
'inHumidity': 'humidity_0',
'outHumidity': 'humidity_1',
'extraHumid1': 'humidity_2',
'extraHumid2': 'humidity_3',
'extraHumid3': 'humidity_4',
'extraHumid4': 'humidity_5',
'extraHumid5': 'humidity_6',
'extraHumid6': 'humidity_7',
'extraHumid7': 'humidity_8',
'dewpoint': 'dewpoint_1',
'extraDewpoint1': 'dewpoint_2',
'extraDewpoint2': 'dewpoint_3',
'extraDewpoint3': 'dewpoint_4',
'extraDewpoint4': 'dewpoint_5',
'extraDewpoint5': 'dewpoint_6',
'extraDewpoint6': 'dewpoint_7',
'extraDewpoint7': 'dewpoint_8',
'heatindex': 'heatindex_1',
'extraHeatindex1': 'heatindex_2',
'extraHeatindex2': 'heatindex_3',
'extraHeatindex3': 'heatindex_4',
'extraHeatindex4': 'heatindex_5',
'extraHeatindex5': 'heatindex_6',
'extraHeatindex6': 'heatindex_7',
'extraHeatindex7': 'heatindex_8',
'windchill': 'windchill',
'rainRate': 'rain_rate'
}
def __init__(self, **stn_dict):
loginf('driver version is %s' % DRIVER_VERSION)
self.model = stn_dict.get('model', 'WMR300')
self.sensor_map = stn_dict.get('sensor_map', self.DEFAULT_MAP)
self.heartbeat = 20 # how often to send a6 messages, in seconds
self.history_retry = 60 # how often to retry history, in seconds
global DEBUG_COMM
DEBUG_COMM = int(stn_dict.get('debug_comm', DEBUG_COMM))
global DEBUG_PACKET
DEBUG_PACKET = int(stn_dict.get('debug_packet', DEBUG_PACKET))
global DEBUG_COUNTS
DEBUG_COUNTS = int(stn_dict.get('debug_counts', DEBUG_COUNTS))
global DEBUG_DECODE
DEBUG_DECODE = int(stn_dict.get('debug_decode', DEBUG_DECODE))
global DEBUG_HISTORY
DEBUG_HISTORY = int(stn_dict.get('debug_history', DEBUG_HISTORY))
global DEBUG_RAIN
DEBUG_RAIN = int(stn_dict.get('debug_rain', DEBUG_RAIN))
self.last_rain = None
self.last_a6 = 0
self.last_65 = 0
self.last_7x = 0
self.last_record = 0
self.station = Station()
self.station.open()
def closePort(self):
self.station.close()
self.station = None
@property
def hardware_name(self):
return self.model
def genLoopPackets(self):
while True:
try:
buf = self.station.read()
if buf:
pkt = Station.decode(buf)
if buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# send ack for most data packets
# FIXME: what is last number in the ACK?
# observed: 0x00 0x20 0xc1 0xc7 0xa0 0x99
cmd = [0x41, 0x43, 0x4b, buf[0], buf[7], _lo(self.last_record)]
self.station.write(cmd)
# we only care about packets with loop data
if pkt['packet_type'] in [0xd3, 0xd4, 0xd5, 0xd6]:
packet = self.convert_loop(pkt)
yield packet
if time.time() - self.last_a6 > self.heartbeat:
logdbg("request station status: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_a6 = time.time()
if self.last_7x == 0:
# FIXME: what are the 72/73 messages?
# observed:
# 73 e5 0a 26 0e c1
# 73 e5 0a 26 88 8b
# 72 a9 c1 60 52 00
# cmd = [0x72, 0xa9, 0xc1, 0x60, 0x52, 0x00]
cmd = [0x73, 0xe5, 0x0a, 0x26, 0x88, 0x8b]
# cmd = [0x73, 0xe5, 0x0a, 0x26, 0x0e, 0xc1]
self.station.write(cmd)
self.last_7x = time.time()
except usb.USBError, e:
if not e.args[0].find('No data available'):
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
def genStartupRecords(self, since_ts):
loginf("reading records since %s" % timestamp_to_string(since_ts))
hbuf = None
last_ts = None
cnt = 0
while True:
try:
buf = self.station.read()
if buf:
if buf[0] == 0xd2:
hbuf = buf
buf = None
elif buf[0] == 0x7f and hbuf is not None:
# FIXME: need better indicator of second half history
buf = hbuf + buf
hbuf = None
if buf and buf[0] == 0xd2:
self.last_record = Station.get_record_index(buf)
ts = Station._extract_ts(buf[4:9])
if ts is not None and ts > since_ts:
keep = True if last_ts is not None else False
pkt = Station.decode(buf)
packet = self.convert_historical(pkt, ts, last_ts)
last_ts = ts
if keep:
logdbg("historical record: %s" % packet)
cnt += 1
yield packet
if buf and buf[0] == 0x57:
idx = Station.get_latest_index(buf)
msg = "count=%s last_index=%s latest_index=%s" % (
cnt, self.last_record, idx)
if self.last_record + 1 >= idx:
loginf("catchup complete: %s" % msg)
break
loginf("catchup in progress: %s" % msg)
if buf and buf[0] == 0x41 and buf[3] == 0x65:
nxtrec = Station.get_next_index(self.last_record)
logdbg("request records starting with %s" % nxtrec)
cmd = [0xcd, 0x18, 0x30, 0x62, _hi(nxtrec), _lo(nxtrec)]
self.station.write(cmd)
if time.time() - self.last_a6 > self.heartbeat:
logdbg("request station status: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_a6 = time.time()
if self.last_7x == 0:
# FIXME: what does 72/73 do?
cmd = [0x73, 0xe5, 0x0a, 0x26, 0x88, 0x8b]
self.station.write(cmd)
self.last_7x = time.time()
if time.time() - self.last_65 > self.history_retry:
logdbg("initiate record request: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0x65, 0x19, 0xe5, 0x04, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_65 = time.time()
except usb.USBError, e:
if not e.args[0].find('No data available'):
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
def convert(self, pkt, ts):
# if debugging packets, log everything we got
if DEBUG_PACKET:
logdbg("raw packet: %s" % pkt)
# timestamp and unit system are the same no matter what
p = {'dateTime': ts, 'usUnits': weewx.METRICWX}
# map hardware names to the requested database schema names
for label in self.sensor_map:
if self.sensor_map[label] in pkt:
p[label] = pkt[self.sensor_map[label]]
# single variable to track last_rain assumes that any historical reads
# will happen before any loop reads, and no historical reads will
# happen after any loop reads. otherwise double-counting of rain
# events could happen.
if 'rain_total' in pkt:
p['rain'] = self.calculate_rain(pkt['rain_total'], self.last_rain)
if DEBUG_RAIN and pkt['rain_total'] != self.last_rain:
logdbg("rain=%s rain_total=%s last_rain=%s" %
(p['rain'], pkt['rain_total'], self.last_rain))
self.last_rain = pkt['rain_total']
if DEBUG_PACKET:
logdbg("converted packet: %s" % p)
return p
def convert_historical(self, pkt, ts, last_ts):
p = self.convert(pkt, ts)
if last_ts is not None:
p['interval'] = ts - last_ts
return p
def convert_loop(self, pkt):
p = self.convert(pkt, int(time.time() + 0.5))
return p
@staticmethod
def calculate_rain(newtotal, oldtotal):
"""Calculate the rain difference given two cumulative measurements."""
if newtotal is not None and oldtotal is not None:
if newtotal >= oldtotal:
delta = newtotal - oldtotal
else:
loginf("rain counter decrement detected: new=%s old=%s" %
(newtotal, oldtotal))
delta = None
else:
loginf("possible missed rain event: new=%s old=%s" %
(newtotal, oldtotal))
delta = None
return delta
class WMR300Error(weewx.WeeWxIOError):
"""map station errors to weewx io errors"""
class WrongLength(WMR300Error):
"""bad packet length"""
class BadChecksum(WMR300Error):
"""bogus checksum"""
class Station(object):
# these identify the weather station on the USB
VENDOR_ID = 0x0FDE
PRODUCT_ID = 0xCA08
MESSAGE_LENGTH = 64
EP_IN = 0x81
EP_OUT = 0x01
MAX_RECORDS = 50000 # FIXME: what is maximum number of records?
def __init__(self, vend_id=VENDOR_ID, prod_id=PRODUCT_ID):
self.vendor_id = vend_id
self.product_id = prod_id
self.handle = None
self.timeout = 100
self.interface = 0
self.recv_counts = dict()
self.send_counts = dict()
def __enter__(self):
self.open()
return self
def __exit__(self, _, value, traceback): # @UnusedVariable
self.close()
def open(self):
dev = self._find_dev(self.vendor_id, self.product_id)
if not dev:
raise WMR300Error("Unable to find station on USB: "
"cannot find device with "
"VendorID=0x%04x ProductID=0x%04x" %
(self.vendor_id, self.product_id))
self.handle = dev.open()
if not self.handle:
raise WMR300Error('Open USB device failed')
self.handle.reset()
# for HID devices on linux, be sure kernel does not claim the interface
try:
self.handle.detachKernelDriver(self.interface)
except (AttributeError, usb.USBError):
pass
# attempt to claim the interface
try:
self.handle.claimInterface(self.interface)
except usb.USBError, e:
self.close()
raise WMR300Error("Unable to claim interface %s: %s" %
(self.interface, e))
def close(self):
if self.handle is not None:
try:
self.handle.releaseInterface()
except (ValueError, usb.USBError), e:
loginf("Release interface failed: %s" % e)
self.handle = None
def reset(self):
self.handle.reset()
def read(self, count=True):
buf = None
try:
buf = self.handle.interruptRead(
Station.EP_IN, self.MESSAGE_LENGTH, self.timeout)
if DEBUG_COMM:
logdbg("read: %s" % _fmt_bytes(buf))
if DEBUG_COUNTS and count:
self.update_count(buf, self.recv_counts)
except usb.USBError, e:
if not e.args[0].find('No data available'):
raise
return buf
def write(self, buf):
if DEBUG_COMM:
logdbg("write: %s" % _fmt_bytes(buf))
# pad with zeros up to the standard message length
while len(buf) < self.MESSAGE_LENGTH:
buf.append(0x00)
sent = self.handle.interruptWrite(Station.EP_OUT, buf, self.timeout)
if DEBUG_COUNTS:
self.update_count(buf, self.send_counts)
return sent
# keep track of the message types for debugging purposes
@staticmethod
def update_count(buf, count_dict):
label = 'empty'
if buf and len(buf) > 0:
if buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# message type and channel for data packets
label = '%02x:%d' % (buf[0], buf[7])
elif (buf[0] in [0x41] and
buf[3] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]):
# message type and channel for data ack packets
label = '%02x:%02x:%d' % (buf[0], buf[3], buf[4])
else:
# otherwise just track the message type
label = '%02x' % buf[0]
if label in count_dict:
count_dict[label] += 1
else:
count_dict[label] = 1
cstr = []
for k in sorted(count_dict):
cstr.append('%s: %s' % (k, count_dict[k]))
logdbg('counts: %s' % ''.join(cstr))
@staticmethod
def _find_dev(vendor_id, product_id):
"""Find the first device with vendor and product ID on the USB."""
for bus in usb.busses():
for dev in bus.devices:
if dev.idVendor == vendor_id and dev.idProduct == product_id:
logdbg('Found station at bus=%s device=%s' %
(bus.dirname, dev.filename))
return dev
return None
@staticmethod
def _verify_length(label, length, buf):
if buf[1] != length:
raise WrongLength("%s: wrong length: expected %02x, got %02x" %
(label, length, buf[1]))
@staticmethod
def _verify_checksum(label, buf, msb_first=True):
"""Calculate and compare checksum"""
try:
cs1 = Station._calc_checksum(buf)
cs2 = Station._extract_checksum(buf, msb_first)
if cs1 != cs2:
raise BadChecksum("%s: bad checksum: %04x != %04x" %
(label, cs1, cs2))
except IndexError, e:
raise BadChecksum("%s: not enough bytes for checksum: %s" %
(label, e))
@staticmethod
def _calc_checksum(buf):
cs = 0
for x in buf[:-2]:
cs += x
return cs
@staticmethod
def _extract_checksum(buf, msb_first):
if msb_first:
return (buf[-2] << 8) | buf[-1]
return (buf[-1] << 8) | buf[-2]
@staticmethod
def _extract_ts(buf):
if buf[0] == 0xee and buf[1] == 0xee and buf[2] == 0xee:
# year, month, and day are 0xee when timestamp is unset
return None
try:
year = int(buf[0]) + 2000
month = int(buf[1])
day = int(buf[2])
hour = int(buf[3])
minute = int(buf[4])
return time.mktime((year, month, day, hour, minute, 0, -1, -1, -1))
except IndexError:
raise WMR300Error("buffer too short for timestamp")
except (OverflowError, ValueError), e:
raise WMR300Error(
"cannot create timestamp from y:%s m:%s d:%s H:%s M:%s: %s" %
(buf[0], buf[1], buf[2], buf[3], buf[4], e))
@staticmethod
def _extract_signed(hi, lo, m):
if hi == 0x7f:
return None
s = 0
if hi & 0xf0 == 0xf0:
s = 0x10000
return ((hi << 8) + lo - s) * m
@staticmethod
def _extract_value(buf, m):
if buf[0] == 0x7f:
return None
if len(buf) == 2:
return ((buf[0] << 8) + buf[1]) * m
return buf[0] * m
@staticmethod
def get_latest_index(buf):
# get the index of the most recent history record
if buf[0] != 0x57:
return None
return (buf[17] << 8) + buf[18]
@staticmethod
def get_next_index(n):
# return the index of the record after indicated index
if n == 0:
return 0x20
if n + 1 > Station.MAX_RECORDS:
return 0x20 # FIXME: verify the wraparound
return n + 1
@staticmethod
def get_record_index(buf):
# extract the index from the history record
if buf[0] != 0xd2:
return None
return (buf[2] << 8) + buf[3]
@staticmethod
def decode(buf):
try:
pkt = getattr(Station, '_decode_%02x' % buf[0])(buf)
if DEBUG_DECODE:
logdbg('decode: %s %s' % (_fmt_bytes(buf), pkt))
return pkt
except IndexError, e:
raise WMR300Error("cannot decode buffer: %s" % e)
except AttributeError:
raise WMR300Error("unknown packet type %02x: %s" %
(buf[0], _fmt_bytes(buf)))
@staticmethod
def _decode_57(buf):
"""57 packet contains station information"""
pkt = dict()
pkt['packet_type'] = 0x57
pkt['station_type'] = ''.join("%s" % chr(x) for x in buf[0:6])
pkt['station_model'] = ''.join("%s" % chr(x) for x in buf[7:11])
if DEBUG_HISTORY:
nrec = (buf[17] << 8) + buf[18]
logdbg("history records: %s" % nrec)
return pkt
@staticmethod
def _decode_41(_):
"""41 43 4b is ACK"""
pkt = dict()
pkt['packet_type'] = 0x41
return pkt
@staticmethod
def _decode_d2(buf):
"""D2 packet contains history data"""
Station._verify_length("D2", 0x80, buf)
Station._verify_checksum("D2", buf[:0x80], msb_first=False)
pkt = dict()
pkt['packet_type'] = 0xd2
pkt['ts'] = Station._extract_ts(buf[4:9])
for i in range(0, 9):
pkt['temperature_%d' % i] = Station._extract_signed(
buf[9 + 2 * i], buf[10 + 2 * i], 0.1) # C
pkt['humidity_%d' % i] = Station._extract_value(
buf[27 + i:28 + i], 1.0) # %
for i in range(1, 9):
pkt['dewpoint_%d' % i] = Station._extract_signed(
buf[36 + 2 * i], buf[37 + 2 * i], 0.1) # C
pkt['heatindex_%d' % i] = Station._extract_signed(
buf[52 + 2 * i], buf[53 + 2 * i], 0.1) # C
pkt['windchill'] = Station._extract_signed(buf[68], buf[69], 0.1) # C
pkt['wind_gust'] = Station._extract_value(buf[72:74], 0.1) # m/s
pkt['wind_avg'] = Station._extract_value(buf[74:76], 0.1) # m/s
pkt['wind_gust_dir'] = Station._extract_value(buf[76:78], 1.0) # degree
pkt['wind_dir'] = Station._extract_value(buf[78:80], 1.0) # degree
pkt['forecast'] = Station._extract_value(buf[80:81], 1.0)
pkt['rain_hour'] = Station._extract_value(buf[83:85], 0.254) # mm
pkt['rain_total'] = Station._extract_value(buf[86:88], 0.254) # mm
pkt['rain_start_dateTime'] = Station._extract_ts(buf[88:93])
pkt['rain_rate'] = Station._extract_value(buf[93:95], 0.254) # mm/hour
pkt['barometer'] = Station._extract_value(buf[95:97], 0.1) # mbar
pkt['pressure_trend'] = Station._extract_value(buf[97:98], 1.0)
return pkt
@staticmethod
def _decode_d3(buf):
"""D3 packet contains temperature/humidity data"""
Station._verify_length("D3", 0x3d, buf)
Station._verify_checksum("D3", buf[:0x3d])
pkt = dict()
pkt['packet_type'] = 0xd3
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['temperature_%d' % pkt['channel']] = Station._extract_signed(
buf[8], buf[9], 0.1) # C
pkt['humidity_%d' % pkt['channel']] = Station._extract_value(
buf[10:11], 1.0) # %
pkt['dewpoint_%d' % pkt['channel']] = Station._extract_signed(
buf[11], buf[12], 0.1) # C
return pkt
@staticmethod
def _decode_d4(buf):
"""D4 packet contains wind data"""
Station._verify_length("D4", 0x36, buf)
Station._verify_checksum("D4", buf[:0x36])
pkt = dict()
pkt['packet_type'] = 0xd4
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['wind_gust'] = Station._extract_value(buf[8:10], 0.1) # m/s
pkt['wind_gust_dir'] = Station._extract_value(buf[10:12], 1.0) # degree
pkt['wind_avg'] = Station._extract_value(buf[12:14], 0.1) # m/s
pkt['wind_dir'] = Station._extract_value(buf[14:16], 1.0) # degree
return pkt
@staticmethod
def _decode_d5(buf):
"""D5 packet contains rain data"""
Station._verify_length("D5", 0x28, buf)
Station._verify_checksum("D5", buf[:0x28])
pkt = dict()
pkt['packet_type'] = 0xd5
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['rain_hour'] = Station._extract_value(buf[9:11], 0.254) # mm
pkt['rain_24_hour'] = Station._extract_value(buf[12:14], 0.254) # mm
pkt['rain_total'] = Station._extract_value(buf[15:17], 0.254) # mm
pkt['rain_rate'] = Station._extract_value(buf[17:19], 0.254) # mm/hour
pkt['rain_start_dateTime'] = Station._extract_ts(buf[19:24])
return pkt
@staticmethod
def _decode_d6(buf):
"""D6 packet contains pressure data"""
Station._verify_length("D6", 0x2e, buf)
Station._verify_checksum("D6", buf[:0x2e])
pkt = dict()
pkt['packet_type'] = 0xd6
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['pressure'] = Station._extract_value(buf[8:10], 0.1) # mbar
pkt['barometer'] = Station._extract_value(buf[10:12], 0.1) # mbar
pkt['altitude'] = Station._extract_value(buf[12:14], 1.0) # meter
return pkt
@staticmethod
def _decode_dc(buf):
"""DC packet contains temperature/humidity range data"""
Station._verify_length("DC", 0x3e, buf)
Station._verify_checksum("DC", buf[:0x3e])
pkt = dict()
pkt['packet_type'] = 0xdc
pkt['ts'] = Station._extract_ts(buf[2:7])
return pkt
@staticmethod
def _decode_db(buf):
"""DB packet is forecast"""
Station._verify_length("DB", 0x20, buf)
Station._verify_checksum("DB", buf[:0x20])
pkt = dict()
pkt['packet_type'] = 0xdb
return pkt
class WMR300ConfEditor(weewx.drivers.AbstractConfEditor):
@property
def default_stanza(self):
return """
[WMR300]
# This section is for WMR300 weather stations.
# The station model, e.g., WMR300A
model = WMR300
# The driver to use:
driver = weewx.drivers.wmr300
"""
def modify_config(self, config_dict):
print """
Setting rainRate, windchill, heatindex, and dewpoint calculations to hardware."""
config_dict.setdefault('StdWXCalculate', {})
config_dict['StdWXCalculate'].setdefault('Calculatios', {})
config_dict['StdWXCalculate']['Calculations']['rainRate'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['windchill'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['heatindex'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['dewpoint'] = 'hardware'
# define a main entry point for basic testing of the station without weewx
# engine and service overhead. invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/user/wmr300.py
if __name__ == '__main__':
import optparse
usage = """%prog [options] [--help]"""
syslog.openlog('wmr300', syslog.LOG_PID | syslog.LOG_CONS)
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
parser = optparse.OptionParser(usage=usage)
parser.add_option('--version', dest='version', action='store_true',
help='display driver version')
(options, args) = parser.parse_args()
if options.version:
print "wmr300 driver version %s" % DRIVER_VERSION
exit(0)
stn_dict = {
'debug_comm': 1,
'debug_packet': 0,
'debug_counts': 1,
'debug_decode': 0
}
stn = WMR300Driver(**stn_dict)
for packet in stn.genLoopPackets():
print packet
#!/usr/bin/env python
# Copyright 2015 Matthew Wall
# See the file LICENSE.txt for your rights.
#
# Credits:
# Thanks to Benji for the identification and decoding of 7 packet types
#
# Thanks to Eric G for posting USB captures and providing hardware for testing
# https://groups.google.com/forum/#!topic/weewx-development/5R1ahy2NFsk
#
# Thanks to Zahlii
# https://bsweather.myworkbook.de/category/weather-software/
#
# No thanks to oregon scientific - repeated requests for hardware and/or
# specifications resulted in no response at all.
# TODO: battery level for each sensor
# TODO: signal strength for each sensor
# TODO: altitude
# TODO: archive interval
# FIXME: warn if altitude in pressure packet does not match weewx altitude
# FIXME: figure out unknown bytes in history packet
# FIXME: decode the 0xdb packets
# FIXME: figure out how to automatically reset the rain counter, otherwise
# rain count is not recorded once the counter hits maximum value.
# FIXME: the read/write logic is rather brittle. it appears that communication
# must be initiated with an interrupt write. after that, the station will
# spew data. this implementation starts with a read, which will fail with
# a 'No data available' usb error. that results in an empty buffer (instead
# of an exception popping up the stack) so that a heartbeat write is sent.
# the genLoopPacket and genStartupRecords logic should be refactored to make
# this behiavor explicit.
# FIXME: if the operating system is localized, the check for the string
# 'No data available' will probably fail. we should check for a code instead,
# but it is not clear whether such an element is available in a usb.USBError
# object, or whether it is available across different pyusb versions.
"""Driver for Oregon Scientific WMR300 weather stations.
Sensor data transmission frequencies:
wind: 2.5 to 3 seconds
TH: 10 to 12 seconds
rain: 20 to 24 seconds
The station supports 1 wind, 1 rain, 1 UV, and up to 8 temperature/humidity
sensors.
Sniffing USB traffic shows all communication is interrupt. The endpoint
descriptors for the device show this as well. Response timing is 1.
The station ships with "Weather OS PRO" software for windows. This was used
for the USB sniffing.
Internal observation names use the convention name_with_specifier. These are
mapped to the wview or other schema as needed with a configuration setting.
For example, for the wview schema, wind_speed maps to windSpeed, temperature_0
maps to inTemp, and humidity_1 maps to outHumidity.
Maximum value for rain counter is 40000 mm (10160 in) (0x9c 0x40). The counter
does not wrap; it must be reset when it hits maximum value otherwise rain data
will not be recorded.
Message types -----------------------------------------------------------------
packet types from station:
57 - station type/model; history count
41 - ACK
D2 - history; 128 bytes
D3 - temperature/humidity/dewpoint; 61 bytes
D4 - wind; 54 bytes
D5 - rain; 40 bytes
D6 - pressure; 46 bytes
DB - forecast; 32 bytes
DC - temperature/humidity ranges; 62 bytes
packet types from host:
A6 - heartbeat
41 - ACK
65 - ? each of these is ack-ed by the station
cd - ? start history request? last two bytes are one after most recent read
35 - ? finish history request? last two bytes are latest record count
72 - ?
73 - ?
notes:
WOP sends A6 message every 20 seconds
WOP requests history at startup, then again every 120 minutes
each A6 is followed by a 57 from the station
each data packet D* from the station is followed by an ack packet 41 from host
D2 (history) records are recorded every minute
D6 (pressure) packets seem to come every 15 minutes (900 seconds)
4,5 of 7x match 12,13 of 57
Message field decodings -------------------------------------------------------
Values are stored in 1 to 3 bytes in big endian order. Negative numbers are
stored as Two's Complement (if the first byte starts with F it is a negative
number).
no data:
7f ff
values for channel number:
0 - console sensor
1 - sensor 1
2 - sensor 2
...
8 - sensor 8
values for trend:
0 - steady
1 - rising
2 - falling
bitwise transformation for compass direction:
1000 0000 0000 0000 = NNW
0100 0000 0000 0000 = NW
0010 0000 0000 0000 = WNW
0001 0000 0000 0000 = W
0000 1000 0000 0000 = WSW
0000 0100 0000 0000 = SW
0000 0010 0000 0000 = SSW
0000 0001 0000 0000 = S
0000 0000 1000 0000 = SSE
0000 0000 0100 0000 = SE
0000 0000 0010 0000 = ESE
0000 0000 0001 0000 = E
0000 0000 0000 1000 = ENE
0000 0000 0000 0100 = NE
0000 0000 0000 0010 = NNE
0000 0000 0000 0001 = N
values for forecast:
0x08 - cloudy
0x0c - rainy
0x1e - partly cloudy
0x0e - partly cloudy at night
0x70 - sunny
0x00 - clear night
Message decodings -------------------------------------------------------------
message: ACK
byte hex dec description decoded value
0 41 A acknowledgement ACK
1 43 C
2 4b K
3 73
4 e5
5 0a
6 26
7 0e
8 c1
examples:
41 43 4b 73 e5 0a 26 0e c1
41 43 4b 65 19 e5 04
message: station info
byte hex dec description decoded value
0 57 W station type WMR300
1 4d M
2 52 R
3 33 3
4 30 0
5 30 0
6 2c ,
7 41 A station model A002
8 30 0
9 30 0
10 32 2
11 2c ,
12 0e
13 c1
14 00
15 00
16 2c ,
17 67 lastest history record 26391 (0x67*256 0x17)
18 17
19 2c ,
20 4b
21 2c ,
22 52
23 2c ,
examples:
57 4d 52 33 30 30 2c 41 30 30 32 2c 0e c1 00 00 2c 67 17 2c 4b 2c 52 2c
57 4d 52 33 30 30 2c 41 30 30 32 2c 88 8b 00 00 2c 2f b5 2c 4b 2c 52 2c
57 4d 52 33 30 30 2c 41 30 30 34 2c 0e c1 00 00 2c 7f e0 2c 4b 2c 49 2c
57 4d 52 33 30 30 2c 41 30 30 34 2c 88 8b 00 00 2c 7f e0 2c 4b 2c 49 2c
message: history
byte hex dec description decoded value
0 d2 packet type
1 80 128 packet length
2 31 count 12694
3 96
4 0f 15 year ee if not set
5 08 8 month ee if not set
6 0a 10 day ee if not set
7 06 6 hour
8 02 2 minute
9 00 temperature 0 21.7 C
10 d9
11 00 temperature 1 25.4 C
12 fe
13 7f temperature 2
14 ff
15 7f temperature 3
16 ff
17 7f temperature 4
18 ff
19 7f temperature 5
20 ff
21 7f temperature 6
22 ff
23 7f temperature 7
24 ff
25 7f temperature 8
26 ff (a*256 + b)/10
27 26 humidity 0 38 %
28 49 humidity 1 73 %
29 7f humidity 2
30 7f humidity 3
31 7f humidity 4
32 7f humidity 5
33 7f humidity 6
34 7f humidity 7
35 7f humidity 8
36 00 dewpoint 1 20.0 C
37 c8 (a*256 + b)/10
38 7f dewpoint 2
39 ff
40 7f dewpoint 3
41 ff
42 7f dewpoint 4
43 ff
44 7f dewpoint 5
45 ff
46 7f dewpoint 6
47 ff
48 7f dewpoint 7
49 ff
50 7f dewpoint 8
51 ff
52 7f heat index 1 C
53 fd (a*256 + b)/10
54 7f heat index 2
55 ff
56 7f heat index 3
57 ff
58 7f heat index 4
59 ff
60 7f heat index 5
61 ff
62 7f heat index 6
63 ff
64 7f heat index 7
65 ff
66 7f heat index 8
67 ff
68 7f wind chill C
69 fd (a*256 + b)/10
70 7f ?
71 ff ?
72 00 wind gust speed 0.0 m/s
73 00 (a*256 + b)/10
74 00 wind average speed 0.0 m/s
75 00 (a*256 + b)/10
76 01 wind gust direction 283 degrees
77 1b (a*256 + b)
78 01 wind average direction 283 degrees
78 1b (a*256 + b)
80 30 forecast
81 00 ?
82 00 ?
83 00 hourly rain hundredths_of_inch
84 00 (a*256 + b)
85 00 ?
86 00 accumulated rain hundredths_of_inch
87 03 (a*256 + b)
88 0f accumulated rain start year
89 07 accumulated rain start month
90 09 accumulated rain start day
91 13 accumulated rain start hour
92 09 accumulated rain start minute
93 00 rain rate hundredths_of_inch/hour
94 00 (a*256 + b)
95 26 pressure mbar
96 ab (a*256 + b)/10
97 01 pressure trend
98 7f ?
99 ff ?
100 7f ?
101 ff ?
102 7f ?
103 ff ?
104 7f ?
105 ff ?
106 7f ?
107 7f ?
108 7f ?
109 7f ?
110 7f ?
111 7f ?
112 7f ?
113 7f ?
114 ff ?
115 7f ?
116 ff ?
117 7f ?
118 ff ?
119 00 ?
120 00 ?
121 00 ?
122 00 ?
123 00 ?
124 00 ?
125 00 ?
126 f8 checksum
127 3b
message: temperature/humidity/dewpoint
byte hex dec description decoded value
0 D3 packet type
1 3D 61 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 12 hour
6 14 20 minute
7 01 1 channel number
8 00 temperature 19.5 C
9 C3
10 2D humidity 45 %
11 00 dewpoint 7.0 C
12 46
13 7F heat index? N/A
14 FD
15 00 temperature trend
16 00 humidity trend
17 0E 14 max_dewpoint_last_day year
18 05 5 month
19 09 9 day
20 0A 10 hour
21 24 36 minute
22 00 max_dewpoint_last_day 13.0 C
23 82
24 0E 14 min_dewpoint_last_day year
25 05 5 month
26 09 9 day
27 10 16 hour
28 1F 31 minute
29 00 min_dewpoint_last_day 6.0 C
30 3C
31 0E 14 max_dewpoint_last_month year
32 05 5 month
33 01 1 day
34 0F 15 hour
35 1B 27 minute
36 00 max_dewpoint_last_month 13.0 C
37 82
38 0E 14 min_dewpoint_last_month year
39 05 5 month
40 04 4 day
41 0B 11 hour
42 08 8 minute
43 FF min_dewpoint_last_month -1.0 C
44 F6
45 0E 14 max_heat_index? year
46 05 5 month
47 09 9 day
48 00 0 hour
49 00 0 minute
50 7F max_heat_index? N/A
51 FF
52 0E 14 min_heat_index? year
53 05 5 month
54 01 1 day
55 00 0 hour
56 00 0 minute
57 7F min_heat_index? N/A
58 FF
59 0B checksum
60 63
0 41 ACK
1 43
2 4B
3 D3 packet type
4 01 channel number
5 8B sometimes DF
examples:
41 43 4b d3 01 20
41 43 4b d3 00 20
message: wind
byte hex dec description decoded value
0 D4 packet type
1 36 54 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 18 hour
6 14 20 minute
7 01 1 channel number
8 00 gust speed 1.4 m/s
9 0E
10 00 gust direction 168 degrees
11 A8
12 00 average speed 2.9 m/s
13 1D
14 00 average direction 13 degrees
15 0D
16 00 compass direction 3 N/NNE
17 03
18 7F windchill 32765 N/A
19 FD
20 0E 14 gust today year
21 05 5 month
22 09 9 day
23 10 16 hour
24 3B 59 minute
25 00 gust today 10 m/s
26 64
27 00 gust direction today 39 degree
28 27
29 0E 14 gust this month year
30 05 5 month
31 09 9 day
32 10 16 hour
33 3B 59 minute
34 00 gust this month 10 m/s
35 64
36 00 gust direction this month 39 degree
37 27
38 0E 14 wind chill today year
39 05 5 month
40 09 9 day
41 00 0 hour
42 00 0 minute
43 7F windchill today N/A
44 FF
45 0E 14 windchill this month year
46 05 5 month
47 03 3 day
48 09 9 hour
49 04 4 minute
50 00 windchill this month 2.9 C
51 1D
52 07 checksum
53 6A
0 41 ACK
1 43
2 4B
3 D4 packet type
4 01 channel number
5 8B
examples:
41 43 4b d4 01 20
41 43 4b d4 01 16
message: rain
byte hex dec description decoded value
0 D5 packet type
1 28 40 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 18 hour
6 15 21 minute
7 01 1 channel number
8 00
9 00 rainfall this hour 0 inch
10 00
11 00
12 00 rainfall last 24 hours 0.12 inch
13 0C 12
14 00
15 00 rainfall accumulated 1.61 inch
16 A1 161
17 00 rainfall rate 0 inch/hr
18 00
19 0E 14 accumulated start year
20 04 4 month
21 1D 29 day
22 12 18 hour
23 00 0 minute
24 0E 14 max rate last 24 hours year
25 05 5 month
26 09 9 day
27 01 1 hour
28 0C 12 minute
29 00 0 max rate last 24 hours 0.11 inch/hr ((0x00<<8)+0x0b)/100.0
30 0B 11
31 0E 14 max rate last month year
32 05 5 month
33 02 2 day
34 04 4 hour
35 0C 12 minute
36 00 0 max rate last month 1.46 inch/hr ((0x00<<8)+0x92)/100.0
37 92 146
38 03 checksum 794 = (0x03<<8) + 0x1a
39 1A
0 41 ACK
1 43
2 4B
3 D5 packet type
4 01 channel number
5 8B
examples:
41 43 4b d5 01 20
41 43 4b d5 01 16
message: pressure
byte hex dec description decoded value
0 D6 packet type
1 2E 46 packet length
2 0E 14 year
3 05 5 month
4 0D 13 day
5 0E 14 hour
6 30 48 minute
7 00 1 channel number
8 26 station pressure 981.7 mbar ((0x26<<8)+0x59)/10.0
9 59
10 27 sea level pressure 1015.3 mbar ((0x27<<8)+0xa9)/10.0
11 A9
12 01 altitude meter 300 m (0x01<<8)+0x2c
13 2C
14 03 ?
15 00
16 0E 14 max pressure today year
17 05 5 max pressure today month
18 0D 13 max pressure today day
19 0C 12 max pressure today hour
20 33 51 max pressure today minute
21 27 max pressure today 1015.7 mbar
22 AD
23 0E 14 min pressure today year
24 05 5 min pressure today month
25 0D 13 min pressure today day
26 00 0 min pressure today hour
27 06 6 min pressure today minute
28 27 min pressure today 1014.1 mbar
29 9D
30 0E 14 max pressure month year
31 05 5 max pressure month month
32 04 4 max pressure month day
33 01 1 max pressure month hour
34 15 21 max pressure month minute
35 27 max pressure month 1022.5 mbar
36 F1
37 0E 14 min pressure month year
38 05 5 min pressure month month
39 0B 11 min pressure month day
40 00 0 min pressure month hour
41 06 6 min pressure month minute
42 27 min pressure month 1007.8 mbar
43 5E
44 06 checksum
45 EC
0 41 ACK
1 43
2 4B
3 D6 packet type
4 00 channel number
5 8B
examples:
41 43 4b d6 00 20
message: forecast
byte hex dec description decoded value
0 DB
1 20
2 0F 15 year
3 07 7 month
4 09 9 day
5 12 18 hour
6 23 35 minute
7 00
8 FA
9 79
10 FC
11 40
12 01
13 4A
14 06
15 17
16 14
17 23
18 06
19 01
20 00
21 00
22 01
23 01
24 01
25 00
26 00
27 00
28 FE
29 00
30 05 checksum
31 A5
0 41 ACK
1 43
2 4B
3 D6 packet type
4 00 channel number
5 20
examples:
41 43 4b db 00 20
message: temperature/humidity ranges
byte hex dec description decoded value
0 DC packet type
1 3E 62 packet length
2 0E 14 year
3 05 5 month
4 0D 13 day
5 0E 14 hour
6 30 48 minute
7 00 0 channel number
8 0E 14 max temp today year
9 05 5 month
10 0D 13 day
11 00 0 hour
12 00 0 minute
13 00 max temp today 20.8 C
14 D0
15 0E 14 min temp today year
16 05 5 month
17 0D 13 day
18 0B 11 hour
19 34 52 minute
20 00 min temp today 19.0 C
21 BE
22 0E 14 max temp month year
23 05 5 month
24 0A 10 day
25 0D 13 hour
26 19 25 minute
27 00 max temp month 21.4 C
28 D6
29 0E 14 min temp month year
30 05 5 month
31 04 4 day
32 03 3 hour
33 2A 42 minute
34 00 min temp month 18.1 C
35 B5
36 0E 14 max humidity today year
37 05 5 month
38 0D 13 day
39 05 5 hour
40 04 4 minute
41 45 max humidity today 69 %
42 0E 14 min numidity today year
43 05 5 month
44 0D 13 day
45 0B 11 hour
46 32 50 minute
47 41 min humidity today 65 %
48 0E 14 max humidity month year
49 05 5 month
50 0C 12 day
51 13 19 hour
52 32 50 minute
53 46 max humidity month 70 %
54 0E 14 min humidity month year
55 05 5 month
56 04 4 day
57 14 20 hour
58 0E 14 minute
59 39 min humidity month 57 %
60 07 checksum
61 BF
0 41 ACK
1 43
2 4B
3 DC packet type
4 00 0 channel number
5 8B
examples:
41 43 4b dc 01 20
41 43 4b dc 00 20
41 43 4b dc 01 16
41 43 4b dc 00 16
"""
from __future__ import with_statement
import syslog
import time
import usb
import weewx.drivers
import weewx.wxformulas
from weeutil.weeutil import timestamp_to_string
DRIVER_NAME = 'WMR300'
DRIVER_VERSION = '0.16b' # 0.12/3.6.2 plus patches
DEBUG_COMM = 0
DEBUG_PACKET = 0
DEBUG_COUNTS = 0
DEBUG_DECODE = 0
DEBUG_HISTORY = 0
DEBUG_RAIN = 1
def loader(config_dict, _):
return WMR300Driver(**config_dict[DRIVER_NAME])
def confeditor_loader():
return WMR300ConfEditor()
def logmsg(level, msg):
syslog.syslog(level, 'wmr300: %s' % msg)
def logdbg(msg):
logmsg(syslog.LOG_DEBUG, msg)
def loginf(msg):
logmsg(syslog.LOG_INFO, msg)
def logerr(msg):
logmsg(syslog.LOG_ERR, msg)
def logcrt(msg):
logmsg(syslog.LOG_CRIT, msg)
def _fmt_bytes(data):
return ' '.join(['%02x' % x for x in data])
def _lo(x):
return x - 256 * (x >> 8)
def _hi(x):
return x >> 8
class WMR300Driver(weewx.drivers.AbstractDevice):
"""weewx driver that communicates with a WMR300 weather station."""
# the default map is for the wview schema
DEFAULT_MAP = {
'pressure': 'pressure',
'barometer': 'barometer',
'windSpeed': 'wind_avg',
'windDir': 'wind_dir',
'windGust': 'wind_gust',
'windGustDir': 'wind_gust_dir',
'inTemp': 'temperature_0',
'outTemp': 'temperature_1',
'extraTemp1': 'temperature_2',
'extraTemp2': 'temperature_3',
'extraTemp3': 'temperature_4',
'extraTemp4': 'temperature_5',
'extraTemp5': 'temperature_6',
'extraTemp6': 'temperature_7',
'extraTemp7': 'temperature_8',
'inHumidity': 'humidity_0',
'outHumidity': 'humidity_1',
'extraHumid1': 'humidity_2',
'extraHumid2': 'humidity_3',
'extraHumid3': 'humidity_4',
'extraHumid4': 'humidity_5',
'extraHumid5': 'humidity_6',
'extraHumid6': 'humidity_7',
'extraHumid7': 'humidity_8',
'dewpoint': 'dewpoint_1',
'extraDewpoint1': 'dewpoint_2',
'extraDewpoint2': 'dewpoint_3',
'extraDewpoint3': 'dewpoint_4',
'extraDewpoint4': 'dewpoint_5',
'extraDewpoint5': 'dewpoint_6',
'extraDewpoint6': 'dewpoint_7',
'extraDewpoint7': 'dewpoint_8',
'heatindex': 'heatindex_1',
'extraHeatindex1': 'heatindex_2',
'extraHeatindex2': 'heatindex_3',
'extraHeatindex3': 'heatindex_4',
'extraHeatindex4': 'heatindex_5',
'extraHeatindex5': 'heatindex_6',
'extraHeatindex6': 'heatindex_7',
'extraHeatindex7': 'heatindex_8',
'windchill': 'windchill',
'rainRate': 'rain_rate'
}
def __init__(self, **stn_dict):
loginf('driver version is %s' % DRIVER_VERSION)
self.model = stn_dict.get('model', 'WMR300')
self.sensor_map = stn_dict.get('sensor_map', self.DEFAULT_MAP)
self.heartbeat = 20 # how often to send a6 messages, in seconds
self.history_retry = 60 # how often to retry history, in seconds
global DEBUG_COMM
DEBUG_COMM = int(stn_dict.get('debug_comm', DEBUG_COMM))
global DEBUG_PACKET
DEBUG_PACKET = int(stn_dict.get('debug_packet', DEBUG_PACKET))
global DEBUG_COUNTS
DEBUG_COUNTS = int(stn_dict.get('debug_counts', DEBUG_COUNTS))
global DEBUG_DECODE
DEBUG_DECODE = int(stn_dict.get('debug_decode', DEBUG_DECODE))
global DEBUG_HISTORY
DEBUG_HISTORY = int(stn_dict.get('debug_history', DEBUG_HISTORY))
global DEBUG_RAIN
DEBUG_RAIN = int(stn_dict.get('debug_rain', DEBUG_RAIN))
self.last_rain = None
self.last_a6 = 0
self.last_65 = 0
self.last_7x = 0
self.last_record = 0
self.station = Station()
self.station.open()
def closePort(self):
self.station.close()
self.station = None
@property
def hardware_name(self):
return self.model
def genLoopPackets(self):
while True:
try:
buf = self.station.read()
if buf:
pkt = Station.decode(buf)
if buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# send ack for most data packets
# FIXME: what is last number in the ACK?
# observed: 0x00 0x20 0xc1 0xc7 0xa0 0x99
cmd = [0x41, 0x43, 0x4b, buf[0], buf[7], _lo(self.last_record)]
self.station.write(cmd)
# we only care about packets with loop data
if pkt['packet_type'] in [0xd3, 0xd4, 0xd5, 0xd6]:
packet = self.convert_loop(pkt)
yield packet
if time.time() - self.last_a6 > self.heartbeat:
logdbg("request station status: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_a6 = time.time()
if self.last_7x == 0:
# FIXME: what are the 72/73 messages?
# observed:
# 73 e5 0a 26 0e c1
# 73 e5 0a 26 88 8b
# 72 a9 c1 60 52 00
# cmd = [0x72, 0xa9, 0xc1, 0x60, 0x52, 0x00]
cmd = [0x73, 0xe5, 0x0a, 0x26, 0x88, 0x8b]
# cmd = [0x73, 0xe5, 0x0a, 0x26, 0x0e, 0xc1]
self.station.write(cmd)
self.last_7x = time.time()
except usb.USBError, e:
errmsg = repr(e)
if not ('No data available' in errmsg or 'No error' in errmsg):
logerr("usb failure: %s" % e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
def genStartupRecords(self, since_ts):
loginf("reading records since %s" % timestamp_to_string(since_ts))
hbuf = None
last_ts = None
cnt = 0
while True:
try:
buf = self.station.read()
if buf:
if buf[0] == 0xd2:
hbuf = buf
buf = None
elif buf[0] == 0x7f and hbuf is not None:
# FIXME: need better indicator of second half history
buf = hbuf + buf
hbuf = None
if buf and buf[0] == 0xd2:
self.last_record = Station.get_record_index(buf)
ts = Station._extract_ts(buf[4:9])
if ts is not None and ts > since_ts:
keep = True if last_ts is not None else False
pkt = Station.decode(buf)
packet = self.convert_historical(pkt, ts, last_ts)
last_ts = ts
if keep:
logdbg("historical record: %s" % packet)
cnt += 1
yield packet
if buf and buf[0] == 0x57:
idx = Station.get_latest_index(buf)
msg = "count=%s last_index=%s latest_index=%s" % (
cnt, self.last_record, idx)
if self.last_record + 1 >= idx:
loginf("catchup complete: %s" % msg)
break
loginf("catchup in progress: %s" % msg)
if buf and buf[0] == 0x41 and buf[3] == 0x65:
nxtrec = Station.get_next_index(self.last_record)
logdbg("request records starting with %s" % nxtrec)
cmd = [0xcd, 0x18, 0x30, 0x62, _hi(nxtrec), _lo(nxtrec)]
self.station.write(cmd)
if time.time() - self.last_a6 > self.heartbeat:
logdbg("request station status: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_a6 = time.time()
if self.last_7x == 0:
# FIXME: what does 72/73 do?
cmd = [0x73, 0xe5, 0x0a, 0x26, 0x88, 0x8b]
self.station.write(cmd)
self.last_7x = time.time()
if time.time() - self.last_65 > self.history_retry:
logdbg("initiate record request: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0x65, 0x19, 0xe5, 0x04, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_65 = time.time()
except usb.USBError, e:
errmsg = repr(e)
if not ('No data available' in errmsg or 'No error' in errmsg):
logerr("usb failure: %s" % e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
def convert(self, pkt, ts):
# if debugging packets, log everything we got
if DEBUG_PACKET:
logdbg("raw packet: %s" % pkt)
# timestamp and unit system are the same no matter what
p = {'dateTime': ts, 'usUnits': weewx.METRICWX}
# map hardware names to the requested database schema names
for label in self.sensor_map:
if self.sensor_map[label] in pkt:
p[label] = pkt[self.sensor_map[label]]
# single variable to track last_rain assumes that any historical reads
# will happen before any loop reads, and no historical reads will
# happen after any loop reads. otherwise double-counting of rain
# events could happen.
if 'rain_total' in pkt:
p['rain'] = self.calculate_rain(pkt['rain_total'], self.last_rain)
if DEBUG_RAIN and pkt['rain_total'] != self.last_rain:
logdbg("rain=%s rain_total=%s last_rain=%s" %
(p['rain'], pkt['rain_total'], self.last_rain))
self.last_rain = pkt['rain_total']
if DEBUG_PACKET:
logdbg("converted packet: %s" % p)
return p
def convert_historical(self, pkt, ts, last_ts):
p = self.convert(pkt, ts)
if last_ts is not None:
p['interval'] = ts - last_ts
return p
def convert_loop(self, pkt):
p = self.convert(pkt, int(time.time() + 0.5))
return p
@staticmethod
def calculate_rain(newtotal, oldtotal):
"""Calculate the rain difference given two cumulative measurements."""
if newtotal is not None and oldtotal is not None:
if newtotal >= oldtotal:
delta = newtotal - oldtotal
else:
loginf("rain counter decrement detected: new=%s old=%s" %
(newtotal, oldtotal))
delta = None
else:
loginf("possible missed rain event: new=%s old=%s" %
(newtotal, oldtotal))
delta = None
return delta
class WMR300Error(weewx.WeeWxIOError):
"""map station errors to weewx io errors"""
class WrongLength(WMR300Error):
"""bad packet length"""
class BadChecksum(WMR300Error):
"""bogus checksum"""
class Station(object):
# these identify the weather station on the USB
VENDOR_ID = 0x0FDE
PRODUCT_ID = 0xCA08
MESSAGE_LENGTH = 64
EP_IN = 0x81
EP_OUT = 0x01
MAX_RECORDS = 50000 # FIXME: what is maximum number of records?
def __init__(self, vend_id=VENDOR_ID, prod_id=PRODUCT_ID):
self.vendor_id = vend_id
self.product_id = prod_id
self.handle = None
self.timeout = 100
self.interface = 0
self.recv_counts = dict()
self.send_counts = dict()
def __enter__(self):
self.open()
return self
def __exit__(self, _, value, traceback): # @UnusedVariable
self.close()
def open(self):
dev = self._find_dev(self.vendor_id, self.product_id)
if not dev:
raise WMR300Error("Unable to find station on USB: "
"cannot find device with "
"VendorID=0x%04x ProductID=0x%04x" %
(self.vendor_id, self.product_id))
self.handle = dev.open()
if not self.handle:
raise WMR300Error('Open USB device failed')
self.handle.reset()
# for HID devices on linux, be sure kernel does not claim the interface
try:
self.handle.detachKernelDriver(self.interface)
except (AttributeError, usb.USBError):
pass
# attempt to claim the interface
try:
self.handle.claimInterface(self.interface)
except usb.USBError, e:
self.close()
raise WMR300Error("Unable to claim interface %s: %s" %
(self.interface, e))
def close(self):
if self.handle is not None:
try:
self.handle.releaseInterface()
except (ValueError, usb.USBError), e:
loginf("Release interface failed: %s" % e)
self.handle = None
def reset(self):
self.handle.reset()
def read(self, count=True):
buf = None
try:
buf = self.handle.interruptRead(
Station.EP_IN, self.MESSAGE_LENGTH, self.timeout)
if DEBUG_COMM:
logdbg("read: %s" % _fmt_bytes(buf))
if DEBUG_COUNTS and count:
self.update_count(buf, self.recv_counts)
except usb.USBError, e:
errmsg = repr(e)
if not ('No data available' in errmsg or 'No error' in errmsg):
raise
return buf
def write(self, buf):
if DEBUG_COMM:
logdbg("write: %s" % _fmt_bytes(buf))
# pad with zeros up to the standard message length
while len(buf) < self.MESSAGE_LENGTH:
buf.append(0x00)
sent = self.handle.interruptWrite(Station.EP_OUT, buf, self.timeout)
if DEBUG_COUNTS:
self.update_count(buf, self.send_counts)
return sent
# keep track of the message types for debugging purposes
@staticmethod
def update_count(buf, count_dict):
label = 'empty'
if buf and len(buf) > 0:
if buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# message type and channel for data packets
label = '%02x:%d' % (buf[0], buf[7])
elif (buf[0] in [0x41] and
buf[3] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]):
# message type and channel for data ack packets
label = '%02x:%02x:%d' % (buf[0], buf[3], buf[4])
else:
# otherwise just track the message type
label = '%02x' % buf[0]
if label in count_dict:
count_dict[label] += 1
else:
count_dict[label] = 1
cstr = []
for k in sorted(count_dict):
cstr.append('%s: %s' % (k, count_dict[k]))
logdbg('counts: %s' % ''.join(cstr))
@staticmethod
def _find_dev(vendor_id, product_id):
"""Find the first device with vendor and product ID on the USB."""
for bus in usb.busses():
for dev in bus.devices:
if dev.idVendor == vendor_id and dev.idProduct == product_id:
logdbg('Found station at bus=%s device=%s' %
(bus.dirname, dev.filename))
return dev
return None
@staticmethod
def _verify_length(label, length, buf):
if buf[1] != length:
raise WrongLength("%s: wrong length: expected %02x, got %02x" %
(label, length, buf[1]))
@staticmethod
def _verify_checksum(label, buf, msb_first=True):
"""Calculate and compare checksum"""
try:
cs1 = Station._calc_checksum(buf)
cs2 = Station._extract_checksum(buf, msb_first)
if cs1 != cs2:
raise BadChecksum("%s: bad checksum: %04x != %04x" %
(label, cs1, cs2))
except IndexError, e:
raise BadChecksum("%s: not enough bytes for checksum: %s" %
(label, e))
@staticmethod
def _calc_checksum(buf):
cs = 0
for x in buf[:-2]:
cs += x
return cs
@staticmethod
def _extract_checksum(buf, msb_first):
if msb_first:
return (buf[-2] << 8) | buf[-1]
return (buf[-1] << 8) | buf[-2]
@staticmethod
def _extract_ts(buf):
if buf[0] == 0xee and buf[1] == 0xee and buf[2] == 0xee:
# year, month, and day are 0xee when timestamp is unset
return None
try:
year = int(buf[0]) + 2000
month = int(buf[1])
day = int(buf[2])
hour = int(buf[3])
minute = int(buf[4])
return time.mktime((year, month, day, hour, minute, 0, -1, -1, -1))
except IndexError:
raise WMR300Error("buffer too short for timestamp")
except (OverflowError, ValueError), e:
raise WMR300Error(
"cannot create timestamp from y:%s m:%s d:%s H:%s M:%s: %s" %
(buf[0], buf[1], buf[2], buf[3], buf[4], e))
@staticmethod
def _extract_signed(hi, lo, m):
if hi == 0x7f:
return None
s = 0
if hi & 0xf0 == 0xf0:
s = 0x10000
return ((hi << 8) + lo - s) * m
@staticmethod
def _extract_value(buf, m):
if buf[0] == 0x7f:
return None
if len(buf) == 2:
return ((buf[0] << 8) + buf[1]) * m
return buf[0] * m
@staticmethod
def get_latest_index(buf):
# get the index of the most recent history record
if buf[0] != 0x57:
return None
return (buf[17] << 8) + buf[18]
@staticmethod
def get_next_index(n):
# return the index of the record after indicated index
if n == 0:
return 0x20
if n + 1 > Station.MAX_RECORDS:
return 0x20 # FIXME: verify the wraparound
return n + 1
@staticmethod
def get_record_index(buf):
# extract the index from the history record
if buf[0] != 0xd2:
return None
return (buf[2] << 8) + buf[3]
@staticmethod
def decode(buf):
try:
pkt = getattr(Station, '_decode_%02x' % buf[0])(buf)
if DEBUG_DECODE:
logdbg('decode: %s %s' % (_fmt_bytes(buf), pkt))
return pkt
except IndexError, e:
raise WMR300Error("cannot decode buffer: %s" % e)
except AttributeError:
raise WMR300Error("unknown packet type %02x: %s" %
(buf[0], _fmt_bytes(buf)))
@staticmethod
def _decode_57(buf):
"""57 packet contains station information"""
pkt = dict()
pkt['packet_type'] = 0x57
pkt['station_type'] = ''.join("%s" % chr(x) for x in buf[0:6])
pkt['station_model'] = ''.join("%s" % chr(x) for x in buf[7:11])
if DEBUG_HISTORY:
nrec = (buf[17] << 8) + buf[18]
logdbg("history records: %s" % nrec)
return pkt
@staticmethod
def _decode_41(_):
"""41 43 4b is ACK"""
pkt = dict()
pkt['packet_type'] = 0x41
return pkt
@staticmethod
def _decode_d2(buf):
"""D2 packet contains history data"""
Station._verify_length("D2", 0x80, buf)
Station._verify_checksum("D2", buf[:0x80], msb_first=False)
pkt = dict()
pkt['packet_type'] = 0xd2
pkt['ts'] = Station._extract_ts(buf[4:9])
for i in range(0, 9):
pkt['temperature_%d' % i] = Station._extract_signed(
buf[9 + 2 * i], buf[10 + 2 * i], 0.1) # C
pkt['humidity_%d' % i] = Station._extract_value(
buf[27 + i:28 + i], 1.0) # %
for i in range(1, 9):
pkt['dewpoint_%d' % i] = Station._extract_signed(
buf[36 + 2 * i], buf[37 + 2 * i], 0.1) # C
pkt['heatindex_%d' % i] = Station._extract_signed(
buf[52 + 2 * i], buf[53 + 2 * i], 0.1) # C
pkt['windchill'] = Station._extract_signed(buf[68], buf[69], 0.1) # C
pkt['wind_gust'] = Station._extract_value(buf[72:74], 0.1) # m/s
pkt['wind_avg'] = Station._extract_value(buf[74:76], 0.1) # m/s
pkt['wind_gust_dir'] = Station._extract_value(buf[76:78], 1.0) # degree
pkt['wind_dir'] = Station._extract_value(buf[78:80], 1.0) # degree
pkt['forecast'] = Station._extract_value(buf[80:81], 1.0)
pkt['rain_hour'] = Station._extract_value(buf[83:85], 0.254) # mm
pkt['rain_total'] = Station._extract_value(buf[86:88], 0.254) # mm
pkt['rain_start_dateTime'] = Station._extract_ts(buf[88:93])
pkt['rain_rate'] = Station._extract_value(buf[93:95], 0.254) # mm/hour
pkt['barometer'] = Station._extract_value(buf[95:97], 0.1) # mbar
pkt['pressure_trend'] = Station._extract_value(buf[97:98], 1.0)
return pkt
@staticmethod
def _decode_d3(buf):
"""D3 packet contains temperature/humidity data"""
Station._verify_length("D3", 0x3d, buf)
Station._verify_checksum("D3", buf[:0x3d])
pkt = dict()
pkt['packet_type'] = 0xd3
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['temperature_%d' % pkt['channel']] = Station._extract_signed(
buf[8], buf[9], 0.1) # C
pkt['humidity_%d' % pkt['channel']] = Station._extract_value(
buf[10:11], 1.0) # %
pkt['dewpoint_%d' % pkt['channel']] = Station._extract_signed(
buf[11], buf[12], 0.1) # C
return pkt
@staticmethod
def _decode_d4(buf):
"""D4 packet contains wind data"""
Station._verify_length("D4", 0x36, buf)
Station._verify_checksum("D4", buf[:0x36])
pkt = dict()
pkt['packet_type'] = 0xd4
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['wind_gust'] = Station._extract_value(buf[8:10], 0.1) # m/s
pkt['wind_gust_dir'] = Station._extract_value(buf[10:12], 1.0) # degree
pkt['wind_avg'] = Station._extract_value(buf[12:14], 0.1) # m/s
pkt['wind_dir'] = Station._extract_value(buf[14:16], 1.0) # degree
return pkt
@staticmethod
def _decode_d5(buf):
"""D5 packet contains rain data"""
Station._verify_length("D5", 0x28, buf)
Station._verify_checksum("D5", buf[:0x28])
pkt = dict()
pkt['packet_type'] = 0xd5
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['rain_hour'] = Station._extract_value(buf[9:11], 0.254) # mm
pkt['rain_24_hour'] = Station._extract_value(buf[12:14], 0.254) # mm
pkt['rain_total'] = Station._extract_value(buf[15:17], 0.254) # mm
pkt['rain_rate'] = Station._extract_value(buf[17:19], 0.254) # mm/hour
pkt['rain_start_dateTime'] = Station._extract_ts(buf[19:24])
return pkt
@staticmethod
def _decode_d6(buf):
"""D6 packet contains pressure data"""
Station._verify_length("D6", 0x2e, buf)
Station._verify_checksum("D6", buf[:0x2e])
pkt = dict()
pkt['packet_type'] = 0xd6
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['pressure'] = Station._extract_value(buf[8:10], 0.1) # mbar
pkt['barometer'] = Station._extract_value(buf[10:12], 0.1) # mbar
pkt['altitude'] = Station._extract_value(buf[12:14], 1.0) # meter
return pkt
@staticmethod
def _decode_dc(buf):
"""DC packet contains temperature/humidity range data"""
Station._verify_length("DC", 0x3e, buf)
Station._verify_checksum("DC", buf[:0x3e])
pkt = dict()
pkt['packet_type'] = 0xdc
pkt['ts'] = Station._extract_ts(buf[2:7])
return pkt
@staticmethod
def _decode_db(buf):
"""DB packet is forecast"""
Station._verify_length("DB", 0x20, buf)
Station._verify_checksum("DB", buf[:0x20])
pkt = dict()
pkt['packet_type'] = 0xdb
return pkt
class WMR300ConfEditor(weewx.drivers.AbstractConfEditor):
@property
def default_stanza(self):
return """
[WMR300]
# This section is for WMR300 weather stations.
# The station model, e.g., WMR300A
model = WMR300
# The driver to use:
driver = weewx.drivers.wmr300
"""
def modify_config(self, config_dict):
print """
Setting rainRate, windchill, heatindex, and dewpoint calculations to hardware."""
config_dict.setdefault('StdWXCalculate', {})
config_dict['StdWXCalculate'].setdefault('Calculatios', {})
config_dict['StdWXCalculate']['Calculations']['rainRate'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['windchill'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['heatindex'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['dewpoint'] = 'hardware'
# define a main entry point for basic testing of the station without weewx
# engine and service overhead. invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/user/wmr300.py
if __name__ == '__main__':
import optparse
usage = """%prog [options] [--help]"""
syslog.openlog('wmr300', syslog.LOG_PID | syslog.LOG_CONS)
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
parser = optparse.OptionParser(usage=usage)
parser.add_option('--version', dest='version', action='store_true',
help='display driver version')
(options, args) = parser.parse_args()
if options.version:
print "wmr300 driver version %s" % DRIVER_VERSION
exit(0)
stn_dict = {
'debug_comm': 1,
'debug_packet': 0,
'debug_counts': 1,
'debug_decode': 0
}
stn = WMR300Driver(**stn_dict)
for packet in stn.genLoopPackets():
print packet
#!/usr/bin/env python
# Copyright 2015 Matthew Wall
# See the file LICENSE.txt for your rights.
#
# Credits:
# Thanks to Benji for the identification and decoding of 7 packet types
#
# Thanks to Eric G for posting USB captures and providing hardware for testing
# https://groups.google.com/forum/#!topic/weewx-development/5R1ahy2NFsk
#
# Thanks to Zahlii
# https://bsweather.myworkbook.de/category/weather-software/
#
# No thanks to oregon scientific - repeated requests for hardware and/or
# specifications resulted in no response at all.
# TODO: battery level for each sensor
# TODO: signal strength for each sensor
# TODO: altitude
# TODO: archive interval
# FIXME: warn if altitude in pressure packet does not match weewx altitude
# FIXME: figure out unknown bytes in history packet
# FIXME: decode the 0xdb packets
# FIXME: figure out how to automatically reset the rain counter, otherwise
# rain count is not recorded once the counter hits maximum value.
# FIXME: the read/write logic is rather brittle. it appears that communication
# must be initiated with an interrupt write. after that, the station will
# spew data. this implementation starts with a read, which will fail with
# a 'No data available' usb error. that results in an empty buffer (instead
# of an exception popping up the stack) so that a heartbeat write is sent.
# the genLoopPacket and genStartupRecords logic should be refactored to make
# this behiavor explicit.
# FIXME: if the operating system is localized, the check for the string
# 'No data available' will probably fail. we should check for a code instead,
# but it is not clear whether such an element is available in a usb.USBError
# object, or whether it is available across different pyusb versions.
"""Driver for Oregon Scientific WMR300 weather stations.
Sensor data transmission frequencies:
wind: 2.5 to 3 seconds
TH: 10 to 12 seconds
rain: 20 to 24 seconds
The station supports 1 wind, 1 rain, 1 UV, and up to 8 temperature/humidity
sensors.
Sniffing USB traffic shows all communication is interrupt. The endpoint
descriptors for the device show this as well. Response timing is 1.
The station ships with "Weather OS PRO" software for windows. This was used
for the USB sniffing.
Internal observation names use the convention name_with_specifier. These are
mapped to the wview or other schema as needed with a configuration setting.
For example, for the wview schema, wind_speed maps to windSpeed, temperature_0
maps to inTemp, and humidity_1 maps to outHumidity.
Maximum value for rain counter is 40000 mm (10160 in) (0x9c 0x40). The counter
does not wrap; it must be reset when it hits maximum value otherwise rain data
will not be recorded.
Message types -----------------------------------------------------------------
packet types from station:
57 - station type/model; history count
41 - ACK
D2 - history; 128 bytes
D3 - temperature/humidity/dewpoint; 61 bytes
D4 - wind; 54 bytes
D5 - rain; 40 bytes
D6 - pressure; 46 bytes
DB - forecast; 32 bytes
DC - temperature/humidity ranges; 62 bytes
packet types from host:
A6 - heartbeat
41 - ACK
65 - ? each of these is ack-ed by the station
cd - ? start history request? last two bytes are one after most recent read
35 - ? finish history request? last two bytes are latest record count
72 - ?
73 - ?
notes:
WOP sends A6 message every 20 seconds
WOP requests history at startup, then again every 120 minutes
each A6 is followed by a 57 from the station
each data packet D* from the station is followed by an ack packet 41 from host
D2 (history) records are recorded every minute
D6 (pressure) packets seem to come every 15 minutes (900 seconds)
4,5 of 7x match 12,13 of 57
Message field decodings -------------------------------------------------------
Values are stored in 1 to 3 bytes in big endian order. Negative numbers are
stored as Two's Complement (if the first byte starts with F it is a negative
number).
no data:
7f ff
values for channel number:
0 - console sensor
1 - sensor 1
2 - sensor 2
...
8 - sensor 8
values for trend:
0 - steady
1 - rising
2 - falling
bitwise transformation for compass direction:
1000 0000 0000 0000 = NNW
0100 0000 0000 0000 = NW
0010 0000 0000 0000 = WNW
0001 0000 0000 0000 = W
0000 1000 0000 0000 = WSW
0000 0100 0000 0000 = SW
0000 0010 0000 0000 = SSW
0000 0001 0000 0000 = S
0000 0000 1000 0000 = SSE
0000 0000 0100 0000 = SE
0000 0000 0010 0000 = ESE
0000 0000 0001 0000 = E
0000 0000 0000 1000 = ENE
0000 0000 0000 0100 = NE
0000 0000 0000 0010 = NNE
0000 0000 0000 0001 = N
values for forecast:
0x08 - cloudy
0x0c - rainy
0x1e - partly cloudy
0x0e - partly cloudy at night
0x70 - sunny
0x00 - clear night
Message decodings -------------------------------------------------------------
message: ACK
byte hex dec description decoded value
0 41 A acknowledgement ACK
1 43 C
2 4b K
3 73
4 e5
5 0a
6 26
7 0e
8 c1
examples:
41 43 4b 73 e5 0a 26 0e c1
41 43 4b 65 19 e5 04
message: station info
byte hex dec description decoded value
0 57 W station type WMR300
1 4d M
2 52 R
3 33 3
4 30 0
5 30 0
6 2c ,
7 41 A station model A002
8 30 0
9 30 0
10 32 2
11 2c ,
12 0e
13 c1
14 00
15 00
16 2c ,
17 67 lastest history record 26391 (0x67*256 0x17)
18 17
19 2c ,
20 4b
21 2c ,
22 52
23 2c ,
examples:
57 4d 52 33 30 30 2c 41 30 30 32 2c 0e c1 00 00 2c 67 17 2c 4b 2c 52 2c
57 4d 52 33 30 30 2c 41 30 30 32 2c 88 8b 00 00 2c 2f b5 2c 4b 2c 52 2c
57 4d 52 33 30 30 2c 41 30 30 34 2c 0e c1 00 00 2c 7f e0 2c 4b 2c 49 2c
57 4d 52 33 30 30 2c 41 30 30 34 2c 88 8b 00 00 2c 7f e0 2c 4b 2c 49 2c
message: history
byte hex dec description decoded value
0 d2 packet type
1 80 128 packet length
2 31 count 12694
3 96
4 0f 15 year ee if not set
5 08 8 month ee if not set
6 0a 10 day ee if not set
7 06 6 hour
8 02 2 minute
9 00 temperature 0 21.7 C
10 d9
11 00 temperature 1 25.4 C
12 fe
13 7f temperature 2
14 ff
15 7f temperature 3
16 ff
17 7f temperature 4
18 ff
19 7f temperature 5
20 ff
21 7f temperature 6
22 ff
23 7f temperature 7
24 ff
25 7f temperature 8
26 ff (a*256 + b)/10
27 26 humidity 0 38 %
28 49 humidity 1 73 %
29 7f humidity 2
30 7f humidity 3
31 7f humidity 4
32 7f humidity 5
33 7f humidity 6
34 7f humidity 7
35 7f humidity 8
36 00 dewpoint 1 20.0 C
37 c8 (a*256 + b)/10
38 7f dewpoint 2
39 ff
40 7f dewpoint 3
41 ff
42 7f dewpoint 4
43 ff
44 7f dewpoint 5
45 ff
46 7f dewpoint 6
47 ff
48 7f dewpoint 7
49 ff
50 7f dewpoint 8
51 ff
52 7f heat index 1 C
53 fd (a*256 + b)/10
54 7f heat index 2
55 ff
56 7f heat index 3
57 ff
58 7f heat index 4
59 ff
60 7f heat index 5
61 ff
62 7f heat index 6
63 ff
64 7f heat index 7
65 ff
66 7f heat index 8
67 ff
68 7f wind chill C
69 fd (a*256 + b)/10
70 7f ?
71 ff ?
72 00 wind gust speed 0.0 m/s
73 00 (a*256 + b)/10
74 00 wind average speed 0.0 m/s
75 00 (a*256 + b)/10
76 01 wind gust direction 283 degrees
77 1b (a*256 + b)
78 01 wind average direction 283 degrees
78 1b (a*256 + b)
80 30 forecast
81 00 ?
82 00 ?
83 00 hourly rain hundredths_of_inch
84 00 (a*256 + b)
85 00 ?
86 00 accumulated rain hundredths_of_inch
87 03 (a*256 + b)
88 0f accumulated rain start year
89 07 accumulated rain start month
90 09 accumulated rain start day
91 13 accumulated rain start hour
92 09 accumulated rain start minute
93 00 rain rate hundredths_of_inch/hour
94 00 (a*256 + b)
95 26 pressure mbar
96 ab (a*256 + b)/10
97 01 pressure trend
98 7f ?
99 ff ?
100 7f ?
101 ff ?
102 7f ?
103 ff ?
104 7f ?
105 ff ?
106 7f ?
107 7f ?
108 7f ?
109 7f ?
110 7f ?
111 7f ?
112 7f ?
113 7f ?
114 ff ?
115 7f ?
116 ff ?
117 7f ?
118 ff ?
119 00 ?
120 00 ?
121 00 ?
122 00 ?
123 00 ?
124 00 ?
125 00 ?
126 f8 checksum
127 3b
message: temperature/humidity/dewpoint
byte hex dec description decoded value
0 D3 packet type
1 3D 61 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 12 hour
6 14 20 minute
7 01 1 channel number
8 00 temperature 19.5 C
9 C3
10 2D humidity 45 %
11 00 dewpoint 7.0 C
12 46
13 7F heat index? N/A
14 FD
15 00 temperature trend
16 00 humidity trend
17 0E 14 max_dewpoint_last_day year
18 05 5 month
19 09 9 day
20 0A 10 hour
21 24 36 minute
22 00 max_dewpoint_last_day 13.0 C
23 82
24 0E 14 min_dewpoint_last_day year
25 05 5 month
26 09 9 day
27 10 16 hour
28 1F 31 minute
29 00 min_dewpoint_last_day 6.0 C
30 3C
31 0E 14 max_dewpoint_last_month year
32 05 5 month
33 01 1 day
34 0F 15 hour
35 1B 27 minute
36 00 max_dewpoint_last_month 13.0 C
37 82
38 0E 14 min_dewpoint_last_month year
39 05 5 month
40 04 4 day
41 0B 11 hour
42 08 8 minute
43 FF min_dewpoint_last_month -1.0 C
44 F6
45 0E 14 max_heat_index? year
46 05 5 month
47 09 9 day
48 00 0 hour
49 00 0 minute
50 7F max_heat_index? N/A
51 FF
52 0E 14 min_heat_index? year
53 05 5 month
54 01 1 day
55 00 0 hour
56 00 0 minute
57 7F min_heat_index? N/A
58 FF
59 0B checksum
60 63
0 41 ACK
1 43
2 4B
3 D3 packet type
4 01 channel number
5 8B sometimes DF
examples:
41 43 4b d3 01 20
41 43 4b d3 00 20
message: wind
byte hex dec description decoded value
0 D4 packet type
1 36 54 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 18 hour
6 14 20 minute
7 01 1 channel number
8 00 gust speed 1.4 m/s
9 0E
10 00 gust direction 168 degrees
11 A8
12 00 average speed 2.9 m/s
13 1D
14 00 average direction 13 degrees
15 0D
16 00 compass direction 3 N/NNE
17 03
18 7F windchill 32765 N/A
19 FD
20 0E 14 gust today year
21 05 5 month
22 09 9 day
23 10 16 hour
24 3B 59 minute
25 00 gust today 10 m/s
26 64
27 00 gust direction today 39 degree
28 27
29 0E 14 gust this month year
30 05 5 month
31 09 9 day
32 10 16 hour
33 3B 59 minute
34 00 gust this month 10 m/s
35 64
36 00 gust direction this month 39 degree
37 27
38 0E 14 wind chill today year
39 05 5 month
40 09 9 day
41 00 0 hour
42 00 0 minute
43 7F windchill today N/A
44 FF
45 0E 14 windchill this month year
46 05 5 month
47 03 3 day
48 09 9 hour
49 04 4 minute
50 00 windchill this month 2.9 C
51 1D
52 07 checksum
53 6A
0 41 ACK
1 43
2 4B
3 D4 packet type
4 01 channel number
5 8B
examples:
41 43 4b d4 01 20
41 43 4b d4 01 16
message: rain
byte hex dec description decoded value
0 D5 packet type
1 28 40 packet length
2 0E 14 year
3 05 5 month
4 09 9 day
5 12 18 hour
6 15 21 minute
7 01 1 channel number
8 00
9 00 rainfall this hour 0 inch
10 00
11 00
12 00 rainfall last 24 hours 0.12 inch
13 0C 12
14 00
15 00 rainfall accumulated 1.61 inch
16 A1 161
17 00 rainfall rate 0 inch/hr
18 00
19 0E 14 accumulated start year
20 04 4 month
21 1D 29 day
22 12 18 hour
23 00 0 minute
24 0E 14 max rate last 24 hours year
25 05 5 month
26 09 9 day
27 01 1 hour
28 0C 12 minute
29 00 0 max rate last 24 hours 0.11 inch/hr ((0x00<<8)+0x0b)/100.0
30 0B 11
31 0E 14 max rate last month year
32 05 5 month
33 02 2 day
34 04 4 hour
35 0C 12 minute
36 00 0 max rate last month 1.46 inch/hr ((0x00<<8)+0x92)/100.0
37 92 146
38 03 checksum 794 = (0x03<<8) + 0x1a
39 1A
0 41 ACK
1 43
2 4B
3 D5 packet type
4 01 channel number
5 8B
examples:
41 43 4b d5 01 20
41 43 4b d5 01 16
message: pressure
byte hex dec description decoded value
0 D6 packet type
1 2E 46 packet length
2 0E 14 year
3 05 5 month
4 0D 13 day
5 0E 14 hour
6 30 48 minute
7 00 1 channel number
8 26 station pressure 981.7 mbar ((0x26<<8)+0x59)/10.0
9 59
10 27 sea level pressure 1015.3 mbar ((0x27<<8)+0xa9)/10.0
11 A9
12 01 altitude meter 300 m (0x01<<8)+0x2c
13 2C
14 03 ?
15 00
16 0E 14 max pressure today year
17 05 5 max pressure today month
18 0D 13 max pressure today day
19 0C 12 max pressure today hour
20 33 51 max pressure today minute
21 27 max pressure today 1015.7 mbar
22 AD
23 0E 14 min pressure today year
24 05 5 min pressure today month
25 0D 13 min pressure today day
26 00 0 min pressure today hour
27 06 6 min pressure today minute
28 27 min pressure today 1014.1 mbar
29 9D
30 0E 14 max pressure month year
31 05 5 max pressure month month
32 04 4 max pressure month day
33 01 1 max pressure month hour
34 15 21 max pressure month minute
35 27 max pressure month 1022.5 mbar
36 F1
37 0E 14 min pressure month year
38 05 5 min pressure month month
39 0B 11 min pressure month day
40 00 0 min pressure month hour
41 06 6 min pressure month minute
42 27 min pressure month 1007.8 mbar
43 5E
44 06 checksum
45 EC
0 41 ACK
1 43
2 4B
3 D6 packet type
4 00 channel number
5 8B
examples:
41 43 4b d6 00 20
message: forecast
byte hex dec description decoded value
0 DB
1 20
2 0F 15 year
3 07 7 month
4 09 9 day
5 12 18 hour
6 23 35 minute
7 00
8 FA
9 79
10 FC
11 40
12 01
13 4A
14 06
15 17
16 14
17 23
18 06
19 01
20 00
21 00
22 01
23 01
24 01
25 00
26 00
27 00
28 FE
29 00
30 05 checksum
31 A5
0 41 ACK
1 43
2 4B
3 D6 packet type
4 00 channel number
5 20
examples:
41 43 4b db 00 20
message: temperature/humidity ranges
byte hex dec description decoded value
0 DC packet type
1 3E 62 packet length
2 0E 14 year
3 05 5 month
4 0D 13 day
5 0E 14 hour
6 30 48 minute
7 00 0 channel number
8 0E 14 max temp today year
9 05 5 month
10 0D 13 day
11 00 0 hour
12 00 0 minute
13 00 max temp today 20.8 C
14 D0
15 0E 14 min temp today year
16 05 5 month
17 0D 13 day
18 0B 11 hour
19 34 52 minute
20 00 min temp today 19.0 C
21 BE
22 0E 14 max temp month year
23 05 5 month
24 0A 10 day
25 0D 13 hour
26 19 25 minute
27 00 max temp month 21.4 C
28 D6
29 0E 14 min temp month year
30 05 5 month
31 04 4 day
32 03 3 hour
33 2A 42 minute
34 00 min temp month 18.1 C
35 B5
36 0E 14 max humidity today year
37 05 5 month
38 0D 13 day
39 05 5 hour
40 04 4 minute
41 45 max humidity today 69 %
42 0E 14 min numidity today year
43 05 5 month
44 0D 13 day
45 0B 11 hour
46 32 50 minute
47 41 min humidity today 65 %
48 0E 14 max humidity month year
49 05 5 month
50 0C 12 day
51 13 19 hour
52 32 50 minute
53 46 max humidity month 70 %
54 0E 14 min humidity month year
55 05 5 month
56 04 4 day
57 14 20 hour
58 0E 14 minute
59 39 min humidity month 57 %
60 07 checksum
61 BF
0 41 ACK
1 43
2 4B
3 DC packet type
4 00 0 channel number
5 8B
examples:
41 43 4b dc 01 20
41 43 4b dc 00 20
41 43 4b dc 01 16
41 43 4b dc 00 16
"""
from __future__ import with_statement
import syslog
import time
import usb
import weewx.drivers
import weewx.wxformulas
from weeutil.weeutil import timestamp_to_string
DRIVER_NAME = 'WMR300'
DRIVER_VERSION = '0.16c' # 0.15 plus diagnostics
DEBUG_COMM = 0
DEBUG_PACKET = 0
DEBUG_COUNTS = 0
DEBUG_DECODE = 0
DEBUG_HISTORY = 0
DEBUG_RAIN = 1
def loader(config_dict, _):
return WMR300Driver(**config_dict[DRIVER_NAME])
def confeditor_loader():
return WMR300ConfEditor()
def logmsg(level, msg):
syslog.syslog(level, 'wmr300: %s' % msg)
def logdbg(msg):
logmsg(syslog.LOG_DEBUG, msg)
def loginf(msg):
logmsg(syslog.LOG_INFO, msg)
def logerr(msg):
logmsg(syslog.LOG_ERR, msg)
def logcrt(msg):
logmsg(syslog.LOG_CRIT, msg)
def _fmt_bytes(data):
return ' '.join(['%02x' % x for x in data])
def _lo(x):
return x - 256 * (x >> 8)
def _hi(x):
return x >> 8
class WMR300Driver(weewx.drivers.AbstractDevice):
"""weewx driver that communicates with a WMR300 weather station."""
# the default map is for the wview schema
DEFAULT_MAP = {
'pressure': 'pressure',
'barometer': 'barometer',
'windSpeed': 'wind_avg',
'windDir': 'wind_dir',
'windGust': 'wind_gust',
'windGustDir': 'wind_gust_dir',
'inTemp': 'temperature_0',
'outTemp': 'temperature_1',
'extraTemp1': 'temperature_2',
'extraTemp2': 'temperature_3',
'extraTemp3': 'temperature_4',
'extraTemp4': 'temperature_5',
'extraTemp5': 'temperature_6',
'extraTemp6': 'temperature_7',
'extraTemp7': 'temperature_8',
'inHumidity': 'humidity_0',
'outHumidity': 'humidity_1',
'extraHumid1': 'humidity_2',
'extraHumid2': 'humidity_3',
'extraHumid3': 'humidity_4',
'extraHumid4': 'humidity_5',
'extraHumid5': 'humidity_6',
'extraHumid6': 'humidity_7',
'extraHumid7': 'humidity_8',
'dewpoint': 'dewpoint_1',
'extraDewpoint1': 'dewpoint_2',
'extraDewpoint2': 'dewpoint_3',
'extraDewpoint3': 'dewpoint_4',
'extraDewpoint4': 'dewpoint_5',
'extraDewpoint5': 'dewpoint_6',
'extraDewpoint6': 'dewpoint_7',
'extraDewpoint7': 'dewpoint_8',
'heatindex': 'heatindex_1',
'extraHeatindex1': 'heatindex_2',
'extraHeatindex2': 'heatindex_3',
'extraHeatindex3': 'heatindex_4',
'extraHeatindex4': 'heatindex_5',
'extraHeatindex5': 'heatindex_6',
'extraHeatindex6': 'heatindex_7',
'extraHeatindex7': 'heatindex_8',
'windchill': 'windchill',
'rainRate': 'rain_rate'
}
def __init__(self, **stn_dict):
loginf('driver version is %s' % DRIVER_VERSION)
loginf('pyusb version is %s' % usb.__version__)
self.model = stn_dict.get('model', 'WMR300')
self.sensor_map = stn_dict.get('sensor_map', self.DEFAULT_MAP)
self.heartbeat = 20 # how often to send a6 messages, in seconds
self.history_retry = 60 # how often to retry history, in seconds
global DEBUG_COMM
DEBUG_COMM = int(stn_dict.get('debug_comm', DEBUG_COMM))
global DEBUG_PACKET
DEBUG_PACKET = int(stn_dict.get('debug_packet', DEBUG_PACKET))
global DEBUG_COUNTS
DEBUG_COUNTS = int(stn_dict.get('debug_counts', DEBUG_COUNTS))
global DEBUG_DECODE
DEBUG_DECODE = int(stn_dict.get('debug_decode', DEBUG_DECODE))
global DEBUG_HISTORY
DEBUG_HISTORY = int(stn_dict.get('debug_history', DEBUG_HISTORY))
global DEBUG_RAIN
DEBUG_RAIN = int(stn_dict.get('debug_rain', DEBUG_RAIN))
self.last_rain = None
self.last_a6 = 0
self.last_65 = 0
self.last_7x = 0
self.last_record = 0
self.station = Station()
self.station.open()
def closePort(self):
self.station.close()
self.station = None
@property
def hardware_name(self):
return self.model
def genLoopPackets(self):
while True:
try:
buf = self.station.read()
if buf:
pkt = Station.decode(buf)
if buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# send ack for most data packets
# FIXME: what is last number in the ACK?
# observed: 0x00 0x20 0xc1 0xc7 0xa0 0x99
cmd = [0x41, 0x43, 0x4b, buf[0], buf[7], _lo(self.last_record)]
self.station.write(cmd)
# we only care about packets with loop data
if pkt['packet_type'] in [0xd3, 0xd4, 0xd5, 0xd6]:
packet = self.convert_loop(pkt)
yield packet
if time.time() - self.last_a6 > self.heartbeat:
logdbg("request station status: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_a6 = time.time()
if self.last_7x == 0:
# FIXME: what are the 72/73 messages?
# observed:
# 73 e5 0a 26 0e c1
# 73 e5 0a 26 88 8b
# 72 a9 c1 60 52 00
# cmd = [0x72, 0xa9, 0xc1, 0x60, 0x52, 0x00]
cmd = [0x73, 0xe5, 0x0a, 0x26, 0x88, 0x8b]
# cmd = [0x73, 0xe5, 0x0a, 0x26, 0x0e, 0xc1]
self.station.write(cmd)
self.last_7x = time.time()
except usb.USBError, e:
loginf("e.errno=%s e.strerror=%s e.message=%s repr=%s" % (e.errno, e.strerror, e.message, repr(e)))
errmsg = repr(e)
if not ('No data available' in errmsg or 'No error' in errmsg):
logerr("usb failure: %s" % e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
def genStartupRecords(self, since_ts):
loginf("reading records since %s" % timestamp_to_string(since_ts))
hbuf = None
last_ts = None
cnt = 0
while True:
try:
buf = self.station.read()
if buf:
if buf[0] == 0xd2:
hbuf = buf
buf = None
elif buf[0] == 0x7f and hbuf is not None:
# FIXME: need better indicator of second half history
buf = hbuf + buf
hbuf = None
if buf and buf[0] == 0xd2:
self.last_record = Station.get_record_index(buf)
ts = Station._extract_ts(buf[4:9])
if ts is not None and ts > since_ts:
keep = True if last_ts is not None else False
pkt = Station.decode(buf)
packet = self.convert_historical(pkt, ts, last_ts)
last_ts = ts
if keep:
logdbg("historical record: %s" % packet)
cnt += 1
yield packet
if buf and buf[0] == 0x57:
idx = Station.get_latest_index(buf)
msg = "count=%s last_index=%s latest_index=%s" % (
cnt, self.last_record, idx)
if self.last_record + 1 >= idx:
loginf("catchup complete: %s" % msg)
break
loginf("catchup in progress: %s" % msg)
if buf and buf[0] == 0x41 and buf[3] == 0x65:
nxtrec = Station.get_next_index(self.last_record)
logdbg("request records starting with %s" % nxtrec)
cmd = [0xcd, 0x18, 0x30, 0x62, _hi(nxtrec), _lo(nxtrec)]
self.station.write(cmd)
if time.time() - self.last_a6 > self.heartbeat:
logdbg("request station status: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_a6 = time.time()
if self.last_7x == 0:
# FIXME: what does 72/73 do?
cmd = [0x73, 0xe5, 0x0a, 0x26, 0x88, 0x8b]
self.station.write(cmd)
self.last_7x = time.time()
if time.time() - self.last_65 > self.history_retry:
logdbg("initiate record request: %s (%02x)" %
(self.last_record, _lo(self.last_record)))
cmd = [0x65, 0x19, 0xe5, 0x04, 0x52, _lo(self.last_record)]
self.station.write(cmd)
self.last_65 = time.time()
except usb.USBError, e:
loginf("e.errno=%s e.strerror=%s e.message=%s repr=%s" % (e.errno, e.strerror, e.message, repr(e)))
errmsg = repr(e)
if not ('No data available' in errmsg or 'No error' in errmsg):
logerr("usb failure: %s" % e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
def convert(self, pkt, ts):
# if debugging packets, log everything we got
if DEBUG_PACKET:
logdbg("raw packet: %s" % pkt)
# timestamp and unit system are the same no matter what
p = {'dateTime': ts, 'usUnits': weewx.METRICWX}
# map hardware names to the requested database schema names
for label in self.sensor_map:
if self.sensor_map[label] in pkt:
p[label] = pkt[self.sensor_map[label]]
# single variable to track last_rain assumes that any historical reads
# will happen before any loop reads, and no historical reads will
# happen after any loop reads. otherwise double-counting of rain
# events could happen.
if 'rain_total' in pkt:
p['rain'] = self.calculate_rain(pkt['rain_total'], self.last_rain)
if DEBUG_RAIN and pkt['rain_total'] != self.last_rain:
logdbg("rain=%s rain_total=%s last_rain=%s" %
(p['rain'], pkt['rain_total'], self.last_rain))
self.last_rain = pkt['rain_total']
if DEBUG_PACKET:
logdbg("converted packet: %s" % p)
return p
def convert_historical(self, pkt, ts, last_ts):
p = self.convert(pkt, ts)
if last_ts is not None:
p['interval'] = ts - last_ts
return p
def convert_loop(self, pkt):
p = self.convert(pkt, int(time.time() + 0.5))
return p
@staticmethod
def calculate_rain(newtotal, oldtotal):
"""Calculate the rain difference given two cumulative measurements."""
if newtotal is not None and oldtotal is not None:
if newtotal >= oldtotal:
delta = newtotal - oldtotal
else:
loginf("rain counter decrement detected: new=%s old=%s" %
(newtotal, oldtotal))
delta = None
else:
loginf("possible missed rain event: new=%s old=%s" %
(newtotal, oldtotal))
delta = None
return delta
class WMR300Error(weewx.WeeWxIOError):
"""map station errors to weewx io errors"""
class WrongLength(WMR300Error):
"""bad packet length"""
class BadChecksum(WMR300Error):
"""bogus checksum"""
class Station(object):
# these identify the weather station on the USB
VENDOR_ID = 0x0FDE
PRODUCT_ID = 0xCA08
MESSAGE_LENGTH = 64
EP_IN = 0x81
EP_OUT = 0x01
MAX_RECORDS = 50000 # FIXME: what is maximum number of records?
def __init__(self, vend_id=VENDOR_ID, prod_id=PRODUCT_ID):
self.vendor_id = vend_id
self.product_id = prod_id
self.handle = None
self.timeout = 100
self.interface = 0
self.recv_counts = dict()
self.send_counts = dict()
def __enter__(self):
self.open()
return self
def __exit__(self, _, value, traceback): # @UnusedVariable
self.close()
def open(self):
dev = self._find_dev(self.vendor_id, self.product_id)
if not dev:
raise WMR300Error("Unable to find station on USB: "
"cannot find device with "
"VendorID=0x%04x ProductID=0x%04x" %
(self.vendor_id, self.product_id))
self.handle = dev.open()
if not self.handle:
raise WMR300Error('Open USB device failed')
self.handle.reset()
# for HID devices on linux, be sure kernel does not claim the interface
try:
self.handle.detachKernelDriver(self.interface)
except (AttributeError, usb.USBError):
pass
# attempt to claim the interface
try:
self.handle.claimInterface(self.interface)
except usb.USBError, e:
self.close()
raise WMR300Error("Unable to claim interface %s: %s" %
(self.interface, e))
def close(self):
if self.handle is not None:
try:
self.handle.releaseInterface()
except (ValueError, usb.USBError), e:
loginf("Release interface failed: %s" % e)
self.handle = None
def reset(self):
self.handle.reset()
def read(self, count=True):
buf = None
try:
buf = self.handle.interruptRead(
Station.EP_IN, self.MESSAGE_LENGTH, self.timeout)
if DEBUG_COMM:
logdbg("read: %s" % _fmt_bytes(buf))
if DEBUG_COUNTS and count:
self.update_count(buf, self.recv_counts)
except usb.USBError, e:
loginf("e.errno=%s e.strerror=%s e.message=%s repr=%s" % (e.errno, e.strerror, e.message, repr(e)))
errmsg = repr(e)
if not ('No data available' in errmsg or 'No error' in errmsg):
raise
return buf
def write(self, buf):
if DEBUG_COMM:
logdbg("write: %s" % _fmt_bytes(buf))
# pad with zeros up to the standard message length
while len(buf) < self.MESSAGE_LENGTH:
buf.append(0x00)
sent = self.handle.interruptWrite(Station.EP_OUT, buf, self.timeout)
if DEBUG_COUNTS:
self.update_count(buf, self.send_counts)
return sent
# keep track of the message types for debugging purposes
@staticmethod
def update_count(buf, count_dict):
label = 'empty'
if buf and len(buf) > 0:
if buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# message type and channel for data packets
label = '%02x:%d' % (buf[0], buf[7])
elif (buf[0] in [0x41] and
buf[3] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]):
# message type and channel for data ack packets
label = '%02x:%02x:%d' % (buf[0], buf[3], buf[4])
else:
# otherwise just track the message type
label = '%02x' % buf[0]
if label in count_dict:
count_dict[label] += 1
else:
count_dict[label] = 1
cstr = []
for k in sorted(count_dict):
cstr.append('%s: %s' % (k, count_dict[k]))
logdbg('counts: %s' % ''.join(cstr))
@staticmethod
def _find_dev(vendor_id, product_id):
"""Find the first device with vendor and product ID on the USB."""
for bus in usb.busses():
for dev in bus.devices:
if dev.idVendor == vendor_id and dev.idProduct == product_id:
logdbg('Found station at bus=%s device=%s' %
(bus.dirname, dev.filename))
return dev
return None
@staticmethod
def _verify_length(label, length, buf):
if buf[1] != length:
raise WrongLength("%s: wrong length: expected %02x, got %02x" %
(label, length, buf[1]))
@staticmethod
def _verify_checksum(label, buf, msb_first=True):
"""Calculate and compare checksum"""
try:
cs1 = Station._calc_checksum(buf)
cs2 = Station._extract_checksum(buf, msb_first)
if cs1 != cs2:
raise BadChecksum("%s: bad checksum: %04x != %04x" %
(label, cs1, cs2))
except IndexError, e:
raise BadChecksum("%s: not enough bytes for checksum: %s" %
(label, e))
@staticmethod
def _calc_checksum(buf):
cs = 0
for x in buf[:-2]:
cs += x
return cs
@staticmethod
def _extract_checksum(buf, msb_first):
if msb_first:
return (buf[-2] << 8) | buf[-1]
return (buf[-1] << 8) | buf[-2]
@staticmethod
def _extract_ts(buf):
if buf[0] == 0xee and buf[1] == 0xee and buf[2] == 0xee:
# year, month, and day are 0xee when timestamp is unset
return None
try:
year = int(buf[0]) + 2000
month = int(buf[1])
day = int(buf[2])
hour = int(buf[3])
minute = int(buf[4])
return time.mktime((year, month, day, hour, minute, 0, -1, -1, -1))
except IndexError:
raise WMR300Error("buffer too short for timestamp")
except (OverflowError, ValueError), e:
raise WMR300Error(
"cannot create timestamp from y:%s m:%s d:%s H:%s M:%s: %s" %
(buf[0], buf[1], buf[2], buf[3], buf[4], e))
@staticmethod
def _extract_signed(hi, lo, m):
if hi == 0x7f:
return None
s = 0
if hi & 0xf0 == 0xf0:
s = 0x10000
return ((hi << 8) + lo - s) * m
@staticmethod
def _extract_value(buf, m):
if buf[0] == 0x7f:
return None
if len(buf) == 2:
return ((buf[0] << 8) + buf[1]) * m
return buf[0] * m
@staticmethod
def get_latest_index(buf):
# get the index of the most recent history record
if buf[0] != 0x57:
return None
return (buf[17] << 8) + buf[18]
@staticmethod
def get_next_index(n):
# return the index of the record after indicated index
if n == 0:
return 0x20
if n + 1 > Station.MAX_RECORDS:
return 0x20 # FIXME: verify the wraparound
return n + 1
@staticmethod
def get_record_index(buf):
# extract the index from the history record
if buf[0] != 0xd2:
return None
return (buf[2] << 8) + buf[3]
@staticmethod
def decode(buf):
try:
pkt = getattr(Station, '_decode_%02x' % buf[0])(buf)
if DEBUG_DECODE:
logdbg('decode: %s %s' % (_fmt_bytes(buf), pkt))
return pkt
except IndexError, e:
raise WMR300Error("cannot decode buffer: %s" % e)
except AttributeError:
raise WMR300Error("unknown packet type %02x: %s" %
(buf[0], _fmt_bytes(buf)))
@staticmethod
def _decode_57(buf):
"""57 packet contains station information"""
pkt = dict()
pkt['packet_type'] = 0x57
pkt['station_type'] = ''.join("%s" % chr(x) for x in buf[0:6])
pkt['station_model'] = ''.join("%s" % chr(x) for x in buf[7:11])
if DEBUG_HISTORY:
nrec = (buf[17] << 8) + buf[18]
logdbg("history records: %s" % nrec)
return pkt
@staticmethod
def _decode_41(_):
"""41 43 4b is ACK"""
pkt = dict()
pkt['packet_type'] = 0x41
return pkt
@staticmethod
def _decode_d2(buf):
"""D2 packet contains history data"""
Station._verify_length("D2", 0x80, buf)
Station._verify_checksum("D2", buf[:0x80], msb_first=False)
pkt = dict()
pkt['packet_type'] = 0xd2
pkt['ts'] = Station._extract_ts(buf[4:9])
for i in range(0, 9):
pkt['temperature_%d' % i] = Station._extract_signed(
buf[9 + 2 * i], buf[10 + 2 * i], 0.1) # C
pkt['humidity_%d' % i] = Station._extract_value(
buf[27 + i:28 + i], 1.0) # %
for i in range(1, 9):
pkt['dewpoint_%d' % i] = Station._extract_signed(
buf[36 + 2 * i], buf[37 + 2 * i], 0.1) # C
pkt['heatindex_%d' % i] = Station._extract_signed(
buf[52 + 2 * i], buf[53 + 2 * i], 0.1) # C
pkt['windchill'] = Station._extract_signed(buf[68], buf[69], 0.1) # C
pkt['wind_gust'] = Station._extract_value(buf[72:74], 0.1) # m/s
pkt['wind_avg'] = Station._extract_value(buf[74:76], 0.1) # m/s
pkt['wind_gust_dir'] = Station._extract_value(buf[76:78], 1.0) # degree
pkt['wind_dir'] = Station._extract_value(buf[78:80], 1.0) # degree
pkt['forecast'] = Station._extract_value(buf[80:81], 1.0)
pkt['rain_hour'] = Station._extract_value(buf[83:85], 0.254) # mm
pkt['rain_total'] = Station._extract_value(buf[86:88], 0.254) # mm
pkt['rain_start_dateTime'] = Station._extract_ts(buf[88:93])
pkt['rain_rate'] = Station._extract_value(buf[93:95], 0.254) # mm/hour
pkt['barometer'] = Station._extract_value(buf[95:97], 0.1) # mbar
pkt['pressure_trend'] = Station._extract_value(buf[97:98], 1.0)
return pkt
@staticmethod
def _decode_d3(buf):
"""D3 packet contains temperature/humidity data"""
Station._verify_length("D3", 0x3d, buf)
Station._verify_checksum("D3", buf[:0x3d])
pkt = dict()
pkt['packet_type'] = 0xd3
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['temperature_%d' % pkt['channel']] = Station._extract_signed(
buf[8], buf[9], 0.1) # C
pkt['humidity_%d' % pkt['channel']] = Station._extract_value(
buf[10:11], 1.0) # %
pkt['dewpoint_%d' % pkt['channel']] = Station._extract_signed(
buf[11], buf[12], 0.1) # C
return pkt
@staticmethod
def _decode_d4(buf):
"""D4 packet contains wind data"""
Station._verify_length("D4", 0x36, buf)
Station._verify_checksum("D4", buf[:0x36])
pkt = dict()
pkt['packet_type'] = 0xd4
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['wind_gust'] = Station._extract_value(buf[8:10], 0.1) # m/s
pkt['wind_gust_dir'] = Station._extract_value(buf[10:12], 1.0) # degree
pkt['wind_avg'] = Station._extract_value(buf[12:14], 0.1) # m/s
pkt['wind_dir'] = Station._extract_value(buf[14:16], 1.0) # degree
return pkt
@staticmethod
def _decode_d5(buf):
"""D5 packet contains rain data"""
Station._verify_length("D5", 0x28, buf)
Station._verify_checksum("D5", buf[:0x28])
pkt = dict()
pkt['packet_type'] = 0xd5
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['rain_hour'] = Station._extract_value(buf[9:11], 0.254) # mm
pkt['rain_24_hour'] = Station._extract_value(buf[12:14], 0.254) # mm
pkt['rain_total'] = Station._extract_value(buf[15:17], 0.254) # mm
pkt['rain_rate'] = Station._extract_value(buf[17:19], 0.254) # mm/hour
pkt['rain_start_dateTime'] = Station._extract_ts(buf[19:24])
return pkt
@staticmethod
def _decode_d6(buf):
"""D6 packet contains pressure data"""
Station._verify_length("D6", 0x2e, buf)
Station._verify_checksum("D6", buf[:0x2e])
pkt = dict()
pkt['packet_type'] = 0xd6
pkt['ts'] = Station._extract_ts(buf[2:7])
pkt['channel'] = buf[7]
pkt['pressure'] = Station._extract_value(buf[8:10], 0.1) # mbar
pkt['barometer'] = Station._extract_value(buf[10:12], 0.1) # mbar
pkt['altitude'] = Station._extract_value(buf[12:14], 1.0) # meter
return pkt
@staticmethod
def _decode_dc(buf):
"""DC packet contains temperature/humidity range data"""
Station._verify_length("DC", 0x3e, buf)
Station._verify_checksum("DC", buf[:0x3e])
pkt = dict()
pkt['packet_type'] = 0xdc
pkt['ts'] = Station._extract_ts(buf[2:7])
return pkt
@staticmethod
def _decode_db(buf):
"""DB packet is forecast"""
Station._verify_length("DB", 0x20, buf)
Station._verify_checksum("DB", buf[:0x20])
pkt = dict()
pkt['packet_type'] = 0xdb
return pkt
class WMR300ConfEditor(weewx.drivers.AbstractConfEditor):
@property
def default_stanza(self):
return """
[WMR300]
# This section is for WMR300 weather stations.
# The station model, e.g., WMR300A
model = WMR300
# The driver to use:
driver = weewx.drivers.wmr300
"""
def modify_config(self, config_dict):
print """
Setting rainRate, windchill, heatindex, and dewpoint calculations to hardware."""
config_dict.setdefault('StdWXCalculate', {})
config_dict['StdWXCalculate'].setdefault('Calculatios', {})
config_dict['StdWXCalculate']['Calculations']['rainRate'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['windchill'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['heatindex'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['dewpoint'] = 'hardware'
# define a main entry point for basic testing of the station without weewx
# engine and service overhead. invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/user/wmr300.py
if __name__ == '__main__':
import optparse
usage = """%prog [options] [--help]"""
syslog.openlog('wmr300', syslog.LOG_PID | syslog.LOG_CONS)
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
parser = optparse.OptionParser(usage=usage)
parser.add_option('--version', dest='version', action='store_true',
help='display driver version')
(options, args) = parser.parse_args()
if options.version:
print "wmr300 driver version %s" % DRIVER_VERSION
exit(0)
stn_dict = {
'debug_comm': 1,
'debug_packet': 0,
'debug_counts': 1,
'debug_decode': 0
}
stn = WMR300Driver(**stn_dict)
for packet in stn.genLoopPackets():
print packet