From e1a2634f9d6546bb5f710dafbd7383a999604c4e Mon Sep 17 00:00:00 2001 From: BlubbFish Date: Thu, 7 Jun 2018 20:49:54 +0000 Subject: [PATCH] [NF] Add Bot-Utils and rewrite code --- Bot-Utils.csproj | 38 +++++++++++- Bot.cs | 90 +++++++++++++++++++++++++++ Events/CronEvent.cs | 16 +++++ Events/ModulEventArgs.cs | 15 +++++ Events/MqttEvent.cs | 16 +++++ Events/OvertakerEvent.cs | 16 +++++ Events/SenmlEvent.cs | 16 +++++ Events/StatusPollingEvent.cs | 18 ++++++ Helper.cs | 79 ++++++++++++++++++++++++ Interfaces/IForceLoad.cs | 4 ++ Moduls/AModul.cs | 70 +++++++++++++++++++++ Moduls/Mqtt.cs | 116 +++++++++++++++++++++++++++++++++++ bin/Release/Bot-Utils.dll | Bin 4096 -> 14848 bytes 13 files changed, 492 insertions(+), 2 deletions(-) create mode 100644 Bot.cs create mode 100644 Events/CronEvent.cs create mode 100644 Events/ModulEventArgs.cs create mode 100644 Events/MqttEvent.cs create mode 100644 Events/OvertakerEvent.cs create mode 100644 Events/SenmlEvent.cs create mode 100644 Events/StatusPollingEvent.cs create mode 100644 Helper.cs create mode 100644 Interfaces/IForceLoad.cs create mode 100644 Moduls/AModul.cs create mode 100644 Moduls/Mqtt.cs diff --git a/Bot-Utils.csproj b/Bot-Utils.csproj index ec2d46c..7c26c2d 100644 --- a/Bot-Utils.csproj +++ b/Bot-Utils.csproj @@ -7,10 +7,12 @@ {BB7BFCB5-3DB0-49E1-802A-3CE3EECC59F9} Library Properties - Bot_Utils + BlubbFish.Utils.IoT.Bots Bot-Utils v4.7.1 512 + + true @@ -29,6 +31,9 @@ prompt 4 + + + @@ -40,8 +45,37 @@ - + + + + + + + + + + + + + + 5.4.0.201 + + + + + {91a14cd2-2940-4500-8193-56d37edddbaa} + litjson_4.7.1 + + + {b870e4d5-6806-4a0b-b233-8907eedc5afc} + Utils-IoT + + + {fac8ce64-bf13-4ece-8097-aeb5dd060098} + Utils + + \ No newline at end of file diff --git a/Bot.cs b/Bot.cs new file mode 100644 index 0000000..ddde4fe --- /dev/null +++ b/Bot.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using BlubbFish.Utils.IoT.Bots.Moduls; +using BlubbFish.Utils.IoT.Bots.Events; +using BlubbFish.Utils.IoT.Bots.Interfaces; + +namespace BlubbFish.Utils.IoT.Bots { + public abstract class Bot { + private Thread sig_thread; + private Boolean RunningProcess = true; + protected ProgramLogger logger = new ProgramLogger(); + protected readonly Dictionary moduls = new Dictionary(); + + protected void WaitForShutdown() { + if (Type.GetType("Mono.Runtime") != null) { + this.sig_thread = new Thread(delegate () { + Mono.Unix.UnixSignal[] signals = new Mono.Unix.UnixSignal[] { + new Mono.Unix.UnixSignal(Mono.Unix.Native.Signum.SIGTERM), + new Mono.Unix.UnixSignal(Mono.Unix.Native.Signum.SIGINT) + }; + Console.WriteLine("Signalhandler Mono attached."); + while (true) { + Int32 i = Mono.Unix.UnixSignal.WaitAny(signals, -1); + Console.WriteLine("Signalhandler Mono INT recieved " + i + "."); + this.RunningProcess = false; + break; + } + }); + this.sig_thread.Start(); + } else { + Console.CancelKeyPress += new ConsoleCancelEventHandler(this.SetupShutdown); + Console.WriteLine("Signalhandler Windows attached."); + } + while (this.RunningProcess) { + Thread.Sleep(100); + } + } + + private void SetupShutdown(Object sender, ConsoleCancelEventArgs e) { + e.Cancel = true; + Console.WriteLine("Signalhandler Windows INT recieved."); + this.RunningProcess = false; + } + + protected void ModulDispose() { + foreach (KeyValuePair item in this.moduls) { + ((AModul)item.Value).Dispose(); + Console.WriteLine("Modul entladen: " + item.Key); + } + if (this.sig_thread != null && this.sig_thread.IsAlive) { + this.sig_thread.Abort(); + } + } + + protected void ModulLoader(String @namespace, Object library) { + Assembly asm = Assembly.GetEntryAssembly(); + foreach (Type item in asm.GetTypes()) { + if (item.Namespace == @namespace) { + Type t = item; + String name = t.Name; + if (InIReader.ConfigExist(name.ToLower())) { + this.moduls.Add(name, (AModul)t.GetConstructor(new Type[] { typeof(T), typeof(InIReader) }).Invoke(new Object[] { library, InIReader.GetInstance(name.ToLower()) })); + Console.WriteLine("Load Modul " + name); + } else if (t.HasInterface(typeof(IForceLoad))) { + this.moduls.Add(name, (AModul)t.GetConstructor(new Type[] { typeof(T), typeof(InIReader) }).Invoke(new Object[] { library, null })); + Console.WriteLine("Load Modul Forced " + name); + } + } + } + } + + protected void ModulInterconnect() { + foreach (KeyValuePair item in this.moduls) { + ((AModul)item.Value).Interconnect(this.moduls); + } + } + + protected void ModulEvents() { + foreach (KeyValuePair item in this.moduls) { + ((AModul)item.Value).Update += this.ModulUpdate; + } + } + + protected void ModulUpdate(Object sender, ModulEventArgs e) { + Console.WriteLine(e.ToString()); + } + } +} diff --git a/Events/CronEvent.cs b/Events/CronEvent.cs new file mode 100644 index 0000000..cb7237e --- /dev/null +++ b/Events/CronEvent.cs @@ -0,0 +1,16 @@ +using System; + +namespace BlubbFish.Utils.IoT.Bots.Events { + public class CronEvent : ModulEventArgs { + + public CronEvent() { + } + + public CronEvent(String addr, String prop, String value) { + this.Address = addr; + this.Property = prop; + this.Value = value; + this.Source = "Cronjob"; + } + } +} diff --git a/Events/ModulEventArgs.cs b/Events/ModulEventArgs.cs new file mode 100644 index 0000000..ee44173 --- /dev/null +++ b/Events/ModulEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace BlubbFish.Utils.IoT.Bots.Events { + public class ModulEventArgs : EventArgs { + public ModulEventArgs() { + } + public String Address { get; protected set; } + public String Property { get; protected set; } + public String Value { get; protected set; } + public String Source { get; protected set; } + public override String ToString() { + return this.Source + ": " + this.Address + " set " + this.Property + " to " + this.Value; + } + } +} diff --git a/Events/MqttEvent.cs b/Events/MqttEvent.cs new file mode 100644 index 0000000..fa30fa7 --- /dev/null +++ b/Events/MqttEvent.cs @@ -0,0 +1,16 @@ +using System; + +namespace BlubbFish.Utils.IoT.Bots.Events { + public class MqttEvent : ModulEventArgs { + public MqttEvent() { + } + public MqttEvent(String topic, String text) { + this.Address = topic; + this.Value = text; + this.Source = "MQTT"; + } + public override String ToString() { + return this.Source + ": on " + this.Address + " set " + this.Value; + } + } +} diff --git a/Events/OvertakerEvent.cs b/Events/OvertakerEvent.cs new file mode 100644 index 0000000..0af0525 --- /dev/null +++ b/Events/OvertakerEvent.cs @@ -0,0 +1,16 @@ +using System; + +namespace BlubbFish.Utils.IoT.Bots.Events { + public class OvertakerEvent : ModulEventArgs { + + public OvertakerEvent() { + } + + public OvertakerEvent(String addr, String prop, String value) { + this.Address = addr; + this.Property = prop; + this.Value = value; + this.Source = "Overtaker"; + } + } +} diff --git a/Events/SenmlEvent.cs b/Events/SenmlEvent.cs new file mode 100644 index 0000000..fcdfce2 --- /dev/null +++ b/Events/SenmlEvent.cs @@ -0,0 +1,16 @@ +using System; + +namespace BlubbFish.Utils.IoT.Bots.Events { + public class SenmlEvent : ModulEventArgs { + public SenmlEvent() { + } + public SenmlEvent(String topic, String text) { + this.Address = topic; + this.Value = text; + this.Source = "Senml"; + } + public override String ToString() { + return this.Source + ": on " + this.Address + " set " + this.Value; + } + } +} diff --git a/Events/StatusPollingEvent.cs b/Events/StatusPollingEvent.cs new file mode 100644 index 0000000..70238ed --- /dev/null +++ b/Events/StatusPollingEvent.cs @@ -0,0 +1,18 @@ +using System; + +namespace BlubbFish.Utils.IoT.Bots.Events { + public class StatusPollingEvent : ModulEventArgs { + public StatusPollingEvent() { + } + + public StatusPollingEvent(String text, String node) { + this.Value = text; + this.Address = node; + this.Source = "POLLING"; + } + + public override String ToString() { + return this.Source + ": " + this.Value + " on " + this.Address; + } + } +} diff --git a/Helper.cs b/Helper.cs new file mode 100644 index 0000000..34fd3e7 --- /dev/null +++ b/Helper.cs @@ -0,0 +1,79 @@ +using System; +using System.Reflection; + +namespace BlubbFish.Utils.IoT.Bots { + public static class Helper { + #region PropertyHelper + public static Boolean HasProperty(this Object o, String type) { + Type t = o.GetType(); + foreach (PropertyInfo item in t.GetProperties()) { + if (item.Name == type) { + return true; + } + } + return false; + } + + public static Object GetProperty(this Object o, String name) { + PropertyInfo prop = o.GetType().GetProperty(name); + if (prop.CanRead) { + return prop.GetValue(o); + } + return null; + } + + public static void SetProperty(this Object o, String name, String value) { + PropertyInfo prop = o.GetType().GetProperty(name); + if (prop.CanWrite) { + if (prop.PropertyType == typeof(Boolean) && Boolean.TryParse(value, out Boolean vb)) { + prop.SetValue(o, vb); + } else if (prop.PropertyType == typeof(Int32) && Int32.TryParse(value, out Int32 v32)) { + prop.SetValue(o, v32); + } else if (prop.PropertyType == typeof(Single) && Single.TryParse(value, out Single vs)) { + prop.SetValue(o, vs); + } else if (prop.PropertyType == typeof(Double) && Double.TryParse(value, out Double vd)) { + prop.SetValue(o, vd); + } else if (prop.PropertyType == typeof(Int64) && Int64.TryParse(value, out Int64 v64)) { + prop.SetValue(o, v64); + } + } + } + #endregion + + #region InterfaceHelper + public static Boolean HasInterface(this Type o, Type interf) { + foreach (Type item in o.GetInterfaces()) { + if (item == interf) { + return true; + } + } + return false; + } + + public static Boolean HasAbstract(this Type o, Type type) { + if (o.BaseType == type) { + return true; + } + return false; + } + #endregion + + #region StringHelper + public static String ToUpperLower(this String s) { + if (s.Length == 0) { + return ""; + } + if (s.Length == 1) { + return s.ToUpper(); + } + return s[0].ToString().ToUpper() + s.Substring(1).ToLower(); + } + + public static void WriteError(String text) { + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine("ERROR: " + text); + Console.ResetColor(); + } + #endregion + } +} diff --git a/Interfaces/IForceLoad.cs b/Interfaces/IForceLoad.cs new file mode 100644 index 0000000..eb89e6a --- /dev/null +++ b/Interfaces/IForceLoad.cs @@ -0,0 +1,4 @@ +namespace BlubbFish.Utils.IoT.Bots.Interfaces { + public interface IForceLoad { + } +} diff --git a/Moduls/AModul.cs b/Moduls/AModul.cs new file mode 100644 index 0000000..2745e60 --- /dev/null +++ b/Moduls/AModul.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using BlubbFish.Utils.IoT.Bots.Events; + +namespace BlubbFish.Utils.IoT.Bots.Moduls { + public abstract class AModul { + protected T library; + private readonly InIReader settings; + protected Dictionary> config = new Dictionary>(); + + public Boolean HasConfig { get; private set; } + public Boolean ConfigPublic { get; private set; } + + public delegate void ModulEvent(Object sender, ModulEventArgs e); + public abstract event ModulEvent Update; + + public AModul(T lib, InIReader settings) { + this.HasConfig = false; + this.ConfigPublic = false; + this.library = lib; + this.settings = settings; + this.ParseConfig(); + } + + private void ParseConfig() { + if (this.settings != null) { + this.HasConfig = true; + foreach (String item in this.settings.GetSections(false)) { + this.config.Add(item, this.settings.GetSection(item)); + } + if (this.config.ContainsKey("modul")) { + this.ConfigPublic = this.config["modul"].ContainsKey("config") && this.config["modul"]["config"].ToLower() == "public"; + } + } + } + + public Dictionary> GetConfig() { + if (this.HasConfig && this.ConfigPublic) { + Dictionary> ret = new Dictionary>(this.config); + if (ret.ContainsKey("modul")) { + ret.Remove("modul"); + } + return ret; + } + return new Dictionary>(); + } + + public virtual void Interconnect(Dictionary moduls) { } + + public virtual void SetInterconnection(String param, Action hook, Object data) { } + + public abstract void Dispose(); + + public void SetConfig(Dictionary> newconf) { + if (this.HasConfig && this.ConfigPublic) { + if (newconf.ContainsKey("modul")) { + newconf.Remove("modul"); + } + if (this.config.ContainsKey("modul")) { + newconf.Add("modul", this.config["modul"]); + } + this.config = newconf; + this.settings.SetSections(this.config); + this.UpdateConfig(); + } + } + + protected abstract void UpdateConfig(); + } +} diff --git a/Moduls/Mqtt.cs b/Moduls/Mqtt.cs new file mode 100644 index 0000000..62424ab --- /dev/null +++ b/Moduls/Mqtt.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using BlubbFish.Utils.IoT.Bots.Events; +using BlubbFish.Utils.IoT.Connector; +using BlubbFish.Utils.IoT.Events; +using LitJson; + +namespace BlubbFish.Utils.IoT.Bots.Moduls { + public abstract class Mqtt : AModul, IDisposable { + protected readonly Thread connectionWatcher; + protected ABackend mqtt; + protected Dictionary modules; + + #region Constructor + public Mqtt(T lib, InIReader settings) : base(lib, settings) { + if (this.config.ContainsKey("settings")) { + this.connectionWatcher = new Thread(this.ConnectionWatcherRunner); + this.connectionWatcher.Start(); + } + } + #endregion + + #region Watcher + protected void ConnectionWatcherRunner() { + while (true) { + try { + if (this.mqtt == null || !this.mqtt.IsConnected) { + this.Reconnect(); + } + Thread.Sleep(10000); + } catch (Exception) { } + } + } + + protected void Reconnect() { + this.Disconnect(); + this.Connect(); + } + + protected abstract void Connect(); + + protected abstract void Disconnect(); + #endregion + + #region AModul + public override void Interconnect(Dictionary moduls) { + this.modules = moduls; + } + + protected override void UpdateConfig() { + this.Reconnect(); + } + #endregion + + protected Tuple ChangeConfig(BackendEvent e, String topic) { + if (e.From.ToString().StartsWith(topic) && (e.From.ToString().EndsWith("/set") || e.From.ToString().EndsWith("/get"))) { + Match m = new Regex("^"+ topic + "(\\w+)/[gs]et$|").Match(e.From.ToString()); + if (!m.Groups[1].Success) { + return new Tuple(false, null); + } + AModul modul = null; + foreach (KeyValuePair item in this.modules) { + if (item.Key.ToLower() == m.Groups[1].Value) { + modul = ((AModul)item.Value); + } + } + if (modul == null) { + return new Tuple(false, null); + } + if (e.From.ToString().EndsWith("/get") && modul.HasConfig && modul.ConfigPublic) { + String t = topic + m.Groups[1].Value; + String d = JsonMapper.ToJson(modul.GetConfig()).ToString(); + ((ADataBackend)this.mqtt).Send(t, d); + return new Tuple(true, new MqttEvent(t, d)); + } else if (e.From.ToString().EndsWith("/set") && modul.HasConfig && modul.ConfigPublic) { + try { + JsonData a = JsonMapper.ToObject(e.Message); + Dictionary> newconf = new Dictionary>(); + foreach (String section in a.Keys) { + Dictionary sectiondata = new Dictionary(); + foreach (String item in a[section].Keys) { + sectiondata.Add(item, a[section][item].ToString()); + } + newconf.Add(section, sectiondata); + } + modul.SetConfig(newconf); + return new Tuple(true, new MqttEvent("New Config", "Write")); + } catch (Exception) { } + } + } + return new Tuple(false, null); + } + + #region IDisposable Support + private Boolean disposedValue = false; + + protected void Dispose(Boolean disposing) { + if (!this.disposedValue) { + if (disposing) { + this.connectionWatcher.Abort(); + while (this.connectionWatcher.ThreadState == ThreadState.Running) { Thread.Sleep(10); } + this.Disconnect(); + } + this.disposedValue = true; + } + } + + public override void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/bin/Release/Bot-Utils.dll b/bin/Release/Bot-Utils.dll index b1fa95c925c81bdc73bdc831b394e1dccacf2168..42947bfa1cf24f9ab2d92381379a777d7d12e380 100644 GIT binary patch literal 14848 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 bwe!;8r{lIuearLm>YomG{@;uL+X(z8vqHXJ delta 1016 zcmX|=Ur3Wt7{;ITIp^k|x#b_!#J)LWRyv#$G!&USG_`O!m5EZ$xn%X}(C-VYJ1HZjuCMe3 z-;|CaQw~tA=CKCALWcR+9C1^tQuR{>>{2U$NrtDxfK>XU$7Z6@1ER+d+{;Qki}ELc ze2U+n2P(N!spF%}ojaaGmP*ya*O<%dCne^~@r%`c$q^RgjIa z3K-b{c9o5yUHUK1=RNUSYy1maLbYmVO()kY6GBOghU*lGCcZkc)^*5*raeJJqCD z>5xHC6&;Vt_GvJqFW9F|If!PI6LWEaXopwwvSf#3ujF;fQOO9g1aYb7Bo`!?h-P?@ z2N$sjb;Pqt^i$))5b+YCM4x15(cYOb>W7WErhBF| z4WR|yh|Odg&tUA97Kq;AT|Ffe(WGBbM_ zeSGxc_2%ZlYQbDp_k*vWU{V>gWT{};BpePA+5{BK?&A1JdDFovpY#6k04GmS{I4qM zY7gnH9MTIJ3rNzy6eBmd) d6>e*4W&Z8Hrrj*7di?&v&D)NapDf*w^B>4VvAO^N