forked from esphome/esphome
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add integration hydreon_rgxx for rain sensors by Hydreon (esphome#2711)
Co-authored-by: Jesse Hills <[email protected]>
- Loading branch information
1 parent
efa6fd0
commit fdda47d
Showing
7 changed files
with
476 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.