-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDeviceMonitor.cs
More file actions
317 lines (311 loc) · 20.4 KB
/
Copy pathDeviceMonitor.cs
File metadata and controls
317 lines (311 loc) · 20.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using System.Xml.Serialization;
using System.Net.Http;
using System.IO;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Text;
using Twilio;
using Twilio.Types;
using Twilio.Clients;
using Twilio.Rest.Api.V2010.Account;
using System.Net.Mail;
using System.Net;
using System.Net.Mime;
using System.Xml;
using LicenseOTC;
namespace devm0n
{
public class DeviceMonitor : BackgroundService
{
private readonly License _License;
private readonly GlobalConfiguration _Global;
private readonly DeviceConfiguration _Device;
private readonly Dictionary<string,GroupConfiguration> _Groups;
private readonly HttpClient _HttpClient;
private readonly HttpClientHandler _HttpClientHandler;
private readonly SendGridClient _SendGridClient;
private readonly TwilioRestClient _TwilioClient;
private readonly SmtpClient _SmtpClient;
private readonly XmlSerializer _XmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(DeviceState));
private Dictionary<string,string> last_DeviceStateDict = null;
private UInt64 poll_totalCount = 0;
private UInt64 poll_changeCount = 0;
private UInt64 poll_nochangeCount = 0;
private CancellationToken _stoppingToken;
public DeviceMonitor(License License, GlobalConfiguration Global, DeviceConfiguration Device, Dictionary<string,GroupConfiguration> Groups)
{
if (Global == null || Device == null || Groups == null || License == null)
throw new ArgumentNullException("DeviceMonitor Constructor");
_License = License;
_Global = Global;
_Device = Device;
_Groups = Groups;
_HttpClientHandler = new HttpClientHandler();
_HttpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
_HttpClient = new HttpClient(_HttpClientHandler);
_SendGridClient = new SendGridClient(_Global.SendGrid.ApiKey);
_TwilioClient = new TwilioRestClient(_Global.Twilio.AccountSid,_Global.Twilio.AuthToken);
_SmtpClient = new SmtpClient(_Global.Smtp.Address,_Global.Smtp.Port);
_SmtpClient.UseDefaultCredentials = false;
_SmtpClient.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
_SmtpClient.EnableSsl = _Global.Smtp.UseSSL;
if (_Global.Smtp.UseAuth)
_SmtpClient.Credentials = new NetworkCredential(_Global.Smtp.Credential.Username,_Global.Smtp.Credential.Password);
System.Net.ServicePointManager.ServerCertificateValidationCallback = (message, cert, chain, errors) => true;
}
protected override async Task ExecuteAsync(CancellationToken StoppingToken)
{
_stoppingToken = StoppingToken;
while (!_stoppingToken.IsCancellationRequested)
{
Log.Debug($"Monitor[{_Device.Name}] polling.");
try
{
string _HttpRequestUrl = _Device.GetRequestUrl();
HttpResponseMessage _httpResponse = await _HttpClient.GetAsync(_HttpRequestUrl, HttpCompletionOption.ResponseHeadersRead);
_httpResponse.EnsureSuccessStatusCode();
Dictionary<string,string> current_DeviceStateDict = new Dictionary<string, string>();
try
{
if (_httpResponse.Content is object)
{
string _XmlDocumentData = string.Empty;
using (Stream _httpResponseStream = await _httpResponse.Content.ReadAsStreamAsync())
using (StreamReader _httpResponseStreamReader = new StreamReader(_httpResponseStream))
_XmlDocumentData = await _httpResponseStreamReader.ReadToEndAsync();
current_DeviceStateDict = await XmlToDictionary(_XmlDocumentData);
}
}
finally
{
_httpResponse.Dispose();
}
poll_totalCount++;
if (current_DeviceStateDict.Count > 0)
{
if (last_DeviceStateDict is null) { last_DeviceStateDict = current_DeviceStateDict; }
Dictionary<string,string> compare_DeviceStateDict = await DictionaryDiff(last_DeviceStateDict,current_DeviceStateDict);
if (compare_DeviceStateDict.Count > 0)
{
Log.Debug($"Monitor[{_Device.Name}] full state change detected.");
await ProcessStateChange(_Device, compare_DeviceStateDict);
}
else
poll_nochangeCount++;
Log.Information($"Monitor[{_Device.Name}] polled. total: {poll_totalCount} - no change: {poll_nochangeCount} - change: {poll_changeCount}");
last_DeviceStateDict = current_DeviceStateDict; // save current state
Log.Debug($"Monitor[{_Device.Name}] internal state updated.");
}
}
catch(Exception _Exception)
{
Log.Error(_Exception,$"Monitor[{_Device.Name}] polling error.");
}
TimeSpan _jitter = _Device.PollInterval.Next();
Log.Debug($"Monitor[{_Device.Name}] sleeping for {_jitter}");
await Task.Delay(_jitter, _stoppingToken);
}
}
private async Task ProcessStateChange(DeviceConfiguration _Device, Dictionary<string,string> _Changes)
{
bool _changeDetected = false;
Log.Debug($"Monitor[{_Device.Name}] processing state change.");
Dictionary<string,Dictionary<string,string>> notify_Groups = new Dictionary<string, Dictionary<string, string>>();
foreach(KeyValuePair<string,string> _Change in _Changes)
{
if (_Device.Fields.ContainsKey(_Change.Key))
{
//field listed see if monitered
if (_Device.Fields[_Change.Key].Enabled)
{
//we have a changed monitored field - this is a FLAGGABLE CHANGE
_changeDetected = true;
//get the group.
if (_Groups.ContainsKey(_Device.Fields[_Change.Key].Group))
{
if (_Groups[_Device.Fields[_Change.Key].Group].Enabled)
{
//we have a monitored field with a notification group
if (!notify_Groups.ContainsKey(_Device.Fields[_Change.Key].Group))
notify_Groups[_Device.Fields[_Change.Key].Group] = new Dictionary<string, string>();
notify_Groups[_Device.Fields[_Change.Key].Group][_Change.Key] = _Change.Value;
}
}
}
}
}
if (notify_Groups.Count > 0)
{
foreach(KeyValuePair<string,Dictionary<string,string>> notify_group in notify_Groups)
{
GroupConfiguration _Group = _Groups[notify_group.Key];
if (_Group.Enabled)
{
foreach(NotificationMethodConfiguration _Method in _Group.NotificationMethods)
{
if (_Method.Enabled)
{
if ((_Method.Type == NotificationType.SENDGRID) && (_License.Type >= LicenseType.ENTERPRISE)) {
Log.Debug($"Monitor[{_Device.Name}] sending notification via SendGrid to {_Method.Address}");
EmailAddress _from = new EmailAddress(_Global.SendGrid.EmailAddress);
EmailAddress _to = new EmailAddress(_Method.Address);
string _subject = $"Device {_Device.Name} state changed.";
string _plaintextContent= await DeviceStateToText(_Device,notify_group.Value);
string _htmlContent = await DeviceStateToHTML(_Device,notify_group.Value);
try {
SendGridMessage _sgMessage = MailHelper.CreateSingleEmail(_from, _to, _subject, _plaintextContent, _htmlContent);
Response _sgResponse = await _SendGridClient.SendEmailAsync(_sgMessage);
if (_sgResponse.StatusCode == System.Net.HttpStatusCode.Accepted)
Log.Debug($"Monitor[{_Device.Name}] sent notification via SendGrid to {_Method.Address}");
else
throw new HttpRequestException($"{_sgResponse.StatusCode}");
}
catch(Exception _SendGridException)
{
Log.Error(_SendGridException, $"Monitor[{_Device.Name}] failed to send notification via SendGrid to {_Method.Address}");
}
}
else if ((_Method.Type == NotificationType.TWILIO) && (_License.Type >= LicenseType.PROFESSIONAL)) {
Log.Debug($"Monitor[{_Device.Name}] sending notification via Twilio to {_Method.Address}");
PhoneNumber _from = new PhoneNumber(_Global.Twilio.PhoneNumber);
PhoneNumber _to = new PhoneNumber(_Method.Address);
string _smsContent = "State Changed\r\n";
_smsContent = _smsContent + await DeviceStateToText(_Device,notify_group.Value);
try {
MessageResource _twMessage = await MessageResource.CreateAsync(to: _to, from: _from, body: _smsContent, client: _TwilioClient);
if (_twMessage.Status == MessageResource.StatusEnum.Queued)
Log.Information($"Monitor[{_Device.Name}] sent notification via Twilio to {_Method.Address}");
else
throw new HttpRequestException($"{_twMessage.Status}");
}
catch(Exception _TwilioException)
{
Log.Error(_TwilioException, $"Monitor[{_Device.Name}] failed to send notification via Twilio to {_Method.Address}");
}
}
else if ((_Method.Type == NotificationType.SMTP) && (_License.Type >= LicenseType.STANDARD)) {
Log.Debug($"Monitor[{_Device.Name}] sending notification via Smtp");
MailAddress _from = new MailAddress(_Global.Smtp.EmailAddress);
MailAddress _to = new MailAddress(_Method.Address);
string _subject = $"Device {_Device.Name} state changed.";
string _plaintextContent= await DeviceStateToText(_Device,notify_group.Value);
string _htmlContent = await DeviceStateToHTML(_Device,notify_group.Value);
try {
MailMessage _smtpMessage = new MailMessage(_from,_to);
_smtpMessage.BodyEncoding = Encoding.UTF8;
_smtpMessage.SubjectEncoding = Encoding.UTF8;
_smtpMessage.Subject = _subject;
_smtpMessage.Body = _plaintextContent;
AlternateView _htmlView = AlternateView.CreateAlternateViewFromString(_htmlContent,Encoding.UTF8, MediaTypeNames.Text.Html);
_htmlView.ContentType = new System.Net.Mime.ContentType(MediaTypeNames.Text.Html);
_smtpMessage.AlternateViews.Add(_htmlView);
await _SmtpClient.SendMailAsync(_smtpMessage,_stoppingToken);
Log.Information($"Monitor[{_Device.Name}] sent notification via Smtp");
}
catch(Exception _SmtpException)
{
Log.Error(_SmtpException, $"Monitor[{_Device.Name}] failed to send notification via Smtp");
}
}
else
{
Log.Warning($"Monitor[{_Device.Name}] detected unknown notification of type {_Method.Type} to {_Method.Address}.");
}
}
}
}
}
}
if (_changeDetected)
poll_changeCount++;
else
poll_nochangeCount++;
Log.Debug($"Monitor[{_Device.Name}] processed state change.");
}
private Task<Dictionary<string,string>> DictionaryDiff(Dictionary<string,string> _dict1, Dictionary<string,string> _dict2)
{
Dictionary<string,string> _result = new Dictionary<string, string>();
foreach(KeyValuePair<string,string> _item in _dict1)
if (_dict2.ContainsKey(_item.Key))
if (_dict2[_item.Key] != _item.Value)
_result.Add(_item.Key,_item.Value);
foreach(KeyValuePair<string,string> _item in _dict2)
if (_dict1.ContainsKey(_item.Key))
if (_dict1[_item.Key] != _item.Value)
if (!_result.ContainsKey(_item.Key))
_result.Add(_item.Key,_item.Value);
return Task.FromResult(_result);
}
private Task<Dictionary<string,string>> XmlToDictionary(string _XmlDocumentData)
{
Dictionary<string, string> _XmlDictionary = new Dictionary<string, string>();
try {
XmlDocument _XmlDocument = new XmlDocument();
_XmlDocument.LoadXml(_XmlDocumentData);
foreach (XmlNode _XmlNode in _XmlDocument.SelectNodes("/datavalues/*"))
{
_XmlDictionary[_XmlNode.Name] = _XmlNode.InnerText;
}
return Task.FromResult(_XmlDictionary);
}
catch
{
return null;
}
}
private async Task<string> DeviceStateToText(DeviceConfiguration _Device, Dictionary<string,string> _Changes)
{
StringBuilder _builder = new StringBuilder();
_builder.AppendLine($"State Change Detected");
_builder.AppendLine($"Device: {_Device.Name}");
foreach(KeyValuePair<string,string> _Change in _Changes)
{
_builder.AppendLine($"{_Device.Fields[_Change.Key].DisplayName}: {_Change.Value}\r");
}
return _builder.ToString();
}
private Task<int> GetLongestLine(string _multiline)
{
int _longest_line=0;
foreach(string _line in _multiline.Split("\r\n"))
{
int _cur_length = _line.Length;
if (_cur_length>_longest_line)
_longest_line=_cur_length;
}
return Task.FromResult(_longest_line);
}
private Task<string> DeviceStateToHTML(DeviceConfiguration _Device, Dictionary<string,string> _Changes)
{
StringBuilder _builder = new StringBuilder();
_builder.Append("<html xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" xmlns:m=\"http://schemas.microsoft.com/office/2004/12/omml\" xmlns=\"http://www.w3.org/TR/REC-html40\">\r\n");
_builder.Append("<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\r\n<meta name=\"Generator\" content=\"Microsoft Word 15 (filtered medium)\">\r\n<style><!--\r\n/* Font Definitions */\r\n@font-face\r\n");
_builder.Append("\t{font-family:\"Cambria Math\";\r\n\tpanose-1:2 4 5 3 5 4 6 3 2 4;}\r\n@font-face\r\n\t{font-family:Calibri;\r\n\tpanose-1:2 15 5 2 2 2 4 3 2 4;}\r\n@font-face\r\n\t{font-family:Verdana;\r\n\tpanose-1:2 11 6 4 3 5 4 4 2 4;}\r\n");
_builder.Append("/* Style Definitions */\r\np.MsoNormal, li.MsoNormal, div.MsoNormal\r\n\t{margin:0in;\r\n\tmargin-bottom:.0001pt;\r\n\tfont-size:11.0pt;\r\n\tfont-family:\"Calibri\",sans-serif;}\r\nspan.EmailStyle17\r\n\t{mso-style-type:personal-compose;\r\n");
_builder.Append("\tfont-family:\"Calibri\",sans-serif;\r\n\tcolor:windowtext;}\r\n.MsoChpDefault\r\n\t{mso-style-type:export-only;\r\n\tfont-family:\"Calibri\",sans-serif;}\r\n@page WordSection1\r\n\t{size:8.5in 11.0in;\r\n\tmargin:1.0in 1.0in 1.0in 1.0in;}\r\n");
_builder.Append("div.WordSection1\r\n\t{page:WordSection1;}\r\n--></style><!--[if gte mso 9]><xml>\r\n<o:shapedefaults v:ext=\"edit\" spidmax=\"1026\" />\r\n</xml><![endif]--><!--[if gte mso 9]><xml>\r\n<o:shapelayout v:ext=\"edit\">\r\n");
_builder.Append("<o:idmap v:ext=\"edit\" data=\"1\" />\r\n</o:shapelayout></xml><![endif]-->\r\n</head>\r\n<body lang=\"EN-US\" link=\"#0563C1\" vlink=\"#954F72\">\r\n<div class=\"WordSection1\">\r\n<div align=\"center\">\r\n");
_builder.Append("<table class=\"MsoNormalTable\" border=\"1\" cellpadding=\"0\" style=\"background:#3B5FA6\">\r\n<tbody>\r\n<tr>\r\n<td colspan=\"2\" style=\"background:#7799EE;padding:3.0pt 3.0pt 3.0pt 3.0pt\">\r\n");
_builder.Append("<p class=\"MsoNormal\" align=\"center\" style=\"mso-margin-top-alt:auto;mso-margin-bottom-alt:auto;text-align:center\">\r\n");
_builder.Append($"<b><span style=\"font-size:24.0pt;font-family:"Verdana",sans-serif;color:black\">{_Device.Name}<o:p></o:p></span></b></p>\r\n</td>\r\n</tr>\r\n");
foreach(KeyValuePair<string,string> _Change in _Changes)
{
_builder.Append("<tr>\r\n<td style=\"padding:3.0pt 3.0pt 3.0pt 3.0pt\">\r\n");
_builder.Append($"<p class=\"MsoNormal\" align=\"center\" style=\"text-align:center\"><b><span style=\"font-size:10.5pt;font-family:"Verdana",sans-serif;color:white\">{_Device.Fields[_Change.Key].DisplayName}<o:p></o:p></span></b></p>\r\n");
_builder.Append("</td>\r\n<td style=\"background:gray;padding:3.0pt 3.0pt 3.0pt 3.0pt\">\r\n");
_builder.Append($"<p class=\"MsoNormal\" align=\"center\" style=\"text-align:center\"><b><span style=\"font-size:10.5pt;font-family:"Verdana",sans-serif;color:white\">{_Change.Value}<o:p></o:p></span></b></p>\r\n");
_builder.Append("</td>\r\n</tr>\r\n");
}
_builder.Append("</tbody>\r\n</table>\r\n</div>\r\n<p class=\"MsoNormal\"><o:p> </o:p></p>\r\n</div>\r\n</body>\r\n</html>\r\n");
return Task.FromResult(_builder.ToString());
}
}
}