Skip to content

Instantly share code, notes, and snippets.

@parabuzzle
Last active June 29, 2025 15:15
Show Gist options
  • Select an option

  • Save parabuzzle/5725d5767aabff069bc026da01c30dc0 to your computer and use it in GitHub Desktop.

Select an option

Save parabuzzle/5725d5767aabff069bc026da01c30dc0 to your computer and use it in GitHub Desktop.
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http-in
bind *:80
default_backend backend_servers
backend backend_servers
server sv1 172.27.153.1:80
listen stats
bind *:8080
stats enable
stats uri /
stats refresh 10s
stats admin if LOCALHOST
import json
import time
import requests
import paho.mqtt.client as mqtt
# Configuration
# If you're running this on the bridge pi.. you can go directly to the ip instead of going through the haproxy
API_URL = "http://172.27.153.1/cgi-bin/dl_cgi?Command=DeviceList"
#API_URL = "http://solarpi.local/cgi-bin/dl_cgi?Command=DeviceList"
# This is your home assistant MQTT server
MQTT_BROKER = "homeassistant.local"
MQTT_PORT = 1883
DISCOVERY_PREFIX = "homeassistant"
DEVICE_PREFIX = "solarbridge"
UPDATE_INTERVAL = 30 # seconds
client = mqtt.Client()
# If your MQTT is password protected
# client.username_pw_set("username", "password")
client.connect(MQTT_BROKER, MQTT_PORT)
client.loop_start()
device_idx = {}
def is_number(val):
try:
float(val)
return True
except:
return False
def device_object(device):
device_type = device.get("DEVICE_TYPE", "unknown").lower()
if device_type == "pvs":
return {
"identifiers": [f"{DEVICE_PREFIX}_gateway_{device.get('SERIAL', 'unknown').replace(' ', '_').lower()}"],
"name": f"SolarBridge Gateway {device_idx.get(device_type, 0)+1}",
"manufacturer": "SunPower",
"model": device.get("MODEL", "unknown"),
"sw_version": device.get("SWVER", "unknown"),
}
elif device_type == "power meter":
return {
"identifiers": [f"{DEVICE_PREFIX}_powermeter_{device.get('SERIAL', 'unknown').replace(' ', '_').lower()}"],
"name": f"SolarBridge Power Meter {device_idx.get(device_type, 0)+1}",
"manufacturer": "SunPower",
"model": device.get("MODEL", "unknown"),
"sw_version": device.get("SWVER", "unknown"),
}
elif device_type == "inverter":
return {
"identifiers": [f"inverter_{device.get('SERIAL', 'unknown').replace(' ', '_').lower()}"],
"name": f"SolarBridge Inverter {device_idx.get(device_type, 0)+1}",
"manufacturer": "SunPower",
"model": device.get("MODEL", "unknown"),
"sw_version": device.get("SWVER", "0"),
}
else:
return None
def device_attributes(device):
device_type = device.get("DEVICE_TYPE", "unknown").lower()
if device_type == "pvs":
return {
"hw_version": device.get("HWVER", "unknown"),
}
elif device_type == "power meter":
return {
"operation": device.get("OPERATION", "unknown"),
"subtype": device.get("subtype", "unknown"),
"type": device.get("TYPE", "unknown"),
"description": device.get("DESCR", "unknown"),
}
elif device_type == "inverter":
return {
"description": device.get("DESCR", "unknown"),
"operation": device.get("OPERATION", "unknown"),
"panel": device.get("PANEL", "unknown"),
}
else:
None
def mqtt_discovery(sensor_id, name, unit, device_class, state_topic, unique_id, device, state_class=None):
config_topic = f"{DISCOVERY_PREFIX}/sensor/{sensor_id}/config"
payload = {
"name": name,
"state_topic": state_topic,
"unit_of_measurement": unit,
"device_class": device_class,
"unique_id": unique_id,
"availability_topic": f"{state_topic}/available",
"device": device_object(device),
"json_attributes_topic": f"{state_topic}/attributes",
}
if state_class:
payload["state_class"] = state_class
#print(f"Publishing discovery for {sensor_id} to {config_topic}: {payload}")
client.publish(config_topic, json.dumps(payload), retain=True)
client.publish(f"{state_topic}/available", "online", retain=True)
attributes = device_attributes(device)
if attributes:
client.publish(f"{state_topic}/attributes", json.dumps(attributes), retain=True)
def push_data():
try:
resp = requests.get(API_URL, timeout=15)
data = resp.json()
devices = data.get("devices", [])
for device in devices:
serial = device.get("SERIAL") or device.get("DESCR") or "unknown"
serial_id = serial.replace(" ", "_").lower()
device_type = device.get("DEVICE_TYPE", "unknown").lower()
if not serial or serial == "unknown":
print(f"[WARN] Skipping device without valid SERIAL: {device}")
continue
if device_type in device_idx.keys():
device_idx[device_type]+=1
else:
device_idx[device_type] = 0
for key, value in device.items():
if key in ["SERIAL", "MODEL", "HWVER", "SWVER", "DEVICE_TYPE", "DATATIME", "CURTIME", "ISDETAIL"]:
continue
if is_number(value):
sensor_id = f"{DEVICE_PREFIX}_{serial_id}_{key}".lower()
state_topic = f"sensors/{sensor_id}/state"
unique_id = sensor_id
# Try to infer units and device class
unit = None
device_class = None
state_class = None
if device_type == "pvs":
if key == "dl_uptime":
unit = "s"
device_class = "duration"
elif key == "dl_scan_time":
unit = "s"
device_class = "duration"
else:
if "kw" in key:
unit = "kW"
device_class = "power"
elif "kwh" in key:
unit = "kWh"
device_class = "energy"
elif "hz" in key:
unit = "Hz"
device_class = "frequency"
elif "kva" in key:
unit = "kVA"
device_class = "current"
elif "kvar" in key:
unit = "kVA"
device_class = "current"
elif "v" in key:
unit = "V"
device_class = "voltage"
elif "a" in key:
unit = "A"
device_class = "current"
elif "degc" in key:
unit = "°C"
device_class = "temperature"
if "ltea" in key:
unit = "kWh"
state_class = "total_increasing"
device_class = "energy"
# Publish discovery (once per restart)
mqtt_discovery(sensor_id, key, unit, device_class, state_topic, unique_id, device, state_class)
# Publish value
client.publish(state_topic, str(float(value)))
#print(f"Publishing {key} to {state_topic}: {value}")
time.sleep(0.1)
# Reset device index after processing
for device_type in device_idx.keys():
device_idx[device_type] = -1 # this is because of initialization BS that I don't want to deal with right now
except Exception as e:
print(f"[ERROR] {e}")
# Initial run
push_data()
# Loop
while True:
time.sleep(UPDATE_INTERVAL)
push_data()
[Unit]
Description=Solar MQTT Uploader
Wants=network.target
After=network.target
[Service]
ExecStart=/usr/bin/python3 /home/pi/solar.py
WorkingDirectory=/home/pi
Restart=always
RestartSec=5
User=pi
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment