monitoring Hawkes Bay ( rosie , barcelona ) using raspberry pi

Started by ClassicCrazy, November 20, 2025, 02:44:58 AM

Previous topic - Next topic

ClassicCrazy

I worked with chatgpt and google gemini AI for many hours to get Raspberry Pi monitoring of my hawkes bay via canbus .
I know others have  used node red to do this but I ran into some issues because a bunch of things don't work right for socketcan in newest Node Red versions.
So I gave up .
But I told Chatgpt that I wanted to get data from my raspberry pi to a computer I have running home assistant which is where I monitor every other part of my solar system.
I am using a Pi3B+ with Canable usb to canbus adapter . Just two wires on pins 4 and 5 is all thta is needed. I forget right now which is high and low but can look it up.
Anyway long story short is Chatgpt wrote python script that gets some of the data from hawkes bay and sends it out on mqtt which can go to my other monitoring computer .
I will do more later to get more data  - but it is too late tonight .
Anyway it works and should work on any linux computer - maybe even windows too ?

Here is python code that I am using - there is another file to put in pi to get this to work too. But this is main one. I am working on getting the kwh total but that needs something extra to show up . I am so happy to get this working .
Larry

#!/usr/bin/env python3

import can
import paho.mqtt.client as mqtt
import json
import time

# ------------------------------------------
# CONFIGURE MQTT
# ------------------------------------------
MQTT_BROKER = "192.168.3.50"
MQTT_PORT = 1883
MQTT_PREFIX = "hawkesbay"   # Base topic e.g. hawkesbay/voltage
MQTT_USER = None            # or "mqttuser"
MQTT_PASS = None            # or "mqttpassword"

# ------------------------------------------
# MQTT SETUP
# ------------------------------------------
client = mqtt.Client()
if MQTT_USER and MQTT_PASS:
    client.username_pw_set(MQTT_USER, MQTT_PASS)
client.connect(MQTT_BROKER, MQTT_PORT, 60)

# ------------------------------------------
# CAN SETUP (socketcan)
# ------------------------------------------

bus = can.interface.Bus(channel='can0', bustype='socketcan')

print(" Listening on CAN0 and publishing Hawkes Bay data to MQTT...")


# ------------------------------------------
# Helper: Publish
# ------------------------------------------
def pub(topic, value):
    full = f"{MQTT_PREFIX}/{topic}"
    client.publish(full, value)
    # print(full, value)   # Uncomment for debugging
# ------------------------------------------
# MAIN LOOP
# ------------------------------------------
while True:
    msg = bus.recv()

    if not msg:
        continue

    canid = msg.arbitration_id
    data = list(msg.data)

    # Extract fields exactly like the Node-RED function
    register = (canid >> 18) & 0x7FF
    device   = (canid >> 11) & 0x7F
    device_index = (canid >> 7) & 0x0F
    bus_id   = canid & 0x7F

    # -------------------------------------------------
    # Battery Voltage / Current / Power (0x0A0)
    # -------------------------------------------------
    if register == 0x0A0:
        voltage = (data[0] * 256 + data[1]) / 10.0
        current = (data[2] * 256 + data[3]) / 10.0
        power   = voltage * current

        pub("battery/voltage", voltage)
        pub("battery/current", current)
        pub("battery/power", power)

    # -------------------------------------------------
    # MPPT2 Instant Voltage / Current (0x081)
    # -------------------------------------------------
    elif register == 0x081:
        mppt_v = (data[0] * 256 + data[1]) / 10.0
        mppt_i = (data[2] * 256 + data[3]) / 10.0

        pub("mppt2/voltage", mppt_v)
        pub("mppt2/current", mppt_i)

    # -------------------------------------------------
    # Charging Status (0x0A3)
    # -------------------------------------------------
    elif register == 0x0A3:
        status_code = data[0]
        status_map = {
            0: "Resting",
            1: "Bulk MPPT",
            2: "Absorb",
            3: "Float",
            4: "Equalize",
            5: "Float MPPT",
            6: "EQ MPPT",
        }
        status = status_map.get(status_code, "Unknown")
        pub(f"mppt{device_index}/status", status)

    # -------------------------------------------------
    # External Battery Temp Sensor (0x2A4)
    # -------------------------------------------------
    elif register == 0x2A4:
        temp_c = (data[2] * 256 + data[3]) / 10.0
        temp_f = temp_c * 1.8 + 32.0

        pub("battery/temp_f", temp_f)
        pub("battery/temp_c", temp_c)

    # -------------------------------------------------
    # Battery State of Charge (0x0A6)
    # -------------------------------------------------
    elif register == 0x0A6:
        soc = data[3]
        pub("battery/soc", soc)

    # -------------------------------------------------
    # ARC Fault (0x2A0)
    # -------------------------------------------------
    elif register == 0x2A0:
        arc = data[0]
        pub("arc_fault", arc)

    # -------------------------------------------------
    # Daily KWh Production (0x372)
    # -------------------------------------------------
    elif register == 0x372:
        day = (data[0] << 8) + data[1]  # first 2 bytes = Day
        if day == 0:  # only publish today's value
            kwh = (data[2] << 24) + (data[3] << 16) + (data[4] << 8) + data[5]  # next 4 bytes = KWh * 100
            kwh = kwh / 100.0
            pub("battery/daily_kwh", kwh)

    # -------------------------------------------------
    # MQTT loop
    # -------------------------------------------------
    client.loop(0.01)


This is the mqtt as seen on mqtt explorer

hawkesbay
mppt2
voltage = 29.3
current = 0.0
battery
voltage = 53.0
current = 0.0
power = 0.0
temp_c = 18.3
temp_f = 64.94
soc = 89
mppt0
status = Resting
arc_fault = 0
 


system 1
Classic 150 , 5s3p  Kyocera 135watt , 12s Soneil 2v 540amp lead crystal 24v pack , Outback 3524 inverter
 5s 135w Kyocero , 3s3p 270w Kyocera   Classic 150 ,8s2p  Kyocera 225w to Hawkes Bay Jakiper 48v 20kwh  ,Gobel 16 kwh  lifepo4 Outback VFX 3648  8s2p 380w Rec pv EG4 6000XP

ClassicCrazy

Here is newest code. Home Assistant will auto discover this on mqtt .
Still having trouble with getting the daily  kWh to display .

#!/usr/bin/env python3
"""
CAN -> MQTT bridge for Midnite Hawkes Bay (fixed endianness)
 - Uses big-endian for sensor registers (0x0A0, 0x081, 0x2A4)
 - Uses little-endian for daily ticks (0x370)
 - Publishes JSON state, individual topics, HA discovery, calibration
"""

import can
import paho.mqtt.client as mqtt
import json
import time
import logging
import sys
from datetime import datetime

# ===== CONFIG =====
MQTT_BROKER = "192.168.3.50"
MQTT_PORT = 1883
MQTT_PREFIX = "hawkesbay"
HA_DISCOVERY_PREFIX = "homeassistant"
CAN_CHANNEL = "can0"
CAN_INTERFACE = "socketcan"
LOG_LEVEL = logging.INFO

DEFAULT_TICKS_PER_KWH = 4728.0

# ===== Logging =====
logger = logging.getLogger("can2mqtt_hbay")
logger.setLevel(LOG_LEVEL)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s"))
logger.addHandler(handler)

# ===== MQTT Setup =====
client = mqtt.Client()
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()

# ===== CAN Setup =====
try:
    bus = can.interface.Bus(channel=CAN_CHANNEL, interface=CAN_INTERFACE)
    logger.info("Opened CAN %s (%s)", CAN_CHANNEL, CAN_INTERFACE)
except Exception as e:
    logger.exception("Failed opening CAN interface")
    raise

# ===== State =====
state = {"battery": {}, "pv": {}, "daily": {}}
ticks_per_kwh = DEFAULT_TICKS_PER_KWH

# ===== Helpers =====
def pub(topic_suffix, value, retain=True):
    topic = f"{MQTT_PREFIX}/{topic_suffix}"
    if isinstance(value, (dict, list)):
        payload = json.dumps(value)
    else:
        payload = str(value)
    client.publish(topic, payload, retain=retain)
    logger.debug("MQTT pub %s -> %s", topic, payload)

def pub_state():
    pub("state", state)

def ha_publish_sensor_config(object_id, name, unit, device_class=None, state_class=None):
    config_topic = f"{HA_DISCOVERY_PREFIX}/sensor/hbay_{object_id}/config"
    parts = object_id.split("_", 1)
    cat = parts[0]
    key = parts[1]
    payload = {
        "name": name,
        "state_topic": f"{MQTT_PREFIX}/state",
        "value_template": "{{ value_json.%s.%s }}" % (cat, key),
        "unique_id": f"hbay_{object_id}",
        "device": {
            "identifiers": ["hawkes_bay"],
            "manufacturer": "Midnite Solar",
            "model": "Hawkes Bay",
            "name": "Hawkes Bay"
        }
    }
    if unit:
        payload["unit_of_measurement"] = unit
    if device_class:
        payload["device_class"] = device_class
    if state_class:
        payload["state_class"] = state_class

    client.publish(config_topic, json.dumps(payload), retain=True)
    logger.info("Published HA discovery for %s", object_id)

# publish discovery
ha_publish_sensor_config("battery_voltage", "HB Battery Voltage", "V", "voltage", "measurement")
ha_publish_sensor_config("battery_current", "HB Battery Current", "A", "current", "measurement")
ha_publish_sensor_config("battery_power", "HB Battery Power", "W", "power", "measurement")
ha_publish_sensor_config("battery_temp_c", "HB Battery Temp C", "°C", "temperature", "measurement")
ha_publish_sensor_config("battery_soc", "HB Battery SOC", "%")
ha_publish_sensor_config("pv_kwh", "HB PV kWh Today", "kWh", None, "total_increasing")
ha_publish_sensor_config("battery_kwh", "HB Battery kWh Today", "kWh", None, "total_increasing")
ha_publish_sensor_config("daily_kwh", "HB Daily kWh Combined", "kWh", None, "total_increasing")

# MQTT calibration
def on_mqtt_message(client_, userdata, msg):
    global ticks_per_kwh
    try:
        topic = msg.topic
        payload = msg.payload.decode()
        logger.info("MQTT recv %s -> %s", topic, payload)
        if topic == f"{MQTT_PREFIX}/calibrate":
            data = json.loads(payload)
            display_kwh = float(data.get("display_kwh", 0))
            if display_kwh <= 0:
                logger.warning("Calibration value invalid")
                return
            pv_ticks = state.get("pv", {}).get("ticks", 0)
            batt_ticks = state.get("battery", {}).get("ticks", 0)
            combined = int(pv_ticks) + int(batt_ticks)
            if combined <= 0:
                logger.warning("No ticks available to calibrate against")
                return
            ticks_per_kwh = combined / display_kwh
            logger.info("Calibrated ticks_per_kwh = %.3f (combined ticks=%d display_kwh=%.3f)",
                        ticks_per_kwh, combined, display_kwh)
            pub("calibration/ticks_per_kwh", round(ticks_per_kwh, 3))
    except Exception:
        logger.exception("Error in calibrate handler")

client.subscribe(f"{MQTT_PREFIX}/calibrate")
client.on_message = on_mqtt_message

# Endianness helpers
def le16(data, offset):
    if len(data) > offset + 1:
        return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8)
    return 0

def be16(data, offset):
    if len(data) > offset + 1:
        return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF)
    return 0

logger.info("Starting main loop (fixed endianness)...")
while True:
    try:
        msg = bus.recv(timeout=1.0)
        if msg is None:
            pub_state()
            time.sleep(0.2)
            continue

        canid = int(msg.arbitration_id)
        data = list(msg.data)
        register = (canid >> 18) & 0x7FF
        timestamp = getattr(msg, "timestamp", time.time())

        logger.debug("RX id=%s reg=0x%03x data=%s", hex(canid), register, data)

        # Battery V/I (big-endian)
        if register == 0x0A0 and len(data) >= 4:
            voltage = be16(data, 0) / 10.0
            current = be16(data, 2) / 10.0
            power = voltage * current
            state["battery"]["voltage"] = round(voltage,2)
            state["battery"]["current"] = round(current,2)
            state["battery"]["power"] = round(power,2)
            state["battery"]["timestamp"] = datetime.fromtimestamp(timestamp).isoformat()
            pub("battery/voltage", state["battery"]["voltage"])
            pub("battery/current", state["battery"]["current"])
            pub("battery/power", state["battery"]["power"])
            logger.info("Battery V=%.2f I=%.2f P=%.2f", voltage, current, power)

        # MPPT2 V/I (big-endian)
        elif register == 0x081 and len(data) >= 4:
            mppt_v = be16(data, 0) / 10.0
            mppt_i = be16(data, 2) / 10.0
            state["pv"]["mppt2_voltage"] = round(mppt_v,2)
            state["pv"]["mppt2_current"] = round(mppt_i,2)
            pub("mppt2/voltage", state["pv"]["mppt2_voltage"])
            pub("mppt2/current", state["pv"]["mppt2_current"])

        # Temp (big-endian)
        elif register == 0x2A4 and len(data) >= 4:
            temp_c = be16(data, 2) / 10.0
            state["battery"]["temp_c"] = round(temp_c,2)
            state["battery"]["temp_f"] = round(temp_c * 1.8 + 32.0,2)
            pub("battery/temp_c", state["battery"]["temp_c"])
            pub("battery/temp_f", state["battery"]["temp_f"])

        # SOC
        elif register == 0x0A6 and len(data) >= 4:
            soc = data[3]
            state["battery"]["soc"] = int(soc)
            pub("battery/soc", int(soc))

        # ARC
        elif register == 0x2A0 and len(data) >= 1:
            arc = data[0]
            state["battery"]["arc_fault"] = int(arc)
            pub("battery/arc_fault", int(arc))

        # DAILY 0x370 (little-endian ticks)
        elif register == 0x370 and len(data) >= 8:
            pv_ticks = le16(data, 4)
            batt_ticks = le16(data, 6)
            state["pv"]["ticks"] = int(pv_ticks)
            state["battery"]["ticks"] = int(batt_ticks)
            combined_ticks = pv_ticks + batt_ticks
            if ticks_per_kwh <= 0:
                ticks_per_kwh = DEFAULT_TICKS_PER_KWH
            pv_kwh = pv_ticks / ticks_per_kwh
            batt_kwh = batt_ticks / ticks_per_kwh
            combined_kwh = combined_ticks / ticks_per_kwh
            state["pv"]["pv_kwh_today"] = round(pv_kwh,3)
            state["battery"]["battery_kwh_today"] = round(batt_kwh,3)
            state["daily"]["daily_kwh_today"] = round(combined_kwh,3)
            state["daily"]["timestamp"] = datetime.fromtimestamp(timestamp).isoformat()
            pub("pv/pv_kwh_today", round(pv_kwh,3))
            pub("battery/battery_kwh_today", round(batt_kwh,3))
            pub("battery/daily_kwh_today", round(combined_kwh,3))
            pub("daily/raw", json.dumps({
                "pv_ticks": pv_ticks,
                "batt_ticks": batt_ticks,
                "combined_ticks": combined_ticks,
                "ticks_per_kwh": round(ticks_per_kwh,3)
            }))
            logger.info("Daily: pv_ticks=%d batt_ticks=%d combined=%.3f kWh (ticks_per_kwh=%.3f)",
                        pv_ticks, batt_ticks, combined_kwh, ticks_per_kwh)

        else:
            pub(f"raw/0x{register:03x}", json.dumps({"canid": hex(canid), "data": data}), retain=True)

        pub_state()

    except KeyboardInterrupt:
        logger.info("Exiting by user request")
        break
    except Exception:
        logger.exception("Error in main loop")
        time.sleep(0.5)
system 1
Classic 150 , 5s3p  Kyocera 135watt , 12s Soneil 2v 540amp lead crystal 24v pack , Outback 3524 inverter
 5s 135w Kyocero , 3s3p 270w Kyocera   Classic 150 ,8s2p  Kyocera 225w to Hawkes Bay Jakiper 48v 20kwh  ,Gobel 16 kwh  lifepo4 Outback VFX 3648  8s2p 380w Rec pv EG4 6000XP

ClassicCrazy

here is newest python code that works correctly for whizbang and daily kwh .
I will probably make a new post on this in monitoring section of forums .
Chatgpt can make a lot of mistakes and overthink some things so it takes time to feed back the errors and sort it all out.
But for now success. It will get the data from hawkes bay and make it into mqtt and then home assistant can pick it up automatically . Once in Home Assitant you can do all kinds of things to integrate into controls or make nice looking dashboard displays . The screenshot is a simple display of data
#!/usr/bin/env python3
"""
can2mqtt_hbay.py — Hawkes Bay CAN → MQTT bridge
Features:
 - Reads battery voltage, current, power
 - Reads MPPT info
 - Reads Whizbang Jr current
 - Reads daily cumulative kWh (0x022) with corrected scaling
 - Publishes MQTT topics and full JSON state
"""

import can
import paho.mqtt.client as mqtt
import json
import time
from datetime import datetime

# ----------------------------
# Configuration
# ----------------------------
MQTT_BROKER = "192.168.3.50"
MQTT_PORT = 1883
MQTT_PREFIX = "hawkesbay"
DISCOVERY_PREFIX = "homeassistant"
CAN_INTERFACE = "can0"

# ----------------------------
# MQTT client setup
# ----------------------------
client = mqtt.Client()
client.connect(MQTT_BROKER, MQTT_PORT, 60)

# ----------------------------
# CAN bus setup
# ----------------------------
bus = can.interface.Bus(channel=CAN_INTERFACE, bustype="socketcan")

print("📡 Hawkes Bay CAN → MQTT Bridge running...")

# ----------------------------
# Helper functions
# ----------------------------
def pub(topic, value, retain=True):
    """Publish a value to hawkesbay/<topic>"""
    full = f"{MQTT_PREFIX}/{topic}"
    if isinstance(value, (dict, list)):
        payload = json.dumps(value)
    else:
        payload = str(value)
    client.publish(full, payload, retain=retain)


def ha_discovery(sensor_id, name, unit, topic, device_class=None, state_class=None):
    """Publish Home Assistant discovery config"""
    cfg_topic = f"{DISCOVERY_PREFIX}/sensor/midnite_hawkes_bay_{sensor_id}/config"
    payload = {
        "name": name,
        "uniq_id": f"midnite_hawkes_bay_{sensor_id}",
        "state_topic": f"{MQTT_PREFIX}/{topic}",
        "unit_of_measurement": unit,
        "device": {
            "identifiers": ["midnite_hawkes_bay"],
            "name": "Midnite Hawkes Bay",
            "model": "Hawke's Bay",
            "manufacturer": "Midnite Solar"
        }
    }
    if device_class:
        payload["device_class"] = device_class
    if state_class:
        payload["state_class"] = state_class
    client.publish(cfg_topic, json.dumps(payload), retain=True)


def publish_all_discovery():
    ha_discovery("battery_voltage", "Battery Voltage", "V", "battery/voltage", "voltage", "measurement")
    ha_discovery("battery_current", "Battery Current", "A", "battery/current", "current", "measurement")
    ha_discovery("battery_power", "Battery Power", "W", "battery/power", "power", "measurement")
    ha_discovery("battery_temp_c", "Battery Temp C", "°C", "battery/temp_c", "temperature", "measurement")
    ha_discovery("battery_temp_f", "Battery Temp F", "°F", "battery/temp_f", "temperature", "measurement")
    ha_discovery("state_of_charge", "State of Charge", "%", "state_of_charge", None, "measurement")
    ha_discovery("arc_fault", "Arc Fault", "", "arc_fault")
    ha_discovery("mppt_0_status", "MPPT 0 Status", "", "mppt_0/status")
    ha_discovery("pv_voltage_mppt2", "PV Voltage MPPT2", "V", "pv/voltage_mppt2", "voltage", "measurement")
    ha_discovery("pv_current_mppt2", "PV Current MPPT2", "A", "pv/current_mppt2", "current", "measurement")
    ha_discovery("daily_kwh_production", "Daily kWh Production", "kWh", "daily/kwh_today", "energy", "total")
    ha_discovery("daily_kwh_yesterday", "Daily kWh Yesterday", "kWh", "daily/kwh_yesterday", "energy", "total")
    ha_discovery("whizbang_jr_amps", "Whizbang Jr Amps", "A", "whizbang/amps", "current", "measurement")
    ha_discovery("full_hbay_json_state", "Midnite Hawkes Bay JSON State", "", "state", None, None)


publish_all_discovery()

# ----------------------------
# State object
# ----------------------------
state = {
    "battery": {},
    "pv": {},
    "daily": {},
    "whizbang": {},
    "mppt": {},
}

# ----------------------------
# Main loop
# ----------------------------
try:
    while True:
        msg = bus.recv()
        if not msg:
            time.sleep(0.01)
            continue

        canid = msg.arbitration_id
        data = list(msg.data)
        register = (canid >> 18) & 0x7FF
        device_index = (canid >> 7) & 0x0F

        # ----------------------------
        # Battery voltage/current/power 0x0A0
        # ----------------------------
        if register == 0x0A0 and len(data) >= 4:
            voltage = (data[0] << 8 | data[1]) / 10.0
            current = (data[2] << 8 | data[3]) / 10.0
            power = voltage * current
            state["battery"].update({"voltage": voltage, "current": current, "power": power})
            pub("battery/voltage", voltage)
            pub("battery/current", current)
            pub("battery/power", power)

        # ----------------------------
        # MPPT2 instant voltage/current 0x081
        # ----------------------------
        elif register == 0x081 and len(data) >= 4:
            mppt2_v = (data[0] << 8 | data[1]) / 10.0
            mppt2_i = (data[2] << 8 | data[3]) / 10.0
            state["pv"].update({"mppt2_voltage": mppt2_v, "mppt2_current": mppt2_i})
            pub("pv/voltage_mppt2", mppt2_v)
            pub("pv/current_mppt2", mppt2_i)

        # ----------------------------
        # Whizbang Jr real-time 0x2A3
        # ----------------------------
        elif register == 0x2A3 and len(data) >= 4:
            status = data[0]
            mode = data[1]
            raw_amps = (data[2] << 8 | data[3])
            if raw_amps & 0x8000:
                raw_amps -= 0x10000
            amps = raw_amps / 10.0
            state["whizbang"].update({"status": status, "mode": mode, "amps": amps})
            pub("whizbang/amps", amps)
            pub("whizbang/status", status)
            pub("whizbang/mode", mode)

        # ----------------------------
        # Daily kWh 0x022
        # ----------------------------
        elif register == 0x022 and len(data) >= 4:
            raw_energy = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]
            kwh = raw_energy / 100.0
            state["daily"]["today"] = kwh
            pub("daily/kwh_today", round(kwh, 3))

        # ----------------------------
        # Update JSON state and MQTT loop
        # ----------------------------
        state["timestamp"] = datetime.utcnow().isoformat() + "Z"
        pub("state", state)
        client.loop(0.01)

except KeyboardInterrupt:
    print("Exiting on user interrupt...")
except Exception as e:
    print("Unhandled exception:", e)
    raise

system 1
Classic 150 , 5s3p  Kyocera 135watt , 12s Soneil 2v 540amp lead crystal 24v pack , Outback 3524 inverter
 5s 135w Kyocero , 3s3p 270w Kyocera   Classic 150 ,8s2p  Kyocera 225w to Hawkes Bay Jakiper 48v 20kwh  ,Gobel 16 kwh  lifepo4 Outback VFX 3648  8s2p 380w Rec pv EG4 6000XP

ClassicCrazy

system 1
Classic 150 , 5s3p  Kyocera 135watt , 12s Soneil 2v 540amp lead crystal 24v pack , Outback 3524 inverter
 5s 135w Kyocero , 3s3p 270w Kyocera   Classic 150 ,8s2p  Kyocera 225w to Hawkes Bay Jakiper 48v 20kwh  ,Gobel 16 kwh  lifepo4 Outback VFX 3648  8s2p 380w Rec pv EG4 6000XP