Skip to content

Instantly share code, notes, and snippets.

@ramonsmits
Last active January 16, 2023 11:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ramonsmits/a3a6388e01f6cdd57d2faee8a0a05ca0 to your computer and use it in GitHub Desktop.
Save ramonsmits/a3a6388e01f6cdd57d2faee8a0a05ca0 to your computer and use it in GitHub Desktop.
Tesla charging load balancer based on events from DSMR Reader and TeslaMate
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