Testing with EC, conductivity, sensor, I think I can parse the data, if it comes in a as string like “50930,27506,33.43,1.025” as it does in the buffer on a manual read. But the EZO sensor just publishes the first float, so just 50930.000. Are you able to see the entire data string in HA or just the verbose logs?
Very cool! I’ll have to look into those blueprints as well, thankyou
If I send the read command, it shows as a string only via verbose logs (just like the EC sensor does).
Atlas Scientific has an Arduino Sample Code (here) which breaks it up already. I was thinking that code could be added via lambdas, but no such luck because it needs to have a return statement. I don’t know anything in the world of C or C++, so my attempt to solve this stuff is just a whole lot of guessing.
Thanks. Hmm, I’m learning how to do this. I used the Arduino code initially before using ESPHome, I may have to take another look at that code for inspiration…
The confusing part to me is that ESPHome can read Arduino code because it has the “Wire” Library, as stated here (whatever that means ), and Atlas Scientific already has (probably) all the code needed on their GitHub here. Two awesome coders created the EZO Sensor and Peristaltic Pump components in ESPHome a handful of months ago, but it seems like they had to either write their own code or couldn’t include/translate/transfer everything Atlas Scientific already had…
I’m so grateful for all the hard work everyone has done to get everything this far. I would be completely and utterly lost without it.
yes, I saw that too! From what I understand, the Pump EZO circuit has multiple sensors to publish, while the HUM and EC sensors only have the one in ASCII string format, but the generic EZO sensor only allows floating numbers, so it only publishes the first float before the ‘,’. We can see the full ACSII string in verbose mode, I do not know how to get that information into ESPHome IDE to parse from… I am trying though… Currently trying a custom text sensor.
This was the idea I had that worked in testing, but can’t get the EZO to send this format into ESPHome:
text_sensor:
- platform: template
name: Test Text
id: hatext
lambda: |-
return {"35.65, 1.026, 13000, 24.95"};
sensor:
- platform: template
name: testing_test_1
id: test_1
update_interval: 10s
accuracy_decimals: 2
lambda: |-
std::string str = id(hatext).state;
std::vector<std::string> v;
char * token;
char seps[] = ",";
token = strtok (&str[0],seps);
while (token != NULL)
{
v.push_back(token);
token = strtok (NULL, seps);
}
std::string target_token = v[0];
return std::stof(target_token);
Would it be possible to make a custom sensor and just copy everything from the Arduino example for HUM and EC because they’re the ones that have multiple sets of data?
I think the text sensor might be hard because the data is always going to be different.
I’m working on something… But unsure so far
#include "esphome.h"
#include <Wire.h> //include arduinos i2c library
#define address 100 //default I2C ID number for EZO EC Circuit.
class MyCustomSensor : public PollingComponent, public Sensor {
public:
MyCustomSensor() : PollingComponent(15000) {}
float get_setup_priority() const override { return esphome::setup_priority::IO; }
char ec_data[32]; //we make a 32 byte character array to hold incoming data from the EC circuit.
byte in_char = 0; //used as a 1 byte buffer to store inbound bytes from the EC Circuit.
byte i = 0; //counter used for ec_data array.
int time_ = 570;
char *ec; //char pointer used in string parsing.
char *tds; //char pointer used in string parsing.
char *sal; //char pointer used in string parsing.
char *sg; //char pointer used in string parsing.
float ec_float; //float var used to hold the float value of the conductivity.
float tds_float; //float var used to hold the float value of the TDS.
float sal_float; //float var used to hold the float value of the salinity.
float sg_float; //float var used to hold the float value of the specific gravity.
void setup() override {
Wire.begin(); //enable I2C port.
}
void update() override {
Wire.beginTransmission(address); //call the circuit by its ID number.
Wire.write("r"); //transmit the command that was sent through the serial port.
Wire.endTransmission(); //end the I2C data transmission.
while (Wire.available()) { //are there bytes to receive.
in_char = Wire.read(); //receive a byte.
ec_data[i] = in_char; //load this byte into our array.
i += 1; //incur the counter for the array element.
if (in_char == 0) { //if we see that we have been sent a null command.
i = 0; //reset the counter i to 0.
break; //exit the while loop.
}
}
string_pars();
publish_state(ec_float);
}
void string_pars() { //this function will break up the CSV string into its 4 individual parts. EC|TDS|SAL|SG.
ec = strtok(ec_data, ","); //let's pars the string at each comma.
tds = strtok(NULL, ","); //let's pars the string at each comma.
sal = strtok(NULL, ","); //let's pars the string at each comma.
sg = strtok(NULL, ","); //let's pars the string at each comma.
ec_float=atof(ec);
tds_float=atof(tds);
sal_float=atof(sal);
sg_float=atof(sg);
}
};
I think I found where this issue is coming from in the ezo.cpp file:
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1], EZO_COMMAND_TYPE_STRINGS[to_run->command_type]);
if (buf[0] == 1) {
std::string payload = reinterpret_cast<char *>(&buf[1]);
if (!payload.empty()) {
switch (to_run->command_type) {
case EzoCommandType::EZO_READ: {
// some sensors return multiple comma-separated values, terminate string after first one
int start_location = 0;
if ((start_location = payload.find(',')) != std::string::npos) { **// this line**
payload.erase(start_location); **// and this line**
}
auto val = parse_number<float>(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
} else {
this->publish_state(*val);
}
break;
}
It’s saying right there to terminate the string, but that’s exactly what we don’t want done for the EC and HUM sensors
Here’s (I believe) the relevant code from the Arduino Sketch file for the Humidity sensor
char *HUM; //char pointer used in string parsing.
char *TMP; //char pointer used in string parsing.
char *NUL; //char pointer used in string parsing (the sensor outputs some text that we don't need).
char *DEW; //char pointer used in string parsing.
float HUM_float; //float var used to hold the float value of the humidity.
float TMP_float; //float var used to hold the float value of the temperatur.
float DEW_float; //float var used to hold the float value of the dew point.
and
//if (computerdata[0] == 'r') string_pars(); //uncomment this function if you would like to break up the comma separated string into its individual parts.
}
}
void string_pars() { //this function will break up the CSV string into its 3 individual parts. HUM|TMP|DEW.
//this is done using the C command “strtok”.
HUM = strtok(Humidity_data, ","); //let's pars the string at each comma.
TMP = strtok(NULL, ","); //let's pars the string at each comma.
NUL = strtok(NULL, ","); //let's pars the string at each comma (the sensor outputs the word "DEW" in the string, we dont need it.
DEW = strtok(NULL, ","); //let's pars the string at each comma.
Serial.println(); //this just makes the output easier to read by adding an extra blank line.
Serial.print("HUM:"); //we now print each value we parsed separately.
Serial.println(HUM); //this is the humidity value.
Serial.print("Air TMP:"); //we now print each value we parsed separately.
Serial.println(TMP); //this is the air temperatur value.
Serial.print("DEW:"); //we now print each value we parsed separately.
Serial.println(DEW); //this is the dew point.
Serial.println(); //this just makes the output easier to read by adding an extra blank line.
//uncomment this section if you want to take the values and convert them into floating point number.
/*
HUM_float=atof(HUM);
TMP_float=atof(TMP);
DEW_float=atof(DEW);
*/
}
@ssieb How do you think it best to go about adjusting the ezo sensor code to allow for the sensors that return multiple values (EC and HUM) to be able to display them? Should we make a Pull Request via GitHub, or would we need to create a separate EZO component entirely?
Yes, that would be nice to have. There must be a reason why they stopped it at the first comma rather than breaking them out.
I tried creating a custom sensor, some progress but I am just not that skilled at programming to do much further. Maybe someone can see if possible?
I did a little more digging and found this (the last pull request regarding the EZO sensors) and commented there to see if @ alfredopironti might be able to help (the pull request stated the data can be parsed out, but it would be a breaking change).
The other idea I had involved using lambdas through a sensor/binary sensor template, and strtok
(which I guess is a C++ command that parses data), but couldn’t figure it out
Yes, using strtok
was the idea I had as well, if I could get it to output all string through ESPHome…
YES! It is terrible. I don’t know if the temp part is in the middle of the probe or not because it’s definitely not the water temp. It is very air dependent. Unless the probe is fully submersed I don’t think it will be accurate. I’m trying to figure out how to get an external probe I can put in a thermowell which kind of defeats the purpose of all the sensors in one.
You can see divergence easily:
yeah PH and ORP is working good but the temp is worthless.
I may have figured out how to parse the string and create sensors for all outputs! I tested this with the EZO Conductivity EC and CO2 circuits, but I created the same for Humidity, as you wanted. Let me know if something doesn’t work as intended.
I added the triggers to the EZO sensor to trigger when certain commands are sent. Also, other sensors that parse the string from raw_value_hum
which is a comma separated string, that when ALL parameters are enabled, in this case there are 3, the lambda in the sensors will take the 2nd, and 3rd values individually and return them. The first value, Humidity, is returned by default by the hum_ezo
sensor:
sensor:
# EZO Circuit - HUM
# Humidity
- platform: ezo
name: "Humidity"
id: hum_ezo
address: 111
unit_of_measurement: "%"
accuracy_decimals: 2
state_class: "measurement"
device_class: "humidity"
on_custom:
then:
- lambda:
id(raw_value_hum).publish_state(x);
on_calibration:
then:
- lambda:
id(result_hum).publish_state(x);
on_device_information:
then:
- lambda:
id(result_hum).publish_state(x);
on_slope:
then:
- lambda:
id(result_hum).publish_state(x);
on_t:
then:
- lambda:
id(result_hum).publish_state(x);
# Air Temperature
- platform: template
name: Air Temperature
id: sensor_hum_air_temperature
accuracy_decimals: 2
disabled_by_default: true
unit_of_measurement: "°C"
state_class: "measurement"
device_class: "temperature"
lambda: |-
std::string str = id(raw_value_hum).state;
std::vector<std::string> v;
char * token;
char seps[] = ",";
token = strtok (&str[0],seps);
while (token != NULL) {
v.push_back(token);
token = strtok (NULL, seps);
}
if (v.size() == 3) {
return std::stof(v[1]);
}
else {
return NAN;
}
# Dew Point
- platform: template
name: Dew Point
id: sensor_hum_dew_point
accuracy_decimals: 2
disabled_by_default: true
unit_of_measurement: "°C"
state_class: "measurement"
device_class: "temperature"
lambda: |-
std::string str = id(raw_value_hum).state;
std::vector<std::string> v;
char * token;
char seps[] = ",";
token = strtok (&str[0],seps);
while (token != NULL) {
v.push_back(token);
token = strtok (NULL, seps);
}
if (v.size() == 3) {
return std::stof(v[2]);
}
else {
return NAN;
}
I made a selector to choose a common command to send to the EZO sensor, and a button to send that command and another button to Read the probe manually:
select:
# Select Command to Send
- platform: template
name: HUM - Command Select
id: select_command_hum
optimistic: true
entity_category: "Config"
options:
- "Read"
- "Information"
- "Status"
- "Get Enabled Parameter(s)"
- "Output Units - Enable Humidity"
- "Output Units - Disable Humidity"
- "Output Units - Enable Temperature"
- "Output Units - Disable Temperature"
- "Output Units - Enable Dew Point"
- "Output Units - Disable Dew Point"
- "Check Calibration"
initial_option: "Read"
set_action:
- logger.log:
format: "Chosen option: %s"
args: ["x.c_str()"]
button:
# Read HUM Sensor
- platform: template
name: HUM - Read
id: read_hum
entity_category: "Config"
on_press:
then:
- lambda: |-
id(hum_ezo).send_custom("R");
# Send Selected EZO Command - HUM
- platform: template
name: HUM - Command Send Selected
id: send_selected_hum
entity_category: "Config"
on_press:
then:
- lambda: |-
if (id(select_command_hum).state == "Read") {
id(hum_ezo).send_custom("R");
}
if (id(select_command_hum).state == "Information") {
id(hum_ezo).get_device_information();
}
if (id(select_command_hum).state == "Status") {
id(hum_ezo).send_custom("Status");
}
if (id(select_command_hum).state == "Get Enabled Parameter(s)") {
id(hum_ezo).send_custom("O,?");
}
if (id(select_command_hum).state == "Output Units - Enable Humidity") {
id(hum_ezo).send_custom("O,HUM,1");
}
if (id(select_command_hum).state == "Output Units - Disable Humidity") {
id(hum_ezo).send_custom("O,HUM,0");
}
if (id(select_command_hum).state == "Output Units - Enable Temperature") {
id(hum_ezo).send_custom("O,T,1");
}
if (id(select_command_hum).state == "Output Units - Disable Temperature") {
id(hum_ezo).send_custom("O,T,0");
}
if (id(select_command_hum).state == "Output Units - Enable Dew Point") {
id(hum_ezo).send_custom("O,DEW,1");
}
if (id(select_command_hum).state == "Output Units - Disable Dew Point") {
id(hum_ezo).send_custom("O,DEW,0");
}
if (id(select_command_hum).state == "Check Calibration") {
id(hum_ezo).get_calibration();
}
Select Output Units - Enable Temperature, then use the HUM - Command Send Selected button to send the command and add Temperature to the raw_value_hum
sensor. Do the same with Output Units - Enable Dew Point.
The raw_value_hum
sensor should be somewhat similar to this 49.48,26.45,24.25
which represents HUM,T,DEW
. The lambda in the sensors above will parse and return the respective results as an individual sensor when the Read command is sent.
Then we output the result in a text sensor. I created two, one to used for the Raw String, but sometimes will be used for certain/most commands becuase they are triggered by the send_custom
trigger. This will be the sensor that the other sensors parse the string from. The another for certain commands that aren’t triggered by send_custom
command.
text_sensor:
# HUM Raw Value
- platform: template
name: HUM - Raw Value
icon: mdi:counter
id: raw_value_hum
on_value:
then:
- component.update: sensor_hum_air_temperature
- component.update: sensor_hum_dew_point
# Selected Command Result - HUM
- platform: template
name: HUM - Command Result
id: result_hum
But, the raw_value_hum
sensor is only updated then the Read
button/command is pressed or sent. But, we want that to happen automatically, so I made an interval
component to Read
the sensor every 60s, and at boot
:
esphome:
on_boot:
priority: 200
then:
- button.press: read_hum
interval:
# Update Raw HUM
- interval: 60s
then:
- button.press: read_hum
What do you think? Does it work? So far, works well on my end with the probes I got as mentioned at the top.
@TheRealFalseReality Thank you so much for working so hard on this! I’ve been trying to get this to work in my system with your code for a few hours now but unfortunately I just can’t get the Air Temp and Dew Point to display properly. I just get nan°C
.
The HUM - Raw Value
text sensor displays the Humidity and the logs for the main EZO humidity sensor reads [V][ezo.sensor:109]: Received buffer "50.83,20.62,Dew,10.12" for command type EZO_CUSTOM
which seems a little off with Dew
there… Why would it return letters and not numbers?
Either way, I tried changing if (v.size() == 3) {
to if (v.size() == 4) {
with no luck, as well as trying to change return std::stof(v[2]);
to return std::stof(v[3]);
just in case there was an issue with that.
I’m sorry to deliver this less-than-perfect news and super appreciate your efforts, and am very happy to hear the new code is working for you! I haven’t tried converting the code to try out with my EC sensors, but when I get some more time I’ll try to see if I can get it to work with them.
I’m curious: Do you think HA would be open to implementing a generic version of your code to easily add different sensors for the comma separated values? If the code works for all comma separated values, all the end user would need to know is what each value represents and create an id that reflects it. (Would that be a Pull Request?)
Does the HUM - Raw Value
sensor display 50.83,20.62,Dew,10.12
as well? You’d need to enable the string output via the selector but it sounds like it does. I don’t have a HUM sensor to test outright, but you have the right idea. If there are 4 values separated by commas, v.size() == 4
would be correct, and shift the Dew Point value to return std::stof(v[3]);
I altered my code but i won’t know for sure until I get that sensor. Here’s a link:
Here’s my code for the EC circuit:
Greetings, Is the switch to turn on/off the sensor controlled manually in HA frontend or from the esp32 board? because I can’t turn off the sensor when the esp32 goes into deep sleep
Took a lot of digging to sort out how to calibrate the rest of the sensors… Thought this might help everyone else to calibrate the ORP, RTD(temp) EC, etc… I found my ORP probe to be off a fair amount.
Example of the settings i used (in addition to the above from the op)
button:
- platform: template
name: "ORP 225mV Calibrate"
id: orp_calibrate_225
on_press:
then:
- lambda: id(ph_orp).send_custom("cal,225");
- platform: template
name: "ORP Calibrate Clear"
id: orp_calibrate_225
on_press:
then:
- lambda: id(ph_orp).send_custom("cal,clear");
- platform: template
name: "RTD 100c Calibrate"
id: orp_calibrate_100
on_press:
then:
- lambda: id(rtd_ezo).send_custom("cal,100");
- platform: template
name: "RTD Calibrate Clear"
id: orp_calibrate_clear
on_press:
then:
- lambda: id(rtd_ezo).send_custom("cal,clear");
Also wanted to give a generic as any other custom commands can be sent at well…
button:
- platform: template
name: "<arbitrary name to show up in ha>"
id: <id_you_want_to_use>
on_press:
then:
- lambda: id(<this is the id from the sensor>).send_custom("<reference the ezo docs for commands, remove what is before and the :>");
Hope this helps others.