Hierbij de centrale Class "Process" in C# waarmee ik de SMA Home Storage 9.6 kWh aanstuur.
wat inmiddels werkt:
- aansturen SMA inverter(s) via modbus
- aansturen Nefit Enviline Monoblock (warmtepomp)
- aansturen van de SMA EV Charger 22 (met hoeveel wordt de EV geladen?)
- Publiseren van de Applicatie naar RaspberryPi met pi-os (arm & arm64)
- Live ophalen van de EPEX spot-market-data via de Tibber API. (dynamische stroomprijzen)
Programma functies:
1. Batterij laden verschuiven naar de uren dat de stroomprijs laag is. (hiermee bespaar ik +/- 40,- euro/jaar)
2. Bij negatieve stroomprijzen batterij laden (stroom kopen) van het grid.
3. Bij een groot verschil in stroomprijs tussen twee opvolgende dagen eventueel stroom verkopen.
4. Tot op zekere hoogte rekening houden met zomer & winter tijd
5. Bij het laden van de EV eventuele conflikt situaties tussen EV en home batterij oplossen.
6. Per dag een email versturen met belangrijke informatie (bv. hoogste en laaste dag prijzen, verkoop / inkoop stroom enz..)
hopelijk lukt het me dit jaar nog het programma functie-compleet en bugvrij te krijgen.
Hierna wordt het geheel incl. libraries op Github gepubliseerd voor de liefhebber(s)
Code: Selecteer alles
// Program..: Process.cs
// Author...: G. Wassink
// Design...:
// Date.....: 12/06/2024 Last revised: 25/10/2024
// Notice...: Copyright 1999, All Rights Reserved
// Notes....: C#12 .Net8
// Files....: None
// Programs.:
// Publish : dotnet publish --runtime linux-arm
// Publish : dotnet publish --runtime linux-arm64
// Reserved.: Type Class (Process)
using System.Net;
using GWCharger;
using GWEmail;
using GWEnviline;
using GWModbus;
using GWTibber;
using SMABatteryControl.Classes;
namespace SMABatteryControl;
public class Process
{
/* Local variables */
private int currDay = 0;
private readonly int eventPeriod = 360; // 360 seconds -> 6 minutes
private Season season = Season.Summer;
/* Local Objects */
private readonly EnvilineApiClient hp = new(IPAddress.Parse("192.168.2.110"), "xxxxxx", "gijs");
private readonly ModbusApiClient stpSE = new(IPAddress.Parse("192.168.2.109"));
private readonly ChargerApiClient evCharger = new(IPAddress.Parse("192.168.2.103"), "xxxxxx", "xxxxxxx");
private readonly TibberApiClient tibber = new("xxxxxx", lat: xxxxxxx, lng: xxxxx); Netherlands -> geo-Location
private readonly EmailUserData eMailUserData = new("xxxxxxx", "xxxxxx", "xxxxxxxx", "xxxxxxxxx", ["xxxxxxxxxx"], [""], true);
private Timer? tmrMain = null; // Event handler (Timer Object)
/* Process Status */
private ProcessStatus status = ProcessStatus.Idle;
/* EPEX spot market Energy (Buying & Selling in Euro cents) */
private const int BuyPrice = 0; // Buy price (Euro cent/kWh)
private const int BuyMarge = 35; // Buy marge (Euro cent/kWh)
private const int SalesPrice = 35; // Seles price (Euro cent/kWh)
private const int SalesMarge = 35; // Sales marge (Euro cent/kWh)
/* Battery -> SOC -> Charge & Discharge power */
private const int SOCMax = 100; // SOC = 100%
private const int SOCMin = 0; // SOC = 0%
private const int SOCBuyMin = 35; // SOC = 35%
private const int SOCSellMin = 75; // SOC = 75%
private const int ChargePower = 5000; // Charge power (Watt)
private const int DischargePower = 5000; // Discharge power (Watt)
/* Heatpump */
private const int dhwTemp = 0;
private const int indoorTempMin = 13;
private const int indoorTempMax = 18;
private const int indoorTempNormal = 15;
private const int outdoorTempMin = 0;
public void Start() => tmrMain = new Timer(MainProcess, null, 10000, (int)TimeSpan.FromSeconds(eventPeriod).TotalMilliseconds); // Start processing in 6 minutes
private void MainProcess(object? state)
{
// tmrMain?.Change(Timeout.Infinite, Timeout.Infinite); // For debug purposese only
var currDateTime = DateTime.Now; // Get current DateTime (Utc->Ignore Summer/Winter time)
season = Util.GetSeason(currDateTime);
/* Testing */
// var a = evCharger.Power; // Ok
// var b = evCharger.Mode; // Ok
// var r = evCharger.Reboot; // Ok
// var c = evCharger.SetPower(0); // Ok
// var m = evCharger.Mode; // Ok
// evCharger.RefreshToken();
// var et = evCharger.EnergyTotal;
// var s = evCharger.Status;
try
{
if (currDay != currDateTime.Day) // Switch day on Midnight
{
tibber.GetEPEX(currDateTime.Year, currDateTime.Month, currDateTime.Day);
Util.InitDay(currDateTime, season, this.hp.OutdoorTemp, this.hp.IndoorTemp, this.hp.DHWTemp, stpSE.BatterySOC(), tibber, eMailUserData);
currDay = currDateTime.Day;
status = ProcessStatus.Break;
}
else if (currDateTime.Hour is 13 or 14) // Tibber: After One o'çlock midday Tibber publishes EPEX spot market tariffs/prices for ToDay and Tomorrow (Try for two hours)
{
if (!tibber.ToMorrow.Valide) // ? ToMorrow already Valide
{
tibber.GetEPEX(currDateTime.Year, currDateTime.Month, currDateTime.Day);
if (tibber.ToMorrow.Valide) // Tomorrow EPEX spot market data is invalide so try load spot tariffs/prices again ()
Util.InitDay(currDateTime, season, this.hp.OutdoorTemp, this.hp.IndoorTemp, this.hp.DHWTemp, stpSE.BatterySOC(), tibber, eMailUserData);
}
}
/* ---------------------------------------- Summer season -------------------------------------------------- */
if (season == Season.Summer && tibber.ToDay.Valide)
{
if (status == ProcessStatus.BatteryChargeFromGrid) // Allwayes -> First action
{
stpSE.BatteryChargeFromGrid(socMax: SOCMax, power: ChargePower);
status = stpSE.BatteryControl == BatteryControlEnum.Remote ?
ProcessStatus.BatteryChargeFromGrid : ProcessStatus.Break; // If stil Remote continue with BatteryChargeFromGrid
}
else if (status == ProcessStatus.BatteryDischargeToGrid)
{
stpSE.BatteryDischageToGrid(socMin: SOCSellMin, power: DischargePower); // SOC minimun is 50% (summer)
status = stpSE.BatteryControl == BatteryControlEnum.Remote ?
ProcessStatus.BatteryDischargeToGrid : ProcessStatus.Break;
}
else if (evCharger.Status == ChargeStatusEnum.Charging) // EV Charger in use then -> set controle to ......
{
if (status != ProcessStatus.EVCharging)
{
Console.WriteLine($"EV -> Charging {DateTime.Now:HH:mm}");
if (tibber.ToDay.Prices[currDateTime.Hour].Energy > BuyPrice) // Current Energy-price > BuyPrice
stpSE.BatteryInverterControlIed();
else
stpSE.BatteryRemoteControlled();
status = ProcessStatus.EVCharging; // ProcessStatus.EVCharging -> Handled in the "else" to Idle
}
}
else if (status == ProcessStatus.BatteryChargeDelayed)
{
if (currDateTime.Hour >= tibber.ToDay.BatteryChargeMinHour)
{
stpSE.BatteryInverterControlIed();
status = ProcessStatus.BatteryChargeFromPV; // ProcessStatus.BatteryChargedFromPV -> Handled in the "else" to Idle
Console.WriteLine($"Battery -> Charge from PV {DateTime.Now:HH:mm}");
}
}
else if ((tibber.ToDay.MaxPrice - tibber.ToMorrow.MinPrice) >= SalesMarge &&
SalesPrice < tibber.ToMorrow.MaxPrice) // Sell: ToDay and buy-back Tomorrow
{
status = ProcessStatus.BatteryDischargeToGrid; // Mandetory start Selling energy (default 1/2 hour 5000watt -> 2.5 kWh)
}
else if (BuyPrice > tibber.ToDay.Prices[currDateTime.Hour].Energy) // Buy: ALL-TIME: by 'negative' Tibber/EPEX tariffs/prices ==> Charge the Battery (-> Buy energy)
{
status = ProcessStatus.BatteryChargeFromGrid; // Mode Start Buying energy
}
else if (Util.TS(currDateTime).TotalHours > tibber.ToDay.SunRise &&
Util.TS(currDateTime).TotalHours < tibber.ToDay.SunSet) // Day light time check for battery delayed charging
{
if (currDateTime.Hour < tibber.ToDay.BatteryChargeMinHour && stpSE.BatteryDischarge() == 0)
{
stpSE.BatteryRemoteControlled();
status = ProcessStatus.BatteryChargeDelayed;
Console.WriteLine($"Battery -> Charge delayed to: {tibber.ToDay.BatteryChargeMinHour}:00");
}
}
} /* -------------------------------------------------- Winter season --------------------------------- */
else if (season == Season.Winter && tibber.ToDay.Valide)
{
if (status == ProcessStatus.BatteryChargeFromGrid) // Allwayes -> First action
{
stpSE.BatteryChargeFromGrid(socMax: SOCMax, power: ChargePower);
status = stpSE.BatteryControl == BatteryControlEnum.Remote ?
ProcessStatus.BatteryChargeFromGrid : ProcessStatus.Break; // If stil Remote continue with BatteryChargeFromGrid
}
else if (status == ProcessStatus.BatteryDischargeToGrid)
{
stpSE.BatteryDischageToGrid(socMin: SOCSellMin, power: DischargePower); // SOC minimun is 75% (summer)
status = stpSE.BatteryControl == BatteryControlEnum.Remote ?
ProcessStatus.BatteryDischargeToGrid : ProcessStatus.Break; // If stil Remote continue with BatteryChargeFromGrid
}
else if (evCharger.Power > 0) // EV Charger in use then -> .......
{
Console.WriteLine($"EV -> Charging: {DateTime.Now:HH:mm}");
if (tibber.ToDay.Prices[currDateTime.Hour].Energy > BuyPrice) // Current Energy-price > BuyPrice
stpSE.BatteryInverterControlIed();
else
stpSE.BatteryRemoteControlled();
status = ProcessStatus.EVCharging; // ProcessStatus.EVCharging -> Handled in the "else" to Idle
}
else if (BuyPrice > tibber.ToDay.Prices[currDateTime.Hour].Energy) // ALL-TIME: By 'negative' Tibber/EPEX tariffs/prices ==> Charge the Battery (-> Buy energy)
{
if (stpSE.BatterySOC() < SOCMax) { }
status = ProcessStatus.BatteryChargeFromGrid; // Start Buying energy
if (hp.IndoorTemp <= indoorTempMax) // Start HP
hp.IndoorTempSetpoint(indoorTempMax);
else
hp.IndoorTempSetpoint(indoorTempNormal);
}
else if ((tibber.ToDay.MaxPrice - tibber.ToMorrow.MinPrice) >= SalesMarge &&
SalesPrice < tibber.ToMorrow.MaxPrice &&
tibber.ToDay.MaxHour == currDateTime.Hour) // Sell: ToDay and buy-back Tomorrow
{
status = ProcessStatus.BatteryDischargeToGrid; // Mandetory start Selling energy (default 1/2 hour 5000watt -> 2.5 kWh)
}
else
{
if (status != ProcessStatus.Idle)
{
stpSE.BatteryInverterControlIed();
status = ProcessStatus.Idle; // Do nothing until day changed to next day
Console.WriteLine($"Idle -> Mode started: {DateTime.Now:dd-MM-yyyy HH:mm}");
}
}
}
else
{
if (status != ProcessStatus.Idle)
{
stpSE.BatteryInverterControlIed();
hp.IndoorTempSetpoint(indoorTempMin);
status = ProcessStatus.Idle; // Do nothing until day changed to next day
Console.WriteLine($"Idle -> Mode started: {DateTime.Now:dd-MM-yyyy HH:mm}");
}
}
}
catch (Exception)
{
}
}
// tmrMain?.Change(0, (int)TimeSpan.FromSeconds(eventPeriod).TotalMilliseconds); //
}