private static bool IsEVChargingInProgress(ModbusTCPClient wallbox) { // read EV status char EVStatus = (char)Utils.ByteSwap(BitConverter.ToUInt16(wallbox.Read( WallbeWallboxModbusUnitID, ModbusTCPClient.FunctionCode.ReadInputRegisters, WallbeWallboxEVStatusAddress, 1))); switch (EVStatus) { case 'A': return(false); // no vehicle connected case 'B': return(false); // vehicle connected, not charging case 'C': return(true); // vehicle connected, charging, no ventilation required case 'D': return(true); // vehicle connected, charging, ventilation required case 'E': return(false); // wallbox has no power case 'F': return(false); // wallbox not available default: return(false); } }
private static void StartEVCharging(ModbusTCPClient wallbox) { if (IsEVConnected(wallbox)) { // start charging wallbox.WriteCoil(WallbeWallboxModbusUnitID, WallbeWallboxEnableChargingFlagAddress, true); } }
private static void OptimizeEVCharging(ModbusTCPClient wallbox, double currentPower) { // we ramp up and down our charging current in 1 Amp increments/decrements // we increase our charging current until a) we have reached the maximum the wallbox can handle or // b) we are just below consuming power from the grid (indicated by currentPower becoming positive), we are setting this to -200 Watts // we decrease our charging current when currentPower is above 0 (again indicated we are comsuming pwoer from the grid) // read maximum current rating ushort maxCurrent = Utils.ByteSwap(BitConverter.ToUInt16(wallbox.Read( WallbeWallboxModbusUnitID, ModbusTCPClient.FunctionCode.ReadInputRegisters, WallbeWallboxMaxCurrentSettingAddress, 1))); // read current current (in Amps) ushort wallbeWallboxCurrentCurrentSetting = Utils.ByteSwap(BitConverter.ToUInt16(wallbox.Read( WallbeWallboxModbusUnitID, ModbusTCPClient.FunctionCode.ReadHoldingRegisters, WallbeWallboxCurrentCurrentSettingAddress, 1))); // check if we have reached our limits (we define a 1KW "deadzone" from -500W to 500W where we keep things the way they are to cater for jitter) // "charge now" overwrites this and charges as quickly as possible if ((wallbeWallboxCurrentCurrentSetting < maxCurrent) && ((currentPower < -500) || _chargeNow)) { // increse desired current by 1 Amp wallbox.WriteHoldingRegisters( WallbeWallboxModbusUnitID, WallbeWallboxDesiredCurrentSettingAddress, new ushort[] { (ushort)(wallbeWallboxCurrentCurrentSetting + 1) }); } else if (currentPower > 500) { // need to decrease our charging current // "charge now" overwrites this and charges with maximum power if (!_chargeNow) { if (wallbeWallboxCurrentCurrentSetting == WallbeWallboxMinChargingCurrent) { // we are already at the minimum, stop instead StopEVCharging(wallbox); } else { // decrease desired current by 1 Amp wallbox.WriteHoldingRegisters( WallbeWallboxModbusUnitID, WallbeWallboxDesiredCurrentSettingAddress, new ushort[] { (ushort)(wallbeWallboxCurrentCurrentSetting - 1) }); } } } }
private static void StartEVCharging(ModbusTCPClient wallbox) { if (IsEVConnected(wallbox)) { // check if we already set our charging enabled flag bool chargingEnabled = BitConverter.ToBoolean(wallbox.Read( WallbeWallboxModbusUnitID, ModbusTCPClient.FunctionCode.ReadCoilStatus, WallbeWallboxEnableChargingFlagAddress, 1)); if (!chargingEnabled) { // start charging wallbox.WriteCoil(WallbeWallboxModbusUnitID, WallbeWallboxEnableChargingFlagAddress, true); } } }
static async Task Main(string[] args) { #if DEBUG // Attach remote debugger while (true) { Console.WriteLine("Waiting for remote debugger to attach..."); if (Debugger.IsAttached) { break; } System.Threading.Thread.Sleep(1000); } #endif // init log file Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File( "logfile.txt", fileSizeLimitBytes: 1024 * 1024, flushToDiskInterval: TimeSpan.FromSeconds(30), rollOnFileSizeLimit: true, retainedFileCountLimit: 2) .MinimumLevel.Debug() .CreateLogger(); Log.Information($"{Assembly.GetExecutingAssembly()} V{FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion}"); // init Modbus TCP client for wallbox ModbusTCPClient wallbox = new ModbusTCPClient(); wallbox.Connect(WallbeWallboxBaseAddress, WallbeWallboxModbusTCPPort); // init Modbus TCP client for inverter ModbusTCPClient inverter = new ModbusTCPClient(); inverter.Connect(FroniusInverterBaseAddress, FroniusInverterModbusTCPPort); // read current inverter power limit (percentage) byte[] WMaxLimit = inverter.Read( FroniusInverterModbusUnitID, ModbusTCPClient.FunctionCode.ReadHoldingRegisters, SunSpecInverterModbusRegisterMapFloat.InverterBaseAddress + SunSpecInverterModbusRegisterMapFloat.WMaxLimPctOffset, SunSpecInverterModbusRegisterMapFloat.WMaxLimPctLength); int existingLimitPercent = Utils.ByteSwap(BitConverter.ToUInt16(WMaxLimit)) / 100; // go to the maximum grid export power limit with immediate effect without timeout ushort InverterPowerOutputPercent = (ushort)((GridExportPowerLimit / FroniusSymoMaxPower) * 100); inverter.WriteHoldingRegisters( FroniusInverterModbusUnitID, SunSpecInverterModbusRegisterMapFloat.InverterBaseAddress + SunSpecInverterModbusRegisterMapFloat.WMaxLimPctOffset, new ushort[] { (ushort)(InverterPowerOutputPercent * 100), 0, 0, 0, 1 }); // check new setting WMaxLimit = inverter.Read( FroniusInverterModbusUnitID, ModbusTCPClient.FunctionCode.ReadHoldingRegisters, SunSpecInverterModbusRegisterMapFloat.InverterBaseAddress + SunSpecInverterModbusRegisterMapFloat.WMaxLimPctOffset, SunSpecInverterModbusRegisterMapFloat.WMaxLimPctLength); int newLimitPercent = Utils.ByteSwap(BitConverter.ToUInt16(WMaxLimit)) / 100; // print a list of all available serial ports for convenience string[] ports = SerialPort.GetPortNames(); foreach (string port in ports) { Log.Information("Serial port available: " + port); } // start processing smart meter messages SmartMessageLanguage sml = new SmartMessageLanguage(LinuxUSBSerialPort); sml.ProcessStream(); DeviceClient deviceClient = null; try { // register the device string scopeId = "0ne0010B637"; string deviceId = "RasPi2B"; string primaryKey = ""; string secondaryKey = ""; var security = new SecurityProviderSymmetricKey(deviceId, primaryKey, secondaryKey); var transport = new ProvisioningTransportHandlerMqtt(TransportFallbackType.TcpWithWebSocketFallback); var provisioningClient = ProvisioningDeviceClient.Create("global.azure-devices-provisioning.net", scopeId, security, transport); var result = await provisioningClient.RegisterAsync(); var connectionString = "HostName=" + result.AssignedHub + ";DeviceId=" + result.DeviceId + ";SharedAccessKey=" + primaryKey; deviceClient = DeviceClient.CreateFromConnectionString(connectionString, TransportType.Mqtt); // register our methods await deviceClient.SetMethodHandlerAsync("ChargeNowToggle", ChargeNowHandler, null); await deviceClient.SetMethodHandlerAsync("ChargingPhases", ChargingPhasesHandler, null); } catch (Exception ex) { Log.Error(ex, "Registering device failed!"); } TelemetryData telemetryData = new TelemetryData(); while (true) { telemetryData.ChargeNow = _chargeNow; telemetryData.NumChargingPhases = _chargingPhases; try { // read the current weather data from web service WebClient webClient = new WebClient { BaseAddress = "https://api.openweathermap.org/" }; string json = webClient.DownloadString("data/2.5/weather?q=Munich,de&units=metric&appid=2898258e654f7f321ef3589c4fa58a9b"); WeatherInfo weather = JsonConvert.DeserializeObject <WeatherInfo>(json); if (weather != null) { telemetryData.Temperature = weather.main.temp; telemetryData.WindSpeed = weather.wind.speed; telemetryData.CloudCover = weather.weather[0].description; } webClient.Dispose(); } catch (Exception ex) { Log.Error(ex, "Getting weather data failed!"); } try { // read the current forecast data from web service WebClient webClient = new WebClient { BaseAddress = "https://api.openweathermap.org/" }; string json = webClient.DownloadString("data/2.5/forecast?q=Munich,de&units=metric&appid=2898258e654f7f321ef3589c4fa58a9b"); Forecast forecast = JsonConvert.DeserializeObject <Forecast>(json); if (forecast != null && forecast.list != null && forecast.list.Count == 40) { telemetryData.CloudinessForecast = string.Empty; for (int i = 0; i < 40; i++) { telemetryData.CloudinessForecast += "Cloudiness on " + forecast.list[i].dt_txt + ": " + forecast.list[i].clouds.all + "%\r\n"; } } webClient.Dispose(); } catch (Exception ex) { Log.Error(ex, "Getting weather forecast failed!"); } try { // read the current converter data from web service WebClient webClient = new WebClient { BaseAddress = "http://" + FroniusInverterBaseAddress }; string json = webClient.DownloadString("solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceID=1&DataCollection=CommonInverterData"); DCACConverter converter = JsonConvert.DeserializeObject <DCACConverter>(json); if (converter != null) { if (converter.Body.Data.PAC != null) { telemetryData.PVOutputPower = converter.Body.Data.PAC.Value; } if (converter.Body.Data.DAY_ENERGY != null) { telemetryData.PVOutputEnergyDay = ((double)converter.Body.Data.DAY_ENERGY.Value) / 1000.0; } if (converter.Body.Data.YEAR_ENERGY != null) { telemetryData.PVOutputEnergyYear = ((double)converter.Body.Data.YEAR_ENERGY.Value) / 1000.0; } if (converter.Body.Data.TOTAL_ENERGY != null) { telemetryData.PVOutputEnergyTotal = ((double)converter.Body.Data.TOTAL_ENERGY.Value) / 1000.0; } } webClient.Dispose(); } catch (Exception ex) { Log.Error(ex, "Getting converter data failed!"); } try { // read the current smart meter data telemetryData.MeterEnergyPurchased = sml.Meter.EnergyPurchased; telemetryData.MeterEnergySold = sml.Meter.EnergySold; telemetryData.CurrentPower = sml.Meter.CurrentPower; telemetryData.EnergyCost = telemetryData.MeterEnergyPurchased * KWhCost; telemetryData.EnergyProfit = telemetryData.MeterEnergySold * KWhProfit; // calculate energy consumed from the other telemetry, if available telemetryData.MeterEnergyConsumed = 0.0; if ((telemetryData.MeterEnergyPurchased != 0.0) && (telemetryData.MeterEnergySold != 0.0) && (telemetryData.PVOutputEnergyTotal != 0.0)) { telemetryData.MeterEnergyConsumed = telemetryData.PVOutputEnergyTotal + sml.Meter.EnergyPurchased - sml.Meter.EnergySold; telemetryData.CurrentPowerConsumed = telemetryData.PVOutputPower + sml.Meter.CurrentPower; } } catch (Exception ex) { Log.Error(ex, "Getting smart meter data failed!"); } try { // ramp up or down EV charging, based on surplus bool chargingInProgress = IsEVChargingInProgress(wallbox); telemetryData.EVChargingInProgress = chargingInProgress? 1 : 0; if (chargingInProgress) { // read current current (in Amps) ushort wallbeWallboxCurrentCurrentSetting = Utils.ByteSwap(BitConverter.ToUInt16(wallbox.Read( WallbeWallboxModbusUnitID, ModbusTCPClient.FunctionCode.ReadHoldingRegisters, WallbeWallboxCurrentCurrentSettingAddress, 1))); telemetryData.WallboxCurrent = wallbeWallboxCurrentCurrentSetting; OptimizeEVCharging(wallbox, sml.Meter.CurrentPower); } else { telemetryData.WallboxCurrent = 0; // check if we should start charging our EV with the surplus power, but we need at least 6A of current per charing phase // or the user set the "charge now" flag via direct method if (((sml.Meter.CurrentPower / 230) < (_chargingPhases * -6.0f)) || _chargeNow) { StartEVCharging(wallbox); } } } catch (Exception ex) { Log.Error(ex, "EV charing control failed!"); } try { string messageString = JsonConvert.SerializeObject(telemetryData); Message cloudMessage = new Message(Encoding.UTF8.GetBytes(messageString)); await deviceClient.SendEventAsync(cloudMessage); Debug.WriteLine("{0}: {1}", DateTime.Now, messageString); } catch (Exception ex) { Log.Error(ex, "Sending telemetry failed!"); } // wait 5 seconds and go again await Task.Delay(5000).ConfigureAwait(false); } }
private static void StopEVCharging(ModbusTCPClient wallbox) { wallbox.WriteCoil(WallbeWallboxModbusUnitID, WallbeWallboxEnableChargingFlagAddress, false); }