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/BinarySensorEntities.cs b/src/CodeCasa.AutoGenerated/BinarySensorEntities.cs index 6e8ec33..ff2cedb 100644 --- a/src/CodeCasa.AutoGenerated/BinarySensorEntities.cs +++ b/src/CodeCasa.AutoGenerated/BinarySensorEntities.cs @@ -17,6 +17,10 @@ public IEnumerable EnumerateAll() => _haContext.GetAllEntiti ///Bathroom Motion Sensor Occupancy public BinarySensorEntity BathroomMotionSensorOccupancy => new(_haContext, "binary_sensor.bathroom_motion_sensor_occupancy"); + ///Front Door Doorbell Person + public BinarySensorEntity FrontDoorFrontDoorDoorbellPerson => new(_haContext, "binary_sensor.front_door_front_door_doorbell_person"); + ///Front Door Doorbell Visitor + public BinarySensorEntity FrontDoorFrontDoorDoorbellVisitor => new(_haContext, "binary_sensor.front_door_front_door_doorbell_visitor"); ///Office Motion Sensor Occupancy public BinarySensorEntity OfficeMotionSensorOccupancy => new(_haContext, "binary_sensor.office_motion_sensor_occupancy"); ///Upstairs Hallway Attic Motion Sensor Occupancy diff --git a/src/CodeCasa.AutoGenerated/InputSelectEntities.cs b/src/CodeCasa.AutoGenerated/InputSelectEntities.cs index 8325bba..645699a 100644 --- a/src/CodeCasa.AutoGenerated/InputSelectEntities.cs +++ b/src/CodeCasa.AutoGenerated/InputSelectEntities.cs @@ -23,6 +23,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"); } \ No newline at end of file diff --git a/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs b/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs index 181712f..425c2f1 100644 --- a/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs +++ b/src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs @@ -1,21 +1,35 @@ using CodeCasa.AutoGenerated; -using CodeCasa.NetDaemon.Utilities.Extensions; +using CodeCasa.CustomEntities.Core.Doorbell; +using CodeCasa.CustomEntities.Core.Events; +using CodeCasa.CustomEntities.Core.GoogleHome; +using CodeCasa.CustomEntities.Core.InputSelect; using NetDaemon.AppModel; using NetDaemon.HassModel; -using Reactive.Boolean; +using NetDaemon.HassModel.Entities; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Text.Json.Serialization; -using CodeCasa.CustomEntities.Core.Events; -using CodeCasa.CustomEntities.Core.GoogleHome; -using CodeCasa.CustomEntities.Core.InputSelect; -using NetDaemon.HassModel.Entities; +using CodeCasa.NetDaemon.Extensions.Observables; +using CodeCasa.NetDaemon.Utilities.Extensions; +using Reactive.Boolean; namespace CodeCasa.Automations.Apps.Dashboard { [NetDaemonApp] internal class LivingRoomPanelNavigation { + private readonly IHaContext _context; + private readonly AutomationEntities _automationEntities; + private readonly InputSelectEntities _inputSelectEntities; + private readonly GoogleHomeAlarmEntities _googleHomeAlarmEntities; + private readonly GoogleHomeTimerEntities _googleHomeTimerEntities; + private readonly LivingRoomWallPanelView _livingRoomWallPanelView; + private readonly FrontDoorDoorbell _frontDoorDoorbell; + private readonly IScheduler _scheduler; + private bool _hasInfoToDisplay; + private DateTime _currentStateActivated = DateTime.MinValue; + private IDisposable? _scheduleDisposable; + public LivingRoomPanelNavigation( IHaContext context, AutomationEntities automationEntities, @@ -23,35 +37,116 @@ public LivingRoomPanelNavigation( GoogleHomeAlarmEntities googleHomeAlarmEntities, GoogleHomeTimerEntities googleHomeTimerEntities, LivingRoomWallPanelView livingRoomWallPanelView, + FrontDoorDoorbell frontDoorDoorbell, IScheduler scheduler) { - var panelInteraction = context.Events.Filter(Events.LivingRoomPanelInteractionEvent).Select(_ => true); - var proximityDetected = context.Events + _context = context; + _automationEntities = automationEntities; + _inputSelectEntities = inputSelectEntities; + _googleHomeAlarmEntities = googleHomeAlarmEntities; + _googleHomeTimerEntities = googleHomeTimerEntities; + _livingRoomWallPanelView = livingRoomWallPanelView; + _frontDoorDoorbell = frontDoorDoorbell; + _scheduler = scheduler; + + livingRoomWallPanelView.StateChangesWithCurrent().Subscribe(_ => + { + DetermineDesiredStateAndActivateWhenAppropriate(); + }); + + SubscribeToProximity(); + SubscribeToInfo(); + SubscribeToDoorbell(); + } + + private LivingRoomWallPanelView.States DetermineDesiredState() + { + if (_frontDoorDoorbell.BellPressed.IsOn() || _frontDoorDoorbell.PersonDetected.IsOn()) + { + return LivingRoomWallPanelView.States.Doorbell; + } + + if (_hasInfoToDisplay) + { + return LivingRoomWallPanelView.States.Home; + } + + return LivingRoomWallPanelView.States.Idle; + } + + private void DetermineDesiredStateAndActivateWhenAppropriate() + { + var desiredState = DetermineDesiredState(); + if (desiredState == LivingRoomWallPanelView.States.Doorbell) + { + ActivateStateAfterMinimumTimeHasPassed(LivingRoomWallPanelView.States.Doorbell, TimeSpan.Zero); + // Should not occur, but just in case to prevent overheating. + ActivateStateAfterMinimumTimeHasPassed(LivingRoomWallPanelView.States.Idle, TimeSpan.FromMinutes(5)); + return; + } + if (desiredState == LivingRoomWallPanelView.States.Home) + { + if (_livingRoomWallPanelView.State == LivingRoomWallPanelView.States.Idle) + { + ActivateStateAfterMinimumTimeHasPassed(desiredState, TimeSpan.FromSeconds(3)); + return; + } + } + ActivateStateAfterMinimumTimeHasPassed(desiredState, TimeSpan.FromSeconds(20)); + } + + private void ActivateStateAfterMinimumTimeHasPassed(LivingRoomWallPanelView.States desiredState, + TimeSpan minimumTime) + { + _scheduleDisposable?.Dispose(); + _scheduleDisposable = null; + if (DateTime.UtcNow - _currentStateActivated < minimumTime) + { + _scheduleDisposable = _scheduler.Schedule(minimumTime - (DateTime.UtcNow - _currentStateActivated), () => + { + _currentStateActivated = DateTime.UtcNow; + _livingRoomWallPanelView.SelectOption(desiredState); + }); + return; + } + _currentStateActivated = DateTime.UtcNow; + _livingRoomWallPanelView.SelectOption(desiredState); + } + + private void SubscribeToProximity() + { + _context.Events .Filter(Events.AutomationTriggeredEvent) - .Where(e => e.Data?.EntityId == automationEntities.WebhookLivingRoomWallPanelProximityDetected.EntityId) - .Select(_ => true); - var anyInteraction = panelInteraction.Merge(proximityDetected); - var recentInteraction = anyInteraction - .Merge( - anyInteraction - .Throttle(TimeSpan.FromSeconds(30)) - .Select(_ => false) - ).Prepend(false); + .Where(e => e.Data?.EntityId == _automationEntities.WebhookLivingRoomWallPanelProximityDetected.EntityId) + .Select(_ => true) + .Subscribe(_ => + { + if (_livingRoomWallPanelView.State == LivingRoomWallPanelView.States.Idle) + { + ActivateStateAfterMinimumTimeHasPassed(LivingRoomWallPanelView.States.Home, TimeSpan.Zero); + } + }); + } + private void SubscribeToInfo() + { var notificationCount = - inputSelectEntities.LivingRoomPanelNotifications.StateAllChangesWithCurrent().Select(s => string.IsNullOrEmpty(s.New?.Attributes?.Options?.FirstOrDefault()) ? 0 : s.New.Attributes.Options.Count); - var alarmCount = googleHomeAlarmEntities.Select( - gt => gt - .StateAllChangesDeserialized()) + _inputSelectEntities.LivingRoomPanelNotifications.StateAllChangesWithCurrent().Select(s => + string.IsNullOrEmpty(s.New?.Attributes?.Options?.FirstOrDefault()) + ? 0 + : s.New.Attributes.Options.Count); + var alarmCount = _googleHomeAlarmEntities.Select(gt => gt + .StateAllChangesDeserialized()) .CombineLatest() .Select(speakerInfos => speakerInfos.Sum(s => s.Alarms?.Count ?? 0)); - var timerCount = googleHomeTimerEntities.Select( - gt => gt - .StateAllChangesDeserialized()) + var timerCount = _googleHomeTimerEntities.Select(gt => gt + .StateAllChangesDeserialized()) .CombineLatest() .Select(speakerInfos => speakerInfos.Sum(s => s.Timers?.Count ?? 0)); var infoCount = notificationCount.CombineLatest(alarmCount, timerCount, (c1, c2, c3) => c1 + c2 + c3); - var infoToDisplay = infoCount + + infoCount.Subscribe(c => _hasInfoToDisplay = c > 0); + infoCount .Scan( (prev: 0, curr: 0), (state, newCount) => (prev: state.curr, curr: newCount) @@ -62,11 +157,14 @@ public LivingRoomPanelNavigation( if (state.curr > state.prev) return true; return (bool?)null; }) - .Where(b => b != null).Select(b => b!.Value); + .Where(b => b != null).Subscribe(_ => DetermineDesiredStateAndActivateWhenAppropriate()); + } - recentInteraction.Or(infoToDisplay, OperatorDistinctness.NotDistinct).SubscribeTrueFalse( - () => livingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Home), - () => livingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Idle)); + private void SubscribeToDoorbell() + { + var personDetected = _frontDoorDoorbell.PersonDetected.ToBooleanObservable(); + var bellPressed = _frontDoorDoorbell.BellPressed.ToBooleanObservable(); + bellPressed.Or(personDetected).Subscribe(_ => DetermineDesiredStateAndActivateWhenAppropriate()); } // ReSharper disable once ClassNeverInstantiated.Local 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 @@ - 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..d30863a --- /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(BinarySensorEntities binarySensorEntities) + : ReolinkDoorbell(binarySensorEntities.FrontDoorFrontDoorDoorbellPerson, binarySensorEntities.FrontDoorFrontDoorDoorbellVisitor); +} diff --git a/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs b/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs new file mode 100644 index 0000000..03b2ac6 --- /dev/null +++ b/src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs @@ -0,0 +1,10 @@ +using CodeCasa.AutoGenerated; + +namespace CodeCasa.CustomEntities.Core.Doorbell +{ + public abstract class ReolinkDoorbell(BinarySensorEntity personDetected, BinarySensorEntity bellPressed) + { + public BinarySensorEntity PersonDetected { get; } = personDetected; + public BinarySensorEntity BellPressed { get; } = bellPressed; + } +} 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 fdeaab5..1a58680 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,12 +12,30 @@ public static IServiceCollection AddCodeCasaCustomCoreEntities(this IServiceColl { return serviceCollection + .AddTransientImplementationsOf() + .AddTransient() .AddTransient() // Input Select Entities .AddTransient() - + .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.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/App.razor b/src/CodeCasa.Dashboard/Components/App.razor index 8d1b601..c0dc3aa 100644 --- a/src/CodeCasa.Dashboard/Components/App.razor +++ b/src/CodeCasa.Dashboard/Components/App.razor @@ -7,10 +7,10 @@ - + + - 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/Clock.razor b/src/CodeCasa.Dashboard/Components/Dashboard/Clock.razor index b0047eb..0c89046 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; @@ -28,31 +34,50 @@ 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 { pointer-events: auto; } - .clock-container.center { - top: 50%; - transform: translateY(-50%); + /* Centered State */ + .clock-container.centered { + top: 20px; + left: 0px; } + .clock-container.centered h1 { + font-size: 144px; /* 30vw at 480px width */ + } + + /* Top State */ .clock-container.top { top: 20px; - transform: translateY(0); + left: 0; } - .clock-container h1 { - margin: 0; - transition: font-size 1s ease; - font-size: 30vw; + .clock-container.top h1 { + font-size: 72px; /* 15vw at 480px width */ + } + + /* Corner State */ + .clock-container.corner { + top: 15px; + left: -180px; } - .clock-container.top h1 { - font-size: 15vw; + .clock-container.corner h1 { + 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/DoorbellIcon.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellIcon.razor new file mode 100644 index 0000000..ce7ff04 --- /dev/null +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellIcon.razor @@ -0,0 +1,72 @@ + +
+ +
+ +@code { + + [Parameter] public bool Visible { get; set; } = true; +} + + \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor new file mode 100644 index 0000000..115f024 --- /dev/null +++ b/src/CodeCasa.Dashboard/Components/Dashboard/DoorbellStream.razor @@ -0,0 +1,165 @@ +@inject IConfiguration Configuration + +@if (_load) +{ +
+ @if (!_isLoaded && !_loadHidden) + { +
+ +

Connecting to doorbell...

+
+ } + + +
+} + +@code { + [Parameter] public EventCallback OnVisible { get; set; } + [Parameter] public string? StreamUrl { get; set; } + + private bool _load; + private bool _loadHidden; + private bool _isLoaded; + private bool _fadeIn; + private bool _fadeOut; + + private string GetStreamUrl() => StreamUrl ?? Configuration["Doorbell:StreamUrl"] ?? string.Empty; + + public async Task LoadAsync(bool hidden) + { + await InvokeAsync(async () => + { + _load = true; + _loadHidden = hidden; + _fadeOut = false; + StateHasChanged(); + // Small delay to allow DOM insertion before triggering CSS transitions + await Task.Delay(10); + _fadeIn = true; + if (!hidden) + { + StateHasChanged(); + await OnVisible.InvokeAsync(); + } + else + { + StateHasChanged(); + } + }); + } + + public async Task DisposeAsync() + { + await InvokeAsync(async () => + { + _fadeOut = true; + StateHasChanged(); + + // Wait for fade-out animation to complete + await Task.Delay(500); + + _load = false; + _fadeIn = false; + _fadeOut = false; + _isLoaded = false; + StateHasChanged(); + }); + } + + private async Task HandleIframeLoadAsync() + { + // Note: this extra delay prevents the "pause" icon from being visible on my ns-panel pro. This is not required for all clients. + await Task.Delay(500); + + await InvokeAsync(async () => + { + _isLoaded = true; + StateHasChanged(); + + if (_loadHidden) + { + await Task.Delay(10); + await OnVisible.InvokeAsync(); + } + }); + } + + private string GetStreamClass() + { + return _isLoaded ? "stream-visible" : "stream-hidden"; + } + + private string GetContainerClass() + { + // Note: we use background-color: transparent when _loadHidden is true to prevent a black background while loading. + return $"doorbell-stream-container {(_fadeIn ? "fade-in" : "")} {(_fadeOut ? "fade-out" : "")} background-color: {(_loadHidden ? "transparent" : "black")}"; + } + +} + + \ 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..d682e9c 100644 --- a/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor +++ b/src/CodeCasa.Dashboard/Components/Pages/Dashboard.razor @@ -1,19 +1,21 @@ @page "/" -@using CodeCasa.CustomEntities.Core.Events +@using System.Reactive +@using System.Reactive.Linq +@using CodeCasa.CustomEntities.Core.Doorbell @using CodeCasa.CustomEntities.Core.InputSelect @using CodeCasa.Dashboard.Components.Demo @using CodeCasa.Dashboard.Components.Dashboard +@using CodeCasa.NetDaemon.Extensions.Observables @using CodeCasa.NetDaemon.RuntimeState -@using global::NetDaemon.HassModel @using global::NetDaemon.HassModel.Entities -@inject IHaContext HaContext @inject IWebHostEnvironment Env @inject NetDaemonRuntimeStateService NetDaemonConnectionStateService @inject LivingRoomWallPanelView LivingRoomWallPanelView +@inject FrontDoorDoorbell FrontDoorDoorbell - + @if (!_netDaemonInitialized) { @@ -35,24 +37,30 @@ else if (!_netDaemonConnected) } - + - + } - - - + + + + + @code { + private DoorbellStream? _doorbellStream; + private bool _netDaemonInitialized; private bool _netDaemonConnected; - private bool _homeScreenActive; + private LivingRoomWallPanelView.States _viewState; private bool _clockWasClicked; + private bool _doorbellFeedWasClicked; + private bool _doorbellPressed; protected override void OnInitialized() { @@ -74,13 +82,56 @@ else if (!_netDaemonConnected) private void NetDaemonInitialized() { - LivingRoomWallPanelView.StateChangesWithCurrent().Subscribe(state => + LivingRoomWallPanelView.StateChangesWithCurrent() + .SelectMany(async state => + { + if (state.New?.State == null) return Unit.Default; + + var doorbellFeedWasClicked = _doorbellFeedWasClicked; + _doorbellFeedWasClicked = false; + var viewState = state.New.State.Value; + + if (viewState == LivingRoomWallPanelView.States.Doorbell) + { + if (_doorbellStream != null) + { + await _doorbellStream.LoadAsync(!doorbellFeedWasClicked); + } + + return Unit.Default; + } + + _viewState = viewState; + if (_doorbellStream != null) + { + await _doorbellStream.DisposeAsync(); + } + + return Unit.Default; + }) + .Subscribe(_ => InvokeAsync(StateHasChanged) + ); + + FrontDoorDoorbell.BellPressed.ToOnOffObservable().Subscribe(pressed => { - _homeScreenActive = state.New?.State == LivingRoomWallPanelView.States.Home; + _doorbellPressed = pressed; InvokeAsync(StateHasChanged); }); } + private void DoorbellStreamVisible() + { + _viewState = LivingRoomWallPanelView.States.Doorbell; + 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) @@ -92,8 +143,25 @@ else if (!_netDaemonConnected) LivingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Idle); } + private void DoorbellButtonClicked() + { + if (!_netDaemonConnected) + { + return; + } + + _doorbellFeedWasClicked = true; + LivingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Doorbell); + } + private void GlobalClicked() { + if (LivingRoomWallPanelView.State != LivingRoomWallPanelView.States.Idle) + { + _clockWasClicked = false; + 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). @@ -102,6 +170,8 @@ else if (!_netDaemonConnected) _clockWasClicked = false; return; } - HaContext.SendEvent(Events.LivingRoomPanelInteractionEvent); + + LivingRoomWallPanelView.SelectOption(LivingRoomWallPanelView.States.Home); } + } diff --git a/src/CodeCasa.Dashboard/appsettings.Development.example.json b/src/CodeCasa.Dashboard/appsettings.Development.example.json index a696813..2f67a4e 100644 --- a/src/CodeCasa.Dashboard/appsettings.Development.example.json +++ b/src/CodeCasa.Dashboard/appsettings.Development.example.json @@ -3,5 +3,8 @@ "HomeAssistant": { "Host": "Enter your home assistant ip here to test this example. e.g. 192.168.0.123", "Token": "Enter your home assistant token here to test this example." + }, + "Doorbell": { + "StreamUrl": "Enter your doorbell stream url here to test this example. e.g. http://192.168.0.123:1984/webrtc.html?src=doorbell_sub&media=video" } } \ No newline at end of file diff --git a/src/CodeCasa.Dashboard/wwwroot/css/material-symbols/material-symbols-outlined.woff2 b/src/CodeCasa.Dashboard/wwwroot/css/material-symbols/material-symbols-outlined.woff2 new file mode 100644 index 0000000..0d5e6c9 Binary files /dev/null and b/src/CodeCasa.Dashboard/wwwroot/css/material-symbols/material-symbols-outlined.woff2 differ diff --git a/src/CodeCasa.Dashboard/wwwroot/css/material-symbols/material-symbols.css b/src/CodeCasa.Dashboard/wwwroot/css/material-symbols/material-symbols.css new file mode 100644 index 0000000..df5dab0 --- /dev/null +++ b/src/CodeCasa.Dashboard/wwwroot/css/material-symbols/material-symbols.css @@ -0,0 +1,22 @@ +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + /* Matches the file name from marella's repository */ + src: url('material-symbols-outlined.woff2') format('woff2'); +} + +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Bold.ttf b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..6516185 Binary files /dev/null and b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Bold.ttf differ diff --git a/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Light.ttf b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Light.ttf new file mode 100644 index 0000000..ee7268f Binary files /dev/null and b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Light.ttf differ diff --git a/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Medium.ttf b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Medium.ttf new file mode 100644 index 0000000..bc5b170 Binary files /dev/null and b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Medium.ttf differ diff --git a/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Regular.ttf b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..3db0d1f Binary files /dev/null and b/src/CodeCasa.Dashboard/wwwroot/css/roboto/Roboto-Regular.ttf differ diff --git a/src/CodeCasa.Dashboard/wwwroot/css/roboto/roboto.css b/src/CodeCasa.Dashboard/wwwroot/css/roboto/roboto.css new file mode 100644 index 0000000..fb52cf1 --- /dev/null +++ b/src/CodeCasa.Dashboard/wwwroot/css/roboto/roboto.css @@ -0,0 +1,31 @@ +/* Light */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: url('Roboto-Light.ttf') format('truetype'); +} + +/* Regular */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: url('Roboto-Regular.ttf') format('truetype'); +} + +/* Medium */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: url('Roboto-Medium.ttf') format('truetype'); +} + +/* Bold */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + src: url('Roboto-Bold.ttf') format('truetype'); +}