diff --git a/HomeApi/Extensions/ContractExtensions.cs b/HomeApi/Extensions/ContractExtensions.cs index 7d75051..bae680a 100644 --- a/HomeApi/Extensions/ContractExtensions.cs +++ b/HomeApi/Extensions/ContractExtensions.cs @@ -53,6 +53,8 @@ public static class ContractExtensions So2 = weather.Current.Air_Quality.So2, // Sulfur Dioxide Pm10 = weather.Current.Air_Quality.Pm10, // Particulate Matter 10 micrometers or less Pm2_5 = weather.Current.Air_Quality.Pm2_5, // Particulate Matter 2.5 micrometers or less + Us_Epa_Index = weather.Current.Air_Quality.Us_Epa_Index, // US EPA Air Quality Index + Gb_Defra_Index = weather.Current.Air_Quality.Gb_Defra_Index, // UK DEFRA Air Quality Index }, AuroraProbability = new Probability { @@ -96,6 +98,8 @@ public static class ContractExtensions MaxTempC = day.Day.Maxtemp_C, MinTempC = day.Day.Mintemp_C, DayIcon = day.Day.Condition.Icon, + IconCode = day.Day.Condition.Code, + ChanceOfRain = day.Day.Daily_Chance_Of_Rain, Astro = new Models.Astro { Moon_Illumination = day.Astro.Moon_Illumination, @@ -141,7 +145,8 @@ public static class ContractExtensions DepartureTime = $"{dep.Date} {dep.Time}", Direction = dep.Direction, JourneyDetailRef = dep.JourneyDetailRef?.Ref, - Notes = dep.Notes?.Note?.Select(n => n.Value).ToList() ?? [] + Notes = dep.Notes?.Note?.Select(n => n.Value).ToList() ?? [], + InternalTransportationName = dep.ProductAtStop?.InternalName }).ToList(); } } \ No newline at end of file diff --git a/HomeApi/Handlers/ImageGeneration.cs b/HomeApi/Handlers/ImageGeneration.cs index f09d205..744537b 100644 --- a/HomeApi/Handlers/ImageGeneration.cs +++ b/HomeApi/Handlers/ImageGeneration.cs @@ -1,3 +1,4 @@ +using System.Dynamic; using System.Reflection; using HomeApi.Models.Configuration; using MediatR; @@ -43,13 +44,17 @@ public static class ImageGeneration if(weather is null) throw new Exception("Weather data not found"); - var engine = new RazorLightEngineBuilder().SetOperatingAssembly(Assembly.GetExecutingAssembly()) - .UseEmbeddedResourcesProject(typeof(ImageGeneration)).UseMemoryCachingProvider().Build(); - var path = Path.Combine(_env.WebRootPath, "index.cshtml"); + var engine = new RazorLightEngineBuilder() + .SetOperatingAssembly(Assembly.GetExecutingAssembly()) + .UseEmbeddedResourcesProject(typeof(ImageGeneration)) + .UseMemoryCachingProvider() + .Build(); - var template = await File.ReadAllTextAsync(path); + var path = Path.Combine(_env.WebRootPath, "index.cshtml"); - var result = await engine.CompileRenderStringAsync("templateKey", template, model); + var template = await File.ReadAllTextAsync(path, cancellationToken); + + var result = await engine.CompileRenderStringAsync("templateKey", template, model, viewBag: new ExpandoObject()); return await CreateImage(result); } @@ -85,15 +90,15 @@ public static class ImageGeneration // Reduce to 3-color e-paper palette image.ProcessPixelRows(accessor => { - for (int y = 0; y < accessor.Height; y++) + for (var y = 0; y < accessor.Height; y++) { var row = accessor.GetRowSpan(y); - for (int x = 0; x < row.Length; x++) + for (var x = 0; x < row.Length; x++) { var pixel = row[x]; // Compute perceived brightness (gray) - float brightness = 0.299f * pixel.R + 0.587f * pixel.G + 0.114f * pixel.B; + var brightness = 0.299f * pixel.R + 0.587f * pixel.G + 0.114f * pixel.B; if (pixel.R > 150 && pixel.G < 80 && pixel.B < 80) // RED threshold { diff --git a/HomeApi/HomeApi.csproj b/HomeApi/HomeApi.csproj index 6e8f8ee..5d6b263 100644 --- a/HomeApi/HomeApi.csproj +++ b/HomeApi/HomeApi.csproj @@ -6,6 +6,7 @@ enable Linux true + false @@ -24,4 +25,10 @@ .dockerignore + + + + Always + + diff --git a/HomeApi/Integration/Client/WeatherClient.cs b/HomeApi/Integration/Client/WeatherClient/WeatherClient.cs similarity index 80% rename from HomeApi/Integration/Client/WeatherClient.cs rename to HomeApi/Integration/Client/WeatherClient/WeatherClient.cs index bf91a73..58c3fc9 100644 --- a/HomeApi/Integration/Client/WeatherClient.cs +++ b/HomeApi/Integration/Client/WeatherClient/WeatherClient.cs @@ -1,16 +1,15 @@ using HomeApi.Models.Response; - -namespace HomeApi.Integration.Client; - using Refit; +namespace HomeApi.Integration.Client.WeatherClient; + public interface IWeatherClient { [Get("/forecast.json")] Task GetForecastAsync( [AliasAs("key")] string apiKey, [AliasAs("q")] string coordinates, - [AliasAs("days")] int days = 7, + [AliasAs("days")] int days = 14, [AliasAs("lang")] string language = "sv", [AliasAs("aqi")] string aqi = "yes", [AliasAs("alerts")] string alerts = "yes"); diff --git a/HomeApi/Integration/WeatherService.cs b/HomeApi/Integration/WeatherService.cs index c1b2caf..a7d2e64 100644 --- a/HomeApi/Integration/WeatherService.cs +++ b/HomeApi/Integration/WeatherService.cs @@ -1,4 +1,4 @@ -using HomeApi.Integration.Client; +using HomeApi.Integration.Client.WeatherClient; using HomeApi.Models.Configuration; using HomeApi.Models.Response; using Microsoft.Extensions.Options; diff --git a/HomeApi/Models/TimeTable.cs b/HomeApi/Models/TimeTable.cs index 007e29d..ad741c5 100644 --- a/HomeApi/Models/TimeTable.cs +++ b/HomeApi/Models/TimeTable.cs @@ -11,4 +11,5 @@ public class TimeTable public string Direction { get; set; } // e.g. "Farsta Strand station" public string JourneyDetailRef { get; set; } // e.g. "1|39437|0|1|15072025" public List Notes { get; set; } // e.g. "Pendeltåg", "Endast 2 klass" + public string InternalTransportationName { get; set; } // e.g. "Pendeltåg 43" } \ No newline at end of file diff --git a/HomeApi/Models/WeatherInformation.cs b/HomeApi/Models/WeatherInformation.cs index 3f981da..3a04298 100644 --- a/HomeApi/Models/WeatherInformation.cs +++ b/HomeApi/Models/WeatherInformation.cs @@ -62,6 +62,9 @@ public class Forecast public WeatherSummary? Day { get; set; } public WeatherSummary? Night { get; set; } public Astro Astro { get; set; } + public int IconCode { get; set; } + + public int ChanceOfRain { get; set; } } public class Astro { diff --git a/HomeApi/Program.cs b/HomeApi/Program.cs index 165360f..817a1de 100644 --- a/HomeApi/Program.cs +++ b/HomeApi/Program.cs @@ -11,6 +11,8 @@ builder.Services.AddIntegration(builder.Configuration); var app = builder.Build(); +app.UseStaticFiles(); + if (app.Environment.IsDevelopment()) { app.MapOpenApi(); diff --git a/HomeApi/Registration/RegisterIntegration.cs b/HomeApi/Registration/RegisterIntegration.cs index b7799ef..50537a2 100644 --- a/HomeApi/Registration/RegisterIntegration.cs +++ b/HomeApi/Registration/RegisterIntegration.cs @@ -1,6 +1,7 @@ using HomeApi.Extensions; using HomeApi.Integration; using HomeApi.Integration.Client; +using HomeApi.Integration.Client.WeatherClient; using HomeApi.Models.Configuration; using Microsoft.Extensions.Options; using Refit; diff --git a/HomeApi/wwwroot/index.cshtml b/HomeApi/wwwroot/index.cshtml index f99bd7d..840c1c1 100644 --- a/HomeApi/wwwroot/index.cshtml +++ b/HomeApi/wwwroot/index.cshtml @@ -1,212 +1,625 @@ +@using HomeApi.Models +@using System; +@using System.Linq; +@using System.Collections.Generic; @model HomeApi.Models.Image +@functions { + private string GetDayStatus(string input) + { + var date = DateTime.Parse(input); + var today = DateTime.Today; + var dayOfWeek = date.DayOfWeek.ToString(); + + if (date.Date == today) + return "Today"; + if (date.Date == today.AddDays(1)) + return "Tomorrow"; + if (date >= today.AddDays(1) && date < today.AddDays(7)) + return dayOfWeek; + return $"On {date:dddd, dd MMM yyyy}"; + } + + private string GetTransPortIcon(string transportType) + { + var transportIcons = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "buss", "fa-bus" }, + { "bus", "fa-bus" }, + { "tunnelbana", "fa-train-subway" }, + { "metro", "fa-train-subway" }, + { "tåg", "fa-train" }, + { "train", "fa-train" }, + { "båt", "fa-ferry" }, + { "ferry", "fa-ferry" } + }; + + foreach (var keyword in transportIcons.Keys) + { + if (transportType.Contains(keyword, StringComparison.OrdinalIgnoreCase)) + return transportIcons[keyword]; + } + + return "fa-question-circle"; + } + + private string GetAirQualityStatus(AirQuality data) + { + var highestAqi = Math.Max(data.Us_Epa_Index, data.Gb_Defra_Index); + + return highestAqi switch + { + <= 50 => "Good", + <= 100 => "Moderate", + <= 150 => "Unhealthy for Sensitive Groups", + <= 200 => "Unhealthy", + <= 300 => "Very Unhealthy", + _ => "Hazardous" + }; + } + + private static string GetWeatherIcon(int code, bool isNight = false) + { + var map = new Dictionary + { + // Clear/Sunny + { 1000, isNight ? "fa-moon" : "fa-sun" }, + // Partly cloudy + { 1003, isNight ? "fa-cloud-moon" : "fa-cloud-sun" }, + // Cloudy/Overcast + { 1006, "fa-cloud" }, + { 1009, "fa-cloud" }, + // Mist/Fog + { 1030, "fa-smog" }, + { 1135, "fa-smog" }, + { 1147, "fa-smog" }, + // Rain/drizzle + { 1063, "fa-cloud-rain" }, + { 1150, "fa-cloud-drizzle" }, + { 1153, "fa-cloud-drizzle" }, + { 1180, "fa-cloud-rain" }, + { 1183, "fa-cloud-rain" }, + { 1186, "fa-cloud-showers-heavy" }, + { 1189, "fa-cloud-showers-heavy" }, + { 1240, "fa-cloud-showers-heavy" }, + { 1243, "fa-cloud-showers-heavy" }, + { 1246, "fa-cloud-showers-heavy" }, + // Sleet, freezing drizzle/rain + { 1069, "fa-cloud-meatball" }, + { 1072, "fa-cloud-meatball" }, + { 1168, "fa-cloud-meatball" }, + { 1171, "fa-cloud-meatball" }, + { 1198, "fa-cloud-meatball" }, + { 1201, "fa-cloud-meatball" }, + { 1204, "fa-cloud-meatball" }, + { 1207, "fa-cloud-meatball" }, + { 1249, "fa-cloud-meatball" }, + { 1252, "fa-cloud-meatball" }, + // Snow + { 1066, "fa-snowflake" }, + { 1114, "fa-snowflake" }, + { 1117, "fa-snowflake" }, + { 1210, "fa-snowflake" }, + { 1213, "fa-snowflake" }, + { 1216, "fa-snowflake" }, + { 1219, "fa-snowflake" }, + { 1222, "fa-snowflake" }, + { 1225, "fa-snowflake" }, + { 1255, "fa-snowflake" }, + { 1258, "fa-snowflake" }, + // Snow showers + { 1261, "fa-snowflake" }, + { 1264, "fa-snowflake" }, + { 1279, "fa-snowflake" }, + { 1282, "fa-snowflake" }, + // Thunderstorms + { 1087, "fa-bolt" }, + { 1273, "fa-bolt" }, + { 1276, "fa-bolt" }, + // Ice pellets + { 1237, "fa-icicles" }, + }; + + return map.TryGetValue(code, out var value) ? value : "fa-question-circle"; + } +} + - + - + + Weather Dashboard + -
-

@Model.Weather.CityName

-
- Current Weather - - - - - - - - - - - - - - - - - - - - - -
TempFeelsCloudsWindGustsUpdated
@Model.Weather.Current.Temperature °C@Model.Weather.Current.Feelslike °C@Model.Weather.Current.Cloud%@Model.Weather.Current.WindPerMeterSecond m/s (@Model.Weather.Current.WindDirection)@Model.Weather.Current.WindGustPerMeterSecond m/s@Model.Weather.Current.LastUpdated
-
- Current Air Quality - - - - - - - - - - - - - - - - - - - - - - - - - -
CONO₂O₃SO₂PM2.5PM10EPADEFRA
@Model.Weather.Current.AirQuality?.Co@Model.Weather.Current.AirQuality?.No2@Model.Weather.Current.AirQuality?.O3@Model.Weather.Current.AirQuality?.So2@Model.Weather.Current.AirQuality?.Pm2_5@Model.Weather.Current.AirQuality?.Pm10@Model.Weather.Current.AirQuality?.Us_Epa_Index@Model.Weather.Current.AirQuality?.Gb_Defra_Index
-
- Aurora Probability - - - - - - - - - - - - - - - - - - - -
ProbabilityColorHighestLocationDate
@Model.Weather.Current.AuroraProbability?.Value%@Model.Weather.Current.AuroraProbability?.Colour@Model.Weather.Current.AuroraProbability?.HighestProbability?.Value%(@Model.Weather.Current.AuroraProbability?.HighestProbability?.Lat, @Model.Weather.Current.AuroraProbability?.HighestProbability?.Long)@Model.Weather.Current.AuroraProbability?.HighestProbability?.Date.ToShortDateString()
- +
+ +
+
+ + @Model.Weather.CityName +
+ +
+
@Model.Weather.Current.Temperature°C
+
Feels like @Model.Weather.Current.Feelslike°C
+
+ +
+
+
+ + Clouds +
+ @Model.Weather.Current.Cloud% +
+ +
+
+ + Wind +
+ @Model.Weather.Current.WindPerMeterSecond.ToString("0.##"); m/s @Model.Weather.Current.WindDirection +
+ +
+
+ + Gusts +
+ @Model.Weather.Current.WindGustPerMeterSecond.ToString("0.##"); m/s +
+ +
+
+ + Aurora +
+ @Model.Weather.Current.AuroraProbability.Value% +
+
+ +
+ Air Quality: @GetAirQualityStatus(Model.Weather.Current.AirQuality) +
+
+ + +
+

@Model.Weather.Forecast.Count-Day Forecast

+ +
+ @foreach (var day in Model.Weather.Forecast) + { +
+
@GetDayStatus(day.Date)
+
+ +
+
@(day.Day?.ConditionText ?? "Unspecified")
+
@day.MinTempC°/@day.MaxTempC°
+
Rain: @day.ChanceOfRain%
+
+ } +
+ +
+
+
+ + Sunrise +
+ @Model.Weather.Forecast[0].Astro.Sunrise +
+ +
+
+ + Sunset +
+ @Model.Weather.Forecast[0].Astro.Sunset +
+ +
+
+ + Moonrise +
+ @Model.Weather.Forecast[0].Astro.Moonrise +
+ +
+
+ + Moonset +
+ @Model.Weather.Forecast[0].Astro.Moonset +
+ +
+ Moon phase + @Model.Weather.Forecast[0].Astro.Moon_Illumination% +
+
+
+ + +
+

Upcoming Departures

+ +
+ @foreach (var transport in Model.TimeTable.Take(5)) + { + + var departureTime = DateTime.Parse(transport.DepartureTime); + var minutesUntilDeparture = (int)(departureTime - DateTime.Now).TotalMinutes; + +
+ +
+
@transport.LineNumber
+
to @transport.Direction
+
+
@DateTime.Parse(transport.DepartureTime).ToShortTimeString() (@minutesUntilDeparture min)
+
+ } +
+ +
+
Last updated:
+
-
-

Forecast

- - - - - - - - - - - - - - - - - - - - @foreach (var f in Model.Weather.Forecast) - { - - - - - - - - - - - - - - - - } - -
DateIconMinMaxDayFeelsRainSnowSunriseSunsetMoonriseMoonsetMoon
@f.DateIcon@f.MinTempC°C@f.MaxTempC°C@f.Day?.ConditionText@f.Day?.AvgFeelslikeC°C@f.Day?.TotalChanceOfRain%@f.Day?.TotalChanceOfSnow%@f.Astro.Sunrise@f.Astro.Sunset@f.Astro.Moonrise@f.Astro.Moonset@f.Astro.Moon_Phase (@f.Astro.Moon_Illumination%)
-
+ \ No newline at end of file