Last active
June 21, 2021 02:52
-
-
Save masonticehurst/1fddf0042fb520deba80100259f73e7b to your computer and use it in GitHub Desktop.
Antminer Monitor Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace AntminerMonitor | |
{ | |
public class STATUS | |
{ | |
// API return status | |
public int Code { get; set; } | |
public string Description { get; set; } | |
} | |
public class STAT | |
{ | |
// Version Info | |
public string BMMiner { get; set; } | |
public string Miner { get; set; } | |
// API Info | |
public string CompileTime { get; set; } | |
public int? STATS { get; set; } | |
// Device type (Antminer S9i, etc) | |
public string Type { get; set; } | |
// ???? | |
public string ID { get; set; } | |
// 5 second & total GH/s averages | |
public double GHS_FiveSecond { get; set; } | |
public double GHS_Average { get; set; } | |
// Total miners | |
public int? miner_count { get; set; } | |
// Miner hash frequency | |
public string frequency { get; set; } | |
// Number of fans and RPM (Rounds per min) | |
public int? fan_num { get; set; } | |
public int? fan3 { get; set; } | |
public int? fan6 { get; set; } | |
// Total number of temperature relays | |
public int? temp_num { get; set; } | |
// PCB Temps | |
public int? temp6 { get; set; } | |
public int? temp7 { get; set; } | |
public int? temp8 { get; set; } | |
// Chip Temps | |
public int? temp2_6 { get; set; } | |
public int? temp2_7 { get; set; } | |
public int? temp2_8 { get; set; } | |
// Board frequencies | |
public double? freq_avg6 { get; set; } | |
public double? freq_avg7 { get; set; } | |
public double? freq_avg8 { get; set; } | |
// Ideal hashrate | |
public double? total_rateideal { get; set; } | |
// Total frequency average | |
public double? total_freqavg { get; set; } | |
// ???? | |
public int? total_acn { get; set; } | |
public double? total_rate { get; set; } | |
// Average hashrate per chair | |
public double? chain_rateideal6 { get; set; } | |
public double? chain_rateideal7 { get; set; } | |
public double? chain_rateideal8 { get; set; } | |
// Max temperature | |
public int? temp_max { get; set; } | |
// ?????? | |
public int? no_matching_work { get; set; } | |
// ?????? | |
public int? chain_acn6 { get; set; } | |
public int? chain_acn7 { get; set; } | |
public int? chain_acn8 { get; set; } | |
// Hashboard chain status | |
public string chain_acs6 { get; set; } | |
public string chain_acs7 { get; set; } | |
public string chain_acs8 { get; set; } | |
// Hashboard chain hardware errors | |
public int? chain_hw6 { get; set; } | |
public int? chain_hw7 { get; set; } | |
public int? chain_hw8 { get; set; } | |
// Hashboard GH/s | |
public double chain_rate6 { get; set; } | |
public double chain_rate7 { get; set; } | |
public double chain_rate8 { get; set; } | |
// Unique miner ID and firmware number | |
public string miner_version { get; set; } | |
public string miner_id { get; set; } | |
} | |
public class APIStatus | |
{ | |
public List<STATUS> STATUS { get; set; } | |
public List<STAT> STATS { get; set; } | |
public int id { get; set; } | |
} | |
public class Extras | |
{ | |
public string pool_name { get; set; } | |
public string pool_link { get; set; } | |
} | |
public class TXN_DATA | |
{ | |
public int height { get; set; } | |
public int reward_block { get; set; } | |
public int reward_fees { get; set; } | |
public long timestamp { get; set; } | |
public Extras extras { get; set; } | |
} | |
public class BlockData | |
{ | |
public TXN_DATA data { get; set; } | |
public int err_no { get; set; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace AntminerMonitor | |
{ | |
public class USD | |
{ | |
public string code { get; set; } | |
public string symbol { get; set; } | |
public string rate { get; set; } | |
public string description { get; set; } | |
public double rate_float { get; set; } | |
} | |
public class Bpi | |
{ | |
public USD USD { get; set; } | |
} | |
public class BTCPrice | |
{ | |
public Bpi bpi { get; set; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Diagnostics; | |
using System.Collections.Generic; | |
using Newtonsoft.Json; | |
using System.Net.NetworkInformation; | |
using System.Drawing; | |
using Console = Colorful.Console; | |
using System.Threading.Tasks; | |
using Telegram.Bot.Types.Enums; | |
using System.Threading; | |
using System.Net; | |
using TimeAgo; | |
namespace AntminerMonitor | |
{ | |
class Program | |
{ | |
public class Miner | |
{ | |
public string id { get; set; } | |
public string ip { get; set; } | |
} | |
const string TELEGRAM_API = "HIDDEN_FOR_SECURITY"; | |
const string TELEGRAM_USER = "HIDDEN_FOR_SECURITY"; | |
const string POOL_NAME = "SlushPool"; | |
const string POOL_LINK = "https://slushpool.com/"; | |
const string POOL_ENDPOINT = "https://slushpool.com/stats/json/"; | |
const string POOL_API_KEY = "HIDDEN_FOR_SECURITY"; | |
const string BOARD_OK = " oooooooo oooooooo oooooooo oooooooo oooooooo oooooooo oooooooo ooooooo"; | |
const int NOT_MATURE = -1; | |
// Global temp data | |
static int? g_LATEST_BLOCK = null; | |
// Telegram init & send message | |
static async Task SendTelegramMessage( string sMessage ) | |
{ | |
var botClient = new Telegram.Bot.TelegramBotClient(TELEGRAM_API); | |
var me = await botClient.GetMeAsync(); | |
botClient.SendTextMessageAsync(TELEGRAM_USER, sMessage, ParseMode.Markdown, true, true).GetAwaiter().GetResult(); | |
} | |
// Pull recent BTC price from coindesk API (GET) | |
static double getBTCPrice() | |
{ | |
string url = "https://api.coindesk.com/v1/bpi/currentprice.json"; | |
string target = string.Empty; | |
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); | |
HttpWebResponse response = (HttpWebResponse)request.GetResponse(); | |
StreamReader streamReader = new StreamReader(response.GetResponseStream(), true); | |
try | |
{ | |
target = streamReader.ReadToEnd(); | |
} | |
finally | |
{ | |
streamReader.Close(); | |
} | |
BTCPrice latest_price = JsonConvert.DeserializeObject<BTCPrice>(target); | |
return latest_price.bpi.USD.rate_float; | |
} | |
// Pull recent SlushPool stats/block rewards (GET) | |
static SlushAPI getRewardAmount() | |
{ | |
string url = POOL_ENDPOINT + POOL_API_KEY; | |
string target = string.Empty; | |
// Yes this part is stolen from StackOverflow, come at me | |
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); | |
HttpWebResponse response = (HttpWebResponse)request.GetResponse(); | |
StreamReader streamReader = new StreamReader(response.GetResponseStream(), true); | |
try | |
{ | |
target = streamReader.ReadToEnd(); | |
} | |
finally | |
{ | |
streamReader.Close(); | |
} | |
SlushAPI latest_slush = JsonConvert.DeserializeObject<SlushAPI>(target); | |
return latest_slush; | |
} | |
// Space filler | |
static void createSpacer(int iAmount) | |
{ | |
for (int i = 0; i < iAmount; i++) | |
{ | |
Console.WriteLine(""); | |
} | |
} | |
// Warn user (Console beep & telegram notification) | |
static void warnUser(string sMinerID, string sMessage) | |
{ | |
for (int i = 0; i < 5; i++) | |
{ | |
// Fuck this is annoying | |
Console.Beep(3000, 200); | |
} | |
SendTelegramMessage("[" + sMinerID + "] " + sMessage).Wait(); | |
} | |
// Pull latest block number from btc.com API (version 3) -- (GET) | |
static BlockData getLatestBlock(string iBlock) | |
{ | |
string toQuery = iBlock; | |
if (toQuery == null) | |
{ | |
toQuery = "latest"; | |
} | |
string url = "https://chain.api.btc.com/v3/block/" + toQuery; | |
string target = string.Empty; | |
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); | |
HttpWebResponse response = (HttpWebResponse)request.GetResponse(); | |
StreamReader streamReader = new StreamReader(response.GetResponseStream(), true); | |
try | |
{ | |
target = streamReader.ReadToEnd(); | |
} | |
finally | |
{ | |
streamReader.Close(); | |
} | |
BlockData latest_block = JsonConvert.DeserializeObject<BlockData>(target); | |
g_LATEST_BLOCK = latest_block.data.height; | |
return latest_block; | |
} | |
// Convert returned unix timestamp to local time | |
static void ReportTimestamp(long dTimeStamp) | |
{ | |
DateTime UNIX_TIMESTAMP = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | |
UNIX_TIMESTAMP = UNIX_TIMESTAMP.AddSeconds(dTimeStamp).ToLocalTime(); | |
Console.WriteLine("\t\tLast SlushPool block solved: ", Color.WhiteSmoke); | |
Console.WriteLine("\t\t\t" + UNIX_TIMESTAMP.TimeAgo(), Color.Fuchsia); | |
createSpacer(1); | |
} | |
// Loop through btc.com blocks until block hash been solved by SlushPool/slushpool.com | |
static long getSlushBlockTime() | |
{ | |
BlockData b = getLatestBlock(null); | |
int iCurBlock = b.data.height; | |
// Reference const globals | |
while (b.data.extras.pool_name != POOL_NAME && b.data.extras.pool_link != POOL_LINK) | |
{ | |
// Sleep main thread to prevent rate-limiting | |
Thread.Sleep(500); | |
iCurBlock -= 1; | |
b = getLatestBlock(iCurBlock.ToString()); | |
} | |
// Push timestamp report | |
ReportTimestamp(b.data.timestamp); | |
return b.data.timestamp; | |
} | |
// Begin running async | |
// Moved to wait for countdown func | |
//Task.Run(() => getSlushBlockTime()); | |
static string getData(string sIP) | |
{ | |
string JSONReturn = ""; | |
var p = new Process | |
{ | |
StartInfo = new ProcessStartInfo | |
{ | |
FileName = "cmd.exe", | |
Arguments = "/k echo {\"command\":\"stats\"} | nc -w 1 " + sIP + " 4028", | |
UseShellExecute = false, | |
RedirectStandardOutput = true, | |
RedirectStandardInput = true, | |
CreateNoWindow = true, | |
} | |
}; | |
p.Start(); | |
p.StandardInput.Close(); | |
JSONReturn = p.StandardOutput.ReadToEnd(); | |
// Fix broken returned JSON structure | |
JSONReturn = JSONReturn.Replace("\"Type\":\"Antminer S9i\"}", "\"Type\":\"Antminer S9i\"},"); | |
// Remove remaining directory text from exit | |
JSONReturn = JSONReturn.Replace(Directory.GetCurrentDirectory() + ">", ""); | |
// Allow "GHS 5s" and "GHS av" to be deserialized without errors | |
JSONReturn = JSONReturn.Replace("GHS 5s", "GHS_FiveSecond"); | |
JSONReturn = JSONReturn.Replace("GHS av", "GHS_Average"); | |
return JSONReturn; | |
} | |
// Determine if miner is active on network | |
static bool pingMiner(string sIP) | |
{ | |
bool OK = false; | |
Ping pingRequest = new Ping(); | |
PingReply pingReply = pingRequest.Send(sIP, 1 * 1000); | |
if (pingReply.Status == IPStatus.Success) | |
{ | |
OK = true; | |
} | |
return OK; | |
} | |
static void Main(string[] args) | |
{ | |
// Allow use of special unicode characters (bitcoin, etc) | |
Console.OutputEncoding = System.Text.Encoding.UTF8; | |
// Initialization Message (Telegram) | |
SendTelegramMessage("Antminer monitor starting...").Wait(); | |
// Grab list of listening addresses | |
string sListeningJSON = System.IO.File.ReadAllText(@"C:\Users\kpjVideo\Downloads\listen_addresses.txt"); | |
dynamic dData = JsonConvert.DeserializeObject(sListeningJSON); | |
while (true) | |
{ | |
foreach ( var miner in dData ) | |
{ | |
// Returned as JSON objects, get their actual values and cache | |
var _IP = miner.ip.Value; | |
var _ID = miner.id.Value; | |
if ( pingMiner( _IP ) ) | |
{ | |
try | |
{ | |
string d = getData(_IP); | |
Console.Write("Status: "); | |
Console.WriteLine(_ID, Color.Magenta); | |
APIStatus api_return = JsonConvert.DeserializeObject<APIStatus>(d); | |
STAT api_stats = api_return.STATS[1]; | |
// Hashboard array for iteration | |
double[] hashboard_rates = { api_stats.chain_rate6, api_stats.chain_rate7, api_stats.chain_rate8 }; | |
int i = 0; | |
foreach (double dRate in hashboard_rates) | |
{ | |
i++; | |
if (dRate < 3500.00) | |
{ | |
Console.Write("ERROR: "); | |
string err = "Hashboard #" + i + " performing very far from ideal hashrates: " + dRate + "!!!"; | |
Console.WriteLine(err, Color.Red); | |
warnUser(_ID, "ERROR: " + err); | |
} | |
else if (dRate < 4300.00) | |
{ | |
Console.Write("Warning: "); | |
string err = "Hashboard #" + i + " performing below ideal hashrate: " + dRate; | |
Console.WriteLine(err, Color.Orange); | |
warnUser(_ID, "Warning: " + err); | |
} | |
} | |
// Fan # | |
int? fan_num = api_stats.fan_num; | |
// Fan Speed | |
int? fanSpeed1 = api_stats.fan3; | |
int? fanSpeed2 = api_stats.fan6; | |
// PCB Temps | |
int? PCBTemp1 = api_stats.temp6; | |
int? PCBTemp2 = api_stats.temp7; | |
int? PCBTemp3 = api_stats.temp8; | |
// Chip temps | |
int? ChipTemp1 = api_stats.temp2_6; | |
int? ChipTemp2 = api_stats.temp2_7; | |
int? ChipTemp3 = api_stats.temp2_8; | |
// Board status' | |
string boardOK1 = api_stats.chain_acs6; | |
string boardOK2 = api_stats.chain_acs7; | |
string boardOK3 = api_stats.chain_acs8; | |
// Board HR (Hashrate in GH/s) | |
double boardRate1 = api_stats.chain_rate6; | |
double boardRate2 = api_stats.chain_rate7; | |
double boardRate3 = api_stats.chain_rate8; | |
// Create array of temps for iteration | |
int?[] temps = { PCBTemp1, PCBTemp2, PCBTemp3 }; | |
Console.WriteLine("\t\t====== Temperature Report =====", Color.White); | |
int iCnt = 0; | |
foreach (int iTemperature in temps) | |
{ | |
iCnt++; | |
Color cCol = Color.Red; | |
if (iTemperature <= 60) | |
{ | |
cCol = Color.DarkGreen; | |
} | |
else if (iTemperature <= 70) | |
{ | |
cCol = Color.Green; | |
} | |
else if (iTemperature <= 80) | |
{ | |
cCol = Color.Orange; | |
} | |
else | |
{ | |
cCol = Color.Red; | |
warnUser(_ID, "Temperature of PCB on board #" + iCnt + "above 80C degrees! (" + iTemperature + ")"); | |
} | |
Console.Write("\t#" + iCnt + " (PCB): ", Color.WhiteSmoke); | |
Console.Write(iTemperature, cCol); | |
} | |
createSpacer(1); | |
int?[] ctemps = { ChipTemp1, ChipTemp2, ChipTemp3 }; | |
int iCntChipTemp = 0; | |
foreach (int iTemperature in ctemps) | |
{ | |
iCntChipTemp++; | |
Color cCol = Color.Red; | |
if (iTemperature <= 70) | |
{ | |
cCol = Color.DarkGreen; | |
} | |
else if (iTemperature <= 80) | |
{ | |
cCol = Color.Green; | |
} | |
else if (iTemperature <= 90) | |
{ | |
cCol = Color.DarkOrange; | |
} | |
else if (iTemperature <= 100) | |
{ | |
cCol = Color.Orange; | |
} | |
else | |
{ | |
cCol = Color.Red; | |
warnUser(_ID, "Temperature of chip on board #" + iCntChipTemp + " above 100C degrees! (" + iTemperature + ")"); | |
} | |
Console.Write("\t#" + iCnt + " (Chip): ", Color.WhiteSmoke); | |
Console.Write(iTemperature, cCol); | |
} | |
createSpacer(1); | |
Console.WriteLine("\t\t====== Fan Airflow Report =====", Color.White); | |
int?[] fanReport = { fanSpeed1, fanSpeed2 }; | |
int iFanNum = 0; | |
foreach (int iFanSpeed in fanReport) | |
{ | |
iFanNum++; | |
Color cFanOK = Color.Red; | |
if (iFanSpeed > 5500) | |
{ | |
cFanOK = Color.Green; | |
} | |
else if (iFanSpeed > 5000) | |
{ | |
cFanOK = Color.DarkGreen; | |
} | |
else if (iFanSpeed > 4000) | |
{ | |
cFanOK = Color.Orange; | |
} | |
else if (iFanSpeed > 3000) | |
{ | |
cFanOK = Color.DarkOrange; | |
} | |
else | |
{ | |
cFanOK = Color.Red; | |
warnUser(_ID, "Fan speed on fan #" + iFanNum + " below 3000 RPM!"); | |
} | |
string fanType = "Intake"; | |
if (iFanNum == 2) | |
{ | |
fanType = "Exhaust"; | |
} | |
Console.Write("\t#" + iFanNum + " (" + fanType + "): ", Color.WhiteSmoke); | |
Console.Write(iFanSpeed + " RPM", cFanOK); | |
} | |
createSpacer(1); | |
double[] boardHashRates = { boardRate1, boardRate2, boardRate3 }; | |
Console.WriteLine("\t\t======= HashRate Report =======", Color.White); | |
int iCurrentBoard = 0; | |
foreach( double boardHashRate in boardHashRates) | |
{ | |
double boardTerraHash = boardHashRate / 1000; | |
iCurrentBoard++; | |
Color cBoardColor = Color.Red; | |
if (boardTerraHash >= 4.4) | |
{ | |
cBoardColor = Color.Green; | |
} | |
else if (boardTerraHash < 4.4) | |
{ | |
cBoardColor = Color.Orange; | |
} | |
else | |
{ | |
cBoardColor = Color.Red; | |
warnUser(_ID, "ERROR: Hashboard #" + iCurrentBoard + " below 3 T/H! (" + boardTerraHash + ")"); | |
} | |
Console.Write("\tHash Board #" + iCurrentBoard + ": ", Color.WhiteSmoke); | |
Console.Write("\t\t\t " + String.Format("{0:0.00}", Math.Round( boardTerraHash, 2 )), cBoardColor); | |
Console.WriteLine(" TH/s", Color.WhiteSmoke); | |
} | |
Console.Write("\t"); | |
for (int iEqual = 0; iEqual <= 45; iEqual++) | |
{ | |
Console.Write("=", Color.WhiteSmoke); | |
} | |
createSpacer(1); | |
double realHR = Math.Round(api_stats.GHS_Average / 1000, 2); | |
Color cColHR = Color.Red; | |
if(realHR >= 13.80) | |
{ | |
cColHR = Color.Green; | |
} | |
else if(realHR >= 13.00) | |
{ | |
cColHR = Color.Orange; | |
} | |
Console.Write("\tTotal Hash Rate:\t\t", Color.WhiteSmoke); | |
Console.Write(" " + String.Format("{0:0.00}", realHR), cColHR); | |
Console.WriteLine(" TH/s", Color.GhostWhite); | |
createSpacer(1); | |
if (realHR < 13.00) | |
{ | |
Console.WriteLine("\tLOW HASHRATE DETECTED -- CHECK HASHBOARDS!", Color.Red); | |
} | |
const string HASH_PROBLEM = " Hashboard issue detected!"; | |
const string HASH_OK = " All hashboards are reporting OK status"; | |
Console.Write("\t"); | |
if (boardOK1 == BOARD_OK && boardOK2 == BOARD_OK && boardOK3 == BOARD_OK) | |
{ | |
// Board reported 100% OK status | |
Console.WriteLine(HASH_OK, Color.DarkGreen); | |
} | |
else | |
{ | |
// Board reported less than 100% OK status | |
Console.WriteLine(HASH_PROBLEM, Color.Red); | |
warnUser(_ID, HASH_PROBLEM); | |
} | |
createSpacer(1); | |
} | |
catch (Exception s) | |
{ | |
Console.WriteLine("ERROR: " + s.ToString()); | |
} | |
} | |
else | |
{ | |
// Cannot ping miner. Warn user and continue | |
string err_conn = "Error contacting " + _ID + " @ " + _IP; | |
Console.WriteLine(err_conn, Color.Red); | |
warnUser(_ID, err_conn); | |
} | |
} | |
getSlushBlockTime(); | |
void getBlockReward() | |
{ | |
try | |
{ | |
SlushAPI currData = getRewardAmount(); | |
foreach (KeyValuePair<int, Block> b in currData.Blocks) | |
{ | |
if (b.Key == g_LATEST_BLOCK) | |
{ | |
float reward = float.Parse(b.Value.Reward); | |
Console.WriteLine("\t\tLast SlushPool block reward:", Color.WhiteSmoke); | |
// Determine block maturity, less than 0 is not mature | |
if (b.Value.is_mature <= NOT_MATURE) | |
{ | |
Console.WriteLine("\t\t Block rewards processing", Color.Orange); | |
} | |
else | |
{ | |
Console.WriteLine("\t\t\t$" + Math.Round(reward * getBTCPrice(), 2) + " USD", Color.Green); | |
Console.WriteLine("\t\t\tɃ" + Math.Round(reward, 6), Color.DarkOrange); | |
} | |
} | |
} | |
} | |
catch | |
{ | |
Console.WriteLine("\tError contacting CoinDesk or " + POOL_NAME + " API!", Color.Red); | |
} | |
} | |
// Async block reward | |
//Task.Run(() => getBlockReward()); | |
getBlockReward(); | |
Console.Write("Next query: ", Color.WhiteSmoke); | |
for (int a = 60; a >= 0; a--) | |
{ | |
Console.CursorLeft = 14; | |
Console.Write(a + " seconds", Color.WhiteSmoke); | |
System.Threading.Thread.Sleep(1000); | |
} | |
Console.Clear(); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace AntminerMonitor | |
{ | |
public partial class SlushAPI | |
{ | |
public Dictionary<int, Block> Blocks { get; set; } | |
public string GhashesPs { get; set; } | |
} | |
public partial class Block | |
{ | |
public int is_mature { get; set; } | |
public string Reward { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment