Last active
January 16, 2023 11:54
-
-
Save ramonsmits/a3a6388e01f6cdd57d2faee8a0a05ca0 to your computer and use it in GitHub Desktop.
Tesla charging load balancer based on events from DSMR Reader and TeslaMate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.Linq; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Microsoft.Extensions.Logging; | |
using MQTTnet; | |
class DsmrTeslaLoadBalancer | |
{ | |
const string HomeZoneName = "Huize Smits"; | |
static readonly TimeSpan HistoryDuration = TimeSpan.FromMinutes(1); | |
readonly HashSet<(DateTime, int)> History = new(); | |
readonly ILogger Logger; | |
readonly TeslaOperations Ops; | |
const int NetMaximum = 25; | |
const int TeslaChargeCurrentMin = 5; | |
int L1, L2, L3, LMax; | |
bool IsConnected; | |
bool IsCharging; | |
bool IsHome = true; | |
int TeslaChargeCurrent; | |
int TeslaChargeCurrentMax = 24; | |
int Max(int a, int b, int c) => Math.Max(a, Math.Max(b, c)); | |
public DsmrTeslaLoadBalancer(ILogger<DsmrTeslaLoadBalancer> logger, TeslaOperations ops) | |
{ | |
Logger = logger; | |
Ops = ops; | |
} | |
public async Task Update() | |
{ | |
var phaseAmpsMax = Max(L1, L2, L3); | |
History.Add((DateTime.UtcNow, phaseAmpsMax)); | |
foreach (var x in History) | |
{ | |
if (x.Item1 < DateTime.UtcNow - HistoryDuration) | |
{ | |
History.Remove(x); | |
} | |
} | |
if (!IsConnected || !IsCharging || !IsHome) | |
{ | |
Logger.LogTrace("Not connected to home charger or not charging ({isConnected}, {isCharging}, {isHome})", IsConnected, IsCharging, IsHome); | |
return; | |
} | |
var diff = NetMaximum - phaseAmpsMax; | |
if (LMax == phaseAmpsMax && diff == 0) | |
{ | |
Logger.LogTrace("No change in LMax and diff"); | |
return; | |
} | |
LMax = phaseAmpsMax; | |
Logger.LogDebug("Phase power current max phases: ({netMax}A) {phaseAmpsMax}A delta {diff}A", NetMaximum, phaseAmpsMax, diff); | |
var currentValue = TeslaChargeCurrent; | |
if (diff > 0) // Omhoog | |
{ | |
if (currentValue < TeslaChargeCurrentMax) | |
{ | |
// Mag enkel omhoog als de afgelopen minuut geen spike is geweest | |
var max = History.Max(x => x.Item2); | |
if (max > NetMaximum) | |
{ | |
Logger.LogDebug("Not increasing as history has values that exceed NetMaximum"); | |
return; | |
} | |
var newValue = currentValue + 1; | |
Logger.LogDebug("Increasing amps to {NewValue}", newValue); | |
await SetChargingAmps(newValue); // Could use diff, but favor small 1 amp increments every DSMR interval of 5 seconds | |
} | |
else | |
{ | |
Logger.LogTrace("Already at max"); | |
} | |
} | |
else if (diff < 0) // Omlaag | |
{ | |
var newValue = currentValue + diff; | |
if (newValue < TeslaChargeCurrentMin) // Failsafe! | |
{ | |
await ChargeStop(); | |
} | |
else | |
{ | |
Logger.LogDebug("DecreasingAmps to {NewValue}", newValue); | |
await SetChargingAmps(newValue); | |
} | |
} | |
} | |
async Task SetChargingAmps(int current) | |
{ | |
if (current > TeslaChargeCurrentMax || current < TeslaChargeCurrentMin) throw new ArgumentOutOfRangeException(nameof(current), current, $"Value must be between {TeslaChargeCurrentMin} and {TeslaChargeCurrentMax}."); | |
TeslaChargeCurrent = current; // Temp set until receiving next update | |
await Ops.SetChargingAmps(current); | |
} | |
async Task ChargeStop() | |
{ | |
Logger.LogInformation("Stop charging"); | |
await Ops.ChargeStop(); | |
} | |
[Subscribe("dsmr/reading/phase_power_current_l1")] | |
public async Task HandleDsmrL1(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
var value = int.Parse(msg.ConvertPayloadToString(), CultureInfo.InvariantCulture); | |
if (L1 == value) return; | |
L1 = value; | |
Logger.LogDebug("Phase {phase}: {amps}A", nameof(L1), L1); | |
await Update(); | |
} | |
[Subscribe("dsmr/reading/phase_power_current_l2")] | |
public async Task HandleDsmrL2(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
var value = int.Parse(msg.ConvertPayloadToString(), CultureInfo.InvariantCulture); | |
if (L2 == value) return; | |
L2 = value; | |
Logger.LogDebug("Phase {phase}: {amps}A", nameof(L2), L2); | |
await Update(); | |
} | |
[Subscribe("dsmr/reading/phase_power_current_l3")] | |
public async Task HandleDsmrL3(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
var value = int.Parse(msg.ConvertPayloadToString(), CultureInfo.InvariantCulture); | |
if (L3 == value) return; | |
L3 = value; | |
Logger.LogDebug("Phase {phase}: {amps}A", nameof(L3), L3); | |
await Update(); | |
} | |
[Subscribe("teslamate/cars/+/plugged_in")] | |
public Task HandlePluggedIn(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
IsConnected = msg.ConvertPayloadToString() == "true"; | |
Logger.LogInformation("IsConnected = {IsConnected}", IsConnected); | |
return Update(); | |
} | |
[Subscribe("teslamate/cars/+/geofence")] | |
public async Task HandleCarGeofence(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
var zoneName = msg.ConvertPayloadToString(); | |
IsHome = !string.IsNullOrEmpty(zoneName) && zoneName.Contains("🏠") || zoneName == HomeZoneName; | |
Logger.LogInformation("IsHome = {IsHome} ({zoneName})", IsHome, zoneName); | |
await Update(); | |
} | |
[Subscribe("teslamate/cars/+/state")] | |
public async Task HandleState(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
IsCharging = msg.ConvertPayloadToString() == "charging"; | |
Logger.LogInformation("IsCharging = {IsCharging}", IsCharging); | |
await Update(); | |
} | |
[Subscribe("teslamate/cars/+/charge_current_request")] | |
public Task HandleChargerActualCurrent(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
TeslaChargeCurrent = int.Parse(msg.ConvertPayloadToString(), CultureInfo.InvariantCulture); | |
if (!IsCharging) | |
{ | |
Logger.LogInformation("Not charging, set TeslaChargeCurrentMax = {charger_actual_current}A", TeslaChargeCurrent); | |
TeslaChargeCurrentMax = TeslaChargeCurrent; | |
} | |
Logger.LogInformation("charger_actual_current = {charger_actual_current}A", TeslaChargeCurrent); | |
return Task.CompletedTask; | |
} | |
[Subscribe("teslamate/cars/+/time_to_full_charge")] | |
public Task HandleTimeToFullCharge(MqttApplicationMessage msg, CancellationToken cancellationToken) | |
{ | |
var value = msg.ConvertPayloadToString(); | |
var hours = double.Parse(value, CultureInfo.InvariantCulture); | |
var at = DateTimeOffset.Now.AddHours(hours); | |
at = at.Round(TimeSpan.FromSeconds(15)); | |
Logger.LogInformation("time_to_full_charge = {at} ({value})", at, value); | |
return Task.CompletedTask; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment