You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(deye): correct the TOU slot payload found by the Sunsynk comparison
Four corrections to the control payload, all surfaced by diffing this
component against the Sunsynk port built from it and against a real
inverter's live settings.
**A zero-length window became a multi-hour full-power grid charge (#4560).**
build_tou_slots() added an action segment at the window start and then
called setdefault() for the return-to-self-use segment at the window end -
a no-op when the two times are equal, so the action slot ran on until the
next boundary, up to four hours. The schedule default is
start = end = "00:00:00" and the control entities are written one at a
time, so an enable event landing before the times do produced exactly
this: a grid charge to the window's target at full power while
_window_active() - which has always guarded start == end - reported the
window inactive. The same guard now runs in build_tou_slots(), on the
normalised times so "02:00:00" against "02:00" is caught too.
**Solar Sell was switched off outside export windows (#4580).**
solarSellAction governs whether surplus PV reaches the grid at all, not
what the battery does, and it was derived from the window active right
now - on inside an export window, off everywhere else. A user with an
ordinary overnight charge window therefore had spare solar curtailed all
through the following day, invisibly: nothing in Predbat's model
represents PV curtailment, so the lost export revenue never showed up.
It is now written on unconditionally. Fixed in build_dynamic_payload()
rather than derive_control_state(), whose solar_sell field also
classifies action-vs-self-use slots.
**Non-export states now use ZERO_EXPORT_TO_CT (#4580).** The CT variant
measures at the grid CT, so the battery serves the whole house without
exporting; ZERO_EXPORT_TO_LOAD measures at the inverter's own output and
on a CT-clamp install would stop the battery serving anything not wired
to it, with the shortfall drawn from the grid. Confirmed on Sunsynk
hardware, which sits behind the same registers.
**Zero slot power is a freeze, and it was on the wrong slots (#4581).**
TimeUseSettingItem.power is the slot's charge/export power and zero is
how the inverter is told to hold. Self-use slots - every interval Predbat
is not actively charging or exporting - carried zero, freezing the
battery as its default state and putting the house on the grid, while the
two freeze states carried the requested power, the opposite of a freeze.
Self-use slots now carry the inverter's rated power, falling back to the
battery's own maximum charge rate for a model whose device/latest has no
RatedPower, and failing closed if neither is known: no payload is built
and nothing is written, because a zero there cannot be justified. That
warning is throttled to once per serial, since _reconcile_control()
rebuilds the payload every cycle. freeze_charge and freeze_export now
carry zero and still classify as action slots, keeping grid_charge and
solar_sell respectively.
hold_charge is deliberately unchanged: the battery is already at target,
so the slot keeps Predbat's chosen charge rate and only the grid-charge
flag differs. Zero there would mean freeze, which is a different state.
Closes#4560, #4580, #4581
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The top-level work mode / on-off flags follow the window active at
648
698
now_minutes (defaults to the current local time); the 6 TOU slots still
649
699
encode every window's per-slot config.
700
+
701
+
Returns {} when there is no honest self-use slot power for this inverter, which
702
+
the caller treats as "do not write": the alternative is a zero-power slot, and
703
+
zero power freezes the battery (see _self_use_slot).
650
704
"""
651
705
ifnow_minutesisNone:
652
706
now_minutes=self._now_minutes()
653
-
slots=self.build_tou_slots(schedule, current_soc)
707
+
self_use_power=self._self_use_power(sn)
708
+
ifnotself_use_power:
709
+
# Once per serial: _reconcile_control rebuilds this payload every cycle, so an
710
+
# unconditional warning would fill the log for as long as the rating is missing.
711
+
# Cleared again below, so a serial that recovers and then regresses is reported
712
+
# afresh rather than staying quiet.
713
+
ifsnnotinself._self_use_power_warned:
714
+
self._self_use_power_warned.add(sn)
715
+
self.log(f"Warn: DEYE {sn} has no inverter rating and no battery config, so self-use slots would be written with zero power (a freeze); skipping the control write")
0 commit comments