using Crestron.SimplSharp;
using PepperDash.Core;
using PepperDash.Core.Logging;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Core
{
///
/// Represents a device that manages room combinations by controlling partitions and scenarios.
///
/// The allows for dynamic configuration of room
/// combinations based on partition states and predefined scenarios. It supports both automatic and manual modes
/// for managing room combinations. In automatic mode, the device determines the current room combination scenario
/// based on partition sensor states. In manual mode, scenarios can be set explicitly by the user.
public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner
{
private EssentialsRoomCombinerPropertiesConfig _propertiesConfig;
private IRoomCombinationScenario _currentScenario;
private List _rooms;
///
/// Gets a list of rooms represented as key-name pairs.
///
public List Rooms
{
get
{
return _rooms.Cast().ToList();
}
}
private bool _isInAutoMode;
///
/// Gets or sets a value indicating whether the system is operating in automatic mode.
///
/// Changing this property triggers an update event via
/// IsInAutoModeFeedback.FireUpdate(). Ensure that any event listeners are properly configured to handle
/// this update.
public bool IsInAutoMode
{
get
{
return _isInAutoMode;
}
set
{
if (value == _isInAutoMode)
{
return;
}
_isInAutoMode = value;
IsInAutoModeFeedback.FireUpdate();
}
}
///
/// Gets a value indicating whether automatic mode is disabled.
///
public bool DisableAutoMode
{
get
{
return _propertiesConfig.DisableAutoMode;
}
}
private CTimer _scenarioChangeDebounceTimer;
private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s
private Mutex _scenarioChange = new Mutex();
///
/// Initializes a new instance of the class, which manages room combination
/// scenarios and partition states.
///
/// The class is designed to handle dynamic room
/// combination scenarios based on partition states. It supports both automatic and manual modes for managing
/// room combinations. By default, the instance starts in automatic mode unless the
/// specifies otherwise. After activation, the room combiner initializes partition state providers and sets up
/// the initial room configuration. Additionally, it subscribes to the event to ensure proper initialization of dependent devices
/// before determining or setting the room combination scenario.
/// The unique identifier for the room combiner instance.
/// The configuration properties for the room combiner, including default settings and debounce times.
public EssentialsRoomCombiner(string key, EssentialsRoomCombinerPropertiesConfig props)
: base(key)
{
_propertiesConfig = props;
Partitions = new List();
RoomCombinationScenarios = new List();
if (_propertiesConfig.ScenarioChangeDebounceTimeSeconds > 0)
{
_scenarioChangeDebounceTimeSeconds = _propertiesConfig.ScenarioChangeDebounceTimeSeconds;
}
IsInAutoModeFeedback = new BoolFeedback(() => _isInAutoMode);
// default to auto mode
IsInAutoMode = true;
if (_propertiesConfig.defaultToManualMode)
{
IsInAutoMode = false;
}
IsInAutoModeFeedback.FireUpdate();
CreateScenarios();
AddPostActivationAction(() =>
{
SetupPartitionStateProviders();
SetRooms();
});
// Subscribe to the AllDevicesInitialized event
// We need to wait until all devices are initialized in case
// any actions are dependent on 3rd party devices already being
// connected and initialized
DeviceManager.AllDevicesInitialized += (o, a) =>
{
if (IsInAutoMode)
{
DetermineRoomCombinationScenario();
}
else
{
SetRoomCombinationScenario(_propertiesConfig.defaultScenarioKey);
}
};
}
private void CreateScenarios()
{
foreach (var scenarioConfig in _propertiesConfig.Scenarios)
{
var scenario = new RoomCombinationScenario(scenarioConfig);
RoomCombinationScenarios.Add(scenario);
}
}
private void SetRooms()
{
_rooms = new List();
foreach (var roomKey in _propertiesConfig.RoomKeys)
{
var room = DeviceManager.GetDeviceForKey(roomKey);
if (DeviceManager.GetDeviceForKey(roomKey) is IEssentialsRoom essentialsRoom)
{
_rooms.Add(essentialsRoom);
}
}
var rooms = DeviceManager.AllDevices.OfType().Cast();
foreach (var room in rooms)
{
room.Deactivate();
}
}
private void SetupPartitionStateProviders()
{
foreach (var pConfig in _propertiesConfig.Partitions)
{
var sensor = DeviceManager.GetDeviceForKey(pConfig.DeviceKey) as IPartitionStateProvider;
var partition = new EssentialsPartitionController(pConfig.Key, pConfig.Name, sensor, _propertiesConfig.defaultToManualMode, pConfig.AdjacentRoomKeys);
partition.PartitionPresentFeedback.OutputChange += PartitionPresentFeedback_OutputChange;
Partitions.Add(partition);
}
}
private void PartitionPresentFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
StartDebounceTimer();
}
private void StartDebounceTimer()
{
// default to 500ms for manual mode
var time = 500;
// if in auto mode, debounce the scenario change
if (IsInAutoMode) time = _scenarioChangeDebounceTimeSeconds * 1000;
if (_scenarioChangeDebounceTimer == null)
{
_scenarioChangeDebounceTimer = new CTimer((o) => DetermineRoomCombinationScenario(), time);
}
else
{
_scenarioChangeDebounceTimer.Reset(time);
}
}
///
/// Determines the current room combination scenario based on the state of the partition sensors
///
private void DetermineRoomCombinationScenario()
{
if (_scenarioChangeDebounceTimer != null)
{
_scenarioChangeDebounceTimer.Dispose();
_scenarioChangeDebounceTimer = null;
}
this.LogInformation("Determining Combination Scenario");
var currentScenario = RoomCombinationScenarios.FirstOrDefault((s) =>
{
this.LogDebug("Checking scenario {scenarioKey}", s.Key);
// iterate the partition states
foreach (var partitionState in s.PartitionStates)
{
this.LogDebug("checking PartitionState {partitionStateKey}", partitionState.PartitionKey);
// get the partition by key
var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionState.PartitionKey));
this.LogDebug("Expected State: {partitionPresent} Actual State: {partitionState}", partitionState.PartitionPresent, partition.PartitionPresentFeedback.BoolValue);
if (partition != null && partitionState.PartitionPresent != partition.PartitionPresentFeedback.BoolValue)
{
// the partition can't be found or the state doesn't match
return false;
}
}
// if it hasn't returned false by now we have the matching scenario
return true;
});
if (currentScenario != null)
{
this.LogInformation("Found combination Scenario {scenarioKey}", currentScenario.Key);
ChangeScenario(currentScenario);
}
}
private async Task ChangeScenario(IRoomCombinationScenario newScenario)
{
if (newScenario == _currentScenario)
{
return;
}
// Deactivate the old scenario first
if (_currentScenario != null)
{
Debug.LogMessage(LogEventLevel.Information, "Deactivating scenario {currentScenario}", this, _currentScenario.Name);
await _currentScenario.Deactivate();
}
_currentScenario = newScenario;
// Activate the new scenario
if (_currentScenario != null)
{
Debug.LogMessage(LogEventLevel.Debug, $"Current Scenario: {_currentScenario.Name}", this);
await _currentScenario.Activate();
}
RoomCombinationScenarioChanged?.Invoke(this, new EventArgs());
}
#region IEssentialsRoomCombiner Members
///
/// Occurs when the room combination scenario changes.
///
/// This event is triggered whenever the configuration or state of the room combination
/// changes. Subscribers can use this event to update their logic or UI based on the new scenario.
public event EventHandler RoomCombinationScenarioChanged;
///
/// Gets the current room combination scenario.
///
public IRoomCombinationScenario CurrentScenario
{
get
{
return _currentScenario;
}
}
///
/// Gets the feedback indicating whether the system is currently in auto mode.
///
public BoolFeedback IsInAutoModeFeedback { get; private set; }
///
/// Enables auto mode for the room combiner and its partitions, allowing automatic room combination scenarios to
/// be determined.
///
/// Auto mode allows the room combiner to automatically adjust its configuration based on
/// the state of its partitions. If auto mode is disabled in the configuration, this method logs a warning and
/// does not enable auto mode.
public void SetAutoMode()
{
if(_propertiesConfig.DisableAutoMode)
{
this.LogWarning("Auto mode is disabled for this room combiner. Cannot set to auto mode.");
return;
}
IsInAutoMode = true;
foreach (var partition in Partitions)
{
partition.SetAutoMode();
}
DetermineRoomCombinationScenario();
}
///
/// Switches the system to manual mode, disabling automatic operations.
///
/// This method sets the system to manual mode by updating the mode state and propagates
/// the change to all partitions. Once in manual mode, automatic operations are disabled for the system and its
/// partitions.
public void SetManualMode()
{
IsInAutoMode = false;
foreach (var partition in Partitions)
{
partition.SetManualMode();
}
}
///
/// Toggles the current mode between automatic and manual.
///
/// If the current mode is automatic, this method switches to manual mode. If the
/// current mode is manual, it switches to automatic mode.
public void ToggleMode()
{
if (IsInAutoMode)
{
SetManualMode();
}
else
{
SetAutoMode();
}
}
///
/// Gets the collection of room combination scenarios.
///
public List RoomCombinationScenarios { get; private set; }
///
/// Gets the collection of partition controllers managed by this instance.
///
public List Partitions { get; private set; }
///
/// Toggles the state of the partition identified by the specified partition key.
///
/// If no partition with the specified key exists, the method performs no
/// action.
/// The key of the partition whose state is to be toggled. This value cannot be null or empty.
public void TogglePartitionState(string partitionKey)
{
var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionKey));
if (partition != null)
{
partition.ToggglePartitionState();
}
}
///
/// Sets the room combination scenario based on the specified scenario key.
///
/// This method manually adjusts the partition states according to the specified
/// scenario. If the application is in auto mode, the operation will not proceed, and a log message will be
/// generated indicating that the mode must be set to manual first. If the specified scenario key does not
/// match any existing scenario, a debug log message will be generated. For each partition state in the
/// scenario, the corresponding partition will be updated to either "Present" or "Not Present" based on the
/// scenario's configuration. If a partition key in the scenario cannot be found, a debug log message will be
/// generated.
/// The key identifying the room combination scenario to apply. This must match the key of an existing scenario.
public void SetRoomCombinationScenario(string scenarioKey)
{
if (IsInAutoMode)
{
Debug.LogMessage(LogEventLevel.Information, this, "Cannot set room combination scenario when in auto mode. Set to auto mode first.");
return;
}
// Get the scenario
var scenario = RoomCombinationScenarios.FirstOrDefault((s) => s.Key.Equals(scenarioKey));
// Set the parition states from the scenario manually
if (scenario != null)
{
Debug.LogMessage(LogEventLevel.Information, this, "Manually setting scenario to '{0}'", scenario.Key);
foreach (var partitionState in scenario.PartitionStates)
{
var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionState.PartitionKey));
if (partition != null)
{
if (partitionState.PartitionPresent)
{
Debug.LogMessage(LogEventLevel.Information, this, "Manually setting state to Present for: '{0}'", partition.Key);
partition.SetPartitionStatePresent();
}
else
{
Debug.LogMessage(LogEventLevel.Information, this, "Manually setting state to Not Present for: '{0}'", partition.Key);
partition.SetPartitionStateNotPresent();
}
}
else
{
Debug.LogMessage(LogEventLevel.Debug, this, "Unable to find partition with key: '{0}'", partitionState.PartitionKey);
}
}
}
else
{
Debug.LogMessage(LogEventLevel.Debug, this, "Unable to find scenario with key: '{0}'", scenarioKey);
}
}
#endregion
}
///
/// Provides a factory for creating instances of devices.
///
/// This factory is responsible for constructing devices
/// based on the provided configuration. It supports the type name "essentialsroomcombiner" for device
/// creation.
public class EssentialsRoomCombinerFactory : EssentialsDeviceFactory
{
///
/// Initializes a new instance of the class.
///
/// This factory is used to create instances of room combiners with the specified type
/// names. By default, the factory includes the type name "essentialsroomcombiner".
public EssentialsRoomCombinerFactory()
{
TypeNames = new List { "essentialsroomcombiner" };
}
///
/// Creates and initializes a new instance of the device.
///
/// This method uses the provided device configuration to extract the properties and
/// create an device. Ensure that the configuration contains valid
/// properties for the device to be created successfully.
/// The device configuration containing the key and properties required to build the device.
/// A new instance of initialized with the specified configuration.
public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.DeviceConfig dc)
{
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new EssentialsRoomCombiner Device");
var props = dc.Properties.ToObject();
return new EssentialsRoomCombiner(dc.Key, props);
}
}
}