Skip to main content

Keg Fill Station

KegFillStation.jpg

This BrewskiTech project guides you in building a smart beer transfer and keg-filling scale using basic electronic components on a breadboard. The device features a local LCD display, specific gravity input knob, and physical tare button for precise standalone volume tracking, while fully integrating with Home Assistant.

Features:

  • Measure and display real-time liquid transfer volume in gallons locally and in Home Assistant
  • Automatically calculate volume using user-adjustable specific gravity input
  • Full standalone physical control via local LCD display, rotary encoder, and tare button
  • Built-in calibration tools accessible via Home Assistant device dashboard
  • Auto-tare on boot for quick setup at the beginning of every transfer session

All source files are available in the GitHub repo.

Video Tutorial​

A video tutorial is available on the BrewskiTech YouTube channel, which can be found here.

Building the electronics​

The section of the video tutorial on assembling the electronics starts here.

Electronics Plans​

A fritzing file is available for download in the github repo.

Breadboard Plan
breadboard

Schematic Diagram
schematic

Electronics Materials​

Materials needed:

  • Wemos D1 mini (or ESPHome compatible CPU board)
  • HX711 Load Cell Amplifier with compatible weighing platform
  • Rotary encoder (with push button)
  • 2x16 or 2x8 character LCD display with PCF8574 I2C Serial Interface Adapter
  • 5V USB power supply (an old cellphone charger will work)
  • Half-size breadboard
  • Breadboard jumper wire kit

Setting up the weighing platform​

Ensure your load cells are securely mounted to a rigid base platform so that weight is evenly distributed. Route your HX711 and peripheral wiring cleanly to your breadboard enclosure, taking care to secure the data and clock lines away from high-interference sources.

See this video for a tutorial on how to modify a bathroom scale for this purpose.

Building an enclosure​

To protect your electronics, consider building or repurposing an enclosure. The enclosure should:

  • Be large enough to house the Wemos D1 Mini, HX711 amplifier, and breadboard with jumper wires.
  • Allow easy access to the rotary encoder and LCD display.

You can use a project box, an old electronics case, or even a custom 3D-printed enclosure. Ensure all components are firmly secured and that wiring is neatly routed to prevent interference or accidental disconnections.

See this part of the video tutorial to see how we built our enclosure.

ESPHome setup​

The section of the video tutorial on configuring the device in ESPHome starts here.

YAML configuration​

The latest YAML file can be found here.

esphome:
name: keg-fill-station1
friendly_name: keg-fill-station1
min_version: 2026.4.0
name_add_mac_suffix: false
on_boot:
priority: 600 # Runs after core components are initialized
then:
- delay: 1s # Give the HX711 a moment to grab its first valid reading
- lambda: |-
// Check if the raw sensor has a valid (non-nan) state before taring
if (!std::isnan(id(hx711_raw).state)) {
id(tare_raw_value) = id(hx711_raw).state;
ESP_LOGD("scale", "Scale auto-tared at boot to raw value: %.0f", id(tare_raw_value));
}

esp8266:
board: d1_mini
restore_from_flash: true # <--- This enables global flash saving

# Enable logging
logger:

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
- platform: esphome
password: !secret esphome_ota_password

wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password

# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "KegFillStation Hotspot"
password: !secret wifi_backup_password

captive_portal:

i2c:
sda: D2
scl: D1

display:
- platform: lcd_pcf8574
dimensions: 16x2
address: 0x27
update_interval: 0.25s
lambda: |-
// Line 1: Volume
it.printf(0, 0, "Vol:%.2fG", id(transferred_gallons).state);

// Line 2: Specific Gravity
it.printf(0, 1, "SG: %.3f", id(specific_gravity).state);

globals:
- id: tare_raw_value
type: float
restore_value: false
initial_value: "0.0"
- id: cal_scale_factor
type: float
restore_value: true # <--- Now successfully restores from flash!
initial_value: '10000.0'

sensor:
# Raw integer readings directly from the HX711 amplifier
- platform: hx711
name: "HX711 Raw Count"
id: hx711_raw
dout_pin: D5
clk_pin: D6
gain: 128
update_interval: 0.1s
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1

# Template sensor to convert raw HX711 counts to pounds (lbs)
- platform: template
name: "Scale Weight"
id: scale_weight
icon: mdi:scale
unit_of_measurement: "lbs"
accuracy_decimals: 2
update_interval: 0.25s
lambda: |-
float raw = id(hx711_raw).state;
float zero = id(tare_raw_value);
float factor = id(cal_scale_factor);

if (factor == 0) return 0.0;

// Negative sign handles your wiring polarity
// NOTE: Remove the negative if Scale Weight values are negative
return -(raw - zero) / factor;

- platform: template
name: "Transferred Volume"
id: transferred_gallons
icon: mdi:cup-beer
unit_of_measurement: "gal"
accuracy_decimals: 2
update_interval: 0.25s
lambda: |-
float weight_lbs = id(scale_weight).state;
float sg = id(specific_gravity).state;

if (std::isnan(weight_lbs) || weight_lbs < 0) {
return 0.0;
}

// 1 gallon of water weighs 8.34 lbs at room temp.
// Volume (gal) = Weight (lbs) / (8.34 * Specific Gravity)
float density_lbs_per_gal = 8.34 * sg;
if (density_lbs_per_gal <= 0) return 0.0;

return weight_lbs / density_lbs_per_gal;

- platform: rotary_encoder
name: "Encoder Input"
id: encoder
pin_a:
number: D7
mode: INPUT_PULLUP
pin_b:
number: D4
mode: INPUT_PULLUP
resolution: 2
on_clockwise:
then:
- lambda: |-
float current_sg = id(specific_gravity).state;
float new_sg = current_sg + 0.0005;
if (new_sg > 1.200) new_sg = 1.200;
id(specific_gravity).publish_state(new_sg);
on_anticlockwise:
then:
- lambda: |-
float current_sg = id(specific_gravity).state;
float new_sg = current_sg - 0.0005;
if (new_sg < 0.900) new_sg = 0.900;
id(specific_gravity).publish_state(new_sg);

number:
- platform: template
name: "Specific Gravity"
id: specific_gravity
icon: mdi:gravity
min_value: 0.900
max_value: 1.200
step: 0.001
initial_value: 1.010
#restore_value: true
mode: box
set_action:
then:
- lambda: |-
id(specific_gravity).publish_state(x);
- platform: template
name: "Calibration Known Weight"
id: cal_known_weight
icon: mdi:weight-pound
min_value: 0.1
max_value: 500.0
step: 0.01
initial_value: 16.0
#restore_value: true
mode: box
entity_category: "diagnostic"
set_action:
then:
- lambda: |-
id(cal_known_weight).publish_state(x);

binary_sensor:
- platform: gpio
pin:
number: D3
mode: INPUT_PULLUP
name: "Tare Button"
id: tare_button
filters:
- invert: {} # Inverts the dashboard state so it reads "Off" normally
on_press:
then:
- lambda: |-
// Capture the current raw count to use as the new zero point
id(tare_raw_value) = id(hx711_raw).state;

button:
- platform: template
name: "Tare Scale"
id: button_tare
icon: mdi:scale
on_press:
then:
- lambda: |-
// Same behavior as physical tare button
id(tare_raw_value) = id(hx711_raw).state;

- platform: template
name: "Calibrate Span (Known Weight)"
id: button_cal_span
icon: mdi:calculator
entity_category: "diagnostic"
on_press:
then:
- lambda: |-
float current_raw = id(hx711_raw).state;
float tare_raw = id(tare_raw_value);
float known_wt = id(cal_known_weight).state;

if (known_wt <= 0) {
ESP_LOGE("scale", "Cannot calibrate: Known weight must be greater than 0");
return;
}

// Calculate scale factor using the current tare baseline
float calculated_factor = std::abs(current_raw - tare_raw) / known_wt;

if (calculated_factor > 0) {
id(cal_scale_factor) = calculated_factor; // Saves directly to flash
ESP_LOGD("scale", "Calibration complete! New scale factor: %.2f", calculated_factor);
} else {
ESP_LOGE("scale", "Calibration failed: Calculated factor is invalid.");
}

Usage​

The section of the video tutorial on using and calibrating the device starts here.

Once assembled and configured in Home Assistant, the Keg Fill Station functions primarily as a standalone physical workstation during your transfer process, while keeping advanced tools accessible remotely.

Operation​

  1. Power On & Auto-Tare: Plug in the device. It will automatically initialize and tare itself to zero after one second. Place your empty keg or vessel onto the scale.
  2. Tare the Vessel: Press the physical Tare Button (or click the Tare button in Home Assistant) to zero out the weight of the empty container.
  3. Set Specific Gravity: Turn the local Rotary Encoder knob to adjust the specific gravity matching your incoming liquid (displayed on the second line of the LCD screen). You can also type this value directly into the Home Assistant number box if preferred.
  4. Monitor Transfer: As liquid fills the container, the local LCD screen will continuously update with the precise volume transferred in gallons (Vol:X.XXG), while Home Assistant tracks live weights, raw counts, and volume trends remotely.

Calibration​

If your weight readings drift or you change the load cell platform, you can recalibrate the scale without modifying the ESPHome configuration:

  1. Clear the platform so nothing is resting on the scale.
  2. Trigger a Tare (either via the physical button or the Home Assistant Tare button) to establish your empty baseline.
  3. Place a known reference weight (such as a 16 lb calibration weight) onto the scale.
  4. In Home Assistant, open the device's diagnostic entities and locate the Calibration Known Weight number box. Enter the exact weight of your test object (e.g., 16.00).
  5. Click the Calibrate Span (Known Weight) button.

The system will instantly calculate the correct scale factor, commit it to the Wemos D1 Mini's flash memory so it persists across reboots, and begin measuring accurately.

Automation​

Now that the hardware is setup and working in Home Assistant we can create automations that will make the beer transfer station even more useful.

Transfer Complete Notification​

When transferring beer into a keg, it is easy to get distracted and overfill or miss your target volume. We can set up an automation to notify your phone when the target volume has been reached.

This can be accomplished in Home Assistant with an automation like this:

  • Trigger: When Transferred Volume reaches 5.0 gallons
  • And If: none
  • Then Do: Send notification to user's phone with the message: "Keg fill target reached!"