SMA Modbus en andere zooi

Voor discussies en hulp met je smarthome en automatisering kan je hier terecht. DE plaats voor alles over home assistant, Philips hue, Zigbee, Sonoff, ...
Gebruikersavatar
conda
Starter Plus
Starter Plus
Berichten: 43
Lid geworden op: 12 dec 2020, 12:56
Bedankt: 1 keer

Bericht

Jeroen_sma schreef: 3 maanden geleden @Gijs Wassink. Hoe heb jij je kunnen aanmelden als Field tester?
Jeroen
@Jeroen_sma De periode van het fieldtesten is ondertussen voorbij. Ik heb me daar toen ook voor kunnen inschrijven. De firmware zou ondertussen uitgerold moeten zijn (versie X.15.5.R). De batterij kan je zelf terug aansturen, jammer genoeg zijn ze register 41255 vergeten te fixen. Deze wordt nog steeds overschreven.
Ik heb dit hen laten weten en hopelijk doen ze er nog iets mee. De batterij is wel terug aanstuurbaar.
liegebeestig
Elite Poster
Elite Poster
Berichten: 2341
Lid geworden op: 01 jun 2006, 13:16
Uitgedeelde bedankjes: 38 keer
Bedankt: 56 keer

Bericht

Is er een manier om de nieuwe Firmware push te triggeren? Bij mij komt ie maar niet.
Gijs Wassink
Starter
Starter
Berichten: 9
Lid geworden op: 17 mei 2024, 20:37
Bedankt: 2 keer

Bericht

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) :-D

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);														//  
}
Laatst gewijzigd door Gijs Wassink 2 maanden geleden, in totaal 3 gewijzigd.
tomdw
Starter
Starter
Berichten: 2
Lid geworden op: 3 maanden geleden

Bericht

Ik sta op het punt om een pv panelen en batterij te plaatsen en was getriggerd door het Flexio verhaal. Ik ben zelf wel wat bedreven in c# en werd zo getriggerd door deze thread. @Gijs Wassink is deze code open source beschikbaar in een git ofzo ? Ik heb wel interesse om mee te werken ... Alvast bedankt.
Gijs Wassink
Starter
Starter
Berichten: 9
Lid geworden op: 17 mei 2024, 20:37
Bedankt: 2 keer

Bericht

Ik heb inmiddels de post viewtopic.php?p=1003464#p1003464 van drie weken gelegen geupdate naar mijn laatste versie:

@tomdw ik ben nog volop met het project bezig en ga het t.z.t. op GitHub publiceren.

Het project is erg gefocust op mijn situatie en als het op Github gepubliseerd is voor alle die geïnteresseerd zijn te downloaden.
tomdw
Starter
Starter
Berichten: 2
Lid geworden op: 3 maanden geleden

Bericht

Bedankt voor je reactie @Gijs Wassink . Laat me zeker weten wanneer er iets beschikbaar komt op GitHub.
Bedankt alvast voor je 'process' class. Het zijn dan vooral de andere namespaces o.a. GWCharger, GWModbus die me wel tijd zouden besparen, kwestie van het warm water niet opnieuw te moeten uitvinden ...