finally got it working with using this driver and adding it to
/usr/share/weewx/weewx/drivers i also added this below into the weewx
config file
[WMR300]
# This section is for WMR300 weather stations.
# The station model, e.g., WMR300A
model = WMR300
# The driver to use:
driver = weewx.drivers.wmr300
history_clear_pct = 5
debug_decode=0
debug_history=0
debug_rain=1
# Set debug_backend to 0 for default lib
# 1 for libusb0
# 2 for libusb1
# 3 for openusb
# 4 for ?
debug_backend=0
On Wednesday, 6 December 2017 19:53:35 UTC, Markus Benedikt Biewer wrote:
>
> I wrote you a private mail, I hope you have received it? I can give you a
> SSH access to my instattion, which might be easier in terms of time
> (although I won't learn sth)... Just let me know....
>
> Salut,
> Markus
>
> --
>
> Am Mittwoch, 6. Dezember 2017 15:23:09 UTC+1 schrieb mwall:
>>
>> scott and markus (and other wmr300 users),
>>
>> as of dec2017, there are two versions of the wmr300 driver:
>>
>> 1) wmr300 version 0.18 (in the weewx distribution)
>>
>> 2) wmr300x version 0.18nolegacy.05.01a
>>
>> of course we would like to bring these into a single driver. the
>> difficult part of that is being able to test all of the permutations.
>>
>> these are the outstanding issues:
>>
>> - how to get the initial startup to work with all libusb versions? the
>> timeouts you see are when the driver cannot get the station to start
>> communication.
>>
>> - need to incorporate the rain counter changes from 0.18nolegacy. that
>> version has code that will automatically reset the rain counter - without
>> that reset, one must manually reset the counter since the hardware does not
>> wrap automatically.
>>
>> - figure out which libusb versions are truly broken. for example, i
>> experienced problems with libusb 1.0.11 that disappeared when i changed to
>> libusb 1.0.20
>>
>> - the pyusb version should not matter; the libusb version seems to be the
>> problem
>>
>> i developed the driver some years ago using a raspberry pi and the legacy
>> libusb. unfortunately the more recent libusb behave differently.
>>
>> i no longer have access to that computer or the weather station, so right
>> now i cannot test any changes.
>>
>> what can you do right now?
>>
>> - for each configuration you try, be sure to record:
>> - the wmr300 driver version
>> - the weewx version
>> - the libusb version
>> - the pyusb version
>>
>> - try using libusb 0.1 instead of libusb 1.x
>>
>> - try using the 'nolegacy' driver with libusb 1.x
>>
>> if we can get a few people with wmr300 stations to do some testing (or
>> give me access to their systems so i can do the testing), then we could
>> consolidate the driver variants.
>>
>> 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
# vim: sw=4 ts=4 expandtab
# 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: figure out battery level for each sensor
# TODO: figure out signal strength for each sensor
# TODO: figure out archive interval
# FIXME: figure out unknown bytes in history packet
# FIXME: decode the 0xdb packets
# FIXME: warn if altitude in pressure packet does not match weewx altitude
"""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 400 in (10160 mm) (40000 = 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 + other status
41 - ACK
D2 - history; 128 bytes
D3 - temperature/humidity/dewpoint/heatindex; 61 bytes
D4 - wind/windchill; 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 - response is 57 (usually)
41 - ACK
65 - do not delete history when it is reported. each of these is ack-ed by the station
b3 - delete history after you give it to me.
cd - start history request. last two bytes are one after most recent read
35 - finish history request. last two bytes are latest record index that was read.
73 - some sort of initialisation packet
72 - ? on rare occasions will be used in place of 73, in both observed cases
the console was already free-running transmitting data
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 (except the one initiating history)
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
---- cameron's extra notes:
Station will free-run transmitting data for about 100s without seeing an ACK.
a6 will always be followed by 91 ca 45 42 but final byte may be 0, 20, 32, 67, 8b, d6, df, or...
0 - when first packet after connection or program startup.
It looks like the final byte is just the last character that was previously written to a static output buffer.
and hence it is meaningless.
41 - ack in - 2 types
41 - ack out - numerous types. - combinations of packet type, channel, last byte.
As for a6, it looks like the last byte is just uncleared residue in the buffer.
b3 59 0a 17 01 <eb> - when you give me history, delete afterwards
- final 2 bytes are probably ignored
response: ACK b3 59 0a 17
65 19 e5 04 52 <b6> - when you give me history, do not delete afterwards
- final 2 bytes are probably ignored
response: ACK 65 19 e5 04
cd 18 30 62 nn mm - start history, starting at record 0xnnmm
response - if preceeded by 65, the ACK is the same string: ACK 65 19 e5 04
- if preceeded by b3 then there is NO ACK.
Initialisation:
out: a6 91 ca 45 52 - note: null 6th byte.
in: 57: WMR300,A004,<b13><b14>\0\0,<history index>,<b21>,<b23>, where numbered bytes are unknown content
then either...
out: 73 e5 0a 26 <b5> <b6> - b5 is set to b13 of pkt 57, b6 <= b14
in: 41 43 4b 73 e5 0a 26 <b8> <b9> - this is the full packet 73 prefixed by "ACK"
or...
out: 72 a9 c1 60 52 - occurs when console is already free-running (but how does WOsP know?)
NO ACK
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). Count values are unsigned.
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
3 - no sensor data
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 command sent from PC
4 e5
5 0a
6 26
7 0e
8 c1
examples:
41 43 4b 73 e5 0a 26 0e c1 last 2 bytes differ
41 43 4b 65 19 e5 04 always same
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 - or 0x34
11 2c ,
12 0e - (3777 dec) or mine always 88 8b (34955)
13 c1
14 00 - always?
15 00
16 2c ,
17 67 next history record 26391 (0x67*256 0x17) (0x7fe0 (32736) is full)
The value at this index has not been used yet.
18 17
19 2c ,
20 4b - usually 'K' (0x4b). occasionally was 0x43 -
or 43 when history is set to delete after downloading.
This is a 1-bit change (1<<3)
NB: Does not return to 4b when latest history record is reset to 0x20 after
history is deleted.
21 2c ,
22 52 - 0x52 (82, 'R'), occasionally 0x49(73, 'G')
0b 0101 0010 (0x52) vs
0b 0100 1001 (0x49) lots of bits flipped!
or 49 this maybe has some link with one or other battery, but does not make sense
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 - index number of this packet
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 (not sure - never saw a falling value)
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 and others
examples:
41 43 4b d3 00 20 - for last byte: 32, 67, 8b, d6
41 43 4b d3 01 20 - for last byte: same + 20, df
for unused temps, last byte always 8b (or is it byte 14 of pkt 57?)
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 variable
examples:
41 43 4b d4 01 20 - last byte: 20, 32, 67, 8b, d6, df
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 - last byte: 20, 32, 67, 8b, d6, df
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 barometric trend (have seen 0,1,2, and 3! )
15 00 only ever observed 0 or 2. is this battery?
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 - last byte: 32, 67, 8b
message: forecast
byte hex dec description decoded value
0 DB
1 20 pkt length
2 0F 15 year
3 07 7 month
4 09 9 day
5 12 18 hour
6 23 35 minute
7 00 below are alternate observations - little overlap
8 FA 0a
9 79 02, 22, 82, a2 - related to console battery!
10 FC 05 - never saw changed
11 40 f9 - never saw changed
12 01 fe - never saw changed
13 4A fc - never saw changed
14 06 variable
15 17 variable
16 14 variable
17 23 variable
18 06 00 to 07 (no 01)
19 01
20 00 00 or 01
21 00 remainder same
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 - last byte: 32, 67, 8b, d6
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 00 20 - last byte: 32, 67, 8b, d6
41 43 4b dc 01 20 - last byte: 20, 32, 67, 8b, d6, df
41 43 4b dc 01 16
41 43 4b dc 00 16
"""
from __future__ import with_statement
import syslog
import time
import usb.core
import usb.util
import weewx.drivers
import weewx.wxformulas
from weeutil.weeutil import timestamp_to_string
# x for experimental - not a model designation.
DRIVER_NAME = 'WMR300'
DRIVER_VERSION = '0.18nolegacy.05.17d'
DEBUG_COMM = 0
DEBUG_PACKET = 0
DEBUG_COUNTS = 0
DEBUG_DECODE = 0
DEBUG_HISTORY = 1
DEBUG_RAIN = 0
DEBUG_BACKEND = 0
def loader(config_dict, _):
return WMR300Driver(**config_dict[DRIVER_NAME])
def confeditor_loader():
return WMR300ConfEditor()
def logmsg(level, msg):
syslog.syslog(level, 'wmr300x: %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 logexception( msg, e):
if e.errno is None:
errnostr="Undefined"
else:
errnostr = str( e.errno )
logerr( "%s exception: %s (#%s), backend: %s" % (msg, e.strerror, errnostr, str(e.backend_error_code)) )
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
# index (left) is standardised name used by weewx
# value (right) is name used within this code. Assigned by xx_decode functions
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', 'WMR300x')
self.sensor_map = dict(self.DEFAULT_MAP)
if 'sensor_map' in stn_dict:
self.sensor_map.update(stn_dict['sensor_map'])
# loginf('sensor map is %s' % self.sensor_map)
self.heartbeat = 20 # how often to send a6 messages, in seconds
self.history_retry = 60 # how often to retry history, in seconds
self.set_history_limit( stn_dict.get('history_clear_pct', Station.HIST_CLEAR_PERCENT ))
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))
global DEBUG_BACKEND
DEBUG_BACKEND = int(stn_dict.get('debug_backend', DEBUG_BACKEND))
self.last_rain = None
self.last_a6 = 0
self.last_65 = 0
#self.last_7x = 0 - we don't need this for anything I can see.
self.last_record_rcvd = Station.HISTORY_START_REC -1
self.final_history_index = 0
self.history_pct = 0.0
self.last_history_pct_logged = -200
# FIXME: make the cache values age
# FIXME: do this generically so it can be used in other drivers
self.pressure_cache = dict()
self.station = Station()
self.station.open()
self.initiateComms()
def set_history_limit( self, limit=50 ):
""" set the limit at which the console's history buffer will be cleared
limit: integer value defining percentage full
"""
self.history_clear_pct = int(limit)
if self.history_clear_pct > 95:
self.history_clear_pct = 95
# it does not clear history for very low values,
# 4% does not work
# 5% does work
if self.history_clear_pct < 5:
self.history_clear_pct = 5
loginf( "Set to clear history at %d%%" % self.history_clear_pct )
return
def set_history_pct(self, index_value ):
""" assign the current history buffer percent
index_value = next available entry index as returned in 57 status packet
"""
self.history_pct = 100.0 * float(index_value - Station.HISTORY_START_REC) / Station.HISTORY_N_RECORDS
#self.history_pct_int = int( self.history_pct ) # don't use any more
def set_final_history_index( self, buf):
""" read the current history index from console status packet and adjust
relevant values.
Updates instance variables final_history_index and history_pct
Returns the index value
"""
if buf[0] != 0x57:
return None
# extract 16-bit number
idx = (buf[17] << 8) + buf[18]
if idx < Station.HISTORY_START_REC:
raise WMR300Error("History index: %d below limit of %d" % (idx, Station.HISTORY_START_REC) )
elif idx > Station.HISTORY_MAX_REC:
raise WMR300Error("History index: %d above limit of %d" % (idx, Station.HISTORY_MAX_REC) )
self.final_history_index = idx
self.set_history_pct( idx )
return idx
def closePort(self):
self.station.close()
self.station = None
@property
def hardware_name(self):
return self.model
def initiateComms( self ):
r""" initiate communications with wmr300 console.
1. send a special a6 packet
2. read the packet 57
3. send the type-73 packet
4. read the ACK
done
"""
retrycount = 0
while retrycount < 3:
retrycount += 1
try:
n_written=0
n_read=0
n_read = self.station.flush_read_buffer( "initComms" )
loginf("send initial heartbeat, try %i" % retrycount )
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52 ]
state = "req status a6 null"
n_written=0
n_written = self.station.write_noTO(cmd, state)
self.last_a6 = time.time()
# now read the packet 57
state = "reading packet 57"
count=0
while count < 10 :
count += 1
buf = self.station.read()
if buf is None:
raise InitiationError( "initComm: failed to read packet57" ) # try loop again - not sure if this is any use.
if buf[0] == 0x57:
break
logerr( "initComm: got 0x%02x instead of 0x57" % buf[0] ) # try loop again - not sure if this is any use.
if buf is None or buf[0] != 0x57:
raise InitiationError( "failed to get initialization packet57" ) # try loop again - not sure if this is any use.
else:
pkt = Station._decode_57( buf )
idx = self.set_final_history_index( buf )
self.magic0 = pkt['magic0']
self.magic1 = pkt['magic1']
# keep these in case we ever work out what they mean.
self.mystery0 = pkt['mystery0']
self.mystery1 = pkt['mystery1']
# now write the packet 73
cmd = [0x73, 0xe5, 0x0a, 0x26, self.magic0, self.magic1 ]
state = "write 73"
n_written=0
n_written = self.station.write_noTO(cmd, "initComm write 73" )
# and read the ACK
state = "reading ACK to 73"
count=0
while count < 10 :
count += 1
buf = self.station.read()
if buf is None:
raise InitiationError( "initComm: failed to read ACK to packet73" ) # try loop again - not sure if this is any use.
if buf[0] == 0x41:
break
logerr( "initComm: got 0x%02x instead of ACK to packet73" % buf[0] )
# I don't see anything useful in the ACK, it should have same contents as the packet 73,
# and have no idea what to do if they are not
if ( retrycount == 1 ):
if DEBUG_HISTORY:
loginf("Initiation completed" )
else:
loginf("Initiation completed in %i tries" % retrycount )
return
except usb.core.USBError as e:
errmsg = repr(e)
if e.backend_error_code == self.station.backend_timeout_code :
logexception( "initiateComms (from "+state+" with "+str(n_written)+" written)", e)
elif not ('No error' in errmsg):
logexception( "USB failure in initiateComms (from "+state+")", e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum, InitiationError), e:
logerr( "initComms: " + repr(e))
time.sleep(0.10)
raise WMR300Error( "Initiation failed, excessive retries" )
def initiateHistory( self, keep_hist=True ):
r""" initiate history record stream from wmr300 console.
keep_hist == True means read all unread history and retain it
False means (attempt to) delete the history
1. For a read and keep history:
1a. send a special a6 packet (which looks like all other a6 packets except that
we get no status reply (0x57) here)
1b. send a 65 packet
or for delete:
1a. send a 0xb3 packet to read with delete,
2. read the ACK
3. send a cd packet
4. do not read an ACK - it might not come.
- return, to start reading history packets.
"""
retrycount = 0
while retrycount < 5:
retrycount += 1
try:
if DEBUG_HISTORY:
loginf("send history startup, try %i" % retrycount )
n_read = self.station.flush_read_buffer( "initHist" )
if keep_hist:
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52, 0x8b ]
state = "req status a6 null"
n_written=0
n_written = self.station.write_noTO(cmd, "initHist a6")
self.last_a6 = time.time()
# now write packet 65
cmd = [0x65, 0x19, 0xe5, 0x04, 0x52, 0x8b ]
cmd_type = 0x65
initiate_with = "0x65"
state = "write '65'"
nxtrec = Station.get_start_index( self.last_record_rcvd )
else:
# ask for delete after sending
cmd = [0xb3, 0x59, 0x0a, 0x17, 0x01, 0xeb ]
cmd_type = 0xb3
initiate_with = "0xb3"
state = "write 'b3'"
# a partial read seems to be sufficient. No need to read stuff we are not going to save
nxtrec = Station.get_start_index( self.final_history_index - 1200 )
#nxtrec = Station.get_start_index( Station.HISTORY_START_REC ) # force full read for the moment
n_written=0
n_written = self.station.write_noTO(cmd, "initHist timeout on write: " + initiate_with)
# read the ACK
# there may be regular data packets interspersed here, so
# potentially we need to read a few...
state = "reading ACK"
count=0
while count < 10 :
count += 1
buf = self.station.read()
if buf is None:
# try big loop again - not sure if this is any use.
raise InitiationError( "initHist: failed to read ACK to packet " + initiate_with )
if buf[0] == 0x41 and buf[3] == cmd_type:
break
# now send the history request start
if DEBUG_HISTORY :
loginf("Initing history req with cmd %s, starting with record %d" % (initiate_with , nxtrec) )
state = "requesting next rec " + str(nxtrec)
cmd = [0xcd, 0x18, 0x30, 0x62, _hi(nxtrec), _lo(nxtrec)]
n_written = self.station.write_noTO(cmd, "initHist timeout on 0xcd write")
# DO NOT expect any ACK
# The history loop is just a pile of writes from the console, and any ACKs or 0x57 packets are often out of sequence
# so we just drop into the read loop and process whatever comes.
if ( retrycount == 1 ):
if DEBUG_HISTORY:
loginf("History read initiated" )
else:
loginf("History read initiated in %i tries" % retrycount )
return
except usb.core.USBError as e:
errmsg = repr(e)
if e.backend_error_code == self.station.backend_timeout_code :
logexception( "initiateHist (from "+state+" with "+str(n_written)+" written)", e)
elif not ('No error' in errmsg):
logexception( "USB failure in initiateHist (from "+state+")", e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum, InitiationError), e:
logerr( "HistRead: " + repr(e) )
time.sleep(0.10)
raise WMR300Error( "History Initiation failed, excessive retries" )
def finaliseHistory( self ):
r""" conclude history record stream from wmr300 console.
1. final a6 has already been sent, and 57 seen
2. send a 35 packet
4. there is no ACK , but sometimes another 57!? ignore whatever.
done
"""
retrycount = 0
while retrycount < 3:
retrycount += 1
try:
if DEBUG_HISTORY:
loginf("history finish, try %i" % retrycount )
# the flush is just in case it has started sending a current condition update...
n_read = self.station.flush_read_buffer( "finHist" )
# now write packet 35
cmd = [0x35, 0x0b, 0x1a, 0x87, _hi(self.last_record_rcvd), _lo( self.last_record_rcvd )]
state = "write 35"
n_written=0
n_written = self.station.write_noTO(cmd, "finaliseHist timeout on 0x35 write: ")
if ( retrycount == 1 ):
if DEBUG_HISTORY:
loginf("History read completed" )
else:
loginf("History read completed in %i tries" % retrycount )
return
except usb.core.USBError as e:
errmsg = repr(e)
if e.backend_error_code == self.station.backend_timeout_code :
logexception( "finaliseHist (from "+state+" with "+str(n_written)+" written)", e)
elif not ('No error' in errmsg):
logexception( "USB failure in finaliseHist (from "+state+")", e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum, InitiationError), e:
logerr( "Histfinalise: " + repr(e) )
time.sleep(0.10)
raise WMR300Error( "finalise Hist failed, excessive retries" )
def genLoopPackets(self):
state = "unset" # debug state for identifying timeouts
n_written=0
data_since_heartbeat = 0
while True:
try:
state = "reading"
buf = self.station.read()
if buf:
pkt = Station.decode(buf)
if buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# compose ack for most data packets
cmd = [0x41, 0x43, 0x4b, buf[0], buf[7] ]
state = "ack write"
n_written=0
# don't bother sending the ACK - the console does not care and it can lead to lockup.
#n_written = self.station.write_noTO(cmd, "genLoopPackets timeout on ACK write")
# we only care about packets with loop data
if pkt['packet_type'] in [0xd3, 0xd4, 0xd5, 0xd6]:
data_since_heartbeat += 1
packet = self.convert_loop(pkt)
yield packet
elif buf[0] == 0x57:
next_pkt_index = self.set_final_history_index( buf )
if next_pkt_index == Station.HISTORY_MAX_REC and self.last_history_pct_logged <= 100.01 :
logerr( "History now at FULL CAPACITY" )
self.last_history_pct_logged = 101 # ensure only logged once.
elif (self.history_pct > self.last_history_pct_logged + Station.HISTORY_LOG_INTERVAL) or (self.history_pct < self.last_history_pct_logged) :
loginf( "History now at %.1f%% capacity" % self.history_pct )
self.last_history_pct_logged = self.history_pct
if time.time() - self.last_a6 > self.heartbeat:
if DEBUG_HISTORY and data_since_heartbeat < 10 :
loginf( "Packets in hearbeat interval = %d" % data_since_heartbeat )
if data_since_heartbeat <= 0 :
logerr( "No data in hearbeat interval, trying to restart" )
n_read = self.station.flush_read_buffer( "genLoopPackets" )
# I think the 0x73 starts the data transmission, but not sure if the
# a6 / 73 order is important.
cmd = [0x73, 0xe5, 0x0a, 0x26, self.magic0, self.magic1 ]
state = "write 73"
n_written=0
n_written = self.station.write_noTO(cmd, "genLoopPackets write 73" )
data_since_heartbeat = 0
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52 ]
state = "req status a6"
n_written=0
n_written = self.station.write_noTO(cmd, "genloop heartbeat" )
self.last_a6 = time.time()
if self.history_pct >= self.history_clear_pct :
# it is time to clear console's history buffer...
loginf( "History at %d%% capacity exceeds limit of %d%%; clearing..." %
(self.history_pct, self.history_clear_pct) )
reread_start_time = time.time()
self.reread_history_records( reread_start_time )
reread_duration = time.time() - reread_start_time
loginf( "History clear completed in %.1f sec" % reread_duration )
pass
except usb.core.USBError as e:
errmsg = repr(e)
if e.backend_error_code == self.station.backend_timeout_code :
logexception( "genLoopPackets (from "+state+" with "+str(n_written)+" written)", e)
elif not ('No error' in errmsg):
logexception( "USB failure in GenLoopPackets (from "+state+")", e)
raise weewx.WeeWxIOError(e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
def genStartupRecords(self, since_ts):
hbuf = None
last_ts = None
cnt = 0
if DEBUG_HISTORY:
loginf("read Hist since %s: from %d to %d" % (timestamp_to_string(since_ts), self.last_record_rcvd, self.final_history_index ) )
state = "unset hist" # debug state for identifying timeouts
if self.final_history_index <= 0 :
# this should never happen - it means
# either we have never seen a 57 status packet since starting
# or else it was bad.
# probably needs a message and exception
return
self.initiateHistory( )
partpkt = 0 # partially completed packet count
state = "reading history"
while True:
try:
buf = self.station.read()
if buf is not None:
if partpkt == 64 :
# need better indicator of second half history?
# There is no indicator - we HAVE to assume packets are consecutive.
# I presume console firmware "guarantees" they will be.
buf = hbuf + buf
hbuf = None
partpkt = 128
elif partpkt == 0 and buf[0] == 0xd2:
pktlength = buf[1]
if pktlength != 128:
raise WMR300Error("History record unexpected length: assumed 128, found %d" % pktlength )
hbuf = buf
buf = None
partpkt = 64
# just read the next half immediately - we don't want possible a6 packets interspersed
# of course, "that can't happen"
continue
if partpkt == 128:
partpkt = 0
new_record = Station.get_record_index(buf)
if new_record != self.last_record_rcvd +1 :
loginf("historical record skipped from : %d to %d" %
(self.last_record_rcvd, new_record ) )
self.last_record_rcvd = new_record
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:
if DEBUG_HISTORY:
if ( pkt['indexnum'] != self.last_record_rcvd ):
loginf("historical record saved: %d, %d" %
(pkt['indexnum'], self.last_record_rcvd ))
cnt += 1
yield packet
elif buf[0] == 0x57:
idx = self.set_final_history_index(buf)
msg = "count=%s kept last_index rcvd=%s final_index=%s; state = %s" % (
cnt, self.last_record_rcvd, idx, state)
if state == "wait57":
loginf("catchup completed: %s" % msg)
break
else:
loginf("catchup in progress: %s" % msg)
elif buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# don't send ack for most data packets - the PC s/w does but they get
# ignored anyway! so we avoid problems trying to write ack when
# device is trying to write data to us.
# I REALLY don't understand how this happens since we don't seem to have trouble
# sending the a6 packets alone.
""" ignore this debug - too much data
if DEBUG_HISTORY:
loginf( "catchup ignoring pkt 0x%2x at %d" % (buf[0], self.last_record_rcvd ) )
"""
# cmd = [0x41, 0x43, 0x4b, buf[0], buf[7] ]
# n_written=0
# n_written = self.station.write_noTO(cmd, "rereadHist timeout on ACK write")
if self.last_record_rcvd >= (self.final_history_index -1) :
if state == "reading history":
if DEBUG_HISTORY:
msg = "count=%s kept, last_received=%s final=%s" % (
cnt, self.last_record_rcvd, self.final_history_index )
loginf("catchup nearly complete: %s; state=%s" % (msg, state) )
state = "finishing"
if (state == "finishing") or (time.time() - self.last_a6 > self.heartbeat):
if DEBUG_HISTORY:
loginf("request station status at index: %s; state: %s" %
(self.last_record_rcvd, state ) )
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52 ]
if state == "finishing" :
# it is possible that another history packet has been created between the
# most recent 0x57 status and now. So, we have to stay in the loop
# to read the possible next packet.
# evidence suggests that such history packet will arrive before the
# 0x57 reply to this request, so presumably it was already in some output queue.
state = "wait57"
self.last_a6 = time.time()
self.station.write_noTO(cmd, "genHist heartbeat" )
except usb.core.USBError as e:
errmsg = repr(e)
if e.backend_error_code == self.station.backend_timeout_code :
logexception( "genStartupRecords (from "+state+")", e)
elif not ('No error' in errmsg):
logexception( "USB failure in genStartupRecord (from "+state+")", e)
raise weewx.WeeWxIOError(e)
else:
logexception( "genStartupRecords no error?", e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
self.finaliseHistory()
def reread_history_records(self, since_ts):
""" This rereads a few history records in the hope of triggering the console to delete them.
Let's assume we are not needing to keep any, nor
do we need to store any other loop records that might be interposed.
"""
hbuf = None
last_ts = None
if DEBUG_HISTORY:
loginf("Reread Hist since %s: from %d to %d" % (timestamp_to_string(since_ts), self.last_record_rcvd, self.final_history_index ) )
cnt = 0
state = "unset hist" # debug state for identifying timeouts
self.initiateHistory( keep_hist=False )
partpkt = 0 # partially completed packet count
state = "reading history"
"""
A lot of this code is duplicated from genStartupRecords() with bits removed.
I decided to split them rather than run a dual-purpose function because
genStartupRecords() is a generator and this is not.
"""
while True:
try:
buf = self.station.read()
if buf is not None:
if partpkt == 64 :
buf = hbuf + buf
hbuf = None
partpkt = 128
elif partpkt == 0 and buf[0] == 0xd2:
pktlength = buf[1]
if pktlength != 128:
raise WMR300Error("History record unexpected length: assumed 128, found %d" % pktlength )
hbuf = buf
buf = None
partpkt = 64
continue
if partpkt == 128:
partpkt = 0
new_record = Station.get_record_index(buf)
self.last_record_rcvd = new_record
ts = Station._extract_ts(buf[4:9])
if ts is not None and ts > since_ts:
keep = False # just ignore it...
pkt = Station.decode(buf)
packet = self.convert_historical(pkt, ts, last_ts)
last_ts = ts
if keep:
if DEBUG_HISTORY:
if ( pkt['indexnum'] != self.last_record_rcvd ):
loginf("historical record saved: %d, %d" %
(pkt['indexnum'], self.last_record_rcvd ))
cnt += 1
elif buf[0] == 0x57:
idx = self.set_final_history_index(buf)
msg = "kept count=%s; last_index rcvd=%s; final_index=%s; state = %s" % (
cnt, self.last_record_rcvd, idx, state)
if state == "wait57":
loginf("History reread completed: %s" % msg)
break
else:
loginf("History reread in progress: %s" % msg)
elif buf[0] in [0xd3, 0xd4, 0xd5, 0xd6, 0xdb, 0xdc]:
# don't send ack for most data packets
if DEBUG_HISTORY:
loginf( "History reread ignoring pkt 0x%2x at %d" % (buf[0], self.last_record_rcvd ) )
if self.last_record_rcvd >= (self.final_history_index -1) :
if state == "reading history":
if DEBUG_HISTORY:
msg = "count=%s kept, last_received=%s final=%s" % (
cnt, self.last_record_rcvd, self.final_history_index )
loginf("History reread nearly complete: %s; state=%s" % (msg, state) )
state = "finishing"
if (state == "finishing") or (time.time() - self.last_a6 > self.heartbeat):
if DEBUG_HISTORY:
loginf("request station status at index: %s; state: %s" %
(self.last_record_rcvd, state ) )
cmd = [0xa6, 0x91, 0xca, 0x45, 0x52 ]
if state == "finishing" :
state = "wait57"
self.last_a6 = time.time()
self.station.write_noTO(cmd, "reread_hist heartbeat" )
except usb.core.USBError as e:
errmsg = repr(e)
if e.backend_error_code == self.station.backend_timeout_code :
logexception( "reread_history (from "+state+")", e)
elif not ('No error' in errmsg):
logexception( "USB failure in reread_history (from "+state+")", e)
raise weewx.WeeWxIOError(e)
else:
logexception( "reread_history no error?", e)
except (WrongLength, BadChecksum), e:
loginf(e)
time.sleep(0.001)
self.finaliseHistory()
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:
loginf("rain=%s rain_total=%s last_rain=%s" %
(p['rain'], pkt['rain_total'], self.last_rain))
self.last_rain = pkt['rain_total']
if pkt['rain_total'] == Station.MAX_RAIN_MM:
loginf("rain counter maximum reached, counter reset required")
if DEBUG_PACKET:
loginf("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:
# the interval is in units of minutes in the DB
p['interval'] = (ts - last_ts) / 60
return p
def convert_loop(self, pkt):
p = self.convert(pkt, int(time.time() + 0.5))
p['rxCheckPercent'] = self.history_pct; # fake value as easiest way to return it.
if 'pressure' in p:
# cache any pressure-related values
for x in ['pressure', 'barometer']:
self.pressure_cache[x] = p[x]
else:
# apply any cached pressure-related values
p.update(self.pressure_cache)
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 InitiationError(WMR300Error):
"""something went wrong in the initiation process"""
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
# default value: clear console history buffer when it reaches this percentage full...
HIST_CLEAR_PERCENT = 5
# only needed for diagnostics
HISTORY_LOG_INTERVAL = 1 # report every time history size changes by more than this percentage
HISTORY_START_REC = 0x20 # index to first history record
HISTORY_MAX_REC = 0x7fe0 # index to history record when full
HISTORY_N_RECORDS = 32704 # maximum number of records returned (equals MAX_REC - START_REC)
MAX_RAIN_MM = 10160 # maximum value of rain counter, in mm
BACKEND_LIBUSB0 = 0
BACKEND_LIBUSB1 = 1
BACKEND_OPENUSB = 2
BACKEND_OTHER = 3
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 = 500
self.interface = 0 # device has only the one interface
self.dev = None
self.recv_counts = dict()
self.send_counts = dict()
self.backend = Station.BACKEND_OTHER
self.backend_name = "do not know"
self.backend_timeout_code = -1111
def __enter__(self):
self.open()
return self
def __exit__(self, _, value, traceback): # @UnusedVariable
self.close()
def open(self):
if DEBUG_BACKEND == 0:
dev = usb.core.find( idVendor=self.vendor_id, idProduct=self.product_id)
else:
# adjust this for test purposes...
if DEBUG_BACKEND == 1:
import usb.backend.libusb0 as libusb
elif DEBUG_BACKEND == 2:
import usb.backend.libusb1 as libusb
elif DEBUG_BACKEND == 3:
import usb.backend.openusb as libusb
else:
raise WMR300Error("invalid backend option" )
dev = usb.core.find( backend=libusb.get_backend(), idVendor=self.vendor_id, idProduct=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.dev = dev
backend = str( dev.backend )
if "libusb0" in backend:
self.backend = Station.BACKEND_LIBUSB0
self.backend_name = "libusb0"
self.backend_timeout_code = -110
elif "libusb1" in backend:
self.backend = Station.BACKEND_LIBUSB1
self.backend_name = "libusb1"
self.backend_timeout_code = -7
elif "openusb" in backend:
self.backend = Station.BACKEND_OPENUSB
self.backend_name = "openusb"
self.backend_timeout_code = -110 # FIXME - I dont know this
else:
self.backend = Station.BACKEND_OTHER
self.backend_name = "some other"
self.backend_timeout_code = -110 # FIXME - I dont know this
loginf( "using PyUSB backend: %s" % self.backend_name )
# for HID devices on linux, be sure kernel does not claim the interface
if self.backend == Station.BACKEND_LIBUSB0:
loginf( "libusb0 - detaching interface %d" % self.interface )
try:
dev.detach_kernel_driver( self.interface )
except (usb.core.USBError):
pass
else:
kernelactive = dev.is_kernel_driver_active( self.interface )
if kernelactive :
loginf( "Kernel driver is active, detaching interface %d" % self.interface )
dev.detach_kernel_driver( self.interface )
dev.set_configuration()
dev.reset()
def close(self):
if self.dev is not None:
try:
usb.util.release_interface(self.dev, self.interface)
except (ValueError, usb.core.USBError), e:
loginf("Release interface failed: %s" % e)
self.dev = None
def reset(self):
self.dev.reset()
def read(self, count=True):
buf = None
try:
buf = self.dev.read(
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.core.USBError as e:
errmsg = repr(e)
if e.backend_error_code == self.backend_timeout_code :
pass
elif not ('No error' in errmsg):
raise
else:
logexception( "dev.read", e)
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.dev.write(Station.EP_OUT, buf, self.timeout)
if DEBUG_COUNTS:
self.update_count(buf, self.send_counts)
return sent
def write_noTO( self, buf, message ):
""" write_noTO - attempt to write a packet, but do not treat timeout as an error.
A timeout probably means that the console is not interested in listening to us at the moment.
"""
n_written = 0
try:
# their bizarre code seems to occasionally time out on writing ACK
# - ignore any for the moment...
n_written = self.write(buf)
except usb.core.USBError as e:
if e.backend_error_code == self.backend_timeout_code :
logexception( message, e )
else:
raise
return n_written # possibly wrong - we cannot tell.
def flush_read_buffer( self, state ):
""" just read whatever the device has ready to send us and discard
"""
lasttimeout = self.timeout
self.timeout = 100
pkts = 0
try:
while( self.read( False ) is not None ) :
pkts+=1
finally:
self.timeout = lasttimeout
if ( pkts > 0 ):
if DEBUG_HISTORY:
loginf( "%s: discarded %d packets" % (state, pkts ) )
return pkts
# 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 _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_start_index(n):
# return the starting history index to read
# the HISTORY_MAX_REC value is what it returned in packet 0x57 when the
# buffer is full. You cannot ask for it, only the one before.
if n < Station.HISTORY_START_REC:
return Station.HISTORY_START_REC
if n >= Station.HISTORY_MAX_REC-1:
return Station.HISTORY_MAX_REC-1 # wraparound never happens
return n
@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])
pkt['magic0'] = buf[12]
pkt['magic1'] = buf[13]
pkt['history_cleared'] = (buf[20] == 0x43 )
pkt['mystery0'] = buf[22]
pkt['mystery1'] = buf[23]
pkt['final_idx'] = (buf[17] << 8) + buf[18]
#if DEBUG_HISTORY:
#loginf("pkt 57 in status records: %s" % pkt['final_idx'] )
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['indexnum'] = Station.get_record_index(buf)
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
pkt['heatindex_%d' % pkt['channel']] = Station._extract_signed(
buf[13], buf[14], 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
pkt['windchill'] = Station._extract_signed(buf[18], buf[19], 0.1) # C
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 """
[WMR300x]
# This section is for WMR300 weather stations.
# The station model, e.g., WMR300A
model = WMR300x
# The driver to use:
driver = user.wmr300x
# the console history buffer will be emptied each
# time it gets to this percent full
# allowed range 5 to 95.
history_clear_pct = 10
"""
def modify_config(self, config_dict):
print """
Setting rainRate, windchill, heatindex calculations to hardware.
Dewpoint from hardware is truncated to integer so use software"""
config_dict.setdefault('StdWXCalculate', {})
config_dict['StdWXCalculate'].setdefault('Calculations', {})
config_dict['StdWXCalculate']['Calculations']['rainRate'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['windchill'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['heatindex'] = 'hardware'
config_dict['StdWXCalculate']['Calculations']['dewpoint'] = 'software'
# 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=. python user/wmr300x.py
if __name__ == '__main__':
import optparse
usage = """%prog [options] [--help]"""
syslog.openlog('wmr300x', 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 = {
'hist_clear_pct' : 20,
'debug_comm': 1,
'debug_loop': 0,
'debug_counts': 0,
'debug_decode': 0,
'debug_history': 0,
'debug_rain': 1,
'debug_backend': 0
}
stn = WMR300Driver(**stn_dict)
logdbg( "Station is %s, prod: %s, by: %s" % (stn.hardware_name, stn.station.dev.product, stn.station.dev.manufacturer) )
test_history= True
test_history= not test_history
if test_history:
last_week=time.time() - 3600*24*7
pktnum = stn.last_record_rcvd
lastidx = stn.final_history_index
for packet in stn.genStartupRecords( last_week ):
if ( (pktnum %100 == 0) or pktnum > 9700 ):
print "record %d (%d to go) index:%d" % (pktnum, lastidx-pktnum, stn.last_record_rcvd )
print packet
pktnum += 1
for packet in stn.genLoopPackets():
print packet