Mine generally don’t change either. ALTHOUGH a few weeks ago I happened to notice that my gas wasn’t getting picked up anymore. Nothing about the position of the sensor changed, but it just randomly started reporting different max/min values. I’ve also had a sensor keep reporting values, but essentially stop working (the values change but don’t really track to anything anymore). A quick change out of the sensor and all worked again.
I think these are just really cheap sensors both in price and reliability.
I’ve changed the code up a bit to handle changes to sensor reading that occur at boot time. As long as you’re reading from the x-axis this code will automatically find your max/min values. It also exposes an option to reset the “calibration” in the event it stop reporting.
This now displays the min and max axis readings (and lets you set them from HA) and lets you set your frequency interval
So here’s everything before it’s fully calibrated (just powered on, waiting for gas to be used)
I haven’t found a way for it to recognize when it has drifted away from being calibrated and is no longer reporting, but for the time being I’m satisfied with this code.
Anyone who wants to use it will need to change the wifi info to fit their setup unless you’re using the same name for your !secret, and you’ll need to enter your api encryption information, but I wanted to include the full code (minus my security information) for reference.
substitutions:
name: "gas-meter"
friendly_name: "Gas Meter"
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
# level: DEBUG
# Enable Home Assistant API
api:
encryption:
key: ""
ota:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: ""
password: ""
captive_portal:
globals:
- id: gas_counter_total
type: long
restore_value: no
initial_value: '0'
- id: gas_counter
type: long
restore_value: no
initial_value: '0'
- id: gas_high
type: bool
restore_value: no
initial_value: 'false'
- id: gas_max_mid
type: float
restore_value: no
initial_value: '-2000.0'
- id: gas_min_mid
type: float
restore_value: no
initial_value: '2000.0'
interval:
- interval: 0.1s
then:
- lambda: |-
auto maxCall = id(gas_max).make_call();
auto minCall = id(gas_min).make_call();
// Set min and max values using the x-axis sensor
if (id(gasx).state > id(gas_max).state) {
maxCall.set_value(id(gasx).state);
maxCall.perform();
if (id(calibrated).state ) {
id(gas_max_mid) = (id(gas_max).state-id(gas_min).state)*0.75+id(gas_min).state;
id(gas_min_mid) = (id(gas_max).state-id(gas_min).state)*0.25+id(gas_min).state;
}
}
if (id(gasx).state < id(gas_min).state) {
minCall.set_value(id(gasx).state);
minCall.perform();
if (id(calibrated).state ) {
id(gas_max_mid) = (id(gas_max).state-id(gas_min).state)*0.75+id(gas_min).state;
id(gas_min_mid) = (id(gas_max).state-id(gas_min).state)*0.25+id(gas_min).state;
}
}
// If the device is calibrated process readings from teh x-axis sensor to calculate gas usage
if (id(calibrated).state ) {
if (id(gasx).state > id(gas_max_mid) && !id(gas_high)) {
id(gas_counter_total) += 1;
id(gas_counter) += 1;
id(gas_high) = true;
} else if (id(gasx).state < id(gas_min_mid) && id(gas_high)) {
id(gas_high) = false;
}
// If the device is not calibrated, wait until the difference between the min/max value is greater than 10 and less than 1000
// On first boot when device is calibrating, this might result in a couple seconds of bad readings, but what can you do?
} else if ((fabs(id(gas_max).state-id(gas_min).state) > 10) && (fabs(id(gas_max).state-id(gas_min).state) < 1000.0)) {
id(calibrated).publish_state(true);
id(gas_max_mid) = (id(gas_max).state-id(gas_min).state)*0.75+id(gas_min).state;
id(gas_min_mid) = (id(gas_max).state-id(gas_min).state)*0.25+id(gas_min).state;
}
i2c:
sda: GPIO21
scl: GPIO22
#scan: true
frequency: 10kHz
# Reports back to HA when device considers itself to be "calibrated"
binary_sensor:
- platform: template
name: "${friendly_name} Calibrated"
id: calibrated
device_class: CONNECTIVITY
publish_initial_state: true
# Uncomment whichever axis gives you the best reading
# For initial setup uncomment all of them and comment out internal: true so you can see historical values in HA
# Then choose the Axis that gives you the greatest variation in it's reading (should look like a sine wave)
# Once you've selected the best axis comment out the rest and set internal: true again.
# If an axis other than X is used you will need to change all the gasx references to gasy or gasz depending on which axis you've selected.
sensor:
- platform: qmc5883l
address: 0x0D
field_strength_x:
name: "Gas Meter Field Strength X"
id: gasx
internal: true
# field_strength_y:
# name: "Gas Meter Field Strength Y"
# id: gasy
# internal: true
# field_strength_z:
# name: "Gas Meter Field Strength Z"
# id: gasz
# internal: true
# heading:
# name: "Gas Meter Heading"
# internal: true
range: 200uT
oversampling: 512x
update_interval: 0.1s
#8 counts per cubic foot, multiplied by 2 to get per minute based on update interval
- platform: template
name: "${friendly_name} Gas Rate"
lambda: |-
if (id(calibrated).state ) {
int temp = id(gas_counter);
id(gas_counter) -= temp;
float temp2 = temp;
float temp3 = (temp2/id(frequency).state)*2;
return temp3;
} else {
return 0;
}
update_interval: 30s
unit_of_measurement: ft³/min
device_class: 'gas'
- platform: template
name: "${friendly_name} Gas Total"
lambda: |-
if (id(calibrated).state ) {
float temp = id(gas_counter_total);
return temp/id(frequency).state;
} else {
return 0;
}
update_interval: 1s
unit_of_measurement: 'ft³'
state_class: 'total_increasing'
device_class: 'gas'
# Used to reset calibration. If for any reason your device is no longer recording data then try resetting the calibration
button:
- platform: template
name: "${friendly_name} Reset Calibration"
id: Calibrate
icon: "mdi:chart-histogram"
on_press:
then:
lambda: |-
auto maxCall = id(gas_max).make_call();
auto minCall = id(gas_min).make_call();
maxCall.set_value(-1000.0);
minCall.set_value(1000.0);
maxCall.perform();
minCall.perform();
id(calibrated).publish_state(false);
id(gas_max_mid) = 500.0;
id(gas_min_mid) = -500.0;
# Display some useful variables to HA.
number:
- platform: template
optimistic: true
name: "${friendly_name} Cycle Frequency"
id: frequency
update_interval: never
initial_value: 8
min_value: 1
max_value: 500
step: 1
restore_value: true
- platform: template
optimistic: true
name: "${friendly_name} Max Value"
id: gas_max
update_interval: never
initial_value: -1000.0
min_value: -5000.0
max_value: 5000.0
step: 0.00001
restore_value: false
unit_of_measurement: ft³/min
device_class: 'gas'
- platform: template
optimistic: true
name: "${friendly_name} Min Value"
id: gas_min
update_interval: never
initial_value: 1000.0
min_value: -5000.0
max_value: 5000.0
step: 0.00001
restore_value: false
unit_of_measurement: ft³/min
device_class: 'gas'
I haven’t really dug in and read through all of your post and code, but what you have done looks awesome, and makes me remember why I’m a part of this community.
I don’t have the expertise to do something like this myself, so seeing others do it, helps me learn and understand the details more.
Thanks! I wish I knew enough to have it A) look at all three axes and select the one with the greatest range, and B) have it recognize drift.
And it might also be smart to have to lock in the values after a couple minutes of being “calibrated” so a random bad reading doesn’t skew everything, or do a background calibration while gas is flowing to make sure limits are still accurate.
I just installed my gas meter sensor with the information I learned from this thread. I didn’t realize these types of meters could be read with a magnetometer until I saw this, so thank you!
I started with the code from @brooksben11 but modified it heavily. I had some hysteresis code for calculating sump pump cycles that I leveraged, and I also set it up so that the actual meter reading could be sent via a service call to the ESP device, so that it reflects what you see on your gas bill. But be warned it will revert to zero after an ESP reboot. I also used substitutions and added an uptime timestamp. Code should be able to be leveraged pretty easily by changing the gas meter parameters and changing the three “passwords” (api key, OTA password, and fallback hotspot password).
substitutions:
# Device Naming
devicename: gas-meter
friendly_name: Gas Meter
device_description: Measurement of gas consumption from diaphragm-style gas meter
# Gas Meter Parameters
measurement_unit: ft³
pulses_per_measurement_unit: '1/0.111' # 0.111 ft³ per rev, and 1 pulse per rev.
pulse_peak: '-1.6' # µT; the code will use a set point which is 75% of the range to ensure it triggers
pulse_valley: '-13' # µT; the code will use a set point which is 75% of the range to ensure it triggers
pulse_min_cycle_duration: '0.1' # seconds; anything shorter than this is assumed to be noise and will be ignored
esphome:
name: $devicename
friendly_name: $friendly_name
comment: $device_description
esp8266:
board: nodemcu
# Enable logging
logger:
level: INFO #WARN
# Enable Home Assistant API
api:
encryption:
key: "your_encryption_key"
services:
- service: set_meter_reading
variables:
meter_reading: float
then:
- logger.log:
format: "Setting Meter Reading: %.1f"
args: [meter_reading]
- globals.set:
id: counter_total
value: !lambda |-
return int(meter_reading * ($pulses_per_measurement_unit * 1.0));
##### Set meter reading from HA using the services tab in developer tools:
## service: esphome.<device_name>_set_meter_reading
## data:
## meter_reading: "125000"
ota:
password: "your_ota_password"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "$friendly_name Fallback Hotspot"
password: "your_fallback_hotspot_password"
captive_portal:
# Sync time with Home Assistant.
time:
- platform: homeassistant
id: time_homeassistant
on_time_sync:
- component.update: sensor_uptime_timestamp
# I2C bus setup for communication with magnetometer
i2c:
sda: D1
scl: D2
globals:
- id: counter_total
type: uint32_t
restore_value: no
initial_value: '0'
################################################################################
# Binary Sensors
################################################################################
binary_sensor:
# Hysteresis sensor to trigger pulses
- platform: template
name: "${friendly_name} Pulse Trigger"
id: pulse_trigger
internal: True
lambda: |-
if (id(field_z).state > id(trigger_high_limit).state) {
return true;
} else if (id(field_z).state < id(trigger_low_limit).state) {
return false;
} else {
return {};
}
filters:
- delayed_on: !lambda |-
return $pulse_min_cycle_duration/2.0 * 1000.0;
- delayed_off: !lambda |-
return $pulse_min_cycle_duration/2.0 * 1000.0;
on_press:
then:
# when state transisions from false to true, increment the pulse counter
- lambda: |-
id(counter_total) += 1;
ESP_LOGI("counter", "Counter is %li", id(counter_total));
################################################################################
# Sensors
################################################################################
sensor:
# Uptime sensor.
- platform: uptime
name: ${friendly_name} Uptime Internal
id: sensor_uptime
internal: True
- platform: template
id: sensor_uptime_timestamp
name: "${friendly_name} Uptime"
device_class: timestamp
entity_category: diagnostic
accuracy_decimals: 0
update_interval: never
lambda: |-
static float timestamp = (
id(time_homeassistant).utcnow().timestamp - id(sensor_uptime).state
);
return timestamp;
# Magnetometer
- platform: qmc5883l
address: 0x0D
range: 200uT
oversampling: 512x
update_interval: 0.1s
# heading:
# name: "Heading"
# id: heading
# field_strength_x:
# name: "Field Strength X"
# id: field_x
# field_strength_y:
# name: "Field Strength Y"
# id: field_y
field_strength_z:
name: "Field Strength Z"
id: field_z
internal: True
# Trigger limits
- platform: template
name: "Trigger High Limit"
id: trigger_high_limit
internal: True
lambda: |-
return $pulse_peak - 0.25 * ($pulse_peak - $pulse_valley);
- platform: template
name: "Trigger Low Limit"
id: trigger_low_limit
internal: True
lambda: |-
return $pulse_valley + 0.25 * ($pulse_peak - $pulse_valley);
# Gas meter reading provided to Home Assistant
- platform: template
name: None
id: value
internal: False
device_class: "gas"
unit_of_measurement: $measurement_unit
state_class: "total_increasing"
icon: mdi:meter-gas
accuracy_decimals: 1
update_interval: 30s
lambda: |-
return id(counter_total) / ($pulses_per_measurement_unit * 1.0);
# Gas meter flow rate provided to Home Assistant (average over past 5 minutes)
- platform: template
name: Rate
id: rate
internal: False
device_class: "gas"
unit_of_measurement: $measurement_unit/min
state_class: "measurement"
icon: mdi:meter-gas
accuracy_decimals: 2
update_interval: 300s
lambda: |-
static uint32 previous_counter = id(counter_total);
int delta_counts = id(counter_total) - previous_counter;
previous_counter = id(counter_total);
return delta_counts / ($pulses_per_measurement_unit * 1.0) * 60.0/300.0;
The only complicated piece of code is the binary sensor. Think of it like an over-center toggle switch where it takes a certain amount of effort to flip the switch. In this code, the toggle doesn’t become true unless the magnetometer reaches 75% of the max value, and doesn’t become false unless it falls below 75% of the minimum value. Any reading between that won’t change the state of the binary sensor so it remains in whatever state it was already in.
In addition to the hysteresis, there is also a debounce where the value has to remain past the threshold for a set amount of time before the binary sensor is allowed to toggle.
Every time the binary sensor toggles from false to true, the meter reading is incremented by one unit.
From what I’ve seen, the readings from the magnetometer are pretty clean so this stuff isn’t strictly necessary but I already had the code and making the sensor extra robust doesn’t hurt anything.
I made the mistake of asking my gas company whether it is acceptable to attach a small non-invasive device to the side of the meter for purposes of reading its measurement. I showed the hardware, including part numbers of the device and I even pointed to the forum pages. After ignoring me for two weeks, I sent another reminder and within the hour, the answer returned with a resounding “NO”. In fact, I could be liable for penalties and meter tampering which could lead to a criminal charge. Of course they had no alternative method to offer. They said I should purchase and install my own gas meter downstream of the revenue meter, and then attach my gizmo to that. I pushed back explaining that other gas utilities either allow meter reading devices or they offer their own smart gateway that I could lease. In reply, they provided a link to their website explaining the benefits of caulking my windows to reduce energy use. That was their not-so-subtle way of telling me to “f$#k off”. Customer service at its finest.
Who is your gas company?
I saw the tech come around the other day to check the meter reading and was paranoid they would say something, The guy didn’t seem to notice it or care.
To be frank, I’m not sure what you were expecting. There is no way they were going to provide written authorization for you to attach some handmade device onto their meter. Their non-response was probably the best they could offer you, and then you asked again. You’ll have to make a decision for yourself whether there’s truly risk that you’ll face fines or criminal charges for measuring the magnetic field near your meter.
This thread is more focused on our “dumb” gas meters, we had to go this route since there is no signal to read like the Smart meters have. I know personally my Gas and water meters are both “dumb” or “non-smart” so no signal to read and had to revert to magnetic fields (Gas) and optical recognition (water).
I used that to read my water meter until last year when the water company swapped the meter with a different type; now I use an ESPcam with AI on the edge. I tried to read my gas meter via the same RTL/SDR method but came to the conclusion that my gas meter only sends out a reading when a command is sent to it. So only once/month when the utility company ‘tickles’ it. Not useful for HA consumption. YMMV.
I also use AI on the Edge for my water meter. it’s good being in the basement protected, but I can’t see a good setup for the gas meter being outside and vulnerable to the environment.
I made a small improvement to @abishur’s code. I’ve added drift compensation and as a benefit measurement error detection. This is simply achieved by slowly reducing the max value and increasing the min value. If the min and max meet there is an issue with the reading and calibrated gets set back to false.
Edit: I’ve fixed an issue I had with the first value being erroneous. I just added a short delay to the interval.
substitutions:
device_name: "gas-meter"
friendly_name: gas-meter
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
# Enable logging
logger:
logs:
qmc5883l: INFO
# Enable Home Assistant API
api:
encryption:
key: ""
ota:
password: ""
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
domain: ""
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${friendly_name} Fallback Hotspot"
password: ""
captive_portal:
web_server:
port: 80
i2c:
sda: GPIO8
scl: GPIO9
globals:
- id: gas_counter_total
type: long
restore_value: no
initial_value: '0'
- id: gas_counter
type: long
restore_value: no
initial_value: '0'
- id: gas_high
type: bool
restore_value: no
initial_value: 'false'
- id: gas_max_mid
type: float
restore_value: no
initial_value: '-2000.0'
- id: gas_min_mid
type: float
restore_value: no
initial_value: '2000.0'
- id: gas_max
type: float
restore_value: no
initial_value: '-1000.0'
- id: gas_min
type: float
restore_value: no
initial_value: '1000.0'
interval:
- interval: 0.1s
startup_delay: 5s
then:
- lambda: |-
// Set min and max values using the x-axis sensor
if (id(gasz).state > id(gas_max)) {
id(gas_max) = (id(gasz).state);
if (id(calibrated).state ) {
id(gas_max_mid) = (id(gas_max)-id(gas_min))*0.75+id(gas_min);
id(gas_min_mid) = (id(gas_max)-id(gas_min))*0.25+id(gas_min);
}
}
if (id(gasz).state < id(gas_min)) {
id(gas_min) = (id(gasz).state);
if (id(calibrated).state ) {
id(gas_max_mid) = (id(gas_max)-id(gas_min))*0.75+id(gas_min);
id(gas_min_mid) = (id(gas_max)-id(gas_min))*0.25+id(gas_min);
}
}
// If the device is calibrated process readings from the x-axis sensor to calculate gas usage
if (id(calibrated).state ) {
if (id(gasz).state > id(gas_max_mid) && !id(gas_high)) {
id(gas_counter_total) += 1;
id(gas_counter) += 1;
id(gas_high) = true;
}
else if (id(gasz).state < id(gas_min_mid) && id(gas_high)) {
id(gas_high) = false;
}
// Slowly decrease max and increase min to deal with drift
float diff = fabs(id(gas_max)-id(gas_min));
id(gas_max) -= diff*0.00001;
id(gas_min) += diff*0.00001;
}
// If the device is not calibrated, wait until the difference between the min/max value is greater than 10 and less than 1000
// On first boot when device is calibrating, this might result in a couple seconds of bad readings, but what can you do?
if ((fabs(id(gas_max)-id(gas_min)) > 10) && (fabs(id(gas_max)-id(gas_min)) < 1000.0)) {
id(calibrated).publish_state(true);
}
// If the sensor isn't reading correctly, the drift compensation above will cause min and max to converge. Setting calibrated to false allows this to be detected.
else
{
id(calibrated).publish_state(false);
}
# Reports back to HA when device considers itself to be "calibrated"
binary_sensor:
- platform: template
name: "Calibrated"
id: calibrated
device_class: RUNNING
publish_initial_state: true
# Uncomment whichever axis gives you the best reading
# For initial setup uncomment all of them and comment out internal: true so you can see historical values in HA
# Then choose the Axis that gives you the greatest variation in it's reading (should look like a sine wave)
# Once you've selected the best axis comment out the rest and set internal: true again.
# If an axis other than X is used you will need to change all the gasx references to gasy or gasz depending on which axis you've selected.
sensor:
- platform: qmc5883l
address: 0x0D
# field_strength_x:
# name: "Gas Meter Field Strength X"
# id: gasx
# internal: true
# field_strength_y:
# name: "Gas Meter Field Strength Y"
# id: gasy
# internal: true
field_strength_z:
name: "Gas Meter Field Strength Z"
id: gasz
internal: true
# heading:
# name: "Gas Meter Heading"
# internal: true
range: 200uT
oversampling: 512x
update_interval: 0.1s
#8 counts per cubic foot, multiplied by 2 to get per minute based on update interval
- platform: template
name: "Gas Rate"
lambda: |-
if (id(calibrated).state ) {
int temp = id(gas_counter);
id(gas_counter) -= temp;
float temp2 = temp;
float temp3 = (temp2*id(ratio).state)*2;
return temp3;
} else {
return 0;
}
update_interval: 30s
unit_of_measurement: dm³/min
device_class: 'gas'
- platform: template
name: "Gas Total"
lambda: |-
if (id(calibrated).state ) {
float temp = id(gas_counter_total);
return temp*id(ratio).state*.001;
} else {
return 0;
}
update_interval: 1s
accuracy_decimals: 3
unit_of_measurement: 'm³'
state_class: 'total_increasing'
device_class: 'gas'
- platform: template
name: "Max Value"
lambda: return id(gas_max);
update_interval: 1s
unit_of_measurement: uT
- platform: template
name: "Min Value"
lambda: return id(gas_min);
update_interval: 1s
unit_of_measurement: uT
# Used to reset calibration. If for any reason your device is no longer recording data then try resetting the calibration
button:
- platform: template
name: "Reset Calibration"
id: Calibrate
icon: "mdi:chart-histogram"
on_press:
then:
lambda: |-
id(gas_max) = -1000;
id(gas_min) = 1000;
id(calibrated).publish_state(false);
id(gas_max_mid) = 500.0;
id(gas_min_mid) = -500.0;
# Display some useful variables to HA.
number:
- platform: template
optimistic: true
name: "dm³/rev"
id: ratio
update_interval: never
initial_value: 8
min_value: 1
max_value: 500
step: 1
restore_value: true
Here is my project that allows reading a gas meter or a water meter. It supports calibration across all axis. It’s a package that allows easy installation. All configuration is done via the UI.
Is this based on the same code from this topic, or are you just providing other options?
I’ve been having trouble with getting readings from mine lately and might try your project.