Hi, nein passiert ab und zu. Ich habe noch nicht rausgefunden wann es genau passiert. Starte den Adapter dann neu und dann ist wieder alles ok.
Bin auch dank KI mit meiner PV Reglung ein Riesen Stück weiter, dass Blockly ist irgendwann ausgeartet :-D und KI hat dann vorgeschlagen ein Js Scrip zu erstellen. Und das Funktioniert seit Heute perfekt.
/**
* PV-Miner-Regelung V2.17
*
* Funktionen:
* - Sofortige Auswahl/Umschaltung zwischen S9 und S19 anhand der verfügbaren PV-Leistung
* - powerTarget-Regelung höchstens alle 3 Minuten
* - powerTarget wird nur bei tatsächlicher Änderung geschrieben
* - Sollwertänderung stoppt den Miner NICHT
* - Steckdosen werden bei Bedarf eingeschaltet
* - Steckdosen werden nach 1 Stunde Leerlauf ausgeschaltet
* - nach dem Einschalten der Steckdose 30 Sekunden warten
* - danach enabled auf true setzen und bis zu 2 Minuten auf Bestätigung warten
* - running erst setzen, wenn enabled tatsächlich true ist
*
* Netzleistung EVCC:
* negativ = Einspeisung
* positiv = Netzbezug
*/
// ------------------------------------------------------------
// Datenpunkte
// ------------------------------------------------------------
const GRID_POWER = 'mqtt.1.evcc_Neu.site.grid.power';
const BATTERY_POWER = 'mqtt.1.evcc_Neu.site.battery.power';
const S9 = {
name: 'S9',
socket: 'sonoff.0.Aktor_MinerS9.POWER',
actualPower: 'sonoff.0.Aktor_MinerS9.ENERGY_Power',
enabled: 'miner.0.miner.c3c555fb-7131-4676-99e1-d0f799ad8001.enabled',
running: 'miner.0.miner.c3c555fb-7131-4676-99e1-d0f799ad8001.control.running',
powerTarget: 'miner.0.miner.c3c555fb-7131-4676-99e1-d0f799ad8001.control.powerTarget',
minPower: 500,
maxPower: 1450
};
const S19 = {
name: 'S19',
socket: 'sonoff.0.Aktor_Miner.POWER',
actualPower: 'sonoff.0.Aktor_Miner.ENERGY_Power',
enabled: 'miner.0.miner.b7b9de41-7da9-4e8c-8469-e6fe4f16d5f1.enabled',
running: 'miner.0.miner.b7b9de41-7da9-4e8c-8469-e6fe4f16d5f1.control.running',
powerTarget: 'miner.0.miner.b7b9de41-7da9-4e8c-8469-e6fe4f16d5f1.control.powerTarget',
minPower: 1500,
maxPower: 2500
};
// ------------------------------------------------------------
// Einstellungen
// ------------------------------------------------------------
const POWER_REGULATION_INTERVAL_MS = 2 * 60 * 1000; // 2 Minuten
const SOCKET_IDLE_OFF_MS = 60 * 60 * 1000; // 1 Stunde
const S9_ENABLE_CHECK_DELAY_MS = 10 * 1000; // S9: 10 Sekunden Bootzeit
const S19_ENABLE_CHECK_DELAY_MS = 60 * 1000; // S19: 60 Sekunden Bootzeit
const ENABLE_RETRY_INTERVAL_MS = 5 * 1000; // danach alle 5 Sekunden prüfen
const ENABLE_WAIT_TIMEOUT_MS = 2 * 60 * 1000; // maximal 2 Minuten warten
const START_PROTECTION_MS = 3 * 60 * 1000;
const S19_POWER_RESTART_MS = 3 * 60 * 1000;
const S19_UP_STABLE_MS = 3 * 60 * 1000; // 3 Minuten stabiler Überschuss vor Erhöhung // S19 braucht nach powerTarget 3 Minuten // 3 Minuten Startschutz
const SWITCH_OVERLAP_MS = 10 * 1000; // 10 Sekunden Überlappung
const PV_RESERVE_W = 100;
const GRID_TARGET_W = -50; // Ziel: 50 W Einspeisung // Einspeisereserve
const S9_POWER_STEP_W = 100; // Sollwert-Schritte S9
const S19_POWER_STEP_W = 200; // Sollwert-Schritte S19
const RUNNING_POWER_THRESHOLD_W = 300; // ab 300 W gilt Miner als laufend
const BATTERY_DISCHARGE_LIMIT_W = 100; // Toleranz für Entladung
const S9_START_THRESHOLD_W = 600;
const S19_START_THRESHOLD_W = 1600;
const S19_KEEP_THRESHOLD_W = 1450;
// ------------------------------------------------------------
// Interne Variablen
// ------------------------------------------------------------
let lastPowerRegulation = 0;
let regulationTimer = null;
const socketOffTimers = {
S9: null,
S19: null
};
const startTimers = {
S9: null,
S19: null
};
const startedAt = {
S9: 0,
S19: 0
};
let switching = false;
let switchTimer = null;
const lastPowerChangeAt = {
S9: 0,
S19: 0
};
let s19UpCandidate = null;
let s19UpCandidateSince = 0;
const lastValidTarget = {
S9: S9.minPower,
S19: S19.minPower
};
// ------------------------------------------------------------
// Hilfsfunktionen
// ------------------------------------------------------------
function getNumber(id, fallback = 0) {
const state = getState(id);
const value = Number(state && state.val);
return Number.isFinite(value) ? value : fallback;
}
function getBoolean(id) {
const state = getState(id);
return Boolean(state && state.val);
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function roundToStep(value, step) {
return Math.round(value / step) * step;
}
function isPowerRestartActive(miner) {
if (miner.name !== 'S19') {
return false;
}
return Date.now() - lastPowerChangeAt.S19 < S19_POWER_RESTART_MS;
}
function isMinerActuallyRunning(miner) {
const measuredPower = getNumber(miner.actualPower, 0);
if (measuredPower >= RUNNING_POWER_THRESHOLD_W) {
return true;
}
if (isPowerRestartActive(miner)) {
return true;
}
return getBoolean(miner.running);
}
function currentMinerPower() {
if (isMinerActuallyRunning(S19)) {
const measured = getNumber(S19.actualPower, 0);
if (measured >= RUNNING_POWER_THRESHOLD_W) {
return measured;
}
const target = getNumber(S19.powerTarget, 0);
if (target >= S19.minPower) {
lastValidTarget.S19 = target;
return target;
}
return lastValidTarget.S19 || S19.minPower;
}
if (isMinerActuallyRunning(S9)) {
const measured = getNumber(S9.actualPower, 0);
if (measured >= RUNNING_POWER_THRESHOLD_W) {
return measured;
}
const target = getNumber(S9.powerTarget, 0);
if (target >= S9.minPower) {
lastValidTarget.S9 = target;
return target;
}
return lastValidTarget.S9 || S9.minPower;
}
return 0;
}
function availablePvPower() {
const gridPower = getNumber(GRID_POWER, 0);
const batteryPower = getNumber(BATTERY_POWER, 0);
// Batterie: positiv = Entladen, negativ = Laden.
// Entladung reduziert die für Miner verfügbare Leistung.
return currentMinerPower() - gridPower - Math.max(0, batteryPower);
}
function clearStartTimer(miner) {
if (startTimers[miner.name]) {
clearTimeout(startTimers[miner.name]);
startTimers[miner.name] = null;
}
}
function cancelSocketOff(miner) {
if (socketOffTimers[miner.name]) {
clearTimeout(socketOffTimers[miner.name]);
socketOffTimers[miner.name] = null;
log(`${miner.name}: Leerlauf-Abschaltung abgebrochen`, 'debug');
}
}
function scheduleSocketOff(miner) {
cancelSocketOff(miner);
socketOffTimers[miner.name] = setTimeout(() => {
if (!getBoolean(S9.socket)) {
handleSocketOff(S9);
}
if (!getBoolean(S19.socket)) {
handleSocketOff(S19);
}
socketOffTimers[miner.name] = null;
if (getBoolean(miner.running)) {
return;
}
log(`${miner.name}: 1 Stunde Leerlauf erreicht, Abschaltsequenz startet`, 'info');
let attempts = 0;
const maxAttempts = 12;
function disableAndSwitchOff() {
if (getBoolean(miner.running)) {
log(`${miner.name}: Abschaltsequenz abgebrochen, Miner läuft wieder`, 'info');
return;
}
if (!getBoolean(miner.enabled)) {
setState(miner.socket, false);
log(`${miner.name}: enabled ist false, Steckdose ausgeschaltet`, 'info');
return;
}
attempts += 1;
setState(miner.enabled, false);
log(`${miner.name}: enabled=false gesendet, Versuch ${attempts}/${maxAttempts}`, 'info');
if (attempts >= maxAttempts) {
log(`${miner.name}: enabled wurde nicht false; Steckdose bleibt eingeschaltet`, 'warn');
return;
}
setTimeout(disableAndSwitchOff, 5000);
}
disableAndSwitchOff();
}, SOCKET_IDLE_OFF_MS);
log(`${miner.name}: Steckdose wird bei weiterem Leerlauf in 1 Stunde ausgeschaltet`, 'debug');
}
function ensureSocket(miner) {
cancelSocketOff(miner);
if (!getBoolean(miner.socket)) {
setState(miner.socket, true);
log(`${miner.name}: Steckdose eingeschaltet`, 'info');
}
}
function stopMiner(miner) {
if (miner.name === 'S19') {
s19UpCandidate = null;
s19UpCandidateSince = 0;
}
clearStartTimer(miner);
if (getBoolean(miner.running)) {
setState(miner.running, false);
log(`${miner.name}: Miner gestoppt`, 'info');
}
scheduleSocketOff(miner);
}
function startMiner(miner) {
cancelSocketOff(miner);
ensureSocket(miner);
if (getBoolean(miner.running)) {
return;
}
if (startTimers[miner.name]) {
return;
}
const startAttemptAt = Date.now();
function checkEnabledAndStart() {
if (getBoolean(miner.running)) {
clearStartTimer(miner);
return;
}
if (getBoolean(miner.enabled)) {
clearStartTimer(miner);
setState(miner.running, true);
startedAt[miner.name] = Date.now();
log(`${miner.name}: enabled ist true, Miner gestartet`, 'info');
return;
}
if (Date.now() - startAttemptAt >= ENABLE_WAIT_TIMEOUT_MS) {
clearStartTimer(miner);
log(`${miner.name}: enabled wurde innerhalb von 2 Minuten nicht true`, 'warn');
return;
}
startTimers[miner.name] = setTimeout(checkEnabledAndStart, ENABLE_RETRY_INTERVAL_MS);
}
const enableDelay =
miner.name === 'S19'
? S19_ENABLE_CHECK_DELAY_MS
: S9_ENABLE_CHECK_DELAY_MS;
startTimers[miner.name] = setTimeout(() => {
startTimers[miner.name] = null;
if (!getBoolean(miner.enabled)) {
setState(miner.enabled, true);
log(`${miner.name}: enabled nach ${Math.round(enableDelay / 1000)} Sekunden auf true gesetzt`, 'info');
}
startTimers[miner.name] = setTimeout(checkEnabledAndStart, ENABLE_RETRY_INTERVAL_MS);
}, enableDelay);
}
function setPowerTargetIfChanged(miner, target) {
const powerStep = miner.name === 'S19' ? S19_POWER_STEP_W : S9_POWER_STEP_W;
const roundedTarget = roundToStep(
clamp(target, miner.minPower, miner.maxPower),
powerStep
);
const currentTarget = getNumber(miner.powerTarget, 0);
if (currentTarget === roundedTarget) {
log(`${miner.name}: Sollwert unverändert bei ${roundedTarget} W`, 'debug');
return false;
}
setState(miner.powerTarget, roundedTarget);
lastValidTarget[miner.name] = roundedTarget;
if (miner.name === 'S19') {
lastPowerChangeAt.S19 = Date.now();
log(`S19: powerTarget geändert – 3 Minuten Neustartschutz aktiv`, 'info');
}
log(`${miner.name}: powerTarget auf ${roundedTarget} W geändert`, 'info');
return true;
}
function clearSwitchTimer() {
if (switchTimer) {
clearTimeout(switchTimer);
switchTimer = null;
}
}
function switchMiner(newMiner, oldMiner) {
if (switching) {
return;
}
switching = true;
clearSwitchTimer();
startMiner(newMiner);
const started = Date.now();
function waitForNewMiner() {
if (getBoolean(newMiner.running)) {
switchTimer = setTimeout(() => {
switchTimer = null;
stopMiner(oldMiner);
switching = false;
log(`${newMiner.name} läuft; ${oldMiner.name} wurde nach 10 Sekunden gestoppt`, 'info');
}, SWITCH_OVERLAP_MS);
return;
}
if (Date.now() - started >= ENABLE_WAIT_TIMEOUT_MS + S19_ENABLE_CHECK_DELAY_MS) {
switching = false;
clearSwitchTimer();
log(`${newMiner.name}: Umschaltung abgebrochen, Miner wurde nicht gestartet`, 'warn');
return;
}
switchTimer = setTimeout(waitForNewMiner, ENABLE_RETRY_INTERVAL_MS);
}
switchTimer = setTimeout(waitForNewMiner, ENABLE_RETRY_INTERVAL_MS);
}
function protectBatteryImmediately() {
const batteryPower = getNumber(BATTERY_POWER, 0);
if (batteryPower <= BATTERY_DISCHARGE_LIMIT_W) {
return;
}
if (switching) {
return;
}
const s19Running = getBoolean(S19.running);
const s9Running = getBoolean(S9.running);
if (s19Running) {
if (isPowerRestartActive(S19)) {
log('Batterieschutz S19 wartet: Neustart nach powerTarget läuft', 'debug');
return;
}
const current = getNumber(S19.powerTarget, S19.minPower);
const reduced = roundToStep(
clamp(current - batteryPower, S19.minPower, S19.maxPower),
S19_POWER_STEP_W
);
if (reduced < S19.minPower || current - batteryPower < S19.minPower) {
log(`Batterie entlädt mit ${batteryPower} W: Wechsel vom S19 auf S9`, 'info');
switchMiner(S9, S19);
return;
}
if (reduced !== current) {
setState(S19.powerTarget, reduced);
lastValidTarget.S19 = reduced;
log(`Batterieschutz: S19 powerTarget sofort auf ${reduced} W reduziert`, 'info');
}
return;
}
if (s9Running) {
const current = getNumber(S9.powerTarget, S9.minPower);
const rawReduced = current - batteryPower;
if (rawReduced < S9.minPower) {
log(`Batterie entlädt mit ${batteryPower} W: S9 wird gestoppt`, 'info');
stopMiner(S9);
return;
}
const reduced = roundToStep(
clamp(rawReduced, S9.minPower, S9.maxPower),
S9_POWER_STEP_W
);
if (reduced !== current) {
setState(S9.powerTarget, reduced);
lastValidTarget.S9 = reduced;
log(`Batterieschutz: S9 powerTarget sofort auf ${reduced} W reduziert`, 'info');
}
}
}
// ------------------------------------------------------------
// Sofortige Miner-Auswahl
// ------------------------------------------------------------
function selectMinerImmediately() {
if (switching) {
return;
}
const available = availablePvPower();
const now = Date.now();
const s9Running = isMinerActuallyRunning(S9);
const s19Running = isMinerActuallyRunning(S19);
if (isPowerRestartActive(S19)) {
return;
}
// Während der ersten 3 Minuten nach einem Start nicht aufgrund
// kurzzeitig unstabiler Adapter-/Netzwerte wieder abschalten.
if (s19Running && now - startedAt.S19 < START_PROTECTION_MS) {
return;
}
if (s9Running && now - startedAt.S9 < START_PROTECTION_MS) {
return;
}
// S19 mit Hysterese beibehalten.
if (s19Running && available >= S19_KEEP_THRESHOLD_W) {
return;
}
// Ab 1900 W vom S9 auf den S19 wechseln.
if (available >= S19_START_THRESHOLD_W) {
if (s19Running) {
return;
}
if (s9Running) {
switchMiner(S19, S9);
} else {
startMiner(S19);
}
return;
}
// Zwischen 600 W und 1899 W den S9 verwenden.
if (available >= S9_START_THRESHOLD_W) {
if (s9Running) {
return;
}
if (s19Running) {
switchMiner(S9, S19);
} else {
startMiner(S9);
}
return;
}
// Zu wenig Überschuss: laufende Miner stoppen.
stopMiner(S9);
stopMiner(S19);
}
// ------------------------------------------------------------
// Leistungsregelung – höchstens alle 3 Minuten
// ------------------------------------------------------------
function regulatePower() {
if (switching) {
log('Regelzyklus übersprungen: Miner-Umschaltung läuft', 'debug');
return;
}
const now = Date.now();
const elapsed = now - lastPowerRegulation;
if (elapsed < POWER_REGULATION_INTERVAL_MS) {
if (!regulationTimer) {
regulationTimer = setTimeout(() => {
regulationTimer = null;
regulatePower();
}, POWER_REGULATION_INTERVAL_MS - elapsed);
}
return;
}
lastPowerRegulation = now;
const gridPower = getNumber(GRID_POWER, 0);
const batteryPower = getNumber(BATTERY_POWER, 0);
const batteryDischarge = Math.max(0, batteryPower);
if (isMinerActuallyRunning(S19)) {
if (isPowerRestartActive(S19)) {
const rest = Math.ceil(
(S19_POWER_RESTART_MS - (Date.now() - lastPowerChangeAt.S19)) / 1000
);
log(`REGELZYKLUS S19 übersprungen: Neustart nach Leistungsänderung, noch ca. ${rest} s`, 'info');
return;
}
const currentTarget = getNumber(S19.powerTarget, S19.minPower);
const correction = gridPower - GRID_TARGET_W + batteryDischarge;
const rawTarget = currentTarget - correction;
const newTarget = roundToStep(
clamp(rawTarget, S19.minPower, S19.maxPower),
S19_POWER_STEP_W
);
log(
`REGELZYKLUS S19 | Netz=${gridPower} W | Ziel=${GRID_TARGET_W} W | ` +
`Batterie=${batteryPower} W | Aktuell=${currentTarget} W | ` +
`Korrektur=${Math.round(correction)} W | Neu=${newTarget} W`,
'info'
);
// Reduzieren immer sofort.
if (newTarget < currentTarget) {
s19UpCandidate = null;
s19UpCandidateSince = 0;
setPowerTargetIfChanged(S19, newTarget);
return;
}
// Keine Änderung nötig.
if (newTarget === currentTarget) {
s19UpCandidate = null;
s19UpCandidateSince = 0;
return;
}
// Erhöhung: erst nach 3 Minuten stabiler Anforderung.
if (s19UpCandidate !== newTarget) {
s19UpCandidate = newTarget;
s19UpCandidateSince = Date.now();
log(`S19: höhere Stufe ${newTarget} W erkannt – 3-Minuten-Stabilitätszeit gestartet`, 'info');
return;
}
const stableFor = Date.now() - s19UpCandidateSince;
if (stableFor >= S19_UP_STABLE_MS) {
log(`S19: Überschuss 3 Minuten stabil – Erhöhung auf ${newTarget} W`, 'info');
s19UpCandidate = null;
s19UpCandidateSince = 0;
setPowerTargetIfChanged(S19, newTarget);
return;
}
const rest = Math.ceil((S19_UP_STABLE_MS - stableFor) / 1000);
log(`S19: höhere Stufe ${newTarget} W noch nicht freigegeben – ca. ${rest} s stabil nötig`, 'info');
return;
}
if (isMinerActuallyRunning(S9)) {
const currentTarget = getNumber(S9.powerTarget, S9.minPower);
const correction = gridPower - GRID_TARGET_W + batteryDischarge;
const rawTarget = currentTarget - correction;
const newTarget = roundToStep(
clamp(rawTarget, S9.minPower, S9.maxPower),
S9_POWER_STEP_W
);
log(
`REGELZYKLUS S9 | Netz=${gridPower} W | Ziel=${GRID_TARGET_W} W | ` +
`Batterie=${batteryPower} W | Aktuell=${currentTarget} W | ` +
`Korrektur=${Math.round(correction)} W | Neu=${newTarget} W`,
'info'
);
setPowerTargetIfChanged(S9, newTarget);
return;
}
log(
`REGELZYKLUS | kein Miner aktiv | Netz=${gridPower} W | Ziel=${GRID_TARGET_W} W | ` +
`Batterie=${batteryPower} W`,
'info'
);
}
function handleSocketOff(miner) {
if (getBoolean(miner.socket)) {
return;
}
clearStartTimer(miner);
cancelSocketOff(miner);
if (getBoolean(miner.running)) {
setState(miner.running, false);
log(`${miner.name}: Steckdose ist AUS -> running=false`, 'info');
}
if (getBoolean(miner.enabled)) {
setState(miner.enabled, false);
log(`${miner.name}: Steckdose ist AUS -> enabled=false`, 'info');
}
}
// ------------------------------------------------------------
// Trigger
// ------------------------------------------------------------
// Sofortige Reaktion auf Änderungen der Netzleistung.
on({ id: GRID_POWER, change: 'ne' }, () => {
selectMinerImmediately();
regulatePower();
});
// Sofortige Reaktion auf Batterieentladung.
on({ id: BATTERY_POWER, change: 'ne' }, () => {
protectBatteryImmediately();
selectMinerImmediately();
regulatePower();
});
// Wenn eine Steckdose – auch durch eine andere Logik – ausgeschaltet wird,
// den Miner im Adapter sofort auf offline setzen.
on({ id: S9.socket, change: 'ne' }, obj => {
if (!Boolean(obj.state.val)) {
handleSocketOff(S9);
}
});
on({ id: S19.socket, change: 'ne' }, obj => {
if (!Boolean(obj.state.val)) {
handleSocketOff(S19);
}
});
// Leerlauftimer zuverlässig an running koppeln.
on({ id: S9.running, change: 'ne' }, obj => {
if (Boolean(obj.state.val)) {
startedAt.S9 = Date.now();
cancelSocketOff(S9);
} else {
scheduleSocketOff(S9);
}
});
on({ id: S19.running, change: 'ne' }, obj => {
if (Boolean(obj.state.val)) {
startedAt.S19 = Date.now();
cancelSocketOff(S19);
} else {
scheduleSocketOff(S19);
}
});
// ------------------------------------------------------------
// Startinitialisierung
// ------------------------------------------------------------
setTimeout(() => {
const s9AlreadyRunning = isMinerActuallyRunning(S9);
const s19AlreadyRunning = isMinerActuallyRunning(S19);
if (s9AlreadyRunning) {
startedAt.S9 = Date.now() - START_PROTECTION_MS;
cancelSocketOff(S9);
log(`STARTERKENNUNG: S9 läuft bereits | Leistung=${getNumber(S9.actualPower, 0)} W`, 'info');
}
if (s19AlreadyRunning) {
startedAt.S19 = Date.now() - START_PROTECTION_MS;
cancelSocketOff(S19);
log(`STARTERKENNUNG: S19 läuft bereits | Leistung=${getNumber(S19.actualPower, 0)} W`, 'info');
}
selectMinerImmediately();
regulatePower();
if (!getBoolean(S9.running)) {
scheduleSocketOff(S9);
}
if (!getBoolean(S19.running)) {
scheduleSocketOff(S19);
}
log('PV-Miner-Regelung V2.17 gestartet', 'info');
}, 5000);