Add integration hydreon_rgxx for rain sensors by Hydreon (#2711)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
functionpointer 2022-04-11 04:50:56 +02:00 committed by GitHub
parent efa6fd03e5
commit fdda47db6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 476 additions and 0 deletions

View file

@ -82,6 +82,7 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/homeassistant/* @OttoWinter esphome/components/homeassistant/* @OttoWinter
esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp/* @RubyBailey
esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
esphome/components/improv_serial/* @esphome/core esphome/components/improv_serial/* @esphome/core
esphome/components/ina260/* @MrEditor97 esphome/components/ina260/* @MrEditor97

View file

@ -0,0 +1,11 @@
import esphome.codegen as cg
from esphome.components import uart
CODEOWNERS = ["@functionpointer"]
DEPENDENCIES = ["uart"]
hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx")
RGModel = hydreon_rgxx_ns.enum("RGModel")
HydreonRGxxComponent = hydreon_rgxx_ns.class_(
"HydreonRGxxComponent", cg.PollingComponent, uart.UARTDevice
)

View file

@ -0,0 +1,36 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import (
CONF_ID,
DEVICE_CLASS_COLD,
)
from . import hydreon_rgxx_ns, HydreonRGxxComponent
CONF_HYDREON_RGXX_ID = "hydreon_rgxx_id"
CONF_TOO_COLD = "too_cold"
HydreonRGxxBinarySensor = hydreon_rgxx_ns.class_(
"HydreonRGxxBinaryComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(HydreonRGxxBinarySensor),
cv.GenerateID(CONF_HYDREON_RGXX_ID): cv.use_id(HydreonRGxxComponent),
cv.Optional(CONF_TOO_COLD): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_COLD
),
}
)
async def to_code(config):
main_sensor = await cg.get_variable(config[CONF_HYDREON_RGXX_ID])
bin_component = cg.new_Pvariable(config[CONF_ID], main_sensor)
await cg.register_component(bin_component, config)
if CONF_TOO_COLD in config:
tc = await binary_sensor.new_binary_sensor(config[CONF_TOO_COLD])
cg.add(main_sensor.set_too_cold_sensor(tc))

View file

@ -0,0 +1,211 @@
#include "hydreon_rgxx.h"
#include "esphome/core/log.h"
namespace esphome {
namespace hydreon_rgxx {
static const char *const TAG = "hydreon_rgxx.sensor";
static const int MAX_DATA_LENGTH_BYTES = 80;
static const uint8_t ASCII_LF = 0x0A;
#define HYDREON_RGXX_COMMA ,
static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREON_RGXX_COMMA)};
void HydreonRGxxComponent::dump_config() {
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
if (this->is_failed()) {
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
}
LOG_UPDATE_INTERVAL(this);
int i = 0;
#define HYDREON_RGXX_LOG_SENSOR(s) \
if (this->sensors_[i++] != nullptr) { \
LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \
}
HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, );
}
void HydreonRGxxComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up hydreon_rgxx...");
while (this->available() != 0) {
this->read();
}
this->schedule_reboot_();
}
bool HydreonRGxxComponent::sensor_missing_() {
if (this->sensors_received_ == -1) {
// no request sent yet, don't check
return false;
} else {
if (this->sensors_received_ == 0) {
ESP_LOGW(TAG, "No data at all");
return true;
}
for (int i = 0; i < NUM_SENSORS; i++) {
if (this->sensors_[i] == nullptr) {
continue;
}
if ((this->sensors_received_ >> i & 1) == 0) {
ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]);
return true;
}
}
return false;
}
}
void HydreonRGxxComponent::update() {
if (this->boot_count_ > 0) {
if (this->sensor_missing_()) {
this->no_response_count_++;
ESP_LOGE(TAG, "data missing %d times", this->no_response_count_);
if (this->no_response_count_ > 15) {
ESP_LOGE(TAG, "asking sensor to reboot");
for (auto &sensor : this->sensors_) {
if (sensor != nullptr) {
sensor->publish_state(NAN);
}
}
this->schedule_reboot_();
return;
}
} else {
this->no_response_count_ = 0;
}
this->write_str("R\n");
#ifdef USE_BINARY_SENSOR
if (this->too_cold_sensor_ != nullptr) {
this->too_cold_sensor_->publish_state(this->too_cold_);
}
#endif
this->too_cold_ = false;
this->sensors_received_ = 0;
}
}
void HydreonRGxxComponent::loop() {
uint8_t data;
while (this->available() > 0) {
if (this->read_byte(&data)) {
buffer_ += (char) data;
if (this->buffer_.back() == static_cast<char>(ASCII_LF) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) {
// complete line received
this->process_line_();
this->buffer_.clear();
}
}
}
}
/**
* Communication with the sensor is asynchronous.
* We send requests and let esphome continue doing its thing.
* Once we have received a complete line, we process it.
*
* Catching communication failures is done in two layers:
*
* 1. We check if all requested data has been received
* before we send out the next request. If data keeps
* missing, we escalate.
* 2. Request the sensor to reboot. We retry based on
* a timeout. If the sensor does not respond after
* several boot attempts, we give up.
*/
void HydreonRGxxComponent::schedule_reboot_() {
this->boot_count_ = 0;
this->set_interval("reboot", 5000, [this]() {
if (this->boot_count_ < 0) {
ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_);
}
this->boot_count_--;
this->write_str("K\n");
if (this->boot_count_ < -5) {
ESP_LOGE(TAG, "hydreon_rgxx can't boot, giving up");
for (auto &sensor : this->sensors_) {
if (sensor != nullptr) {
sensor->publish_state(NAN);
}
}
this->mark_failed();
}
});
}
bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) {
return this->buffer_starts_with_(prefix.c_str());
}
bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; }
void HydreonRGxxComponent::process_line_() {
ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
if (buffer_[0] == ';') {
ESP_LOGI(TAG, "Comment: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
return;
}
if (this->buffer_starts_with_("PwrDays")) {
if (this->boot_count_ <= 0) {
this->boot_count_ = 1;
} else {
this->boot_count_++;
}
this->cancel_interval("reboot");
this->no_response_count_ = 0;
ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
this->write_str("P\nH\nM\n"); // set sensor to polling mode, high res mode, metric mode
return;
}
if (this->buffer_starts_with_("SW")) {
std::string::size_type majend = this->buffer_.find('.');
std::string::size_type endversion = this->buffer_.find(' ', 3);
if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) {
ESP_LOGW(TAG, "invalid version string: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
}
int major = strtol(this->buffer_.substr(3, majend - 3).c_str(), nullptr, 10);
int minor = strtol(this->buffer_.substr(majend + 1, endversion - (majend + 1)).c_str(), nullptr, 10);
if (major > 10 || minor >= 1000 || minor < 0 || major < 0) {
ESP_LOGW(TAG, "invalid version: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
}
this->sw_version_ = major * 1000 + minor;
ESP_LOGI(TAG, "detected sw version %i", this->sw_version_);
return;
}
bool is_data_line = false;
for (int i = 0; i < NUM_SENSORS; i++) {
if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) {
is_data_line = true;
break;
}
}
if (is_data_line) {
std::string::size_type tc = this->buffer_.find("TooCold");
this->too_cold_ |= tc != std::string::npos;
if (this->too_cold_) {
ESP_LOGD(TAG, "Received TooCold");
}
for (int i = 0; i < NUM_SENSORS; i++) {
if (this->sensors_[i] == nullptr) {
continue;
}
std::string::size_type n = this->buffer_.find(PROTOCOL_NAMES[i]);
if (n == std::string::npos) {
continue;
}
int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10);
this->sensors_[i]->publish_state(data);
ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
this->sensors_received_ |= (1 << i);
}
} else {
ESP_LOGI(TAG, "Got unknown line: %s", this->buffer_.c_str());
}
}
float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; }
} // namespace hydreon_rgxx
} // namespace esphome

View file

@ -0,0 +1,76 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/sensor/sensor.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace hydreon_rgxx {
enum RGModel {
RG9 = 1,
RG15 = 2,
};
#ifdef HYDREON_RGXX_NUM_SENSORS
static const uint8_t NUM_SENSORS = HYDREON_RGXX_NUM_SENSORS;
#else
static const uint8_t NUM_SENSORS = 1;
#endif
#ifndef HYDREON_RGXX_PROTOCOL_LIST
#define HYDREON_RGXX_PROTOCOL_LIST(F, SEP) F("")
#endif
class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice {
public:
void set_sensor(sensor::Sensor *sensor, int index) { this->sensors_[index] = sensor; }
#ifdef USE_BINARY_SENSOR
void set_too_cold_sensor(binary_sensor::BinarySensor *sensor) { this->too_cold_sensor_ = sensor; }
#endif
void set_model(RGModel model) { model_ = model; }
/// Schedule data readings.
void update() override;
/// Read data once available
void loop() override;
/// Setup the sensor and test for a connection.
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
void process_line_();
void schedule_reboot_();
bool buffer_starts_with_(const std::string &prefix);
bool buffer_starts_with_(const char *prefix);
bool sensor_missing_();
sensor::Sensor *sensors_[NUM_SENSORS] = {nullptr};
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *too_cold_sensor_ = nullptr;
#endif
int16_t boot_count_ = 0;
int16_t no_response_count_ = 0;
std::string buffer_;
RGModel model_ = RG9;
int sw_version_ = 0;
bool too_cold_ = false;
// bit field showing which sensors we have received data for
int sensors_received_ = -1;
};
class HydreonRGxxBinaryComponent : public Component {
public:
HydreonRGxxBinaryComponent(HydreonRGxxComponent *parent) {}
};
} // namespace hydreon_rgxx
} // namespace esphome

View file

@ -0,0 +1,119 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart, sensor
from esphome.const import (
CONF_ID,
CONF_MODEL,
CONF_MOISTURE,
DEVICE_CLASS_HUMIDITY,
STATE_CLASS_MEASUREMENT,
)
from . import RGModel, HydreonRGxxComponent
UNIT_INTENSITY = "intensity"
UNIT_MILLIMETERS = "mm"
UNIT_MILLIMETERS_PER_HOUR = "mm/h"
CONF_ACC = "acc"
CONF_EVENT_ACC = "event_acc"
CONF_TOTAL_ACC = "total_acc"
CONF_R_INT = "r_int"
RG_MODELS = {
"RG_9": RGModel.RG9,
"RG_15": RGModel.RG15,
# https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf
# https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf
# https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf
}
SUPPORTED_SENSORS = {
CONF_ACC: ["RG_15"],
CONF_EVENT_ACC: ["RG_15"],
CONF_TOTAL_ACC: ["RG_15"],
CONF_R_INT: ["RG_15"],
CONF_MOISTURE: ["RG_9"],
}
PROTOCOL_NAMES = {
CONF_MOISTURE: "R",
CONF_ACC: "Acc",
CONF_R_INT: "Rint",
CONF_EVENT_ACC: "EventAcc",
CONF_TOTAL_ACC: "TotalAcc",
}
def _validate(config):
for conf, models in SUPPORTED_SENSORS.items():
if conf in config:
if config[CONF_MODEL] not in models:
raise cv.Invalid(
f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HydreonRGxxComponent),
cv.Required(CONF_MODEL): cv.enum(
RG_MODELS,
upper=True,
space="_",
),
cv.Optional(CONF_ACC): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_R_INT): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
unit_of_measurement=UNIT_INTENSITY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA),
_validate,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add_define(
"HYDREON_RGXX_PROTOCOL_LIST(F, sep)",
cg.RawExpression(
" sep ".join([f'F("{name}")' for name in PROTOCOL_NAMES.values()])
),
)
cg.add_define("HYDREON_RGXX_NUM_SENSORS", len(PROTOCOL_NAMES))
for i, conf in enumerate(PROTOCOL_NAMES):
if conf in config:
sens = await sensor.new_sensor(config[conf])
cg.add(var.set_sensor(sens, i))

View file

@ -349,6 +349,24 @@ sensor:
name: 'Temperature' name: 'Temperature'
humidity: humidity:
name: 'Humidity' name: 'Humidity'
- platform: hydreon_rgxx
model: "RG 9"
uart_id: uart6
id: "hydreon_rg9"
moisture:
name: "hydreon_rain"
id: hydreon_rain
- platform: hydreon_rgxx
model: "RG_15"
uart_id: uart6
acc:
name: "hydreon_acc"
event_acc:
name: "hydreon_event_acc"
total_acc:
name: "hydreon_total_acc"
r_int:
name: "hydreon_r_int"
- platform: adc - platform: adc
pin: VCC pin: VCC
id: my_sensor id: my_sensor
@ -796,6 +814,10 @@ binary_sensor:
then: then:
- cover.toggle: time_based_cover - cover.toggle: time_based_cover
- cover.toggle: endstop_cover - cover.toggle: endstop_cover
- platform: hydreon_rgxx
hydreon_rgxx_id: "hydreon_rg9"
too_cold:
name: "rg9_toocold"
- platform: template - platform: template
id: 'pzemac_reset_energy' id: 'pzemac_reset_energy'
on_press: on_press: