Last active
February 27, 2025 08:33
-
-
Save hugokernel/0763e8cd93ede264ca25997d4d091c67 to your computer and use it in GitHub Desktop.
ESPHome version of muino water meter
This file contains hidden or 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
substitutions: | |
name: watermeter | |
friendly_name: "WaterMeter" | |
esphome: | |
name: $name | |
on_boot: | |
priority: -10 | |
then: | |
- script.execute: | |
id: init_pio | |
- script.execute: | |
id: init_vemls | |
esp32: | |
board: esp32dev | |
# Add basic networking | |
wifi: | |
ssid: !secret wifi_ssid | |
password: !secret wifi_password | |
# Enable fallback hotspot in case of connection failure | |
ap: | |
ssid: "Watermeterhome Fallback" | |
password: "Watermeterhome Fallback" | |
# Enable Home Assistant API | |
api: | |
encryption: | |
key: !secret api_encryption_key | |
ota: | |
- platform: esphome | |
password: !secret ota_password | |
i2c: | |
sda: GPIO21 | |
scl: GPIO22 | |
scan: true | |
frequency: 10khz | |
logger: | |
level: INFO | |
# Triggers reading adc values and iterating algoritm | |
interval: | |
- interval: 250ms | |
then: | |
- lambda: |- | |
static uint32_t last_time = 0; | |
uint32_t now = millis(); | |
uint32_t interval = now - last_time; | |
switch (id(status)) { | |
case 0: | |
// At the beginning, we do the initialization | |
/* | |
ESP_LOGE("sensor", "COUCOU"); | |
id(init_pio)->execute(); | |
id(init_vemls)->execute(); | |
id(led_power)->execute(false); | |
id(status) = 1; | |
break; | |
*/ | |
case 1: | |
// First, we read the dark sensors | |
id(read_dark_sensors)->execute(); | |
id(led_power)->execute(true); | |
id(status) = 2; | |
break; | |
case 2: | |
// Then, we read directly the light sensors | |
id(read_light_sensors)->execute(); | |
id(led_power)->execute(false); | |
id(phase_coarse)->update(); | |
id(status) = 1; | |
} | |
ESP_LOGW("sensor", "Status: %i, Interval: %ums, Duration %ums", id(status), interval, millis() - now); | |
last_time = now; | |
- interval: 100ms | |
then: | |
#- component.update: light_sensor_a_dark | |
- if: | |
condition: | |
lambda: |- | |
return id(fastupdate).state; // If fastupdate is on, update immediately | |
then: | |
- lambda: |- | |
id(last_reported_liters__mili) = (int)(1000*(id(liters)+id(phase)/6.0)); | |
id(last_reported_liters) = id(liters); | |
id(last_water_flow) = millis(); | |
- if: | |
condition: | |
lambda: |- | |
return id(debugmodus).state; | |
then: | |
- text_sensor.template.publish: | |
id: debug_json | |
state: !lambda |- | |
return "{\"liters\":" + to_string(id(liters)) + | |
",\"phase\":" + to_string(id(phase)) + | |
",\"last_reported_liters\":" + to_string(id(last_reported_liters)) + | |
",\"last_reported_liters__mili\":" + to_string(id(last_reported_liters__mili)) + | |
",\"aa\":" + to_string(id(aa)) + | |
",\"bb\":" + to_string(id(bb)) + | |
",\"cc\":" + to_string(id(cc)) + | |
",\"max_a\":" + to_string(id(max_a)) + | |
",\"max_b\":" + to_string(id(max_b)) + | |
",\"max_c\":" + to_string(id(max_c)) + | |
",\"min_a\":" + to_string(id(min_a)) + | |
",\"min_b\":" + to_string(id(min_b)) + | |
",\"min_c\":" + to_string(id(min_c)) + | |
",\"upper_bound\":" + to_string(id(upper_bound)) + | |
",\"lower_bound\":" + to_string(id(lower_bound)) + | |
"}"; | |
else: | |
- lambda: |- | |
// If fastupdate is off, only update every 60 seconds | |
if ((millis() - id(last_water_flow)) >= 60000) { | |
id(last_reported_liters__mili) = (int)(1000*(id(liters)+id(phase)/6.0)); | |
id(last_reported_liters) = id(liters); | |
id(last_water_flow) = millis(); | |
} | |
# - interval: 10s # Check every 10 seconds to reduce load, adjust as needed | |
# then: | |
# - lambda: |- | |
# - interval: 1s | |
# then: | |
# # - component.update: report_liters | |
# # - component.update: report_liters_rounded | |
# - logger.log: | |
# level: INFO | |
# tag: time | |
# format: "o:%d" | |
# args: [ 'id(last_water_flow)'] | |
# # - logger.log: | |
# level: INFO | |
# tag: max_average | |
# format: "a:%d b:%d c:%d" | |
# args: [ 'id(max_a)', 'id(max_b)' , 'id(max_c)'] | |
# - logger.log: | |
# level: INFO | |
# tag: min_average | |
# format: "a:%d b:%d c:%d" | |
# args: [ 'id(min_a)', 'id(min_b)' , 'id(min_c)'] | |
# Toggle switch | |
switch: | |
- platform: template | |
optimistic: true | |
id: fastupdate | |
name: Speed mode | |
icon: "mdi:emoticon-cool-outline" | |
- platform: template | |
optimistic: true | |
id: debugmodus | |
name: Debug mode | |
icon: "mdi:bug" | |
turn_on_action: | |
- switch.turn_on: fastupdate | |
turn_off_action: | |
- switch.turn_off: fastupdate | |
# for manual calibration or auto | |
- platform: template | |
id: calibration_mode | |
name: "Manual Calibration" | |
icon: "mdi:tune-vertical" | |
optimistic: true | |
restore_mode: RESTORE_DEFAULT_OFF # <--- important/ maybe made here mistake | |
turn_on_action: | |
- lambda: |- | |
id(manual_calibration) = true; | |
turn_off_action: | |
- lambda: |- | |
id(manual_calibration) = false; | |
text_sensor: | |
- platform: template | |
name: "debug_JSON" | |
id: debug_json | |
# No automatic interval, we'll update manually | |
update_interval: never | |
button: | |
- platform: template | |
name: "Init PIO" | |
on_press: | |
- script.execute: | |
id: init_pio | |
- platform: template | |
name: "Init VEML" | |
on_press: | |
- script.execute: | |
id: init_vemls | |
- platform: template | |
name: "I2C Scan" | |
on_press: | |
- script.execute: | |
id: i2c_scan | |
- platform: template | |
name: "LED On" | |
on_press: | |
- script.execute: | |
id: led_power | |
state: true | |
- platform: template | |
name: "LED Off" | |
on_press: | |
- script.execute: | |
id: led_power | |
state: false | |
- platform: template | |
name: "Read Light sensors" | |
on_press: | |
- script.execute: | |
id: read_light_sensors | |
- platform: template | |
name: "Read Dark Sensors" | |
on_press: | |
- script.execute: | |
id: read_dark_sensors | |
script: | |
- id: i2c_write | |
parameters: | |
addr: int | |
reg: int | |
val: int | |
then: | |
- lambda: |- | |
uint8_t i2c_addr = static_cast<uint8_t>(addr); | |
uint8_t reg_addr = static_cast<uint8_t>(reg); | |
uint8_t value = static_cast<uint8_t>(val); | |
Wire.beginTransmission(i2c_addr); | |
Wire.write(reg_addr); | |
Wire.write(value); | |
if (Wire.endTransmission() != 0) { | |
ESP_LOGE("i2c", "Failed to write to 0x%02X", i2c_addr); | |
} | |
- id: i2c_write_2 | |
parameters: | |
addr: int | |
reg: int | |
val0: int | |
val1: int | |
then: | |
- lambda: |- | |
uint8_t i2c_addr = static_cast<uint8_t>(addr); | |
uint8_t reg_addr = static_cast<uint8_t>(reg); | |
uint8_t value0 = static_cast<uint8_t>(val0); | |
uint8_t value1 = static_cast<uint8_t>(val1); | |
Wire.beginTransmission(i2c_addr); | |
Wire.write(reg_addr); | |
Wire.write(value0); | |
Wire.write(value1); | |
if (Wire.endTransmission() != 0) { | |
ESP_LOGE("i2c", "Failed to write to 0x%02X", i2c_addr); | |
} | |
- id: init_pio | |
then: | |
- script.execute: | |
id: i2c_write | |
addr: 0x43 | |
reg: 0x03 # IO_DIRECTION | |
val: 0b01111000 # SENS0 | SENS1 | SENS2 | LED | |
- script.execute: | |
id: i2c_write | |
addr: 0x43 | |
reg: 0x05 # IO_OUTPUT | |
val: 0x00 | |
- script.execute: | |
id: i2c_write | |
addr: 0x43 | |
reg: 0x07 # PI4IO_OUTPUT_HI_IMPEDANCE | |
val: 0b10000111 # ~(SENS0 | SENS1 | SENS2 | LED) | |
- id: set_pio_pin | |
parameters: | |
pin: int | |
val: int | |
then: | |
- lambda: |- | |
uint8_t pin_number = static_cast<uint8_t>(pin); | |
uint8_t value = static_cast<uint8_t>(val); | |
Wire.beginTransmission(0x43); | |
Wire.write(0x05); | |
if (Wire.endTransmission(false) != 0) { | |
ESP_LOGE("i2c", "Failed to read from 0x%02X", 0x43); | |
} | |
Wire.requestFrom(0x43, 1); | |
uint8_t current = Wire.read(); | |
if (value) | |
current |= (1 << pin_number); | |
else | |
current &= ~(1 << pin_number); | |
id(i2c_write)->execute(0x43, 0x05, current); | |
- id: init_veml | |
then: | |
- script.execute: | |
id: i2c_write_2 | |
addr: 0x48 | |
reg: 0x00 # VEML6030_ALS_SD | |
val0: 0 | |
val1: 1 | |
- id: init_vemls | |
then: | |
- script.execute: | |
id: set_pio_pin | |
pin: 3 | |
val: 1 | |
- script.execute: | |
id: init_veml | |
- script.execute: | |
id: set_pio_pin | |
pin: 4 | |
val: 1 | |
- script.execute: | |
id: init_veml | |
- script.execute: | |
id: set_pio_pin | |
pin: 5 | |
val: 1 | |
- script.execute: | |
id: init_veml | |
- id: read_sensor | |
parameters: | |
sensor_id: int | |
then: | |
- lambda: |- | |
if (sensor_id == 0) | |
id(set_pio_pin)->execute(3, true); | |
else if (sensor_id == 1) | |
id(set_pio_pin)->execute(4, true); | |
else if (sensor_id == 2) | |
id(set_pio_pin)->execute(5, true); | |
Wire.beginTransmission(0x48); | |
Wire.write(0x04); | |
if (Wire.endTransmission(false) != 0) { | |
ESP_LOGE("i2c", "Failed to read from 0x%02X", 0x43); | |
} | |
Wire.requestFrom(0x48, 2); | |
uint8_t data[2]; | |
data[0] = Wire.read(); | |
data[1] = Wire.read(); | |
id(raw_sensor) = (static_cast<uint16_t>(data[1]) << 8) | data[0]; | |
if (sensor_id == 0) | |
id(set_pio_pin)->execute(3, false); | |
else if (sensor_id == 1) | |
id(set_pio_pin)->execute(4, false); | |
else if (sensor_id == 2) | |
id(set_pio_pin)->execute(5, false); | |
- id: led_power | |
parameters: | |
state: bool | |
then: | |
- lambda: |- | |
id(set_pio_pin)->execute(6, state); | |
- id: read_dark_sensors | |
then: | |
- lambda: |- | |
id(read_sensor)->execute(0); | |
id(light_sensor_a_dark) = id(raw_sensor); | |
id(read_sensor)->execute(1); | |
id(light_sensor_b_dark) = id(raw_sensor); | |
id(read_sensor)->execute(2); | |
id(light_sensor_c_dark) = id(raw_sensor); | |
- id: read_light_sensors | |
then: | |
- lambda: |- | |
id(read_sensor)->execute(0); | |
id(light_sensor_a_light) = id(raw_sensor); | |
id(read_sensor)->execute(1); | |
id(light_sensor_b_light) = id(raw_sensor); | |
id(read_sensor)->execute(2); | |
id(light_sensor_c_light) = id(raw_sensor); | |
- id: i2c_scan | |
then: | |
- lambda: |- | |
ESP_LOGD("i2c", "Starting I2C scan..."); | |
for (uint8_t address = 1; address < 127; address++) { | |
Wire.beginTransmission(address); | |
if (Wire.endTransmission() == 0) { | |
ESP_LOGI("i2c", "Device found at 0x%02X", address); | |
} | |
} | |
ESP_LOGD("i2c", "I2C scan end."); | |
sensor: | |
# liters | |
- platform: template | |
name: "water_liter_sensor" | |
id: report_liters | |
force_update: false | |
device_class: "water" | |
unit_of_measurement: "mL" | |
accuracy_decimals: 0 | |
state_class: total_increasing | |
lambda: |- | |
if (id(liters) < 2){ | |
return 0; | |
} | |
return id(last_reported_liters__mili); | |
- platform: template | |
name: "liters" | |
id: report_liters_rounded | |
force_update: false | |
device_class: "water" | |
unit_of_measurement: "L" | |
accuracy_decimals: 0 | |
state_class: total_increasing | |
lambda: |- | |
if (id(last_reported_liters) < 2){ | |
return 0; | |
} | |
return id(last_reported_liters); | |
- platform: template | |
id: phase_coarse | |
device_class: "water" | |
state_class: "total" | |
internal: true | |
on_value: | |
if: | |
condition: | |
# Same syntax for is_off | |
switch.is_on: fastupdate | |
then: | |
- component.update: report_liters | |
- component.update: report_liters_rounded | |
lambda: |- | |
static bool first = true; | |
// Read the raw values from the sensors | |
int a = id(light_sensor_a_light) - id(light_sensor_a_dark); | |
int b = id(light_sensor_b_light) - id(light_sensor_b_dark); | |
int c = id(light_sensor_c_light) - id(light_sensor_c_dark); | |
// Update history buffers | |
id(a_history)[id(history_index)] = a; | |
id(b_history)[id(history_index)] = b; | |
id(c_history)[id(history_index)] = c; | |
// Compute moving averages | |
id(smoothed_a) = (id(a_history)[0] + id(a_history)[1] + id(a_history)[2]) / 3; | |
id(smoothed_b) = (id(b_history)[0] + id(b_history)[1] + id(b_history)[2]) / 3; | |
id(smoothed_c) = (id(c_history)[0] + id(c_history)[1] + id(c_history)[2]) / 3; | |
// ESP_LOGE("data", ">> %i, %i, %i", id(smoothed_a), id(smoothed_b), id(smoothed_c)); | |
// Move to the next history index | |
id(history_index) = (id(history_index) + 1) % 3; | |
// Use the smoothed values instead of raw values | |
id(aa) = id(smoothed_a); | |
id(bb) = id(smoothed_b); | |
id(cc) = id(smoothed_c); | |
int max = id(upper_bound); | |
int min = id(lower_bound); | |
if (a < min || b < min || c < min ){ | |
ESP_LOGW("light_level", "Too dark, ignoring this measurement"); | |
return 0; | |
} | |
if (a > max || b > max || c > max){ | |
ESP_LOGW("light_level", "Too bright, ignoring this measurement"); | |
return 0; | |
} | |
if (first){ | |
id(min_a)= a; | |
id(min_b)= b; | |
id(min_c)= c; | |
id(max_a)= 0; | |
id(max_b)= 0; | |
id(max_c)= 0; | |
first = false; | |
return 0; | |
} | |
float alpha_cor = 0.001; | |
if (id(liters) < 2) { | |
alpha_cor = 0.1; // when 2 liter not found correct harder | |
if (id(liters) < 0) { | |
id(liters) = 0; | |
} | |
} | |
auto mini_average = [](float x, float y, float alpha_cor){ | |
if ((x + 5) <= y && y > 10) { | |
return x; | |
} else { | |
return (1 - alpha_cor) * x + alpha_cor * y; | |
} | |
}; | |
auto max_average = [](int x, int y, float alpha_cor) { | |
// ESP_LOGI("main", "x: %d, y: %d, a: %f",x,y,alpha_cor); | |
if ((x - 5) >= y && y < 2500) { | |
return x; | |
} else { | |
return (int)((1 - alpha_cor) * (float)x + alpha_cor * (float)y); | |
} | |
}; | |
id(min_a)= mini_average(id(min_a), a, alpha_cor); | |
id(min_b)= mini_average(id(min_b), b, alpha_cor); | |
id(min_c)= mini_average(id(min_c), c, alpha_cor); | |
id(max_a)= max_average(id(max_a), a, alpha_cor); | |
id(max_b)= max_average(id(max_b), b, alpha_cor); | |
id(max_c)= max_average(id(max_c), c, alpha_cor); | |
if (id(manual_calibration)) { | |
// Manual offsets | |
a -= id(manual_offset_a); | |
b -= id(manual_offset_b); | |
c -= id(manual_offset_c); | |
} else { | |
// Auto-calibration offsets | |
a -= (id(min_a) + id(max_a)) >> 1; | |
b -= (id(min_b) + id(max_b)) >> 1; | |
c -= (id(min_c) + id(max_c)) >> 1; | |
} | |
short pn[5]; | |
if (id(phase) & 1) | |
pn[0] = a + a - b - c, pn[1] = b + b - a - c, | |
pn[2] = c + c - a - b; // same | |
else | |
pn[0] = b + c - a - a, // less | |
pn[1] = a + c - b - b, // more | |
pn[2] = a + b - c - c; // same | |
pn[3] = pn[0], pn[4] = pn[1]; | |
short i = id(phase) > 2 ? id(phase) - 3 : id(phase); | |
if (pn[i + 2] < pn[i + 1] && pn[i + 2] < pn[i]){ | |
id(last_water_flow) = millis(); | |
if (pn[i + 1] > pn[i]) | |
id(phase)++; | |
else | |
id(phase)--; | |
} | |
if (id(phase) == 6) | |
id(liters)++, id(phase) = 0; | |
else if (id(phase) == -1) | |
id(liters)--, id(phase) = 5; | |
return id(liters); | |
globals: | |
- id: status | |
type: int | |
initial_value: "0" | |
restore_value: no | |
- id: raw_sensor | |
type: int | |
- id: light_sensor_a_dark | |
type: int | |
- id: light_sensor_b_dark | |
type: int | |
- id: light_sensor_c_dark | |
type: int | |
- id: light_sensor_a_light | |
type: int | |
- id: light_sensor_b_light | |
type: int | |
- id: light_sensor_c_light | |
type: int | |
- id: last_reported_liters | |
type: int | |
initial_value: '0' | |
- id: last_reported_liters__mili | |
type: int | |
initial_value: '0' | |
- id: last_water_flow | |
type: uint32_t | |
initial_value: '0' | |
- id: phase | |
type: int | |
initial_value: "0" | |
- id: liters | |
type: int | |
initial_value: "0" | |
- id: aa | |
type: int | |
initial_value: "0" | |
- id: bb | |
type: int | |
initial_value: "0" | |
- id: cc | |
type: int | |
initial_value: "0" | |
- id: max_a | |
type: int | |
- id: max_b | |
type: int | |
- id: max_c | |
type: int | |
- id: min_a | |
type: int | |
- id: min_b | |
type: int | |
- id: min_c | |
type: int | |
- id: upper_bound | |
initial_value: "1500" | |
type: int | |
- id: lower_bound | |
type: int | |
initial_value: "5" | |
# Switch that toggles between auto-calibration and manual calibration | |
- id: manual_calibration | |
type: bool | |
restore_value: yes # <--- important | |
initial_value: "false" | |
# Manual offsets for a, b, c | |
- id: manual_offset_a | |
type: int | |
initial_value: "0" | |
- id: manual_offset_b | |
type: int | |
initial_value: "0" | |
- id: manual_offset_c | |
type: int | |
initial_value: "0" | |
# Parameters for the moving average of size 3 | |
- id: a_history | |
type: int[3] | |
initial_value: "{0, 0, 0}" | |
- id: b_history | |
type: int[3] | |
initial_value: "{0, 0, 0}" | |
- id: c_history | |
type: int[3] | |
initial_value: "{0, 0, 0}" | |
- id: history_index | |
type: int | |
initial_value: "0" | |
- id: smoothed_a | |
type: int | |
initial_value: "0" | |
- id: smoothed_b | |
type: int | |
initial_value: "0" | |
- id: smoothed_c | |
type: int | |
initial_value: "0" | |
number: | |
- platform: template | |
id: offset_a_number | |
name: "Offset A (0-3300)" | |
restore_value: true # <--- important | |
optimistic: true | |
initial_value: 0 | |
min_value: 0 | |
max_value: 3300 | |
step: 1 | |
on_value: | |
then: | |
- lambda: |- | |
id(manual_offset_a) = (int) x; | |
- platform: template | |
id: offset_b_number | |
name: "Offset B (0-3300)" | |
restore_value: true # <--- important | |
optimistic: true | |
initial_value: 0 | |
min_value: 0 | |
max_value: 3300 | |
step: 1 | |
on_value: | |
then: | |
- lambda: |- | |
id(manual_offset_b) = (int) x; | |
- platform: template | |
id: offset_c_number | |
name: "Offset C (0-3300)" | |
restore_value: true # <--- important | |
optimistic: true | |
initial_value: 0 | |
min_value: 0 | |
max_value: 3300 | |
step: 1 | |
on_value: | |
then: | |
- lambda: |- | |
id(manual_offset_c) = (int) x; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment