|
| 1 | +/** @file |
| 2 | + Homelead HG9901 soil moisture/temp/light level sensor decoder. |
| 3 | +
|
| 4 | + Copyright (C) 2025 Boing <[email protected]>, \@inonoob |
| 5 | + and Christian W. Zuckschwerdt <[email protected]> |
| 6 | +
|
| 7 | + This program is free software; you can redistribute it and/or modify |
| 8 | + it under the terms of the GNU General Public License as published by |
| 9 | + the Free Software Foundation; either version 2 of the License, or |
| 10 | + (at your option) any later version. |
| 11 | +*/ |
| 12 | + |
| 13 | +#include "decoder.h" |
| 14 | + |
| 15 | +/** |
| 16 | +Homelead HG9901 soil moisture/temp/light level sensor decoder. |
| 17 | +
|
| 18 | +- Shenzhen Homelead Electronics Co., LTD. Wireless Soil Monitor HG9901, e.g. ASIN B0CRKN18C9 |
| 19 | + FCC ID: 2AAXF‐HG9901, Model No: HG01, https://fccid.io/2AAXF-HG9901 |
| 20 | +
|
| 21 | +Known rebrands: |
| 22 | +- Geevon T23033 / T230302 Soil Moisture/Temp/Light Level Sensor, ASIN B0D9Z9HLYD |
| 23 | + see #2977 by emmjaibi for excellent analysis |
| 24 | +- Dr.Meter soil sensor, ASIN B0CQKYTBC6 |
| 25 | +- Royal Gardineer ZX8859-944, ASIN B0DQTYYZK8 |
| 26 | +- Various other rebrands: Reyke, Vodeson, Midlocater, Kithouse, Vingnut |
| 27 | +- some unbranded sensors on AliEexpress |
| 28 | +
|
| 29 | +S.a. #2977 #3189 #3190 #3194 #3299 |
| 30 | +
|
| 31 | +This device is a simple garden temperature/moisture transmitter with a small LCD display for local viewing. |
| 32 | +
|
| 33 | +Example codes: |
| 34 | + raw {65}55aaee8ddae84fcf |
| 35 | + inverted {65}aa5513fd001630800 |
| 36 | +
|
| 37 | +The sensor will send a message every ~30 mins if no changes are measured. |
| 38 | +If changes are measured the sensor will instantly send messages. |
| 39 | +This might not happen if the changes have a matching checksum -- apparently that's the check used by the sensor. |
| 40 | +E.g. Moisture 62%, Temperature 23 C, Light Level: 4 |
| 41 | +matches Moisture 59%, Temperature 24 C, Light Level: 6. |
| 42 | +
|
| 43 | +The minimum battery voltage seems to be 1.18V. |
| 44 | +
|
| 45 | +# Data transmission |
| 46 | +
|
| 47 | +9 repeats of 433.92 MHz (EU region). |
| 48 | +Modulation is OOK PWM with 400/1200 us timing, inverted bits. |
| 49 | +
|
| 50 | +# Data Layout |
| 51 | +
|
| 52 | + PPPP PPPP PPPP PPPP IIII IIII IIII IIII MMMM MMMM STTT TTTT QQBB LLLL CCCC XXXXXXXX |
| 53 | +
|
| 54 | +- P = Preamble of 16 bits with 0xaa55 (inverted) |
| 55 | +- I = ID 16 bits, seems to survive battery changes |
| 56 | +- M = soil moisture 0-100% as an 8 bit integer |
| 57 | +- S = sign for temperature (0 for positive or 1 for negative) |
| 58 | +- T = Temperature as 7 bit integer ~0-100C |
| 59 | +- Q = 2 sequence bits |
| 60 | + - device sends message on CHS change ! |
| 61 | + - sequence: |
| 62 | + - S 00 initial phase duration 150 secs |
| 63 | + - S 01 interval timer 3 mins |
| 64 | + - S 02 interval timer 15 mins |
| 65 | + - S 03 interval timer 30 mins |
| 66 | +- B = battery status of 1 (1.22 V) to 3 (above 1.42 V), 0 so far has not been observed? |
| 67 | +- L = light level (9 states from LOW- to HIGH+) |
| 68 | + - 0 (LOW-) 0 |
| 69 | + - 1 (LOW) > 120 Lux |
| 70 | + - 2 (LOW+) > 250 Lux |
| 71 | + - 3 (NOR-) > 480 Lux |
| 72 | + - 4 (NOR) > 750 Lux |
| 73 | + - 5 (NOR+) >1200 Lux |
| 74 | + - 6 (HIGH-) >1700 Lux |
| 75 | + - 7 (HIGH) >3800 Lux |
| 76 | + - 8 (HIGH+) >5200 Lux, max should be 15000 Lux |
| 77 | +- C = 4 bit checksum |
| 78 | +- X = Trailer of 8 bits equal to 0xf8 , can be ignored |
| 79 | +
|
| 80 | +Note: Device drifts in direct sun and shows up to 12C offset. |
| 81 | +Note: Device is NOT waterproof (IP27), don't immerse in water. |
| 82 | +Note: Uses one AA battery AA or rechargeable cell, lasts for up to: 18 months. |
| 83 | +*/ |
| 84 | +static int homelead_hg9901_decoder(r_device *decoder, bitbuffer_t *bitbuffer) |
| 85 | +{ |
| 86 | + uint8_t const preamble[] = {0x55, 0xaa}; |
| 87 | + // Rough estimate of Lux values for light levels 0 - 8. |
| 88 | + int const lux_estimate[] = {60, 200, 400, 600, 1000, 1500, 2800, 4500, 10000, -1, -1, -1, -1, -1, -1, -1}; |
| 89 | + |
| 90 | + int row = bitbuffer_find_repeated_row(bitbuffer, 1, 65); // expected are 12 repeats but 1 is enough |
| 91 | + if (row < 0) { |
| 92 | + return DECODE_ABORT_EARLY; // no good row found |
| 93 | + } |
| 94 | + |
| 95 | + // Check that bits_per_row is 65 or a few bits more |
| 96 | + unsigned row_len = bitbuffer->bits_per_row[row]; |
| 97 | + if (row_len > 65 + 8) { |
| 98 | + return DECODE_ABORT_EARLY; // wrong Data Length (must be 65) |
| 99 | + } |
| 100 | + |
| 101 | + // Search preamble |
| 102 | + unsigned pos = bitbuffer_search(bitbuffer, row, 0, preamble, 16); |
| 103 | + if (pos + 65 > row_len) { |
| 104 | + return DECODE_ABORT_LENGTH; // preamble not found or packet truncated |
| 105 | + } |
| 106 | + |
| 107 | + // Invert data |
| 108 | + bitbuffer_invert(bitbuffer); |
| 109 | + |
| 110 | + uint8_t *b = bitbuffer->bb[row]; |
| 111 | + |
| 112 | + // Nibble-wide checksum validation |
| 113 | + int chk = (b[7] & 0xf0) >> 4; |
| 114 | + int sum = add_nibbles(b, 7) & 0x0f; |
| 115 | + |
| 116 | + if (sum != chk) { |
| 117 | + return DECODE_FAIL_MIC; // Checksum mismatch |
| 118 | + } |
| 119 | + |
| 120 | + int id = (b[2] << 8) | b[3]; |
| 121 | + int moisture = b[4]; |
| 122 | + int t_sign = (b[5] & 0x80) >> 7; |
| 123 | + int temperature = b[5] & 0x7f; |
| 124 | + int sequence = (b[6] & 0xc0) >> 6; |
| 125 | + int batt_lvl = (b[6] & 0x30) >> 4; |
| 126 | + int light_lvl = (b[6] & 0x0f); |
| 127 | + int light_lux = lux_estimate[light_lvl]; |
| 128 | + |
| 129 | + if (t_sign) { |
| 130 | + temperature = (0 - temperature); |
| 131 | + } |
| 132 | + |
| 133 | + /* clang-format off */ |
| 134 | + data_t *data = data_make( |
| 135 | + "model", "Model", DATA_STRING, "Homelead-HG9901", |
| 136 | + "id", "ID", DATA_FORMAT, "%04X", DATA_INT, id, |
| 137 | + "battery_ok", "Battery", DATA_INT, batt_lvl > 1, // Level 1 means "Low" |
| 138 | + "battery_pct", "Battery level", DATA_INT, 100 * batt_lvl / 3, // Note: this might change with #3103 |
| 139 | + "temperature_C", "Temperature", DATA_FORMAT, "%.0f C", DATA_DOUBLE, (double)temperature, |
| 140 | + "moisture", "Moisture", DATA_FORMAT, "%d %%", DATA_INT, moisture, |
| 141 | + "light_lvl", "Light level", DATA_INT, light_lvl, |
| 142 | + "light_lux", "Light", DATA_FORMAT, "%d lux", DATA_INT, light_lux, |
| 143 | + "sequence", "TX Sequence", DATA_INT, sequence, |
| 144 | + "mic", "Integrity", DATA_STRING, "CHECKSUM", |
| 145 | + NULL); |
| 146 | + /* clang-format on */ |
| 147 | + |
| 148 | + decoder_output_data(decoder, data); |
| 149 | + bitbuffer_invert(bitbuffer); // FIXME: DEBUG, remove this once account_event() is fixed |
| 150 | + return 1; |
| 151 | +} |
| 152 | + |
| 153 | +static char const *const output_fields[] = { |
| 154 | + "model", |
| 155 | + "id", |
| 156 | + "battery_ok", |
| 157 | + "battery_pct", |
| 158 | + "temperature_C", |
| 159 | + "moisture", |
| 160 | + "light_lvl", |
| 161 | + "light_lux", |
| 162 | + "sequence", |
| 163 | + "mic", |
| 164 | + NULL, |
| 165 | +}; |
| 166 | + |
| 167 | +r_device const homelead_hg9901 = { |
| 168 | + .name = "Homelead HG9901 (Geevon, Dr.Meter, Royal Gardineer) soil moisture/temp/light level sensor", |
| 169 | + .modulation = OOK_PULSE_PWM, |
| 170 | + .short_width = 432, // gap is 1000 |
| 171 | + .long_width = 1228, // gap is 230 |
| 172 | + .gap_limit = 2000, // packet gap is 3700 |
| 173 | + .reset_limit = 4500, |
| 174 | + .decode_fn = &homelead_hg9901_decoder, |
| 175 | + .fields = output_fields, |
| 176 | +}; |
0 commit comments