diff --git a/HomeAssistant/HassTemplateExtender.cs b/HomeAssistant/HassTemplateExtender.cs index 0c1027b..bc6b143 100644 --- a/HomeAssistant/HassTemplateExtender.cs +++ b/HomeAssistant/HassTemplateExtender.cs @@ -1,3 +1,4 @@ +using System.Globalization; using DisplayUtil.Template; using NetDaemon.HassModel; using Scriban.Runtime; @@ -14,6 +15,7 @@ public void Enrich(ScriptObject context, EnrichScope scope) hassObject.Import("get_state", GetState); hassObject.Import("get_attribute", GetAttribute); hassObject.Import("get_float_state", GetFloatState); + hassObject.Import("get_datetime", GetDateTime); context.Add("hass", hassObject); } @@ -39,8 +41,21 @@ private float GetFloatState(string entityId) { var state = GetState(entityId); if (state == null) return 0f; - return TemplateContextProvider.ToFloat(state); + return UtilTemplateExtender.ToFloat(state); } + private DateTime? GetDateTime(string entityId) + { + var state = GetState(entityId); + if (state is null) return null; + if (!DateTime.TryParseExact(state, + "yyyy-MM-dd HH:mm:ss", + CultureInfo.GetCultureInfo("de-DE"), + DateTimeStyles.None, + out var dt)) + return null; + + return dt; + } } \ No newline at end of file diff --git a/Layouting/ElementCollection.cs b/Layouting/ElementCollection.cs index b2790e4..ac4c1ce 100644 --- a/Layouting/ElementCollection.cs +++ b/Layouting/ElementCollection.cs @@ -46,6 +46,16 @@ public ElementCollection Append(Element element) return this; } + /// + /// Add all Elements to this Collection + /// + /// The elements + public ElementCollection Append(IEnumerable elements) + { + Children.AddRange(elements); + return this; + } + protected override SKSize CalculateSize(DrawContext drawContext) { var childContext = BuildChildDrawContext(drawContext); diff --git a/MqttExport/MqttExportJob.cs b/MqttExport/MqttExportJob.cs index 4ff73e8..7f3284d 100644 --- a/MqttExport/MqttExportJob.cs +++ b/MqttExport/MqttExportJob.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Options; +using NetDaemon.HassModel; namespace DisplayUtil.MqttExport; @@ -7,10 +8,13 @@ public class MqttExportJob( IOptions optionsSnapshot ) : IHostedService { + private static readonly TimeSpan InitTimeout = TimeSpan.FromSeconds(5); private CancellationTokenSource? _cancellationTokenSource; private async Task RunAsync(CancellationToken cancellation) { + await Task.Delay(InitTimeout); + while (!cancellation.IsCancellationRequested) { await using (var scope = scopeFactory.CreateAsyncScope()) diff --git a/MqttExport/MqttExporter.cs b/MqttExport/MqttExporter.cs index 1131868..dbb5795 100644 --- a/MqttExport/MqttExporter.cs +++ b/MqttExport/MqttExporter.cs @@ -1,9 +1,3 @@ -using System.Diagnostics; -using DisplayUtil.EspUtilities; -using DisplayUtil.Scenes; -using Microsoft.Extensions.Options; -using MQTTnet.Client; - namespace DisplayUtil.MqttExport; /// @@ -11,37 +5,10 @@ namespace DisplayUtil.MqttExport; /// Scoped /// public class MqttExporter( - ExportingMqttClient exportingMqttClient, - IOptions options + ExportingMqttClient exportingMqttClient ) { - public Task ExportUriToMqtt(string providerId) - { - var settings = options.Value; - - var query = providerId.IndexOf('?'); - var providerPath = query == -1 - ? providerId - : providerId[0..(query - 1)]; - - var uriBuilder = new UriBuilder - { - Port = 80, - Scheme = "http", - Host = settings.ServerHostName, - Path = EspUtilitiesInitExtension.CompressedImageRoute - }; - - uriBuilder.Path = EspUtilitiesInitExtension.CompressedImageRoute - .Replace("{providerId}", providerPath); - - if (query != -1) - uriBuilder.Query = providerId[query..]; - - return SubmitAsync(uriBuilder.Uri); - } - - public virtual async Task SubmitAsync(Uri uri) + public virtual async Task PublishUriToMqttAsync(Uri uri) { await exportingMqttClient.SendAsync(uri.ToString()); } @@ -49,15 +16,14 @@ public virtual async Task SubmitAsync(Uri uri) internal partial class CachedMqttExporter( ExportingMqttClient exportingMqttClient, - IOptions options, ILogger logger) - : MqttExporter(exportingMqttClient, options) + : MqttExporter(exportingMqttClient) { private readonly ILogger _logger = logger; private Uri? _lastSubmission; - public override Task SubmitAsync(Uri uri) + public override Task PublishUriToMqttAsync(Uri uri) { if (_lastSubmission == uri) { @@ -67,7 +33,7 @@ public override Task SubmitAsync(Uri uri) LogSubmitting(uri); _lastSubmission = uri; - return base.SubmitAsync(uri); + return base.PublishUriToMqttAsync(uri); } [LoggerMessage(LogLevel.Debug, "Skipping resubmission of {uri}")] diff --git a/MqttExport/MqttInitExtension.cs b/MqttExport/MqttInitExtension.cs index 74378dd..4446e0c 100644 --- a/MqttExport/MqttInitExtension.cs +++ b/MqttExport/MqttInitExtension.cs @@ -61,4 +61,23 @@ MqttSettings settings return true; } + + public static WebApplication UseMqttWriter(this WebApplication app) + { + if (app.Services.GetService() is null) return app; + + app.MapGet("/mqtt/uri", async (MqttUrlRenderer renderer) => + { + return Results.Ok(await renderer.GetMqttTemplateUriAsync()); + }) + .WithName("Get MQTT URI") + .WithOpenApi(); + + app.MapGet("/mqtt/template", async (MqttUrlRenderer renderer) + => Results.Ok(await renderer.GetMqttTemplateAsync())) + .WithName("Get MQTT Template") + .WithOpenApi(); + + return app; + } } diff --git a/MqttExport/MqttUrlRenderer.cs b/MqttExport/MqttUrlRenderer.cs index 7a7e76a..fac1fe1 100644 --- a/MqttExport/MqttUrlRenderer.cs +++ b/MqttExport/MqttUrlRenderer.cs @@ -1,3 +1,4 @@ +using DisplayUtil.EspUtilities; using DisplayUtil.Template; using Microsoft.Extensions.Options; @@ -9,19 +10,51 @@ public class MqttUrlRenderer( IOptionsSnapshot options ) { + public async Task GetMqttTemplateUriAsync() + { + var providerId = await GetMqttTemplateAsync(); - /// - /// Exports the Uri to MQTT - /// - /// Task - public async Task RenderUrlAndPublish() + var settings = options.Value; + + var query = providerId.IndexOf('?'); + var providerPath = query == -1 + ? providerId + : providerId[0..query]; + + var uriBuilder = new UriBuilder + { + Port = 80, + Scheme = "http", + Host = settings.ServerHostName, + Path = EspUtilitiesInitExtension.CompressedImageRoute + }; + + uriBuilder.Path = EspUtilitiesInitExtension.CompressedImageRoute + .Replace("{providerId}", providerPath); + + if (query != -1) + uriBuilder.Query = providerId[query..]; + + return uriBuilder.Uri; + } + + public async Task GetMqttTemplateAsync() { var template = options.Value.ScreenDetectTemplate!; var result = await renderer.RenderAsync(template, EnrichScope.TemplateProvider); - await exporter.ExportUriToMqtt(result); + return result.Trim(); + } + + /// + /// Exports the Uri to MQTT + /// + /// Task + public async Task RenderUrlAndPublish() + { + await exporter.PublishUriToMqttAsync(await GetMqttTemplateUriAsync()); } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 3561b68..64bf8b7 100644 --- a/Program.cs +++ b/Program.cs @@ -25,7 +25,8 @@ .AddSingleton(); builder.Services.AddScoped() - .AddScoped(); + .AddScoped() + .AddSingleton(); builder.Services.AddTransient(); @@ -48,7 +49,8 @@ .WithName("Preview Image") .WithOpenApi(); -app.UseEspUtilities(); +app.UseEspUtilities() + .UseMqttWriter(); app.UseStaticFiles(); app.Run(); \ No newline at end of file diff --git a/Resources/screens/_icon_data.sbntxt b/Resources/screens/_icon_data.sbntxt index 4263654..5a91a96 100644 --- a/Resources/screens/_icon_data.sbntxt +++ b/Resources/screens/_icon_data.sbntxt @@ -1,8 +1,8 @@ {{ func icon_data(sIconName, sText) }} -{{ if (sText | string.strip | string.size ) > 0}} - - - - -{{ end }} + {{ if (sText | string.strip | string.size ) > 0}} + + + + + {{ end }} {{ end }} \ No newline at end of file diff --git a/Resources/screens/_media_player.sbntxt b/Resources/screens/_media_player.sbntxt index 0e55eef..a196baa 100644 --- a/Resources/screens/_media_player.sbntxt +++ b/Resources/screens/_media_player.sbntxt @@ -26,14 +26,15 @@ {{ if state == "playing" media_position = hass.get_attribute sPlayer "media_position" | to_float media_duration = hass.get_attribute sPlayer "media_duration" | to_float + media_duration_ts = timespan.from_seconds media_duration media_updated = hass.get_attribute sPlayer "media_position_updated_at" | date.parse media_start = media_updated | date.add_seconds (-media_position) media_end = media_start | date.add_seconds media_duration }} - - + {{ icon_data "timer" (timespan.to_string media_duration_ts "c") }} + {{ icon_data "clock" (media_end | date.to_string "%T") }} {{ else if state == "paused" icon_data "pause" " -> Pausiert <- " diff --git a/Resources/screens/_timer.sbntxt b/Resources/screens/_timer.sbntxt new file mode 100644 index 0000000..168ae36 --- /dev/null +++ b/Resources/screens/_timer.sbntxt @@ -0,0 +1,20 @@ +{{ func timer() }} + {{ + start_time = hass.get_datetime "input_datetime.googletimerstart" + end_time = hass.get_datetime "input_datetime.googletimerover" + timer_text = hass.get_state "input_text.countdown_text" + }} + + {{ if start_time.year < 2000 + ret 0 + end}} + + + + {{ icon_data "hourglass-start" (start_time | date.to_string "%T") }} + {{ icon_data "hourglass-end" (end_time | date.to_string "%T") }} + + + {{ ret 1 }} + +{{ end }} \ No newline at end of file diff --git a/Resources/screens/export_screen.sbntxt b/Resources/screens/export_screen.sbntxt new file mode 100644 index 0000000..d1caca0 --- /dev/null +++ b/Resources/screens/export_screen.sbntxt @@ -0,0 +1,8 @@ +{{ + state = hass.get_state "sensor.wohnzimmer_display_mode" +}} +{{ if state == "Normal" }} +main?version={{ date.to_string date.now "%s" }} +{{ else }} +{{ state | string.downcase }} +{{ end }} diff --git a/Resources/screens/idle.sbntxt b/Resources/screens/idle.sbntxt new file mode 100644 index 0000000..1a40969 --- /dev/null +++ b/Resources/screens/idle.sbntxt @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/screens/main.sbntxt b/Resources/screens/main.sbntxt index c12e93d..00ca714 100644 --- a/Resources/screens/main.sbntxt +++ b/Resources/screens/main.sbntxt @@ -1,6 +1,9 @@ {{ include "_media_player" include "_icon_data" + include "_timer" + + items = 0 media_players = [ "media_player.buro", @@ -31,19 +34,22 @@ - + {{ icon_data "couch" (hass.get_float_state "sensor.smart_hygrometer_0998_temperature" | math.round 2) + " °C"}} {{ icon_data "cloud-sun" (hass.get_float_state "sensor.hue_outdoor_motion_sensor_1_temperature" | math.round 2) + " °C"}} + + {{ icon_data "dumbbell" (hass.get_state "sensor.fitx_auslastung") + "%" }} + + {{ for player in media_players - media_player player - end - }} + items += media_player player + end }} - + {{ items += timer }} diff --git a/Resources/screens/night.sbntxt b/Resources/screens/night.sbntxt new file mode 100644 index 0000000..ffb505d --- /dev/null +++ b/Resources/screens/night.sbntxt @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Template/TemplateContextProvider.cs b/Template/TemplateContextProvider.cs index f46d010..8207c45 100644 --- a/Template/TemplateContextProvider.cs +++ b/Template/TemplateContextProvider.cs @@ -15,7 +15,6 @@ public class TemplateContextProvider( public TemplateContext GetTemplateContext(EnrichScope scope) { var scriptObject = new ScriptObject(); - scriptObject.Import("to_float", ToFloat); foreach (var extender in extenders) { @@ -32,10 +31,4 @@ public TemplateContext GetTemplateContext(EnrichScope scope) return context; } - - public static float ToFloat(string? content) - { - if (content == null) return 0; - return float.Parse(content, CultureInfo.InvariantCulture); - } } diff --git a/Template/UtilTemplateExtender.cs b/Template/UtilTemplateExtender.cs new file mode 100644 index 0000000..a665bb5 --- /dev/null +++ b/Template/UtilTemplateExtender.cs @@ -0,0 +1,28 @@ +using System.Globalization; +using Scriban.Runtime; + +namespace DisplayUtil.Template; + +internal class UtilTemplateExtender : ITemplateExtender +{ + public void Enrich(ScriptObject scriptObject, EnrichScope scope) + { + scriptObject.Import("to_float", ToFloat); + + var timespanObject = new ScriptObject(); + timespanObject.Import(typeof(TimeSpan)); + timespanObject.Import("to_string", TimespanToString); + scriptObject.Add("timespan", timespanObject); + } + + public static float ToFloat(string? content) + { + if (content == null) return 0; + return float.Parse(content, CultureInfo.InvariantCulture); + } + + public static string TimespanToString(TimeSpan timeSpan, string format) + { + return timeSpan.ToString(format); + } +} \ No newline at end of file diff --git a/XmlModel/Models/IXmlModel.cs b/XmlModel/Models/IXmlModel.cs index 486f5a0..0efd17f 100644 --- a/XmlModel/Models/IXmlModel.cs +++ b/XmlModel/Models/IXmlModel.cs @@ -37,10 +37,9 @@ protected virtual ElementCollection FillWithChildren( FaIconDrawer iconDrawer, FontProvider fontProvider, DefaultDefinition defaults ) { - foreach (var child in Children) - { - collection.Append(child.AsElement(iconDrawer, fontProvider, defaults)); - } + collection.Append( + Children.Select(e => e.AsElement(iconDrawer, fontProvider, defaults)) + ); return collection; } } \ No newline at end of file