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