From 86509b0c1f0335453072ca0c42f1629b91ff7207 Mon Sep 17 00:00:00 2001 From: Jasper Date: Fri, 10 Jul 2026 11:54:16 +0200 Subject: [PATCH 01/15] Fixing automations project. --- src/CodeCasa.Automations/CodeCasa.Automations.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeCasa.Automations/CodeCasa.Automations.csproj b/src/CodeCasa.Automations/CodeCasa.Automations.csproj index 07d0d50..0aa7012 100644 --- a/src/CodeCasa.Automations/CodeCasa.Automations.csproj +++ b/src/CodeCasa.Automations/CodeCasa.Automations.csproj @@ -23,6 +23,7 @@ + @@ -41,7 +42,6 @@ - From ed236a62872870660df7e4e7c925b459cab2f032 Mon Sep 17 00:00:00 2001 From: Jasper Date: Fri, 10 Jul 2026 14:29:03 +0200 Subject: [PATCH 02/15] First implementation of reolink doorbell feed --- .../Extensions/GeneratedExtensions.cs | 1 + .../InputSelectEntities.cs | 56 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 4 +- .../Doorbell/FrontDoorDoorbell.cs | 7 +++ .../Doorbell/ReolinkDoorbell.cs | 18 ++++++ .../Extensions/ServiceCollectionExtensions.cs | 21 ++++++- .../Dashboard/BackgroundPicture.razor | 2 +- .../Components/Dashboard/DoorbellStream.razor | 35 ++++++++++++ .../Components/Pages/Dashboard.razor | 1 + 9 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 src/CodeCasa.CustomEntities.Core/Doorbell/FrontDoorDoorbell.cs create mode 100644 src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs create mode 100644 src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor diff --git a/src/CodeCasa.AutoGenerated/Extensions/GeneratedExtensions.cs b/src/CodeCasa.AutoGenerated/Extensions/GeneratedExtensions.cs index 7d72c44..8e6db04 100644 --- a/src/CodeCasa.AutoGenerated/Extensions/GeneratedExtensions.cs +++ b/src/CodeCasa.AutoGenerated/Extensions/GeneratedExtensions.cs @@ -11,6 +11,7 @@ public static IServiceCollection AddHomeAssistantGenerated(this IServiceCollecti .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/src/CodeCasa.AutoGenerated/InputSelectEntities.cs b/src/CodeCasa.AutoGenerated/InputSelectEntities.cs index 8325bba..f5915a2 100644 --- a/src/CodeCasa.AutoGenerated/InputSelectEntities.cs +++ b/src/CodeCasa.AutoGenerated/InputSelectEntities.cs @@ -1,4 +1,6 @@ using NetDaemon.HassModel; +using NetDaemon.HassModel.Entities; +using System.Text.Json.Serialization; namespace CodeCasa.AutoGenerated; @@ -25,4 +27,58 @@ public InputSelectEntities(IHaContext haContext) public InputSelectEntity LivingRoomPanelNotifications => new(_haContext, "input_select.living_room_panel_notifications"); ///Variable Living Room Wall Panel View public InputSelectEntity VariableLivingRoomWallPanelView => new(_haContext, "input_select.variable_living_room_wall_panel_view"); +} + +public partial record CameraEntity : Entity, CameraAttributes>, ICameraEntityCore +{ + public CameraEntity(IHaContext haContext, string entityId) : base(haContext, entityId) + { + } + + public CameraEntity(IEntityCore entity) : base(entity) + { + } +} + +public partial record CameraAttributes +{ + [JsonPropertyName("device_class")] + public string? DeviceClass { get; init; } + + [JsonPropertyName("entity_picture")] + public string? EntityPicture { get; init; } + + [JsonPropertyName("friendly_name")] + public string? FriendlyName { get; init; } + + [JsonPropertyName("supported_features")] + public double? SupportedFeatures { get; init; } + + [JsonPropertyName("access_token")] + public string? AccessToken { get; init; } + + [JsonPropertyName("motion_detection")] + public bool? MotionDetection { get; init; } + + [JsonPropertyName("client_id")] + public string? ClientId { get; init; } + + [JsonPropertyName("camera_name")] + public string? CameraName { get; init; } +} + +public partial class CameraEntities +{ + private readonly IHaContext _haContext; + public CameraEntities(IHaContext haContext) + { + _haContext = haContext; + } + + /// Enumerates all camera entities currently registered (at runtime) in Home Assistant as CameraEntity + public IEnumerable EnumerateAll() => _haContext.GetAllEntities().Where(e => e.EntityId.StartsWith("camera.")).Select(e => new CameraEntity(e)); + ///Doorbell + public CameraEntity Doorbell => new(_haContext, "camera.doorbell"); + ///Front Door Doorbell Fluent + public CameraEntity FrontDoorFrontDoorDoorbellFluent => new(_haContext, "camera.front_door_front_door_doorbell_fluent"); } \ No newline at end of file diff --git a/src/CodeCasa.CustomEntities.Automation/Extensions/ServiceCollectionExtensions.cs b/src/CodeCasa.CustomEntities.Automation/Extensions/ServiceCollectionExtensions.cs index 57b8ec3..e317e9a 100644 --- a/src/CodeCasa.CustomEntities.Automation/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeCasa.CustomEntities.Automation/Extensions/ServiceCollectionExtensions.cs @@ -16,7 +16,6 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddCodeCasaCustomAutomationEntities(this IServiceCollection serviceCollection) { serviceCollection - .AddCodeCasaCustomCoreEntities() .AddLightNotifications(); @@ -39,8 +38,7 @@ public static IServiceCollection AddCodeCasaCustomAutomationEntities(this IServi .AddTransient() // People - .AddTransient() - .AddTransient() + .AddTransientImplementationsOf() .AddTransient(); return serviceCollection; diff --git a/src/CodeCasa.CustomEntities.Core/Doorbell/FrontDoorDoorbell.cs b/src/CodeCasa.CustomEntities.Core/Doorbell/FrontDoorDoorbell.cs new file mode 100644 index 0000000..6086fa4 --- /dev/null +++ b/src/CodeCasa.CustomEntities.Core/Doorbell/FrontDoorDoorbell.cs @@ -0,0 +1,7 @@ +using CodeCasa.AutoGenerated; + +namespace CodeCasa.CustomEntities.Core.Doorbell +{ + public class FrontDoorDoorbell(CameraEntities cameraEntities) + : ReolinkDoorbell(cameraEntities.Doorbell, cameraEntities.FrontDoorFrontDoorDoorbellFluent); +} diff --git a/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs b/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs new file mode 100644 index 0000000..2bc6538 --- /dev/null +++ b/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs @@ -0,0 +1,18 @@ +using System.Reactive.Linq; +using CodeCasa.AutoGenerated; +using NetDaemon.HassModel.Entities; + +namespace CodeCasa.CustomEntities.Core.Doorbell; + +public abstract class ReolinkDoorbell(CameraEntity cameraEntity, CameraEntity fluentCameraEntity) +{ + public IObservable FluentUrl => cameraEntity.StateAllChangesWithCurrent() + .Select(s => s.New?.Attributes?.AccessToken) + .Where(at => !string.IsNullOrEmpty(at)) + .Select(at => + { + var url = $"http://10.10.0.10:8123/api/camera_proxy_stream/camera.doorbell?token={at!}"; + return url; + }) + .DistinctUntilChanged(); +} \ No newline at end of file diff --git a/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs b/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs index fdeaab5..115b90c 100644 --- a/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using CodeCasa.CustomEntities.Core.GoogleHome; +using CodeCasa.CustomEntities.Core.Doorbell; +using CodeCasa.CustomEntities.Core.GoogleHome; using CodeCasa.CustomEntities.Core.InputSelect; using CodeCasa.CustomEntities.Core.Weather; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +12,8 @@ public static IServiceCollection AddCodeCasaCustomCoreEntities(this IServiceColl { return serviceCollection + .AddTransientImplementationsOf() + .AddTransient() .AddTransient() @@ -19,4 +22,20 @@ public static IServiceCollection AddCodeCasaCustomCoreEntities(this IServiceColl .AddTransient(); } + + private static IServiceCollection AddTransientImplementationsOf(this IServiceCollection services) + { + var baseType = typeof(TBase); + + var types = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => baseType.IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract).ToArray(); + + foreach (var type in types) + { + services.AddTransient(type); + } + + return services; + } } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/BackgroundPicture.razor b/src/CodeCasa.Dashboard/Components/Dashboard/BackgroundPicture.razor index 60fcdd5..4c053c4 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/BackgroundPicture.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/BackgroundPicture.razor @@ -11,7 +11,7 @@ [Parameter] public bool ToBackground { get; set; } private string[] _images = []; - private int _currentIndex = 0; + private int _currentIndex; private System.Threading.Timer? _timer; protected override void OnInitialized() diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor new file mode 100644 index 0000000..a9f8879 --- /dev/null +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -0,0 +1,35 @@ +@using CodeCasa.CustomEntities.Core.Doorbell + +@inject FrontDoorDoorbell FrontDoorDoorbell +@implements IDisposable + +@if (_stream != null) +{ + Front Door Live Stream +} +else +{ +

Loading doorbell stream...

+} + +@code { + private string? _stream; + private IDisposable? _subscription; + + protected override void OnInitialized() + { + _subscription = FrontDoorDoorbell.FluentUrl.Subscribe(url => + { + InvokeAsync(() => + { + _stream = url; + StateHasChanged(); + }); + }); + } + + public void Dispose() + { + _subscription?.Dispose(); + } +} \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor index ef11aa6..998c37b 100644 --- a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor +++ b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor @@ -46,6 +46,7 @@ else if (!_netDaemonConnected) + @code { private bool _netDaemonInitialized; From 86d531f9ce0e62e4be68dde8fcdb7d4f9c7c485c Mon Sep 17 00:00:00 2001 From: Jasper Date: Fri, 10 Jul 2026 17:07:40 +0200 Subject: [PATCH 03/15] Using hardcoded webrtc for testing --- .../Doorbell/ReolinkDoorbell.cs | 17 ++++---- .../Components/Dashboard/DoorbellStream.razor | 40 +++---------------- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs b/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs index 2bc6538..f17b9a2 100644 --- a/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs +++ b/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs @@ -4,15 +4,12 @@ namespace CodeCasa.CustomEntities.Core.Doorbell; -public abstract class ReolinkDoorbell(CameraEntity cameraEntity, CameraEntity fluentCameraEntity) +public abstract class ReolinkDoorbell { - public IObservable FluentUrl => cameraEntity.StateAllChangesWithCurrent() - .Select(s => s.New?.Attributes?.AccessToken) - .Where(at => !string.IsNullOrEmpty(at)) - .Select(at => - { - var url = $"http://10.10.0.10:8123/api/camera_proxy_stream/camera.doorbell?token={at!}"; - return url; - }) - .DistinctUntilChanged(); + private readonly CameraEntity _fluentCameraEntity; + + protected ReolinkDoorbell(CameraEntity cameraEntity, CameraEntity fluentCameraEntity) + { + _fluentCameraEntity = fluentCameraEntity; + } } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor index a9f8879..61edd16 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -1,35 +1,5 @@ -@using CodeCasa.CustomEntities.Core.Doorbell - -@inject FrontDoorDoorbell FrontDoorDoorbell -@implements IDisposable - -@if (_stream != null) -{ - Front Door Live Stream -} -else -{ -

Loading doorbell stream...

-} - -@code { - private string? _stream; - private IDisposable? _subscription; - - protected override void OnInitialized() - { - _subscription = FrontDoorDoorbell.FluentUrl.Subscribe(url => - { - InvokeAsync(() => - { - _stream = url; - StateHasChanged(); - }); - }); - } - - public void Dispose() - { - _subscription?.Dispose(); - } -} \ No newline at end of file + + From e0b025fe3df17592a9d90da611b533507ef73e8f Mon Sep 17 00:00:00 2001 From: Jasper Date: Sat, 11 Jul 2026 14:38:53 +0200 Subject: [PATCH 04/15] Progress on doorbell preview implementation. --- .gitignore | 1 + Dockerfile.automations | 4 +- Dockerfile.dashboard | 4 +- .../InputSelectEntities.cs | 2 + .../Dashboard/LivingRoomPanelNavigation.cs | 31 +++++++++++-- .../Events/Events.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../InputSelect/DoorbellState.cs | 18 ++++++++ .../InputSelect/LivingRoomWallPanelView.cs | 3 +- .../Components/Dashboard/Clock.razor | 46 +++++++++++++++---- .../Components/Dashboard/DoorbellStream.razor | 42 +++++++++++++++-- .../Components/Pages/Dashboard.razor | 25 +++++++--- 12 files changed, 149 insertions(+), 31 deletions(-) create mode 100644 src/CodeCasa.CustomEntities.Core/InputSelect/DoorbellState.cs diff --git a/.gitignore b/.gitignore index be64ddc..7ef5aae 100644 --- a/.gitignore +++ b/.gitignore @@ -401,3 +401,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +/BuildAndPublishDashboard.ps1 diff --git a/Dockerfile.automations b/Dockerfile.automations index b8620bd..ddd3003 100644 --- a/Dockerfile.automations +++ b/Dockerfile.automations @@ -1,5 +1,5 @@ # Build stage -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy everything @@ -9,7 +9,7 @@ COPY . . RUN dotnet publish "src/CodeCasa.Automations/CodeCasa.Automations.csproj" -c Release -o /app/publish /p:UseAppHost=false # Runtime stage -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . diff --git a/Dockerfile.dashboard b/Dockerfile.dashboard index 6931bd0..9b9314a 100644 --- a/Dockerfile.dashboard +++ b/Dockerfile.dashboard @@ -1,5 +1,5 @@ # Build stage -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # Copy everything @@ -9,7 +9,7 @@ COPY . . RUN dotnet publish "src/CodeCasa.Dashboard/CodeCasa.Dashboard.csproj" -c Release -o /app/publish /p:UseAppHost=false # Runtime stage -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY --from=build /app/publish . diff --git a/src/CodeCasa.AutoGenerated/InputSelectEntities.cs b/src/CodeCasa.AutoGenerated/InputSelectEntities.cs index f5915a2..bb801c8 100644 --- a/src/CodeCasa.AutoGenerated/InputSelectEntities.cs +++ b/src/CodeCasa.AutoGenerated/InputSelectEntities.cs @@ -25,6 +25,8 @@ public InputSelectEntities(IHaContext haContext) public InputSelectEntity JasperState => new(_haContext, "input_select.jasper_state"); ///Living Room Panel Notifications public InputSelectEntity LivingRoomPanelNotifications => new(_haContext, "input_select.living_room_panel_notifications"); + ///Variable Doorbell State + public InputSelectEntity VariableDoorbellState => new(_haContext, "input_select.variable_doorbell_state"); ///Variable Living Room Wall Panel View public InputSelectEntity VariableLivingRoomWallPanelView => new(_haContext, "input_select.variable_living_room_wall_panel_view"); } diff --git a/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs b/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs index 181712f..df9c428 100644 --- a/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs +++ b/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs @@ -9,6 +9,7 @@ using CodeCasa.CustomEntities.Core.Events; using CodeCasa.CustomEntities.Core.GoogleHome; using CodeCasa.CustomEntities.Core.InputSelect; +using CodeCasa.NetDaemon.Extensions.Observables; using NetDaemon.HassModel.Entities; namespace CodeCasa.Automations.Apps.Dashboard @@ -23,6 +24,7 @@ public LivingRoomPanelNavigation( GoogleHomeAlarmEntities googleHomeAlarmEntities, GoogleHomeTimerEntities googleHomeTimerEntities, LivingRoomWallPanelView livingRoomWallPanelView, + DoorbellState doorbellState, IScheduler scheduler) { var panelInteraction = context.Events.Filter(Events.LivingRoomPanelInteractionEvent).Select(_ => true); @@ -63,10 +65,33 @@ public LivingRoomPanelNavigation( return (bool?)null; }) .Where(b => b != null).Select(b => b!.Value); + var displayHomeScreen = recentInteraction.Or(infoToDisplay, OperatorDistinctness.NotDistinct); - recentInteraction.Or(infoToDisplay, OperatorDistinctness.NotDistinct).SubscribeTrueFalse( - () => livingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Home), - () => livingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Idle)); + var viewFeedEvents = context.Events.Filter(Events.LivingRoomPanelViewDoorbellFeedEvent).Select(_ => true); + var viewFeed = viewFeedEvents + .Merge( + viewFeedEvents + .Throttle(TimeSpan.FromSeconds(30)) + .Select(_ => false) + ).Prepend(false); + var doorbellActive = doorbellState.ToBooleanObservable(s => s.State != DoorbellState.States.Idle); + var displayDoorbell = viewFeed.Merge(doorbellActive); + + displayDoorbell.CombineLatest(displayHomeScreen).Subscribe(tuple => + { + if (tuple.First) + { + livingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Doorbell); + return; + } + if (tuple.Second) + { + livingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Home); + return; + } + + livingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Idle); + }); } // ReSharper disable once ClassNeverInstantiated.Local diff --git a/src/CodeCasa.CustomEntities.Core/Events/Events.cs b/src/CodeCasa.CustomEntities.Core/Events/Events.cs index 56a7632..4ae6b91 100644 --- a/src/CodeCasa.CustomEntities.Core/Events/Events.cs +++ b/src/CodeCasa.CustomEntities.Core/Events/Events.cs @@ -6,5 +6,6 @@ public static class Events public const string LivingRoomPanelNotificationDemoEvent = nameof(LivingRoomPanelNotificationDemoEvent); public const string PhoneNotificationDemoEvent = nameof(PhoneNotificationDemoEvent); public const string LivingRoomPanelInteractionEvent = nameof(LivingRoomPanelInteractionEvent); + public const string LivingRoomPanelViewDoorbellFeedEvent = nameof(LivingRoomPanelViewDoorbellFeedEvent); } } diff --git a/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs b/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs index 115b90c..4ab77cb 100644 --- a/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeCasa.CustomEntities.Core/Extensions/ServiceCollectionExtensions.cs @@ -19,7 +19,8 @@ public static IServiceCollection AddCodeCasaCustomCoreEntities(this IServiceColl // Input Select Entities .AddTransient() - + .AddTransient() + .AddTransient(); } diff --git a/src/CodeCasa.CustomEntities.Core/InputSelect/DoorbellState.cs b/src/CodeCasa.CustomEntities.Core/InputSelect/DoorbellState.cs new file mode 100644 index 0000000..0bbceb7 --- /dev/null +++ b/src/CodeCasa.CustomEntities.Core/InputSelect/DoorbellState.cs @@ -0,0 +1,18 @@ +using CodeCasa.AutoGenerated; + +namespace CodeCasa.CustomEntities.Core.InputSelect; + +public record DoorbellState : TypeSafeInputSelectEntity +{ + public DoorbellState(InputSelectEntities inputSelectEntities) + : base(inputSelectEntities.VariableDoorbellState) + { + } + + public enum States + { + Idle, + Detection, + Ringing + } +} \ No newline at end of file diff --git a/src/CodeCasa.CustomEntities.Core/InputSelect/LivingRoomWallPanelView.cs b/src/CodeCasa.CustomEntities.Core/InputSelect/LivingRoomWallPanelView.cs index 7d6b995..163b5c2 100644 --- a/src/CodeCasa.CustomEntities.Core/InputSelect/LivingRoomWallPanelView.cs +++ b/src/CodeCasa.CustomEntities.Core/InputSelect/LivingRoomWallPanelView.cs @@ -12,6 +12,7 @@ public LivingRoomWallPanelView(InputSelectEntities inputSelectEntities) public enum States { Idle, - Home + Home, + Doorbell } } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor b/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor index b0047eb..93e4315 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor @@ -2,12 +2,18 @@ @using Occurify.Reactive.Extensions @using Occurify.TimeZones -
-

+
+

+ +

@code { - [Parameter] public bool Centered { get; set; } + public enum ClockPosition { Centered, Top, Corner } + + [Parameter] public ClockPosition Position { get; set; } = ClockPosition.Centered; [Parameter] public EventCallback OnClick { get; set; } private DateTime _currentTime = DateTime.Now; @@ -30,29 +36,49 @@ width: 100%; transition: all 0.5s ease; text-align: center; + left: 0; } .clock-container button { pointer-events: auto; } - .clock-container.center { + /* Centered State */ + .clock-container.centered { top: 50%; transform: translateY(-50%); } + .clock-container.centered h1 { + font-size: 30vw; + } + + /* Top State */ .clock-container.top { top: 20px; transform: translateY(0); } - .clock-container h1 { - margin: 0; - transition: font-size 1s ease; - font-size: 30vw; + .clock-container.top h1 { + font-size: 15vw; + } + + /* Corner State */ + .clock-container.corner { + top: 15px; + left: 15px; + width: auto; + text-align: left; + transform: translate(0, 0); } - .clock-container.top h1 { - font-size: 15vw; + .clock-container.corner h1 { + font-size: 5vw; + } + + /* Base H1 transitions for smooth resizing */ + .clock-container h1 { + margin: 0; + transition: font-size 0.5s ease; } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor index 61edd16..afaaa21 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -1,5 +1,37 @@ - - +
+ +
+ +@code +{ + [Parameter] public DoorbellState State { get; set; } = DoorbellState.Hidden; + + public enum DoorbellState { Hidden, Preview, Motion, Ringing } +} + + \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor index 998c37b..90662ea 100644 --- a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor +++ b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor @@ -13,7 +13,7 @@ @inject LivingRoomWallPanelView LivingRoomWallPanelView - + @if (!_netDaemonInitialized) { @@ -35,24 +35,24 @@ else if (!_netDaemonConnected) } - + - + } - - + + @code { private bool _netDaemonInitialized; private bool _netDaemonConnected; - private bool _homeScreenActive; + private LivingRoomWallPanelView.States _viewState; private bool _clockWasClicked; protected override void OnInitialized() @@ -77,11 +77,22 @@ else if (!_netDaemonConnected) { LivingRoomWallPanelView.StateChangesWithCurrent().Subscribe(state => { - _homeScreenActive = state.New?.State == LivingRoomWallPanelView.States.Home; + if (state.New?.State == null) + { + return; + } + _viewState = state.New.State.Value; InvokeAsync(StateHasChanged); }); } + private Clock.ClockPosition GetClockPosition() => _viewState switch + { + LivingRoomWallPanelView.States.Doorbell => Clock.ClockPosition.Corner, + LivingRoomWallPanelView.States.Home => Clock.ClockPosition.Top, + _ => Clock.ClockPosition.Centered + }; + private void ClockClicked() { if (!_netDaemonConnected || LivingRoomWallPanelView.State == LivingRoomWallPanelView.States.Idle) From d3ec896508765f88022b420f207a657fc64ab791 Mon Sep 17 00:00:00 2001 From: Jasper Date: Sat, 11 Jul 2026 17:34:45 +0200 Subject: [PATCH 05/15] Progress on doorbell feed --- .../Components/Dashboard/Clock.razor | 21 ++-- .../Components/Dashboard/DoorbellButton.razor | 28 ++++++ .../Components/Dashboard/DoorbellStream.razor | 98 ++++++++++++++----- .../Components/Pages/Dashboard.razor | 18 +++- 4 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 src/CodeCasa.Dashboard/Components/Dashboard/DoorbellButton.razor diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor b/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor index 93e4315..0c89046 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor @@ -34,9 +34,10 @@ pointer-events: none; position: fixed; width: 100%; - transition: all 0.5s ease; text-align: center; left: 0; + /* Explicit transitions for smooth animation */ + transition: top 0.5s ease, left 0.5s ease, transform 0.5s ease; } .clock-container button { @@ -45,40 +46,38 @@ /* Centered State */ .clock-container.centered { - top: 50%; - transform: translateY(-50%); + top: 20px; + left: 0px; } .clock-container.centered h1 { - font-size: 30vw; + font-size: 144px; /* 30vw at 480px width */ } /* Top State */ .clock-container.top { top: 20px; - transform: translateY(0); + left: 0; } .clock-container.top h1 { - font-size: 15vw; + font-size: 72px; /* 15vw at 480px width */ } /* Corner State */ .clock-container.corner { top: 15px; - left: 15px; - width: auto; - text-align: left; - transform: translate(0, 0); + left: -180px; } .clock-container.corner h1 { - font-size: 5vw; + font-size: 24px; /* 5vw at 480px width */ } /* Base H1 transitions for smooth resizing */ .clock-container h1 { margin: 0; transition: font-size 0.5s ease; + white-space: nowrap; } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellButton.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellButton.razor new file mode 100644 index 0000000..5de2079 --- /dev/null +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellButton.razor @@ -0,0 +1,28 @@ +
+ +
+ +@code { + [Parameter] public bool Visible { get; set; } + [Parameter] public EventCallback OnClick { get; set; } +} + + diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor index afaaa21..ef9c5e4 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -1,16 +1,43 @@ -
- -
- -@code +@if (Visible) { - [Parameter] public DoorbellState State { get; set; } = DoorbellState.Hidden; +
+ @if (!isLoaded) + { +
+ +

Connecting to doorbell...

+
+ } + + +
+} + +@code { + [Parameter] public bool Visible { get; set; } + private bool isLoaded; + private bool previousVisible; - public enum DoorbellState { Hidden, Preview, Motion, Ringing } + // Resets the loading state whenever Visible changes from false to true + protected override void OnParametersSet() + { + if (Visible && !previousVisible) + { + isLoaded = false; + } + previousVisible = Visible; + } + + private void HandleIframeLoad() + { + isLoaded = true; + } } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor index 90662ea..1817390 100644 --- a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor +++ b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor @@ -43,10 +43,10 @@ else if (!_netDaemonConnected) } - + - + @code { private bool _netDaemonInitialized; @@ -104,8 +104,22 @@ else if (!_netDaemonConnected) LivingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Idle); } + private void DoorbellButtonClicked() + { + if (!_netDaemonConnected) + { + return; + } + + HaContext.SendEvent(Events.LivingRoomPanelViewDoorbellFeedEvent); + } + private void GlobalClicked() { + if (LivingRoomWallPanelView.State != LivingRoomWallPanelView.States.Idle) + { + return; + } if (_clockWasClicked) { // We don't want to fire the event if the clock was clicked (as it would immediately go to the home screen again). From 2f15b5d3e4b3736c500faf61478d3126538ce8bc Mon Sep 17 00:00:00 2001 From: Jasper Date: Sat, 11 Jul 2026 20:36:12 +0200 Subject: [PATCH 06/15] Adding loading time to doorbell control. --- .../Components/Dashboard/DoorbellStream.razor | 39 ++++++++++++------- .../Components/Pages/Dashboard.razor | 28 +++++++++++-- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor index ef9c5e4..cc4ad89 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -1,7 +1,7 @@ -@if (Visible) +@if (_load) {
- @if (!isLoaded) + @if (!_isLoaded && !_loadHidden) {
@@ -13,30 +13,41 @@ allow="autoplay; encrypted-media" frameborder="0" scrolling="no" - class="@(isLoaded ? "stream-visible" : "stream-hidden")" - @onload="HandleIframeLoad"> + class="@(_isLoaded ? "stream-visible" : "stream-hidden")" + @onload="HandleIframeLoadAsync">
} @code { - [Parameter] public bool Visible { get; set; } - private bool isLoaded; - private bool previousVisible; + [Parameter] public EventCallback OnVisible { get; set; } - // Resets the loading state whenever Visible changes from false to true - protected override void OnParametersSet() + private bool _load; + private bool _loadHidden; + private bool _isLoaded; + + public async Task LoadAsync(bool hidden) { - if (Visible && !previousVisible) + _load = true; + _loadHidden = hidden; + if (!hidden) { - isLoaded = false; + await OnVisible.InvokeAsync(); } - previousVisible = Visible; } - private void HandleIframeLoad() + public void Dispose() + { + _load = false; + } + + private async Task HandleIframeLoadAsync() { - isLoaded = true; + _isLoaded = true; + if (_loadHidden) + { + await OnVisible.InvokeAsync(); + } } } diff --git a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor index 1817390..0c9883b 100644 --- a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor +++ b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor @@ -43,17 +43,20 @@ else if (!_netDaemonConnected) } - + @code { + private DoorbellStream? _doorbellStream; + private bool _netDaemonInitialized; private bool _netDaemonConnected; private LivingRoomWallPanelView.States _viewState; private bool _clockWasClicked; + private bool _doorbellFeedWasClicked; protected override void OnInitialized() { @@ -81,11 +84,30 @@ else if (!_netDaemonConnected) { return; } - _viewState = state.New.State.Value; + var viewState = state.New.State.Value; + if (viewState == LivingRoomWallPanelView.States.Doorbell) + { + _doorbellStream?.LoadAsync(!_doorbellFeedWasClicked).GetAwaiter().GetResult(); + } + + _doorbellFeedWasClicked = false; + if (viewState != LivingRoomWallPanelView.States.Doorbell) + { + // If doorbell is requested, we don't want to change the view state until the doorbell stream is loaded, so we keep the view unchanged. + // The doorbell stream will call back to this component when it is loaded, and then we can change the view state. + _viewState = viewState; + } + InvokeAsync(StateHasChanged); }); } + private void DoorbellStreamVisible() + { + _viewState = LivingRoomWallPanelView.States.Doorbell; + InvokeAsync(StateHasChanged); + } + private Clock.ClockPosition GetClockPosition() => _viewState switch { LivingRoomWallPanelView.States.Doorbell => Clock.ClockPosition.Corner, @@ -110,7 +132,7 @@ else if (!_netDaemonConnected) { return; } - + _doorbellFeedWasClicked = true; HaContext.SendEvent(Events.LivingRoomPanelViewDoorbellFeedEvent); } From 7470aa8de819a748995b78eacfd559b8d9b6fb5e Mon Sep 17 00:00:00 2001 From: Jasper Date: Sat, 11 Jul 2026 20:49:22 +0200 Subject: [PATCH 07/15] Giving doorbell feed time to load. --- .../Components/Pages/Dashboard.razor | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor index 0c9883b..897331f 100644 --- a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor +++ b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor @@ -84,20 +84,23 @@ else if (!_netDaemonConnected) { return; } - var viewState = state.New.State.Value; - if (viewState == LivingRoomWallPanelView.States.Doorbell) - { - _doorbellStream?.LoadAsync(!_doorbellFeedWasClicked).GetAwaiter().GetResult(); - } + var doorbellFeedWasClicked = _doorbellFeedWasClicked; _doorbellFeedWasClicked = false; - if (viewState != LivingRoomWallPanelView.States.Doorbell) + var viewState = state.New.State.Value; + if (viewState == LivingRoomWallPanelView.States.Doorbell) { - // If doorbell is requested, we don't want to change the view state until the doorbell stream is loaded, so we keep the view unchanged. - // The doorbell stream will call back to this component when it is loaded, and then we can change the view state. - _viewState = viewState; + if (doorbellFeedWasClicked) + { + _doorbellStream?.LoadAsync(false).GetAwaiter().GetResult(); + return; + } + _doorbellStream?.LoadAsync(true).GetAwaiter().GetResult(); + InvokeAsync(StateHasChanged); + return; } + _viewState = viewState; InvokeAsync(StateHasChanged); }); } From 98548b7a4aeeafb3af441ae2ecebd43063a3bfe2 Mon Sep 17 00:00:00 2001 From: Jasper Date: Sat, 11 Jul 2026 21:01:27 +0200 Subject: [PATCH 08/15] Fixing doorbell streaming loading. --- .../Components/Dashboard/DoorbellStream.razor | 31 +++++++++++++------ .../Components/Pages/Dashboard.razor | 23 +++++++------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor index cc4ad89..bc78fb7 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -28,26 +28,37 @@ public async Task LoadAsync(bool hidden) { - _load = true; - _loadHidden = hidden; - if (!hidden) + await InvokeAsync(async () => { - await OnVisible.InvokeAsync(); - } + _load = true; + _loadHidden = hidden; + StateHasChanged(); + + if (!hidden) + { + await OnVisible.InvokeAsync(); + } + }); } - public void Dispose() + public async Task DisposeAsync() { _load = false; + await InvokeAsync(StateHasChanged); } private async Task HandleIframeLoadAsync() { - _isLoaded = true; - if (_loadHidden) + await InvokeAsync(async () => { - await OnVisible.InvokeAsync(); - } + _isLoaded = true; + StateHasChanged(); + + if (_loadHidden) + { + await OnVisible.InvokeAsync(); + } + }); } } diff --git a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor index 897331f..1972d44 100644 --- a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor +++ b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor @@ -78,30 +78,31 @@ else if (!_netDaemonConnected) private void NetDaemonInitialized() { - LivingRoomWallPanelView.StateChangesWithCurrent().Subscribe(state => + LivingRoomWallPanelView.StateChangesWithCurrent().Subscribe(async state => { - if (state.New?.State == null) - { - return; - } + if (state.New?.State == null) return; var doorbellFeedWasClicked = _doorbellFeedWasClicked; _doorbellFeedWasClicked = false; var viewState = state.New.State.Value; + if (viewState == LivingRoomWallPanelView.States.Doorbell) { - if (doorbellFeedWasClicked) + if (_doorbellStream != null) { - _doorbellStream?.LoadAsync(false).GetAwaiter().GetResult(); - return; + // Use await inside an async lambda instead of blocking .GetAwaiter().GetResult() + await _doorbellStream.LoadAsync(!doorbellFeedWasClicked); } - _doorbellStream?.LoadAsync(true).GetAwaiter().GetResult(); - InvokeAsync(StateHasChanged); return; } + if (_doorbellStream != null) + { + await _doorbellStream.DisposeAsync(); + } + _viewState = viewState; - InvokeAsync(StateHasChanged); + await InvokeAsync(StateHasChanged); }); } From 55e63e01a3f7f0b8f4ea833addac0ccb92db1ff3 Mon Sep 17 00:00:00 2001 From: Jasper Date: Mon, 13 Jul 2026 15:54:16 +0200 Subject: [PATCH 09/15] Adding fade in and making page logic cleaner. --- .../Components/Dashboard/DoorbellStream.razor | 61 +++++++++++++++++-- .../Components/Pages/Dashboard.razor | 5 +- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor index bc78fb7..797aa82 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -1,9 +1,9 @@ @if (_load) { -
+
@if (!_isLoaded && !_loadHidden) { -
+

Connecting to doorbell...

@@ -13,7 +13,7 @@ allow="autoplay; encrypted-media" frameborder="0" scrolling="no" - class="@(_isLoaded ? "stream-visible" : "stream-hidden")" + class="@GetStreamClass()" @onload="HandleIframeLoadAsync">
@@ -25,6 +25,9 @@ private bool _load; private bool _loadHidden; private bool _isLoaded; + private bool _fadeInPlaceholder; + private bool _fadeInStream; + private bool _fadeOut; public async Task LoadAsync(bool hidden) { @@ -32,10 +35,15 @@ { _load = true; _loadHidden = hidden; + _fadeOut = false; StateHasChanged(); if (!hidden) { + // Small delay to ensure the DOM is rendered before fading in + await Task.Delay(10); + _fadeInPlaceholder = true; + StateHasChanged(); await OnVisible.InvokeAsync(); } }); @@ -43,8 +51,20 @@ public async Task DisposeAsync() { - _load = false; - await InvokeAsync(StateHasChanged); + await InvokeAsync(async () => + { + _fadeOut = true; + StateHasChanged(); + + // Wait for fade-out animation to complete + await Task.Delay(500); + + _load = false; + _fadeInPlaceholder = false; + _fadeInStream = false; + _isLoaded = false; + StateHasChanged(); + }); } private async Task HandleIframeLoadAsync() @@ -56,10 +76,27 @@ if (_loadHidden) { + // Small delay to ensure the DOM is rendered before fading in + await Task.Delay(10); + _fadeInStream = true; + StateHasChanged(); await OnVisible.InvokeAsync(); } }); } + + private string GetStreamClass() + { + if (_loadHidden && _fadeInStream) + { + return "stream-visible"; + } + else if (!_loadHidden && _isLoaded) + { + return "stream-visible"; + } + return "stream-hidden"; + } } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor index 7c34616..fad272f 100644 --- a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -9,7 +9,8 @@
} -