This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revision | |||
| en:tech:rainsensor [2022/11/10 20:06] – bullar | en:tech:rainsensor [2026/08/12 13:39] (current) – claude | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Rain Gauge Sensor ====== | + | ====== |
| + | |||
| + | ===== Motivation ===== | ||
| + | |||
| + | The lush vegetation here in Thailand is thanks to the warmth but also to the sometimes heavy rainfall. Temperatures always stay in the range of +25...35°C, | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | Annual rainfall can reach up to 1600 mm/ | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | However, we haven' | ||
| + | |||
| + | ===== Sensors ===== | ||
| + | |||
| + | There' | ||
| + | |||
| + | A digital rain gauge uses a funnel to collect the rainwater and channel it onto a tipping bucket. Once one side fills up, the bucket tips, empties the full half, and the other side starts filling. Counting the number of tips gives a good measure of rainfall. Of course you need to know the funnel opening size and the capacity of the tipping bucket. You can actually build a sensor like this yourself — [[https:// | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | The sensor is battery-powered and transmits its readings on 433MHz, so it can be placed anywhere out in the garden. To use it for our own applications, | ||
| + | The funnel housing comes off easily. Underneath is the tipping bucket with the electronics housing behind it. A magnet in the tipping bucket triggers a reed switch inside. The battery compartment (2xAA) is accessible from below. | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | Inside there are two PCBs: one with the reed contact and a COB (Chip On Board), and the second is the 433MHz transmitter. | ||
| + | |||
| + | <WRAP group> | ||
| + | <WRAP half column> | ||
| + | {{ : | ||
| + | </ | ||
| + | <WRAP half column> | ||
| + | {{: | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | This separation makes it easy for us to tap the protocol going to the transmitter. | ||
| + | |||
| + | |||
| + | ===== Analysis ===== | ||
| + | |||
| + | There' | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | Zooming in further, the [[https:// | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | Let's take a look at what's actually being transmitted. | ||
| + | |||
| + | |||
| + | ===== SDR (Software Defined Radio) ===== | ||
| + | |||
| + | An affordable way to investigate the frequency range up to about 1766 MHz is a Software Defined Radio (SDR). That's more than enough for the rain sensor' | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | We don't need the remote or the driver CD; the antenna and the stick are enough for further analysis. The stick is automatically recognized on most PCs. All further investigation was done on macOS, but should work similarly on Windows. | ||
| + | |||
| + | For the stick we need the right analysis software. [[https:// | ||
| + | - [[https:// | ||
| + | - [[https:// | ||
| + | - [[https:// | ||
| + | - [[https:// | ||
| + | |||
| + | They' | ||
| + | First we look at the frequency spectrum around 433MHz to check whether the rain sensor is actually transmitting a signal. In URH we start the //Spectrum Analyzer...// | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | The blue block shows the signal over time (x=frequency, | ||
| + | |||
| + | <WRAP group> | ||
| + | <WRAP half column> | ||
| + | {{ : | ||
| + | </ | ||
| + | <WRAP half column> | ||
| + | {{: | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | The result now shows the recorded signal over time. We can see the eight repeating frame sections, and, zoomed in, the pulses and pauses already described in the digital signal earlier. We save this recording somewhere we can find again. | ||
| + | |||
| + | Now it's time to create a new project in URH so we can save the next steps too. Then we go to the // | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | Here too we find the pulse sequence with the two differently-timed pauses: '' | ||
| + | |||
| + | {{: | ||
| + | |||
| + | To use the built-in //Morse Code// decoding option, we first invert the signal with //Invert//. Then we need to set the Morse code parameters. After inversion we have exactly one '' | ||
| + | * Maximum length of 1-sequence for Low (0) = 4 | ||
| + | * Minimum length of 1-sequence for High (1) = 7 | ||
| + | * Number of 0s between 1-sequences (just for encoding) = 1 | ||
| + | With everything set correctly, the frame decodes without errors. The 8 frames now appear in the list. Each frame consists of 36 bits whose meaning we now have to figure out empirically. For that we create a new message type // | ||
| + | |||
| + | < | ||
| + | | ||
| + | 0000 0000 0011 1111 1111 2222 2222 2233 3333 | ||
| + | 0123 4567 8901 2345 6789 0123 4567 8901 2345 | ||
| + | |----|----|----|----|----|----|----|----|---- | ||
| + | aaaa aaaa RRbs tttt tttt tttt rrrr rrrr CCCC | ||
| + | LSB - MSB LSB ------ MSB LSB - MSB | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | | ||
| + | t: 12 bit signed temperature * 10 (eg: 23,1°C send as 231) | ||
| + | | ||
| + | | ||
| + | </ | ||
| + | |||
| + | With this knowledge in hand, we can now build the new receiver that can be integrated into [[https:// | ||
| + | |||
| + | |||
| + | ===== Hardware ===== | ||
| + | |||
| + | To get at the rain sensor' | ||
| + | |||
| + | <WRAP group> | ||
| + | <WRAP half column> | ||
| + | {{ : | ||
| + | </ | ||
| + | <WRAP half column> | ||
| + | {{: | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | Be sure to order the matching 433MHz helical antenna along with it. | ||
| + | |||
| + | The receiver module runs on 5V and provides a digital output that can be connected directly to an ESP module. Here's my solution on a piece of prototype board with an ESP8266 module, the RXB6 receiver, and a micro-USB connector. Since we have 5V USB sockets at every outlet in the house, we can do without an external power supply. | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | |||
| + | ===== Software (ESPHome) ===== | ||
| + | |||
| + | Now for the software. For integration into Home Assistant, ESPHome and a //custom component// are again the way to go. The following C program handles the decoding and provides four sensors: | ||
| + | * Rain Sensor | ||
| + | * Rain Temperature Sensor | ||
| + | * Lowbat Rain Sensor | ||
| + | * ID Rain Sensor | ||
| + | |||
| + | The pin the receiver output is connected to is defined via ''# | ||
| + | |||
| + | ++++ rf433_rain_sensor.h | | ||
| + | <file C rf433_rain_sensor.h> | ||
| + | include " | ||
| + | |||
| + | //#define DEBUG | ||
| + | //#define MSB | ||
| + | |||
| + | #ifdef DEBUG | ||
| + | #define debug_print(x, | ||
| + | #define debug_println(x, | ||
| + | #else | ||
| + | #define debug_print(x, | ||
| + | #define debug_println(x, | ||
| + | #endif | ||
| + | |||
| + | #define RX_PIN 3 | ||
| + | #define IS_START_PULSE(interval) | ||
| + | #define IS_MARK_PULSE(interval) | ||
| + | #define IS_LOW_PULSE(interval) | ||
| + | #define IS_HIGH_PULSE(interval) | ||
| + | #define IS_GLITCH(interval) (interval < 250) | ||
| + | |||
| + | enum sampling_state {WAIT_FOR_START, | ||
| + | |||
| + | static const char* TAG = " | ||
| + | bool have_data = false; | ||
| + | bool have_batt_data = false; | ||
| + | bool have_id_data = false; | ||
| + | bool lowbat = false; | ||
| + | int Temp = 0; | ||
| + | int Rain = 0; | ||
| + | int Id = 0; | ||
| + | |||
| + | // --------------------------------------------------------------------------- | ||
| + | class RF433_Rain_Sensor : public PollingComponent { | ||
| + | | ||
| + | Sensor *temp_sensor = new Sensor(); | ||
| + | Sensor *rain_sensor = new Sensor(); | ||
| + | |||
| + | RF433_Rain_Sensor() : PollingComponent(500) { } | ||
| + | |||
| + | float get_setup_priority() const override { return esphome:: | ||
| + | |||
| + | static void ICACHE_RAM_ATTR pin_ISR() { | ||
| + | |||
| + | static unsigned long last = 0; | ||
| + | static unsigned long micros_now; | ||
| + | static byte nibble[9] = {0, | ||
| + | static uint16_t pulse; | ||
| + | static byte sampling_state = WAIT_FOR_START; | ||
| + | static byte bits = 0; | ||
| + | static byte npos = 0; | ||
| + | static byte chksum = 0; | ||
| + | |||
| + | micros_now = micros(); | ||
| + | pulse = micros_now - last; | ||
| + | |||
| + | // ignore glitches | ||
| + | if (IS_GLITCH(pulse)) | ||
| + | pulse = 0; | ||
| + | else | ||
| + | last = micros_now; | ||
| + | |||
| + | // state machine for decoding on the fly | ||
| + | switch (sampling_state) { | ||
| + | case WAIT_FOR_START: | ||
| + | if (IS_START_PULSE(pulse)) | ||
| + | sampling_state = DATA_MARK; | ||
| + | bits = 0; | ||
| + | } | ||
| + | break; | ||
| + | case DATA_MARK: | ||
| + | if (IS_MARK_PULSE(pulse)) | ||
| + | sampling_state = DATA_BIT; | ||
| + | } else { | ||
| + | sampling_state = WAIT_FOR_START; | ||
| + | } | ||
| + | break; | ||
| + | case DATA_BIT: | ||
| + | // LSB | ||
| + | if (IS_LOW_PULSE(pulse)) | ||
| + | #ifdef MSB | ||
| + | nibble[bits/ | ||
| + | #else | ||
| + | nibble[bits/ | ||
| + | #endif | ||
| + | bits++; | ||
| + | sampling_state = DATA_MARK; | ||
| + | } else if (IS_HIGH_PULSE(pulse)) | ||
| + | #ifdef MSB | ||
| + | nibble[bits/ | ||
| + | #else | ||
| + | nibble[bits/ | ||
| + | #endif | ||
| + | bits++; | ||
| + | sampling_state = DATA_MARK; | ||
| + | } else { | ||
| + | sampling_state = WAIT_FOR_START; | ||
| + | } | ||
| + | if (bits == 36) sampling_state = DATA_RECEIVED; | ||
| + | break; | ||
| + | case DATA_RECEIVED: | ||
| + | chksum = 0; | ||
| + | for (npos=0; | ||
| + | chksum += nibble[npos]; | ||
| + | } | ||
| + | chksum &= 0x0F; | ||
| + | ESP_LOGD(TAG, | ||
| + | if (chksum == nibble[8]) { | ||
| + | Temp = (nibble[5]<< | ||
| + | Rain = (((nibble[2] & 0xC0)>> | ||
| + | have_data = true; | ||
| + | if ((nibble[2] & 0x8) > 0) { | ||
| + | lowbat = true; | ||
| + | } else { | ||
| + | lowbat = false; | ||
| + | } | ||
| + | have_batt_data = true; | ||
| + | Id = (nibble[1]<< | ||
| + | have_id_data = true; | ||
| + | } | ||
| + | bits = 0; | ||
| + | sampling_state = WAIT_FOR_START; | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ------------------------------------------------------------------------- | ||
| + | void setup() override { | ||
| + | // Prepare RF input pin | ||
| + | Serial.end(); | ||
| + | pinMode(RX_PIN, | ||
| + | attachInterrupt(RX_PIN, | ||
| + | } | ||
| + | |||
| + | // ------------------------------------------------------------------------- | ||
| + | void update() override { | ||
| + | // This is the actual sensor reading logic. | ||
| + | if (have_data) | ||
| + | { | ||
| + | temp_sensor-> | ||
| + | rain_sensor-> | ||
| + | have_data = false; | ||
| + | } | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | // --------------------------------------------------------------------------- | ||
| + | class Batt_RF433_Rain_Sensor : public PollingComponent, | ||
| + | public: | ||
| + | |||
| + | // constructor | ||
| + | Batt_RF433_Rain_Sensor() : PollingComponent(10000) {} | ||
| + | |||
| + | void setup() override { | ||
| + | // This will be called by App.setup() | ||
| + | } | ||
| + | |||
| + | void update() override { | ||
| + | if (have_batt_data) { | ||
| + | if (lowbat) { | ||
| + | publish_state(true); | ||
| + | } else { | ||
| + | publish_state(false); | ||
| + | } | ||
| + | have_batt_data = false; | ||
| + | } | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | // --------------------------------------------------------------------------- | ||
| + | class ID_RF433_Rain_Sensor : public PollingComponent, | ||
| + | public: | ||
| + | |||
| + | // constructor | ||
| + | ID_RF433_Rain_Sensor() : PollingComponent(10000) {} | ||
| + | |||
| + | void setup() override { | ||
| + | // This will be called by App.setup() | ||
| + | } | ||
| + | |||
| + | void update() override { | ||
| + | if (have_id_data) | ||
| + | { | ||
| + | publish_state(Id); | ||
| + | have_id_data = false; | ||
| + | } | ||
| + | } | ||
| + | }; | ||
| + | </ | ||
| + | ++++ | ||
| + | In ESPHome we create a new device and add our own code via an // | ||
| + | |||
| + | <code yaml> | ||
| + | esphome: | ||
| + | name: rain-sensor | ||
| + | includes: | ||
| + | - rf433_rain_sensor.h | ||
| + | </ | ||
| + | |||
| + | Then we define the four sensors as shown in the following example. | ||
| + | |||
| + | <code yaml> | ||
| + | sensor: | ||
| + | - platform: custom | ||
| + | lambda: |- | ||
| + | auto rf433_sensor_data = new RF433_Rain_Sensor(); | ||
| + | App.register_component(rf433_sensor_data); | ||
| + | return {rf433_sensor_data-> | ||
| + | |||
| + | sensors: | ||
| + | - name: "Rain Temperature Sensor" | ||
| + | unit_of_measurement: | ||
| + | accuracy_decimals: | ||
| + | - name: "Rain Sensor" | ||
| + | unit_of_measurement: | ||
| + | accuracy_decimals: | ||
| + | |||
| + | - platform: custom | ||
| + | lambda: |- | ||
| + | auto id_rf433_sensor_data = new ID_RF433_Rain_Sensor(); | ||
| + | App.register_component(id_rf433_sensor_data); | ||
| + | return {id_rf433_sensor_data}; | ||
| + | |||
| + | sensors: | ||
| + | - name: "ID Rain Sensor" | ||
| + | unit_of_measurement: | ||
| + | accuracy_decimals: | ||
| + | |||
| + | binary_sensor: | ||
| + | - platform: custom | ||
| + | lambda: |- | ||
| + | auto batt_rf433_rain_sensor = new Batt_RF433_Rain_Sensor(); | ||
| + | App.register_component(batt_rf433_rain_sensor); | ||
| + | return {batt_rf433_rain_sensor}; | ||
| + | |||
| + | binary_sensors: | ||
| + | - name: " | ||
| + | </ | ||
| + | |||
| + | After integration, | ||
| + | |||
| + | |||
| + | ===== Results ===== | ||
| + | |||
| + | Tested indoors, transmission works without any issues. An extended outdoor test is still pending, since the device is currently in Germany. | ||
| + | I'll add this part in 2023... | ||
| + | |||
| + | |||
| + | =====Links===== | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | * [[https:// | ||
| + | |||
| + | ===== Donate ===== | ||
| + | |||
| + | If you'd like to support my work, feel free to buy me a cappuccino or so: . | ||
| + | |||
| + | < | ||
| + | |||
| + | <form action=" | ||
| + | <input type=" | ||
| + | <input type=" | ||
| + | <img alt="" | ||
| + | </ | ||
| + | |||
| + | </ | ||
| - | Coming soon… | ||