Skip to content

Commit

Permalink
Merge pull request #38
Browse files Browse the repository at this point in the history
* Init automation right in the config of the room

* Update docs
  • Loading branch information
x00Pavel authored Mar 18, 2024
1 parent f986b34 commit 6d282db
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 210 deletions.
3 changes: 3 additions & 0 deletions Docs/Advanced/room-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Room Controls

The NetEntityAutomation creates several services for controlling automation in individual rooms.
105 changes: 68 additions & 37 deletions Docs/how-to-use.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ Configuration class is used to define rooms and entities that are present in spe
Following example illustrates how to define a room configuration:

```csharp
using NetEntityAutomation.Core.Automations;
using NetEntityAutomation.Core.RoomManager;

namespace nd_app.apps.HomeAutomation.Configs;

public class LivingRoomConfig: IRoomConfig
public class LivingRoomConfigV1: IRoomConfig
{
public string Name => "Living room";
public ILogger Logger { get; set; }
public IEnumerable<AutomationConfig> Entities { get; set; }
public IEnumerable<AutomationBase> Entities { get; set; }
public NightModeConfig? NightMode { get; set; }

public LivingRoomConfigV1(
Expand All @@ -41,54 +44,82 @@ public class LivingRoomConfig: IRoomConfig
)
{
Logger = logger;
var secondaryLight = new AutomationConfig
{
AutomationType = AutomationType.SecondaryLight,
ServiceAccountId = personEntities.Netdaemon.Attributes?.UserId ?? "",
Entities = new[]
{
lights.LivingroomBulb,
lights.LedLivingRoomSonoffLed,
},
Triggers = new []
var mainLight = ;
var secondaryLight = ;
Entities = new List<AutomationBase>
{
new MainLightAutomationBase
{
new MotionSensor(
new []
EntitiesList = new List<ILightEntityCore>
{
lights.LivingroomBulbLight
},
Triggers = new []
{
new MotionSensor(new []
{
sensors.MotionSensorLivingRoomMotion,
sensors.MotionSensorLivingRoom2Motion
sensors.LivingRoomMotionSensorMotion,
sensors.LivingRoomMotionSensor1Motion
}, context)
},
WaitTime = TimeSpan.FromHours(1)
},
NightMode = new NightModeConfig
{
IsEnabled = true,
}
};
var blinds = new AutomationConfig
{
AutomationType = AutomationType.Blinds,
Entities = new Entity[]
new LightAutomationBase
{
EntitiesList = new List<ILightEntityCore>
{
lights.LivingroomBulb,
lights.LedLivingRoomSonoffLed,
},
Triggers = new []
{
new MotionSensor(
new[]
{
sensors.LivingRoomMotionSensor1Motion,
sensors.LivingRoomMotionSensorMotion
}, context)
},
ServiceAccountId = personEntities.Netdaemon.Attributes?.UserId ?? "",
WaitTime = TimeSpan.FromMinutes(20),
SwitchTimer = TimeSpan.FromHours(1),
StopAtTimeFunc = () => DateTime.Parse(sun.Sun.Attributes?.NextDawn ?? "06:00:00").TimeOfDay,
StartAtTimeFunc = () =>
DateTime.Parse(sun.Sun.Attributes?.NextDusk ?? "20:00:00").TimeOfDay -
TimeSpan.FromMinutes(30),
NightMode = new NightModeConfig
{
IsEnabled = true,
Devices = new List<ILightEntityCore>
{
lights.LivingroomBulb,
},
StartAtTimeFunc = () => DateTime.Parse("23:00:00").TimeOfDay
}
},
new BlindAutomationBase
{
covers.LumiLumiCurtainAcn002Cover,
sun.Sun
Sun = sun.Sun,
EntitiesList = new List<ICoverEntityCore>
{
covers.LivingRoomBlinds1Cover,
},
StartAtTimeFunc = null,
StopAtTimeFunc = null
}
};
Entities = new List<AutomationConfig>
{
secondaryLight,
blinds
};
}
}
```

In the example above, `LivingRoomConfig` class is used to define a living room.
Abstracting from the details, it contains a list of entities that are grouped for a specific automation scenario: secondary light and blinds.
Each entity is represented by an instance of `AutomationConfig` class.
`AutomationConfig` class contains information about the entity, its triggers, and other settings that are specific to the automation scenario.
There is more options available in `AutomationConfig` class, but the example above shows the most common ones.
Abstracting from the details, it contains a list of entities that are grouped for a specific automation scenario: main light, secondary light and blinds.
Each entity is represented by an instance of `AutomationBase` class.
`AutomationBase` class contains information about the entity, its triggers, and other settings that are specific to the automation scenario.
There is more options available in `AutomationBase` class, but the example above shows the most common ones.
This class should be inherited in order to define a custom automation scenario.

> This configuration might not be ideal and might change in the future.
> It is recommended to check the latest version of the library to see the most up-to-date configuration.
More datailed information about configuration classes and their properties can be found in the API documentation.
More detailed information about configuration classes and their properties can be found in the API documentation.
72 changes: 31 additions & 41 deletions src/Core/Automations/AutomationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@
using Microsoft.Extensions.Logging;
using NetDaemon.HassModel;
using NetDaemon.HassModel.Entities;
using NetEntityAutomation.Core.Conditions;
using NetEntityAutomation.Core.Configs;
using NetEntityAutomation.Core.Triggers;
using NetEntityAutomation.Extensions.Events;
using NetEntityAutomation.Extensions.ExtensionMethods;

namespace NetEntityAutomation.Core.Automations;

internal enum ServiceAction
{
Disable,
Enable,
Toggle,
}

internal record ServiceData
{
public string? action { get; init; }
public string? value { get; init; }
}

public abstract class IAutomationBase
public abstract class AutomationBase: IAutomationConfig
{
private bool _enabled = true;
public IObservable<bool> IsEnabledObserver;
public readonly IObservable<bool> IsEnabledObserver;
public event EventHandler<bool>? IsEnabledChanged;
public IEnumerable<IStateChangeTrigger> Triggers { get; set; }
public IEnumerable<ICondition> Conditions { get; set; } = new []{new DefaultCondition()};
public string ServiceAccountId { get; set; }
public TimeSpan WaitTime { get; set; }
public TimeSpan SwitchTimer { get; set; }
public Func<TimeSpan>? StopAtTimeFunc { get; set; }
public Func<TimeSpan>? StartAtTimeFunc { get; set; }
public NightModeConfig NightMode { get; set; }
public IHaContext Context { get; set; }
public ILogger Logger { get; set; }
public abstract void ConfigureAutomation();

public bool IsEnabled
{
get => _enabled;
Expand All @@ -36,6 +38,13 @@ public bool IsEnabled
IsEnabledChanged?.Invoke(this, value);
}
}
protected AutomationBase()
{
IsEnabledObserver = Observable.FromEventPattern<bool>(
handler => IsEnabledChanged += handler,
handler => IsEnabledChanged -= handler
).Select(pattern => pattern.EventArgs);
}
}

/// <summary>
Expand All @@ -44,30 +53,11 @@ public bool IsEnabled
/// </summary>
/// <typeparam name="TEntity">Type of entities an automation will work with</typeparam>
/// <typeparam name="TFsm">Type of Finite State Machine that will be used for storing and representing the state of TEntity</typeparam>
public abstract class AutomationBase<TEntity, TFsm>: IAutomationBase
public abstract class AutomationBase<TEntity, TFsm>: AutomationBase
{
protected IHaContext Context { get; set; }
protected ILogger Logger { get; set; }
protected AutomationConfig Config { get; set; }
protected List<TEntity> EntitiesList { get; set; }
protected List<TFsm> FsmList;
public List<TEntity> EntitiesList { get; set; }
protected List<TFsm> FsmList { get; } = [];

protected AutomationBase(IHaContext context, AutomationConfig config, ILogger logger)
{
Context = context;
Logger = logger;
Config = config;
IsEnabledObserver = Observable.FromEventPattern<bool>(
handler => IsEnabledChanged += handler,
handler => IsEnabledChanged -= handler
).Select(pattern => pattern.EventArgs);
FsmList = new List<TFsm>();
EntitiesList = Config.Entities.OfType<TEntity>().ToList();
if (Config is { StartAtTimeFunc: not null, StopAtTimeFunc: not null })
Logger.LogDebug("Working hours from {Start} - {End}", Config.StartAtTimeFunc() , Config.StopAtTimeFunc());
Logger.LogDebug("Night mode from {Start} - {End}", Config.NightMode.StartAtTimeFunc(), Config.NightMode.StopAtTimeFunc());
}

/// <summary>
/// Helper method to trigger an event at a specific time of the day.
/// It uses Observable.Timer to trigger the event.
Expand Down Expand Up @@ -122,7 +112,7 @@ protected void CreateFsm()
/// <param name="id"></param>
/// <returns></returns>
protected IObservable<StateChange> UserEvent(string id) => EntityEvent(id)
.Where(e => !e.IsAutomationInitiated(Config.ServiceAccountId));
.Where(e => !e.IsAutomationInitiated(ServiceAccountId));

/// <summary>
/// Observable for all state changes of a specific entity initiated by the automation.
Expand All @@ -131,7 +121,7 @@ protected IObservable<StateChange> UserEvent(string id) => EntityEvent(id)
/// <param name="id">Account ID which owns the token for NetDaemon</param>
/// <returns></returns>
protected IObservable<StateChange> AutomationEvent(string id) => EntityEvent(id)
.Where(e => e.IsAutomationInitiated(Config.ServiceAccountId));
.Where(e => e.IsAutomationInitiated(ServiceAccountId));

/// <summary>
/// Helper function to choose between two actions based on a condition.
Expand All @@ -154,8 +144,8 @@ protected IObservable<StateChange> AutomationEvent(string id) => EntityEvent(id)

protected bool IsWorkingHours()
{
if (Config is { StartAtTimeFunc: not null, StopAtTimeFunc: not null })
return UtilsMethods.NowInTimeRange(Config.StartAtTimeFunc(), Config.StopAtTimeFunc());
if (NightMode is { StartAtTimeFunc: not null, StopAtTimeFunc: not null })
return UtilsMethods.NowInTimeRange(StartAtTimeFunc(), StopAtTimeFunc());
Logger.LogWarning("Can't determine working hours. StartAtTimeFunc or StopAtTimeFunc is not set");
return true;
}
Expand All @@ -172,4 +162,4 @@ protected bool IsWorkingHours()
action();
return null;
}
}
}
46 changes: 20 additions & 26 deletions src/Core/Automations/BlindAutomationBase.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System.Reactive.Linq;
using Microsoft.Extensions.Logging;
using NetDaemon.HassModel;
using NetDaemon.HassModel.Entities;
using NetEntityAutomation.Extensions.ExtensionMethods;
using NetEntityAutomation.Core.Configs;
using NetEntityAutomation.Core.Fsm;
using NetEntityAutomation.Extensions.ExtensionMethods;

namespace NetEntityAutomation.Core.Automations;

/// <summary>
Expand All @@ -13,24 +12,18 @@ namespace NetEntityAutomation.Core.Automations;
/// </summary>
public class BlindAutomationBase : AutomationBase<ICoverEntityCore, BlindsFsm>
{
private readonly ISunEntityCore _sun;
public required ISunEntityCore Sun { get; set; }
private int OpenBlinds => EntitiesList.Count(b => Context.GetState(b.EntityId)?.State == "open");

public BlindAutomationBase(IHaContext haContext, AutomationConfig automation, ILogger roomConfigLogger): base(haContext, automation, roomConfigLogger)
{
_sun = Config.Entities.OfType<ISunEntityCore>().First();
CreateFsm();
ConfigureAutomation();
}


private bool IsOpenTime()
{
return (Config.StartAtTimeFunc == null, Config.StopAtTimeFunc == null) switch
return (StartAtTimeFunc == null, StopAtTimeFunc == null) switch
{
(true, true) => _sun.IsAboveHorizon(),
(false, false) => UtilsMethods.NowInTimeRange(Config.StartAtTimeFunc!(), Config.StopAtTimeFunc!()),
(true, false) => _sun.IsAboveHorizon() && UtilsMethods.NowAfterTime(Config.StopAtTimeFunc!()),
(false, true) => UtilsMethods.NowBeforeTime(Config.StartAtTimeFunc!()) && _sun.IsAboveHorizon(),
(true, true) => Sun.IsAboveHorizon(),
(false, false) => UtilsMethods.NowInTimeRange(StartAtTimeFunc!(), StopAtTimeFunc!()),
(true, false) => Sun.IsAboveHorizon() && UtilsMethods.NowAfterTime(StopAtTimeFunc!()),
(false, true) => UtilsMethods.NowBeforeTime(StartAtTimeFunc!()) && Sun.IsAboveHorizon(),
};
}

Expand All @@ -47,38 +40,39 @@ private BlindsStateActivateAction BlindsActivateActions(ICoverEntityCore blind)

protected override BlindsFsm ConfigureFsm(ICoverEntityCore blind)
{
var fsm = new BlindsFsm(Config, Logger, blind).Configure(BlindsActivateActions(blind));
var fsm = new BlindsFsm(Logger, blind).Configure(BlindsActivateActions(blind));
UserClose(blind.EntityId).Subscribe(_ => ChooseAction(OpenBlinds > 0, fsm.FireManualClose, FsmList.FireAllOff));
UserOpen(blind.EntityId).Subscribe(_ => fsm.FireManualOpen());
AutomationClose(blind.EntityId).Subscribe(_ =>ChooseAction(OpenBlinds > 0, fsm.FireAutomationClose, fsm.FireAutomationClose));
AutomationOpen(blind.EntityId).Subscribe(_ => fsm.FireAutomationOpen());
return fsm;
}

private void ConfigureAutomation()
{
public override void ConfigureAutomation()
{
CreateFsm();
Logger.LogDebug("Configuring blind automation");
if (Config.StartAtTimeFunc == null)
if (StartAtTimeFunc == null)
{
_sun.AboveHorizon().Subscribe(_ => EntitiesList.OpenCover());
Sun.AboveHorizon().Subscribe(_ => EntitiesList.OpenCover());
Logger.LogDebug("Subscribed to sun above horizon event to open blinds");
}
else
{
var time = Config.StartAtTimeFunc.Invoke();
var time = StartAtTimeFunc.Invoke();
DailyEventAtTime(time, EntitiesList.OpenCover);
Logger.LogDebug("Subscribed to daily event at {Time} to open blinds", time);
}

if (Config.StopAtTimeFunc == null)
if (StopAtTimeFunc == null)
{
_sun.BelowHorizon().Subscribe(_ => EntitiesList.CloseCover());
Sun.BelowHorizon().Subscribe(_ => EntitiesList.CloseCover());
Logger.LogDebug("Subscribed to sun below horizon event to close blinds");
}
else
{
DailyEventAtTime(Config.StopAtTimeFunc.Invoke(), EntitiesList.CloseCover);
Logger.LogDebug("Subscribed to daily event at {Time} to close blinds", Config.StopAtTimeFunc.Invoke());
DailyEventAtTime(StopAtTimeFunc.Invoke(), EntitiesList.CloseCover);
Logger.LogDebug("Subscribed to daily event at {Time} to close blinds", StopAtTimeFunc.Invoke());
}
}

Expand Down
Loading

0 comments on commit 6d282db

Please sign in to comment.