It works!
This commit is contained in:
		
							
								
								
									
										19
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | Copyright © 2017 Scott Alfter | ||||||
|  |  | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a | ||||||
|  | copy of this software and associated documentation files (the "Software"), | ||||||
|  | to deal in the Software without restriction, including without limitation | ||||||
|  | the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||||||
|  | and/or sell copies of the Software, and to permit persons to whom the | ||||||
|  | Software is furnished to do so, subject to the following conditions: | ||||||
|  |  | ||||||
|  | The above copyright notice and this permission notice shall be included in | ||||||
|  | all copies or substantial portions of the Software. | ||||||
|  |  | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL | ||||||
|  | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||||||
|  | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||||||
|  | DEALINGS IN THE SOFTWARE. | ||||||
							
								
								
									
										28
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,20 +1,21 @@ | |||||||
| zpool auto-switcher | zpool auto-switcher | ||||||
| =================== | =================== | ||||||
|  |  | ||||||
| TODO: documentation...still early days, but should be similar to my NiceHash | This is an auto-switcher for zpool (www.zpool.ca) that chooses the most | ||||||
| and MiningPoolHub switchers. | profitable coin based on the statistics provided by zpool and the hashrates | ||||||
|  | supported by your GPUs.  It's similar to my MiningPoolHub and NiceHash | ||||||
|  | auto-switchers, and is set up similarly, except that configuration is | ||||||
|  | somewhat more modularized.  In the future, the other switchers will change | ||||||
|  | to a similar configuration so that it will be possible to share most of your | ||||||
|  | configuration between the different pools. | ||||||
|  |  | ||||||
| zpool's API is largely undocumented.  A further complication is that their | I started this under nvOC, but have since migrated my mining rig from nvOC | ||||||
| profitability estimates aren't reported in the same units for each | to a standard Gentoo Linux setup. | ||||||
| algorithm, and while some deviations from the norm are noted on their |  | ||||||
| homepage, others aren't.  I'm mostly going by trial and error, and by |  | ||||||
| comparison with what MiningPoolHub reports for the algorithms the two pools |  | ||||||
| have in common. |  | ||||||
|  |  | ||||||
| miners.json | miners.json | ||||||
| =========== | =========== | ||||||
|  |  | ||||||
| Should ultimately be able to share this with the MiningPoolHub and NiceHash | We should ultimately be able to share this with the MiningPoolHub and NiceHash | ||||||
| switchers.  Each available algorithm is described with the following values: | switchers.  Each available algorithm is described with the following values: | ||||||
|  |  | ||||||
| bin: command line to launch miner.  {HOST}, {PORT}, {USERNAME}, and | bin: command line to launch miner.  {HOST}, {PORT}, {USERNAME}, and | ||||||
| @@ -29,6 +30,8 @@ power: total power consumption, kW | |||||||
| gpu_oc, mem_oc, and fan may be provided as a single value each to be applied | gpu_oc, mem_oc, and fan may be provided as a single value each to be applied | ||||||
| to all cards or as an array each to set cards individually. | to all cards or as an array each to set cards individually. | ||||||
|  |  | ||||||
|  | Any additional fields (such as last_benchmark) are ignored. | ||||||
|  |  | ||||||
| algo_map.json | algo_map.json | ||||||
| ============= | ============= | ||||||
|  |  | ||||||
| @@ -36,6 +39,9 @@ Since different pools may use different labels for the same algorithm, we | |||||||
| need to map what the pool uses to what we used in miners.json.  The index is | need to map what the pool uses to what we used in miners.json.  The index is | ||||||
| the pool's label; the value is our label. | the pool's label; the value is our label. | ||||||
|  |  | ||||||
|  | When zpool adds a new algorithm, this file is automatically updated with an | ||||||
|  | empty entry for the new algorithm. | ||||||
|  |  | ||||||
| conf.json | conf.json | ||||||
| ========= | ========= | ||||||
|  |  | ||||||
| @@ -51,4 +57,6 @@ payment_currency: how you want zpool to pay you | |||||||
| payment_addr: where zpool should send payment | payment_addr: where zpool should send payment | ||||||
|  |  | ||||||
| The user_name and miner_name values in the example aren't used by the zpool | The user_name and miner_name values in the example aren't used by the zpool | ||||||
| switcher, but may be used by other switchers. | switcher, but may be used by other switchers.  As with miners.json, we might | ||||||
|  | be able to share this file with other pool auto-switchers. | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										160
									
								
								zpool-switch.py
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								zpool-switch.py
									
									
									
									
									
								
							| @@ -1,33 +1,25 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
| # API description: http://www.zpool.ca/site/api | # API description (such as it is): http://www.zpool.ca/site/api | ||||||
| # estimates: values in mBTC/MH/day,  |  | ||||||
| #                      mBTC/PH/day for sha256 |  | ||||||
| #		       mBTC/GH/day for scrypt, blake, decred, x11, quark, qubit |  | ||||||
| #		       mBTC/kS/day for equihash |  | ||||||
|  |  | ||||||
| import pprint | import pprint | ||||||
| import json | import json | ||||||
| import urllib.request |  | ||||||
| import urllib.parse |  | ||||||
| import sys | import sys | ||||||
| import datetime | import datetime | ||||||
| import time | import time | ||||||
| import subprocess | import subprocess | ||||||
| import os | import os | ||||||
| import socket | import socket | ||||||
|  | import urllib.request | ||||||
|  | import urllib.parse | ||||||
|  |  | ||||||
| # load config | # load config | ||||||
|  |  | ||||||
| DEBUG=False |  | ||||||
|  |  | ||||||
| cfg=json.loads(open(sys.argv[1]).read()) | cfg=json.loads(open(sys.argv[1]).read()) | ||||||
| miners=json.loads(open(sys.argv[2]).read()) | miners=json.loads(open(sys.argv[2]).read()) | ||||||
| algo_map=json.loads(open(sys.argv[3]).read()) | algo_map=json.loads(open(sys.argv[3]).read()) | ||||||
|  |  | ||||||
| card_type=cfg["card_type"] | card_type=cfg["card_type"] | ||||||
| #user_name=cfg["user_name"] |  | ||||||
| #miner_name=cfg["miner_name"] |  | ||||||
| currency=cfg["currency"] | currency=cfg["currency"] | ||||||
| pwrcost=cfg["pwrcost"] | pwrcost=cfg["pwrcost"] | ||||||
| min_profit=cfg["min_profit"] | min_profit=cfg["min_profit"] | ||||||
| @@ -36,29 +28,20 @@ payment_addr=cfg["payment_addr"] | |||||||
|  |  | ||||||
| os.environ["DISPLAY"]=":0" | os.environ["DISPLAY"]=":0" | ||||||
|  |  | ||||||
| # IPv4 address lookup |  | ||||||
|  |  | ||||||
| def addr(host): |  | ||||||
|   return [addr[4][0] for addr in socket.getaddrinfo(host, None) if addr[0] == socket.AF_INET][0] |  | ||||||
|  |  | ||||||
| # grab something from a website | # grab something from a website | ||||||
|  |  | ||||||
| def fetch(prot, host, path, forceipv4=False): | def fetch(url): | ||||||
|   if (forceipv4): |   r=urllib.request.Request(url, None, {"User-Agent": "Lynx/2.8.8dev.3 libwww-FM/2.14 SSL-MM/1.4.1", "Pragma": "no-cache"}) | ||||||
|     url=prot+"://"+addr(host)+"/"+path |  | ||||||
|   else: |  | ||||||
|     url=prot+"://"+host+"/"+path |  | ||||||
|   r=urllib.request.Request(url, None, {"User-Agent": "Lynx/2.8.8dev.3 libwww-FM/2.14 SSL-MM/1.4.1", "Pragma": "no-cache", "Host": host}) |  | ||||||
|   return urllib.request.urlopen(r).read().decode("utf-8") |   return urllib.request.urlopen(r).read().decode("utf-8") | ||||||
|  |  | ||||||
| # main | # main | ||||||
|  |  | ||||||
| if (DEBUG!=True): | try: | ||||||
|   exchrate=float(json.loads(fetch("https", "api.coinbase.com", "/v2/exchange-rates?currency=BTC"))["data"]["rates"][currency]) |   exchrate=float(json.loads(fetch("https://api.coinbase.com/v2/exchange-rates?currency=BTC"))["data"]["rates"][currency]) | ||||||
|   data=json.loads(fetch("http", "www.zpool.ca", "/api/status", True)) |   data=json.loads(fetch("http://www.zpool.ca/api/status")) | ||||||
| else: | except: | ||||||
|   exchrate=float(json.loads(open("dbgdata-exchange-rates").read())["data"]["rates"][currency]) |   print("unable to retrieve remote data", file=sys.stderr) | ||||||
|   data=json.loads(open("dbgdata-status").read()) |   sys.exit(-1) | ||||||
|  |  | ||||||
| # update algo map | # update algo map | ||||||
|  |  | ||||||
| @@ -74,23 +57,36 @@ if (changed==True): | |||||||
|   with open(sys.argv[3], "w") as outfile: |   with open(sys.argv[3], "w") as outfile: | ||||||
|     json.dump(algo_map, outfile, sort_keys=True, indent=2) |     json.dump(algo_map, outfile, sort_keys=True, indent=2) | ||||||
|  |  | ||||||
| # adjust estimates so they're all in the same units: BTC/day per GH/s  | # weed out miners not supported by the pool | ||||||
|  |  | ||||||
|  | filtered_miners={} | ||||||
| for i in data: | for i in data: | ||||||
|   data[i]["adjusted_estimate"]=float(data[i]["estimate_current"])*1000 |   try: | ||||||
|  |     filtered_miners[algo_map[i]]=miners[algo_map[i]] | ||||||
|  |   except: | ||||||
|  |     pass | ||||||
|  | miners=filtered_miners | ||||||
|  |  | ||||||
|  | # adjust estimates so they're all in the same units: BTC/day per GH/s  | ||||||
|  |  | ||||||
|  | for i in data: | ||||||
|  |   data[i]["estimate_current"]=float(data[i]["estimate_current"])*1000 | ||||||
| try: | try: | ||||||
|   data["sha256"]["adjusted_estimate"]/=1000000000000 |   data["sha256"]["estimate_current"]/=1000000000 | ||||||
| except: | except: | ||||||
|   pass |   pass | ||||||
| try: | try: | ||||||
|   data["equihash"]["adjusted_estimate"]*=1000 |   data["equihash"]["estimate_current"]*=1000 | ||||||
| except: | except: | ||||||
|   pass |   pass | ||||||
| for i in ["scrypt", "blakecoin", "blake2s", "decred", "x11", "quark", "qubit", "keccak"]: | for i in ["scrypt", "blakecoin", "blake2s", "decred", "x11", "quark", "qubit", "keccak"]: | ||||||
|   try: |   try: | ||||||
|     data[i]["adjusted_estimate"]/=1000 |     data[i]["estimate_current"]/=1000 | ||||||
|   except: |   except: | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  | # calculate profitability for our hardware | ||||||
|  |  | ||||||
| coins={} | coins={} | ||||||
| for i in data: | for i in data: | ||||||
|   if (algo_map[i]!=""): |   if (algo_map[i]!=""): | ||||||
| @@ -99,13 +95,99 @@ for i in data: | |||||||
|     coins[i]["algo"]=i |     coins[i]["algo"]=i | ||||||
|     coins[i]["mapped_algo"]=algo_map[i] |     coins[i]["mapped_algo"]=algo_map[i] | ||||||
|     coins[i]["name"]=i |     coins[i]["name"]=i | ||||||
|     coins[i]["estimate"]=data[i]["adjusted_estimate"]*float(coins[i]["speed"]) # factor in our speed     |     coins[i]["estimate"]=data[i]["estimate_current"]*float(coins[i]["speed"])-24.0*coins[i]["power"]*pwrcost/exchrate | ||||||
|  |  | ||||||
| for i in data: | # sort by profitability | ||||||
|   print(i+": "+str(data[i]["estimate_current"])+" "+str(data[i]["adjusted_estimate"])) |  | ||||||
|    |  | ||||||
| print("") |  | ||||||
|  |  | ||||||
|  | sort={} | ||||||
| for i in coins: | for i in coins: | ||||||
|   print(i+": "+str(coins[i]["estimate"])) |   sort[i]=coins[i]["estimate"] | ||||||
|  | sort=sorted(sort.items(), key=lambda x:x[1], reverse=True) | ||||||
|  |  | ||||||
|  | log=open("current-profit", "w") | ||||||
|  | for i in sort: | ||||||
|  |   log.write(i[0]+": "+format(i[1], ".8f")+" BTC/day ("+format(i[1]*exchrate, ".2f")+" "+currency+"/day)\n") | ||||||
|  | log.close() | ||||||
|  |  | ||||||
|  | miner=coins[sort[0][0]] | ||||||
|  |  | ||||||
|  | if (len(sys.argv)==4): | ||||||
|  |   # exit if maximum is below minimum | ||||||
|  |   if (miner["estimate"]<min_profit): | ||||||
|  |     algo_log=open("algo-log", "a") | ||||||
|  |     algo_log.write(str(datetime.datetime.now())+": **NONE**\n") | ||||||
|  |     algo_log.close() | ||||||
|  |     for algo in coins: | ||||||
|  |       subprocess.call(["pkill", "-f", "^"+coins[algo].replace("+", "\\+")]) | ||||||
|  |     sys.exit() | ||||||
|  | else: # manual override | ||||||
|  |   if (sys.argv[4]!="list"): | ||||||
|  |     miner=coins[sys.argv[4]] | ||||||
|  |   else: # list available algos | ||||||
|  |     print("algos: ", end="") | ||||||
|  |     for i in coins: | ||||||
|  |       print(i+" ", end="") | ||||||
|  |     print("") | ||||||
|  |     sys.exit() | ||||||
|  |  | ||||||
|  | # see if miner's already running | ||||||
|  | try: | ||||||
|  |   subprocess.check_output(["pgrep", "-f", "^"+miner["bin"].replace("+", "\\+")]) | ||||||
|  |   current=True | ||||||
|  | except: | ||||||
|  |   current=False | ||||||
|  | other=False; | ||||||
|  | if (current==False): | ||||||
|  |   for algo in miners: | ||||||
|  |     try: | ||||||
|  |       subprocess.check_output(["pgrep", "-f", "^"+miners[algo]["bin"].replace("+", "\\+")]) | ||||||
|  |       other=True | ||||||
|  |     except: | ||||||
|  |       pass | ||||||
|  |  | ||||||
|  | if (current==False): | ||||||
|  |   # log a change | ||||||
|  |   algo_log=open("algo-log", "a") | ||||||
|  |   algo_log.write(str(datetime.datetime.now())+": * ("+miner["algo"]+") "+format(miner["estimate"], ".8f")+" "+format(miner["estimate"]*exchrate, ".2f")+"\n") | ||||||
|  |   algo_log.close() | ||||||
|  |   # kill existing miners | ||||||
|  |   for algo in miners: | ||||||
|  |     subprocess.call(["pkill", "-f", "^"+miners[algo]["bin"].replace("+", "\\+")]) | ||||||
|  |   time.sleep(3) | ||||||
|  |   if (card_type=="nvidia"): # update card settings | ||||||
|  |     cards=int(subprocess.check_output("nvidia-smi --query-gpu=count --format=csv,noheader,nounits".split(" ")).decode("utf-8").split("\n")[-2]) | ||||||
|  |     for i in range(0, cards): | ||||||
|  |       # power limit | ||||||
|  |       if type(miner["power_limit"]) is int: | ||||||
|  |         subprocess.call(("sudo nvidia-smi -i "+str(i)+" -pl "+str(miner["power_limit"])).split(" ")) | ||||||
|  |       else: | ||||||
|  |         subprocess.call(("sudo nvidia-smi -i "+str(i)+" -pl "+str(miner["power_limit"][i])).split(" ")) | ||||||
|  |       # core overclock | ||||||
|  |       if type(miner["gpu_oc"]) is int: | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUGraphicsClockOffset[2]="+str(miner["gpu_oc"])).split(" ")) | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUGraphicsClockOffset[3]="+str(miner["gpu_oc"])).split(" ")) | ||||||
|  |       else: | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUGraphicsClockOffset[2]="+str(miner["gpu_oc"][i])).split(" ")) | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUGraphicsClockOffset[3]="+str(miner["gpu_oc"][i])).split(" ")) | ||||||
|  |       # memory overclock | ||||||
|  |       if type(miner["mem_oc"]) is int: | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUMemoryTransferRateOffset[2]="+str(miner["mem_oc"])).split(" ")) | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUMemoryTransferRateOffset[3]="+str(miner["mem_oc"])).split(" ")) | ||||||
|  |       else: | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUMemoryTransferRateOffset[2]="+str(miner["mem_oc"][i])).split(" ")) | ||||||
|  |         subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUMemoryTransferRateOffset[3]="+str(miner["mem_oc"][i])).split(" ")) | ||||||
|  |       # fan speed | ||||||
|  |       if type(miner["fan"]) is int: | ||||||
|  |         if (miner["fan"]==0): | ||||||
|  |           subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUFanControlState=0").split(" ")) | ||||||
|  |         else: | ||||||
|  |           subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUFanControlState=1").split(" ")) | ||||||
|  |           subprocess.call(("nvidia-settings -a [fan:"+str(i)+"]/GPUTargetFanSpeed="+str(miner["fan"])).split(" ")) | ||||||
|  |       else: | ||||||
|  |         if (miner["fan"][i]==0): | ||||||
|  |           subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUFanControlState=0").split(" ")) | ||||||
|  |         else: | ||||||
|  |           subprocess.call(("nvidia-settings -a [gpu:"+str(i)+"]/GPUFanControlState=1").split(" ")) | ||||||
|  |           subprocess.call(("nvidia-settings -a [fan:"+str(i)+"]/GPUTargetFanSpeed="+str(miner["fan"][i])).split(" ")) | ||||||
|  |   # launch new miner | ||||||
|  |   subprocess.call(("screen -dmS miner "+miner["bin"]).split(" ")) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user