Version: 2.36 Author: dbus-tristar driver Target: Morningstar TriStar MPPT 60 via Modbus TCP
- Overview
- D-Bus Paths Reference
- Voltage Override System
- Current Override System
- Charge Profile Management System
- Automatic Battery Full Detection
- Nightly Reset Logic
- Daily History & State Persistence
- Configuration
- Modbus Register Reference
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
These paths follow the Victron solar charger specification:
/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"
/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)
/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
/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
/History/Daily/1/Yield = Yesterday's yield (kWh)
/History/Daily/1/MaxPower = Yesterday's max power (W)
... (same fields for days 1-30)
Located at: venus-home/N/.../settings/0/Settings/TristarMPPT/...
All settings are writeable and persistent.
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)
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)
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)
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.5Example: 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.5Located at: venus-home/N/.../solarcharger/0/Control/...
/Mode WRITEABLE
1 = On (clears COIL_DISCONNECT, allows charging)
4 = Off (sets COIL_DISCONNECT, stops charging)
/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)
/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
/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
/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):
- Save state.json — preserves daily counters (reset by controller restart)
- DISCONNECT → verify controller is disconnected
- Send reset command
- Smart reconnect — polls until controller responds
- 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}'Located at: venus-home/N/.../solarcharger/0/Custom/...
/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
/Custom/CurrentOverride/Active (bool) - Is current override active?
/Custom/CurrentOverride/CurrentValue (A) - Current override setpoint
/Custom/CurrentOverride/RegisterReadback (A) - Readback from PDU 88
/Custom/Daily/ChargeWh (Wh) - Raw REG_WHC_DAILY value
/Custom/Daily/ChargeAh (Ah) - Daily Ah from TriStar
/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.
/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"
/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
/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
/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
/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)
Top-charge (equalize) the battery to a higher voltage than normal absorption for cell balancing.
Topic: W/.../solarcharger/0/Control/VoltageOverride
Payload: 28.7 (target voltage)
# 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)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_accumulatedBehavior 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.
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!
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)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.
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"
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.
Limit charge current (rarely needed, voltage override alone is sufficient).
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
Topic: W/.../solarcharger/0/Control/CurrentOverride
Payload: 10.0 (current limit in Amps)
Less tested than voltage-only override. Testing showed voltage override alone is sufficient for top-charging.
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.
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 withrestoratcabinafterwards.
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.
At 02:30 local time the driver checks whether the season has changed. Two cases:
- Rest profile active: New season's rest profile is applied automatically.
- Visit profile active: The same visit type is re-applied for the new season (e.g.
autumnplannedvisit→winterplannedvisit). 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).
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)
# 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 9000All sixteen profiles are fully customizable via D-Bus Settings.
# 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"}'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:
- FloatExitTime (Et_float_exit_cum, register 0xE006) — integer seconds
- FloatVoltage (EV_float, register 0xE001)
- AbsorptionVoltage (EV_absorp, register 0xE000)
- 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
- 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)
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 voltageThis matches MS View behavior and ensures exact values (e.g., 26.2V reads back as 26.2V, not 26.19V).
# 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"# 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 GetValuedbus -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.
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- Parameter validation - Ensures float < absorption, values in safe ranges
- DISCONNECT before writes - Controller not actively regulating
- Backups - Automatic timestamped backup before changes
- Verification - Read-after-write with ±0.01V tolerance
- Thread safety - Main loop paused, no Modbus collisions
- Voltage limits - Enforced in Settings (22.0-32.0V for all voltage params)
- Smart retry - Control path resets to '' after each operation, allows immediate retry
- Status tracking - ApplyStatus prevents concurrent operations
- Idempotency - If the requested profile is already active, the EEPROM write is skipped entirely (no unnecessary EEPROM cycles)
- 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
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)
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 GetValueCheck 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 GetValueNotes:
- 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.
autumnmaybevisit→wintermaybevisit). - Same-profile writes are no-ops (idempotent) — safe to re-send from HA without causing unnecessary EEPROM cycles.
Battery is considered "full" when all conditions are met:
- Voltage at target:
v_battery >= target_voltage - Low current:
i_charge < BatteryFullCurrent(e.g., 2.0A) - Excess power (if enabled):
excess_power > ExcessPowerThresholdOR threshold = -1 - Sustained: Condition held for
TailCurrentTimeseconds (e.g., 300s)
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).
/Custom/VoltageOverride/TailCurrentTimer = 0-300 (seconds)
When timer reaches TailCurrentTime setting → Battery full, override disables.
Prevent stuck charging sessions (e.g., if battery never reaches tail current).
Every night at NightlyResetHour (default: 03:00 local time).
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- Battery is not charging (no sun)
- Unlikely to interfere with user activity
- Configurable via
/Settings/TristarMPPT/NightlyResetHour
Location: /data/dbus-tristar/state.json
{
"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
}
}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.jsonTrigger: 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()State is saved:
- Every
StateSaveIntervalseconds (default: 300s = 5 min) - Immediately at midnight rollover
- Immediately when important changes occur
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).
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
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
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_RESET_CONTROL = 0 # Reset controller
COIL_RESET_COMM = 1 # Reset comm server
COIL_DISCONNECT = 2 # Disconnect charger
COIL_EQUALIZE = 5 # Trigger equalize
# 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)-
Check safety limit:
/Settings/TristarMPPT/MaxVoltageOverrideVoltage >= target -
Verify register write:
/Custom/VoltageOverride/RegisterReadback should match CurrentVoltage -
Check TriStar charge state:
/Custom/TriStarChargeState should = 9 (SLAVE) when active
-
Check tail current:
/Dc/0/Current < BatteryFullCurrent? -
Check timer:
/Custom/VoltageOverride/TailCurrentTimer should increase to TailCurrentTime -
Check excess power (if enabled):
/Custom/VoltageOverride/ExcessPower > ExcessPowerThreshold? # Or disable check: /Settings/TristarMPPT/ExcessPowerThreshold = -1
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
Fixed in v2.24. If still present:
- Check logs for "Large /Yield/System change" warnings
- Indicates timing issue in rollover or reset detection
# 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# 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# Reset at 02:00 instead of 03:00
dbus -y com.victronenergy.settings \
/Settings/TristarMPPT/NightlyResetHour SetValue 2v2.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
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