Skip to content

Commit

Permalink
Merge pull request #34
Browse files Browse the repository at this point in the history
* Add main light automation
  • Loading branch information
x00Pavel authored Feb 25, 2024
1 parent ba0bd53 commit 2449ad9
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 48 deletions.
19 changes: 16 additions & 3 deletions src/Core/Automations/AutomationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ protected void DailyEventAtTime(TimeSpan timeSpan, Action action)
protected void CreateFsm()
{
Logger.LogDebug("Configuring {FsmName} ", typeof(TFsm).Name);
foreach (var blind in EntitiesList)
FsmList.Add(ConfigureFsm(blind));
foreach (var entity in EntitiesList)
FsmList.Add(ConfigureFsm(entity));
}

/// <summary>
Expand All @@ -107,7 +107,7 @@ protected void CreateFsm()
/// </summary>
/// <param name="id">HomeAssistant ID of the entity</param>
/// <returns>Observable of StateChange</returns>
private IObservable<StateChange> EntityEvent(string id) => Context.StateAllChanges().Where(e => e.New?.EntityId == id);
protected IObservable<StateChange> EntityEvent(string id) => Context.StateAllChanges().Where(e => e.New?.EntityId == id);

/// <summary>
/// Observable for all state changes of a specific entity initiated by a user.
Expand Down Expand Up @@ -178,4 +178,17 @@ protected bool IsWorkingHours()
Logger.LogWarning("Can't determine working hours. StartAtTimeFunc or StopAtTimeFunc is not set");
return true;
}

protected CustomTimer? ResetTimerOrAction(CustomTimer timer, TimeSpan time, Action action, Func<bool> resetCondition)
{
if (resetCondition())
{
Logger.LogDebug("Resetting timer with time {Time}", time);
timer.StartTimer(time, () => ResetTimerOrAction(timer, time, action, resetCondition));
return timer;
}
Logger.LogDebug("Doing action {Action}", action.Method.Name);
action();
return null;
}
}
37 changes: 12 additions & 25 deletions src/Core/Automations/LightAutomationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ private void TurnOnByAutomation(StateChange e)
Logger.LogDebug("Time of Night Mode {Time}", DateTime.Now.TimeOfDay);
foreach (var fsm in LightsOffByAutomation)
{
if (Config.NightMode.Devices?.Contains(fsm.Light) ?? false)
if (Config.NightMode.Devices?.Contains(fsm.Entity) ?? false)
{
_lightParameters[fsm.Light.EntityId] = fsm.Light.GetLightParameters() ?? new LightParameters
_lightParameters[fsm.Entity.EntityId] = fsm.Entity.GetLightParameters() ?? new LightParameters
{
Brightness = 255
};

fsm.Light.TurnOn(Config.NightMode.LightParameters);
fsm.Entity.TurnOn(Config.NightMode.LightParameters);
}
}
Logger.LogDebug("Stored values for light {Light}", _lightParameters);
Expand All @@ -81,44 +81,31 @@ private void TurnOnByAutomation(StateChange e)
// Restore light parameters after night mode
foreach (var fsm in LightsOffByAutomation)
{
var lightParams = _lightParameters.TryGetValue(fsm.Light.EntityId, out var parameters)
var lightParams = _lightParameters.TryGetValue(fsm.Entity.EntityId, out var parameters)
? parameters
: new LightParameters
{
Brightness = 255
};
fsm.Light.TurnOn(lightParams);
_lightParameters.Remove(fsm.Light.EntityId);
fsm.Entity.TurnOn(lightParams);
_lightParameters.Remove(fsm.Entity.EntityId);
}
Logger.LogDebug("Idle values {Light}", _lightParameters);
}
else
{
LightsOffByAutomation.Select(fsm => fsm.Light).TurnOn();
LightsOffByAutomation.Select(fsm => fsm.Entity).TurnOn();
}
break;
case { IsEnabled: false }:
LightsOffByAutomation.Select(fsm => fsm.Light).TurnOn();
LightsOffByAutomation.Select(fsm => fsm.Entity).TurnOn();
break;
default:
Logger.LogDebug("Not working hours {Time}", DateTime.Now.TimeOfDay);
break;
}
}

private void ResetTimerOrDoAction(LightFsmBase fsm, TimeSpan time, Action action, Func<bool> resetCondition)
{

if (resetCondition())
{
Logger.LogDebug("Resetting timer with time {Time}", time);
fsm.Timer.StartTimer(time, () => ResetTimerOrDoAction(fsm, time, action, resetCondition));
return;
}
Logger.LogDebug("Doing action {Action}", action.Method.Name);
action();
}

private bool OnConditionsMet() => IsWorkingHours() && Config.Triggers.Any(s => s.IsOn()) && Config.Conditions.All(c => c.IsTrue());

private LightStateActivateAction ActionForLight(ILightEntityCore l)
Expand All @@ -136,7 +123,7 @@ private LightStateActivateAction ActionForLight(ILightEntityCore l)
if (OnConditionsMet())
{
l.TurnOn();
ResetTimerOrDoAction(lightFsm, Config.WaitTime, lightFsm.Light.TurnOff, OnConditionsMet);
ResetTimerOrAction(lightFsm.Timer, Config.WaitTime, lightFsm.Entity.TurnOff, OnConditionsMet);
}
else
{
Expand All @@ -149,7 +136,7 @@ private LightStateActivateAction ActionForLight(ILightEntityCore l)
if (OnConditionsMet())
{
l.TurnOn();
ResetTimerOrDoAction(lightFsm, Config.WaitTime, lightFsm.Light.TurnOff, OnConditionsMet);
ResetTimerOrAction(lightFsm.Timer, Config.WaitTime, lightFsm.Entity.TurnOff, OnConditionsMet);
}
else
{
Expand All @@ -176,7 +163,7 @@ protected override LightFsmBase ConfigureFsm(ILightEntityCore l)
e.New?.EntityId, e.New?.State, e.New?.Context?.UserId);
lightFsm.FireMotionOn();
// This is needed to reset the timer if timeout is expired, but there is still a motion
ResetTimerOrDoAction(lightFsm, Config.WaitTime, lightFsm.Light.TurnOff, OnConditionsMet);
ResetTimerOrAction(lightFsm.Timer, Config.WaitTime, lightFsm.Entity.TurnOff, OnConditionsMet);
});
AutomationOff(l.EntityId)
.Subscribe(e =>
Expand All @@ -195,7 +182,7 @@ protected override LightFsmBase ConfigureFsm(ILightEntityCore l)
e.New?.State,
e.New?.Context?.UserId);
lightFsm.FireOn();
lightFsm.Timer.StartTimer(Config.SwitchTimer, lightFsm.Light.TurnOff);
lightFsm.Timer.StartTimer(Config.SwitchTimer, lightFsm.Entity.TurnOff);
});
UserOff(l.EntityId)
.Subscribe(e =>
Expand Down
77 changes: 77 additions & 0 deletions src/Core/Automations/MainLightAutomationBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Reactive.Linq;
using Microsoft.Extensions.Logging;
using NetDaemon.HassModel;
using NetDaemon.HassModel.Entities;
using NetEntityAutomation.Core.Configs;
using NetEntityAutomation.Core.Fsm;
using NetEntityAutomation.Core.Triggers;
using NetEntityAutomation.Extensions.Events;
using NetEntityAutomation.Extensions.ExtensionMethods;

namespace NetEntityAutomation.Core.Automations;

public class MainLightAutomationBase: AutomationBase<ILightEntityCore, MainLightFsmBase>
{
private readonly IEnumerable<ILightEntityCore> _lights;
private readonly IEnumerable<MotionSensor> _motionSensors;
private readonly CustomTimer _timer;

public MainLightAutomationBase(IHaContext context, AutomationConfig config, ILogger logger) : base(context, config, logger)
{
_lights = config.Entities.OfType<ILightEntityCore>();
_motionSensors = config.Triggers.OfType<MotionSensor>();
_timer = new CustomTimer(Logger);
CreateFsm();
ConfigureAutomation();
}

/// <summary>
/// The automation scenario is to turn off the main light in specific time frame specified by
/// <c>Config.WaitTime</c> parameter.
/// On the timeout, if there are no triggers in on state (usually, this is a motion sensor), the light will be turned off.
/// </summary>
private void ConfigureAutomation()
{
foreach (var light in _lights)
{
light.OnEvent().Subscribe(e =>
{
Observable
.Timer(Config.WaitTime)
.Subscribe(_ => ResetTimerOrAction(_timer, Config.WaitTime, light.TurnOff, _motionSensors.IsAnyOn));
});
light.OffEvent()
.Subscribe(e => _timer.Dispose());
}
}

/// <summary>
/// <inheritdoc/>
/// <para>
/// For main light there is only two states: On and Off. The state of the light is fully controlled by the user.
/// </para>
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
protected override MainLightFsmBase ConfigureFsm(ILightEntityCore entity)
{
var lightFsm = new MainLightFsmBase(entity, Config, Logger).Configure(ActionForLight());
entity.OnEvent().Subscribe(_ => lightFsm.FireOn());
entity.OffEvent().Subscribe(_ => lightFsm.FireOff());
return lightFsm;
}

/// <summary>
/// For main automation there is no sense to check any conditions to turn off/on the light as the control over main
/// light is fully on the user.
/// </summary>
/// <returns></returns>
private static MainLightActivateAction ActionForLight()
{
return new MainLightActivateAction
{
OffAction = l => l.Entity.TurnOff(),
OnAction = l => l.Entity.TurnOn()
};
}
}
6 changes: 2 additions & 4 deletions src/Core/Fsm/BlindsFsm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@ public struct BlindsStateActivateAction

public class BlindsFsm : FsmBase<BlindsState, BlindsTrigger>
{
private ICoverEntityCore Blinds { get; set; }
// private new ICoverEntityCore Entity { get; set; }

public BlindsFsm(AutomationConfig config, ILogger logger, ICoverEntityCore blinds) : base(config, logger)
public BlindsFsm(AutomationConfig config, ILogger logger, ICoverEntityCore blinds) : base(blinds, config, logger)
{
Logger = logger;
Blinds = blinds;
DefaultState = BlindsState.Closed;
StoragePath = $"storage/v1/{Blinds.EntityId}_fsm.json";
InitFsm();
}

Expand Down
16 changes: 12 additions & 4 deletions src/Core/Fsm/IFsmBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using NetDaemon.HassModel.Entities;
using NetEntityAutomation.Extensions.Events;
using NetEntityAutomation.Core.Configs;
using Newtonsoft.Json;
Expand All @@ -22,16 +23,18 @@ public static void FireAllOff(this IEnumerable<IFsmBase> state)
}
}

public abstract class FsmBase<TState, TTrigger>(AutomationConfig config, ILogger logger): IFsmBase
public abstract class FsmBase<TState, TTrigger>(IEntityCore entity, AutomationConfig config, ILogger logger): IFsmBase
{
private AutomationConfig Config { get; set; } = config;
protected StateMachine<TState, TTrigger> _fsm;
protected ILogger Logger = logger;
public CustomTimer Timer;
public CustomTimer Timer = new (logger);
public TState State => _fsm.State;
protected TState DefaultState;
protected string StoragePath;
public bool IsEnabled { get; set; } = true;
public readonly IEntityCore Entity = entity;
private string StorageDir => $"storage/{GetType().Name}";
private string StoragePath => $"{StorageDir}/{Entity.EntityId}_fsm.json";
protected record JsonStorageSchema(TState State);

protected void InitFsm()
Expand All @@ -47,9 +50,14 @@ private void StoreState(TState state)

private TState GetStateFromStorage()
{
if (!Directory.Exists(StorageDir))
{
Logger.LogDebug("Storage directory {Path} does not exist, creating new one", StorageDir);
Directory.CreateDirectory(StorageDir);
}
if (!File.Exists(StoragePath))
{
Logger.LogDebug("Storage file does not exist, creating new one");
Logger.LogDebug("Storage file {Path} does not exist, creating new one", StoragePath);
File.Create(StoragePath).Dispose();
File.WriteAllText(StoragePath, "{\"State\": " + JsonConvert.SerializeObject(DefaultState) + "}");
return DefaultState;
Expand Down
8 changes: 2 additions & 6 deletions src/Core/Fsm/LightFsmBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,11 @@ public struct LightStateActivateAction
}
public class LightFsmBase : FsmBase<LightState, LightTrigger>
{
public ILightEntityCore Light { get; set; }
public new readonly ILightEntityCore Entity;

public LightFsmBase(ILightEntityCore light, AutomationConfig config, ILogger logger) : base(config, logger)
public LightFsmBase(ILightEntityCore light, AutomationConfig config, ILogger logger) : base(light, config, logger)
{
Logger = logger;
Light = light;
DefaultState = LightState.Off;
StoragePath = $"storage/v1/{light.EntityId}_fsm.json";
Timer = new CustomTimer(logger);
InitFsm();
}

Expand Down
64 changes: 64 additions & 0 deletions src/Core/Fsm/MainLightFsmBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.Extensions.Logging;
using NetDaemon.HassModel.Entities;
using NetEntityAutomation.Core.Configs;
using NetEntityAutomation.Extensions.Events;

namespace NetEntityAutomation.Core.Fsm;

public enum MainLightState
{
Off,
On
}

public enum MainLightTrigger
{
SwitchOnTrigger,
SwitchOffTrigger,
TimerElapsed,
AllOff
}

public struct MainLightActivateAction
{
public Action<MainLightFsmBase> OffAction { get; init; }
public Action<MainLightFsmBase> OnAction { get; init; }
}

public class MainLightFsmBase : FsmBase<MainLightState, MainLightTrigger>
{
public new ILightEntityCore Entity { get; set; }
public MainLightFsmBase(ILightEntityCore light, AutomationConfig config, ILogger logger) : base(light, config, logger)
{
DefaultState = MainLightState.Off;
Entity = light;
InitFsm();
}

public MainLightFsmBase Configure(MainLightActivateAction actions)
{
_fsm.Configure(MainLightState.Off)
.OnActivate(() => actions.OffAction(this))
.Ignore(MainLightTrigger.TimerElapsed)
.PermitReentry(MainLightTrigger.SwitchOffTrigger)
.PermitReentry(MainLightTrigger.AllOff)
.Permit(MainLightTrigger.SwitchOnTrigger, MainLightState.On);

_fsm.Configure(MainLightState.On)
.OnActivate(() => actions.OnAction(this))
.Ignore(MainLightTrigger.TimerElapsed)
.PermitReentry(MainLightTrigger.SwitchOnTrigger)
.Permit(MainLightTrigger.AllOff, MainLightState.Off)
.Permit(MainLightTrigger.SwitchOffTrigger, MainLightState.Off);

return this;
}

public void FireOn() => _fsm.Fire(MainLightTrigger.SwitchOnTrigger);

public void FireOff() => _fsm.Fire(MainLightTrigger.SwitchOffTrigger);

public void FireTimerElapsed() => _fsm.Fire(MainLightTrigger.TimerElapsed);

public override void FireAllOff() => _fsm.Fire(MainLightTrigger.AllOff);
}
4 changes: 2 additions & 2 deletions src/Core/RoomManager/Room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ private void InitAutomations()
_roomConfig.Logger.LogDebug("Creating automations");
foreach (var automation in _roomConfig.Entities)
{
_roomConfig.Logger.LogDebug("Created {AutomationType}", automation.AutomationType);
switch (automation.AutomationType)
{
case AutomationType.MainLight:
_automations.Add(new MainLightAutomationBase(_haContext, automation, _roomConfig.Logger));
break;
case AutomationType.SecondaryLight:
_automations.Add(new LightAutomationBase(_haContext, automation, _roomConfig.Logger));
_roomConfig.Logger.LogDebug("Created SecondaryLight");
break;
case AutomationType.Blinds:
_automations.Add(new BlindAutomationBase(_haContext, automation, _roomConfig.Logger));
_roomConfig.Logger.LogDebug("Created Blinds automation");
break;
default:
break;
Expand Down
Loading

0 comments on commit 2449ad9

Please sign in to comment.