Here is a version with the previous problem fixed. I have also included some diagnostic code that checks for the console stopping reporting (as noted in the second post in this thread) If it gets no useful data in the 20 seconds between a6 hearbeats then it sends a packet that I think restarts sending. This is redundant in most cases but it does not seem to cause a problem. So far I have seen it triggered between zero and 5 times in a day.
The diagnostics exposed some behaviour that I cannot explain. The normal number of loop data packets sent within the 20 seconds heartbeat interval is around 130. But every so often it drops dramatically and with a fairly consistent pattern: - it is only the first 20 second block in each minute where loop data stops, the 2nd and 3rd are normal - the number of loop data packets in the first block of the minute gradually increases until it is similar to the other blocks, then the cycle restarts - the time interval of the long cycle is not constant, but varies between 70 and 100 minutes. - at the moment I cannot tell if it is something in the driver that is causing it, or if it is normal. At some time I will do some experimenting and check the windows captures. Here is an edited snippet from my log file to illustrate the point: 18:21:53 Packets in heartbeat interval = 134 18:22:13 Packets in heartbeat interval = 135 18:22:34 Packets in heartbeat interval = 129 18:22:54 Packets in heartbeat interval = 135 18:23:14 Packets in heartbeat interval = 136 18:23:34 Packets in heartbeat interval = 133 18:23:54 Packets in heartbeat interval = 134 18:24:14 Packets in heartbeat interval = 134 18:24:34 Packets in heartbeat interval = 133 18:24:54 Packets in heartbeat interval = 134 18:25:14 Packets in heartbeat interval = 1 18:25:35 Packets in heartbeat interval = 137 18:25:55 Packets in heartbeat interval = 135 18:26:15 Packets in heartbeat interval = 6 18:26:35 Packets in heartbeat interval = 135 18:26:55 Packets in heartbeat interval = 135 18:27:15 Packets in heartbeat interval = 8 18:27:35 Packets in heartbeat interval = 134 18:27:55 Packets in heartbeat interval = 134 18:28:15 Packets in heartbeat interval = 8 18:28:35 Packets in heartbeat interval = 134 18:28:55 Packets in heartbeat interval = 134 18:29:15 Packets in heartbeat interval = 9 18:29:35 Packets in heartbeat interval = 134 18:29:55 Packets in heartbeat interval = 135 18:30:15 Packets in heartbeat interval = 9 18:30:36 Packets in heartbeat interval = 134 18:30:56 Packets in heartbeat interval = 135 18:31:16 Packets in heartbeat interval = 11 18:31:36 Packets in heartbeat interval = 135 18:31:56 Packets in heartbeat interval = 135 18:32:16 Packets in heartbeat interval = 14 18:32:36 Packets in heartbeat interval = 135 18:32:56 Packets in heartbeat interval = 134 -- 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 = 'WMR300x' 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
