Hi all,
Here’s how I passively read my water meter. The water company already uses the reed switch terminals, and have it all sealed-up tamper-proof.
I’m using a cheap 3-axis magnetometer (digital compass) to read the rotating magnet that the nutating disc spins.
Here’s my dirty, annotated, but functional code:
substitutions:
device_name: "esp8266-qmc5883l-water"
device_label: "Digital Magnetometer Water"
device_nickname: "Magnetometer"
device_static_ip: 192.168.xx.xxx
esphome:
name: ${device_name}
friendly_name: ${device_label}
esp8266:
board: d1_mini
restore_from_flash: true
# Enable logging
logger: # One example disabled the logger and is using hardware UART2
baud_rate: 0
level: DEBUG
preferences:
flash_write_interval: 60min
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
ota:
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip:
static_ip: ${device_static_ip}
gateway: !secret gateway
subnet: !secret subnet
dns1: !secret dns1
# Turn Off Power Save Mode
#power_save_mode: none # none is already the default for an ESP8266
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Water Meter Fallback"
password: "your_password"
captive_portal:
# Creates the device page
web_server:
port: 80
number:
- platform: template
name: "${device_nickname} Set Reading"
optimistic: True
min_value: 0
max_value: 99999
step: 1
id: act_reading
entity_category: config
mode: box
icon: 'mdi:counter'
set_action:
then:
- lambda: |-
int d;
d = floor(x);
id(water_counter_total) = d ;
# QMC5883L Sensor Configuration #
# https://esphome.io/components/sensor/qmc5883l.html
globals:
- id: water_counter_total # increasing count
type: long
restore_value: yes
initial_value: '0'
- id: water_counter # current count
type: long
restore_value: no
initial_value: '0'
- id: water_high # high/low threshold for count
type: bool
restore_value: no
initial_value: 'false'
interval: # set both the high and low thresholds below for a trigger. Y-Axiz range -69 to -121 when cycling slowly. Using -90 to -100.
- interval: 10ms # this is why the water meter was slipping, 250 ms was way too slow.
then:
- lambda: |-
if (id(qmc5883l_y).state > -90 && !id(water_high)) {
id(water_counter_total) += 1;
id(water_counter) += 1;
id(water_high) = true;
id(led).turn_on();
} else if (id(qmc5883l_y).state < -100 && id(water_high)) {
id(water_high) = false;
id(led).turn_off();
}
i2c:
sda: GPIO4 #D2, SDA
scl: GPIO5 #D1, SCL
frequency: 100kHz # Supports both 100kHz and 400kHz. Fastest sensor reading is 200 Hz, so low-speed is just fine.
# scan: true # Probably don't need to scan, the address is known at 0x0D
id: bus_a # Others omitted this line?
sensor:
- platform: qmc5883l
address: 0x0D
# field_strength_x:
# name: "QMC5883L X-axis" # Not using.
# id: qmc5883l_x
field_strength_y:
name: "QMC5883L Y-axis" # Using this one, hide others.
id: qmc5883l_y
internal: true # Change to false if you need to see readings. This will log a ton of data.
# field_strength_z:
# name: "QMC5883L Z-axis" # Not using.
# id: qmc5883l_z
# heading:
# name: "qMC5883L Heading" # once I have the best X, Y, or Z sensor for reading the meter, I can comment out the other two sensors as well.
oversampling: 128x # 512x (default), 256x, 128x, 64x (give other settings a try, see if it reduces noise)
range: 200uT # 200 uT, 800 uT
update_interval: 10ms # Make sure to also set calculation interval above too!
# Oversampling rate is tied to refresh:
# 512x = 10 Hz
# 256x = 50 Hz
# 128x = 100 Hz
# 64x = 200 Hz
# QMC5883L has a maximum output of 200 Hz.
# Can read at 10, 50, 100 or 200 Hz:
# 200 Hz = 5ms
# 100 Hz = 10ms
# 50 Hz = 20ms
# 10 Hz = 100ms
# “maximum 75 pulses/second on 5/8” T10" (another source below claims 77 pulses/sec)
# The reed switch in the meter generates 2X pulses per rotation.
# My magnetometer generates 1X waveform per rotation. I get half the pulses of a reed switch.
# 77 pulses per second /2 x 60 = 2310 rpm.
# 75 pulses per second /2 x 60 = 2250 rpm.
# Sampling a waveform should be 2X or more, so at least 77 Hz which is 12.98ms
# The next compatible HARDWARE sample rate above 77 Hz is 100 Hz or 10ms.
- platform: template
name: "Nutating Disc Count" # This works, doesn't miss a drop from low to high flows.
lambda: |-
float temp1 = id(water_counter_total);
return temp1;
update_interval: 1s
state_class: 'total_increasing'
accuracy_decimals: 0
- platform: template
name: "Nutating Disc RPM" # this seems to work, number is plausible.
lambda: |-
int temp2 = id(water_counter);
id(water_counter) -= temp2;
return temp2 * (6);
update_interval: 10s
unit_of_measurement: "rpm"
accuracy_decimals: 0
- platform: template
name: "Water Flow L/min"
lambda: |-
int temp3 = id(water_counter);
id(water_counter) -= temp3;
return temp3 * (6 * 0.032736241);
update_interval: 10s
unit_of_measurement: "L/min"
accuracy_decimals: 2
- platform: template
name: "Water Total (L)" # This works, doesn't miss a drop from low to high flows.
lambda: |-
return ((float)id(water_counter_total) * (0.032736241));
update_interval: 10s
unit_of_measurement: 'L'
accuracy_decimals: 2
state_class: 'total_increasing'
device_class: 'water'
- platform: template
name: "Water Total (m³)" # This works, doesn't miss a drop from low to high flows.
lambda: |-
return ((float)id(water_counter_total) * (0.032736241 * 0.001));
update_interval: 60s
unit_of_measurement: 'm³'
accuracy_decimals: 3
state_class: 'total_increasing'
device_class: 'water'
# 5/8" Neptune T-10 Calibration:
# Source: https://www.riotronics.com/wp-content/uploads/2019/11/NT10-4P-WaterRead-pdf3.01.pdf
# Riotronics makes a device that fits between the meter head and the base, they say:
# 0.004324 gallons per pulse
# 231.24 pulses per gallon
# Max 20 gpm or 77 pulses/sec
# My sensor gets 1/2 the pulses of a reed switch. See below.
# In metric:
# 0.032736241 L per rotation
# 30.54718459 rotations per litre
# 0.000032736 m3 per rotation
# A rotating magnet next to a reed switch results in 2X closes per revolution.
# When the magnet axis is parallel, the switch closes.
# When the magnet axis is perpendicular, the switch opens.
# Although the poles reverse, they still induce opposite poles in the reed switch.
# A reed switch is an omni-polar device.
- platform: wifi_signal
name: "${device_nickname} WiFi Signal"
update_interval: 60s
entity_category: diagnostic
filters:
- sliding_window_moving_average:
window_size: 6
send_every: 6
- platform: uptime
name: "${device_nickname} Uptime"
id: uptime_sensor
update_interval: 60s
entity_category: diagnostic
on_raw_value:
then:
- text_sensor.template.publish:
id: uptime_human
state: !lambda |-
int seconds = round(id(uptime_sensor).raw_state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
return (
(days ? to_string(days) + "d " : "") +
(hours ? to_string(hours) + "h " : "") +
(minutes ? to_string(minutes) + "m " : "") +
(to_string(seconds) + "s")
).c_str();
text_sensor:
- platform: template
name: "${device_nickname} Uptime Human Readable"
id: uptime_human
icon: mdi:clock-start
entity_category: diagnostic
# LED flashes faster/slower as magnet rotates on water meter.
light:
- platform: status_led
name: "On Board LED"
pin:
number: GPIO2
inverted: true
switch:
- platform: gpio
id: led
pin:
number: GPIO2
inverted: true
mode: OUTPUT
restore_mode: ALWAYS_OFF
Happy metering!