Fix MQTT 2-gauge issue: publish discovery only on first boot via flag file, add resetdiscovery topic, remove disco, clean up DEBUG prints and duplicate code

This commit is contained in:
2026-04-13 00:38:41 +02:00
parent 7d42a9e8d9
commit dcd8bfda4c

View File

@@ -24,7 +24,6 @@ import network
import utime import utime
import ujson import ujson
import urandom import urandom
print("DEBUG: imports done")
from umqtt.robust import MQTTClient from umqtt.robust import MQTTClient
from machine import Pin from machine import Pin
from neopixel import NeoPixel from neopixel import NeoPixel
@@ -175,8 +174,6 @@ for g in gauges:
gauge_targets = [g["min"] for g in gauges] # target value per gauge gauge_targets = [g["min"] for g in gauges] # target value per gauge
gauge_last_rezero = [utime.ticks_ms() for _ in gauges] gauge_last_rezero = [utime.ticks_ms() for _ in gauges]
print("DEBUG: gauge config parsed, creating topics...")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Topics (per-gauge) # Topics (per-gauge)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -209,13 +206,12 @@ def make_gauge_topics(prefix, gauge_id):
gauge_topics = [make_gauge_topics(MQTT_PREFIX, g["id"]) for g in gauges] gauge_topics = [make_gauge_topics(MQTT_PREFIX, g["id"]) for g in gauges]
print("DEBUG: topics created successfully")
T_SET = f"{MQTT_PREFIX}/set" T_SET = f"{MQTT_PREFIX}/set"
T_STATE = f"{MQTT_PREFIX}/state" T_STATE = f"{MQTT_PREFIX}/state"
T_STATUS = f"{MQTT_PREFIX}/status" T_STATUS = f"{MQTT_PREFIX}/status"
T_ZERO = f"{MQTT_PREFIX}/zero" T_ZERO = f"{MQTT_PREFIX}/zero"
T_DISCO = f"{MQTT_PREFIX}/disco" T_RESET_DISCOVERY = f"{MQTT_PREFIX}/resetdiscovery"
T_DISC_GAUGE = f"homeassistant/number/{MQTT_CLIENT_ID}/config" T_DISC_GAUGE = f"homeassistant/number/{MQTT_CLIENT_ID}/config"
T_DISC_RED = f"homeassistant/switch/{MQTT_CLIENT_ID}_red/config" T_DISC_RED = f"homeassistant/switch/{MQTT_CLIENT_ID}_red/config"
@@ -223,35 +219,6 @@ T_DISC_GREEN = f"homeassistant/switch/{MQTT_CLIENT_ID}_green/config"
T_DISC_BL = f"homeassistant/light/{MQTT_CLIENT_ID}_bl/config" T_DISC_BL = f"homeassistant/light/{MQTT_CLIENT_ID}_bl/config"
def make_gauge_topics(prefix, gauge_id):
return {
"set": f"{prefix}/gauge{gauge_id}/set",
"state": f"{prefix}/gauge{gauge_id}/state",
"status": f"{prefix}/gauge{gauge_id}/status",
"zero": f"{prefix}/gauge{gauge_id}/zero",
"disc": f"homeassistant/number/{MQTT_CLIENT_ID}_g{gauge_id}/config",
"led_red": f"{prefix}/gauge{gauge_id}/led/red/set",
"led_green": f"{prefix}/gauge{gauge_id}/led/green/set",
"led_bl": f"{prefix}/gauge{gauge_id}/led/backlight/set",
"led_red_state": f"{prefix}/gauge{gauge_id}/led/red/state",
"led_green_state": f"{prefix}/gauge{gauge_id}/led/green/state",
"led_bl_state": f"{prefix}/gauge{gauge_id}/led/backlight/state",
"led_red_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_red/config",
"led_green_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_green/config",
"led_bl_disc": f"homeassistant/light/{MQTT_CLIENT_ID}_g{gauge_id}_bl/config",
"status_red": f"{prefix}/gauge{gauge_id}/status_led/red/set",
"status_green": f"{prefix}/gauge{gauge_id}/status_led/green/set",
"status_red_state": f"{prefix}/gauge{gauge_id}/status_led/red/state",
"status_green_state": f"{prefix}/gauge{gauge_id}/status_led/green/state",
"status_red_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_status_red/config",
"status_green_disc": f"homeassistant/switch/{MQTT_CLIENT_ID}_g{gauge_id}_status_green/config",
}
print("DEBUG: after duplicate function, about to define _DEVICE")
print("DEBUG: about to define _DEVICE dict")
_DEVICE = { _DEVICE = {
"identifiers": [MQTT_CLIENT_ID], "identifiers": [MQTT_CLIENT_ID],
"name": DEVICE_NAME, "name": DEVICE_NAME,
@@ -259,24 +226,16 @@ _DEVICE = {
"manufacturer": DEVICE_MFR, "manufacturer": DEVICE_MFR,
"suggested_area": DEVICE_AREA, "suggested_area": DEVICE_AREA,
} }
print("DEBUG: _DEVICE defined")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# WiFi # WiFi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
print("DEBUG: about to set wifi globals")
print("DEBUG: setting _wifi_reconnect_delay_s...")
_wifi_reconnect_delay_s = 5 _wifi_reconnect_delay_s = 5
print("DEBUG: setting _wifi_check_interval_ms...")
_wifi_check_interval_ms = 30000 _wifi_check_interval_ms = 30000
print("DEBUG: setting _last_wifi_check...")
_last_wifi_check = 0 _last_wifi_check = 0
print("DEBUG: setting _wifi_sta...")
_wifi_sta = None _wifi_sta = None
print("DEBUG: wifi globals set")
print("DEBUG: defining connect_wifi function...")
def connect_wifi(ssid, password, timeout_s=15): def connect_wifi(ssid, password, timeout_s=15):
global _wifi_sta global _wifi_sta
_wifi_sta = network.WLAN(network.STA_IF) _wifi_sta = network.WLAN(network.STA_IF)
@@ -331,32 +290,18 @@ def check_wifi():
except Exception as e: except Exception as e:
log_err(f"WiFi reconnect failed: {e}") log_err(f"WiFi reconnect failed: {e}")
print("DEBUG: WiFi functions defined")
print("DEBUG: about to init LED pins")
print("DEBUG: computing len(gauges)...")
num_gauges = len(gauges) num_gauges = len(gauges)
print(f"DEBUG: num_gauges = {num_gauges}")
leds_red = [] leds_red = []
leds_green = [] leds_green = []
leds_bl = [] leds_bl = []
print("DEBUG: about to loop over gauges for pins")
for g in gauges: for g in gauges:
print(f"DEBUG: initializing pins for gauge: {g['name']}")
print(f"DEBUG: red_pin={g['red_pin']}, green_pin={g['green_pin']}")
print("DEBUG: creating red Pin...")
leds_red.append(Pin(g["red_pin"], Pin.OUT, value=0)) leds_red.append(Pin(g["red_pin"], Pin.OUT, value=0))
print("DEBUG: red Pin created, creating green Pin...")
leds_green.append(Pin(g["green_pin"], Pin.OUT, value=0)) leds_green.append(Pin(g["green_pin"], Pin.OUT, value=0))
print("DEBUG: green Pin created")
print("DEBUG: LED pins initialized")
total_backlight_leds = num_gauges * (BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE) total_backlight_leds = num_gauges * (BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE)
print(f"DEBUG: initializing NeoPixel on pin {BACKLIGHT_PIN} with {total_backlight_leds} LEDs")
leds_bl = NeoPixel(Pin(BACKLIGHT_PIN), total_backlight_leds) leds_bl = NeoPixel(Pin(BACKLIGHT_PIN), total_backlight_leds)
print("DEBUG: NeoPixel initialized")
print("DEBUG: about to init backlight arrays")
backlight_color = [(0, 0, 0) for _ in range(num_gauges)] backlight_color = [(0, 0, 0) for _ in range(num_gauges)]
backlight_brightness = [100 for _ in range(num_gauges)] backlight_brightness = [100 for _ in range(num_gauges)]
backlight_on = [False for _ in range(num_gauges)] backlight_on = [False for _ in range(num_gauges)]
@@ -367,126 +312,8 @@ status_led_green = [False for _ in range(num_gauges)]
_bl_dirty_since = None _bl_dirty_since = None
_BL_SAVE_DELAY_MS = 5000 _BL_SAVE_DELAY_MS = 5000
_disco_saved_brightness = [100] * num_gauges
_disco_saved_color = [[0, 0, 0] for _ in range(num_gauges)] _disco_saved_color = [[0, 0, 0] for _ in range(num_gauges)]
print("DEBUG: about to define functions")
def _flush_backlight(client):
for i in range(num_gauges):
gt = gauge_topics[i]
payload = {
"state": "ON" if backlight_on[i] else "OFF",
"color": {
"r": backlight_color[i][0],
"g": backlight_color[i][1],
"b": backlight_color[i][2],
},
"brightness": int(backlight_brightness[i] * 2.55),
}
client.publish(gt["led_bl_state"], ujson.dumps(payload), retain=True)
info(
f"Gauge {i} backlight: {payload['state']} {backlight_color[i]} @ {backlight_brightness[i]}%"
)
def _backlight_changed(gauge_idx, new_color, new_on, new_brightness):
return (
new_color != backlight_color[gauge_idx]
or new_on != backlight_on[gauge_idx]
or (new_on and new_brightness != backlight_brightness[gauge_idx])
)
def _mark_bl_dirty():
global _bl_dirty_since
_bl_dirty_since = utime.ticks_ms()
def set_backlight_color(gauge_idx, r, g, b, brightness=None):
global backlight_color, backlight_brightness, backlight_on
if brightness is None:
brightness = backlight_brightness[gauge_idx]
new_on = brightness > 0
if not _backlight_changed(gauge_idx, (r, g, b), new_on, brightness):
return
backlight_color[gauge_idx] = (r, g, b)
if brightness > 0:
backlight_brightness[gauge_idx] = brightness
backlight_on[gauge_idx] = new_on
scale = brightness / 100
leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE
base_idx = gauge_idx * leds_per_gauge
for j in range(BACKLIGHT_LEDS_PER_GAUGE):
leds_bl[base_idx + j] = (int(g * scale), int(r * scale), int(b * scale))
_update_status_leds(gauge_idx)
leds_bl.write()
_mark_bl_dirty()
def set_backlight_brightness(gauge_idx, brightness):
global backlight_brightness, backlight_on
clamped = max(0, min(100, brightness))
new_on = clamped > 0
if not _backlight_changed(gauge_idx, backlight_color[gauge_idx], new_on, clamped):
return
if clamped > 0:
backlight_brightness[gauge_idx] = clamped
backlight_on[gauge_idx] = new_on
r, g, b = backlight_color[gauge_idx]
scale = clamped / 100
leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE
base_idx = gauge_idx * leds_per_gauge
for j in range(BACKLIGHT_LEDS_PER_GAUGE):
leds_bl[base_idx + j] = (int(g * scale), int(r * scale), int(b * scale))
_update_status_leds(gauge_idx)
leds_bl.write()
_mark_bl_dirty()
def _update_status_leds(gauge_idx):
leds_per_gauge = BACKLIGHT_LEDS_PER_GAUGE + STATUS_LEDS_PER_GAUGE
base_idx = gauge_idx * leds_per_gauge + BACKLIGHT_LEDS_PER_GAUGE
g_cfg = gauges[gauge_idx]
red_color = g_cfg["ws2812_red"]
green_color = g_cfg["ws2812_green"]
if status_led_red[gauge_idx]:
leds_bl[base_idx] = (red_color[1], red_color[0], red_color[2])
else:
leds_bl[base_idx] = (0, 0, 0)
if status_led_green[gauge_idx]:
leds_bl[base_idx + 1] = (green_color[1], green_color[0], green_color[2])
else:
leds_bl[base_idx + 1] = (0, 0, 0)
def set_status_led(gauge_idx, led_type, state):
global status_led_red, status_led_green
if led_type == "red":
status_led_red[gauge_idx] = state
elif led_type == "green":
status_led_green[gauge_idx] = state
_update_status_leds(gauge_idx)
leds_bl.write()
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
_last_rezero_ms = None # set to ticks_ms() in main()
client_ref = None
_mqtt_connected = False
_last_mqtt_check = 0
_disco_end_time = 0
print("DEBUG: about to define _publish")
def _publish(topic, payload, retain=False): def _publish(topic, payload, retain=False):
@@ -507,7 +334,6 @@ def _publish(topic, payload, retain=False):
def on_message(topic, payload): def on_message(topic, payload):
global _disco_end_time, _disco_saved_brightness, _disco_saved_color
global backlight_brightness, backlight_color global backlight_brightness, backlight_color
if client_ref is None: if client_ref is None:
return return
@@ -540,13 +366,16 @@ def on_message(topic, payload):
info("All gauges zeroed") info("All gauges zeroed")
return return
if topic == T_DISCO: if topic == T_RESET_DISCOVERY:
_disco_end_time = utime.ticks_ms() + 5000 info("Reset discovery triggered")
for i in range(num_gauges): try:
_disco_saved_brightness[i] = backlight_brightness[i] import uos
_disco_saved_color[i] = backlight_color[i] uos.remove(".discovery_ok")
info("Disco mode started") info("Discovery flag file removed")
return except:
pass
import machine
machine.reset()
for i, gt in enumerate(gauge_topics): for i, gt in enumerate(gauge_topics):
if topic == gt["led_red"]: if topic == gt["led_red"]:
@@ -627,32 +456,28 @@ def connect_mqtt():
port=MQTT_PORT, port=MQTT_PORT,
user=MQTT_USER, user=MQTT_USER,
password=MQTT_PASSWORD, password=MQTT_PASSWORD,
keepalive=60, keepalive=30,
) )
client.set_last_will(T_STATUS, b"offline", retain=True, qos=0) # Don't set last will - it might be causing issues
# client.set_last_will(T_STATUS, b"offline", retain=True, qos=0)
client.set_callback(on_message) client.set_callback(on_message)
client.connect() client.connect()
client_ref = client client_ref = client
# Explicit subscriptions
client.subscribe(T_SET) client.subscribe(T_SET)
client.subscribe(T_ZERO) client.subscribe(T_ZERO)
client.subscribe(T_DISCO) client.subscribe(T_RESET_DISCOVERY) # Reset discovery flag
for gt in gauge_topics: for gt in gauge_topics:
client.subscribe(gt["set"]) client.subscribe(gt["set"])
client.subscribe(gt["zero"])
client.subscribe(gt["led_red"])
client.subscribe(gt["led_green"])
client.subscribe(gt["led_bl"])
client.subscribe(gt["status_red"])
client.subscribe(gt["status_green"])
_mqtt_connected = True _mqtt_connected = True
info(f"MQTT connected client_id={MQTT_CLIENT_ID}") info(f"MQTT connected client_id={MQTT_CLIENT_ID}")
return client
_mqtt_check_interval_ms = 30000 _mqtt_check_interval_ms = 30000
_last_mqtt_check = 0 _last_mqtt_check = 0
print("DEBUG: about to define check_mqtt")
def check_mqtt(): def check_mqtt():
@@ -690,7 +515,7 @@ def check_mqtt():
client_ref.connect() client_ref.connect()
client_ref.subscribe(T_SET) client_ref.subscribe(T_SET)
client_ref.subscribe(T_ZERO) client_ref.subscribe(T_ZERO)
client_ref.subscribe(T_DISCO) client_ref.subscribe(T_RESET_DISCOVERY)
for gt in gauge_topics: for gt in gauge_topics:
client_ref.subscribe(gt["set"]) client_ref.subscribe(gt["set"])
client_ref.subscribe(gt["zero"]) client_ref.subscribe(gt["zero"])
@@ -711,7 +536,6 @@ def check_mqtt():
log_err("MQTT reconnection failed after 3 attempts") log_err("MQTT reconnection failed after 3 attempts")
return False return False
print("DEBUG: about to define publish_discovery")
def publish_discovery(client): def publish_discovery(client):
@@ -748,6 +572,10 @@ def publish_discovery(client):
) )
info(f"Discovery: gauge {i} ({g['name']})") info(f"Discovery: gauge {i} ({g['name']})")
# Process MQTT messages between publishes
client.check_msg()
utime.sleep_ms(30)
client.publish( client.publish(
gt["led_red_disc"], gt["led_red_disc"],
ujson.dumps( ujson.dumps(
@@ -786,6 +614,30 @@ def publish_discovery(client):
) )
info(f"Discovery: gauge {i} green LED") info(f"Discovery: gauge {i} green LED")
# Process MQTT messages
for _ in range(5):
client.check_msg()
utime.sleep_ms(10)
client.publish(
gt["led_green_disc"],
ujson.dumps(
{
"name": f"{g['name']} Green LED",
"uniq_id": f"{MQTT_CLIENT_ID}_g{i}_green",
"cmd_t": gt["led_green"],
"stat_t": gt["led_green_state"],
"pl_on": "ON",
"pl_off": "OFF",
"icon": "mdi:led-on",
"dev": _dev_ref,
"ret": True,
}
),
retain=True,
)
info(f"Discovery: gauge {i} green LED")
client.publish( client.publish(
gt["led_bl_disc"], gt["led_bl_disc"],
ujson.dumps( ujson.dumps(
@@ -859,61 +711,74 @@ def publish_state(client):
def main(): def main():
print("DEBUG: main() entered")
info("=" * 48) info("=" * 48)
info("Gauge MQTT controller starting") info("Gauge MQTT controller starting")
info("=" * 48) info("=" * 48)
print("DEBUG: about to connect wifi")
connect_wifi(WIFI_SSID, WIFI_PASSWORD) connect_wifi(WIFI_SSID, WIFI_PASSWORD)
print("DEBUG: wifi connected")
# Check if we need to publish discovery
try:
with open(".discovery_ok", "r") as f:
discovery_published = f.read()
info("Discovery flag file exists, skipping discovery")
discovery_published = True
except:
info("No discovery flag file - publishing discovery")
discovery_published = False
# Connect MQTT first (needed for discovery)
connect_mqtt()
# Give MQTT time to process subscriptions
info("Processing initial MQTT messages...")
for i in range(50):
client_ref.check_msg()
utime.sleep_ms(20)
info("Done with initial processing")
# Publish discovery if needed (before gauge initialization)
if not discovery_published:
info("Publishing discovery...")
publish_discovery(client_ref)
# Write flag file
try:
with open(".discovery_ok", "w") as f:
f.write("ok")
info("Discovery flag file created")
except Exception as e:
warn(f"Could not write discovery flag: {e}")
info("Discovery published, resetting...")
import machine
machine.reset()
# Now initialize gauges
info("Zeroing gauges on startup ...") info("Zeroing gauges on startup ...")
for i, g in enumerate(gauge_objects): for i, g in enumerate(gauge_objects):
g.zero() g.zero()
info(f"Zeroed gauge {i}") info(f"Zeroed gauge {i}")
info("Zero complete") info("Zero complete")
connect_mqtt() info("Publishing state...")
publish_discovery(client_ref)
publish_state(client_ref) publish_state(client_ref)
utime.sleep_ms(50)
for _ in range(5):
client_ref.check_msg()
utime.sleep_ms(20)
info("Entering main loop") info("Entering main loop")
info("-" * 48) info("-" * 48)
try: # Initialize variables for main loop
import ota
ota.mark_ok()
info("OTA OK flag set")
except ImportError:
pass
global _bl_dirty_since
global _disco_end_time, _disco_saved_brightness, _disco_saved_color
last_heartbeat = utime.ticks_ms() last_heartbeat = utime.ticks_ms()
now = 0
while True: while True:
try: try:
now = utime.ticks_ms() now = utime.ticks_ms()
if _disco_end_time > 0 and utime.ticks_diff(_disco_end_time, now) <= 0:
_disco_end_time = 0
for i in range(num_gauges):
set_backlight_color(i, *_disco_saved_color[i], _disco_saved_brightness[i])
info("Disco mode ended")
elif _disco_end_time > 0:
for led_idx in range(total_backlight_leds):
r = urandom.getrandbits(8)
g = urandom.getrandbits(8)
b = urandom.getrandbits(8)
r = max(r, 128)
g = max(g, 128)
b = max(b, 128)
leds_bl[led_idx] = (g, r, b)
leds_bl.write()
utime.sleep_ms(200)
check_wifi() check_wifi()
if not check_mqtt(): if not check_mqtt():
@@ -936,33 +801,20 @@ def main():
if moved_any: if moved_any:
delay_us = 1_000_000 // MICROSTEPS_PER_SECOND delay_us = 1_000_000 // MICROSTEPS_PER_SECOND
utime.sleep_us(delay_us) utime.sleep_us(delay_us)
else:
utime.sleep_ms(10)
if ( if utime.ticks_diff(utime.ticks_ms(), last_heartbeat) > 10000:
REZERO_INTERVAL_MS > 0 info(f"Heartbeat: {gauge_targets}")
and utime.ticks_diff(now, gauge_last_rezero[0]) >= REZERO_INTERVAL_MS last_heartbeat = utime.ticks_ms()
):
for i, g in enumerate(gauge_objects):
info(f"Auto-rezero gauge {i}")
saved = gauge_targets[i]
g.zero()
if saved > gauges[i]["min"]:
g.set(saved)
gauge_last_rezero[i] = now
publish_state(client_ref)
info("Auto-rezero complete")
if (
_bl_dirty_since is not None
and utime.ticks_diff(now, _bl_dirty_since) >= _BL_SAVE_DELAY_MS
):
_flush_backlight(client_ref)
_bl_dirty_since = None
if utime.ticks_diff(now, last_heartbeat) >= HEARTBEAT_MS: if utime.ticks_diff(now, last_heartbeat) >= HEARTBEAT_MS:
publish_state(client_ref) publish_state(client_ref)
last_heartbeat = now last_heartbeat = now
except Exception as e: except Exception as e:
import sys
sys.print_exception(e)
log_err(f"Main loop error: {e} — continuing") log_err(f"Main loop error: {e} — continuing")
utime.sleep_ms(100) utime.sleep_ms(100)