Passed
Push — master ( ed280a...13e7ac )
by Dave
01:13
created

domain.mining.MinerStatistics.__init__()   A

Complexity

Conditions 1

Size

Total Lines 24
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 24
nop 18
dl 0
loc 24
rs 9.304
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
        if self.minerid is not None and self.minerid and self.minerid != 'unknown':
106
            return self.minerid
107
        elif self.networkid is not None and self.networkid and str(self.networkid) != '{}':
108
            return str(self.networkid)
109
        elif self.ipaddress is not None and self.ipaddress:
110
            return '{0}:{1}'.format(self.ipaddress, self.port)
111
        else:
112
            return self.name
113
114
    def set_ftp_port(self, port):
115
        if self.ftpport is not None and self.ftpport: return
116
        self.ftpport = port
117
118
    #todo: move ui code out of entity
119
    def summary(self):
120
        #datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S%f%z')
121
        return '{0} {1} {2} {3}'.format(self.name, self.hash_or_offline(), self.formattime(self.lastmonitor), self.currentpoolname())
122
123
    def currentpoolname(self):
124
        if self.minerpool is None:
125
            return '?'
126
        #todo:look up pools here?
127
        return self.minerpool.poolname
128
129
    def hash_or_offline(self):
130
        '''hash or offline status of miner'''
131
        if self.status != MinerStatus.Online:
132
            return self.status
133
        if self.minerstats is None: return self.status
134
        return self.minerstats.stats_summary()
135
136
    #todo: move to appservice
137
    def utc_to_local(self, utc_dt):
138
        return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
139
140
    def formattime(self, ptime):
141
        '''format time'''
142
        if ptime is None:
143
            return ''
144
        if isinstance(ptime, datetime):
145
            return self.utc_to_local(ptime).strftime('%m-%d %H:%M:%S')
146
        stime = ptime
147
        if '.' in stime:
148
            stime = stime[0:stime.index('.') - 1]
149
        try:
150
            parsedtime = datetime.strptime(stime, '%Y-%m-%dT%H:%M:%S')
151
            return self.utc_to_local(parsedtime).strftime('%m-%d %H:%M:%S')
152
        except ValueError:
153
            return stime
154
155
    def uptime(self, seconds):
156
        minutes, _ = divmod(seconds, 60)
157
        hours, minutes = divmod(minutes, 60)
158
        days, hours = divmod(hours, 24)
159
        return "%dd%dh%02dm" % (days, hours, minutes)
160
161
    def is_disabled(self):
162
        if self.is_manually_disabled() or self.status == MinerStatus.Disabled:
163
            return True
164
        return False
165
166
    def is_manually_disabled(self):
167
        if self.name.startswith("#"):
168
            return True
169
        return False
170
171
    def can_monitor(self):
172
        if not self.ipaddress:
173
            return False
174
        if not self.port:
175
            return False
176
        return True
177
178
    def should_monitor(self):
179
        #always monitor at least once when fcm app starts up
180
        if self.lastmonitor is None:
181
            return True
182
        #no need to monitor if manually disabled
183
        if self.is_manually_disabled():
184
            return False
185
        if self.is_disabled():
186
            #keep monitoring if it was us that disabled the miner
187
            #need to keep monitoring (at longer interval) so we can detect when comes online
188
            #if its a planned outage then user should manually disable to stop monitoring
189
            #since = (datetime.utcnow() - self.lastmonitor).total_seconds()
190
            #if since > 10 * 60:
191
            return True
192
            #return False
193
        return True
194
195
    def offline_now(self):
196
        self.status = MinerStatus.Offline
197
        self.offlinecount += 1
198
199
    def online_now(self):
200
        self.status = MinerStatus.Online
201
        self.offlinecount = 0
202
203
    def is_send_offline_alert(self):
204
        #todo: make configurable
205
        if self.offlinecount <= 3:
206
            return True
207
        return False
208
209
    def monitored(self, stats, pool=None, info=None, sec=None):
210
        if stats:
211
            self.lastmonitor = datetime.utcnow()
212
            self.status = MinerStatus.Online
213
        if sec is not None:
214
            self.monitorcount += 1
215
            self.monitortime += sec
216
        #todo: process stats and pool
217
        self.setminerinfo(info)
218
        if pool is not None:
219
            self.minerpool = pool
220
        self.minerstats = stats
221
222
    def monitorresponsetime(self):
223
        if self.monitorcount == 0: return 0
224
        return self.monitortime/self.monitorcount
225
226
    def setminerinfo(self, info):
227
        if info is not None:
228
            self.minerinfo = info
229
            if not self.miner_type:
230
                self.miner_type = info.miner_type
231
            if not self.minerid:
232
                self.minerid = info.minerid
233
234
    def updatefrom(self, updatedminer):
235
        if self.minerid != updatedminer.minerid and self.name != updatedminer.name:
236
            return
237
        if self.minerid == updatedminer.minerid and self.name != updatedminer.name:
238
            self.name = updatedminer.name
239
        self.setminerinfo(updatedminer.minerinfo)
240
        if updatedminer.lastmonitor:
241
            self.lastmonitor = updatedminer.lastmonitor
242
        if updatedminer.status:
243
            self.status = updatedminer.status
244
        if updatedminer.ipaddress:
245
            self.ipaddress = updatedminer.ipaddress
246
        if updatedminer.port:
247
            self.port = updatedminer.port
248
        if updatedminer.username:
249
            self.username = updatedminer.username
250
        if updatedminer.password:
251
            self.password = updatedminer.password
252
        if updatedminer.clientid:
253
            self.clientid = updatedminer.clientid
254
        if updatedminer.networkid:
255
            self.networkid = updatedminer.networkid
256
        #self.minerid = updatedminer.minerid
257
        if updatedminer.offlinecount:
258
            self.offlinecount = updatedminer.offlinecount
259
        if updatedminer.defaultpool:
260
            self.defaultpool = updatedminer.defaultpool
261
        if updatedminer.minerpool is not None:
262
            self.minerpool = updatedminer.minerpool
263
        if updatedminer.minerstats is not None:
264
            self.minerstats = updatedminer.minerstats
265
266
          #"Pool Stale%": 0,
267
          #"Discarded": 86497,
268
          #"Diff": "65.5K",
269
          #"Rejected": 15,
270
          #"Proxy Type": "",
271
          #"Getworks": 3311,
272
          #"Last Share Time": "0:00:20",
273
          #"Pool Rejected%": 0.1838,
274
          #"Accepted": 8148,
275
          #"Last Share Difficulty": 65536,
276
          #"Difficulty Accepted": 533987328,
277
          #"Has Stratum": true,
278
          #"Priority": 1,
279
          #"Stale": 3,
280
          #"Long Poll": "N",
281
          #"Quota": 1,
282
          #"URL": "stratum+tcp://solo.antpool.com:3333",
283
          #"Proxy": "",
284
          #"Get Failures": 1,
285
          #"Diff1 Shares": 0,
286
          #"Best Share": 255598083,
287
          #"Stratum Active": true,
288
          #"POOL": 0,
289
          #"Has GBT": false,
290
          #"User": "antminer_1",
291
          #"Status": "Alive",
292
          #"Stratum URL": "solo.antpool.com",
293
          #"Remote Failures": 1,
294
          #"Difficulty Rejected": 983040,
295
          #"Difficulty Stale": 0
296
class AvailablePool(object):
297
    """A pool available on a miner
298
    pool_type is the miner type (e.g. Antminer S9)
299
    """
300
301
    def __init__(self, pool_type, named_pool=None, url='', user='', password='x', priority=None):
302
        self.pool_type = pool_type
303
        self.named_pool = named_pool
304
        self.url = url
305
        self.user = user
306
        self.password = password
307
        self.priority = priority
308
309
    @property
310
    def key(self):
311
        return '{0}|{1}'.format(self.url, self.user)
312
313
class MinerApiCall(object):
314
    '''info about one call to miner'''
315
    def __init__(self, miner: Miner):
316
        self.miner = miner
317
        self.when = datetime.now()
318
        self.start_time = None
319
        self.stop_time = None
320
321
    def start(self):
322
        self.start_time = time.perf_counter()
323
    def stop(self):
324
        self.stop_time = time.perf_counter()
325
    def elapsed(self):
326
        return self.stop_time - self.start_time
327
328
class Pool(object):
329
    """A configured (Named) Pool.
330
    Does not have to be attached to miner yet
331
    """
332
333
    def __init__(self, pool_type, name, url, user, priority, password='x'):
334
        self.pool_type = pool_type
335
        self.name = name
336
        self.url = url
337
        self.user = user
338
        self.priority = priority
339
        self.password = password
340
341
    def is_same_as(self, available_pool: AvailablePool):
342
        return available_pool.url == self.url and available_pool.user.startswith(self.user)
343
344
class MinerCurrentPool(object):
345
    '''The current pool where a miner is mining'''
346
    def __init__(self, miner, currentpool=None, currentworker=None, allpools=None):
347
        self.miner = miner
348
        self.poolname = '?'
349
        self.currentpool = currentpool
350
        self.currentworker = currentworker
351
        #allpools is a json object
352
        self.allpools = allpools
353
354
    def findpoolnumberforpool(self, url, worker):
355
        jpools = self.allpools["POOLS"]
356
        for pool in jpools:
357
            thisurl = pool["URL"]
358
            thisworker = pool["User"]
359
            if thisurl == url and thisworker.startswith(worker):
360
                return pool["POOL"]
361
        return None
362
363