using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using SkiaSharp;
const int Width = 250;
const int Height = 122;
const int SensorBottom = 44;
const int DetailsTop = 75;
const int LeftPaneRight = 126;
const int RightPaneLeft = 127;
const string OutputPath = "output.png";
const float LineSpacing = -3;
const string LocationCode = "14104";
const string SensorReadingsUrl = "http://10.16.1.100:8016/odata/SensorReadings?$orderby=Ts%20desc&$top=1&$select=TemperatureC,HumidityRh";
const string ReadyUrl = "http://10.16.1.100:8016/health/ready";
using var bitmap = new SKBitmap(Width, Height, SKColorType.Bgra8888, SKAlphaType.Premul);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.White);
using var textPaint = new SKPaint
{
Color = SKColors.Black,
IsAntialias = true
};
using var typeface = SKTypeface.Default;
using var sensorFont = new SKFont(typeface, 40);
using var weatherFont = new SKFont(typeface, 21);
using var statusFont = new SKFont(typeface, 27);
using var dateFont = new SKFont(typeface, 14);
using var timeFont = new SKFont(typeface, 23);
var currentSensor = await GetCurrentSensor();
var weatherForecast = await GetWeatherForecast();
using var statusHttpClient = new HttpClient();
var statusText = (await statusHttpClient.GetStringAsync(ReadyUrl)).Trim();
var now = DateTime.Now;
var date = now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var time = now.ToString("HH:mm", CultureInfo.InvariantCulture);
DrawTextFit(canvas, currentSensor, sensorFont, textPaint, new SKRect(0, 0, Width, SensorBottom), SKTextAlign.Center);
DrawTextFit(canvas, weatherForecast, weatherFont, textPaint, new SKRect(0, SensorBottom, Width, DetailsTop), SKTextAlign.Center);
DrawTextFit(canvas, statusText, statusFont, textPaint, new SKRect(0, DetailsTop, LeftPaneRight, Height), SKTextAlign.Center);
DrawTextStack(
canvas,
[(date, dateFont), (time, timeFont)],
textPaint,
new SKRect(RightPaneLeft, DetailsTop, Width, Height),
SKTextAlign.Center,
LineSpacing);
using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
if (data is null)
{
Console.Error.WriteLine("SkiaSharp failed to encode the bitmap as PNG.");
return;
}
var fullOutputPath = Path.GetFullPath(OutputPath);
await using (var fileStream = File.Create(fullOutputPath))
{
data.SaveTo(fileStream);
}
Console.WriteLine($"Generated {fullOutputPath} ({Width}x{Height}).");
var showImageScriptPath = Path.GetFullPath("show_image.py");
using var showImageProcess = Process.Start(new ProcessStartInfo
{
FileName = "python3",
ArgumentList = { showImageScriptPath, fullOutputPath },
UseShellExecute = false
});
if (showImageProcess != null)
{
await showImageProcess.WaitForExitAsync();
}
static void DrawTextFit(
SKCanvas canvas,
string text,
SKFont font,
SKPaint paint,
SKRect bounds,
SKTextAlign align)
{
var originalSize = font.Size;
var measuredWidth = font.MeasureText(text);
if (measuredWidth > bounds.Width)
{
font.Size = Math.Max(8, originalSize * bounds.Width / measuredWidth);
}
var metrics = font.Metrics;
var x = align switch
{
SKTextAlign.Center => bounds.MidX,
SKTextAlign.Right => bounds.Right,
_ => bounds.Left
};
var y = bounds.MidY - (metrics.Ascent + metrics.Descent) / 2;
canvas.DrawText(text, x, y, align, font, paint);
font.Size = originalSize;
}
static void DrawTextStack(
SKCanvas canvas,
IReadOnlyList<(string Text, SKFont Font)> lines,
SKPaint paint,
SKRect bounds,
SKTextAlign align,
float lineSpacing)
{
var originalSizes = lines.Select(line => line.Font.Size).ToArray();
for (var i = 0; i < lines.Count; i++)
{
var line = lines[i];
var measuredWidth = line.Font.MeasureText(line.Text);
if (measuredWidth > bounds.Width)
{
line.Font.Size = Math.Max(8, line.Font.Size * bounds.Width / measuredWidth);
}
}
var lineHeights = lines
.Select(line =>
{
var metrics = line.Font.Metrics;
return metrics.Descent - metrics.Ascent;
})
.ToArray();
var totalHeight = lineHeights.Sum() + lineSpacing * Math.Max(0, lines.Count - 1);
var y = bounds.MidY - totalHeight / 2;
var x = align switch
{
SKTextAlign.Center => bounds.MidX,
SKTextAlign.Right => bounds.Right,
_ => bounds.Left
};
for (var i = 0; i < lines.Count; i++)
{
var line = lines[i];
var metrics = line.Font.Metrics;
var baseline = y - metrics.Ascent;
canvas.DrawText(line.Text, x, baseline, align, line.Font, paint);
y += lineHeights[i] + lineSpacing;
}
for (var i = 0; i < lines.Count; i++)
{
lines[i].Font.Size = originalSizes[i];
}
}
static async Task<string> GetCurrentSensor()
{
using var httpClient = new HttpClient();
using var stream = await httpClient.GetStreamAsync(SensorReadingsUrl);
using var json = await JsonDocument.ParseAsync(stream);
var latest = json.RootElement.GetProperty("value").EnumerateArray().FirstOrDefault();
var temp = latest.GetProperty("TemperatureC").GetDouble();
var humidity = latest.GetProperty("HumidityRh").GetDouble();
return $"{temp:0}°C {humidity:0}%";
}
static async Task<string> GetWeatherForecast()
{
var forecastAfter = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
var filter = Uri.EscapeDataString($"ForecastDt gt {forecastAfter} and LocationCode eq '{LocationCode}'");
var url = $"http://10.16.1.100:8016/odata/Weather?$filter={filter}&$orderby=ForecastDt asc&$top=1&$select=ForecastDt,WeatherText,TemperatureC";
using var httpClient = new HttpClient();
using var stream = await httpClient.GetStreamAsync(url);
using var json = await JsonDocument.ParseAsync(stream);
var forecast = json.RootElement.GetProperty("value").EnumerateArray().FirstOrDefault();
var temp = forecast.GetProperty("TemperatureC").GetDouble();
var weatherText = forecast.GetProperty("WeatherText").GetString() ?? "";
var weatherMatch = Regex.Match(weatherText, @"\(([^()]*)\)");
var weather = weatherMatch.Success ? weatherMatch.Groups[1].Value : weatherText;
var forecastDt = forecast.GetProperty("ForecastDt").GetDateTimeOffset();
var (low, high) = await GetDailyTemperatureRange(httpClient, forecastDt);
return $"{low:0}~{high:0}°C {temp:0}°C {weather}";
}
static async Task<(double Low, double High)> GetDailyTemperatureRange(HttpClient httpClient, DateTimeOffset forecastDt)
{
var dayStart = new DateTimeOffset(forecastDt.Year, forecastDt.Month, forecastDt.Day, 0, 0, 0, forecastDt.Offset);
var dayEnd = dayStart.AddDays(1);
var filter = Uri.EscapeDataString(
$"ForecastDt ge {dayStart.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} and ForecastDt lt {dayEnd.UtcDateTime:yyyy-MM-ddTHH:mm:ssZ} and LocationCode eq '{LocationCode}'");
var url = $"http://10.16.1.100:8016/odata/Weather?$filter={filter}&$orderby=ForecastDt asc&$select=TemperatureC";
using var stream = await httpClient.GetStreamAsync(url);
using var json = await JsonDocument.ParseAsync(stream);
var temperatures = json.RootElement
.GetProperty("value")
.EnumerateArray()
.Select(item => item.GetProperty("TemperatureC").GetDouble())
.ToArray();
return (temperatures.Min(), temperatures.Max());
}