feat: enhance IHasDirectoryMessenger with new actions and state messaging for directory management

feat: update MobileControlConfig to enable messenger subscriptions by default
fix: improve HandleBatchDeviceFullStatus to support granular device status requests and enhance error handling
This commit is contained in:
Neil Dorin 2026-06-25 11:46:27 -06:00
parent 77c78af74b
commit 8748157362
4 changed files with 93 additions and 28 deletions

View file

@ -50,7 +50,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
};
}
private void SendCurrentSourceStatus(string id = null)
private void SendCurrentSourceStatus(string id)
{
var message = new CurrentSourcesStateMessage
{

View file

@ -36,12 +36,16 @@ namespace PepperDash.Essentials.AppServer.Messengers
AddAction("/getDirectory", (id, content) => GetDirectoryRoot());
AddAction("/setCurrentDirectoryToRoot", (id, content) => _directory.SetCurrentDirectoryToRoot());
AddAction("/directoryById", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
GetDirectory(msg.Value);
});
AddAction("/getDirectoryPareentFolderContents", (id, content) => _directory.GetDirectoryParentFolderContents());
AddAction("/directorySearch", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
@ -52,6 +56,14 @@ namespace PepperDash.Essentials.AppServer.Messengers
_directory.DirectoryResultReturned += DirectoryResultReturned;
_directory.PhonebookSyncState.InitialSyncCompleted += PhonebookSyncState_InitialSyncCompleted;
_directory.CurrentDirectoryResultIsNotDirectoryRoot.OutputChange += (s, e) =>
{
PostStatusMessage(new IHasDirectoryStateMessage
{
DirectorySelectedFolderIsNotRoot = _directory.CurrentDirectoryResultIsNotDirectoryRoot?.BoolValue
});
};
}
private void DirectoryResultReturned(object sender, DirectoryEventArgs e)
@ -125,16 +137,22 @@ namespace PepperDash.Essentials.AppServer.Messengers
{
PostStatusMessage(new IHasDirectoryStateMessage
{
DirectoryRoot = _directory.DirectoryRoot,
CurrentDirectory = _directory.CurrentDirectoryResult,
InitialPhonebookSyncComplete = _directory.PhonebookSyncState.InitialSyncComplete,
HasDirectory = true,
HasDirectorySearch = true,
DirectorySelectedFolderName = _directory.CurrentDirectoryResult?.CurrentDirectoryResults?.Count > 0 ? _directory.CurrentDirectoryResult.CurrentDirectoryResults[0].Name : null,
DirectorySelectedFolderIsNotRoot = _directory.CurrentDirectorResultIsNotDirectoryRoot?.BoolValue
});
}
}
public class IHasDirectoryStateMessage : DeviceStateMessageBase
{
[JsonProperty("directoryRoot", NullValueHandling = NullValueHandling.Ignore)]
public CodecDirectory DirectoryRoot { get; set; }
[JsonProperty("currentDirectory", NullValueHandling = NullValueHandling.Ignore)]
public CodecDirectory CurrentDirectory { get; set; }
@ -153,5 +171,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
[JsonProperty("directorySelectedFolderName", NullValueHandling = NullValueHandling.Ignore)]
public string DirectorySelectedFolderName { get; set; }
[JsonProperty("directorySelectedFolderIsNotRoot", NullValueHandling = NullValueHandling.Ignore)]
public bool? DirectorySelectedFolderIsNotRoot { get; set; }
}
}

View file

@ -40,10 +40,11 @@ namespace PepperDash.Essentials
public bool EnableApiServer { get; set; } = true;
/// <summary>
/// Enable subscriptions for Messengers
/// Enable subscriptions for Messengers.
/// Defaults to true for v3.x+
/// </summary>
[JsonProperty("enableMessengerSubscriptions")]
public bool EnableMessengerSubscriptions { get; set; }
public bool EnableMessengerSubscriptions { get; set; } = true;
}
/// <summary>

View file

@ -1501,8 +1501,14 @@ namespace PepperDash.Essentials
}
/// <summary>
/// Handles a batch request for full status of multiple devices.
/// Triggers all registered messengers for each device key in parallel,
/// Handles a batch request for device status. Supports two content formats:
/// <para>
/// Granular format: <c>{ "devices": { "deviceKey": ["/fullStatus", "/layoutStatus"], ... } }</c>
/// </para>
/// <para>
/// Legacy format: <c>{ "deviceKeys": ["deviceKey1", "deviceKey2"] }</c> (equivalent to /fullStatus for each)
/// </para>
/// Triggers the specified action paths for each device in parallel,
/// then sends an /system/initialSyncComplete message after all have been processed.
/// </summary>
private void HandleBatchDeviceFullStatus(string clientId, JToken content)
@ -1513,11 +1519,37 @@ namespace PepperDash.Essentials
return;
}
var deviceKeys = content.SelectToken("deviceKeys")?.ToObject<List<string>>();
// Build a dictionary of deviceKey -> list of action paths
Dictionary<string, List<string>> deviceActionPaths;
if (deviceKeys == null || deviceKeys.Count == 0)
var devicesToken = content.SelectToken("devices");
if (devicesToken != null)
{
this.LogWarning("BatchDeviceFullStatus: No device keys provided");
// Granular format: { "devices": { "key": ["/path1", "/path2"], ... } }
deviceActionPaths = devicesToken.ToObject<Dictionary<string, List<string>>>();
}
else
{
// Legacy format: { "deviceKeys": ["key1", "key2"] } -> each gets /fullStatus
var deviceKeys = content.SelectToken("deviceKeys")?.ToObject<List<string>>();
if (deviceKeys == null || deviceKeys.Count == 0)
{
this.LogWarning("BatchDeviceFullStatus: No device keys or devices provided");
SendMessageObject(new MobileControlMessage
{
Type = "/system/initialSyncComplete",
ClientId = clientId
});
return;
}
deviceActionPaths = deviceKeys.ToDictionary(k => k, _ => new List<string> { "/fullStatus" });
}
if (deviceActionPaths == null || deviceActionPaths.Count == 0)
{
this.LogWarning("BatchDeviceFullStatus: Empty devices dictionary");
SendMessageObject(new MobileControlMessage
{
Type = "/system/initialSyncComplete",
@ -1526,38 +1558,49 @@ namespace PepperDash.Essentials
return;
}
this.LogInformation("BatchDeviceFullStatus: Processing {count} device keys", deviceKeys.Count);
this.LogInformation("BatchDeviceFullStatus: Processing {count} devices", deviceActionPaths.Count);
var tasks = new List<Task>();
foreach (var deviceKey in deviceKeys)
foreach (var kvp in deviceActionPaths)
{
var fullStatusPath = $"/device/{deviceKey}/fullStatus";
var deviceKey = kvp.Key;
var actionPaths = kvp.Value;
var handlers = _actionDictionary
.Where(kv => fullStatusPath.StartsWith(kv.Key + "/"))
.SelectMany(kv => kv.Value)
.ToList();
if (handlers.Count == 0)
if (actionPaths == null || actionPaths.Count == 0)
{
this.LogDebug("BatchDeviceFullStatus: No handlers for {deviceKey}", deviceKey);
continue;
}
foreach (var handler in handlers)
foreach (var actionPath in actionPaths)
{
tasks.Add(Task.Run(() =>
var fullPath = $"/device/{deviceKey}{actionPath}";
var handlers = _actionDictionary
.Where(kv => fullPath.StartsWith(kv.Key + "/"))
.SelectMany(kv => kv.Value)
.ToList();
if (handlers.Count == 0)
{
try
this.LogDebug("BatchDeviceFullStatus: No handlers for {deviceKey} at path {actionPath}", deviceKey, actionPath);
continue;
}
foreach (var handler in handlers)
{
tasks.Add(Task.Run(() =>
{
handler.Action(fullStatusPath, clientId, JToken.FromObject(new { deviceKey }));
}
catch (Exception ex)
{
this.LogError("BatchDeviceFullStatus: Exception in handler for {deviceKey}: {message}", deviceKey, ex.Message);
}
}));
try
{
handler.Action(fullPath, clientId, JToken.FromObject(new { deviceKey }));
}
catch (Exception ex)
{
this.LogError("BatchDeviceFullStatus: Exception in handler for {deviceKey} at {actionPath}: {message}", deviceKey, actionPath, ex.Message);
}
}));
}
}
}