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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/automation/appsettings.json
/automation/config.json
/automation/bin
/automation/obj
/automation/.config
Expand All @@ -8,4 +9,6 @@
/automation/Dockerfile
/automation/NetDaemonCodegen
/automation/Release
/TestAutomation/bin
/TestAutomation/obj
.idea
129 changes: 129 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Plan: Per-persoon aanwezigheid (Vincent weg / Carleen weg / beide weg)

## Doel
Aparte aanwezigheidssensoren voor Vincent en Carleen introduceren, zodat het systeem
onderscheid maakt tussen drie scenario's — **Vincent weg**, **Carleen weg** en **beide weg** —
en per scenario passende acties uitvoert.

## Vastgelegde beslissingen
- **Sensoren:** twee HA-helpers, handmatig door Vincent aangemaakt:
`input_boolean.vincent_away` en `input_boolean.carleen_away`.
- **`input_boolean.away` = beide weg (afgeleid):** `away = vincent_away && carleen_away`.
Bestaande code die op `away` leunt blijft werken.
- **Acties verschillen per scenario.**
- **Notificaties:** wél een melding wanneer **Vincent** weggaat; **geen** melding voor Carleen.
- **Hue-knop in de hal:** betekent **iedereen weg** (forceert beide weg).
- **`IsNightMode`-fix wordt in deze klus meegenomen** (woonkamer niet in nightmode louter
omdat Carleen slaapt).

---

## Huidige situatie (waarom dit nodig is)
- Er is nu maar één presence-begrip: `input_boolean.away`.
- `Vincent.IsHome` is afgeleid van `away` (`automation/Models/Persons/VincentModel.cs:20`),
dus "Vincent" en "het huis" zijn nu hetzelfde concept.
- `Carleen.IsHome` komt los van `person.carleen` (`automation/Models/Persons/CarleenModel.cs:20`).
- `away` wordt verspreid handmatig aan/uit gezet:
- `automation/apps/General/AwayManager.cs` (`VincentHomeHandler`, `AutoAway`, `ExecuteAwayActions`)
- `automation/apps/Rooms/Hall/HallLightOnMovement.cs:123-126`
- `automation/apps/General/SleepManager.cs:123`
- `IsNightMode` (`automation/apps/BaseApp.cs:53`):
`Vincent.IsSleeping || (Carleen.IsHome && Carleen.IsSleeping)` — zet de woonkamer in
nachtmodus zodra Carleen slaapt, ook als Vincent wakker en actief is.

---

## Stap 1 — HA helpers (door Vincent)
Aanmaken in Home Assistant:
- `input_boolean.vincent_away`
- `input_boolean.carleen_away`

Daarna `dotnet tool run nd-codegen` draaien zodat ze in `HomeAssistantGenerated.cs` verschijnen.

## Stap 2 — Centrale presence-enum
Nieuw bestand `automation/Enum/PresenceScenario.cs`:
```csharp
public enum PresenceScenario { BothHome, VincentAwayOnly, CarleenAwayOnly, BothAway }
```
Maakt de actiematrix expliciet en testbaar, los van de `away`-boolean.

## Stap 3 — PersonModels ontkoppelen van `away`
- **`VincentModel.cs`** — `IsHome` afleiden van `input_boolean.vincent_away` (geïnverteerd),
niet meer van `away`.
- **`CarleenModel.cs`** — `IsHome` afleiden van `input_boolean.carleen_away` (geïnverteerd).

> Gedragswijziging: alle bestaande `Vincent.IsHome`-gebruikers gaan op de nieuwe bron werken.
> Na te lopen: `Alarm.cs:48`, `HallLightOnMovement`, `BathRoomLights.cs:224`, `BaseApp` (IsNightMode).

## Stap 4 — Nieuwe `PresenceManager` app
Nieuw: `automation/apps/General/PresenceManager.cs` (inherit `BaseApp`).
Verantwoordelijk voor het **zetten** van de per-persoon-booleans en het afleiden van `away`:

1. **Per-persoon away zetten** op basis van `person.*` + de auto-away distance-logica
(verplaatst vanuit `AwayManager.AutoAway`):
- `person.vincent → home` ⇒ `vincent_away` uit; weg + `away_from` ⇒ aan.
- idem voor Carleen (`person.carleen` / `device_tracker.carleen_mobiel`).
2. **`away` afleiden** (de "beide weg"-regel, op één centrale plek):
```
away = vincent_away && carleen_away
```
3. **Scenario bepalen** → `PresenceScenario`; bij wijziging de bijbehorende actie triggeren (stap 5).

## Stap 5 — Actiematrix per scenario

| Scenario | Acties |
|---|---|
| **Beide thuis** | Normale automatisering; niets uitzetten. |
| **Vincent weg, Carleen thuis** | Geen lampen/tv uit. **Melding naar Vincent** ("Tot ziens" / op kantoordag vóór 09:00 "Werkse Vincent"). Carleen stuurt woonkamer-/nightmode-logica. |
| **Carleen weg, Vincent thuis** | Geen lampen/tv uit. **Geen melding.** `IsNightMode` valt terug op alleen Vincent. |
| **Beide weg** | Echte away-acties: `Light.TurnAllOff()`, tv + soundbar uit, alarm-condities. (Melding is al verstuurd op het moment dat Vincent als laatste wegging.) |

De huidige inhoud van `ExecuteAwayActions` (`AwayManager.cs:187-210`) verhuist hiernaartoe:
- De Vincent-vertrekmelding hangt aan de overgang **naar `vincent_away`**, niet aan `away`.
- De `Light.TurnAllOff()` / tv / soundbar hangen aan de overgang **naar `BothAway`**.

## Stap 6 — `AwayManager` afslanken
Behoudt de **welcome-home state machine** (Returning → WelcomingHome → Home), maar:
- `AutoAway` en `VincentHomeHandler` (het zetten van `away`) verhuizen naar `PresenceManager`.
- `ExecuteAwayActions`-inhoud verhuist naar de scenario-matrix.
- De Carleen-slaap-onderdrukking (`AwayManager.cs:189-195`) + `ScheduleCarleenWakeUp` blijven,
maar haken op het nieuwe presence-model.

## Stap 7 — `IsNightMode`-fix (woonkamer)
`BaseApp.cs:53` opsplitsen zodat de woonkamer niet in nachtmodus gaat puur omdat Carleen slaapt.
Voorstel:
- `IsNightMode` (huisbreed/persoonlijk) blijft `Vincent.IsSleeping || (Carleen.IsHome && Carleen.IsSleeping)`.
- Nieuwe property voor ruimtes waar alleen Vincents slaap telt, bv.
`protected bool IsVincentNightMode => Vincent.IsSleeping;`
- `LivingRoomLights.cs:34,40` gaat `IsVincentNightMode` gebruiken i.p.v. `IsNightMode`,
zodat de woonkamer blijft reageren als Vincent wakker is terwijl Carleen slaapt.
- Slaapkamer-nabije ruimtes (hal, badkamer) houden het bestaande gedrag — die hebben al
een office-day-uitzondering (`HallLightOnMovement`, `BathRoomLights`).

## Stap 8 — Losse plekken bijwerken
- **`HallLightOnMovement.cs:123-126`** (Hue-knop): ombouwen naar **iedereen weg** —
zet zowel `vincent_away` als `carleen_away` aan (i.p.v. direct `away`). Tweede druk /
thuiskomst zet ze weer uit.
- **`SleepManager.cs:123`** (`away` uit bij wakker worden): controleren/aanpassen zodat dit
via de per-persoon-booleans loopt.

## Stap 9 — Tests
- `TestAutomation/Apps/General/PresenceManagerTests.cs` (nieuw): tabel-test over de vier
scenario's, de `away`-afleiding en de Vincent-vertrekmelding.
- Bestaande `AwayManager`-tests bijwerken voor de afgeslankte verantwoordelijkheid.
- `VincentModel`/`CarleenModel`-tests: `IsHome` volgt nu de nieuwe booleans.
- Test voor `IsVincentNightMode` + woonkamergedrag (Vincent wakker / Carleen slaapt).

---

## Volgorde van uitvoeren
1. Vincent: helpers in HA + `nd-codegen`.
2. Enum + PersonModels ontkoppelen.
3. `PresenceManager` (zetten + afleiden + matrix).
4. `AwayManager` afslanken.
5. Losse plekken (Hall-knop, Sleep) + `IsNightMode`-fix.
6. Tests.

## Blokkade
Stap 2+ kunnen pas zodra de HA-helpers bestaan en `nd-codegen` is gedraaid
(`Entities.InputBoolean.VincentAway` / `CarleenAway` moeten gegenereerd zijn).
64 changes: 64 additions & 0 deletions TestAutomation/Apps/General/PresenceManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Automation.apps.General;
using NSubstitute;
using TestAutomation.Helpers;
using Xunit;

namespace TestAutomation.Apps.General;

public class PresenceManagerTests
{
private static AppTestContext Arrange(string awayVincent, string awayCarleen, string away)
{
var ctx = AppTestContext.NewWithScheduler();
ctx.HaContext.GetState("input_boolean.awayvincent").Returns(
new NetDaemon.HassModel.Entities.EntityState { EntityId = "input_boolean.awayvincent", State = awayVincent });
ctx.HaContext.GetState("input_boolean.awaycarleen").Returns(
new NetDaemon.HassModel.Entities.EntityState { EntityId = "input_boolean.awaycarleen", State = awayCarleen });
ctx.HaContext.GetState("input_boolean.away").Returns(
new NetDaemon.HassModel.Entities.EntityState { EntityId = "input_boolean.away", State = away });
ctx.HaContext.GetState("input_boolean.holliday").Returns(
new NetDaemon.HassModel.Entities.EntityState { EntityId = "input_boolean.holliday", State = "off" });
ctx.InitApp<PresenceManager>();
return ctx;
}

[Fact]
public void Away_TurnsOn_WhenBothPeopleAreAway()
{
// Carleen already away, Vincent now leaves too → both away → derived away on
var ctx = Arrange(awayVincent: "off", awayCarleen: "on", away: "off");

ctx.ChangeStateFor("input_boolean.awayvincent").FromState("off").ToState("on");
ctx.HaContextMock.ProcessPendingOperations();

ctx.VerifyCallService("input_boolean", "turn_on", "away");
}

[Fact]
public void Away_StaysOff_WhenOnlyVincentIsAway()
{
// Carleen still home, Vincent leaves → not both away → away must not turn on
var ctx = Arrange(awayVincent: "off", awayCarleen: "off", away: "off");

ctx.ChangeStateFor("input_boolean.awayvincent").FromState("off").ToState("on");
ctx.HaContextMock.ProcessPendingOperations();

ctx.VerifyNotCallService("input_boolean.turn_on");
}

[Fact]
public void Away_TurnsOff_WhenSomeoneComesHome()
{
// Both were away, Carleen comes home → away should turn off
var ctx = Arrange(awayVincent: "on", awayCarleen: "on", away: "on");

// Carleen comes home: her away boolean is cleared, then the derivation runs
ctx.HaContext.GetState("input_boolean.awaycarleen").Returns(
new NetDaemon.HassModel.Entities.EntityState { EntityId = "input_boolean.awaycarleen", State = "off" });
ctx.ChangeStateFor("input_boolean.awaycarleen").FromState("on").ToState("off");
ctx.HaContextMock.ProcessPendingOperations();

ctx.VerifyCallService("input_boolean", "turn_off", "away");
}

}
52 changes: 52 additions & 0 deletions TestAutomation/Apps/Rooms/LivingRoomLightsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Automation.apps.Rooms.LivingRoom;
using NetDaemon.HassModel.Entities;
using NSubstitute;
using TestAutomation.Helpers;
using Xunit;

namespace TestAutomation.Apps.Rooms;

public class LivingRoomLightsTests
{
private static AppTestContext ArrangeWithLightOn()
{
var ctx = AppTestContext.NewWithScheduler();
ctx.HaContext.GetState("light.woonkamer").Returns(
new EntityState { EntityId = "light.woonkamer", State = "on" });
return ctx;
}

[Fact]
public void LivingRoomLight_StaysOn_WhenVincentAwakeButCarleenSleeping()
{
var ctx = ArrangeWithLightOn();

// Carleen home and sleeping; Vincent awake (sleepingvincent never turned on → IsSleeping=false)
ctx.InitApp<LivingRoomLights>();
ctx.ChangeStateFor("input_boolean.awaycarleen").FromState("on").ToState("off"); // Carleen home
ctx.ChangeStateFor("input_boolean.sleepingcarleen").FromState("off").ToState("on"); // Carleen sleeping
ctx.HaContextMock.ProcessPendingOperations();

// Motion stops in the living room
ctx.ChangeStateFor("binary_sensor.motionwoonkamer").FromState("on").ToState("off");
ctx.HaContextMock.ProcessPendingOperations();

// Living room light must NOT be turned off — Vincent is awake
ctx.VerifyCallService("light", "turn_off", "woonkamer", times: 0);
}

[Fact]
public void LivingRoomLight_TurnsOff_WhenVincentSleeping()
{
var ctx = ArrangeWithLightOn();

ctx.InitApp<LivingRoomLights>();
ctx.ChangeStateFor("input_boolean.sleepingvincent").FromState("off").ToState("on"); // Vincent sleeping
ctx.HaContextMock.ProcessPendingOperations();

ctx.ChangeStateFor("binary_sensor.motionwoonkamer").FromState("on").ToState("off");
ctx.HaContextMock.ProcessPendingOperations();

ctx.VerifyCallService("light", "turn_off", "woonkamer");
}
}
29 changes: 29 additions & 0 deletions automation/Enum/PresenceScenario.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Automation.Enum;

/// <summary>
/// Represents the combined presence of Vincent and Carleen.
/// Used by the <see cref="Automation.apps.General.PresenceManager"/> to decide which
/// actions to execute when presence changes.
/// </summary>
public enum PresenceScenario
{
/// <summary>
/// Both Vincent and Carleen are home.
/// </summary>
BothHome,

/// <summary>
/// Vincent is away, Carleen is still home.
/// </summary>
VincentAwayOnly,

/// <summary>
/// Carleen is away, Vincent is still home.
/// </summary>
CarleenAwayOnly,

/// <summary>
/// Both Vincent and Carleen are away. This drives the house-wide "away" boolean.
/// </summary>
BothAway
}
5 changes: 0 additions & 5 deletions automation/Globals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,4 @@ public static HouseState GetHouseState(IEntities entities)
_ => HouseState.Day
};
}

public static bool AmIHomeCheck(Entities entities)
{
return (entities.Person.VincentMaarschalkerweerd.State ?? "not_home") != "home" || entities.InputBoolean.Onvacation.IsOn() && (entities.Person.Timo.State ?? "not_home") != "home";
}
}
Loading
Loading