Skip to content

Instantly share code, notes, and snippets.

@hugokernel
Last active February 27, 2025 08:33
Show Gist options
  • Save hugokernel/0763e8cd93ede264ca25997d4d091c67 to your computer and use it in GitHub Desktop.
Save hugokernel/0763e8cd93ede264ca25997d4d091c67 to your computer and use it in GitHub Desktop.
ESPHome version of muino water meter
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