Skip to content

Latest commit

 

History

History
1375 lines (1079 loc) · 48.6 KB

File metadata and controls

1375 lines (1079 loc) · 48.6 KB

TriStar MPPT Driver - Complete Documentation

Version: 2.36 Author: dbus-tristar driver Target: Morningstar TriStar MPPT 60 via Modbus TCP


Table of Contents

  1. Overview
  2. D-Bus Paths Reference
  3. Voltage Override System
  4. Current Override System
  5. Charge Profile Management System
  6. Automatic Battery Full Detection
  7. Nightly Reset Logic
  8. Daily History & State Persistence
  9. Configuration
  10. Modbus Register Reference

Overview

This driver reads data from a Morningstar TriStar MPPT charge controller via Modbus TCP and exposes it to Venus OS via D-Bus. It implements advanced features including:

  • Charge profile management for seasonal battery optimization (EEPROM programming)
  • Voltage override for battery top-charging (equalization)
  • Automatic battery full detection via tail current monitoring
  • Cumulative time tracking (tolerates brief voltage dips)
  • 31-day history tracking with daily max/min values (Day 0-30)
  • Nightly reset to prevent stuck charging sessions
  • State persistence across reboots

D-Bus Paths Reference

Standard Victron Paths

These paths follow the Victron solar charger specification:

Device Information

/ProductName         = "TriStar MPPT 60"
/Mgmt/ProductId      = 0xABCD
/Mgmt/Connection     = "Modbus TCP 192.168.2.103:502"
/FirmwareVersion     = 42 (TriStar firmware version)
/HardwareVersion     = "v1.1"
/Serial              = "1712069"
/DeviceInstance      = 0 (configurable)
/Connected           = 1
/ProductId           = 0xABCD
/CustomName          = "TriStar MPPT 60"

Real-time Measurements

/Dc/0/Voltage            = Battery voltage (V)
/Dc/0/Current            = Charge current (A)
/Pv/V                    = PV array voltage (V)
/Pv/I                    = PV array current (A)
/Yield/Power             = Output power (W)
/Dc/0/Temperature        = Battery temperature (°C)

Charge Control & State

/Mode                    = 1 (On) or 4 (Off) - WRITEABLE
/State                   = Victron charge state (0=Off, 2=Fault, 3=Bulk, 4=Absorption, 5=Float)
/MppOperationMode        = 0 (Off), 1 (Voltage/Current limited), 2 (MPPT tracking)
/ErrorCode               = Victron error code

Daily & Lifetime Yield

/History/Daily/0/Yield           = Today's yield (kWh)
/History/Daily/0/MaxPower        = Today's max power (W)
/History/Daily/0/MaxVoltage      = Today's max battery voltage (V)
/History/Daily/0/MinVoltage      = Today's min battery voltage (V)
/History/Daily/0/MaxCurrent      = Today's max charge current (A)
/History/Daily/0/TimeInBulk      = Time in Bulk today (seconds)
/History/Daily/0/TimeInAbsorption = Time in Absorption today (seconds)
/History/Daily/0/TimeInFloat     = Time in Float today (seconds)

/Yield/System                    = Lifetime total yield (kWh) - from state.json

Historical Data (Days 1-30)

/History/Daily/1/Yield           = Yesterday's yield (kWh)
/History/Daily/1/MaxPower        = Yesterday's max power (W)
... (same fields for days 1-30)

Settings Paths

Located at: venus-home/N/.../settings/0/Settings/TristarMPPT/...

All settings are writeable and persistent.

Connection Settings

IPAddress               = "192.168.2.103"  (IP address of TriStar)
PortNumber              = 502              (Modbus TCP port, range: 1-65535)
SlaveID                 = 1                (Modbus slave ID, range: 1-247)
Interval                = 5000             (Poll interval in ms, range: 1000-60000)
DeviceInstance          = 0                (Venus device instance, range: 0-255)

Operational Settings

StateSaveInterval       = 300              (Save state.json every N seconds, range: 60-3600)
WatchdogTimeout         = 180              (Mark disconnected after N seconds, range: 30-600)
NightlyResetHour        = 3                (Reset at this hour local time, range: 0-23)

Voltage Override Settings

MaxVoltageOverrideVoltage = 28.7           (SAFETY LIMIT - max voltage, range: 24.0-32.0V)
MaxVoltageOverrideTime    = 7200           (Max cumulative seconds battery voltage has been ≥ target voltage before override stops with StopReason="TimeLimit". Counter pauses (not resets) when voltage dips below target. Range: 0-86400)
BatteryFullCurrent        = 2.0            (Tail current threshold in A, range: 0.0-100.0)
TailCurrentTime           = 300            (Sustained tail current time in seconds, range: 0-3600)
ExcessPowerThreshold      = 100            (Minimum excess power in W, range: -1-5000, -1=disabled)

Charge Profile Settings

Seventeen profiles in three categories: 4 rest + 12 seasonal visit + 1 global. All four parameters per profile, all ranges identical.

Rest profiles (seasonal maintenance, auto-applied at 02:30 when season changes):

ChargeProfiles/SummerRest/AbsorptionVoltage   = 27.4  (V, ~55-60% SOC, range: 22.0-32.0)
ChargeProfiles/SummerRest/FloatVoltage        = 27.2  (V, ~45-50% SOC, range: 22.0-32.0)
ChargeProfiles/SummerRest/AbsorptionTime      =  900  (seconds, range: 0-86400)
ChargeProfiles/SummerRest/FloatExitTime       = 7200  (seconds, range: 0-86400)

ChargeProfiles/AutumnRest/AbsorptionVoltage   = 27.8  (V, ~65-70% SOC, range: 22.0-32.0)
ChargeProfiles/AutumnRest/FloatVoltage        = 27.6  (V, ~60-65% SOC, range: 22.0-32.0)
ChargeProfiles/AutumnRest/AbsorptionTime      = 1800  (seconds, range: 0-86400)
ChargeProfiles/AutumnRest/FloatExitTime       = 5400  (seconds, range: 0-86400)

ChargeProfiles/WinterRest/AbsorptionVoltage   = 28.0  (V, ~75-80% SOC, range: 22.0-32.0)
ChargeProfiles/WinterRest/FloatVoltage        = 27.8  (V, ~65-70% SOC, range: 22.0-32.0)
ChargeProfiles/WinterRest/AbsorptionTime      = 2700  (seconds, range: 0-86400)
ChargeProfiles/WinterRest/FloatExitTime       = 5400  (seconds, range: 0-86400)

ChargeProfiles/SpringRest/AbsorptionVoltage   = 27.6  (V, ~60-65% SOC, range: 22.0-32.0)
ChargeProfiles/SpringRest/FloatVoltage        = 27.4  (V, ~55-60% SOC, range: 22.0-32.0)
ChargeProfiles/SpringRest/AbsorptionTime      = 1200  (seconds, range: 0-86400)
ChargeProfiles/SpringRest/FloatExitTime       = 7200  (seconds, range: 0-86400)

Visit profiles (manually activated via HA logical names — see Control Paths). 12 seasonal variants; colder seasons use higher voltages to compensate for OCV shift. Auto-switch at 02:30 re-applies the same visit type for the new season.

MaybeVisit (possibly arriving soon, ~75-82% SOC target):

ChargeProfiles/SummerMaybeVisit/AbsorptionVoltage = 27.9  (V, range: 22.0-32.0)
ChargeProfiles/SummerMaybeVisit/FloatVoltage      = 27.6  (V, range: 22.0-32.0)
ChargeProfiles/SummerMaybeVisit/AbsorptionTime    = 2100  (seconds, range: 0-86400)
ChargeProfiles/SummerMaybeVisit/FloatExitTime     = 3600  (seconds, range: 0-86400)

ChargeProfiles/AutumnMaybeVisit/AbsorptionVoltage = 28.1  (V, range: 22.0-32.0)
ChargeProfiles/AutumnMaybeVisit/FloatVoltage      = 27.8  (V, range: 22.0-32.0)
ChargeProfiles/AutumnMaybeVisit/AbsorptionTime    = 2700  (seconds, range: 0-86400)
ChargeProfiles/AutumnMaybeVisit/FloatExitTime     = 3600  (seconds, range: 0-86400)

ChargeProfiles/WinterMaybeVisit/AbsorptionVoltage = 28.3  (V, range: 22.0-32.0)
ChargeProfiles/WinterMaybeVisit/FloatVoltage      = 28.0  (V, range: 22.0-32.0)
ChargeProfiles/WinterMaybeVisit/AbsorptionTime    = 3600  (seconds, range: 0-86400)
ChargeProfiles/WinterMaybeVisit/FloatExitTime     = 3600  (seconds, range: 0-86400)

ChargeProfiles/SpringMaybeVisit/AbsorptionVoltage = 28.0  (V, range: 22.0-32.0)
ChargeProfiles/SpringMaybeVisit/FloatVoltage      = 27.7  (V, range: 22.0-32.0)
ChargeProfiles/SpringMaybeVisit/AbsorptionTime    = 2400  (seconds, range: 0-86400)
ChargeProfiles/SpringMaybeVisit/FloatExitTime     = 3600  (seconds, range: 0-86400)

PlannedVisit (trip confirmed, charge up in advance, ~85-92% SOC target):

ChargeProfiles/SummerPlannedVisit/AbsorptionVoltage = 28.2  (V, range: 22.0-32.0)
ChargeProfiles/SummerPlannedVisit/FloatVoltage      = 27.6  (V, range: 22.0-32.0)
ChargeProfiles/SummerPlannedVisit/AbsorptionTime    = 2700  (seconds, range: 0-86400)
ChargeProfiles/SummerPlannedVisit/FloatExitTime     = 1800  (seconds, range: 0-86400)

ChargeProfiles/AutumnPlannedVisit/AbsorptionVoltage = 28.4  (V, range: 22.0-32.0)
ChargeProfiles/AutumnPlannedVisit/FloatVoltage      = 27.8  (V, range: 22.0-32.0)
ChargeProfiles/AutumnPlannedVisit/AbsorptionTime    = 3600  (seconds, range: 0-86400)
ChargeProfiles/AutumnPlannedVisit/FloatExitTime     = 1800  (seconds, range: 0-86400)

ChargeProfiles/WinterPlannedVisit/AbsorptionVoltage = 28.6  (V, range: 22.0-32.0)
ChargeProfiles/WinterPlannedVisit/FloatVoltage      = 28.0  (V, range: 22.0-32.0)
ChargeProfiles/WinterPlannedVisit/AbsorptionTime    = 4500  (seconds, range: 0-86400)
ChargeProfiles/WinterPlannedVisit/FloatExitTime     = 1800  (seconds, range: 0-86400)

ChargeProfiles/SpringPlannedVisit/AbsorptionVoltage = 28.3  (V, range: 22.0-32.0)
ChargeProfiles/SpringPlannedVisit/FloatVoltage      = 27.7  (V, range: 22.0-32.0)
ChargeProfiles/SpringPlannedVisit/AbsorptionTime    = 3000  (seconds, range: 0-86400)
ChargeProfiles/SpringPlannedVisit/FloatExitTime     = 1800  (seconds, range: 0-86400)

AtCabin (currently at the cabin, keep battery topped up, ~88-95% SOC target):

ChargeProfiles/SummerAtCabin/AbsorptionVoltage = 28.2   (V, range: 22.0-32.0)
ChargeProfiles/SummerAtCabin/FloatVoltage      = 27.8   (V, range: 22.0-32.0)
ChargeProfiles/SummerAtCabin/AbsorptionTime    = 4500   (seconds, range: 0-86400)
ChargeProfiles/SummerAtCabin/FloatExitTime     =  900   (seconds, range: 0-86400)

ChargeProfiles/AutumnAtCabin/AbsorptionVoltage = 28.36  (V, range: 22.0-32.0)
ChargeProfiles/AutumnAtCabin/FloatVoltage      = 28.0   (V, range: 22.0-32.0)
ChargeProfiles/AutumnAtCabin/AbsorptionTime    = 5400   (seconds, range: 0-86400)
ChargeProfiles/AutumnAtCabin/FloatExitTime     =  900   (seconds, range: 0-86400)

ChargeProfiles/WinterAtCabin/AbsorptionVoltage = 28.6   (V, range: 22.0-32.0)
ChargeProfiles/WinterAtCabin/FloatVoltage      = 28.2   (V, range: 22.0-32.0)
ChargeProfiles/WinterAtCabin/AbsorptionTime    = 6300   (seconds, range: 0-86400)
ChargeProfiles/WinterAtCabin/FloatExitTime     =  900   (seconds, range: 0-86400)

ChargeProfiles/SpringAtCabin/AbsorptionVoltage = 28.3   (V, range: 22.0-32.0)
ChargeProfiles/SpringAtCabin/FloatVoltage      = 27.9   (V, range: 22.0-32.0)
ChargeProfiles/SpringAtCabin/AbsorptionTime    = 4800   (seconds, range: 0-86400)
ChargeProfiles/SpringAtCabin/FloatExitTime     =  900   (seconds, range: 0-86400)

Global profile (season-independent, manual activation only):

ChargeProfiles/ChargeTo100/AbsorptionVoltage  = 28.7  (V, 100% SOC, range: 22.0-32.0)
ChargeProfiles/ChargeTo100/FloatVoltage       = 28.5  (V, ~97-98% SOC, range: 22.0-32.0)
ChargeProfiles/ChargeTo100/AbsorptionTime     = 3600  (seconds, range: 0-86400)
ChargeProfiles/ChargeTo100/FloatExitTime      =  900  (seconds, range: 0-86400)

Season start dates (MM-DD format, configurable):

Season/SpringStart  = "03-01"   (1st March)
Season/SummerStart  = "06-01"   (1st June)
Season/AutumnStart  = "09-01"   (1st September)
Season/WinterStart  = "12-01"   (1st December)

Note: All voltage values are actual system voltages. Driver automatically converts to 12V-equivalent when writing to EEPROM (12V/24V/48V systems supported).

FloatExitTime (Et_float_exit_cum, register 0xE006) = cumulative time the battery voltage must remain below float voltage before the controller exits float stage. Longer = stays in float longer.

Example: Adjust a profile parameter

dbus -y com.victronenergy.settings /Settings/TristarMPPT/ChargeProfiles/WinterRest/AbsorptionVoltage SetValue 28.4
dbus -y com.victronenergy.settings /Settings/TristarMPPT/ChargeProfiles/WinterRest/FloatExitTime SetValue 9000
dbus -y com.victronenergy.settings /Settings/TristarMPPT/ChargeProfiles/AutumnPlannedVisit/AbsorptionVoltage SetValue 28.5

Example: Change autumn start date to October 1st

dbus -y com.victronenergy.settings /Settings/TristarMPPT/Season/AutumnStart SetValue "10-01"

Example: Change tail current threshold

dbus -y com.victronenergy.settings /Settings/TristarMPPT/BatteryFullCurrent SetValue 1.5

Control Paths (Writeable)

Located at: venus-home/N/.../solarcharger/0/Control/...

Mode Control

/Mode                           WRITEABLE
  1 = On  (clears COIL_DISCONNECT, allows charging)
  4 = Off (sets COIL_DISCONNECT, stops charging)

Voltage Override

/Control/VoltageOverride        WRITEABLE (V)
  > 0   = Enable override at specified voltage (e.g., 28.7)
  <= 0  = Disable override

  Safety: Limited to MaxVoltageOverrideVoltage setting
  Register: PDU 89 (vb_ref_slave)
  Effect: Puts TriStar in SLAVE mode (ChargeState = 9)

Current Override

/Control/CurrentOverride        WRITEABLE (A)
  > 0   = Enable current limit (e.g., 10.0)
  <= 0  = Disable current limit

  Register: PDU 88 (Ib_ref_slave)
  NOTE: Auto-enables voltage override to maintain slave mode
  WARNING: Less tested than voltage-only override

Charge Profile Management

/Control/ApplyChargeProfile     WRITEABLE (text)

  Home Assistant logical names (recommended — driver maps to current season):
  "rest"          = Auto-select current season's rest profile
  "maybevisit"    = Apply current season's MaybeVisit profile
  "plannedvisit"  = Apply current season's PlannedVisit profile
  "atcabin"       = Apply current season's AtCabin profile

  Direct profile names (bypass seasonal mapping):
  "summerrest" | "autumnrest" | "winterrest" | "springrest"
  "summermaybevisit" | "autumnmaybevisit" | "wintermaybevisit" | "springmaybevisit"
  "summerplannedvisit" | "autumnplannedvisit" | "winterplannedvisit" | "springplannedvisit"
  "summeratcabin" | "autumnatcabin" | "winteratcabin" | "springatcabin"
  "chargeto100"

  Idempotency: If the requested profile is already active, the write is skipped.

  Effect: Writes charge parameters to EEPROM with safety checks
  Safety: DISCONNECT → Write → Verify → Reset controller
  Thread-safe: Main loop paused during operation
  Backup: Automatic backup before changes
  Status: Monitor via /Custom/ChargeProfile/ApplyStatus

Coil Controls (Momentary Triggers)

/Control/EqualizeTriggered      WRITEABLE (bool) - Trigger equalize charge
/Control/ChargerDisconnect      WRITEABLE (bool) - Force disconnect
/Control/ResetCommServer        WRITEABLE (bool) - Reset TriStar comm server (fire-and-forget)
/Control/ResetController        WRITEABLE (bool) - Safe controller reset (async)

Safe reset procedure (same as after EEPROM writes):

  1. Save state.json — preserves daily counters (reset by controller restart)
  2. DISCONNECT → verify controller is disconnected
  3. Send reset command
  4. Smart reconnect — polls until controller responds
  5. Set daily_register_has_reset=False — driver uses state.json until next sunrise

MQTT command (same path as before):

mosquitto_pub -h <venus-ip> -t "W/<portal-id>/solarcharger/0/Control/ResetController" -m '{"value": 1}'

Custom Monitoring Paths (Read-Only)

Located at: venus-home/N/.../solarcharger/0/Custom/...

Voltage Override Status

/Custom/VoltageOverride/Active              (bool) - Is override active?
/Custom/VoltageOverride/CurrentVoltage      (V) - Current override setpoint
/Custom/VoltageOverride/RegisterReadback    (V) - Readback from PDU 89
/Custom/VoltageOverride/TimeAtTargetVoltage (sec) - Cumulative time at/above target
/Custom/VoltageOverride/TailCurrentTimer    (sec) - Time at tail current condition
/Custom/VoltageOverride/StopReason          (text) - Why override stopped
  Values: "BatteryFull", "TimeLimit", "UserDisabled", "NightlyReset"
/Custom/VoltageOverride/ExcessPower         (W) - Excess power available

Current Override Status

/Custom/CurrentOverride/Active              (bool) - Is current override active?
/Custom/CurrentOverride/CurrentValue        (A) - Current override setpoint
/Custom/CurrentOverride/RegisterReadback    (A) - Readback from PDU 88

Daily Register Tracking (For Debugging)

/Custom/Daily/ChargeWh                      (Wh) - Raw REG_WHC_DAILY value
/Custom/Daily/ChargeAh                      (Ah) - Daily Ah from TriStar

EEPROM Counters & Charge Parameters (TriStar's Internal)

/Custom/EEPROM/ChargeKwhTotal               (kWh) - Lifetime total from 0xE087
/Custom/EEPROM/ChargeKwhResetable           (kWh) - Resetable counter from 0xE086
/Custom/EEPROM/AbsorptionVoltage            (V) - Active absorption voltage (0xE000)
/Custom/EEPROM/FloatVoltage                 (V) - Active float voltage (0xE001)
/Custom/EEPROM/AbsorptionTime               (sec) - Active absorption time (0xE002)
/Custom/EEPROM/FloatExitTime                (sec) - Cumulative float exit timer (0xE006)
/Custom/EEPROM/EqualizeVoltage              (V) - Configured equalize voltage
/Custom/EEPROM/TempCompensation             (V/C) - Temperature compensation
/Custom/EEPROM/MaxRegulationLimit           (V) - Max regulation voltage limit

Note: Voltage values are automatically scaled for 12V/24V/48V systems. EEPROM stores 12V-equivalent values, driver converts to actual system voltage.

Charge Profile Status

/Custom/ChargeProfile/ApplyStatus           (text) - Profile apply operation status
  Values: "idle", "validating", "disconnecting", "writing", "resetting", "success", "failed"
/Custom/ChargeProfile/ProgressPercent       (%) - Apply operation progress (0-100)
/Custom/ChargeProfile/StatusDetail          (text) - Human-readable detail for current/last operation
  During apply: step description (e.g. "Writing EEPROM parameters...")
  On success:   "success" or "success (no changes needed)"
  On failure:   error message
/Custom/ChargeProfile/LastApplied           (text) - "profile_name at YYYY-MM-DD HH:MM:SS"

Seasonal Profile Status

/Custom/Season/CurrentSeason                (text) - Current season: "summer"|"autumn"|"winter"|"spring"
/Custom/Season/ActiveProfile                (text) - Last successfully applied profile name (e.g. "autumnrest")
/Custom/ChargeProfile/PlannedVisitSOC       (%) - Estimated SOC at current season's PlannedVisit absorption voltage
                                                   Based on NMC 7S OCV table at 15°C

Balance Tracking

/Custom/VoltageOverride/BalanceComplete     (bool) - True if BatteryFull detected during last VoltageOverride session
/Custom/VoltageOverride/LastBalanceTimestamp (text) - ISO-8601 timestamp of last BatteryFull event, e.g. "2026-04-01T14:23:05"
                                                      Empty string if no balance has occurred

Battery & Internal Monitoring

/Custom/Battery/SenseVoltage                (V) - Battery sense terminal voltage
/Custom/Battery/CurrentFast                 (A) - Fast charge current sample
/Custom/Battery/VoltageSlow                 (V) - Slow (filtered) battery voltage
/Custom/InternalSupply/Rail12V              (V) - Internal 12V rail
/Custom/InternalSupply/Rail3V               (V) - Internal 3.3V rail
/Custom/InternalSupply/Rail5V               (V) - Internal 5V rail

TriStar Charge State (Raw)

/Custom/TriStarChargeState                  (int) - Raw TriStar charge state
  0 = Start/Night
  1 = Night Check
  2 = Disconnect
  3 = Night
  4 = Fault
  5 = Bulk (MPPT)
  6 = Absorption
  7 = Float
  8 = Equalize
  9 = Slave (override active)

Voltage Override System

Purpose

Top-charge (equalize) the battery to a higher voltage than normal absorption for cell balancing.

How It Works

1. User Enables Override

Topic: W/.../solarcharger/0/Control/VoltageOverride
Payload: 28.7   (target voltage)

2. Driver Validates & Writes

# Safety check
if voltage > MaxVoltageOverrideVoltage:
    REJECT  # Critical safety limit

# Convert to register value
register_value = int(voltage * 182.04)

# Write to TriStar
write_holding_register(89, register_value)  # PDU 89 = vb_ref_slave

# TriStar enters SLAVE mode (ChargeState = 9)

3. Cumulative Time Tracking

Goal: Measure total time at or above target voltage, tolerating brief dips.

# At target: V >= target (NOT within ±0.1V tolerance!)
at_target = v_battery >= target_voltage

# Accumulate time (Home Assistant history_stats pattern)
if at_target and override_start_time is None:
    override_start_time = current_time
    time_above_target_accumulated = 0

if at_target:  # ONLY accumulates when V >= target!
    delta = current_time - last_update
    if 0 < delta < 10:  # Sanity check
        time_above_target_accumulated += delta
# If V < target: Counter PAUSES (doesn't increase, doesn't reset)

# Result: Cumulative seconds at/above target
time_at_target = time_above_target_accumulated

Behavior during voltage dips:

  • ✅ Counts ONLY when V >= target voltage
  • ✅ PAUSES when V < target (stops counting)
  • ✅ Does NOT reset when voltage dips
  • ✅ RESUMES counting when V >= target again

Example:

10:00 - V=28.70V → Counter starts, accumulated = 0s
10:30 - V=28.71V → accumulated = 1800s (30 min AT target)
10:30 - V=28.68V → PAUSED at 1800s (below target, stops counting)
10:31 - V=28.67V → Still 1800s (paused, not counting)
10:32 - V=28.70V → RESUMES counting from 1800s
10:33 - V=28.71V → accumulated = 1860s (30 min + 1 min AT target)

This is cumulative time AT/ABOVE target, not total elapsed time. Brief dips don't reset the counter, they just pause it.

4. Tail Current Detection

Condition: Battery is "full" when:

  • Voltage ≥ target voltage, AND
  • Current < BatteryFullCurrent (e.g., 2.0A), AND
  • Excess power check passes (if enabled), AND
  • Sustained for TailCurrentTime seconds (e.g., 300s)
if at_target and i_charge < battery_full_current:
    if tail_current_start_time is None:
        tail_current_start_time = current_time

    tail_duration = current_time - tail_current_start_time

    if tail_duration >= tail_current_time:
        # Battery full!
        stop_reason = "BatteryFull"
        write_holding_register(89, -16)  # Disable override (0xFFF0)
else:
    tail_current_start_time = None  # RESET timer to zero!

Behavior during current fluctuations:

  • ✅ Counts only when ALL conditions are met (V >= target AND I < threshold)
  • ❌ RESETS to zero if ANY condition fails
  • ⚠️ Different from TimeAtTarget which only pauses!

Example:

11:00 - V=28.70V, I=1.8A → Timer starts, duration = 0s
11:02 - V=28.71V, I=1.7A → duration = 120s
11:03 - V=28.72V, I=2.1A → RESET to 0s! (I > 2.0A)
11:04 - V=28.71V, I=1.9A → Timer starts again, duration = 0s
11:09 - V=28.70V, I=1.8A → duration = 300s → BATTERY FULL!

CRITICAL: This must be 300 seconds continuously with low current. Any spike above threshold resets the timer!

5. Time Limit

Condition: TimeAtTargetVoltage (cumulative seconds at/above target — pauses during dips, never resets) reaches MaxVoltageOverrideTime.

if time_at_target >= max_voltage_override_time:
    # Time limit reached!
    stop_reason = "TimeLimit"
    write_holding_register(89, -16)  # Disable override (0xFFF0)

6. User Disables

Payload: 0   (or any value <= 0)

Immediately writes 0xFFF0 (-16 signed) to PDU 89 to disable slave mode.

Note: The Morningstar Modbus spec says "write a negative value to disable slave control" without specifying which value. 0xFFFF (-1) and 0x0000 (0) both cause incorrect behavior (state 10 or slave control timeout). The correct value is 0xFFF0 (-16), confirmed by Morningstar technical support.

Monitoring

Active status:

/Custom/VoltageOverride/Active = True/False

Progress:

/Custom/VoltageOverride/TimeAtTargetVoltage = 1234  (seconds)
/Custom/VoltageOverride/TailCurrentTimer = 45       (seconds at tail current)

Completion:

/Custom/VoltageOverride/StopReason = "BatteryFull"

Timer Comparison: TimeAtTarget vs TailCurrent

Important: These two timers work differently!

Timer Behavior Reset Condition Purpose
TimeAtTarget PAUSES during dips Never resets (only on disable) Safety time limit (2h max)
TailCurrent RESETS to zero Resets if I > threshold OR V < target Detect battery full

Example scenario:

10:00 - V=28.70V, I=8.0A
  → TimeAtTarget starts (0s)
  → TailCurrent not started (I too high)

10:30 - V=28.71V, I=1.8A
  → TimeAtTarget = 1800s (counting continuously)
  → TailCurrent starts (0s)

10:32 - V=28.68V, I=1.9A  (brief voltage dip)
  → TimeAtTarget = 1920s (PAUSED, but not reset)
  → TailCurrent RESET to 0s! (V < target)

10:33 - V=28.70V, I=1.7A
  → TimeAtTarget = 1920s (RESUMES counting)
  → TailCurrent starts again (0s)

10:38 - V=28.71V, I=1.6A
  → TimeAtTarget = 2220s
  → TailCurrent = 300s → BATTERY FULL!

Key insight: TimeAtTarget is forgiving (pauses), TailCurrent is strict (resets). This ensures we only stop charging when conditions have been stable for 5 continuous minutes.


Current Override System

Purpose

Limit charge current (rarely needed, voltage override alone is sufficient).

How It Works

Very similar to voltage override, but:

  • Writes to PDU 88 (Ib_ref_slave) instead of PDU 89
  • Auto-enables voltage override to maintain slave mode (TriStar requirement)
  • Sets voltage to MaxVoltageOverrideVoltage

Usage

Topic: W/.../solarcharger/0/Control/CurrentOverride
Payload: 10.0   (current limit in Amps)

Warning

Less tested than voltage-only override. Testing showed voltage override alone is sufficient for top-charging.


Charge Profile Management System

Purpose

Switch between seasonal battery charging profiles by safely programming EEPROM charge parameters. Optimizes NMC/NCA 7S lithium-ion battery life based on season and cabin usage patterns.

Profile Categories

Rest profiles (cabin unoccupied — maintenance charging, 4 total):

  • summerrest — Gentle maintenance, lowest voltages (~55% SOC)
  • autumnrest — Moderate maintenance (~65% SOC)
  • winterrest — Higher voltage for cold temperatures (~75% SOC)
  • springrest — Gentle spring maintenance (~60% SOC)

Global profiles (season-independent, manual activation only, 1 total):

  • chargeto100 — Charge to 100% SOC (28.7V abs, 28.5V float). Use when you want a full battery. Follow up with rest or atcabin afterwards.

Visit profiles (manual activation via HA logical names, 12 total = 4 seasons × 3 types):

  • maybevisit — Possibly arriving soon (~75-82% SOC target)
  • plannedvisit — Trip confirmed, charge up in advance (~85-92% SOC target)
  • atcabin — Currently at the cabin, keep topped up (~88-95% SOC target)

Each visit type exists in four seasonal variants (e.g. summermaybevisit, autumnmaybevisit, etc.) with voltages adjusted for temperature-dependent OCV shifts. When using HA logical names the driver automatically selects the correct seasonal variant.

Seasonal Auto-Switch

At 02:30 local time the driver checks whether the season has changed. Two cases:

  1. Rest profile active: New season's rest profile is applied automatically.
  2. Visit profile active: The same visit type is re-applied for the new season (e.g. autumnplannedvisitwinterplannedvisit). This keeps the correct SOC target but adjusts for cold-weather OCV.

If the season hasn't changed, no action is taken.

Season boundaries are configurable (default: Spring=Mar 1, Summer=Jun 1, Autumn=Sep 1, Winter=Dec 1).

Parameters Programmed

Each profile sets four EEPROM charge parameters:

  • AbsorptionVoltage (EV_absorp, 0xE000) - Target voltage for absorption stage
  • FloatVoltage (EV_float, 0xE001) - Voltage for float/maintenance stage
  • AbsorptionTime (Et_absorp, 0xE002) - Max duration of absorption stage (seconds)
  • FloatExitTime (Et_float_exit_cum, 0xE006) - Cumulative time below float voltage before exiting float (seconds)

How It Works

1. Configure Profile (Optional)

# Adjust a profile parameter via D-Bus Settings
dbus -y com.victronenergy.settings /Settings/TristarMPPT/ChargeProfiles/WinterRest/AbsorptionVoltage SetValue 28.4
dbus -y com.victronenergy.settings /Settings/TristarMPPT/ChargeProfiles/WinterRest/FloatVoltage SetValue 27.6
dbus -y com.victronenergy.settings /Settings/TristarMPPT/ChargeProfiles/WinterRest/AbsorptionTime SetValue 9000
dbus -y com.victronenergy.settings /Settings/TristarMPPT/ChargeProfiles/WinterRest/FloatExitTime SetValue 9000

All sixteen profiles are fully customizable via D-Bus Settings.

2. Apply Profile

# Auto-select rest profile for current season
dbus -y com.victronenergy.solarcharger.tristar_0 /Control/ApplyChargeProfile SetValue "rest"

# Apply current season's PlannedVisit profile (e.g. autumnplannedvisit in autumn)
dbus -y com.victronenergy.solarcharger.tristar_0 /Control/ApplyChargeProfile SetValue "plannedvisit"

# Apply a specific seasonal profile directly
dbus -y com.victronenergy.solarcharger.tristar_0 /Control/ApplyChargeProfile SetValue "winteratcabin"

# Via MQTT (Home Assistant) — use HA logical names
mosquitto_pub -h <venus-ip> -t "W/<portal-id>/solarcharger/0/Control/ApplyChargeProfile" -m '{"value": "plannedvisit"}'

3. Driver Executes Safe EEPROM Write Procedure

Following Morningstar's recommended procedure:

Step 1: Validation

  • Read profile settings from D-Bus
  • Validate parameter relationships (float < absorption, etc.)
  • Read current EEPROM values
  • Compare and filter (only write changed parameters)

Step 2: Backup

  • Create timestamped backup: /data/dbus-tristar/eeprom_backups/backup_YYYYMMDD_HHMMSS.json
  • Backup includes all four charge parameters

Step 3: DISCONNECT

  • Set COIL_DISCONNECT (stop charging)
  • Poll for DISCONNECT state (max 5s)
  • Ensures controller is not actively regulating

Step 4: Write EEPROM

  • Write parameters in safe order:
    1. FloatExitTime (Et_float_exit_cum, register 0xE006) — integer seconds
    2. FloatVoltage (EV_float, register 0xE001)
    3. AbsorptionVoltage (EV_absorp, register 0xE000)
    4. AbsorptionTime (Et_absorp, register 0xE002)
  • Automatic voltage scaling (12V→24V→48V conversion)
  • 200ms delay between writes

Step 5: Verify

  • Read back all written parameters
  • Convert from 12V-equivalent to actual voltage
  • Tolerance: ±0.01V for voltages, exact match for time
  • Raises exception if verification fails

Step 6: Reset Controller

  • Write COIL_RESET_CTRL (Morningstar recommended)
  • Controller reboots (connection closes - expected)
  • Smart reconnect with polling (typical 0.5-1.0s)

Step 7: Verify Normal Operation

  • Check charge state (should exit DISCONNECT)
  • Update active EEPROM display paths
  • Mark operation as successful

Thread Safety

  • Main update loop paused during entire operation
  • Dedicated Modbus client created for EEPROM writes
  • Prevents collision with regular polling
  • Main loop resumes in finally block (even on error)

Voltage Scaling (Critical)

EEPROM stores 12V-equivalent values. Driver automatically detects system voltage from V_PU:

  • V_PU < 100: 12V system (scale = 1×)
  • V_PU < 200: 24V system (scale = 2×)
  • V_PU ≥ 200: 48V system (scale = 4×)

Example: Writing 28.6V on 24V system:

voltage_12v = 28.6 / 2 = 14.3V               # Convert to 12V-equivalent
raw_value = round(14.3 / (v_pu * 2^-15))     # Scale to register value
# Write raw_value to EEPROM
# Read back: 14.3V × 2 = 28.6V                # Convert back to system voltage

This matches MS View behavior and ensures exact values (e.g., 26.2V reads back as 26.2V, not 26.19V).

Monitoring

Status

# Check operation status
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/ChargeProfile/ApplyStatus GetValue
# Returns: "idle", "validating", "disconnecting", "writing", "resetting", "success", "failed"

# Check progress
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/ChargeProfile/ProgressPercent GetValue
# Returns: 0-100

# Check for errors
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/ChargeProfile/StatusDetail GetValue

# Check what was last applied
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/ChargeProfile/LastApplied GetValue
# Returns: "winter at 2026-03-26 20:45:48"

Active EEPROM Values

# Check what's actually programmed in the controller
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/EEPROM/AbsorptionVoltage GetValue
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/EEPROM/FloatVoltage GetValue
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/EEPROM/AbsorptionTime GetValue
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/EEPROM/FloatExitTime GetValue

Season & Active Profile

dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/Season/CurrentSeason GetValue
# Returns: "summer", "autumn", "winter", or "spring"

dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/Season/ActiveProfile GetValue
# Returns last applied profile name, e.g. "autumnrest" or "winterplannedvisit"

dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/ChargeProfile/PlannedVisitSOC GetValue
# Returns estimated SOC% at current season's PlannedVisit absorption voltage (NMC 7S OCV @ 15°C)

These values update automatically after successful profile apply.

Balance Tracking

dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/VoltageOverride/BalanceComplete GetValue
# Returns: True if last VoltageOverride session ended with BatteryFull detection

dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/VoltageOverride/LastBalanceTimestamp GetValue
# Returns: ISO-8601 timestamp of last successful balance, e.g. "2026-04-01T14:23:05"
# Returns empty string if no balance has occurred

# Note: BalanceComplete resets to False when a new VoltageOverride session starts

Safety Features

  1. Parameter validation - Ensures float < absorption, values in safe ranges
  2. DISCONNECT before writes - Controller not actively regulating
  3. Backups - Automatic timestamped backup before changes
  4. Verification - Read-after-write with ±0.01V tolerance
  5. Thread safety - Main loop paused, no Modbus collisions
  6. Voltage limits - Enforced in Settings (22.0-32.0V for all voltage params)
  7. Smart retry - Control path resets to '' after each operation, allows immediate retry
  8. Status tracking - ApplyStatus prevents concurrent operations
  9. Idempotency - If the requested profile is already active, the EEPROM write is skipped entirely (no unnecessary EEPROM cycles)

Typical Operation Time

  • No changes: < 1 second (validation only)
  • With EEPROM writes: 10-15 seconds
    • Validation: 1s
    • DISCONNECT: 1s
    • Write 4 parameters: 2s
    • Verify: 1s
    • Reset + reconnect: 5-8s
    • Final verification: 1s

Error Handling

If operation fails:

  • Status set to "failed"
  • Error message available in StatusDetail path
  • Controller attempts to re-enable charging (clear DISCONNECT)
  • Main loop resumes (driver continues normal operation)
  • User can retry after fixing issue (status resets to "idle" on next attempt)

Home Assistant Integration Example

The driver accepts four HA logical names. The seasonal variant is resolved automatically by the driver based on the current season.

automation:
  # Trip confirmed — charge up in advance (driver picks summerplannedvisit / autumnplannedvisit etc.)
  - alias: "Planned cabin visit - charge up"
    action:
      - service: mqtt.publish
        data:
          topic: "W/<portal-id>/solarcharger/0/Control/ApplyChargeProfile"
          payload: '{"value": "plannedvisit"}'

  # Left the cabin — return to seasonal maintenance
  - alias: "Back from cabin - return to seasonal rest"
    action:
      - service: mqtt.publish
        data:
          topic: "W/<portal-id>/solarcharger/0/Control/ApplyChargeProfile"
          payload: '{"value": "rest"}'

  # Monthly cell balance — trigger equalization override
  - alias: "Monthly cell balance"
    action:
      - service: mqtt.publish
        data:
          topic: "W/<portal-id>/solarcharger/0/Control/VoltageOverride"
          payload: '{"value": 28.7}'

Check balance status:

dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/VoltageOverride/BalanceComplete GetValue
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/VoltageOverride/LastBalanceTimestamp GetValue
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/ChargeProfile/PlannedVisitSOC GetValue

Check current season and active profile:

dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/Season/CurrentSeason GetValue
dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/Season/ActiveProfile GetValue

Notes:

  • Seasonal rest switching is handled automatically at 02:30 local time — no HA automation needed.
  • Visit profile auto-switch at 02:30 re-applies the same type for the new season (e.g. autumnmaybevisitwintermaybevisit).
  • Same-profile writes are no-ops (idempotent) — safe to re-send from HA without causing unnecessary EEPROM cycles.

Automatic Battery Full Detection

Algorithm

Battery is considered "full" when all conditions are met:

  1. Voltage at target: v_battery >= target_voltage
  2. Low current: i_charge < BatteryFullCurrent (e.g., 2.0A)
  3. Excess power (if enabled): excess_power > ExcessPowerThreshold OR threshold = -1
  4. Sustained: Condition held for TailCurrentTime seconds (e.g., 300s)

Excess Power Check

Purpose: Only stop charging if there's excess solar power available. Don't stop if battery acceptance is low due to lack of sun.

excess_power = (v_pv * i_pv) - (v_battery * i_charge)

# Check passes if:
# - Threshold is -1 (disabled), OR
# - excess_power > 0
check_excess_power = (threshold == -1) or (excess_power > 0)

Example:

  • Solar input: 28V × 8A = 224W
  • Battery charge: 28.7V × 2A = 57W
  • Excess: 224 - 57 = 167W > threshold (100W) ✓

If excess < threshold, don't stop (battery might accept more if sun was stronger).

Monitoring Tail Current

/Custom/VoltageOverride/TailCurrentTimer = 0-300  (seconds)

When timer reaches TailCurrentTime setting → Battery full, override disables.


Nightly Reset Logic

Purpose

Prevent stuck charging sessions (e.g., if battery never reaches tail current).

When

Every night at NightlyResetHour (default: 03:00 local time).

What It Does

if local_hour == reset_hour and 0 <= local_minute < 5:
    if not nightly_reset_done:
        # 1. Disable voltage override
        write_holding_register(89, -1)
        pending_voltage_override = None
        voltage_override_active = False
        stop_reason = "NightlyReset"

        # 2. Reset cumulative time counter
        time_above_target_accumulated = 0
        override_start_time = None
        tail_current_start_time = None

        # 3. Reset TriStar comm server (clears any stale state)
        write_coil(COIL_RESET_COMM, True)

        nightly_reset_done = True

elif local_hour != reset_hour:
    nightly_reset_done = False  # Reset flag for next night

Why 03:00?

  • Battery is not charging (no sun)
  • Unlikely to interfere with user activity
  • Configurable via /Settings/TristarMPPT/NightlyResetHour

Daily History & State Persistence

State File

Location: /data/dbus-tristar/state.json

Structure

{
  "current_date": "2026-03-15",
  "daily_register_has_reset": false,
  "total_yield_kwh": 266.11,
  "today": {
    "date": "2026-03-15",
    "yield_kwh": 0.537,
    "max_power_w": 201,
    "max_voltage": 28.55,
    "min_voltage": 27.82,
    "max_current": 6.9,
    "time_bulk_s": 1234,
    "time_absorption_s": 567,
    "time_float_s": 890
  },
  "history": [
    {
      "date": "2026-03-14",
      "yield_kwh": 0.69,
      "max_power_w": 182,
      ...
    },
    ...
  ],
  "lifetime": {
    "max_voltage": 29.1,
    "min_voltage": 24.5,
    "max_power_w": 350,
    "max_current": 12.5
  }
}

Daily Register Reset Detection

Problem: TriStar's daily register (REG_WHC_DAILY) is frozen from midnight until sunrise.

Solution: Track reset state with flag.

# At midnight (local time):
daily_register_has_reset = False  # Register is now frozen

# At sunrise - detect actual reset:
if current_daily_wh < last_daily_register_value:
    # Register decreased = reset happened!
    daily_register_has_reset = True
elif current_daily_wh > 0 and last_daily_register_value == 0:
    # Edge case: No charging yesterday (was 0, now > 0)
    daily_register_has_reset = True

# Use appropriate value:
if daily_register_has_reset:
    daily_kwh = current_daily_wh / 1000.0  # Use Modbus
else:
    daily_kwh = state['today']['yield_kwh']  # Use state.json

Midnight Rollover

Trigger: Local date changes (checked every update cycle).

if local_date != state['current_date']:
    # 1. Save today's final values to history
    history.insert(0, {
        'date': state['current_date'],
        'yield_kwh': state['today']['yield_kwh'],
        'max_power_w': state['today']['max_power_w'],
        ...
    })

    # 2. Add today's yield to lifetime total
    total_yield_kwh += state['today']['yield_kwh']

    # 3. Keep only 30 days of history (Day 1-30, plus Day 0 = 31 total)
    history = history[:30]

    # 4. Reset today's values
    state['today'] = {
        'date': local_date,
        'yield_kwh': 0.0,
        'max_power_w': 0,
        'max_voltage': 0.0,
        'min_voltage': 999.0,
        ...
    }

    # 5. Set flag to False (register is frozen)
    daily_register_has_reset = False

    # 6. Update current date
    state['current_date'] = local_date

    # 7. Save state.json immediately
    save_state()

Periodic Save

State is saved:

  • Every StateSaveInterval seconds (default: 300s = 5 min)
  • Immediately at midnight rollover
  • Immediately when important changes occur

Configuration

CONFIG Dictionary (dbus_tristar.py)

CONFIG = {
    # Connection
    'default_ip': '192.168.2.103',
    'default_modbus_port': 502,
    'default_slave_id': 1,
    'default_device_instance': 0,
    'default_poll_interval_ms': 5000,

    # Timing
    'state_save_interval_sec': 300,
    'watchdog_timeout_sec': 180,
    'nightly_reset_hour': 3,

    # Voltage override
    'excess_power_threshold': 100,
    'max_voltage_override_voltage': 28.7,
    'max_voltage_override_time': 7200,
    'battery_full_current': 2.0,
    'tail_current_time': 300,

    # Device
    'custom_name': 'TriStar MPPT 60',
}

All values can be overridden via /Settings/TristarMPPT/... paths (persistent).


Modbus Register Reference

Key Registers Used

Input Registers (Read-Only)

REG_V_PU = 0                   # Voltage scaling factor (v_pu)
REG_I_PU = 2                   # Current scaling factor (i_pu)
REG_VER_SW = 4                 # Software version
REG_V_BAT = 24                 # Battery voltage
REG_I_CC = 28                  # Charge current
REG_V_PV = 27                  # PV array voltage
REG_I_PV = 29                  # PV array current
REG_T_BAT = 37                 # Battery temperature
REG_I_CC_1M = 39               # Charge current (1-min average)
REG_CHARGE_STATE = 50          # Charge state (0-9)
REG_V_TARGET = 51              # Target regulation voltage
REG_POUT = 58                  # Output power
REG_V_BAT_MIN = 64             # Min battery voltage (daily)
REG_V_BAT_MAX = 65             # Max battery voltage (daily)
REG_V_PV_MAX = 66              # Max PV voltage (daily)
REG_AHC_DAILY = 67             # Ah charge daily
REG_WHC_DAILY = 68             # Wh charge daily
REG_POUT_MAX_DAILY = 70        # Max power (daily)
REG_T_ABS = 77                 # Time in absorption
REG_T_EQ = 78                  # Time in equalize
REG_T_FLOAT = 79               # Time in float
REG_FLAGS = 91                 # Alarm/fault flags

Holding Registers (Read/Write)

PDU 88 = Ib_ref_slave          # Current override (slave mode)
PDU 89 = vb_ref_slave          # Voltage override (slave mode) ← PRIMARY
PDU 90 = va_ref_fixed          # Array voltage fixed (disables MPPT)
PDU 91 = va_ref_fixed_pct      # Array voltage fixed percent

Note: PDU address = Logical address - 1

EEPROM Registers (Input, Read-Only)

0xE000 = EV_absorp             # Absorption voltage
0xE007 = EV_eq                 # Equalize voltage
0xE00D = EV_tempcomp           # Temperature compensation
0xE010 = Evb_ref_lim           # Max regulation voltage limit
0xE086 = EkWhc_r               # kWh charge resetable
0xE087 = EkWhc_t               # kWh charge total (lifetime)

Coil Registers (Read/Write Boolean)

COIL_RESET_CONTROL = 0         # Reset controller
COIL_RESET_COMM = 1            # Reset comm server
COIL_DISCONNECT = 2            # Disconnect charger
COIL_EQUALIZE = 5              # Trigger equalize

Scaling Formulas

# Voltage (V)
v_pu = read_input_register(0) / 32768.0 * 96.667
voltage = register_value * v_pu / 32768.0

# Current (A)
i_pu = read_input_register(2) / 32768.0 * 79.16
current = register_value * i_pu / 32768.0

# Temperature (°C)
temp = register_value / 10.0

# Power (W)
power = register_value * v_pu * i_pu / 131072.0

# Override register conversion
voltage_register = int(voltage * 182.04)  # 24V system
current_register = int(current * 409.6)

Troubleshooting

Voltage Override Not Working

  1. Check safety limit:

    /Settings/TristarMPPT/MaxVoltageOverrideVoltage >= target
    
  2. Verify register write:

    /Custom/VoltageOverride/RegisterReadback should match CurrentVoltage
    
  3. Check TriStar charge state:

    /Custom/TriStarChargeState should = 9 (SLAVE) when active
    

Battery Full Detection Not Triggering

  1. Check tail current:

    /Dc/0/Current < BatteryFullCurrent?
    
  2. Check timer:

    /Custom/VoltageOverride/TailCurrentTimer should increase to TailCurrentTime
    
  3. Check excess power (if enabled):

    /Custom/VoltageOverride/ExcessPower > ExcessPowerThreshold?
    
    # Or disable check:
    /Settings/TristarMPPT/ExcessPowerThreshold = -1
    

Daily Yield Shows 0

Check if register has reset:

/Custom/Daily/ChargeWh shows actual Modbus value

If non-zero but /History/Daily/0/Yield = 0:

  • Flag issue: Driver thinks register is frozen
  • Check logs for "daily_register_has_reset" messages

Midnight Spike in /Yield/System

Fixed in v2.24. If still present:

  • Check logs for "Large /Yield/System change" warnings
  • Indicates timing issue in rollover or reset detection

Example Workflows

Manual Top-Charge Session

# 1. Enable voltage override to 28.7V
dbus -y com.victronenergy.solarcharger.tristar_0 \
  /Control/VoltageOverride SetValue 28.7

# 2. Monitor progress
watch 'dbus -y com.victronenergy.solarcharger.tristar_0 /Custom/VoltageOverride/TimeAtTargetVoltage GetValue'

# 3. Check tail current timer
dbus -y com.victronenergy.solarcharger.tristar_0 \
  /Custom/VoltageOverride/TailCurrentTimer GetValue

# 4. Wait for automatic stop (BatteryFull)
# OR manually disable:
dbus -y com.victronenergy.solarcharger.tristar_0 \
  /Control/VoltageOverride SetValue 0

Adjust Tail Current Settings

# Set tail current threshold to 1.5A
dbus -y com.victronenergy.settings \
  /Settings/TristarMPPT/BatteryFullCurrent SetValue 1.5

# Set sustained time to 10 minutes
dbus -y com.victronenergy.settings \
  /Settings/TristarMPPT/TailCurrentTime SetValue 600

Change Nightly Reset Time

# Reset at 02:00 instead of 03:00
dbus -y com.victronenergy.settings \
  /Settings/TristarMPPT/NightlyResetHour SetValue 2

Version History

v2.24 (Current)

  • Fix: Midnight spike in /Yield/System (recalculate daily_kwh after flag changes)
  • Fix: Sunrise spike detection (don't set flag prematurely on charging detection)
  • Add: Comprehensive logging for spike debugging
  • Add: EEPROM lifetime kWh counters exposed
  • Add: Daily register tracking for debugging

v2.23

  • Add: Cumulative time tracking (Home Assistant style)
  • Add: Nightly reset at configurable hour
  • Fix: Voltage override register addressing (PDU 89, not 90)

v2.22

  • Add: Tail current detection for battery full
  • Add: Excess power threshold check
  • Add: Automatic override stopping conditions

Support

Issues & Questions:

  • Check logs: /var/log/dbus-tristar/current
  • State file: /data/dbus-tristar/state.json
  • Driver code: /data/dbus-tristar/dbus_tristar.py

Key Log Messages:

"Voltage override enabled: X.XXV"
"Tail current detected: V=X.XX, I=X.XX"
"Battery full detected: voltage at X.XX, tail current X.XX for XXXs"
"Detected REG_WHC_DAILY reset: XXX → XXX Wh (sun came up)"
"Performing midnight rollover"
"Nightly reset at XX:00"

End of Documentation