diff --git a/HomeApi/Controllers/HomeController.cs b/HomeApi/Controllers/HomeController.cs index a561a39..ddb3195 100644 --- a/HomeApi/Controllers/HomeController.cs +++ b/HomeApi/Controllers/HomeController.cs @@ -6,13 +6,30 @@ using Microsoft.AspNetCore.Mvc; namespace HomeApi.Controllers; [ApiController] -[Route("[controller]")] +[Route("home")] public class HomeController(IMediator mediator) : ControllerBase { - [HttpGet(Name = "GetHome")] + [HttpGet(Name = "getHome")] public async Task> Get() { - var result = await mediator.Send(new GetWeather.Command()); - return Ok(result); + return Ok(await mediator.Send(new Weather.Command())); + } + + [HttpGet("default.jpg")] + public async Task GetImage() + { + return File(await mediator.Send(new ImageGeneration.Command()), "image/jpeg"); + } + + [HttpGet("configuration")] + public async Task> GetCombinedBuffers() + { + return Ok(await mediator.Send(new Configuration.Command())); + } + + [HttpGet("departure-board")] + public async Task>> GetDepartureBoard() + { + return Ok(await mediator.Send(new DepartureBoard.Command())); } } \ No newline at end of file diff --git a/HomeApi/Extensions/ContractExtensions.cs b/HomeApi/Extensions/ContractExtensions.cs index 8937159..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, @@ -126,4 +130,23 @@ public static class ContractExtensions }; } + public static List? ToContract(this TrafikLabsApiResponse response) + { + if (response?.Departure is null) + return []; + + return response.Departure.Select(dep => new TimeTable + { + LineNumber = dep.ProductAtStop?.DisplayNumber ?? dep.ProductAtStop?.Line, + LineName = dep.ProductAtStop?.Name, + TransportType = dep.ProductAtStop?.CatOutL, + Operator = dep.ProductAtStop?.Operator, + StopName = dep.Stop, + DepartureTime = $"{dep.Date} {dep.Time}", + Direction = dep.Direction, + JourneyDetailRef = dep.JourneyDetailRef?.Ref, + 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/Configuration.cs b/HomeApi/Handlers/Configuration.cs new file mode 100644 index 0000000..e306d76 --- /dev/null +++ b/HomeApi/Handlers/Configuration.cs @@ -0,0 +1,31 @@ +using HomeApi.Models; +using HomeApi.Models.Configuration; +using MediatR; +using Microsoft.Extensions.Options; + +namespace HomeApi.Handlers; + +public static class Configuration +{ + public record Command : IRequest; + + public class Handler(IOptions configuration) + : IRequestHandler + { + private readonly ApiConfiguration _apiConfiguration = configuration.Value; + + public Task Handle(Command request, CancellationToken cancellationToken) + { + return Task.FromResult(new MicroProcessorConfiguration + { + InformationBoardImageUrl = _apiConfiguration.EspConfiguration.InformationBoardImageUrl, + UpdateIntervalMinutes = _apiConfiguration.EspConfiguration.UpdateIntervalMinutes, + BlackTextThreshold = _apiConfiguration.EspConfiguration.BlackTextThreshold, + ContrastStrength = _apiConfiguration.EspConfiguration.ContrastStrength, + DitheringStrength = _apiConfiguration.EspConfiguration.DitheringStrength, + EnableDithering = _apiConfiguration.EspConfiguration.EnableDithering, + EnhanceContrast = _apiConfiguration.EspConfiguration.EnhanceContrast + }); + } + } +} \ No newline at end of file diff --git a/HomeApi/Handlers/DepartureBoard.cs b/HomeApi/Handlers/DepartureBoard.cs new file mode 100644 index 0000000..eb425f9 --- /dev/null +++ b/HomeApi/Handlers/DepartureBoard.cs @@ -0,0 +1,18 @@ +using HomeApi.Integration; +using HomeApi.Models; +using MediatR; + +namespace HomeApi.Handlers; + +public static class DepartureBoard +{ + public record Command : IRequest>; + + public class Handler(IDepartureBoardService departureBoardService) : IRequestHandler> + { + public async Task> Handle(Command request, CancellationToken cancellationToken) + { + return await departureBoardService.GetDepartureBoard() ?? new List(); + } + } +} \ No newline at end of file diff --git a/HomeApi/Handlers/ImageGeneration.cs b/HomeApi/Handlers/ImageGeneration.cs new file mode 100644 index 0000000..22a88f2 --- /dev/null +++ b/HomeApi/Handlers/ImageGeneration.cs @@ -0,0 +1,79 @@ +using System.Dynamic; +using System.Reflection; +using HomeApi.Models.Configuration; +using MediatR; +using Microsoft.Extensions.Options; +using PuppeteerSharp; +using RazorLight; + +namespace HomeApi.Handlers; + +public static class ImageGeneration +{ + public record Command : IRequest; + + public class Handler( + IWebHostEnvironment env, + IMediator mediator, + IOptions apiConfiguration) + : IRequestHandler + { + private readonly ApiConfiguration _apiConfiguration = apiConfiguration.Value; + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var weather = await mediator.Send(new Weather.Command(), cancellationToken); + var departureBoard = await mediator.Send(new DepartureBoard.Command(), cancellationToken); + + var model = new Models.Image + { + Weather = weather, + TimeTable = departureBoard + }; + + 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 template = await File.ReadAllTextAsync(path, cancellationToken); + + dynamic viewBag = new ExpandoObject(); + viewBag.IsHighContrast = _apiConfiguration.EspConfiguration.IsHighContrastMode; + + var result = await engine.CompileRenderStringAsync("templateKey", template, model, viewBag: viewBag); + + if (!string.IsNullOrEmpty(result)) + return await CreateImage(result); + + throw new Exception("Failed to generate HTML content for image."); + } + + private static async Task CreateImage(string htmlContent) + { + var browserFetcher = new BrowserFetcher(); + await browserFetcher.DownloadAsync(); + var browser = await Puppeteer.LaunchAsync(new LaunchOptions + { + Headless = true, + Args = ["--disable-gpu"] + }); + + var page = await browser.NewPageAsync(); + await page.SetViewportAsync(new ViewPortOptions + { + Width = 800, + Height = 480 + }); + + await page.SetContentAsync(htmlContent, new NavigationOptions { WaitUntil = [WaitUntilNavigation.Networkidle0] }); + return await page.ScreenshotStreamAsync(new ScreenshotOptions { Type = ScreenshotType.Jpeg, Quality = 60 }); + } + } +} \ No newline at end of file diff --git a/HomeApi/Handlers/GetWeather.cs b/HomeApi/Handlers/Weather.cs similarity index 98% rename from HomeApi/Handlers/GetWeather.cs rename to HomeApi/Handlers/Weather.cs index 2deb672..07ab89d 100644 --- a/HomeApi/Handlers/GetWeather.cs +++ b/HomeApi/Handlers/Weather.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Options; namespace HomeApi.Handlers; -public static class GetWeather +public static class Weather { public record Command : IRequest; diff --git a/HomeApi/HomeApi.csproj b/HomeApi/HomeApi.csproj index cfae6fe..0d348c1 100644 --- a/HomeApi/HomeApi.csproj +++ b/HomeApi/HomeApi.csproj @@ -5,12 +5,16 @@ enable enable Linux + true + false + + @@ -21,4 +25,9 @@ + + + Always + + diff --git a/HomeApi/HomeApi.http b/HomeApi/HomeApi.http index e57bcb3..e69de29 100644 --- a/HomeApi/HomeApi.http +++ b/HomeApi/HomeApi.http @@ -1,6 +0,0 @@ -@HomeApi_HostAddress = http://localhost:5128 - -GET {{HomeApi_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/HomeApi/Integration/AuroraService.cs b/HomeApi/Integration/AuroraService.cs index 52ef175..d1bfd65 100644 --- a/HomeApi/Integration/AuroraService.cs +++ b/HomeApi/Integration/AuroraService.cs @@ -12,6 +12,14 @@ public class AuroraService(IAuroraClient auroraApi) : IAuroraService { public Task GetAuroraForecastAsync(string lat, string lon) { - return auroraApi.GetForecastAsync(latitude: lat, longitude: lon); + try + { + return auroraApi.GetForecastAsync(latitude: lat, longitude: lon); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } } } \ No newline at end of file diff --git a/HomeApi/Integration/Client/ResRobotClient.cs b/HomeApi/Integration/Client/ResRobotClient.cs new file mode 100644 index 0000000..9ed0f33 --- /dev/null +++ b/HomeApi/Integration/Client/ResRobotClient.cs @@ -0,0 +1,30 @@ +using HomeApi.Models.Response; +using Refit; + +namespace HomeApi.Integration.Client; + +public interface IResRobotClient +{ + [Get("/v2.1/departureBoard")] + Task GetDepartureBoardAsync( + [AliasAs("accessId")] string accessId, + [AliasAs("id")] string stopId, + [AliasAs("direction")] string direction = null, + [AliasAs("date")] string date = null, // Format: YYYY-MM-DD + [AliasAs("time")] string time = null, // Format: HH:MM + [AliasAs("duration")] int? duration = null, + [AliasAs("maxJourneys")] int? maxJourneys = null, + [AliasAs("operators")] string operators = null, // Example: "275,287" + [AliasAs("products")] int? products = null, + [AliasAs("passlist")] int? passlist = 0, + [AliasAs("lang")] string language = "sv", + [AliasAs("format")] string format = "json" + ); + + [Get("/v2.1/location.name")] + Task GetLocationsByNameAsync( + [AliasAs("input")] string input, + [AliasAs("format")] string format = "json", + [AliasAs("accessId")] string accessId = "YOUR_API_KEY" + ); +} \ No newline at end of file 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/DepartureBoardService.cs b/HomeApi/Integration/DepartureBoardService.cs new file mode 100644 index 0000000..ca7a424 --- /dev/null +++ b/HomeApi/Integration/DepartureBoardService.cs @@ -0,0 +1,46 @@ +using HomeApi.Extensions; +using HomeApi.Integration.Client; +using HomeApi.Models; +using HomeApi.Models.Configuration; +using Microsoft.Extensions.Options; + +namespace HomeApi.Integration; + +public interface IDepartureBoardService +{ + Task?> GetDepartureBoard(); +} + +public class DepartureBoardService(IResRobotClient departureBoardApi, IOptions options) : IDepartureBoardService +{ + private readonly ApiConfiguration _apiConfig = options.Value; + + public async Task?> GetDepartureBoard() + { + var locationResponse = await departureBoardApi.GetLocationsByNameAsync( + input: _apiConfig.DefaultStation, + format: "json", + accessId: _apiConfig.Keys.ResRobot + ); + + var id = locationResponse.StopLocationOrCoordLocation.FirstOrDefault()?.StopLocation?.ExtId; + + if (id == null) + return null; + + var result = await departureBoardApi.GetDepartureBoardAsync( + accessId: _apiConfig.Keys.ResRobot, + stopId: id, + direction: null, + date: DateTime.Now.ToString("yyyy-MM-dd"), + time: DateTime.Now.ToString("HH:mm"), + duration: 60, + maxJourneys: 10, + passlist: 1, + language: "sv", + format: "json" + ); + + return result.ToContract(); + } +} \ No newline at end of file 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/Configuration/ApiConfiguration.cs b/HomeApi/Models/Configuration/ApiConfiguration.cs index 61f3bba..c4644f9 100644 --- a/HomeApi/Models/Configuration/ApiConfiguration.cs +++ b/HomeApi/Models/Configuration/ApiConfiguration.cs @@ -5,6 +5,8 @@ public class ApiConfiguration public Keys Keys { get; set; } = new(); public BaseUrls BaseUrls { get; set; } = new(); public string DefaultCity { get; set; } = "Vega stockholms lan"; + public string DefaultStation { get; set; } = "Vega station"; + public EspConfig EspConfiguration { get; set; } = new(); } public class BaseUrls @@ -12,6 +14,7 @@ public class BaseUrls public string Weather { get; set; } = string.Empty; public string Nominatim { get; set; } = string.Empty; public string Aurora { get; set; } = string.Empty; + public string ResRobot { get; set; } = string.Empty; } public class Keys @@ -19,4 +22,17 @@ public class Keys public string Weather { get; set; } = string.Empty; public string Nominatim { get; set; } = string.Empty; public string Aurora { get; set; } = string.Empty; + public string ResRobot { get; set; } = string.Empty; +} + +public class EspConfig +{ + public string InformationBoardImageUrl { get; set; } = string.Empty; + public int UpdateIntervalMinutes { get; set; } = 2; + public int BlackTextThreshold { get; set; } = 190; // (0-255) + public bool EnableDithering { get; set; } = true; + public int DitheringStrength { get; set; } = 8; // (8-32) + public bool EnhanceContrast { get; set; } = true; + public int ContrastStrength { get; set; } = 10; // (0-100) + public bool IsHighContrastMode { get; set; } = true; } \ No newline at end of file diff --git a/HomeApi/Models/ImageGeneration.cs b/HomeApi/Models/ImageGeneration.cs new file mode 100644 index 0000000..e5b1350 --- /dev/null +++ b/HomeApi/Models/ImageGeneration.cs @@ -0,0 +1,7 @@ +namespace HomeApi.Models; + +public class Image +{ + public WeatherInformation Weather { get; set; } + public List TimeTable { get; set; } +} \ No newline at end of file diff --git a/HomeApi/Models/MicroProcessorConfiguration.cs b/HomeApi/Models/MicroProcessorConfiguration.cs new file mode 100644 index 0000000..fdb8bd3 --- /dev/null +++ b/HomeApi/Models/MicroProcessorConfiguration.cs @@ -0,0 +1,12 @@ +namespace HomeApi.Models; + +public class MicroProcessorConfiguration +{ + public string InformationBoardImageUrl { get; set; } = string.Empty; + public int UpdateIntervalMinutes { get; set; } = 2; + public int BlackTextThreshold { get; set; } = 190; // (0-255) + public bool EnableDithering { get; set; } = true; + public int DitheringStrength { get; set; } = 8; // (8-32) + public bool EnhanceContrast { get; set; } = true; + public int ContrastStrength { get; set; } = 10; // (0-100) +} \ No newline at end of file diff --git a/HomeApi/Models/Response/LocationNameResponse.cs b/HomeApi/Models/Response/LocationNameResponse.cs new file mode 100644 index 0000000..04e6d90 --- /dev/null +++ b/HomeApi/Models/Response/LocationNameResponse.cs @@ -0,0 +1,88 @@ +using System.Text.Json.Serialization; + +public class LocationNameResponse +{ + [JsonPropertyName("stopLocationOrCoordLocation")] + public List StopLocationOrCoordLocation { get; set; } + + [JsonPropertyName("TechnicalMessages")] + public TechnicalMessages TechnicalMessages { get; set; } + + [JsonPropertyName("serverVersion")] + public string ServerVersion { get; set; } + + [JsonPropertyName("dialectVersion")] + public string DialectVersion { get; set; } + + [JsonPropertyName("requestId")] + public string RequestId { get; set; } +} + +public class StopLocationOrCoordLocation +{ + [JsonPropertyName("StopLocation")] + public StopLocation StopLocation { get; set; } +} + +public class StopLocation +{ + [JsonPropertyName("productAtStop")] + public List ProductAtStop { get; set; } + + [JsonPropertyName("timezoneOffset")] + public int TimezoneOffset { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("extId")] + public string ExtId { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("lon")] + public double Lon { get; set; } + + [JsonPropertyName("lat")] + public double Lat { get; set; } + + [JsonPropertyName("weight")] + public int Weight { get; set; } + + [JsonPropertyName("products")] + public int Products { get; set; } + + [JsonPropertyName("minimumChangeDuration")] + public string MinimumChangeDuration { get; set; } +} + +public class ProductAtStop +{ + [JsonPropertyName("icon")] + public Icon Icon { get; set; } + + [JsonPropertyName("cls")] + public string Cls { get; set; } +} + +public class Icon +{ + [JsonPropertyName("res")] + public string Res { get; set; } +} + +public class TechnicalMessages +{ + [JsonPropertyName("TechnicalMessage")] + public List TechnicalMessage { get; set; } +} + +public class TechnicalMessage +{ + [JsonPropertyName("value")] + public string Value { get; set; } + + [JsonPropertyName("key")] + public string Key { get; set; } +} diff --git a/HomeApi/Models/Response/TrafikLabsApiResponse.cs b/HomeApi/Models/Response/TrafikLabsApiResponse.cs new file mode 100644 index 0000000..6bcc814 --- /dev/null +++ b/HomeApi/Models/Response/TrafikLabsApiResponse.cs @@ -0,0 +1,84 @@ +namespace HomeApi.Models.Response; +public class TrafikLabsApiResponse +{ + public List Departure { get; set; } +} + +public class Departure +{ + public JourneyDetailRef JourneyDetailRef { get; set; } + public string JourneyStatus { get; set; } + public ProductDetail ProductAtStop { get; set; } + public List Product { get; set; } + public Notes Notes { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string Stop { get; set; } + public string Stopid { get; set; } + public string StopExtId { get; set; } + public double Lon { get; set; } + public double Lat { get; set; } + public string Time { get; set; } + public string Date { get; set; } + public bool Reachable { get; set; } + public string Direction { get; set; } + public string DirectionFlag { get; set; } +} + +public class JourneyDetailRef +{ + public string Ref { get; set; } +} + +public class ProductDetail +{ + public Icon Icon { get; set; } + public OperatorInfo OperatorInfo { get; set; } + public string Name { get; set; } + public string InternalName { get; set; } + public string DisplayNumber { get; set; } + public string Num { get; set; } + public string Line { get; set; } + public string LineId { get; set; } + public string CatOut { get; set; } + public string CatIn { get; set; } + public string CatCode { get; set; } + public string Cls { get; set; } + public string CatOutS { get; set; } + public string CatOutL { get; set; } + public string OperatorCode { get; set; } + public string Operator { get; set; } + public string Admin { get; set; } + public string MatchId { get; set; } + public int? RouteIdxFrom { get; set; } + public int? RouteIdxTo { get; set; } +} + +public class Icon +{ + public string Res { get; set; } +} + +public class OperatorInfo +{ + public string Name { get; set; } + public string NameS { get; set; } + public string NameN { get; set; } + public string NameL { get; set; } + public string Id { get; set; } +} + +public class Notes +{ + public List Note { get; set; } +} + +public class Note +{ + public string Value { get; set; } + public string Key { get; set; } + public string Type { get; set; } + public int RouteIdxFrom { get; set; } + public int RouteIdxTo { get; set; } + public string TxtN { get; set; } +} diff --git a/HomeApi/Models/TimeTable.cs b/HomeApi/Models/TimeTable.cs new file mode 100644 index 0000000..ad741c5 --- /dev/null +++ b/HomeApi/Models/TimeTable.cs @@ -0,0 +1,15 @@ +namespace HomeApi.Models; + +public class TimeTable +{ + public string LineNumber { get; set; } // e.g. "43", "832" + public string LineName { get; set; } // e.g. "Länstrafik - Tåg 43" + public string TransportType { get; set; } // e.g. "Tåg", "Buss" + public string Operator { get; set; } // e.g. "SL" + public string StopName { get; set; } // e.g. "Vega station (Haninge kn)" + public string DepartureTime { get; set; } // e.g. 2025-07-15 01:03 + 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..5482fb1 100644 --- a/HomeApi/Program.cs +++ b/HomeApi/Program.cs @@ -11,11 +11,10 @@ builder.Services.AddIntegration(builder.Configuration); var app = builder.Build(); -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); - app.MapScalarApiReference(); -} +app.UseStaticFiles(); + +app.MapOpenApi(); +app.MapScalarApiReference(); app.UseHttpsRedirection(); diff --git a/HomeApi/Registration/RegisterIntegration.cs b/HomeApi/Registration/RegisterIntegration.cs index 78952db..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; @@ -28,7 +29,11 @@ public static class RegisterIntegration services.AddRefitClient() .ConfigureBaseAddress(apiConfiguration => apiConfiguration.BaseUrls.Weather); + + services.AddRefitClient() + .ConfigureBaseAddress(apiConfiguration => apiConfiguration.BaseUrls.ResRobot); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/HomeApi/appsettings.Development.json b/HomeApi/appsettings.Development.json index 9e738ac..c288e85 100644 --- a/HomeApi/appsettings.Development.json +++ b/HomeApi/appsettings.Development.json @@ -1,4 +1,11 @@ { + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://*:5000" + } + } + }, "Logging": { "LogLevel": { "Default": "Information", @@ -6,15 +13,28 @@ } }, "ApiConfiguration": { + "EspConfiguration": { + "InformationBoardImageUrl": "http://192.168.101.178:5000/home/default.jpg", + "UpdateIntervalMinutes": 2, + "BlackTextThreshold": 190, + "EnableDithering": true, + "DitheringStrength": 8, + "EnhanceContrast": true, + "ContrastStrength": 10, + "IsHighContrastMode": true + }, "Keys": { - "Weather": "KEY", - "SL": "" + "Weather": "NOT COMMITED", + "ResRobot": "NOT COMMITED" }, "BaseUrls": { "Nominatim": "https://nominatim.openstreetmap.org", "Aurora": "http://api.auroras.live", - "Weather": "https://api.weatherapi.com/v1" + "Weather": "https://api.weatherapi.com/v1", + "ResRobot": "https://api.resrobot.se" }, - "DefaultCity": "Vega stockholms lan" - } + "DefaultCity": "Vega stockholms lan", + "DefaultStation": "Vega Station" + }, + "AllowedHosts": "*" } diff --git a/HomeApi/appsettings.json b/HomeApi/appsettings.json index a419db2..c288e85 100644 --- a/HomeApi/appsettings.json +++ b/HomeApi/appsettings.json @@ -1,4 +1,11 @@ { + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://*:5000" + } + } + }, "Logging": { "LogLevel": { "Default": "Information", @@ -6,16 +13,28 @@ } }, "ApiConfiguration": { + "EspConfiguration": { + "InformationBoardImageUrl": "http://192.168.101.178:5000/home/default.jpg", + "UpdateIntervalMinutes": 2, + "BlackTextThreshold": 190, + "EnableDithering": true, + "DitheringStrength": 8, + "EnhanceContrast": true, + "ContrastStrength": 10, + "IsHighContrastMode": true + }, "Keys": { - "Weather": "KEY", - "SL": "" + "Weather": "NOT COMMITED", + "ResRobot": "NOT COMMITED" }, "BaseUrls": { "Nominatim": "https://nominatim.openstreetmap.org", "Aurora": "http://api.auroras.live", - "Weather": "https://api.weatherapi.com/v1" + "Weather": "https://api.weatherapi.com/v1", + "ResRobot": "https://api.resrobot.se" }, - "DefaultCity": "Vega stockholms lan" + "DefaultCity": "Vega stockholms lan", + "DefaultStation": "Vega Station" }, "AllowedHosts": "*" } diff --git a/HomeApi/wwwroot/index.cshtml b/HomeApi/wwwroot/index.cshtml new file mode 100644 index 0000000..f6cd989 --- /dev/null +++ b/HomeApi/wwwroot/index.cshtml @@ -0,0 +1,635 @@ +@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"; + } + + private static string UseHighContrast(bool isHighContrast) + { + return isHighContrast ? "high-contrast" : string.Empty; + } +} + + + + + + + Weather Dashboard + + + + +
+ +
+
+ + @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:
+
+
+
+ + + + \ No newline at end of file