Passed
Push — master ( 93c134...fbe99c )
by Dave
01:10
created

domain.mining.Miner.__init__()   A

Complexity

Conditions 1

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 28
nop 21
dl 0
loc 45
rs 9.208
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='',
41
                 port='', ftpport='', username='', password='', clientid='', networkid='',
42
                 minerid='', lastmonitor=None, offlinecount=0, defaultpool='', minerinfo=None,
43
                 minerpool=None, minerstats=None, laststatuschanged=None,
44
                 in_service_date=None, location=None):
45
        #friendly name for your miner
46
        self.name = name
47
        self._status = status
48
        #saved or derived from monitoring? type of miner. Antminer S9, Antminer D3, etc.
49
        self.miner_type = miner_type
50
        #ip address, usuall will be local ip address. example: 192.168.x.y
51
        self.ipaddress = ipaddress
52
        #ip port, usually will be 4028
53
        self.port = port
54
        self.ftpport = ftpport
55
        self.username = username
56
        self.password = password
57
        #the mydevices clientid for device
58
        self.clientid = clientid
59
        #network identifier for miner. usually the macaddress
60
        self.networkid = networkid
61
        #so far have only seen Antminer S9 have the minerid from STATS command
62
        self.minerid = minerid
63
        #last time the miner was monitored
64
        self.lastmonitor = lastmonitor
65
        self.monitorcount = 0
66
        self.monitortime = 0
67
        #number of times miner is offline during this session
68
        self.offlinecount = offlinecount
69
        #name of the pool that the miner should default to when it is provisioned
70
        self.defaultpool = defaultpool
71
        #meta info on the miner. should be assigned during discovery and monitor
72
        self.minerinfo = minerinfo
73
        #MinerCurrentPool
74
        self.minerpool = minerpool
75
        #MinerStatistics
76
        self.minerstats = minerstats
77
        #status of the miner. online, offline,disabled etc
78
        self.laststatuschanged = laststatuschanged
79
        #store is where the object was stored. mem is for memcache.
80
        self.store = ''
81
        # the date that the miner was put into service or first discovered
82
        self.in_service_date = in_service_date
83
        # location of miner. could be name of facility or rack
84
        self.location = location
85
86
    @classmethod
87
    def create(cls, values):
88
        '''create entity from values dict'''
89
        miner = Miner('', '', '', '', '', '', '')
90
        #todo: find pythonic way to do this
91
        for pair in values:
92
            if 'minerid' in pair:
93
                miner.minerid = pair['minerid']
94
            if 'name' in pair:
95
                miner.name = pair['name']
96
            if 'ipaddress' in pair:
97
                miner.ipaddress = pair['ipaddress']
98
            if 'port' in pair:
99
                miner.port = pair['port']
100
            if 'location' in pair:
101
                miner.location = pair['location']
102
            if 'in_service_date' in pair:
103
                miner.in_service_date = pair['in_service_date']
104
        return miner
105
106
    @property
107
    def status(self):
108
        return self._status
109
110
    @status.setter
111
    def status(self, value):
112
        if value != '' and value != MinerStatus.Online and value != MinerStatus.Offline and value != MinerStatus.Disabled:
113
            raise ValueError('Invalid miner status {0}'.format(value))
114
        if self._status != value:
115
            self.laststatuschanged = datetime.utcnow()
116
        self._status = value
117
118
    @property
119
    def pools_available(self):
120
        if self.minerpool is None:
121
            return None
122
        available = []
123
        if 'POOLS' in self.minerpool.allpools:
124
            jpools = self.minerpool.allpools['POOLS']
125
            for jpool in jpools:
126
                available.append(AvailablePool(pool_type=self.miner_type, named_pool=None, url=jpool['URL'], user=jpool['User'], priority=jpool['Priority']))
127
        return available
128
129
    #@property
130
    def key(self):
131
        '''cache key for this entity'''
132
        thekey = self.name
133
        if self.isvalid_minerid():
134
            thekey = self.minerid
135
        elif self.isvalid_networkid():
136
            thekey = str(self.networkid)
137
        elif self.isvalid_ipaddress():
138
            thekey = '{0}:{1}'.format(self.ipaddress, self.port)
139
        return thekey
140
141
    @property
142
    def is_unknown(self):
143
        return self.minerid == 'unknown'
144
145
    def isvalid_minerid(self):
146
        return self.minerid is not None and self.minerid and not self.is_unknown
147
148
    def isvalid_networkid(self):
149
        return self.networkid is not None and self.networkid and str(self.networkid) != '{}'
150
151
    def isvalid_ipaddress(self):
152
        return self.ipaddress is not None and self.ipaddress
153
154
    def set_ftp_port(self, port):
155
        if self.ftpport is not None and self.ftpport: return
156
        self.ftpport = port
157
158
    #todo: move ui code out of entity
159
    def summary(self):
160
        #datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S%f%z')
161
        return '{0} {1} {2} {3}'.format(self.name, self.hash_or_offline(), self.formattime(self.lastmonitor), self.currentpoolname())
162
163
    def currentpoolname(self):
164
        if self.minerpool is None:
165
            return '?'
166
        #todo:look up pools here?
167
        return self.minerpool.poolname
168
169
    def hash_or_offline(self):
170
        '''hash or offline status of miner'''
171
        if self.status != MinerStatus.Online:
172
            return self.status
173
        if self.minerstats is None: return self.status
174
        return self.minerstats.stats_summary()
175
176
    #todo: move to appservice
177
    def utc_to_local(self, utc_dt):
178
        return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
179
180
    def formattime(self, ptime):
181
        '''format time'''
182
        if ptime is None:
183
            return ''
184
        if isinstance(ptime, datetime):
185
            return self.utc_to_local(ptime).strftime('%m-%d %H:%M:%S')
186
        stime = ptime
187
        if '.' in stime:
188
            stime = stime[0:stime.index('.') - 1]
189
        try:
190
            parsedtime = datetime.strptime(stime, '%Y-%m-%dT%H:%M:%S')
191
            return self.utc_to_local(parsedtime).strftime('%m-%d %H:%M:%S')
192
        except ValueError:
193
            return stime
194
195
    def uptime(self, seconds):
196
        minutes, _ = divmod(seconds, 60)
197
        hours, minutes = divmod(minutes, 60)
198
        days, hours = divmod(hours, 24)
199
        return "%dd%dh%02dm" % (days, hours, minutes)
200
201
    def is_disabled(self):
202
        if self.is_manually_disabled() or self.status == MinerStatus.Disabled:
203
            return True
204
        return False
205
206
    def is_manually_disabled(self):
207
        if self.name.startswith("#"):
208
            return True
209
        return False
210
211
    def can_monitor(self):
212
        if not self.ipaddress:
213
            return False
214
        if not self.port:
215
            return False
216
        return True
217
218
    def should_monitor(self):
219
        #always monitor at least once when fcm app starts up
220
        if self.lastmonitor is None:
221
            return True
222
        #no need to monitor if manually disabled
223
        if self.is_manually_disabled():
224
            return False
225
        if self.is_disabled():
226
            #keep monitoring if it was us that disabled the miner
227
            #need to keep monitoring (at longer interval) so we can detect when comes online
228
            #if its a planned outage then user should manually disable to stop monitoring
229
            #since = (datetime.utcnow() - self.lastmonitor).total_seconds()
230
            #if since > 10 * 60:
231
            return True
232
            #return False
233
        return True
234
235
    def offline_now(self):
236
        self.status = MinerStatus.Offline
237
        self.offlinecount += 1
238
239
    def online_now(self):
240
        self.status = MinerStatus.Online
241
        self.offlinecount = 0
242
243
    def is_send_offline_alert(self):
244
        #todo: make configurable
245
        if self.offlinecount <= 3:
246
            return True
247
        return False
248
249
    def monitored(self, stats, pool=None, info=None, sec=None):
250
        if stats:
251
            self.lastmonitor = datetime.utcnow()
252
            self.status = MinerStatus.Online
253
        if sec is not None:
254
            self.monitorcount += 1
255
            self.monitortime += sec
256
        #todo: process stats and pool
257
        self.setminerinfo(info)
258
        if pool is not None:
259
            self.minerpool = pool
260
        self.minerstats = stats
261
262
    def monitorresponsetime(self):
263
        if self.monitorcount == 0: return 0
264
        return self.monitortime/self.monitorcount
265
266
    def setminerinfo(self, info):
267
        if info is not None:
268
            self.minerinfo = info
269
            if not self.miner_type:
270
                self.miner_type = info.miner_type
271
            if not self.minerid:
272
                self.minerid = info.minerid
273
274
    def updatefrom(self, updatedminer):
275
        if self.minerid != updatedminer.minerid and self.name != updatedminer.name:
276
            return
277
        if self.minerid == updatedminer.minerid and self.name != updatedminer.name:
278
            self.name = updatedminer.name
279
        self.setminerinfo(updatedminer.minerinfo)
280
281
        #self.minerid = updatedminer.minerid
282
        fields = ['lastmonitor', 'status', 'ipaddress', 'port', 'username', 'password', 'clientid']
283
        fields.append('offlinecount')
284
        fields.append('defaultpool')
285
        fields.append('minerpool')
286
        fields.append('minerstats')
287
        fields.append('networkid')
288
        fields.append('location')
289
        fields.append('in_service_date')
290
        for fld in fields:
291
            val = getattr(updatedminer, fld)
292
            if val:
293
                setattr(self, fld, val)
294
295
class AvailablePool(object):
296
    """A pool available on a miner
297
    pool_type is the miner type (e.g. Antminer S9)
298
    """
299
300
    def __init__(self, pool_type, named_pool=None, url='', user='', password='x', priority=None):
301
        self.pool_type = pool_type
302
        self.named_pool = named_pool
303
        self.url = url
304
        self.user = user
305
        self.password = password
306
        self.priority = priority
307
308
    @property
309
    def key(self):
310
        return '{0}|{1}'.format(self.url, self.user)
311
312
class MinerApiCall(object):
313
    '''info about one call to miner'''
314
    def __init__(self, miner: Miner):
315
        self.miner = miner
316
        self.when = datetime.now()
317
        self.start_time = None
318
        self.stop_time = None
319
320
    def start(self):
321
        self.start_time = time.perf_counter()
322
    def stop(self):
323
        self.stop_time = time.perf_counter()
324
    def elapsed(self):
325
        return self.stop_time - self.start_time
326
327
class Pool(object):
328
    """A configured (Named) Pool.
329
    Does not have to be attached to miner yet
330
    """
331
332
    def __init__(self, pool_type, name, url, user, priority, password='x'):
333
        self.pool_type = pool_type
334
        self.name = name
335
        self.url = url
336
        self.user = user
337
        self.priority = priority
338
        self.password = password
339
340
    @classmethod
341
    def create(cls, values):
342
        '''create entity from values dict'''
343
        entity = Pool('', '', '', '', '')
344
        #todo: find pythonic way to do this
345
        for pair in values:
346
            if 'pool_type' in pair:
347
                entity.pool_type = pair['pool_type']
348
            if 'name' in pair:
349
                entity.name = pair['name']
350
            if 'url' in pair:
351
                entity.url = pair['url']
352
            if 'user' in pair:
353
                entity.user = pair['user']
354
            if 'priority' in pair:
355
                entity.priority = pair['priority']
356
            if 'password' in pair:
357
                entity.password = pair['password']
358
        return entity
359
360
361
    def is_same_as(self, available_pool: AvailablePool):
362
        return available_pool.url == self.url and available_pool.user.startswith(self.user)
363
364
class MinerCurrentPool(object):
365
    '''The current pool where a miner is mining'''
366
    def __init__(self, miner, currentpool=None, currentworker=None, allpools=None):
367
        self.miner = miner
368
        self.poolname = '?'
369
        self.currentpool = currentpool
370
        self.currentworker = currentworker
371
        #allpools is a json object
372
        self.allpools = allpools
373
374
    def findpoolnumberforpool(self, url, worker):
375
        jpools = self.allpools["POOLS"]
376
        for pool in jpools:
377
            thisurl = pool["URL"]
378
            thisworker = pool["User"]
379
            if thisurl == url and thisworker.startswith(worker):
380
                return pool["POOL"]
381
        return None
382