using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp.Net; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.DeviceTypeInterfaces; namespace PepperDash.Essentials.AppServer.Messengers { /// /// Provides a messaging bridge /// public abstract class MessengerBase : EssentialsDevice, IMobileControlMessengerWithSubscriptions { /// /// The device this messenger is associated with /// protected IKeyName _device; /// /// Enable subscriptions /// protected bool enableMessengerSubscriptions; /// /// List of clients subscribed to this messenger /// /// /// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection. /// private readonly HashSet subscriberIds = new HashSet(); /// /// Lock object for thread-safe access to SubscriberIds /// private readonly object _subscriberLock = new object(); private readonly List _deviceInterfaces; private readonly Dictionary> _actions = new Dictionary>(); /// /// Gets the DeviceKey /// public string DeviceKey => _device?.Key ?? ""; /// /// Gets or sets the AppServerController /// public IMobileControl AppServerController { get; private set; } /// /// Gets or sets the MessagePath /// public string MessagePath { get; private set; } /// /// /// /// /// protected MessengerBase(string key, string messagePath) : base(key) { Key = key; if (string.IsNullOrEmpty(messagePath)) throw new ArgumentException("messagePath must not be empty or null"); MessagePath = messagePath; } /// /// Constructor for a messenger associated with a device /// /// /// /// protected MessengerBase(string key, string messagePath, IKeyName device) : this(key, messagePath) { _device = device; _deviceInterfaces = GetInterfaces(_device as Device); } /// /// Gets the interfaces implmented on the device /// /// /// private List GetInterfaces(Device device) { return device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); } /// /// Registers this messenger with appserver controller /// /// public void RegisterWithAppServer(IMobileControl appServerController) { AppServerController = appServerController ?? throw new ArgumentNullException("appServerController"); AppServerController.AddAction(this, HandleMessage); RegisterActions(); } /// /// Register this messenger with appserver controller /// /// Parent controller for this messenger /// Enable subscriptions public void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions) { this.enableMessengerSubscriptions = enableMessengerSubscriptions; AppServerController = appServerController ?? throw new ArgumentNullException("appServerController"); AppServerController.AddAction(this, HandleMessage); RegisterActions(); } private void HandleMessage(string path, string id, JToken content) { // replace base path with empty string. Should leave something like /fullStatus var route = path.Replace(MessagePath, string.Empty); if (!_actions.TryGetValue(route, out var action)) { return; } this.LogDebug("Executing action for path {path}", path); action(id, content); } /// /// Adds an action for a given path /// /// /// protected void AddAction(string path, Action action) { if (_actions.ContainsKey(path)) { return; } _actions.Add(path, action); } /// /// GetActionPaths method /// public List GetActionPaths() { return _actions.Keys.ToList(); } /// /// Removes an action for a given path /// /// protected void RemoveAction(string path) { if (!_actions.ContainsKey(path)) { return; } _actions.Remove(path); } /// /// Implemented in extending classes. Wire up API calls and feedback here /// protected virtual void RegisterActions() { } /// /// Add client to the susbscription list for unsolicited feedback /// /// Client ID to add protected void SubscribeClient(string clientId) { if (!enableMessengerSubscriptions) { return; } lock (_subscriberLock) { if (!subscriberIds.Add(clientId)) { this.LogVerbose("Client {clientId} already subscribed", clientId); return; } } this.LogDebug("Client {clientId} subscribed", clientId); } /// /// Remove a client from the subscription list /// /// Client ID to remove public void UnsubscribeClient(string clientId) { if (!enableMessengerSubscriptions) { return; } bool wasSubscribed; lock (_subscriberLock) { wasSubscribed = subscriberIds.Contains(clientId); if (wasSubscribed) { subscriberIds.Remove(clientId); } } if (!wasSubscribed) { this.LogVerbose("Client with ID {clientId} is not subscribed", clientId); return; } this.LogDebug("Client with ID {clientId} unsubscribed", clientId); } /// /// Helper for posting status message /// /// /// Optional client id that will direct the message back to only that client protected void PostStatusMessage(DeviceStateMessageBase message, string clientId = null) { try { if (message == null) { throw new ArgumentNullException("message"); } if (_device == null) { throw new ArgumentNullException("device"); } message.SetInterfaces(_deviceInterfaces); message.Key = _device.Key; message.Name = _device.Name; var token = JToken.FromObject(message); PostStatusMessage(token, MessagePath, clientId); } catch (Exception ex) { this.LogError("Exception posting status message for {messagePath} to {clientId}: {message}", MessagePath, clientId ?? "all clients", ex.Message); this.LogDebug(ex, "Stack trace: "); } } /// /// Helper for posting status message /// /// /// /// Optional client id that will direct the message back to only that client protected void PostStatusMessage(string type, DeviceStateMessageBase deviceState, string clientId = null) { try { //Debug.Console(2, this, "*********************Setting DeviceStateMessageProperties on MobileControlResponseMessage"); deviceState.SetInterfaces(_deviceInterfaces); deviceState.Key = _device.Key; deviceState.Name = _device.Name; deviceState.MessageBasePath = MessagePath; var token = JToken.FromObject(deviceState); PostStatusMessage(token, type, clientId); } catch (Exception ex) { this.LogError("Exception posting status message for {type} to {clientId}: {message}", type, clientId ?? "all clients", ex.Message); this.LogDebug(ex, "Stack trace: "); } } /// /// Helper for posting status message /// /// /// /// Optional client id that will direct the message back to only that client protected void PostStatusMessage(JToken content, string type = "", string clientId = null) { try { // Allow for legacy method to continue without subscriptions if (!enableMessengerSubscriptions) { AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content }); return; } // handle subscription feedback // If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties if (string.IsNullOrEmpty(clientId)) { // Create a snapshot of subscribers to avoid collection modification during iteration List subscriberSnapshot; lock (_subscriberLock) { subscriberSnapshot = new List(subscriberIds); } foreach (var client in subscriberSnapshot) { AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content }); } return; } SubscribeClient(clientId); AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content }); } catch (Exception ex) { this.LogError("Exception posting status message: {message}", ex.Message); this.LogDebug(ex, "Stack Trace: "); } } /// /// Helper for posting event message /// /// protected void PostEventMessage(DeviceEventMessageBase message) { message.Key = _device.Key; message.Name = _device.Name; AppServerController?.SendMessageObject(new MobileControlMessage { Type = $"/event{MessagePath}/{message.EventType}", Content = JToken.FromObject(message), }); } /// /// Helper for posting event message /// /// /// protected void PostEventMessage(DeviceEventMessageBase message, string eventType) { message.Key = _device.Key; message.Name = _device.Name; message.EventType = eventType; AppServerController?.SendMessageObject(new MobileControlMessage { Type = $"/event{MessagePath}/{eventType}", Content = JToken.FromObject(message), }); } /// /// Helper for posting event message with no content /// /// protected void PostEventMessage(string eventType) { AppServerController?.SendMessageObject(new MobileControlMessage { Type = $"/event{MessagePath}/{eventType}", Content = JToken.FromObject(new { }), }); } } }