Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,4 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
/BuildAndPublishDashboard.ps1
4 changes: 2 additions & 2 deletions Dockerfile.automations
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 .

Expand Down
4 changes: 2 additions & 2 deletions Dockerfile.dashboard
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 .

Expand Down
4 changes: 4 additions & 0 deletions src/CodeCasa.AutoGenerated/BinarySensorEntities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public IEnumerable<BinarySensorEntity> EnumerateAll() => _haContext.GetAllEntiti

///<summary>Bathroom Motion Sensor Occupancy</summary>
public BinarySensorEntity BathroomMotionSensorOccupancy => new(_haContext, "binary_sensor.bathroom_motion_sensor_occupancy");
///<summary>Front Door Doorbell Person</summary>
public BinarySensorEntity FrontDoorFrontDoorDoorbellPerson => new(_haContext, "binary_sensor.front_door_front_door_doorbell_person");
///<summary>Front Door Doorbell Visitor</summary>
public BinarySensorEntity FrontDoorFrontDoorDoorbellVisitor => new(_haContext, "binary_sensor.front_door_front_door_doorbell_visitor");
///<summary>Office Motion Sensor Occupancy</summary>
public BinarySensorEntity OfficeMotionSensorOccupancy => new(_haContext, "binary_sensor.office_motion_sensor_occupancy");
///<summary>Upstairs Hallway Attic Motion Sensor Occupancy</summary>
Expand Down
2 changes: 2 additions & 0 deletions src/CodeCasa.AutoGenerated/InputSelectEntities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public InputSelectEntities(IHaContext haContext)
public InputSelectEntity JasperState => new(_haContext, "input_select.jasper_state");
///<summary>Living Room Panel Notifications</summary>
public InputSelectEntity LivingRoomPanelNotifications => new(_haContext, "input_select.living_room_panel_notifications");
///<summary>Variable Doorbell State</summary>
public InputSelectEntity VariableDoorbellState => new(_haContext, "input_select.variable_doorbell_state");
///<summary>Variable Living Room Wall Panel View</summary>
public InputSelectEntity VariableLivingRoomWallPanelView => new(_haContext, "input_select.variable_living_room_wall_panel_view");
}
156 changes: 127 additions & 29 deletions src/CodeCasa.Automations/Apps/Dashboard/LivingRoomPanelNavigation.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,152 @@
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,
InputSelectEntities inputSelectEntities,
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<AutomationTriggeredEventData>(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<SpeakerAlarmInfo>())
_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<SpeakerAlarmInfo>())
.CombineLatest()
.Select(speakerInfos => speakerInfos.Sum(s => s.Alarms?.Count ?? 0));
var timerCount = googleHomeTimerEntities.Select(
gt => gt
.StateAllChangesDeserialized<SpeakerTimerInfo>())
var timerCount = _googleHomeTimerEntities.Select(gt => gt
.StateAllChangesDeserialized<SpeakerTimerInfo>())
.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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/CodeCasa.Automations/CodeCasa.Automations.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="CodeCasa.AutomationPipelines.Lights.NetDaemon" Version="1.22.0" />
<PackageReference Include="CodeCasa.AutomationPipelines.Lights.Mqtt" Version="1.22.0" />
<PackageReference Include="CodeCasa.Lights.Timelines" Version="1.22.0" />
<PackageReference Include="CodeCasa.NetDaemon.Extensions.Observables" Version="1.22.0" />
Expand All @@ -41,7 +42,6 @@
<ItemGroup>
<ProjectReference Include="..\CodeCasa.AutoGenerated\CodeCasa.AutoGenerated.csproj" />
<ProjectReference Include="..\CodeCasa.CustomEntities.Automation\CodeCasa.CustomEntities.Automation.csproj" />
<ProjectReference Include="..\CodeCasa.Mqtt\CodeCasa.Mqtt.csproj" />
<ProjectReference Include="..\CodeCasa.NetDaemon.Utilities\CodeCasa.NetDaemon.Utilities.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddCodeCasaCustomAutomationEntities(this IServiceCollection serviceCollection)
{
serviceCollection

.AddCodeCasaCustomCoreEntities()
.AddLightNotifications();

Expand All @@ -39,8 +38,7 @@ public static IServiceCollection AddCodeCasaCustomAutomationEntities(this IServi
.AddTransient<JasperPhoneNotifications>()

// People
.AddTransient<Jane>()
.AddTransient<Jasper>()
.AddTransientImplementationsOf<CompositePersonEntity>()
.AddTransient<PeopleEntities>();

return serviceCollection;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using CodeCasa.AutoGenerated;

namespace CodeCasa.CustomEntities.Core.Doorbell
{
public class FrontDoorDoorbell(BinarySensorEntities binarySensorEntities)
: ReolinkDoorbell(binarySensorEntities.FrontDoorFrontDoorDoorbellPerson, binarySensorEntities.FrontDoorFrontDoorDoorbellVisitor);
}
10 changes: 10 additions & 0 deletions src/CodeCasa.CustomEntities.Core/Doorbell/ReolinkDoorbell.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions src/CodeCasa.CustomEntities.Core/Events/Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,12 +12,30 @@ public static IServiceCollection AddCodeCasaCustomCoreEntities(this IServiceColl
{
return serviceCollection

.AddTransientImplementationsOf<ReolinkDoorbell>()

.AddTransient<GoogleHomeAlarmEntities>()
.AddTransient<GoogleHomeTimerEntities>()

// Input Select Entities
.AddTransient<LivingRoomWallPanelView>()

.AddTransient<ForecastHome>();
}

private static IServiceCollection AddTransientImplementationsOf<TBase>(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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public LivingRoomWallPanelView(InputSelectEntities inputSelectEntities)
public enum States
{
Idle,
Home
Home,
Doorbell
}
}
4 changes: 2 additions & 2 deletions src/CodeCasa.Dashboard/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
<base href="/" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["CodeCasa.Dashboard.styles.css"]" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="css/roboto/roboto.css" rel="stylesheet" />
<link href="css/material-symbols/material-symbols.css" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/Z.Blazor.Diagrams/style.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading