From fb5f8db88652eb4998de6940ad6fe4d61b9840d6 Mon Sep 17 00:00:00 2001 From: sp00n Date: Wed, 29 May 2024 18:21:32 +0200 Subject: [PATCH 1/3] Change path detection for liquidctl.exe The path should now always resolve to the same as the one where the FanControl.Liquidctl.dll is --- LiquidctlCLIWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiquidctlCLIWrapper.cs b/LiquidctlCLIWrapper.cs index e10c152..f27a428 100644 --- a/LiquidctlCLIWrapper.cs +++ b/LiquidctlCLIWrapper.cs @@ -8,7 +8,7 @@ namespace FanControl.Liquidctl { internal static class LiquidctlCLIWrapper { - public static string liquidctlexe = "Plugins\\liquidctl.exe"; //TODO extract path to executable to config + public static string liquidctlexe = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "liquidctl.exe"); // This should always resolve to the same directory as the FanControl.Liquidctl.dll //TODO extract path to executable to config internal static void Initialize() { LiquidctlCall($"--json initialize all"); From 1dbb10d914273f6575ba2e312496f6ff93e20558 Mon Sep 17 00:00:00 2001 From: sp00n Date: Thu, 30 May 2024 21:58:05 +0200 Subject: [PATCH 2/3] - Added support for fan controllers that return multiple fan entries ("Fan 1 speed", "Fan 2 speed", etc) - I probably violated a dozen of best practices though - Fixed a bug where status values that are not numbers/floats prevented the plugin from working - Replaced the hardcoded location of the liquidctl.exe to match the directory where the FanControl.Liquidctl.dll is located. So you can now also install the plugin over the Fan Control interface --- .gitignore | 4 + FanControl.Liquidctl.csproj | 4 +- LiquidctlCLIWrapper.cs | 46 ++++++++++- LiquidctlDevice.cs | 151 ++++++++++++++++++++++++++++++++++-- LiquidctlPlugin.cs | 30 ++++++- LiquidctlStatusJSON.cs | 4 +- README.md | 9 +++ 7 files changed, 234 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index ea5c60c..44ecc48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ +*.git-credentials FanControl.Liquidctl.zip liquidctl/ liquidctl.exe +include/ + + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## diff --git a/FanControl.Liquidctl.csproj b/FanControl.Liquidctl.csproj index 1cfdb8b..72abac3 100644 --- a/FanControl.Liquidctl.csproj +++ b/FanControl.Liquidctl.csproj @@ -32,10 +32,10 @@ - ..\..\..\..\Documents\FanControl\FanControl.Plugins.dll + include\FanControl.Plugins.dll - ..\..\..\..\Documents\FanControl\Newtonsoft.Json.dll + include\Newtonsoft.Json.dll diff --git a/LiquidctlCLIWrapper.cs b/LiquidctlCLIWrapper.cs index f27a428..f93d8ef 100644 --- a/LiquidctlCLIWrapper.cs +++ b/LiquidctlCLIWrapper.cs @@ -1,28 +1,41 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using Newtonsoft.Json; +using FanControl.Plugins; +using Newtonsoft.Json.Linq; + namespace FanControl.Liquidctl { internal static class LiquidctlCLIWrapper { public static string liquidctlexe = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "liquidctl.exe"); // This should always resolve to the same directory as the FanControl.Liquidctl.dll //TODO extract path to executable to config - internal static void Initialize() + internal static IPluginLogger logger; + + internal static void Initialize(IPluginLogger pluginLogger) { + logger = pluginLogger; + LiquidctlCall($"--json initialize all"); } + internal static List ReadStatus() { Process process = LiquidctlCall($"--json status"); - return JsonConvert.DeserializeObject>(process.StandardOutput.ReadToEnd()); + //return JsonConvert.DeserializeObject>(process.StandardOutput.ReadToEnd()); + return ParseStatuses(process.StandardOutput.ReadToEnd()); } + internal static List ReadStatus(string address) { Process process = LiquidctlCall($"--json --address {address} status"); - return JsonConvert.DeserializeObject>(process.StandardOutput.ReadToEnd()); + //return JsonConvert.DeserializeObject>(process.StandardOutput.ReadToEnd()); + return ParseStatuses(process.StandardOutput.ReadToEnd()); } + internal static void SetPump(string address, int value) { LiquidctlCall($"--address {address} set pump speed {(value)}"); @@ -33,6 +46,11 @@ internal static void SetFan(string address, int value) LiquidctlCall($"--address {address} set fan speed {(value)}"); } + internal static void SetFanNumber(string address, int index, int value) + { + LiquidctlCall($"--address {address} set fan{index} speed {(value)}"); + } + private static Process LiquidctlCall(string arguments) { Process process = new Process(); @@ -56,5 +74,27 @@ private static Process LiquidctlCall(string arguments) return process; } + + + // Code by akotulu + // See https://github.com/jmarucha/FanControl.Liquidctl/pull/29/commits/145978bdf1c2d1a464b2a036b4fc26f559bb77dc#diff-d7a2c0cf4c270870ed263c55d2cd4fc41258347085a3cded3a78b48e73f78092 + private static List ParseStatuses(string json) { + JArray statusArray = JArray.Parse(json); + List statuses = new List(); + + foreach (JObject statusObject in statusArray) { + try + { + LiquidctlStatusJSON status = statusObject.ToObject(); + statuses.Add(status); + } + catch (Exception e) + { + logger.Log($"Unable to parse {statusObject}\n{e.Message}"); + } + } + + return statuses; + } } } diff --git a/LiquidctlDevice.cs b/LiquidctlDevice.cs index e21883f..1187438 100644 --- a/LiquidctlDevice.cs +++ b/LiquidctlDevice.cs @@ -15,6 +15,7 @@ public LiquidTemperature(LiquidctlStatusJSON output) _name = $"Liquid Temp. - {output.description}"; UpdateFromJSON(output); } + public void UpdateFromJSON(LiquidctlStatusJSON output) { _value = (float)output.status.Single(entry => entry.key == KEY).value; @@ -33,6 +34,7 @@ public void UpdateFromJSON(LiquidctlStatusJSON output) public void Update() { } // plugin updates sensors } + public class PumpSpeed : IPluginSensor { public PumpSpeed(LiquidctlStatusJSON output) @@ -41,6 +43,7 @@ public PumpSpeed(LiquidctlStatusJSON output) _name = $"Pump - {output.description}"; UpdateFromJSON(output); } + public void UpdateFromJSON(LiquidctlStatusJSON output) { _value = (float)output.status.Single(entry => entry.key == KEY).value; @@ -59,6 +62,7 @@ public void UpdateFromJSON(LiquidctlStatusJSON output) public void Update() { } // plugin updates sensors } + public class PumpDuty : IPluginControlSensor { public PumpDuty(LiquidctlStatusJSON output) @@ -87,6 +91,7 @@ public void UpdateFromJSON(LiquidctlStatusJSON output) {2780, 90}, {2789, 91}, {2798, 92}, {2807, 93}, {2816, 94}, {2825, 95}, {2834, 96}, {2843, 97}, {2852, 98}, {2861, 99}, {MAX_RPM, 100} }; + static readonly int MAX_RPM = 2870; public string Id => _id; @@ -122,6 +127,7 @@ public FanSpeed(LiquidctlStatusJSON output) _name = $"Fan - {output.description}"; UpdateFromJSON(output); } + public void UpdateFromJSON(LiquidctlStatusJSON output) { _value = (float)output.status.Single(entry => entry.key == KEY).value; @@ -150,6 +156,7 @@ public FanControl(LiquidctlStatusJSON output) _name = $"Fan Control - {output.description}"; UpdateFromJSON(output); } + // We can only estimate, as it is not provided in any output public void UpdateFromJSON(LiquidctlStatusJSON output) { @@ -171,6 +178,7 @@ public void UpdateFromJSON(LiquidctlStatusJSON output) {1530, 90}, {1550, 91}, {1570, 92}, {1590, 93}, {1610, 94}, {1630, 95}, {1650, 96}, {1670, 97}, {1690, 98}, {1720, 99}, {MAX_RPM, 100} }; + static readonly int MAX_RPM = 1980; public string Id => _id; @@ -197,32 +205,145 @@ public void Update() { } // plugin updates sensors } + // Try to get the speeds for multiple fans + public class FanSpeedMultiple : IPluginSensor + { + public FanSpeedMultiple(int index, LiquidctlStatusJSON output) + { + _id = $"{output.address}-fan{index}rpm"; + _name = $"Fan {index} - {output.description}"; + UpdateFromJSON(index, output); + } + + public void UpdateFromJSON(int index, LiquidctlStatusJSON output) + { + string currentKey = KEY.Replace("###", index.ToString()); + _value = (float) output.status.Single(entry => entry.key == currentKey).value; + } + + public static string KEY = "Fan ### speed"; + + public string Id => _id; + readonly string _id; + + public string Name => _name; + readonly string _name; + + public float? Value => _value; + float _value; + + public void Update() { } // plugin updates sensors + } + + // Try to control multiple fans + public class FanControlMultiple : IPluginControlSensor + { + public FanControlMultiple(int index, LiquidctlStatusJSON output) + { + _address = output.address; + _id = $"{output.address}-fan{index}ctrl"; + _name = $"Fan {index} Control - {output.description}"; + _index = index; + + UpdateFromJSON(index, output); + } + + // We can only estimate, as it is not provided in any output + public void UpdateFromJSON(int index, LiquidctlStatusJSON output) { + string currentKey = FanSpeedMultiple.KEY.Replace("###", index.ToString()); + float reading = (float)output.status.Single(entry => entry.key == currentKey).value; + //_value = reading > MAX_RPM ? 100.0f : (float)Math.Ceiling(100.0f * reading / MAX_RPM); + _value = RPM_LOOKUP.OrderBy(e => Math.Abs(e.Key - reading)).FirstOrDefault().Value; + } + + public static string KEY = "Fan ### speed"; + //public static string KEY = $"Fan {_index} speed"; + + static readonly Dictionary RPM_LOOKUP = new Dictionary + { // We can only estimate, as it is not provided in any output. Hence I applied this ugly hack + {520, 20}, {521, 21}, {522, 22}, {523, 23}, {524, 24}, {525, 25}, {526, 26}, {527, 27}, {528, 28}, {529, 29}, + {530, 30}, {532, 31}, {534, 32}, {536, 33}, {538, 34}, {540, 35}, {542, 36}, {544, 37}, {546, 38}, {548, 39}, + {550, 40}, {571, 41}, {592, 42}, {613, 43}, {634, 44}, {655, 45}, {676, 46}, {697, 47}, {718, 48}, {739, 49}, + {760, 50}, {781, 51}, {802, 52}, {823, 53}, {844, 54}, {865, 55}, {886, 56}, {907, 57}, {928, 58}, {949, 59}, + {970, 60}, {989, 61}, {1008, 62}, {1027, 63}, {1046, 64}, {1065, 65}, {1084, 66}, {1103, 67}, {1122, 68}, {1141, 69}, + {1160, 70}, {1180, 71}, {1200, 72}, {1220, 73}, {1240, 74}, {1260, 75}, {1280, 76}, {1300, 77}, {1320, 78}, {1340, 79}, + {1360, 80}, {1377, 81}, {1394, 82}, {1411, 83}, {1428, 84}, {1445, 85}, {1462, 86}, {1479, 87}, {1496, 88}, {1513, 89}, + {1530, 90}, {1550, 91}, {1570, 92}, {1590, 93}, {1610, 94}, {1630, 95}, {1650, 96}, {1670, 97}, {1690, 98}, {1720, 99}, + {MAX_RPM, 100} + }; + + static readonly int MAX_RPM = 1980; + + public string Id => _id; + readonly string _id; + string _address; + + public string Name => _name; + readonly string _name; + + public float? Value => _value; + float _value; + + public int Index => _index; + int _index; + + public void Reset() + { + Set(50.0f); + } + + public void Set(float val) + { + LiquidctlCLIWrapper.SetFanNumber(_address, _index, (int) val); + } + + public void Update() { } // plugin updates sensors + } + public LiquidctlDevice(LiquidctlStatusJSON output, IPluginLogger pluginLogger) { logger = pluginLogger; address = output.address; hasPumpSpeed = output.status.Exists(entry => entry.key == PumpSpeed.KEY && !(entry.value is null)); - if (hasPumpSpeed) + if (hasPumpSpeed) { pumpSpeed = new PumpSpeed(output); + } hasPumpDuty = output.status.Exists(entry => entry.key == PumpDuty.KEY && !(entry.value is null)); - if (hasPumpDuty) + if (hasPumpDuty) { pumpDuty = new PumpDuty(output); + } hasFanSpeed = output.status.Exists(entry => entry.key == FanSpeed.KEY && !(entry.value is null)); - if(hasFanSpeed) - { + if (hasFanSpeed) { fanSpeed = new FanSpeed(output); fanControl = new FanControl(output); } hasLiquidTemperature = output.status.Exists(entry => entry.key == LiquidTemperature.KEY && !(entry.value is null)); - if (hasLiquidTemperature) + if (hasLiquidTemperature) { liquidTemperature = new LiquidTemperature(output); + } + + + // Get the info for multiple fans + for (int i=0; i<20; i++) { + int index = i+1; + string currentKey = FanSpeedMultiple.KEY.Replace("###", index.ToString()); + hasMultipleFanSpeed[i] = output.status.Exists(entry => entry.key == currentKey && !(entry.value is null)); + + if (hasMultipleFanSpeed[i]) { + fanSpeedMultiple[i] = new FanSpeedMultiple(index, output); + fanControlMultiple[i] = new FanControlMultiple(index, output); + } + } } + public readonly bool hasPumpSpeed, hasPumpDuty, hasLiquidTemperature, hasFanSpeed; + public readonly bool[] hasMultipleFanSpeed = new bool[20]; + public void UpdateFromJSON(LiquidctlStatusJSON output) { @@ -233,8 +354,16 @@ public void UpdateFromJSON(LiquidctlStatusJSON output) fanSpeed.UpdateFromJSON(output); fanControl.UpdateFromJSON(output); } + + for (int i = 0; i<20; i++) { + if (hasMultipleFanSpeed[i]) { + fanSpeedMultiple[i].UpdateFromJSON(i+1, output); + fanControlMultiple[i].UpdateFromJSON(i+1, output); + } + } } + internal IPluginLogger logger; public string address; public LiquidTemperature liquidTemperature; @@ -243,6 +372,10 @@ public void UpdateFromJSON(LiquidctlStatusJSON output) public FanSpeed fanSpeed; public FanControl fanControl; + public FanSpeedMultiple[] fanSpeedMultiple = new FanSpeedMultiple[20]; + public FanControlMultiple[] fanControlMultiple = new FanControlMultiple[20]; + + public void LoadJSON() { try @@ -256,12 +389,20 @@ public void LoadJSON() } } + public String GetDeviceInfo() { String ret = $"Device @ {address}"; if (hasLiquidTemperature) ret += $", Liquid @ {liquidTemperature.Value}"; if (hasPumpSpeed) ret += $", Pump @ {pumpSpeed.Value}"; if (hasPumpDuty) ret += $"({pumpDuty.Value})"; if (hasFanSpeed) ret += $", Fan @ {fanSpeed.Value} ({fanControl.Value})"; + + for (int i = 0; i<20; i++) { + if (hasMultipleFanSpeed[i]) { + ret += $", Fan{i+1} @ {fanSpeedMultiple[i].Value} ({fanControlMultiple[i].Value})"; + } + } + return ret; } } diff --git a/LiquidctlPlugin.cs b/LiquidctlPlugin.cs index 5494460..85f04a9 100644 --- a/LiquidctlPlugin.cs +++ b/LiquidctlPlugin.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using FanControl.Plugins; @@ -18,28 +21,48 @@ public LiquidctlPlugin(IPluginLogger pluginLogger) public void Initialize() { - - LiquidctlCLIWrapper.Initialize(); + LiquidctlCLIWrapper.Initialize(logger); } public void Load(IPluginSensorsContainer _container) { List input = LiquidctlCLIWrapper.ReadStatus(); + foreach (LiquidctlStatusJSON liquidctl in input) { LiquidctlDevice device = new LiquidctlDevice(liquidctl, logger); logger.Log(device.GetDeviceInfo()); + if (device.hasPumpSpeed) + { _container.FanSensors.Add(device.pumpSpeed); + } + if (device.hasPumpDuty) + { _container.ControlSensors.Add(device.pumpDuty); + } + if (device.hasLiquidTemperature) + { _container.TempSensors.Add(device.liquidTemperature); + } + if (device.hasFanSpeed) { _container.FanSensors.Add(device.fanSpeed); _container.ControlSensors.Add(device.fanControl); } + + for (int i = 0; i<20; i++) + { + if (device.hasMultipleFanSpeed[i]) + { + _container.FanSensors.Add(device.fanSpeedMultiple[i]); + _container.ControlSensors.Add(device.fanControlMultiple[i]); + } + } + devices.Add(device); } } @@ -48,6 +71,7 @@ public void Close() { devices.Clear(); } + public void Update() { foreach (LiquidctlDevice device in devices) diff --git a/LiquidctlStatusJSON.cs b/LiquidctlStatusJSON.cs index 3886c47..ff77dee 100644 --- a/LiquidctlStatusJSON.cs +++ b/LiquidctlStatusJSON.cs @@ -6,7 +6,9 @@ public class LiquidctlStatusJSON public class StatusRecord { public string key { get; set; } - public float? value { get; set; } + //public float? value { get; set; } // float fails if the returned value is not a number (e.g. for "Fan control mode", which returns "PWM" or "DC") + // Setting it to dynamic seems to fix the issue(?) + public dynamic value { get; set; } public string unit { get; set; } } public string bus { get; set; } diff --git a/README.md b/README.md index 8f22ad5..c035b95 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # FanControl.Liquidctl + +## Mod by sp00n +I tried to add support for fan controllers that return multiple fan entries (e.g. "Fan 1 speed", "Fan 2 speed", etc), so that controllers like the ones from NZXT are supported. +I probably violated a dozen of best practices for C# in doing so, but it appears to be working for me. + + + +## Original description + This is a simple plugin that uses [liquidctl](https://github.com/liquidctl/liquidctl) to provide sensor data and pump control to variety of AIOs. So far it is tested with NZXT Kraken X63, but in principle shall work with [supported devices](https://github.com/liquidctl/liquidctl#supported-devices) ## Installation From 9bf83af38dc1ae37ed50f626f53b9ad1b1a3aa64 Mon Sep 17 00:00:00 2001 From: sp00n Date: Thu, 30 May 2024 22:31:48 +0200 Subject: [PATCH 3/3] - Comment update --- LiquidctlCLIWrapper.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/LiquidctlCLIWrapper.cs b/LiquidctlCLIWrapper.cs index f93d8ef..a135017 100644 --- a/LiquidctlCLIWrapper.cs +++ b/LiquidctlCLIWrapper.cs @@ -12,7 +12,8 @@ namespace FanControl.Liquidctl { internal static class LiquidctlCLIWrapper { - public static string liquidctlexe = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "liquidctl.exe"); // This should always resolve to the same directory as the FanControl.Liquidctl.dll //TODO extract path to executable to config + public static string liquidctlexe = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "liquidctl.exe"); // This should always resolve to the same directory as the FanControl.Liquidctl.dll + // TODO: extract path to executable to config(?) - Seems to work fine now though internal static IPluginLogger logger; internal static void Initialize(IPluginLogger pluginLogger) @@ -25,14 +26,12 @@ internal static void Initialize(IPluginLogger pluginLogger) internal static List ReadStatus() { Process process = LiquidctlCall($"--json status"); - //return JsonConvert.DeserializeObject>(process.StandardOutput.ReadToEnd()); return ParseStatuses(process.StandardOutput.ReadToEnd()); } internal static List ReadStatus(string address) { Process process = LiquidctlCall($"--json --address {address} status"); - //return JsonConvert.DeserializeObject>(process.StandardOutput.ReadToEnd()); return ParseStatuses(process.StandardOutput.ReadToEnd()); }