diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index 8fad4748a..f13e1e1b1 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -48,6 +48,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasIndex(c => c.Key) .IsUnique(); + + modelBuilder.Entity() + .HasIndex(c => c.TeslaMateCarId) + .IsUnique(); } #pragma warning disable CS8618 diff --git a/TeslaSolarCharger.Model/Migrations/20240127112220_MakeCarTeslaMateCarIdUnique.Designer.cs b/TeslaSolarCharger.Model/Migrations/20240127112220_MakeCarTeslaMateCarIdUnique.Designer.cs new file mode 100644 index 000000000..0b86a77ce --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240127112220_MakeCarTeslaMateCarIdUnique.Designer.cs @@ -0,0 +1,249 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TeslaSolarCharger.Model.EntityFramework; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + [DbContext(typeof(TeslaSolarChargerContext))] + [Migration("20240127112220_MakeCarTeslaMateCarIdUnique")] + partial class MakeCarTeslaMateCarIdUnique + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("CarStateJson") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CachedCarStates"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TeslaFleetApiState") + .HasColumnType("INTEGER"); + + b.Property("TeslaMateCarId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TeslaMateCarId") + .IsUnique(); + + b.ToTable("Cars"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargePrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddSpotPriceToGridPrice") + .HasColumnType("INTEGER"); + + b.Property("EnergyProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(6); + + b.Property("EnergyProviderConfiguration") + .HasColumnType("TEXT"); + + b.Property("GridPrice") + .HasColumnType("TEXT"); + + b.Property("SolarPrice") + .HasColumnType("TEXT"); + + b.Property("SpotPriceCorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("ValidSince") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChargePrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageSpotPrice") + .HasColumnType("TEXT"); + + b.Property("CalculatedPrice") + .HasColumnType("TEXT"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("UsedGridEnergy") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HandledCharges"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargingPower") + .HasColumnType("INTEGER"); + + b.Property("GridProportion") + .HasColumnType("REAL"); + + b.Property("HandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("PowerFromGrid") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.Property("UsedWattHours") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("HandledChargeId"); + + b.ToTable("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.SpotPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SpotPrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TeslaToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("IdToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Region") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TeslaTokens"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TscConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("TscConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") + .WithMany("PowerDistributions") + .HasForeignKey("HandledChargeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HandledCharge"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Navigation("PowerDistributions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20240127112220_MakeCarTeslaMateCarIdUnique.cs b/TeslaSolarCharger.Model/Migrations/20240127112220_MakeCarTeslaMateCarIdUnique.cs new file mode 100644 index 000000000..a02f03e04 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20240127112220_MakeCarTeslaMateCarIdUnique.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class MakeCarTeslaMateCarIdUnique : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Cars_TeslaMateCarId", + table: "Cars", + column: "TeslaMateCarId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Cars_TeslaMateCarId", + table: "Cars"); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index 0aff8881b..f6684975f 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -55,6 +55,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("TeslaMateCarId") + .IsUnique(); + b.ToTable("Cars"); }); diff --git a/TeslaSolarCharger/Server/Contracts/ITeslaMateMqttService.cs b/TeslaSolarCharger/Server/Contracts/ITeslaMateMqttService.cs index db16dc86e..4c104b50c 100644 --- a/TeslaSolarCharger/Server/Contracts/ITeslaMateMqttService.cs +++ b/TeslaSolarCharger/Server/Contracts/ITeslaMateMqttService.cs @@ -2,7 +2,6 @@ public interface ITeslaMateMqttService { - Task ConnectMqttClient(); bool IsMqttClientConnected { get; } Task ConnectClientIfNotConnected(); Task DisconnectClient(string reason); diff --git a/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleDataResult.cs b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleDataResult.cs new file mode 100644 index 000000000..ad18522c6 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/TeslaFleetApi/DtoVehicleDataResult.cs @@ -0,0 +1,846 @@ +using Newtonsoft.Json; + +namespace TeslaSolarCharger.Server.Dtos.TeslaFleetApi; + +public class DtoVehicleDataResult +{ + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("user_id")] + public long UserId { get; set; } + + [JsonProperty("vehicle_id")] + public long VehicleId { get; set; } + + [JsonProperty("vin")] + public string Vin { get; set; } + + [JsonProperty("color")] + public object Color { get; set; } + + [JsonProperty("access_type")] + public string AccessType { get; set; } + + [JsonProperty("granular_access")] + public GranularAccess GranularAccess { get; set; } + + [JsonProperty("tokens")] + public List Tokens { get; set; } + + [JsonProperty("state")] + public string State { get; set; } + + [JsonProperty("in_service")] + public bool InService { get; set; } + + [JsonProperty("id_s")] + public string IdS { get; set; } + + [JsonProperty("calendar_enabled")] + public bool CalendarEnabled { get; set; } + + [JsonProperty("api_version")] + public int ApiVersion { get; set; } + + [JsonProperty("backseat_token")] + public object BackseatToken { get; set; } + + [JsonProperty("backseat_token_updated_at")] + public object BackseatTokenUpdatedAt { get; set; } + + [JsonProperty("charge_state")] + public ChargeState ChargeState { get; set; } + + [JsonProperty("climate_state")] + public ClimateState ClimateState { get; set; } + + [JsonProperty("drive_state")] + public DriveState DriveState { get; set; } + + [JsonProperty("gui_settings")] + public GuiSettings GuiSettings { get; set; } + + [JsonProperty("vehicle_config")] + public VehicleConfig VehicleConfig { get; set; } + + [JsonProperty("vehicle_state")] + public VehicleState VehicleState { get; set; } +} + +public class ChargeState +{ + [JsonProperty("battery_heater_on")] + public bool BatteryHeaterOn { get; set; } + + [JsonProperty("battery_level")] + public int BatteryLevel { get; set; } + + [JsonProperty("battery_range")] + public double BatteryRange { get; set; } + + [JsonProperty("charge_amps")] + public int ChargeAmps { get; set; } + + [JsonProperty("charge_current_request")] + public int ChargeCurrentRequest { get; set; } + + [JsonProperty("charge_current_request_max")] + public int ChargeCurrentRequestMax { get; set; } + + [JsonProperty("charge_enable_request")] + public bool ChargeEnableRequest { get; set; } + + [JsonProperty("charge_energy_added")] + public double ChargeEnergyAdded { get; set; } + + [JsonProperty("charge_limit_soc")] + public int ChargeLimitSoc { get; set; } + + [JsonProperty("charge_limit_soc_max")] + public int ChargeLimitSocMax { get; set; } + + [JsonProperty("charge_limit_soc_min")] + public int ChargeLimitSocMin { get; set; } + + [JsonProperty("charge_limit_soc_std")] + public int ChargeLimitSocStd { get; set; } + + [JsonProperty("charge_miles_added_ideal")] + public float ChargeMilesAddedIdeal { get; set; } + + [JsonProperty("charge_miles_added_rated")] + public float ChargeMilesAddedRated { get; set; } + + [JsonProperty("charge_port_cold_weather_mode")] + public bool ChargePortColdWeatherMode { get; set; } + + [JsonProperty("charge_port_color")] + public string ChargePortColor { get; set; } + + [JsonProperty("charge_port_door_open")] + public bool ChargePortDoorOpen { get; set; } + + [JsonProperty("charge_port_latch")] + public string ChargePortLatch { get; set; } + + [JsonProperty("charge_rate")] + public float ChargeRate { get; set; } + + [JsonProperty("charger_actual_current")] + public int ChargerActualCurrent { get; set; } + + [JsonProperty("charger_phases")] + public int ChargerPhases { get; set; } + + [JsonProperty("charger_pilot_current")] + public int ChargerPilotCurrent { get; set; } + + [JsonProperty("charger_power")] + public int ChargerPower { get; set; } + + [JsonProperty("charger_voltage")] + public int ChargerVoltage { get; set; } + + [JsonProperty("charging_state")] + public string ChargingState { get; set; } + + [JsonProperty("conn_charge_cable")] + public string ConnChargeCable { get; set; } + + [JsonProperty("est_battery_range")] + public double EstBatteryRange { get; set; } + + [JsonProperty("fast_charger_brand")] + public string FastChargerBrand { get; set; } + + [JsonProperty("fast_charger_present")] + public bool FastChargerPresent { get; set; } + + [JsonProperty("fast_charger_type")] + public string FastChargerType { get; set; } + + [JsonProperty("ideal_battery_range")] + public double IdealBatteryRange { get; set; } + + [JsonProperty("managed_charging_active")] + public bool ManagedChargingActive { get; set; } + + [JsonProperty("managed_charging_start_time")] + public object ManagedChargingStartTime { get; set; } + + [JsonProperty("managed_charging_user_canceled")] + public bool ManagedChargingUserCanceled { get; set; } + + [JsonProperty("max_range_charge_counter")] + public int MaxRangeChargeCounter { get; set; } + + [JsonProperty("minutes_to_full_charge")] + public int MinutesToFullCharge { get; set; } + + [JsonProperty("not_enough_power_to_heat")] + public object NotEnoughPowerToHeat { get; set; } + + [JsonProperty("off_peak_charging_enabled")] + public bool OffPeakChargingEnabled { get; set; } + + [JsonProperty("off_peak_charging_times")] + public string OffPeakChargingTimes { get; set; } + + [JsonProperty("off_peak_hours_end_time")] + public int OffPeakHoursEndTime { get; set; } + + [JsonProperty("preconditioning_enabled")] + public bool PreconditioningEnabled { get; set; } + + [JsonProperty("preconditioning_times")] + public string PreconditioningTimes { get; set; } + + [JsonProperty("scheduled_charging_mode")] + public string ScheduledChargingMode { get; set; } + + [JsonProperty("scheduled_charging_pending")] + public bool ScheduledChargingPending { get; set; } + + [JsonProperty("scheduled_charging_start_time")] + public long? ScheduledChargingStartTime { get; set; } + + [JsonProperty("scheduled_departure_time")] + public int? ScheduledDepartureTime { get; set; } + + [JsonProperty("scheduled_departure_time_minutes")] + public int? ScheduledDepartureTimeMinutes { get; set; } + + [JsonProperty("supercharger_session_trip_planner")] + public bool SuperchargerSessionTripPlanner { get; set; } + + [JsonProperty("time_to_full_charge")] + public float TimeToFullCharge { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + + [JsonProperty("trip_charging")] + public bool TripCharging { get; set; } + + [JsonProperty("usable_battery_level")] + public int UsableBatteryLevel { get; set; } + + [JsonProperty("user_charge_enable_request")] + public object UserChargeEnableRequest { get; set; } +} + +public class ClimateState +{ + [JsonProperty("allow_cabin_overheat_protection")] + public bool AllowCabinOverheatProtection { get; set; } + + [JsonProperty("auto_seat_climate_left")] + public bool AutoSeatClimateLeft { get; set; } + + [JsonProperty("auto_seat_climate_right")] + public bool AutoSeatClimateRight { get; set; } + + [JsonProperty("auto_steering_wheel_heat")] + public bool AutoSteeringWheelHeat { get; set; } + + [JsonProperty("battery_heater")] + public bool BatteryHeater { get; set; } + + [JsonProperty("battery_heater_no_power")] + public object BatteryHeaterNoPower { get; set; } + + [JsonProperty("bioweapon_mode")] + public bool BioweaponMode { get; set; } + + [JsonProperty("cabin_overheat_protection")] + public string CabinOverheatProtection { get; set; } + + [JsonProperty("cabin_overheat_protection_actively_cooling")] + public bool CabinOverheatProtectionActivelyCooling { get; set; } + + [JsonProperty("climate_keeper_mode")] + public string ClimateKeeperMode { get; set; } + + [JsonProperty("cop_activation_temperature")] + public string CopActivationTemperature { get; set; } + + [JsonProperty("defrost_mode")] + public int DefrostMode { get; set; } + + [JsonProperty("driver_temp_setting")] + public float DriverTempSetting { get; set; } + + [JsonProperty("fan_status")] + public int FanStatus { get; set; } + + [JsonProperty("hvac_auto_request")] + public string HvacAutoRequest { get; set; } + + [JsonProperty("inside_temp")] + public double InsideTemp { get; set; } + + [JsonProperty("is_auto_conditioning_on")] + public bool IsAutoConditioningOn { get; set; } + + [JsonProperty("is_climate_on")] + public bool IsClimateOn { get; set; } + + [JsonProperty("is_front_defroster_on")] + public bool IsFrontDefrosterOn { get; set; } + + [JsonProperty("is_preconditioning")] + public bool IsPreconditioning { get; set; } + + [JsonProperty("is_rear_defroster_on")] + public bool IsRearDefrosterOn { get; set; } + + [JsonProperty("left_temp_direction")] + public float LeftTempDirection { get; set; } + + [JsonProperty("max_avail_temp")] + public float MaxAvailTemp { get; set; } + + [JsonProperty("min_avail_temp")] + public float MinAvailTemp { get; set; } + + [JsonProperty("outside_temp")] + public float OutsideTemp { get; set; } + + [JsonProperty("passenger_temp_setting")] + public float PassengerTempSetting { get; set; } + + [JsonProperty("remote_heater_control_enabled")] + public bool RemoteHeaterControlEnabled { get; set; } + + [JsonProperty("right_temp_direction")] + public float RightTempDirection { get; set; } + + [JsonProperty("seat_heater_left")] + public int SeatHeaterLeft { get; set; } + + [JsonProperty("seat_heater_rear_center")] + public int SeatHeaterRearCenter { get; set; } + + [JsonProperty("seat_heater_rear_left")] + public int SeatHeaterRearLeft { get; set; } + + [JsonProperty("seat_heater_rear_right")] + public int SeatHeaterRearRight { get; set; } + + [JsonProperty("seat_heater_right")] + public int SeatHeaterRight { get; set; } + + [JsonProperty("side_mirror_heaters")] + public bool SideMirrorHeaters { get; set; } + + [JsonProperty("steering_wheel_heat_level")] + public int SteeringWheelHeatLevel { get; set; } + + [JsonProperty("steering_wheel_heater")] + public bool SteeringWheelHeater { get; set; } + + [JsonProperty("supports_fan_only_cabin_overheat_protection")] + public bool SupportsFanOnlyCabinOverheatProtection { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + + [JsonProperty("wiper_blade_heater")] + public bool WiperBladeHeater { get; set; } +} + +public class DriveState +{ + [JsonProperty("active_route_latitude")] + public double ActiveRouteLatitude { get; set; } + + [JsonProperty("active_route_longitude")] + public double ActiveRouteLongitude { get; set; } + + [JsonProperty("active_route_traffic_minutes_delay")] + public float ActiveRouteTrafficMinutesDelay { get; set; } + + [JsonProperty("gps_as_of")] + public int GpsAsOf { get; set; } + + [JsonProperty("heading")] + public int Heading { get; set; } + + [JsonProperty("latitude")] + public double Latitude { get; set; } + + [JsonProperty("longitude")] + public double Longitude { get; set; } + + [JsonProperty("native_latitude")] + public double NativeLatitude { get; set; } + + [JsonProperty("native_location_supported")] + public int NativeLocationSupported { get; set; } + + [JsonProperty("native_longitude")] + public double NativeLongitude { get; set; } + + [JsonProperty("native_type")] + public string NativeType { get; set; } + + [JsonProperty("power")] + public int Power { get; set; } + + [JsonProperty("shift_state")] + public string? ShiftState { get; set; } + + [JsonProperty("speed")] + public object Speed { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } +} + +public class GuiSettings +{ + [JsonProperty("gui_24_hour_time")] + public bool Gui24HourTime { get; set; } + + [JsonProperty("gui_charge_rate_units")] + public string GuiChargeRateUnits { get; set; } + + [JsonProperty("gui_distance_units")] + public string GuiDistanceUnits { get; set; } + + [JsonProperty("gui_range_display")] + public string GuiRangeDisplay { get; set; } + + [JsonProperty("gui_temperature_units")] + public string GuiTemperatureUnits { get; set; } + + [JsonProperty("gui_tirepressure_units")] + public string GuiTirepressureUnits { get; set; } + + [JsonProperty("show_range_units")] + public bool ShowRangeUnits { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } +} + +public class MediaInfo +{ + [JsonProperty("a2dp_source_name")] + public string A2dpSourceName { get; set; } + + [JsonProperty("audio_volume")] + public double AudioVolume { get; set; } + + [JsonProperty("audio_volume_increment")] + public double AudioVolumeIncrement { get; set; } + + [JsonProperty("audio_volume_max")] + public double AudioVolumeMax { get; set; } + + [JsonProperty("media_playback_status")] + public string MediaPlaybackStatus { get; set; } + + [JsonProperty("now_playing_album")] + public string NowPlayingAlbum { get; set; } + + [JsonProperty("now_playing_artist")] + public string NowPlayingArtist { get; set; } + + [JsonProperty("now_playing_duration")] + public int NowPlayingDuration { get; set; } + + [JsonProperty("now_playing_elapsed")] + public int NowPlayingElapsed { get; set; } + + [JsonProperty("now_playing_source")] + public string NowPlayingSource { get; set; } + + [JsonProperty("now_playing_station")] + public string NowPlayingStation { get; set; } + + [JsonProperty("now_playing_title")] + public string NowPlayingTitle { get; set; } +} + +public class MediaState +{ + [JsonProperty("remote_control_enabled")] + public bool RemoteControlEnabled { get; set; } +} + +public class SoftwareUpdate +{ + [JsonProperty("download_perc")] + public int DownloadPerc { get; set; } + + [JsonProperty("expected_duration_sec")] + public int ExpectedDurationSec { get; set; } + + [JsonProperty("install_perc")] + public int InstallPerc { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } +} + +public class SpeedLimitMode +{ + [JsonProperty("active")] + public bool Active { get; set; } + + [JsonProperty("current_limit_mph")] + public float CurrentLimitMph { get; set; } + + [JsonProperty("max_limit_mph")] + public float MaxLimitMph { get; set; } + + [JsonProperty("min_limit_mph")] + public float MinLimitMph { get; set; } + + [JsonProperty("pin_code_set")] + public bool PinCodeSet { get; set; } +} + +public class VehicleConfig +{ + [JsonProperty("aux_park_lamps")] + public string AuxParkLamps { get; set; } + + [JsonProperty("badge_version")] + public int BadgeVersion { get; set; } + + [JsonProperty("can_accept_navigation_requests")] + public bool CanAcceptNavigationRequests { get; set; } + + [JsonProperty("can_actuate_trunks")] + public bool CanActuateTrunks { get; set; } + + [JsonProperty("car_special_type")] + public string CarSpecialType { get; set; } + + [JsonProperty("car_type")] + public string CarType { get; set; } + + [JsonProperty("charge_port_type")] + public string ChargePortType { get; set; } + + [JsonProperty("cop_user_set_temp_supported")] + public bool CopUserSetTempSupported { get; set; } + + [JsonProperty("dashcam_clip_save_supported")] + public bool DashcamClipSaveSupported { get; set; } + + [JsonProperty("default_charge_to_max")] + public bool DefaultChargeToMax { get; set; } + + [JsonProperty("driver_assist")] + public string DriverAssist { get; set; } + + [JsonProperty("ece_restrictions")] + public bool EceRestrictions { get; set; } + + [JsonProperty("efficiency_package")] + public string EfficiencyPackage { get; set; } + + [JsonProperty("eu_vehicle")] + public bool EuVehicle { get; set; } + + [JsonProperty("exterior_color")] + public string ExteriorColor { get; set; } + + [JsonProperty("exterior_trim")] + public string ExteriorTrim { get; set; } + + [JsonProperty("exterior_trim_override")] + public string ExteriorTrimOverride { get; set; } + + [JsonProperty("has_air_suspension")] + public bool HasAirSuspension { get; set; } + + [JsonProperty("has_ludicrous_mode")] + public bool HasLudicrousMode { get; set; } + + [JsonProperty("has_seat_cooling")] + public bool HasSeatCooling { get; set; } + + [JsonProperty("headlamp_type")] + public string HeadlampType { get; set; } + + [JsonProperty("interior_trim_type")] + public string InteriorTrimType { get; set; } + + [JsonProperty("key_version")] + public int KeyVersion { get; set; } + + [JsonProperty("motorized_charge_port")] + public bool MotorizedChargePort { get; set; } + + [JsonProperty("paint_color_override")] + public string PaintColorOverride { get; set; } + + [JsonProperty("performance_package")] + public string PerformancePackage { get; set; } + + [JsonProperty("plg")] + public bool Plg { get; set; } + + [JsonProperty("pws")] + public bool Pws { get; set; } + + [JsonProperty("rear_drive_unit")] + public string RearDriveUnit { get; set; } + + [JsonProperty("rear_seat_heaters")] + public int RearSeatHeaters { get; set; } + + [JsonProperty("rear_seat_type")] + public int RearSeatType { get; set; } + + [JsonProperty("rhd")] + public bool Rhd { get; set; } + + [JsonProperty("roof_color")] + public string RoofColor { get; set; } + + [JsonProperty("seat_type")] + public object SeatType { get; set; } + + [JsonProperty("spoiler_type")] + public string SpoilerType { get; set; } + + [JsonProperty("sun_roof_installed")] + public object SunRoofInstalled { get; set; } + + [JsonProperty("supports_qr_pairing")] + public bool SupportsQrPairing { get; set; } + + [JsonProperty("third_row_seats")] + public string ThirdRowSeats { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + + [JsonProperty("trim_badging")] + public string TrimBadging { get; set; } + + [JsonProperty("use_range_badging")] + public bool UseRangeBadging { get; set; } + + [JsonProperty("utc_offset")] + public int UtcOffset { get; set; } + + [JsonProperty("webcam_selfie_supported")] + public bool WebcamSelfieSupported { get; set; } + + [JsonProperty("webcam_supported")] + public bool WebcamSupported { get; set; } + + [JsonProperty("wheel_type")] + public string WheelType { get; set; } +} + +public class VehicleState +{ + [JsonProperty("api_version")] + public int ApiVersion { get; set; } + + [JsonProperty("autopark_state_v3")] + public string AutoparkStateV3 { get; set; } + + [JsonProperty("autopark_style")] + public string AutoparkStyle { get; set; } + + [JsonProperty("calendar_supported")] + public bool CalendarSupported { get; set; } + + [JsonProperty("car_version")] + public string CarVersion { get; set; } + + [JsonProperty("center_display_state")] + public int CenterDisplayState { get; set; } + + [JsonProperty("dashcam_clip_save_available")] + public bool DashcamClipSaveAvailable { get; set; } + + [JsonProperty("dashcam_state")] + public string DashcamState { get; set; } + + [JsonProperty("df")] + public int Df { get; set; } + + [JsonProperty("dr")] + public int Dr { get; set; } + + [JsonProperty("fd_window")] + public int FdWindow { get; set; } + + [JsonProperty("feature_bitmask")] + public string FeatureBitmask { get; set; } + + [JsonProperty("fp_window")] + public int FpWindow { get; set; } + + [JsonProperty("ft")] + public int Ft { get; set; } + + [JsonProperty("homelink_device_count")] + public int HomelinkDeviceCount { get; set; } + + [JsonProperty("homelink_nearby")] + public bool HomelinkNearby { get; set; } + + [JsonProperty("is_user_present")] + public bool IsUserPresent { get; set; } + + [JsonProperty("last_autopark_error")] + public string LastAutoparkError { get; set; } + + [JsonProperty("locked")] + public bool Locked { get; set; } + + [JsonProperty("media_info")] + public MediaInfo MediaInfo { get; set; } + + [JsonProperty("media_state")] + public MediaState MediaState { get; set; } + + [JsonProperty("notifications_supported")] + public bool NotificationsSupported { get; set; } + + [JsonProperty("odometer")] + public double Odometer { get; set; } + + [JsonProperty("parsed_calendar_supported")] + public bool ParsedCalendarSupported { get; set; } + + [JsonProperty("pf")] + public int Pf { get; set; } + + [JsonProperty("pr")] + public int Pr { get; set; } + + [JsonProperty("rd_window")] + public int RdWindow { get; set; } + + [JsonProperty("remote_start")] + public bool RemoteStart { get; set; } + + [JsonProperty("remote_start_enabled")] + public bool RemoteStartEnabled { get; set; } + + [JsonProperty("remote_start_supported")] + public bool RemoteStartSupported { get; set; } + + [JsonProperty("rp_window")] + public int RpWindow { get; set; } + + [JsonProperty("rt")] + public int Rt { get; set; } + + [JsonProperty("santa_mode")] + public int SantaMode { get; set; } + + [JsonProperty("sentry_mode")] + public bool SentryMode { get; set; } + + [JsonProperty("sentry_mode_available")] + public bool SentryModeAvailable { get; set; } + + [JsonProperty("service_mode")] + public bool ServiceMode { get; set; } + + [JsonProperty("service_mode_plus")] + public bool ServiceModePlus { get; set; } + + [JsonProperty("smart_summon_available")] + public bool SmartSummonAvailable { get; set; } + + [JsonProperty("software_update")] + public SoftwareUpdate SoftwareUpdate { get; set; } + + [JsonProperty("speed_limit_mode")] + public SpeedLimitMode SpeedLimitMode { get; set; } + + [JsonProperty("summon_standby_mode_enabled")] + public bool SummonStandbyModeEnabled { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + + [JsonProperty("tpms_hard_warning_fl")] + public bool TpmsHardWarningFl { get; set; } + + [JsonProperty("tpms_hard_warning_fr")] + public bool TpmsHardWarningFr { get; set; } + + [JsonProperty("tpms_hard_warning_rl")] + public bool TpmsHardWarningRl { get; set; } + + [JsonProperty("tpms_hard_warning_rr")] + public bool TpmsHardWarningRr { get; set; } + + [JsonProperty("tpms_last_seen_pressure_time_fl")] + public int TpmsLastSeenPressureTimeFl { get; set; } + + [JsonProperty("tpms_last_seen_pressure_time_fr")] + public int TpmsLastSeenPressureTimeFr { get; set; } + + [JsonProperty("tpms_last_seen_pressure_time_rl")] + public int TpmsLastSeenPressureTimeRl { get; set; } + + [JsonProperty("tpms_last_seen_pressure_time_rr")] + public int TpmsLastSeenPressureTimeRr { get; set; } + + [JsonProperty("tpms_pressure_fl")] + public float TpmsPressureFl { get; set; } + + [JsonProperty("tpms_pressure_fr")] + public float TpmsPressureFr { get; set; } + + [JsonProperty("tpms_pressure_rl")] + public float TpmsPressureRl { get; set; } + + [JsonProperty("tpms_pressure_rr")] + public float TpmsPressureRr { get; set; } + + [JsonProperty("tpms_rcp_front_value")] + public float TpmsRcpFrontValue { get; set; } + + [JsonProperty("tpms_rcp_rear_value")] + public float TpmsRcpRearValue { get; set; } + + [JsonProperty("tpms_soft_warning_fl")] + public bool TpmsSoftWarningFl { get; set; } + + [JsonProperty("tpms_soft_warning_fr")] + public bool TpmsSoftWarningFr { get; set; } + + [JsonProperty("tpms_soft_warning_rl")] + public bool TpmsSoftWarningRl { get; set; } + + [JsonProperty("tpms_soft_warning_rr")] + public bool TpmsSoftWarningRr { get; set; } + + [JsonProperty("valet_mode")] + public bool ValetMode { get; set; } + + [JsonProperty("valet_pin_needed")] + public bool ValetPinNeeded { get; set; } + + [JsonProperty("vehicle_name")] + public string VehicleName { get; set; } + + [JsonProperty("vehicle_self_test_progress")] + public int VehicleSelfTestProgress { get; set; } + + [JsonProperty("vehicle_self_test_requested")] + public bool VehicleSelfTestRequested { get; set; } + + [JsonProperty("webcam_available")] + public bool WebcamAvailable { get; set; } +} diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs index 4c34de7d2..4be612c19 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs @@ -25,5 +25,6 @@ public class IssueKeys public string FleetApiTokenRequestExpired => "FleetApiTokenRequestExpired"; public string FleetApiTokenNotReceived => "FleetApiTokenNotReceived"; public string FleetApiTokenExpired => "FleetApiTokenExpired"; + public string FleetApiTokenNoApiRequestsAllowed => "FleetApiRequestsNotAllowed"; public string CrashedOnStartup => "CrashedOnStartup"; } diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index edb7eb947..42f94acef 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -179,6 +179,12 @@ public PossibleIssues(IssueKeys issueKeys) "Look into the logfiles for further details." ) }, + { + issueKeys.FleetApiTokenNoApiRequestsAllowed, CreateIssue("Fleet API requests are not allowed.", + IssueType.Error, + "Update TSC to the latest version." + ) + }, }; } diff --git a/TeslaSolarCharger/Server/Scheduling/JobManager.cs b/TeslaSolarCharger/Server/Scheduling/JobManager.cs index c375480b6..280786a1c 100644 --- a/TeslaSolarCharger/Server/Scheduling/JobManager.cs +++ b/TeslaSolarCharger/Server/Scheduling/JobManager.cs @@ -50,6 +50,7 @@ public async Task StartJobs() var newVersionCheckJob = JobBuilder.Create().Build(); var spotPriceJob = JobBuilder.Create().Build(); var fleetApiTokenRefreshJob = JobBuilder.Create().Build(); + var vehicleDataRefreshJob = JobBuilder.Create().Build(); var currentDate = _dateTimeProvider.DateTimeOffSetNow(); var chargingTriggerStartTime = currentDate.AddSeconds(5); @@ -92,6 +93,9 @@ public async Task StartJobs() var fleetApiTokenRefreshTrigger = TriggerBuilder.Create() .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(59)).Build(); + var vehicleDataRefreshTrigger = TriggerBuilder.Create() + .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(11)).Build(); + var triggersAndJobs = new Dictionary> { {chargingValueJob, new HashSet { chargingValueTrigger }}, @@ -103,6 +107,7 @@ public async Task StartJobs() {newVersionCheckJob, new HashSet {newVersionCheckTrigger}}, {spotPriceJob, new HashSet {spotPricePlanningTrigger}}, {fleetApiTokenRefreshJob, new HashSet {fleetApiTokenRefreshTrigger}}, + {vehicleDataRefreshJob, new HashSet {vehicleDataRefreshTrigger}}, }; await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/VehicleDataRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/VehicleDataRefreshJob.cs new file mode 100644 index 000000000..29c253597 --- /dev/null +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/VehicleDataRefreshJob.cs @@ -0,0 +1,14 @@ +using Quartz; +using TeslaSolarCharger.Server.Services.Contracts; + +namespace TeslaSolarCharger.Server.Scheduling.Jobs; + +[DisallowConcurrentExecution] +public class VehicleDataRefreshJob(ILogger logger, ITeslaFleetApiService service) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogTrace("{method}({context})", nameof(Execute), context); + await service.RefreshCarData().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 3355066ef..427d08167 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -46,6 +46,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs index 02cc8a365..f6a633357 100644 --- a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs @@ -45,7 +45,10 @@ public async Task UpdateBaseConfigurationAsync(DtoBaseConfiguration baseConfigur _logger.LogTrace("{method}({@baseConfiguration})", nameof(UpdateBaseConfigurationAsync), baseConfiguration); var restartNeeded = await _jobManager.StopJobs().ConfigureAwait(false); await _configurationWrapper.UpdateBaseConfigurationAsync(baseConfiguration).ConfigureAwait(false); - await _teslaMateMqttService.ConnectMqttClient().ConfigureAwait(false); + if (!_configurationWrapper.GetVehicleDataFromTesla()) + { + await _teslaMateMqttService.ConnectClientIfNotConnected().ConfigureAwait(false); + } await _solarMqttService.ConnectMqttClient().ConfigureAwait(false); if (_configurationWrapper.FrontendConfiguration()?.GridValueSource == SolarValueSource.None) { diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs index c233cbf6c..fc6807280 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -14,4 +14,5 @@ public interface ITeslaFleetApiService DtoValue IsFleetApiEnabled(); DtoValue IsFleetApiProxyEnabled(); Task IsFleetApiProxyNeededInDatabase(); + Task RefreshCarData(); } diff --git a/TeslaSolarCharger/Server/Services/IssueValidationService.cs b/TeslaSolarCharger/Server/Services/IssueValidationService.cs index 998371c92..d037aa99e 100644 --- a/TeslaSolarCharger/Server/Services/IssueValidationService.cs +++ b/TeslaSolarCharger/Server/Services/IssueValidationService.cs @@ -91,6 +91,9 @@ public async Task> RefreshIssues(TimeSpan clientTimeZoneId) case FleetApiTokenState.Expired: issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenExpired)); break; + case FleetApiTokenState.NoApiRequestsAllowed: + issueList.Add(_possibleIssues.GetIssueByKey(_issueKeys.FleetApiTokenNoApiRequestsAllowed)); + break; case FleetApiTokenState.UpToDate: break; default: @@ -172,7 +175,7 @@ private List GetMqttIssues() { _logger.LogTrace("{method}()", nameof(GetMqttIssues)); var issues = new List(); - if (!_teslaMateMqttService.IsMqttClientConnected) + if (!_teslaMateMqttService.IsMqttClientConnected && !_configurationWrapper.GetVehicleDataFromTesla()) { issues.Add(_possibleIssues.GetIssueByKey(_issueKeys.MqttNotConnected)); } diff --git a/TeslaSolarCharger/Server/Services/MqttConnectionService.cs b/TeslaSolarCharger/Server/Services/MqttConnectionService.cs index 5b8f28f63..cad87881a 100644 --- a/TeslaSolarCharger/Server/Services/MqttConnectionService.cs +++ b/TeslaSolarCharger/Server/Services/MqttConnectionService.cs @@ -2,38 +2,30 @@ namespace TeslaSolarCharger.Server.Services; -public class MqttConnectionService : IMqttConnectionService +public class MqttConnectionService( + ILogger logger, + ITeslaMateMqttService teslaMateMqttService, + ISolarMqttService solarMqttService) + : IMqttConnectionService { - private readonly ILogger _logger; - private readonly ITeslaMateMqttService _teslaMateMqttService; - private readonly ISolarMqttService _solarMqttService; - - public MqttConnectionService(ILogger logger, - ITeslaMateMqttService teslaMateMqttService, ISolarMqttService solarMqttService) - { - _logger = logger; - _teslaMateMqttService = teslaMateMqttService; - _solarMqttService = solarMqttService; - } - public async Task ReconnectMqttServices() { - _logger.LogTrace("{method}()", nameof(ReconnectMqttServices)); + logger.LogTrace("{method}()", nameof(ReconnectMqttServices)); try { - await _teslaMateMqttService.ConnectClientIfNotConnected().ConfigureAwait(false); + await teslaMateMqttService.ConnectClientIfNotConnected().ConfigureAwait(false); } catch (Exception ex) { - _logger.LogError(ex, "Error while connecting TeslaMateMqttService"); + logger.LogError(ex, "Error while connecting TeslaMateMqttService"); } try { - await _solarMqttService.ConnectClientIfNotConnected().ConfigureAwait(false); + await solarMqttService.ConnectClientIfNotConnected().ConfigureAwait(false); } catch (Exception ex) { - _logger.LogError(ex, "Error while connecting SolarMqttService"); + logger.LogError(ex, "Error while connecting SolarMqttService"); } } } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 9a131c4c6..966166d0b 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -33,7 +33,8 @@ public class TeslaFleetApiService( IConstants constants, ITscConfigurationService tscConfigurationService, IBackendApiService backendApiService, - ISettings settings) + ISettings settings, + IConfigJsonService configJsonService) : ITeslaService, ITeslaFleetApiService { private DtoFleetApiRequest ChargeStartRequest => new() @@ -72,6 +73,12 @@ public class TeslaFleetApiService( NeedsProxy = false, }; + private DtoFleetApiRequest VehicleDataRequest => new() + { + RequestUrl = $"vehicle_data?endpoints={Uri.EscapeDataString("drive_state;location_data;vehicle_state;charge_state;climate_state")}", + NeedsProxy = false, + }; + public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) { logger.LogTrace("{method}({carId}, {startAmp}, {carState})", nameof(StartCharging), carId, startAmp, carState); @@ -85,7 +92,7 @@ public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) var vin = await GetVinByCarId(carId).ConfigureAwait(false); await SetAmp(carId, startAmp).ConfigureAwait(false); - var result = await SendCommandToTeslaApi(vin, ChargeStartRequest).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, ChargeStartRequest, HttpMethod.Post).ConfigureAwait(false); } @@ -93,7 +100,7 @@ public async Task WakeUpCar(int carId) { logger.LogTrace("{method}({carId})", nameof(WakeUpCar), carId); var vin = await GetVinByCarId(carId).ConfigureAwait(false); - var result = await SendCommandToTeslaApi(vin, WakeUpRequest).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, WakeUpRequest, HttpMethod.Post).ConfigureAwait(false); await teslamateApiService.ResumeLogging(carId).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(20)).ConfigureAwait(false); } @@ -102,7 +109,7 @@ public async Task StopCharging(int carId) { logger.LogTrace("{method}({carId})", nameof(StopCharging), carId); var vin = await GetVinByCarId(carId).ConfigureAwait(false); - var result = await SendCommandToTeslaApi(vin, ChargeStopRequest).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, ChargeStopRequest, HttpMethod.Post).ConfigureAwait(false); } public async Task SetAmp(int carId, int amps) @@ -116,13 +123,13 @@ public async Task SetAmp(int carId, int amps) } var vin = await GetVinByCarId(carId).ConfigureAwait(false); var commandData = $"{{\"charging_amps\":{amps}}}"; - var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, commandData).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData).ConfigureAwait(false); if (amps < 5 && car.CarState.LastSetAmp >= 5 || amps >= 5 && car.CarState.LastSetAmp < 5) { logger.LogDebug("Double set amp to be able to jump over or below 5A"); await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false); - result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, commandData).ConfigureAwait(false); + result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData).ConfigureAwait(false); } if (result?.Response?.Result == true) @@ -144,7 +151,7 @@ public async Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartT await WakeUpCarIfNeeded(carId, car.CarState.State).ConfigureAwait(false); - var result = await SendCommandToTeslaApi(vin, SetScheduledChargingRequest, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, SetScheduledChargingRequest, HttpMethod.Post, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); //assume update was sucessfull as update is not working after mosquitto restart (or wrong cached State) if (parameters["enable"] == "false") { @@ -162,7 +169,7 @@ public async Task SetChargeLimit(int carId, int limitSoC) { { "percent", limitSoC }, }; - await SendCommandToTeslaApi(vin, SetChargeLimitRequest, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); + await SendCommandToTeslaApi(vin, SetChargeLimitRequest, HttpMethod.Post, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); } public async Task> TestFleetApiAccess(int carId) @@ -173,7 +180,7 @@ public async Task> TestFleetApiAccess(int carId) try { await WakeUpCarIfNeeded(carId, inMemoryCar.CarState.State).ConfigureAwait(false); - var result = await SendCommandToTeslaApi(vin, OpenChargePortDoorRequest).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, OpenChargePortDoorRequest, HttpMethod.Post).ConfigureAwait(false); var successResult = result?.Response?.Result == true; var car = teslaSolarChargerContext.Cars.First(c => c.TeslaMateCarId == carId); car.TeslaFleetApiState = successResult ? TeslaCarFleetApiState.Ok : TeslaCarFleetApiState.NotWorking; @@ -207,7 +214,110 @@ public async Task OpenChargePortDoor(int carId) { logger.LogTrace("{method}({carId})", nameof(OpenChargePortDoor), carId); var vin = await GetVinByCarId(carId).ConfigureAwait(false); - var result = await SendCommandToTeslaApi(vin, OpenChargePortDoorRequest).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, OpenChargePortDoorRequest, HttpMethod.Post).ConfigureAwait(false); + } + + public async Task RefreshCarData() + { + logger.LogTrace("{method}()", nameof(RefreshCarData)); + if ((!configurationWrapper.GetVehicleDataFromTesla()) && (!configurationWrapper.GetVehicleDataFromTeslaDebug())) + { + logger.LogDebug("Vehicle Data are coming from TeslaMate. Do not refresh car states via Fleet API"); + return; + } + logger.LogTrace("Actually refreshing car data"); + var carIds = settings.CarsToManage.Select(c => c.Id).ToList(); + foreach (var carId in carIds) + { + var vin = await GetVinByCarId(carId).ConfigureAwait(false); + try + { + var vehicleData = await SendCommandToTeslaApi(vin, VehicleDataRequest, HttpMethod.Get) + .ConfigureAwait(false); + logger.LogTrace("Got vehicleData {@vehicleData}", vehicleData); + var teslaResult = vehicleData?.Response; + if (teslaResult == default) + { + await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(RefreshCarData), + $"Could not deserialize vehicle data: {JsonConvert.SerializeObject(vehicleData)}").ConfigureAwait(false); + logger.LogError("Could not deserialize vehicle data for car {carId}: {@vehicleData}", carId, vehicleData); + continue; + } + + if (configurationWrapper.GetVehicleDataFromTesla()) + { + var car = settings.Cars.First(c => c.Id == carId); + var carState = car.CarState; + carState.Name = teslaResult.VehicleState.VehicleName; + carState.SoC = teslaResult.ChargeState.BatteryLevel; + carState.SocLimit = teslaResult.ChargeState.ChargeLimitSoc; + var minimumSettableSocLimit = teslaResult.ChargeState.ChargeLimitSocMin; + if (car.CarConfiguration.MinimumSoC > car.CarState.SocLimit && car.CarState.SocLimit > minimumSettableSocLimit) + { + logger.LogWarning("Reduce Minimum SoC {minimumSoC} as charge limit {chargeLimit} is lower.", car.CarConfiguration.MinimumSoC, car.CarState.SocLimit); + car.CarConfiguration.MinimumSoC = (int)car.CarState.SocLimit; + await configJsonService.UpdateCarConfiguration().ConfigureAwait(false); + } + carState.ChargerPhases = teslaResult.ChargeState.ChargerPhases; + carState.ChargerVoltage = teslaResult.ChargeState.ChargerVoltage; + carState.ChargerActualCurrent = teslaResult.ChargeState.ChargerActualCurrent; + carState.PluggedIn = teslaResult.ChargeState.ChargingState != "Disconnected"; + carState.ClimateOn = teslaResult.ClimateState.IsClimateOn; + carState.TimeUntilFullCharge = TimeSpan.FromHours(teslaResult.ChargeState.TimeToFullCharge); + var teslaCarStateString = teslaResult.State; + var teslaCarShiftState = teslaResult.DriveState.ShiftState; + var teslaCarSoftwareUpdateState = teslaResult.VehicleState.SoftwareUpdate.Status; + var chargingState = teslaResult.ChargeState.ChargingState; + carState.State = DetermineCarState(teslaCarStateString, teslaCarShiftState, teslaCarSoftwareUpdateState, chargingState); + if (carState.State == CarStateEnum.Unknown) + { + await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(RefreshCarData), + $"Could not determine car state. TeslaCarStateString: {teslaCarStateString}, TeslaCarShiftState: {teslaCarShiftState}, TeslaCarSoftwareUpdateState: {teslaCarSoftwareUpdateState}, ChargingState: {chargingState}").ConfigureAwait(false); + } + carState.Healthy = true; + carState.ChargerRequestedCurrent = teslaResult.ChargeState.ChargeCurrentRequest; + carState.ChargerPilotCurrent = teslaResult.ChargeState.ChargerPilotCurrent; + carState.ScheduledChargingStartTime = teslaResult.ChargeState.ScheduledChargingStartTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(teslaResult.ChargeState.ScheduledChargingStartTime.Value); + carState.Longitude = teslaResult.DriveState.Longitude; + carState.Latitude = teslaResult.DriveState.Latitude; + } + + + } + catch (Exception ex) + { + logger.LogError(ex, "Could not get vehicle data for car {carId}", carId); + await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(RefreshCarData), + $"Error getting vehicle data: {ex.Message} {ex.StackTrace}").ConfigureAwait(false); + } + } + } + + private CarStateEnum? DetermineCarState(string teslaCarStateString, string? teslaCarShiftState, string teslaCarSoftwareUpdateState, string chargingState) + { + if (teslaCarStateString == "asleep") + { + return CarStateEnum.Asleep; + } + + if (teslaCarStateString == "offline") + { + return CarStateEnum.Offline; + } + if (teslaCarShiftState is "R" or "D") + { + return CarStateEnum.Driving; + } + if (chargingState == "Charging") + { + return CarStateEnum.Charging; + } + if (teslaCarSoftwareUpdateState == "installing") + { + return CarStateEnum.Updating; + } + logger.LogWarning("Could not determine car state. TeslaCarStateString: {teslaCarStateString}, TeslaCarShiftState: {teslaCarShiftState}, TeslaCarSoftwareUpdateState: {teslaCarSoftwareUpdateState}, ChargingState: {chargingState}", teslaCarStateString, teslaCarShiftState, teslaCarSoftwareUpdateState, chargingState); + return CarStateEnum.Unknown; } private async Task GetVinByCarId(int carId) @@ -318,7 +428,7 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) } } - private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, string contentData = "{}") where T : class + private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, HttpMethod httpMethod, string contentData = "{}") where T : class { logger.LogTrace("{method}({vin}, {@fleetApiRequest}, {contentData})", nameof(SendCommandToTeslaApi), vin, fleetApiRequest, contentData); var accessToken = await GetAccessTokenAndRefreshWhenNeededAsync().ConfigureAwait(false); @@ -329,8 +439,20 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) var baseUrl = GetFleetApiBaseUrl(accessToken.Region, fleetApiRequest.NeedsProxy); var requestUri = $"{baseUrl}api/1/vehicles/{vin}/{fleetApiRequest.RequestUrl}"; settings.TeslaApiRequestCounter++; - var response = await httpClient.PostAsync(requestUri, content).ConfigureAwait(false); + var request = new HttpRequestMessage() + { + Content = content, + RequestUri = new Uri(requestUri), + Method = httpMethod, + }; + var response = await httpClient.SendAsync(request).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (configurationWrapper.GetVehicleDataFromTeslaDebug()) + { + await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), + $"Logged Response string: {responseString}").ConfigureAwait(false); + } + var teslaCommandResultResponse = JsonConvert.DeserializeObject>(responseString); if (!response.IsSuccessStatusCode) { @@ -404,6 +526,7 @@ private string GetFleetApiBaseUrl(TeslaFleetApiRegion region, bool useProxyBaseU public async Task RefreshTokenAsync() { logger.LogTrace("{method}()", nameof(RefreshTokenAsync)); + settings.AllowUnlimitedFleetApiRequests = await CheckIfFleetApiRequestsAreAllowed().ConfigureAwait(false); var tokenState = (await GetFleetApiTokenState().ConfigureAwait(false)).Value; switch (tokenState) { @@ -426,6 +549,9 @@ public async Task RefreshTokenAsync() case FleetApiTokenState.UpToDate: logger.LogDebug("Token is up to date."); break; + case FleetApiTokenState.NoApiRequestsAllowed: + logger.LogError("No API requests allowed."); + return; default: throw new ArgumentOutOfRangeException(); } @@ -449,6 +575,36 @@ await backendApiService.PostErrorInformation(nameof(TeslaFleetApiService), nameo var dbToken = await GetAccessTokenAndRefreshWhenNeededAsync().ConfigureAwait(false); } + private async Task CheckIfFleetApiRequestsAreAllowed() + { + if (settings.AllowUnlimitedFleetApiRequests && (settings.LastFleetApiRequestAllowedCheck > dateTimeProvider.UtcNow().AddHours(-1))) + { + return true; + } + settings.LastFleetApiRequestAllowedCheck = dateTimeProvider.UtcNow(); + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(2); + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var url = configurationWrapper.BackendApiBaseUrl() + $"Tsc/AllowUnlimitedFleetApiAccess?installationId={installationId}"; + try + { + var response = await httpClient.GetAsync(url).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + return true; + } + + var responseValue = JsonConvert.DeserializeObject>(responseString); + return responseValue?.Value != false; + } + catch (Exception) + { + return true; + } + + } + public async Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token) { var currentTokens = await teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); @@ -471,6 +627,11 @@ public async Task> GetFleetApiTokenState() { return new DtoValue(FleetApiTokenState.NotNeeded); } + + if (!settings.AllowUnlimitedFleetApiRequests) + { + return new DtoValue(FleetApiTokenState.NoApiRequestsAllowed); + } var isCurrentRefreshTokenUnauthorized = await teslaSolarChargerContext.TscConfigurations .Where(c => c.Key == constants.TokenRefreshUnauthorized) .AnyAsync().ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs b/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs index ee9ab64aa..870f62ca1 100644 --- a/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs @@ -205,6 +205,12 @@ public async Task ConnectClientIfNotConnected() _logger.LogTrace("MqttClient is connected"); return; } + + if (_configurationWrapper.GetVehicleDataFromTesla()) + { + _logger.LogInformation("Not connecting to TeslaMate as data is retrieved from Teslas Fleet API"); + return; + } _logger.LogWarning("MqttClient is not connected"); await ConnectMqttClient().ConfigureAwait(false); } diff --git a/TeslaSolarCharger/Server/appsettings.Development.json b/TeslaSolarCharger/Server/appsettings.Development.json index dc36ba250..9fabe6703 100644 --- a/TeslaSolarCharger/Server/appsettings.Development.json +++ b/TeslaSolarCharger/Server/appsettings.Development.json @@ -58,6 +58,7 @@ "DisplayApiRequestCounter": true, "UseFleetApi": true, "UseFleetApiProxy": true, + "GetVehicleDataFromTesla": true, "GridPriceProvider": { "EnergyProvider": "Tibber", "Octopus": { diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 50137bb6d..d3bf5944f 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -67,6 +67,8 @@ "TeslaFleetApiBaseUrl": "https://www.teslasolarcharger.de/teslaproxy/", "UseFleetApiProxy": false, "LogLocationData": false, + "GetVehicleDataFromTesla": false, + "GetVehicleDataFromTeslaDebug": false, "AwattarBaseUrl": "https://api.awattar.de/v1/marketdata", "GridPriceProvider": { "EnergyProvider": "FixedPrice", diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index 214619f92..3049952cd 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -97,4 +97,6 @@ public interface IConfigurationWrapper string ConfigFileDirectory(); string AutoBackupsZipDirectory(); bool LogLocationData(); + bool GetVehicleDataFromTesla(); + bool GetVehicleDataFromTeslaDebug(); } diff --git a/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs b/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs index ed184c341..dfece4237 100644 --- a/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs +++ b/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs @@ -20,4 +20,6 @@ public interface ISettings bool CrashedOnStartup { get; set; } string? StartupCrashMessage { get; set; } bool FleetApiProxyNeeded { get; set; } + bool AllowUnlimitedFleetApiRequests { get; set; } + DateTime LastFleetApiRequestAllowedCheck { get; set; } } diff --git a/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs b/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs index e47f2876c..6e3092606 100644 --- a/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs +++ b/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs @@ -27,5 +27,8 @@ public Settings() public bool FleetApiProxyNeeded { get; set; } + public bool AllowUnlimitedFleetApiRequests { get; set; } + public DateTime LastFleetApiRequestAllowedCheck { get; set; } + public List Cars { get; set; } } diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs index 215c423c6..39d9ff8eb 100644 --- a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs +++ b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs @@ -10,4 +10,5 @@ public enum FleetApiTokenState NotReceived, Expired, UpToDate, + NoApiRequestsAllowed, } diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index 515289e73..793869bf8 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -140,6 +140,19 @@ public bool UseFleetApiProxy() return value; } + public bool GetVehicleDataFromTesla() + { + var environmentVariableName = "GetVehicleDataFromTesla"; + var value = configuration.GetValue(environmentVariableName); + return value; + } + public bool GetVehicleDataFromTeslaDebug() + { + var environmentVariableName = "GetVehicleDataFromTeslaDebug"; + var value = configuration.GetValue(environmentVariableName); + return value; + } + public bool LogLocationData() { var environmentVariableName = "LogLocationData";