Skip to content

Moisture sensor HG9901 - Rebranded from Amazon #3190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
inonoob opened this issue Feb 7, 2025 · 52 comments
Open

Moisture sensor HG9901 - Rebranded from Amazon #3190

inonoob opened this issue Feb 7, 2025 · 52 comments

Comments

@inonoob
Copy link

inonoob commented Feb 7, 2025

Dear all,

I have a new challenge. I recently bought some sensors for checking the moisture and temperature of the garden.

I bought a rebranded version from amazon but with the FCCid, I could find the real supplier.

https://fccid.io/2AAXF-HG9901

https://www.amazon.de/dp/B0CRKN18C9

Sensor has the following data which is tracked:

  • Temperature
  • Moisture
  • Light

It is working on the 433Mhz range. I manage to get a capture of the signal and could create a decoder:

rtl_433 -t biastee -X 'n=name,m=OOK_PWM,s=600,l=1392,r=3808,g=1056,t=318,y=0, preamble=55aa'

I use a RTL_SDR version 4 with an adapter therefore I had the biastee activated.

signal.zip

With the data and my past experience, my feeling say the 55aa needs to be inverted and is the preamble. I tried bitbench but I'm not yet there.

Does anyone have a better idea how to decode. I wasn't sure about the 8 at the end if it is CRC ?

I already had some signal dumps:

55aa 3006fd7e0f4f8
55aa 3006ffff0fff8
55aa 30069bff0f5f8
55aa 30069bff0f5f8
55aa 30069bfe0f4f8
55aa 3006ffff0fff8
55aa 3006ffff0fff8
55aa 30069bfe0c1f8
55aa 3006fffe0cbf8
55aa 3006fffe0cbf8
55aa 30069bfe0c1f8
55aa 30069bfe0c1f8
55aa 3006fffe0cbf8
55aa 3006fffe0cbf8
55aa 30069bfe0c1f8
55aa 3006fffe0cbf8
55aa 7f29f3fb0b3f8
55aa 7f29f3fb0b3f8
55aa 7f29f3fb080f8
55aa 7f29f3fa07ef8
55aa 7f29ddfc0f0f8
55aa 3006e4ff0f3f8
55aa 3006e4ff0f3f8
55aa 3006e4ff0f3f8
55aa 3006e47e0faf8
55aa 3006e4ff0f3f8
55aa 3006e4ff0f3f8

Many thanks for the support. This is only the second sensor I try to decode.

best regards

@inonoob
Copy link
Author

inonoob commented Feb 7, 2025

Some more infos:

55aa3006e4ff0f3f8
55aa7f29defc0f1f8

inverted and  preamble removed

cff91b00f0c07
80d62103f0e07

I made progress: 

SENSOR_ID:hhhh HUM:hh TEMP:hh h LIGHT:hh hh

SENSOR_ID: cff9 HUM:1b TEMP:00 f 0c (LOW_) 07
SENSOR_ID: 80d6 HUM:21 TEMP:03 f 0e (LOW_)  07

new data point: 

55aa7f29e0fb07bf8

SENSOR_ID:80d6 HUM:1f TEMP:04 f LIGHT:84 (HIGH+) 07

55aa3006e6fd07bf8

SENSOR_ID:cff9 HUM:19 TEMP:02 f LIGHT:84 (HIGH+) 07

According to the monitor:
Sensor 1: (80d6)
Moisture: 33%
light: low_
temp: 3°C

Sensor 2: (cff9)
Moisture: 27%
light: low_
temp: 0°C

Sensor 1: (80d6)
Moisture: 31%
light: HIGH+
temp: 4°C

Sensor 2: (cff9)
Moisture: 25%
light: HIGH+
temp: 2°C

I think the last info missing is the "f" , the "light" part and the final "07". I don't have a clue what those are and how the light is calculated.

But we are close to fully decode the sensor

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

Hey,

So the display also show battery level and signal strength. It might be that the "f" could indicate the battery level.

Additional note: The sensors will only send data if the sensor register a change in values. It is not time based but status based

BR

@zuckschwerdt
Copy link
Collaborator

zuckschwerdt commented Feb 8, 2025

Copy some decoder as template, change it (at least the r_device name), edit include/rtl_433_devices.h, add your files with git (no need to commit yet), run ./maintainer_update.py (for the compile rules), then compile, add files again, run ./maintainer_update.py for the readme files.

There is now also https://triq.org/rtl_433/CONTRIBUTING.html#adding-a-new-decoder

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

Hey,

I just found out in Reddit. Some people are also working on it but the Moisture sensor is called differently. At the end it is still a HG9901:

https://www.reddit.com/r/RTLSDR/comments/1ce3fef/anyone_played_with_decoding_bigtride_soil_sensors/

But I was right with my research.

Good Thing, I could practice again how to decode a sensor :D

BR

@zuckschwerdt
Copy link
Collaborator

Always a good puzzle and nice to have independent confirmation.
#3189 is also asking for a decoder for likely the same device.

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

@zuckschwerdt could you please help again. I'm don't know C at all. I finish to edit a hg9901.c file. When I add it to the rtl_433_devices.h. I get the following error when i try to compile:

My rtl_433_devices.h looks like that:

…
    DECL(hg9901) \
    /* Add new decoders here. */
…
/usr/bin/ld: libr_433.a(r_api.c.o):r_api.c:(.text+0xadb4): more undefined references to `hg9901' follow

@zuckschwerdt
Copy link
Collaborator

Did you run ./maintainer_update.py to change the src/CMakeLists.txt?

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

So

I have the code ready but, I fail currently to use the ./maintainer_update.py

hg9901.c 
/** @file
    Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut.

    Contributed by Inonoob

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
*/

#include "decoder.h"

/**
Moisture Sensor HG9901 - Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut..

This device is a simple garden temperature/moisture transmitter with a small LCD display for local viewing.

Data layout:

    Byte 0   Byte 1   Byte 2   Byte 3   Byte 4   Byte 5   Byte 6   Byte 7   Byte 8
     55	      aa       30       06       e4        ff      0f        3f       8 

Format string:

    SENSOR_ID:hhhh HUM:hh TEMP:hh h LIGHT:hh hh

Example packets:

    55aa3006e4ff0f3f8
    55aa7f29defc0f1f8

invert 

 cff91b00f0c07
 80d62103f0e07


Result

SENSOR_ID: cff9 HUM:1b TEMP:00 f 0c 07
SENSOR_ID: 80d6 HUM:21 TEMP:03 f 0e 07

*/

static int hg9901_callback(r_device *decoder, bitbuffer_t *bitbuffer)
{
    // invert all the bits
    bitbuffer_invert(bitbuffer);

    // find the most common row, nominal we expect 5 packets
    int r = bitbuffer_find_repeated_prefix(bitbuffer, bitbuffer->num_rows > 5 ? 5 : 3, 67);
    if (r < 0) {
        return DECODE_ABORT_LENGTH;
    }

    // work with the best/most repeated capture
    uint8_t *b = bitbuffer->bb[r];

    // Check if the packet has the correct number of bits
    if (bitbuffer->bits_per_row[r] != 68) {
        return DECODE_ABORT_LENGTH;
    }

    // Check if the fixed bytes are correct
    if (b[5] != 0xAA || b[6] != 0x55 || b[7] != 0xAA) {
        return DECODE_FAIL_MIC;
    }


    // Extract the data from the packet
    int sensor_id = (b[0] << 8) | b[1];
    int humidity = b[2];
    int temp_raw = b[3];
    int battery_low = (b[4] >> 7);
    int light_intensity = b[5];

    // Store the decoded data
    /* clang-format off */
    data_t *data = data_make(
            "model",            "",             DATA_STRING, "HG9901",
            "sensor_id",        "Sensor_id",    DATA_INT,    sensor_id,
            "battery_ok",       "Battery",      DATA_INT,    !battery_low,
            "temperature_C",    "Temperature",  DATA_FORMAT, "%u C", DATA_INT, temp_raw,
            "humidity",         "Humidity",     DATA_FORMAT, "%u %%", DATA_INT, humidity,
            "light",            "Light Intensity",    DATA_FORMAT, "%u ", DATA_INT, light_intensity,
            NULL);
    /* clang-format on */

    decoder_output_data(decoder, data);
    return 1;
}

static char const *const output_fields[] = {
        "model",
        "sensor_id",
        "battery_ok",
        "temperature_C",
        "humidity",
        "light",
        NULL,
};

r_device const HG9901 = {
        .name        = "HG9901 Moisture sensor",
        .modulation  = OOK_PULSE_PWM,
        .short_width = 600,
        .long_width  = 1392,
        .gap_limit   = 1056,  // long gap (with short pulse) is ~472 us, sync gap is ~728 us
        .reset_limit = 3808, // maximum gap is 1250 us (long gap + longer sync gap on last repeat)
        .decode_fn   = &hg9901_callback,
        .fields      = output_fields,
};


@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

I did follow the manual:

There is now also https://triq.org/rtl_433/CONTRIBUTING.html#adding-a-new-decoder

I'm stuck at this point of the tutorial after adding the git add command. Maintainer is complaining

Add your files with Git (no need to commit yet)
E.g. git add src/devices/my_device.c include/rtl_433_devices.h

./maintainer_update.py
Please commit or stash your changes.

What did I do wrong ?


sudo cmake --build build --target install
[ 93%] Built target r_433
[ 93%] Linking C executable rtl_433
/usr/bin/ld: libr_433.a(r_api.c.o): warning: relocation against `hg9901' in read-only section `.text'
/usr/bin/ld: libr_433.a(r_api.c.o): in function `r_init_cfg':
r_api.c:(.text+0xad3d): undefined reference to `hg9901'
/usr/bin/ld: r_api.c:(.text+0xad4d): undefined reference to `hg9901'
/usr/bin/ld: r_api.c:(.text+0xad9d): undefined reference to `hg9901'
/usr/bin/ld: r_api.c:(.text+0xada4): undefined reference to `hg9901'
/usr/bin/ld: r_api.c:(.text+0xadac): undefined reference to `hg9901'
/usr/bin/ld: libr_433.a(r_api.c.o):r_api.c:(.text+0xadb4): more undefined references to `hg9901' follow
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
gmake[2]: *** [src/CMakeFiles/rtl_433.dir/build.make:100: src/rtl_433] Error 1
gmake[1]: *** [CMakeFiles/Makefile2:977: src/CMakeFiles/rtl_433.dir/all] Error 2
gmake: *** [Makefile:146: all] Error 2


Still get compile errors.

@zuckschwerdt
Copy link
Collaborator

You need to re-add with git add src/devices/my_device.c before every ./maintainer_update.py.
Also you need to add all other changed files, get an overview with git status

@zuckschwerdt
Copy link
Collaborator

Oh and hg9901_callback should be renamed to hg9901_decode (twice) and important:
r_device const HG9901 = { needs to be r_device const hg9901 = {, same spelling as in DECL(hg9901) \

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

@zuckschwerdt thank you I did manage to compile it !! Now I can improve the code

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

Thank you again for your patience !!

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

:( my sensor code is not working.

So transition from found encoding to getting it seen by rtl_433 by the decoder

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

Someone with better coding experience could take a look. I'm having some issues.

Please see below my current code.

hg9901.c 
/** @file
    Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut.

    Contributed by Inonoob

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
*/

#include "decoder.h"

/**
Moisture Sensor HG9901 - Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut..

This device is a simple garden temperature/moisture transmitter with a small LCD display for local viewing.

Data layout:

    Byte 0   Byte 1   Byte 2   Byte 3   Byte 4   Byte 5   Byte 6   Byte 7   Byte 8
     55	      aa       30       06       e4        ff      0f        3f       8 

Format string:

    SENSOR_ID:hhhh HUM:hh TEMP:hh h LIGHT:hh hh

Example packets:

    55aa3006e4ff0f3f8
    55aa7f29defc0f1f8

invert 

 cff91b00f0c07
 80d62103f0e07


Result

SENSOR_ID: cff9 HUM:1b TEMP:00 f 0c 07
SENSOR_ID: 80d6 HUM:21 TEMP:03 f 0e 07

*/

static int hg9901_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
    // invert all the bits
    bitbuffer_invert(bitbuffer);

    // find the most common row, nominal we expect 5 packets
    int r = bitbuffer_find_repeated_prefix(bitbuffer, bitbuffer->num_rows > 5 ? 5 : 3, 67);
    if (r < 0) {
        return DECODE_ABORT_LENGTH;
    }

    // work with the best/most repeated capture
    uint8_t *b = bitbuffer->bb[r];

    // Check if the packet has the correct number of bits
    if (bitbuffer->bits_per_row[r] != 68) {
        return DECODE_ABORT_LENGTH;
    }

    // Check if the fixed bytes are correct
    if (b[5] != 0xAA || b[6] != 0x55 || b[7] != 0xAA) {
        return DECODE_FAIL_MIC;
    }


    // Extract the data from the packet
    int sensor_id = (b[0] << 8) | b[1];
    int humidity = b[2];
    int temp_raw = b[3];
    int battery_low = (b[4] >> 7);
    int light_intensity = b[5];

    // Store the decoded data
    /* clang-format off */
    data_t *data = data_make(
            "model",            "",             DATA_STRING, "HG9901",
            "sensor_id",        "Sensor_id",    DATA_INT,    sensor_id,
            "battery_ok",       "Battery",      DATA_INT,    !battery_low,
            "temperature_C",    "Temperature",  DATA_FORMAT, "%u C", DATA_INT, temp_raw,
            "humidity",         "Humidity",     DATA_FORMAT, "%u %%", DATA_INT, humidity,
            "light",            "Light Intensity",    DATA_FORMAT, "%u ", DATA_INT, light_intensity,
            NULL);
    /* clang-format on */

    decoder_output_data(decoder, data);
    return 1;
}

static char const *const output_fields[] = {
        "model",
        "sensor_id",
        "battery_ok",
        "temperature_C",
        "humidity",
        "light",
        NULL,
};

r_device const hg9901 = {
        .name        = "HG9901 Moisture sensor",
        .modulation  = OOK_PULSE_PWM,
        .short_width = 600,
        .long_width  = 1392,
        .gap_limit   = 1056,  // long gap (with short pulse) is ~472 us, sync gap is ~728 us
        .reset_limit = 3808, // maximum gap is 1250 us (long gap + longer sync gap on last repeat)
        .decode_fn   = &hg9901_decode,
        .fields      = output_fields,
};

@zuckschwerdt
Copy link
Collaborator

Maybe add decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "Before invert"); and after invert etc.

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

Hey just checked,

rtl_433 -vvv -R 275
rtl_433 version 24.10-56-g14327e65 branch feat-mydevice at 202502081356 inputs file rtl_tcp RTL-SDR
Registering protocol [275] "HG9901 Moisture sensor"
[Protocols] Registered 1 out of 275 device decoding protocols
[Input] The internals of input handling changed, read about and report problems on PR #1978
[SDR] Found 1 device(s)
[SDR] trying device 0: RTLSDRBlog, Blog V4, SN: 00000001
Found Rafael Micro R828D tuner
RTL-SDR Blog V4 Detected
[SDR] Using device 0: RTLSDRBlog, Blog V4, SN: 00000001, "Generic RTL2832U OEM"
Exact sample rate is: 250000.000414 Hz
[SDR] Sample rate set to 250000 S/s.
[Input] Bit detection level set to 0.0 (Auto).
[SDR] Tuner gain set to Auto.
[Input] Reading samples in async mode...
[SDR] rtlsdr_set_center_freq 433920000 = 0
[SDR] Tuned to 433.920MHz.
[acquire_thread] acquire_thread enter...
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {65}aa5580d6001530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {65}aa5580d6001530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {32}aa5580d6, {33}001530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {65}aa5580d6001530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {32}aa5580d6, {8}00, {25}1530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {65}aa5580d6001530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {65}aa5580d6001530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {32}aa5580d6, {33}001530200, {32}aa5580d6, {33}001530200
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {65}aa5580d6001530200, {65}aa5580d6001530200

So baby steps. But still can show the normal way RTL_433 normally shows it with the decoding of the infos

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

I have my first version working !!!

Still open points:

  • only take from bitbuffer if it contain only one code and is at least 65 long. It might happend that more than 1 packages is in the bitbuffer. This could be improved.
  • Battery part of the code still not sure needs rework. Battery part still no 100% understood
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-08 21:02:38
model     : HG9901       Sensor_id : 32982         Battery   : 1             Temperature: 23 C
Humidity  : 0 %          Light Intensity: 64 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-08 21:02:38
model     : HG9901       Sensor_id : 32982         Battery   : 1             Temperature: 23 C
Humidity  : 0 %          Light Intensity: 64 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-08 21:02:38
model     : HG9901       Sensor_id : 32982         Battery   : 1             Temperature: 23 C
Humidity  : 0 %          Light Intensity: 64 

/** @file
    Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut.

    Contributed by Inonoob

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
*/

/**
Moisture Sensor HG9901 - Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut..

This device is a simple garden temperature/moisture transmitter with a small LCD display for local viewing.

Data layout:

    Byte 0   Byte 1   Byte 2   Byte 3   Byte 4   Byte 5   Byte 6   Byte 7   Byte 8
     55	      aa       30       06       e4        ff      0f        3f       8 

Format string:

    SENSOR_ID:hhhh HUM:hh TEMP:hh h LIGHT:hh hh

Example packets:

    55aa3006e4ff0f3f8
    55aa7f29defc0f1f8

invert 

 cff91b00f0c07
 80d62103f0e07



Result

SENSOR_ID: cff9 HUM:1b TEMP:00 f 0c 07
SENSOR_ID: 80d6 HUM:21 TEMP:03 f 0e 07

*/

#include "decoder.h"

static int hg9901_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
    // Ensure that the length of the packet is correct (68 bits)
    if (bitbuffer->num_rows < 1 || bitbuffer->bits_per_row[0] != 65) {
        return DECODE_ABORT_LENGTH;
    }

    // Check if the first two bytes are the correct preamble (0x55 0xAA)
    uint8_t *b = bitbuffer->bb[0];
    if (b[0] != 0x55 || b[1] != 0xAA) {
        return DECODE_FAIL_MIC; // Invalid preamble
    }

    decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After preamble check");

    // Invert the bits of the entire packet (only if valid)
    bitbuffer_invert(bitbuffer);
    decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After invert");

    // Extract the data from the packet (after inversion)
    int sensor_id = (b[2] << 8) | b[3]; // Sensor ID from bytes 2 and 3
    int humidity = b[4];                 // Humidity from byte 4
    int temp_raw = b[5];                 // Temperature from byte 5
    int battery_low = (b[6] >> 7);      // Battery Low status from MSB of byte 6
    int light_intensity = b[7];         // Light Intensity from byte 7

    // Store the decoded data and output it
    data_t *data = data_make(
            "model",            "",             DATA_STRING, "HG9901",
            "sensor_id",        "Sensor_id",    DATA_INT,    sensor_id,
            "battery_ok",       "Battery",      DATA_INT,    !battery_low,
            "temperature_C",    "Temperature",  DATA_FORMAT, "%u C", DATA_INT, temp_raw,
            "humidity",         "Humidity",     DATA_FORMAT, "%u %%", DATA_INT, humidity,
            "light",            "Light Intensity", DATA_FORMAT, "%u ", DATA_INT, light_intensity,
            NULL);

    decoder_output_data(decoder, data);

    return 1;  // Successful decoding
}

static char const *const output_fields[] = {
        "model",
        "sensor_id",
        "battery_ok",
        "temperature_C",
        "humidity",
        "light",
        NULL,
};

r_device const hg9901 = {
        .name        = "HG9901 Moisture sensor",
        .modulation  = OOK_PULSE_PWM,
        .short_width = 600,
        .long_width  = 1392,
        .gap_limit   = 1056,  // long gap (with short pulse) is ~472 us, sync gap is ~728 us
        .reset_limit = 3808, // maximum gap is 1250 us (long gap + longer sync gap on last repeat)
        .decode_fn   = &hg9901_decode,
        .fields      = output_fields,
};

@inonoob
Copy link
Author

inonoob commented Feb 8, 2025

Fixed that issue. Last challenge the battery

/** @file
    Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut.

    Contributed by Inonoob

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
*/


/**
Moisture Sensor HG9901 - Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut..

This device is a simple garden temperature/moisture transmitter with a small LCD display for local viewing.

Data layout:

    Byte 0   Byte 1   Byte 2   Byte 3   Byte 4   Byte 5   Byte 6   Byte 7   Byte 8
     55	      aa       30       06       e4        ff      0f        3f       8 

Format string:

    SENSOR_ID:hhhh HUM:hh TEMP:hh h LIGHT:hh hh

Example packets:

    55aa3006e4ff0f3f8
    55aa7f29defc0f1f8

invert 

 cff91b00f0c07
 80d62103f0e07


Result

SENSOR_ID: cff9 HUM:1b TEMP:00 f 0c 07
SENSOR_ID: 80d6 HUM:21 TEMP:03 f 0e 07

*/

#include "decoder.h"

static int hg9901_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
    // Iterate through all rows to find a valid 65-bit row
    for (int i = 0; i < bitbuffer->num_rows; ++i) {
        uint8_t *b = bitbuffer->bb[i];

        // Ensure the current row length is 65 bits
        if (bitbuffer->bits_per_row[i] != 65) {
            continue;  // Skip this row if the length is not 65 bits
        }

        // Check if the first two bytes are the correct preamble (0x55 0xAA)
        if (b[0] != 0x55 || b[1] != 0xAA) {
            continue;  // Skip this row if the preamble is invalid
        }



    decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After preamble check");

    // Invert the bits of the entire packet (only if valid)
    bitbuffer_invert(bitbuffer);
    decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After invert");

    // Extract the data from the packet (after inversion)
    int sensor_id = (b[2] << 8) | b[3]; // Sensor ID from bytes 2 and 3
    int humidity = b[4];                 // Humidity from byte 4
    int temp_raw = b[5];                 // Temperature from byte 5
    int battery_low = (b[6] >> 7);      // Battery Low status from MSB of byte 6
    int light_intensity = b[7];         // Light Intensity from byte 7

    // Store the decoded data and output it
    data_t *data = data_make(
            "model",            "",             DATA_STRING, "HG9901",
            "sensor_id",        "Sensor_id",    DATA_INT,    sensor_id,
            "battery_ok",       "Battery",      DATA_INT,    !battery_low,
            "temperature_C",    "Temperature",  DATA_FORMAT, "%u C", DATA_INT, temp_raw,
            "humidity",         "Humidity",     DATA_FORMAT, "%u %%", DATA_INT, humidity,
            "light",            "Light Intensity", DATA_FORMAT, "%u ", DATA_INT, light_intensity,
            NULL);

    decoder_output_data(decoder, data);

    return 1;  // Successful decoding
    
  }
  return DECODE_ABORT_LENGTH; // If no valid row found, return an error
}  // <-- Close function here properly

static char const *const output_fields[] = {
        "model",
        "sensor_id",
        "battery_ok",
        "temperature_C",
        "humidity",
        "light",
        NULL,
};

r_device const hg9901 = {
        .name        = "HG9901 Moisture sensor",
        .modulation  = OOK_PULSE_PWM,
        .short_width = 600,
        .long_width  = 1392,
        .gap_limit   = 1056,  // long gap (with short pulse) is ~472 us, sync gap is ~728 us
        .reset_limit = 3808, // maximum gap is 1250 us (long gap + longer sync gap on last repeat)
        .decode_fn   = &hg9901_decode,
        .fields      = output_fields,
};

@inonoob
Copy link
Author

inonoob commented Feb 9, 2025

Hey again,

So I can read my both sensors but according to my readings my soil is boiling :D.

I need to see how to fix it. 0x84 ==> -4°C.

And Battery part still looks fishy from the data. .

time      : 2025-02-09 07:55:55
model     : HG9901       Sensor_id : 53241         Battery   : 0             Temperature: 132 C        soil moisture: 61 %       Light Intensity: 240 
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {3}0, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {65}aa55cff93d84f3f00, {61}aa55cff93d84f3f0
[hg9901_decode] After preamble check
codes     : {3}e, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {30}55aa6f7c, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {65}55aa7f29bafd4def8, {61}55aa7f29bafd4de8
[hg9901_decode] After invert
codes     : {3}0, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {30}aa559080, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {61}aa5580d64502b210
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
time      : 2025-02-09 07:56:54
model     : HG9901       Sensor_id : 32982         Battery   : 0             Temperature: 2 C          soil moisture: 69 %       Light Intensity: 16 
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {3}0, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {30}aa559080, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {65}aa5580d64502b2100, {61}aa5580d64502b210

@merbanan
Copy link
Owner

merbanan commented Feb 9, 2025

Bit 8 must indicate negative values somehow. You need to log the real temp and the hex values so we can figure out the encoding.

@inonoob
Copy link
Author

inonoob commented Feb 9, 2025

Thanks for the feedback,

I got the following numbers:

00 ==> 0
02 ==> 2
03 ==> 3
82 ==> -2
84 ==> -4

aa55 cff9 3d 82 f 60 00

Preamble: aa55
ID: cff9
3d: soil moisutre
82 : temp here -2
f : battery ?
60: light intensity
00: no idea

And Battery part seems off. It might battery %

@ProfBoc75
Copy link
Collaborator

@inonoob : yes your temp is signed value by the first bit.

Add this 2 lines before the data_make :

if (temp_raw >= 128)
    temp_raw = 128 - temp_raw;

@merbanan
Copy link
Owner

merbanan commented Feb 9, 2025

82 ==> -2
84 ==> -4

Just mask out bit 8 and if set multiply the rest with -1.
temp = (b[x] & 0x7F)
if (b[x] >> 7)
temp = temp * -1

@inonoob
Copy link
Author

inonoob commented Feb 9, 2025

The latest version:

#include "decoder.h"

static int hg9901_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
    // Loop through all rows in the bitbuffer to process all potential packets
    for (int i = 0; i < bitbuffer->num_rows; ++i) {
        uint8_t *b = bitbuffer->bb[i];

        // Ensure that the length of the packet is correct (65 bits)
        if (bitbuffer->bits_per_row[i] != 65) {
            continue;  // Skip this row if the length is not correct
        }

        // Check if the first two bytes are the correct preamble (0x55 0xAA)
        if (b[0] != 0x55 || b[1] != 0xAA) {
            continue;  // Skip this row if the preamble is invalid
        }

        decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After preamble check");

        // Invert the bits of the entire packet (only if valid)
        bitbuffer_invert(bitbuffer);

        decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After invert");

        // Extract the data from the packet (after inversion)
        int sensor_id = (b[2] << 8) | b[3]; // Sensor ID from bytes 2 and 3
        int soil_moisture = b[4];                 // Humidity from byte 4
        int temp_raw = b[5];                 // Temperature from byte 5
        int battery_low = b[6] & 0xF;      // Battery Low status from MSB of byte 6
        int light_intensity = b[7];         // Light Intensity from byte 7

        if (temp_raw >= 128)
            temp_raw = 128 - temp_raw;


        // Store the decoded data and output it
        data_t *data = data_make(
                "model",            "",             DATA_STRING, "HG9901",
                "sensor_id",        "Sensor_id",    DATA_INT,    sensor_id,
                "battery_ok",       "Battery",      DATA_INT,    !battery_low,
                "temperature_C",    "Temperature",  DATA_FORMAT, "%u C", DATA_INT, temp_raw,
                "soil moisture",    "soil moisture",DATA_FORMAT, "%u %%", DATA_INT, soil_moisture,
                "light",            "Light Intensity", DATA_FORMAT, "%u ", DATA_INT, light_intensity,
                NULL);

        decoder_output_data(decoder, data);

        return 1;  // Successfully decoded one valid packet and returned
    }

    return DECODE_ABORT_LENGTH;  // No valid packet found
}

static char const *const output_fields[] = {
        "model",
        "sensor_id",
        "battery_ok",
        "temperature_C",
        "humidity",
        "light",
        NULL,
};

r_device const hg9901 = {
        .name        = "HG9901 Moisture sensor",
        .modulation  = OOK_PULSE_PWM,
        .short_width = 600,
        .long_width  = 1392,
        .gap_limit   = 1056,  
        .reset_limit = 3808, 
        .decode_fn   = &hg9901_decode,
        .fields      = output_fields,
};


@inonoob
Copy link
Author

inonoob commented Feb 9, 2025

Hey all,

so code is running ! But I'm not sure about the battery support.

For the presume battery status I did see the following numbers: 0x3, 0x7, 0xb, 0xf. It could be the percentage of the battery.

I also need to see if my code is working for temps under 0°C. I hope this night it get cold enough !

As soon as I'm ready, I need to check ask @zuckschwerdt how to push my code to the repo.

Proof:

time      : 2025-02-09 17:17:51
model     : HG9901       Sensor_id : 53241         Battery   : 7             Temperature: 3 C
soil moisture: 51 %      Light Intensity: 52 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-09 17:17:55
model     : HG9901       Sensor_id : 32982         Battery   : 7             Temperature: 5 C
soil moisture: 38 %      Light Intensity: 48 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-09 17:18:08
model     : HG9901       Sensor_id : 53241         Battery   : 7             Temperature: 3 C
soil moisture: 51 %      Light Intensity: 52 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-09 17:18:14
model     : HG9901       Sensor_id : 32982         Battery   : 7             Temperature: 5 C
soil moisture: 38 %      Light Intensity: 48 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-09 17:18:23
model     : HG9901       Sensor_id : 53241         Battery   : 7             Temperature: 3 C
soil moisture: 51 %      Light Intensity: 52 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-09 17:18:30
model     : HG9901       Sensor_id : 32982         Battery   : 7             Temperature: 5 C
soil moisture: 38 %      Light Intensity: 48 




I also check against the display the classification for the light intensity. My current values.

60 NOR_
47 48 NOR_
48 0C LOW_
49 0E LOW_
50 79 HIGH
51 84 HIGH+
52 8E HIGH+
53 81 HIHG+

Fully working code if someone wants to try:

/** @file
    Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut.

    Contributed by Inonoob

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
*/


/**
Moisture Sensor HG9901 - Moisture Sensor HG9901 - Homelead, Reyke (mine), Dr.meter, Vodeson, Midlocater, Kithouse, Vingnut..

This device is a simple garden temperature/moisture transmitter with a small LCD display for local viewing.

Data layout:

    Byte 0   Byte 1   Byte 2   Byte 3   Byte 4   Byte 5   Byte 6   Byte 7   Byte 8
     55	      aa       30       06       e4        ff      0f        3f       8 

Format string:

    SENSOR_ID:hhhh HUM:hh TEMP:hh h LIGHT:hh hh

Example packets:

    55aa3006e4ff0f3f8
    55aa7f29defc0f1f8

invert 

 cff91b00f0c07
 80d62103f0e07


Result

SENSOR_ID: cff9 HUM:1b TEMP:00 f 0c 07
SENSOR_ID: 80d6 HUM:21 TEMP:03 f 0e 07

*/

#include "decoder.h"

static int hg9901_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
    // Loop through all rows in the bitbuffer to process all potential packets
    for (int i = 0; i < bitbuffer->num_rows; ++i) {
        uint8_t *b = bitbuffer->bb[i];

        // Ensure that the length of the packet is correct (65 bits)
        if (bitbuffer->bits_per_row[i] != 65) {
            continue;  // Skip this row if the length is not correct
        }

        // Check if the first two bytes are the correct preamble (0x55 0xAA)
        if (b[0] != 0x55 || b[1] != 0xAA) {
            continue;  // Skip this row if the preamble is invalid
        }

        decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After preamble check");

        // Invert the bits of the entire packet (only if valid)
        bitbuffer_invert(bitbuffer);

        decoder_log_bitbuffer(decoder, 1, __func__, bitbuffer, "After invert");

        // Extract the data from the packet (after inversion)
        int sensor_id = (b[2] << 8) | b[3]; // Sensor ID from bytes 2 and 3
        int soil_moisture = b[4];                 // Humidity from byte 4
        int temp_raw = b[5];                 // Temperature from byte 5
        int battery = (b[6] & 0xF0) >> 4;      // Battery Low status from MSB of byte 6
        int light_intensity = ((b[6] & 0x0F) << 4) | (b[7] >> 4) ; // Light Intensity from byte 7

        if (temp_raw >= 128)
            temp_raw = 128 - temp_raw;


        // Store the decoded data and output it
        data_t *data = data_make(
                "model",            "",             DATA_STRING, "HG9901",
                "sensor_id",        "Sensor_id",    DATA_INT,    sensor_id,
                "battery",          "Battery",      DATA_INT,    battery,
                "temperature_C",    "Temperature",  DATA_FORMAT, "%d C", DATA_INT, temp_raw,
                "soil moisture",    "soil moisture",DATA_FORMAT, "%u %%", DATA_INT, soil_moisture,
                "light",            "Light Intensity", DATA_FORMAT, "%u ", DATA_INT, light_intensity,
                NULL);

        decoder_output_data(decoder, data);

        return 1;  // Successfully decoded one valid packet and returned
    }

    return DECODE_ABORT_LENGTH;  // No valid packet found
}

static char const *const output_fields[] = {
        "model",
        "sensor_id",
        "battery",
        "temperature_C",
        "soil moisture",
        "light",
        NULL,
};

r_device const hg9901 = {
        .name        = "HG9901 Moisture sensor",
        .modulation  = OOK_PULSE_PWM,
        .short_width = 600,
        .long_width  = 1392,
        .gap_limit   = 1056,  // long gap (with short pulse) is ~472 us, sync gap is ~728 us
        .reset_limit = 3808, // maximum gap is 1250 us (long gap + longer sync gap on last repeat)
        .decode_fn   = &hg9901_decode,
        .fields      = output_fields,
};


@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

Hey all,

I have my latest version done for release!

@zuckschwerdt please see attached my hg9901.c . You can integrate it to the main code if you want.

hg9901.zip

Current output example of the current code.

  • Sensor ID ==> DONE
  • Battery raw ==> 0x0, 0x3, 0x7, 0xb, 0xf ==> DONE
  • Battery % ==> DONE
  • Temperature ==> negative temp and positive work ==> DONE
  • Soil Moisture ==> DONE
  • Light Intensity ==> Only raw value from 0 to 133 observed. Classification like on the display not yet understood. ==> OPEN if more people can test the code we might can find the solution

Proof:

time      : 2025-02-10 19:40:18
model     : HG9901       Sensor_id : 53241         Battery raw: 15           Battery status: 100 %
Temperature: -1 C        Soil moisture: 61 %       Light Intensity: 9 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-10 19:42:51
model     : HG9901       Sensor_id : 53241         Battery raw: 15           Battery status: 100 %
Temperature: 0 C         Soil moisture: 61 %       Light Intensity: 0 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-10 19:42:56
model     : HG9901       Sensor_id : 53241         Battery raw: 15           Battery status: 100 %
Temperature: -1 C        Soil moisture: 61 %       Light Intensity: 9 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
time      : 2025-02-10 19:45:15
model     : HG9901       Sensor_id : 53241         Battery raw: 15           Battery status: 100 %
Temperature: -1 C        Soil moisture: 61 %       Light Intensity: 9 



For the Light Intensity topic: This table was proposed by Chatgpt but I don't think it is correct.

LOW-: 0 - 10
LOW: 11 - 30
LOW+: 31 - 33
NOR-: 34 - 50
NOR: 51 - 66
NOR+: 67 - 70
HIGH-: 71 - 77
HIGH: 78 - 85
HIGH+: 86 - 133

@ProfBoc75
Copy link
Collaborator

@inonoob : About Light Intensity. may I propose to convert to LUX, values as per the user guide, the range is from 0 to 15000LUX
So raw value is scale 100.

Light Level Raw Hexa Raw Decimal LUX
LOW - 0x00 - 0x0F 0 - 15 0 - 1500
LOW 0x10 - 0x1F 16 - 31 1600 - 3100
LOW+ 0x20 - 0x2F 32 - 47 3200 - 4700
NORMAL- 0x30 - 0x3F 48 - 63 4800 - 6300
NORMAL 0x40 - 0x4F 64 - 79 6400 - 7900
NORMAL+ 0x50 - 0x5F 80 - 95 8000 - 9500
HIGH- 0x60 - 0x6F 96 - 111 9600 - 11100
HIGH 0x70 - 0x7F 112 - 127 11200 - 12700
HIGH+ 0x80 - 0x8F and above 0xFF ? 128 - 143 12800 - 14300 or up to 25500 ?

@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

@ProfBoc75 thank you for finding the last piece of puzzle.

we could add even Lux and the Light level.

Now it must be translated to code :D

@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

Here might be the working code:

    // Extract the data from the packet (after inversion)
    int sensor_id = (b[2] << 8) | b[3]; // Sensor ID from bytes 2 and 3
    int soil_moisture = b[4];                 // Humidity from byte 4
    int temp_raw = b[5];                 // Temperature from byte 5
    int battery_raw = (b[6] & 0xF0) >> 4;      // Battery status is together with the light value
    int light_intensity = ((b[6] & 0x0F) << 4) | (b[7] >> 4); // Extract light intensity

    if (temp_raw >= 128)                    // Ensure value goes below 0°C if needed
        temp_raw = 128 - temp_raw;

    int battery_percentage = (battery_raw * 100) / 15; // Battery status

    // Determine LUX value and Light Level
    int lux = 0;
    char *light_level = "UNKNOWN";

    if (light_intensity >= 0x00 && light_intensity <= 0x0F) {
        lux = light_intensity * 100; // Approximation
        light_level = "LOW -";
    } else if (light_intensity >= 0x10 && light_intensity <= 0x1F) {
        lux = 1600 + (light_intensity - 0x10) * 100;
        light_level = "LOW";
    } else if (light_intensity >= 0x20 && light_intensity <= 0x2F) {
        lux = 3200 + (light_intensity - 0x20) * 100;
        light_level = "LOW+";
    } else if (light_intensity >= 0x30 && light_intensity <= 0x3F) {
        lux = 4800 + (light_intensity - 0x30) * 100;
        light_level = "NORMAL-";
    } else if (light_intensity >= 0x40 && light_intensity <= 0x4F) {
        lux = 6400 + (light_intensity - 0x40) * 100;
        light_level = "NORMAL";
    } else if (light_intensity >= 0x50 && light_intensity <= 0x5F) {
        lux = 8000 + (light_intensity - 0x50) * 100;
        light_level = "NORMAL+";
    } else if (light_intensity >= 0x60 && light_intensity <= 0x6F) {
        lux = 9600 + (light_intensity - 0x60) * 100;
        light_level = "HIGH-";
    } else if (light_intensity >= 0x70 && light_intensity <= 0x7F) {
        lux = 11200 + (light_intensity - 0x70) * 100;
        light_level = "HIGH";
    } else if (light_intensity >= 0x80) {
        lux = 12800 + (light_intensity - 0x80) * 100;
        if (light_intensity == 0xFF) {
            lux = 25500; // Max limit case
        }
        light_level = "HIGH+";
    }

    // Store the decoded data and output it
    data_t *data = data_make(
        "model", "", DATA_STRING, "HG9901",
        "sensor_id", "Sensor_id", DATA_INT, sensor_id,
        "battery_raw", "Battery raw", DATA_INT, battery_raw,
        "battery_percentage", "Battery status", DATA_FORMAT, "%u %%", DATA_INT, battery_percentage,
        "temperature_C", "Temperature", DATA_FORMAT, "%d C", DATA_INT, temp_raw,
        "soil moisture", "Soil moisture", DATA_FORMAT, "%u %%", DATA_INT, soil_moisture,
        "light", "Light Intensity", DATA_FORMAT, "%u ", DATA_INT, light_intensity,
        "lux", "LUX", DATA_FORMAT, "%u ", DATA_INT, lux,
        "light_level", "Light Level", DATA_STRING, light_level,
        NULL);

@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

I need to test it tomorrow. I don't have the rtl sdr with me.

@ProfBoc75
Copy link
Collaborator

ProfBoc75 commented Feb 10, 2025

@inonoob : more simple, for all values

int lux = light_intensity * 100

Then the 9 tests to set the light level.

In result (data_make) keep only lux and light_level (remove light intensity)

@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

like this


  // Extract the data from the packet (after inversion)
    int sensor_id = (b[2] << 8) | b[3]; // Sensor ID from bytes 2 and 3
    int soil_moisture = b[4];                 // Humidity from byte 4
    int temp_raw = b[5];                 // Temperature from byte 5
    int battery_raw = (b[6] & 0xF0) >> 4;      // Battery status is together with the light value
    int light_intensity = ((b[6] & 0x0F) << 4) | (b[7] >> 4); // Extract light intensity

    if (temp_raw >= 128)                    // Ensure value goes below 0°C if needed
        temp_raw = 128 - temp_raw;

    int battery_percentage = (battery_raw * 100) / 15; // Battery status

    // Determine LUX value and Light Level
    int lux = 0;

    lux = light_intensity * 100;

    char *light_level = "UNKNOWN";

    if (light_intensity >= 0x00 && light_intensity <= 0x0F) {
        light_level = "LOW -";
    } else if (light_intensity >= 0x10 && light_intensity <= 0x1F) {
        lux = 1600 + (light_intensity - 0x10) * 100;
        light_level = "LOW";
    } else if (light_intensity >= 0x20 && light_intensity <= 0x2F) {
        light_level = "LOW+";
    } else if (light_intensity >= 0x30 && light_intensity <= 0x3F) {
        light_level = "NORMAL-";
    } else if (light_intensity >= 0x40 && light_intensity <= 0x4F) {
        light_level = "NORMAL";
    } else if (light_intensity >= 0x50 && light_intensity <= 0x5F) {
        light_level = "NORMAL+";
    } else if (light_intensity >= 0x60 && light_intensity <= 0x6F) {
        light_level = "HIGH-";
    } else if (light_intensity >= 0x70 && light_intensity <= 0x7F) {
        light_level = "HIGH";
    } else if (light_intensity >= 0x80) {
        light_level = "HIGH+";
    }

    // Store the decoded data and output it
    data_t *data = data_make(
        "model", "", DATA_STRING, "HG9901",
        "sensor_id", "Sensor_id", DATA_INT, sensor_id,
        "battery_raw", "Battery raw", DATA_INT, battery_raw,
        "battery_percentage", "Battery status", DATA_FORMAT, "%u %%", DATA_INT, battery_percentage,
        "temperature_C", "Temperature", DATA_FORMAT, "%d C", DATA_INT, temp_raw,
        "soil moisture", "Soil moisture", DATA_FORMAT, "%u %%", DATA_INT, soil_moisture,
        "light", "Light Intensity", DATA_FORMAT, "%u ", DATA_INT, light_intensity,
        "lux", "LUX", DATA_FORMAT, "%u ", DATA_INT, lux,
        "light_level", "Light Level", DATA_STRING, light_level,
        NULL);

@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

I'm not too good in C :

char *light_level = "UNKNOWN";

const char *light_level = "UNKNOWN";

Which one should I use ?

@zuckschwerdt
Copy link
Collaborator

We don't really want that "light level" string. Other devices don't have that and it can easily be added for all light sensors with the hass script or something.

I'll now change light to light_lux. Thanks @ProfBoc75

@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

Ok cool then I would say sensor fully defined :). Case closed.

Thanks for the support and again patience. I learned a lot again

@ProfBoc75
Copy link
Collaborator

ProfBoc75 commented Feb 10, 2025

We don't really want that "light level" string. Other devices don't have that and it can easily be added for all light sensors with the hass script or something.

Another approach, just to get values as close as the sensor display (but agree with you, can be done externally) :

int light_level       = b[6] & 0x0F;
int light_intensity   = (light_level << 4) | (b[7] >> 4);
int light_lux         = light_intensity * 100;

...

data_t *data = data_make(
    "model",              "",               DATA_STRING, "HG9901",
    "sensor_id",          "Sensor_id",      DATA_INT, sensor_id,
    "battery_ok",         "Battery",        DATA_DOUBLE, battery_pct,
    "temperature_C",      "Temperature",    DATA_FORMAT, "%d C", DATA_INT, temp_raw,
    "soil moisture",      "Soil moisture",  DATA_FORMAT, "%u %%", DATA_INT, soil_moisture,
    "light_lux",          "Light",          DATA_FORMAT, "%u lux", DATA_INT, light_lux,
    "light_level",        "Light Level",    DATA_COND, light_level == 0, DATA_STRING, "LOW-",
    "light_level",        "Light Level",    DATA_COND, light_level == 1, DATA_STRING, "LOW",
    "light_level",        "Light Level",    DATA_COND, light_level == 2, DATA_STRING, "LOW+",
    "light_level",        "Light Level",    DATA_COND, light_level == 3, DATA_STRING, "NORMAL-",
    "light_level",        "Light Level",    DATA_COND, light_level == 4, DATA_STRING, "NORMAL",
    "light_level",        "Light Level",    DATA_COND, light_level == 5, DATA_STRING, "NORMAL+",
    "light_level",        "Light Level",    DATA_COND, light_level == 6, DATA_STRING, "HIGH-",
    "light_level",        "Light Level",    DATA_COND, light_level == 7, DATA_STRING, "HIGH",
    "light_level",        "Light Level",    DATA_COND, light_level >= 8, DATA_STRING, "HIGH+",
    NULL);

or only level value without text:

data_t *data = data_make(
    "model",              "",               DATA_STRING, "HG9901",
    "sensor_id",          "Sensor_id",      DATA_INT, sensor_id,
    "battery_ok",         "Battery",        DATA_DOUBLE, battery_pct,
    "temperature_C",      "Temperature",    DATA_FORMAT, "%d C", DATA_INT, temp_raw,
    "soil moisture",      "Soil moisture",  DATA_FORMAT, "%u %%", DATA_INT, soil_moisture,
    "light_lux",          "Light",          DATA_FORMAT, "%u lux", DATA_INT, light_lux,
    "light_level",        "Light Level",    DATA_INT, light_level,  // Values from 0 to 8 or up to 15
    NULL);

@inonoob
Copy link
Author

inonoob commented Feb 10, 2025

From a home assistant point of view it might be easier with the idea from @ProfBoc75 ?

@zuckschwerdt
Copy link
Collaborator

What is the connection between light_level and light_intensity? Is it some (linear) computed value or something like a UV index?
A simple level value would be best as some users might want to plot graphs.

@inonoob
Copy link
Author

inonoob commented Feb 11, 2025

Hey @zuckschwerdt

As of now the sensor only transmit the values in hex and represent the lux level from 0 to 15000.

The classification is only done on the display.

Question here is if RTL_433 should provide this classification or just provide the "raw" lux value as INT.

If only raw value is given then in home assistant the classification from lux to low, nor and high needs to be done with the example above.

Or

Classification is done via 0 to 8 and in home assistant only 0 equals Low- ; 1= low ; 2 = low+ ;....

@zuckschwerdt from your experience what would you prefer or is your guidance? To stick with the philosophy of rtl_433 less calculations is more ?

I'm fine with both ways and will adapte in home assistant.

@ProfBoc75
Copy link
Collaborator

@inonoob : The purpose of rtl_433 is to get data and presents them with as less as possible modification, but to be as much as possible aligned with what is on the sensor display, and at least scaled when conversion and unit are known.

For a weather station, the light value in Lux is the expected information that we want and nothing else from this value.
For this soil sensor, we have such information, at hex level scale 100 and Lux unit, so nice to have, but they are monitoring this light level for plant needs, and I guess this is what you want to know or I'm wrong ? Or Lux is enough for you ?

@zuckschwerdt

What is the connection between light_level and light_intensity? Is it some (linear) computed value or something like a UV index? A simple level value would be best as some users might want to plot graphs.

...the light is required by the plants, so based on the light intensity, they divided it into 9 levels, the 3 firsts are not enough for the plants (LOW), the 3 last levels are too much (HIGH), and the 3 in the middle (NORMAL) are the expected level of light for the plants. This level of light is only for this purpose / use case, and shown on the sensor's display.

We know how to get / define levels, so, it could be nice to have at rtl_433, the 9 words or the 9 values, that could trigger a light ON if Low Light for example, but yes, this can be done by HA or any smart home automation solution.
Less code at rtl_433 is better and easier to support/maintain.

FYI, extract of the user guide:

Image

Image

@inonoob
Copy link
Author

inonoob commented Feb 11, 2025

The background is, I did that for my wife and challenge to bring it to rtl_433 ;). I want to create a dashboard in HA with the same details as shown on the external display.

For me sort of you have the same info on the display and HA. Different source to display information but same content.

Lux is now nice to have as addition but I wanted to avoid the topic why is there Lux and there low, nor and high.

But HA has a good plant add-on

https://github.com/Olen/homeassistant-plant

Which works with lux. It depend on the user requirements.

@ProfBoc75
Copy link
Collaborator

@inonoob , @zuckschwerdt : In conclusion from my side, the decoding should stay at rtl_433 level providing standardized values (understandable by smart home automation, like HA / Domoticz / ... ), and yes, the final presentation is at HA level, which is much more powerful, flexible, can be customized, and so on.

@inonoob
Copy link
Author

inonoob commented Feb 12, 2025

Hey all,

I now have it reporting over MQTT in HA but the values get overwritten. So the ID and the values.

On the Raspberry pi: /usr/local/bin/rtl_433 -R 264 -R 91 -R 275 -C si -F mqtt://192.168.2.61 1883 user mqtt-user

MQTT topic: rtl_433/rtl433serverpi/devices/HG9901

It should look like this:

MQTT topic: rtl_433/rtl433serverpi/devices/HG9901/53241
MQTT topic: rtl_433/rtl433serverpi/devices/HG9901/32982

Is it because it is not defined as ID ?

HG9901
time = 2025-02-12 18:36:47
sensor_id = 53241 
battery_raw = 15
battery_percentage = 100
temperature_C = -2
soil moisture = 60
light = 9
lux = 900
light_level = LOW -

@inonoob inonoob reopened this Feb 12, 2025
@inonoob
Copy link
Author

inonoob commented Feb 12, 2025

I can confirm. I did recompile with the change and it is working now.

we need to rename from sensor_id to id:

HG9901

32982
time = 2025-02-12 19:36:02
id = 32982
battery_raw = 15
battery_percentage = 100
temperature_C = 1
soil moisture = 61
light = 9
lux = 900
light_level = LOW -

53241
time = 2025-02-12 19:42:49
id = 53241
battery_raw = 15
battery_percentage = 100
temperature_C = -2
soil moisture = 60
light = 9
lux = 900
light_level = LOW -

@merbanan
Copy link
Owner

Hey @zuckschwerdt

As of now the sensor only transmit the values in hex and represent the lux level from 0 to 15000.

The classification is only done on the display.

Question here is if RTL_433 should provide this classification or just provide the "raw" lux value as INT.

If only raw value is given then in home assistant the classification from lux to low, nor and high needs to be done with the example above.

Or

Classification is done via 0 to 8 and in home assistant only 0 equals Low- ; 1= low ; 2 = low+ ;....

@zuckschwerdt from your experience what would you prefer or is your guidance? To stick with the philosophy of rtl_433 less calculations is more ?

I'm fine with both ways and will adapte in home assistant.

As I see it the display is the true representation. Unless values are cumulative we should present all the parameters the display provides. If there are more details to show we can do that also. So in this case we should output the lux and the 9 different light intensity strings. For normalized data the strings are ignored and the lux value is used.

@zuckschwerdt
Copy link
Collaborator

If the light text strings are critical to the sensor then I agree, we should output that. I'm not sure if there is a simple lux to light level mapping? The light level is indicated with a nibble. Can you capture that (int) value together with the lux (int) value and plot a graph over a day-night cycle (for a wide range as possible)? The idea is then to either also output the light level as number, for graphing, or to just use them for the text as graphing lux is all anybody would ever need.

@inonoob
Copy link
Author

inonoob commented Feb 13, 2025

Hey all,

Here is an example of HA how it currently look likes:

Image

You can see the text and the LUX level as plot.

Lux has an official unit "lx". For the light level there is non. For me the Mapping and Lux level is ok.

Here is my MQTT extract. I did use my code with all the available information:

HG9901
53241
time = 2025-02-13 18:21:09
id = 53241
battery_raw = 15
battery_percentage = 100
temperature_C = -1
soil_moisture = 60
light = 8
lux = 800
light_level = LOW -
32982
time = 2025-02-13 18:21:14
id = 32982
battery_raw = 15
battery_percentage = 100
temperature_C = 2
soil_moisture = 61
light = 10
lux = 1000
light_level = LOW -

@inonoob
Copy link
Author

inonoob commented Feb 13, 2025

Here is a test dashboard as example:

Image

@Tuxyso
Copy link

Tuxyso commented Apr 18, 2025

I also own this moisture sensor. It is very interesting because the sensor is very cheap and has a very strong signal (compared to other more expensive moisture sensors). In Germany it is also sold under the brand "dr meter", see

https://www.amazon.de/Dr-meter-Feuchtigkeitsmesser-Feuchtigkeitsmessger%C3%A4t-Bodenwassermonitor-Gartenarbeit/dp/B0CQL27NZL?th=1

Image

Probably for some people it might be interesting how the sensor looks like without chassis:

Image

I would appreciate it a lot if you add it to the main branch.

@AkaBoing
Copy link

AkaBoing commented Apr 18, 2025

thx inonoob, good work, and a usefull device not only for gardener..
give Christian some time and he will add it.

thx Tuxyso: i ordered it an hour ago, for my wife.. 26.99 Euro , we will see..

@onlymejosh
Copy link

@inonoob - thanks for the work on this! I've been trying to get it working locally. But to no avail :/

➜  rtl_433 git:(feat-hg9901) ✗ rtl_433 -vvv -R 275

rtl_433 version 24.10-59-g1a6c32f7 branch feat-hg9901 at 202502102227 inputs file rtl_tcp RTL-SDR with TLS
Registering protocol [275] "HG9901 Moisture sensor"
[Protocols] Registered 1 out of 275 device decoding protocols
[Input] The internals of input handling changed, read about and report problems on PR #1978
[SDR] Found 1 device(s)
[SDR] trying device 0: Nooelec, NESDR SMArt v5, SN: 92595036
Found Rafael Micro R820T tuner
[SDR] Using device 0: Nooelec, NESDR SMArt v5, SN: 92595036, "Generic RTL2832U OEM"
Exact sample rate is: 250000.000414 Hz
[R82XX] PLL not locked!
[SDR] Sample rate set to 250000 S/s.
[Input] Bit detection level set to 0.0 (Auto).
[SDR] Tuner gain set to Auto.
[Input] Reading samples in async mode...
[SDR] rtlsdr_set_center_freq 433920000 = 0
[SDR] Tuned to 433.920MHz.
[acquire_thread] acquire_thread enter...
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {30}fdf4ff5c
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {30}fdf4ff5c
[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {30}fdf4ff5c

I assume

[pulse_slicer_pwm] HG9901 Moisture sensor
codes     : {30}fdf4ff5c

means its found something? Or does this mean it is just checking for the correct sensor.

I'm not seeing any results though.

I believe I've pulled your latest code, but I have also ran the code from the branch in #3194

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants