├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------