Passed
Push — master ( 933317...73c77f )
by Dave
01:02
created

domain.mining.MinerCurrentPool.__init__()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nop 5
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
'''#Full Cycle Mining Domain'''
2
import time
3
from datetime import datetime, timezone
4
5
class MinerStatus:
6
    '''Status of Miner'''
7
    Online = 'online'
8
    Offline = 'offline'
9
    Disabled = 'disabled'
10
11
class MinerAccessLevel:
12
    Restricted = 'restricted'
13
    Privileged = 'priviledged'
14
    #not really a level, waiting for access upgrade
15
    Waiting = 'waiting'
16
17
class Login(object):
18
    """Login name and password for access to miners"""
19
    def __init__(self, username, password):
20
        self.username = username
21
        self.password = password
22
23
class MinerInfo(object):
24
    '''Meta information about a miner
25
    type and algo
26
    '''
27
    def __init__(self, miner_type, minerid):
28
        self.miner_type = miner_type
29
        self.minerid = minerid
30
31
class MinerCommand(object):
32
    """Command that could be sent to a miner"""
33
    def __init__(self, command='', parameter=''):
34
        self.command = command
35
        self.parameter = parameter
36
37
class Miner(object):
38
    """Miner"""
39
40
    def __init__(self, name, status=MinerStatus.Online, miner_type='', ipaddress='', port='', ftpport='', username='', password='', clientid='', networkid='', minerid='',
41
                 lastmonitor=None, offlinecount=0, defaultpool='', minerinfo=None, minerpool=None, minerstats=None, laststatuschanged=None):
42
        #friendly name for your miner
43
        self.name = name
44
        self._status = status
45
        #saved or derived from monitoring? type of miner. Antminer S9, Antminer D3, etc.
46
        self.miner_type = miner_type
47
        #ip address, usuall will be local ip address. example: 192.168.x.y
48
        self.ipaddress = ipaddress
49
        #ip port, usually will be 4028
50
        self.port = port
51
        self.ftpport = ftpport
52
        self.username = username
53
        self.password = password
54
        #the mydevices clientid for device
55
        self.clientid = clientid
56
        #network identifier for miner. usually the macaddress
57
        self.networkid = networkid
58
        #so far have only seen Antminer S9 have the minerid from STATS command
59
        self.minerid = minerid
60
        #last time the miner was monitored
61
        self.lastmonitor = lastmonitor
62
        self.monitorcount = 0
63
        self.monitortime = 0
64
        #number of times miner is offline during this session
65
        self.offlinecount = offlinecount
66
        #name of the pool that the miner should default to when it is provisioned
67
        self.defaultpool = defaultpool
68
        #meta info on the miner. should be assigned during discovery and monitor
69
        self.minerinfo = minerinfo
70
        #MinerCurrentPool
71
        self.minerpool = minerpool
72
        #MinerStatistics
73
        self.minerstats = minerstats
74
        #status of the miner. online, offline,disabled etc
75
        self.laststatuschanged = laststatuschanged
76
        #store is where the object was stored. mem is for memcache.
77
        self.store = ''
78
79
    @property
80
    def status(self):
81
        return self._status
82
83
    @status.setter
84
    def status(self, value):
85
        if value != '' and value != MinerStatus.Online and value != MinerStatus.Offline and value != MinerStatus.Disabled:
86
            raise ValueError('Invalid miner status {0}'.format(value))
87
        if self._status != value:
88
            self.laststatuschanged = datetime.utcnow()
89
        self._status = value
90
91
    @property
92
    def pools_available(self):
93
        if self.minerpool is None:
94
            return None
95
        available = []
96
        if 'POOLS' in self.minerpool.allpools:
97
            jpools = self.minerpool.allpools['POOLS']
98
            for jpool in jpools:
99
                available.append(AvailablePool(pool_type=self.miner_type, named_pool=None, url=jpool['URL'], user=jpool['User'], priority=jpool['Priority']))
100
        return available
101
102
    #@property
103
    def key(self):
104
        '''cache key for this entity'''
105
        thekey = self.name
106
        if isvalid_minerid():
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable isvalid_minerid does not seem to be defined.
Loading history...
107
            thekey = self.minerid
108
        elif isvalid_networkid():
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable isvalid_networkid does not seem to be defined.
Loading history...
109
            thekey = str(self.networkid)
110
        elif isvalid_ipaddress:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable isvalid_ipaddress does not seem to be defined.
Loading history...
111
            thekey = '{0}:{1}'().format(self.ipaddress, self.port)
112
        return thekey
113
114
    def isvalid_minerid(self):
115
        return self.minerid is not None and self.minerid and self.minerid != 'unknown'
116
117
    def isvalid_networkid(self):
118
        self.networkid is not None and self.networkid and str(self.networkid) != '{}'
119
120
    def isvalid_ipaddress(self):
121
        self.ipaddress is not None and self.ipaddress
122
123
    def set_ftp_port(self, port):
124
        if self.ftpport is not None and self.ftpport: return
125
        self.ftpport = port
126
127
    #todo: move ui code out of entity
128
    def summary(self):
129
        #datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S%f%z')
130
        return '{0} {1} {2} {3}'.format(self.name, self.hash_or_offline(), self.formattime(self.lastmonitor), self.currentpoolname())
131
132
    def currentpoolname(self):
133
        if self.minerpool is None:
134
            return '?'
135
        #todo:look up pools here?
136
        return self.minerpool.poolname
137
138
    def hash_or_offline(self):
139
        '''hash or offline status of miner'''
140
        if self.status != MinerStatus.Online:
141
            return self.status
142
        if self.minerstats is None: return self.status
143
        return self.minerstats.stats_summary()
144
145
    #todo: move to appservice
146
    def utc_to_local(self, utc_dt):
147
        return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
148
149
    def formattime(self, ptime):
150
        '''format time'''
151
        if ptime is None:
152
            return ''
153
        if isinstance(ptime, datetime):
154
            return self.utc_to_local(ptime).strftime('%m-%d %H:%M:%S')
155
        stime = ptime
156
        if '.' in stime:
157
            stime = stime[0:stime.index('.') - 1]
158
        try:
159
            parsedtime = datetime.strptime(stime, '%Y-%m-%dT%H:%M:%S')
160
            return self.utc_to_local(parsedtime).strftime('%m-%d %H:%M:%S')
161
        except ValueError:
162
            return stime
163
164
    def uptime(self, seconds):
165
        minutes, _ = divmod(seconds, 60)
166
        hours, minutes = divmod(minutes, 60)
167
        days, hours = divmod(hours, 24)
168
        return "%dd%dh%02dm" % (days, hours, minutes)
169
170
    def is_disabled(self):
171
        if self.is_manually_disabled() or self.status == MinerStatus.Disabled:
172
            return True
173
        return False
174
175
    def is_manually_disabled(self):
176
        if self.name.startswith("#"):
177
            return True
178
        return False
179
180
    def can_monitor(self):
181
        if not self.ipaddress:
182
            return False
183
        if not self.port:
184
            return False
185
        return True
186
187
    def should_monitor(self):
188
        #always monitor at least once when fcm app starts up
189
        if self.lastmonitor is None:
190
            return True
191
        #no need to monitor if manually disabled
192
        if self.is_manually_disabled():
193
            return False
194
        if self.is_disabled():
195
            #keep monitoring if it was us that disabled the miner
196
            #need to keep monitoring (at longer interval) so we can detect when comes online
197
            #if its a planned outage then user should manually disable to stop monitoring
198
            #since = (datetime.utcnow() - self.lastmonitor).total_seconds()
199
            #if since > 10 * 60:
200
            return True
201
            #return False
202
        return True
203
204
    def offline_now(self):
205
        self.status = MinerStatus.Offline
206
        self.offlinecount += 1
207
208
    def online_now(self):
209
        self.status = MinerStatus.Online
210
        self.offlinecount = 0
211
212
    def is_send_offline_alert(self):
213
        #todo: make configurable
214
        if self.offlinecount <= 3:
215
            return True
216
        return False
217
218
    def monitored(self, stats, pool=None, info=None, sec=None):
219
        if stats:
220
            self.lastmonitor = datetime.utcnow()
221
            self.status = MinerStatus.Online
222
        if sec is not None:
223
            self.monitorcount += 1
224
            self.monitortime += sec
225
        #todo: process stats and pool
226
        self.setminerinfo(info)
227
        if pool is not None:
228
            self.minerpool = pool
229
        self.minerstats = stats
230
231
    def monitorresponsetime(self):
232
        if self.monitorcount == 0: return 0
233
        return self.monitortime/self.monitorcount
234
235
    def setminerinfo(self, info):
236
        if info is not None:
237
            self.minerinfo = info
238
            if not self.miner_type:
239
                self.miner_type = info.miner_type
240
            if not self.minerid:
241
                self.minerid = info.minerid
242
243
    def updatefrom(self, updatedminer):
244
        if self.minerid != updatedminer.minerid and self.name != updatedminer.name:
245
            return
246
        if self.minerid == updatedminer.minerid and self.name != updatedminer.name:
247
            self.name = updatedminer.name
248
        self.setminerinfo(updatedminer.minerinfo)
249
250
        #self.minerid = updatedminer.minerid
251
        fields = ['lastmonitor', 'status', 'ipaddress', 'port', 'username', 'password', 'clientid']
252
        fields.append('offlinecount')
253
        fields.append('defaultpool')
254
        fields.append('minerpool')
255
        fields.append('minerstats')
256
        fields.append('networkid')
257
        for fld in fields:
258
            val = getattr(updatedminer, fld)
259
            if val:
260
                setattr(self, fld, val)
261
262
class AvailablePool(object):
263
    """A pool available on a miner
264
    pool_type is the miner type (e.g. Antminer S9)
265
    """
266
267
    def __init__(self, pool_type, named_pool=None, url='', user='', password='x', priority=None):
268
        self.pool_type = pool_type
269
        self.named_pool = named_pool
270
        self.url = url
271
        self.user = user
272
        self.password = password
273
        self.priority = priority
274
275
    @property
276
    def key(self):
277
        return '{0}|{1}'.format(self.url, self.user)
278
279
class MinerApiCall(object):
280
    '''info about one call to miner'''
281
    def __init__(self, miner: Miner):
282
        self.miner = miner
283
        self.when = datetime.now()
284
        self.start_time = None
285
        self.stop_time = None
286
287
    def start(self):
288
        self.start_time = time.perf_counter()
289
    def stop(self):
290
        self.stop_time = time.perf_counter()
291
    def elapsed(self):
292
        return self.stop_time - self.start_time
293
294
class Pool(object):
295
    """A configured (Named) Pool.
296
    Does not have to be attached to miner yet
297
    """
298
299
    def __init__(self, pool_type, name, url, user, priority, password='x'):
300
        self.pool_type = pool_type
301
        self.name = name
302
        self.url = url
303
        self.user = user
304
        self.priority = priority
305
        self.password = password
306
307
    def is_same_as(self, available_pool: AvailablePool):
308
        return available_pool.url == self.url and available_pool.user.startswith(self.user)
309
310
class MinerCurrentPool(object):
311
    '''The current pool where a miner is mining'''
312
    def __init__(self, miner, currentpool=None, currentworker=None, allpools=None):
313
        self.miner = miner
314
        self.poolname = '?'
315
        self.currentpool = currentpool
316
        self.currentworker = currentworker
317
        #allpools is a json object
318
        self.allpools = allpools
319
320
    def findpoolnumberforpool(self, url, worker):
321
        jpools = self.allpools["POOLS"]
322
        for pool in jpools:
323
            thisurl = pool["URL"]
324
            thisworker = pool["User"]
325
            if thisurl == url and thisworker.startswith(worker):
326
                return pool["POOL"]
327
        return None
328