├── tests ├── rtl-weather-30k-1.raw ├── rtl-weather-30k-2.raw ├── rtl-weather-30k-3.raw ├── rtl-weather-30k-4.raw └── test_decode_elv_wde1.py ├── README.md ├── decode_mebus.py └── decode_elv_wde1.py /tests/rtl-weather-30k-1.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skaringa/weather-sdr-decode/HEAD/tests/rtl-weather-30k-1.raw -------------------------------------------------------------------------------- /tests/rtl-weather-30k-2.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skaringa/weather-sdr-decode/HEAD/tests/rtl-weather-30k-2.raw -------------------------------------------------------------------------------- /tests/rtl-weather-30k-3.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skaringa/weather-sdr-decode/HEAD/tests/rtl-weather-30k-3.raw -------------------------------------------------------------------------------- /tests/rtl-weather-30k-4.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skaringa/weather-sdr-decode/HEAD/tests/rtl-weather-30k-4.raw -------------------------------------------------------------------------------- /tests/test_decode_elv_wde1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Unit test for decode_elv_wde1 4 | 5 | import io 6 | import struct 7 | import unittest 8 | import sys 9 | from os import path 10 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 11 | from decode_elv_wde1 import decoder 12 | 13 | # Test hook: store the decoder output into an instance variable 14 | # instead of printing it 15 | class test_decoder(decoder): 16 | def print_decoder_output(self, decoder_out): 17 | self.decoder_out = decoder_out 18 | 19 | # Unit test class 20 | class test_decode_elv_wde1(unittest.TestCase): 21 | 22 | # Process the given file with the decoder 23 | def process_file(self, filename, decoder): 24 | fin = io.open("{0}/{1}".format(path.dirname(path.abspath(__file__)), filename), mode="rb") 25 | b = fin.read(512) 26 | while len(b) == 512: 27 | values = struct.unpack('256h', b) 28 | for val in values: 29 | decoder.process(val) 30 | b = fin.read(512) 31 | 32 | fin.close() 33 | 34 | # Feed several real-world samples into the decoder 35 | # and verify the decoder output 36 | def test_sample_1(self): 37 | dec = test_decoder() 38 | self.process_file('rtl-weather-30k-1.raw', dec) 39 | self.assertEqual(dec.decoder_out['sensor_type_str'], 'Thermo/Hygro') 40 | self.assertEqual(dec.decoder_out['address'], 6) 41 | self.assertEqual(dec.decoder_out['temperature'], 20.2) 42 | self.assertEqual(dec.decoder_out['humidity'], 61.7) 43 | 44 | def test_sample_2(self): 45 | dec = test_decoder() 46 | self.process_file('rtl-weather-30k-2.raw', dec) 47 | self.assertEqual(dec.decoder_out['sensor_type_str'], 'Kombi') 48 | self.assertEqual(dec.decoder_out['address'], 1) 49 | self.assertEqual(dec.decoder_out['temperature'], 17.6) 50 | self.assertEqual(dec.decoder_out['humidity'], 54) 51 | self.assertEqual(dec.decoder_out['wind'], 0) 52 | self.assertEqual(dec.decoder_out['rain_sum'], 1634) 53 | self.assertEqual(dec.decoder_out['rain_detect'], False) 54 | 55 | def test_sample_3(self): 56 | dec = test_decoder() 57 | self.process_file('rtl-weather-30k-3.raw', dec) 58 | self.assertEqual(dec.decoder_out['sensor_type_str'], 'Thermo/Hygro') 59 | self.assertEqual(dec.decoder_out['address'], 4) 60 | self.assertEqual(dec.decoder_out['temperature'], 18.8) 61 | self.assertEqual(dec.decoder_out['humidity'], 61.1) 62 | 63 | def test_sample_4(self): 64 | dec = test_decoder() 65 | self.process_file('rtl-weather-30k-4.raw', dec) 66 | self.assertEqual(dec.decoder_out['sensor_type_str'], 'Thermo/Hygro') 67 | self.assertEqual(dec.decoder_out['address'], 6) 68 | self.assertEqual(dec.decoder_out['temperature'], 20.4) 69 | self.assertEqual(dec.decoder_out['humidity'], 61.3) 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | weather-sdr-decode 2 | ==================== 3 | 4 | Decoders for wireless weather sensor data received with RTL SDR. 5 | 6 | Prerequisites 7 | ============= 8 | 9 | * [rtl-sdr](https://osmocom.org/projects/rtl-sdr/wiki/Rtl-sdr) 10 | * Python 2.7 or 3, on slow machines [PyPy](https://pypy.org) is recommended 11 | * Wireless weather sensor 12 | 13 | decode\_elv\_wde1.py 14 | ==================== 15 | 16 | This program decodes weather data produces by wiresless sensors from [ELV](https://www.elv.de). 17 | 18 | It should work with the following sensors: 19 | 20 | * Temperature sensor S 300 IA 21 | * Temperature/Hygro sensor S 300 TH und ASH 2200 22 | * Temperature/Hygro/Wind/Rain ("Kombi") sensor KS 200/300 23 | 24 | I've tested it with: 25 | 26 | * Temperature/Hygro sensor S 300 TH 27 | * Kombi sensor KS 300-2 (Picture below) 28 | 29 | ![Picture of KS 300](https://www.kompf.de/weather/images/20090419_003.jpg) 30 | 31 | *Typical usage:* 32 | 33 | rtl\_fm -M am -f 868.35M -s 30k | ./decode\_elv\_wde1.py - 34 | 35 | *Help:* 36 | 37 | ./decode\_elv\_wde1.py -h 38 | 39 | References 40 | ---------- 41 | 42 | * [RTL SDR](https://osmocom.org/projects/rtl-sdr/wiki/Rtl-sdr) 43 | * The weather sensors are manufactured by [ELV](https://www.elv.de/) 44 | * Helmut Bayerlein describes the [communication protocol](http://www.dc3yc.homepage.t-online.de/protocol.htm) 45 | 46 | 47 | decode\_mebus.py 48 | =============== 49 | 50 | This program decodes weather data produces by wiresless sensors from _Mebus_ like this one: 51 | 52 | ![Picture of Mebus outdoor sensor](https://www.kompf.de/weather/images/mebus_outdoor.jpg) 53 | 54 | *Typical usage:* 55 | 56 | rtl\_fm -M am -f 433.84M -s 30k | ./decode\_mebus.py - 57 | 58 | *Help:* 59 | 60 | ./decode\_mebus.py -h 61 | 62 | References 63 | ---------- 64 | 65 | * [RTL SDR](https://osmocom.org/projects/rtl-sdr/wiki/Rtl-sdr) 66 | * The weather sensors are manufactured by Albert Mebus GmbH, Haan, Germany 67 | 68 | Performance 69 | =========== 70 | 71 | To decode in real time on machines with a slower CPU like the Raspberry Pi, the usage of [PyPy](https://pypy.org) as Python interpreter is recommended. The script decode\_elv\_wde1.py runs four times faster with PyPy. To set it as standard interpreter, change the first line of the script into 72 | 73 | #!/usr/bin/env pypy 74 | 75 | License 76 | ======= 77 | 78 | Copyright 2014,2022 Martin Kompf 79 | 80 | This program is free software: you can redistribute it and/or modify 81 | it under the terms of the GNU General Public License as published by 82 | the Free Software Foundation, either version 3 of the License, or 83 | (at your option) any later version. 84 | 85 | This program is distributed in the hope that it will be useful, 86 | but WITHOUT ANY WARRANTY; without even the implied warranty of 87 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 88 | GNU General Public License for more details. 89 | 90 | You should have received a copy of the GNU General Public License 91 | along with this program. If not, see . 92 | 93 | -------------------------------------------------------------------------------- /decode_mebus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Decoder for weather data of sensors from Mebus received with RTL SDR 4 | # Typical usage: 5 | # rtl_fm -M am -f 433.84M -s 30k | ./decode_mebus.py - 6 | # Help: 7 | # ./decode_mebus.py -h 8 | 9 | # Copyright 2014,2022 Martin Kompf 10 | # 11 | # This program is free software: you can redistribute it and/or modify 12 | # it under the terms of the GNU General Public License as published by 13 | # the Free Software Foundation, either version 3 of the License, or 14 | # (at your option) any later version. 15 | # 16 | # This program is distributed in the hope that it will be useful, 17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | # GNU General Public License for more details. 20 | # 21 | # You should have received a copy of the GNU General Public License 22 | # along with this program. If not, see . 23 | 24 | # References: 25 | # RTL SDR 26 | # The weather sensors are manufactured by Albert Mebus GmbH, Haan, Germany 27 | 28 | import sys 29 | import io 30 | import time 31 | import struct 32 | import math 33 | import logging 34 | import argparse 35 | 36 | class decoder(object): 37 | def __init__(self): 38 | # Because we are sampling at 30khz (33.3us), 39 | # the length of the sync block is about 90 samples. 40 | self.buf = [0] * 90 41 | self.decoder_state = 'wait' 42 | self.pulse_len = 0 43 | self.clipped = 0 44 | self.noise_level = 0 45 | self.signal_state = 0 46 | self.data = [] 47 | self.frames = [] 48 | self.pulse_border = 88 # distinguish between logical 0 and 1 49 | self.pulse_limit = 177 # to determine the end of a packet 50 | 51 | def reset(self): 52 | logging.info("WAITING...") 53 | self.decoder_state = 'wait' 54 | self.frames = [] 55 | self.pulse_len = 0 56 | 57 | def repeat(self): 58 | logging.info("REPEATING...") 59 | self.frames.append(self.data) 60 | self.decoder_state = 'repeat_1' 61 | 62 | def process(self, value): 63 | self.buf.pop(0) 64 | self.buf.append(value) 65 | self.pulse_len += 1 66 | 67 | if self.clipped % 10000 == 1: 68 | logging.error("Clipped signal detected, you should reduce gain of receiver!") 69 | 70 | if self.decoder_state == 'wait': 71 | if self.pulse_len > 90: 72 | self.test_sync_block() 73 | return 74 | 75 | # at this point we have a valid sync block and know the noise_level 76 | # to detect pulses 77 | next_signal_state = 1 if value > self.noise_level else 0 78 | if self.signal_state == 0: 79 | if next_signal_state == 0: 80 | if (self.decoder_state == 'data') and (self.pulse_len > self.pulse_limit): 81 | # end of frame 82 | self.dump() 83 | self.repeat() 84 | if (self.decoder_state == 'repeat_2') and (self.pulse_len > 2 * self.pulse_limit): 85 | # End of packet 86 | self.decode() 87 | self.reset() 88 | else: 89 | self.signal_goto_on() 90 | elif self.signal_state == 1: 91 | if next_signal_state == 0: 92 | self.signal_goto_off() 93 | self.signal_state = next_signal_state 94 | 95 | def test_sync_block(self): 96 | # A valid sync block has the levels on-off-on-off-on-off. 97 | # Each level has a duration of avr. 15 samples. 98 | # But allow a jitter of 5 samples for each level change. 99 | avh0 = self.signal_avr(0, 10) 100 | avl0 = self.signal_avr(20, 25) 101 | if avh0 < avl0 * 2: 102 | return # high signal ampl. should be greater than low 103 | 104 | rgh0 = self.signal_range(0, 10) 105 | rgl0 = self.signal_range(20, 25) 106 | if avh0 < rgh0 or avh0 < rgl0: 107 | return # average of high signal amplitude should be greater than noise 108 | 109 | avh1 = self.signal_avr(35, 40) 110 | avl1 = self.signal_avr(50, 55) 111 | if avh1 < avl1 * 2: 112 | return # high signal ampl. of second pulse should be greater than low 113 | 114 | if avh1 < avl0 or avh0 < avl1: 115 | return # high of second pluse should be greate than low of first 116 | 117 | avh2 = self.signal_avr(65, 70) 118 | avl2 = self.signal_avr(80, 90) 119 | if avh2 < avl2 * 2: 120 | return # high signal ampl. of third pulse should be greater than low 121 | 122 | if avh2 < avl0 or avh0 < avl2: 123 | return # high of third pluse should be greater than low of first 124 | 125 | # Valid sync block found! 126 | self.decoder_state = 'sync' 127 | self.signal_state = 0 128 | self.noise_level = (avh0 + avl0 + avh1 + avl1 + avh2 + avl2) / 6 129 | self.pulse_len = 0 130 | logging.info("SYNC!") 131 | logging.debug("noise_level={0}".format(self.noise_level)) 132 | 133 | def signal_avr(self, begin, end): 134 | sm = sum(self.buf[begin:end]) 135 | return sm/(end-begin) 136 | 137 | def signal_range(self, begin, end): 138 | mn = min(self.buf[begin:end]) 139 | mx = max(self.buf[begin:end]) 140 | if mx > 32500 or mn < -32500: 141 | self.clipped += 1 142 | return mx-mn 143 | 144 | def signal_goto_on(self): 145 | self.signal_off(self.pulse_len) 146 | self.pulse_len = 0 147 | 148 | def signal_goto_off(self): 149 | self.signal_on(self.pulse_len) 150 | self.pulse_len = 0 151 | 152 | def signal_on(self, length): 153 | logging.debug(" ON: {0}".format(length)) 154 | if self.decoder_state == 'sync': 155 | self.decoder_state = 'start' 156 | if self.decoder_state == 'repeat_1': 157 | self.decoder_state = 'repeat_2' 158 | 159 | def signal_off(self, length): 160 | logging.debug("OFF: {0}".format(length)) 161 | if self.decoder_state == 'repeat_2': 162 | self.expect_repeat(self.bitval(length)) 163 | elif self.decoder_state == 'start': 164 | self.expect_start(self.bitval(length)) 165 | elif self.decoder_state == 'data': 166 | self.data.append(self.bitval(length)) 167 | 168 | def bitval(self, length): 169 | if length < self.pulse_border: 170 | return 0 171 | if (length > self.pulse_border) and (length < self.pulse_limit): 172 | return 1 173 | logging.warn("off pulse too long") 174 | self.reset() 175 | 176 | def expect_start(self, value): 177 | if value == 1: 178 | self.data = [] 179 | self.decoder_state = 'data' 180 | logging.info("START") 181 | else: 182 | logging.warn("Start bit is not 1") 183 | self.reset() 184 | 185 | def expect_repeat(self, value): 186 | if value == 0: 187 | self.data = [] 188 | self.decoder_state = 'start' 189 | logging.info("REPEAT") 190 | else: 191 | logging.warn("Repeat bit is not 0") 192 | self.reset() 193 | 194 | def popbits(self, num): 195 | val = 0 196 | if len(self.data) < num: 197 | logging.warn("data exhausted") 198 | return 0 199 | for i in range(0, num): 200 | val <<= 1 201 | val += self.data.pop(0) 202 | return val 203 | 204 | def dump(self): 205 | logging.info("DUMP Frame") 206 | s = '' 207 | i = 0 208 | for d in self.data: 209 | if i%4 == 0: 210 | s += ' ' 211 | s += str(d) 212 | i += 1 213 | logging.info(s) 214 | 215 | def decode(self): 216 | logging.info("DECODE") 217 | if len(self.frames) == 0: 218 | logging.warn("Frame contains no data") 219 | self.reset() 220 | return 221 | 222 | # check if all frames contain the same data 223 | check = self.frames[0] 224 | for i in range(1, len(self.frames)): 225 | if check != self.frames[i]: 226 | logging.warn("Frame {0} is not equals to first one".format(i)) 227 | self.reset() 228 | return 229 | 230 | id = self.popbits(11) 231 | setkey = self.popbits(1) 232 | channel = self.popbits(2) 233 | temp = self.popbits(12) 234 | if temp >= 2048: 235 | # negative value 236 | temp = temp - 4096 237 | hum = self.popbits(8) 238 | 239 | print(time.strftime("time: %x %X")) 240 | print("id: {0}".format(id)) 241 | print("setkey: {0}".format(setkey)) 242 | print("channel: {0}".format(channel + 1)) 243 | print("temperature: {0}".format(temp/10.0)) 244 | print("humidity: {0}".format(hum)) 245 | 246 | print 247 | 248 | def main(): 249 | parser = argparse.ArgumentParser(description='Decoder for weather data of sensors from Mebus received with RTL SDR') 250 | parser.add_argument('--log', type=str, default='WARN', help='Log level: DEBUG|INFO|WARN|ERROR. Default: WARN') 251 | parser.add_argument('inputfile', type=str, nargs=1, help="Input file name. Expects a raw file with signed 16-bit samples in platform default byte order and 30 kHz sample rate. Use '-' to read from stdin. Example: rtl_fm -M am -f 433.84M -s 30k | ./decode_mebus.py -") 252 | 253 | args = parser.parse_args() 254 | 255 | loglevel = args.log 256 | loglevel_num = getattr(logging, loglevel.upper(), None) 257 | if not isinstance(loglevel_num, int): 258 | raise ValueError('Invalid log level: ' + loglevel) 259 | logging.basicConfig(stream=sys.stderr, level=loglevel_num) 260 | 261 | dec = decoder() 262 | 263 | filename = args.inputfile[0] 264 | if filename == '-': 265 | filename = sys.stdin.fileno() 266 | fin = io.open(filename, mode="rb") 267 | b = fin.read(512) 268 | while len(b) == 512: 269 | values = struct.unpack('256h', b) 270 | for val in values: 271 | dec.process(val) 272 | b = fin.read(512) 273 | 274 | fin.close() 275 | 276 | if __name__ == '__main__': 277 | main() 278 | -------------------------------------------------------------------------------- /decode_elv_wde1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Decoder for weather data of sensors from ELV received with RTL SDR 4 | # Typical usage: 5 | # rtl_fm -M am -f 868.35M -s 30k | ./decode_elv_wde1.py - 6 | # Help: 7 | # ./decode_elv_wde1.py -h 8 | 9 | # Copyright 2014,2022 Martin Kompf 10 | # 11 | # This program is free software: you can redistribute it and/or modify 12 | # it under the terms of the GNU General Public License as published by 13 | # the Free Software Foundation, either version 3 of the License, or 14 | # (at your option) any later version. 15 | # 16 | # This program is distributed in the hope that it will be useful, 17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | # GNU General Public License for more details. 20 | # 21 | # You should have received a copy of the GNU General Public License 22 | # along with this program. If not, see . 23 | 24 | # References: 25 | # RTL SDR 26 | # The weather sensors are manufactured by ELV 27 | # Helmut Bayerlein describes the communication protocol 28 | 29 | import sys 30 | import io 31 | import time 32 | import struct 33 | import math 34 | import logging 35 | import argparse 36 | 37 | class decoder(object): 38 | def __init__(self): 39 | # We are sampling at 30khz (33.3us), 40 | # and the length of a bit is always 1220us. 41 | # Therefore the length of the buffer fo a whole bit is 36.6 samples. 42 | # Round this down to 35 avoid getting the next bit into the buffer 43 | self.buf = [0] * 35 44 | self.decoder_state = 'wait' 45 | self.pulse_len = 0 46 | self.on_level = 0 47 | self.sync_count = 0 48 | self.data = [] 49 | self.clipped = 0 50 | 51 | def process(self, value): 52 | self.buf.pop(0) 53 | self.buf.append(value) 54 | self.pulse_len += 1 55 | if self.pulse_len <= 35: 56 | return # buffer not filled 57 | 58 | if self.clipped % 10000 == 1: 59 | logging.error("Clipped signal detected, you should reduce gain of receiver!") 60 | 61 | if self.decoder_state == 'wait': 62 | self.sync_count = 0 63 | self.data = [] 64 | self.test_sync0() # search for first sync bit 65 | else: 66 | val = self.bitval() 67 | logging.debug("bitval = {0}".format(val)) 68 | if val == -1: 69 | # Failed to decode bitval 70 | self.decoder_state = 'wait' 71 | elif val == -10: 72 | if (self.decoder_state == 'data'): 73 | # end of frame? 74 | self.decode() 75 | self.decoder_state = 'wait' 76 | elif self.decoder_state == 'sync': 77 | if val == 0: 78 | # another sync pulse 79 | self.sync_count += 1 80 | elif val == 1 and self.sync_count > 6: 81 | # got the start bit 82 | logging.info('DATA') 83 | self.decoder_state = 'data' 84 | elif self.decoder_state == 'data': 85 | self.data.append(val) 86 | 87 | def test_sync0(self): 88 | # Test if the data in the buffer is the first sync bit 89 | # This bit consists of high amplitude with ~21 samples 90 | # and low amplitude with ~10 samples 91 | 92 | avh = self.signal_avr(0, 20) 93 | avl = self.signal_avr(26, 33) 94 | if avh < avl * 2: 95 | return # high signal ampl. should be greater than low 96 | 97 | rgh = self.signal_range(0, 20) 98 | rgl = self.signal_range(26, 33) 99 | if avh < rgh or avh < rgl: 100 | return # average of high signal amplitude should be greater than noise 101 | 102 | # We found a valid sync 0 103 | self.decoder_state = 'sync' 104 | self.on_level = (avh+avl)/2 105 | self.pulse_len = 0 106 | logging.info("SYNC!") 107 | logging.debug("avh={0} avl={1}".format(avh, avl)) 108 | return 109 | 110 | def signal_avr(self, begin, end): 111 | sm = sum(self.buf[begin:end]) 112 | return sm/(end-begin) 113 | 114 | def signal_range(self, begin, end): 115 | mn = min(self.buf[begin:end]) 116 | mx = max(self.buf[begin:end]) 117 | if mx > 32500 or mn < -32500: 118 | self.clipped += 1 119 | return mx-mn 120 | 121 | def bitval(self): 122 | # detect start of bit: the signal shouldnow be at off level, 123 | # so detect a transition to on 124 | skip = 0 125 | while skip < 4: 126 | x = self.buf[skip] 127 | if x > self.on_level: 128 | break 129 | skip += 1 130 | 131 | self.pulse_len = -skip 132 | logging.debug("skip={0}".format(skip)) 133 | if skip >= 4: 134 | logging.debug("No starting slope off->on deteced") 135 | return -10 136 | 137 | # first 12 samples always high signal 138 | # but allow a jitter of 1 sample 139 | val = -1 140 | aa = self.signal_avr(skip, skip+10); 141 | # Next 12 samples either low or high depending on bitval 142 | ma = self.signal_avr(skip+13, skip+22) 143 | # last 12 samples should be always low signal 144 | ea = self.signal_avr(skip+25, 33) 145 | 146 | if aa > ea: 147 | if abs(ma-aa) > abs(ma-ea): 148 | val = 1 149 | else: 150 | val = 0 151 | 152 | self.on_level = (aa+ea)/2 153 | logging.debug("bitval: a={0} m={1} e={2} val={3}".format(aa, ma, ea, val)) 154 | return val 155 | 156 | def popbits(self, num): 157 | val = 0 158 | if len(self.data) < num: 159 | logging.warn("data exhausted") 160 | return 0 161 | for i in range(0, num): 162 | val += self.data.pop(0) << i 163 | return val 164 | 165 | def decode(self): 166 | sensor_types = ('Thermo', 'Thermo/Hygro', 'Rain(?)', 'Wind(?)', 'Thermo/Hygro/Baro', 'Luminance(?)', 'Pyrano(?)', 'Kombi') 167 | sensor_data_count = (5, 8, 5, 8, 12, 6, 6, 14) 168 | 169 | logging.info("DECODE") 170 | check = 0 171 | sum = 0 172 | sensor_type = self.popbits(4) & 7 173 | if not self.expect_eon(): 174 | return 175 | check ^= sensor_type 176 | sum += sensor_type 177 | 178 | # read data as nibbles 179 | nibble_count = sensor_data_count[sensor_type] 180 | dec = [] 181 | for i in range(0, nibble_count): 182 | nibble = self.popbits(4) 183 | if not self.expect_eon(): 184 | return 185 | dec.append(nibble) 186 | check ^= nibble 187 | sum += nibble 188 | 189 | # check 190 | if check != 0: 191 | logging.warn("Check is not 0 but {0}".format(check)) 192 | return 193 | 194 | # sum 195 | sum_read = self.popbits(4) 196 | sum += 5 197 | sum &= 0xF 198 | if sum_read != sum: 199 | logging.warn("Sum read is {0} but computed is {1}".format(sum_read, sum)) 200 | return 201 | 202 | # compute values 203 | decoder_out = { 204 | 'sensor_type': sensor_type, 205 | 'sensor_type_str': sensor_types[sensor_type], 206 | 'address': dec[0] & 7, 207 | 'temperature': (dec[3]*10. + dec[2] + dec[1]/10.) * (-1. if dec[0]&8 else 1.), 208 | 'humidity': 0., 209 | 'wind': 0., 210 | 'rain_sum': 0, 211 | 'rain_detect': 0, 212 | 'pressure': 0 213 | } 214 | 215 | if sensor_type == 7: 216 | # Kombisensor 217 | decoder_out['humidity'] = dec[5]*10. + dec[4] 218 | decoder_out['wind'] = dec[8]*10. + dec[7] + dec[6]/10. 219 | decoder_out['rain_sum'] = dec[11]*16*16 + dec[10]*16 + dec[9] 220 | decoder_out['rain_detect'] = dec[0]&2 == 1 221 | 222 | if (sensor_type == 1) or (sensor_type == 4): 223 | # Thermo/Hygro 224 | decoder_out['humidity'] = dec[6]*10. + dec[5] + dec[4]/10. 225 | 226 | if sensor_type == 4: 227 | # Thermo/Hygro/Baro 228 | decoder_out['pressure'] = 200 + dec[9]*100 + dec[8]*10 + dec[7] 229 | 230 | self.print_decoder_output(decoder_out) 231 | 232 | def print_decoder_output(self, decoder_out): 233 | print(time.strftime("time: %x %X")) 234 | print("sensor type: " + decoder_out['sensor_type_str']) 235 | print("address: {0}".format(decoder_out['address'])) 236 | 237 | print("temperature: {0}".format(decoder_out['temperature'])) 238 | 239 | if decoder_out['sensor_type'] == 7: 240 | # Kombisensor 241 | print("humidity: {0}".format(decoder_out['humidity'])) 242 | print("wind: {0}".format(decoder_out['wind'])) 243 | print("rain sum: {0}".format(decoder_out['rain_sum'])) 244 | print("rain detector: {0}".format(decoder_out['rain_detect'])) 245 | if (decoder_out['sensor_type'] == 1) or (decoder_out['sensor_type'] == 4): 246 | # Thermo/Hygro 247 | print("humidity: {0}".format(decoder_out['humidity'])) 248 | if decoder_out['sensor_type'] == 4: 249 | # Thermo/Hygro/Baro 250 | print("pressure: {0}".format(decoder_out['pressure'])) 251 | 252 | print 253 | 254 | def expect_eon(self): 255 | # check end of nibble (1) 256 | if self.popbits(1) != 1: 257 | logging.warn("end of nibble is not 1") 258 | return False 259 | return True 260 | 261 | def main(): 262 | parser = argparse.ArgumentParser(description='Decoder for weather data of sensors from ELV received with RTL SDR.') 263 | parser.add_argument('--log', type=str, default='WARN', help='Log level: DEBUG|INFO|WARN|ERROR. Default: WARN') 264 | parser.add_argument('inputfile', type=str, nargs=1, help="Input file name. Expects a raw file with signed 16-bit samples in platform default byte order and 30 kHz sample rate. Use '-' to read from stdin. Example: rtl_fm -M am -f 868.35M -s 30k | ./decode_elv_wde1.py -") 265 | 266 | args = parser.parse_args() 267 | 268 | loglevel = args.log 269 | loglevel_num = getattr(logging, loglevel.upper(), None) 270 | if not isinstance(loglevel_num, int): 271 | raise ValueError('Invalid log level: ' + loglevel) 272 | logging.basicConfig(stream=sys.stderr, level=loglevel_num) 273 | 274 | dec = decoder() 275 | 276 | filename = args.inputfile[0] 277 | if filename == '-': 278 | filename = sys.stdin.fileno() 279 | fin = io.open(filename, mode="rb") 280 | b = fin.read(512) 281 | while len(b) == 512: 282 | values = struct.unpack('256h', b) 283 | for val in values: 284 | dec.process(val) 285 | b = fin.read(512) 286 | 287 | fin.close() 288 | 289 | if __name__ == '__main__': 290 | main() 291 | --------------------------------------------------------------------------------