Passed
Push — master ( f401c8...fbf2d0 )
by Dave
53s
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
    def isvalid_minerid(self):
142
        return self.minerid is not None and self.minerid and self.minerid != 'unknown'
143
144
    def isvalid_networkid(self):
145
        return self.networkid is not None and self.networkid and str(self.networkid) != '{}'
146
147
    def isvalid_ipaddress(self):
148
        return self.ipaddress is not None and self.ipaddress
149
150
    def set_ftp_port(self, port):
151
        if self.ftpport is not None and self.ftpport: return
152
        self.ftpport = port
153
154
    #todo: move ui code out of entity
155
    def summary(self):
156
        #datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S%f%z')
157
        return '{0} {1} {2} {3}'.format(self.name, self.hash_or_offline(), self.formattime(self.lastmonitor), self.currentpoolname())
158
159
    def currentpoolname(self):
160
        if self.minerpool is None:
161
            return '?'
162
        #todo:look up pools here?
163
        return self.minerpool.poolname
164
165
    def hash_or_offline(self):
166
        '''hash or offline status of miner'''
167
        if self.status != MinerStatus.Online:
168
            return self.status
169
        if self.minerstats is None: return self.status
170
        return self.minerstats.stats_summary()
171
172
    #todo: move to appservice
173
    def utc_to_local(self, utc_dt):
174
        return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
175
176
    def formattime(self, ptime):
177
        '''format time'''
178
        if ptime is None:
179
            return ''
180
        if isinstance(ptime, datetime):
181
            return self.utc_to_local(ptime).strftime('%m-%d %H:%M:%S')
182
        stime = ptime
183
        if '.' in stime:
184
            stime = stime[0:stime.index('.') - 1]
185
        try:
186
            parsedtime = datetime.strptime(stime, '%Y-%m-%dT%H:%M:%S')
187
            return self.utc_to_local(parsedtime).strftime('%m-%d %H:%M:%S')
188
        except ValueError:
189
            return stime
190
191
    def uptime(self, seconds):
192
        minutes, _ = divmod(seconds, 60)
193
        hours, minutes = divmod(minutes, 60)
194
        days, hours = divmod(hours, 24)
195
        return "%dd%dh%02dm" % (days, hours, minutes)
196
197
    def is_disabled(self):
198
        if self.is_manually_disabled() or self.status == MinerStatus.Disabled:
199
            return True
200
        return False
201
202
    def is_manually_disabled(self):
203
        if self.name.startswith("#"):
204
            return True
205
        return False
206
207
    def can_monitor(self):
208
        if not self.ipaddress:
209
            return False
210
        if not self.port:
211
            return False
212
        return True
213
214
    def should_monitor(self):
215
        #always monitor at least once when fcm app starts up
216
        if self.lastmonitor is None:
217
            return True
218
        #no need to monitor if manually disabled
219
        if self.is_manually_disabled():
220
            return False
221
        if self.is_disabled():
222
            #keep monitoring if it was us that disabled the miner
223
            #need to keep monitoring (at longer interval) so we can detect when comes online
224
            #if its a planned outage then user should manually disable to stop monitoring
225
            #since = (datetime.utcnow() - self.lastmonitor).total_seconds()
226
            #if since > 10 * 60:
227
            return True
228
            #return False
229
        return True
230
231
    def offline_now(self):
232
        self.status = MinerStatus.Offline
233
        self.offlinecount += 1
234
235
    def online_now(self):
236
        self.status = MinerStatus.Online
237
        self.offlinecount = 0
238
239
    def is_send_offline_alert(self):
240
        #todo: make configurable
241
        if self.offlinecount <= 3:
242
            return True
243
        return False
244
245
    def monitored(self, stats, pool=None, info=None, sec=None):
246
        if stats:
247
            self.lastmonitor = datetime.utcnow()
248
            self.status = MinerStatus.Online
249
        if sec is not None:
250
            self.monitorcount += 1
251
            self.monitortime += sec
252
        #todo: process stats and pool
253
        self.setminerinfo(info)
254
        if pool is not None:
255
            self.minerpool = pool
256
        self.minerstats = stats
257
258
    def monitorresponsetime(self):
259
        if self.monitorcount == 0: return 0
260
        return self.monitortime/self.monitorcount
261
262
    def setminerinfo(self, info):
263
        if info is not None:
264
            self.minerinfo = info
265
            if not self.miner_type:
266
                self.miner_type = info.miner_type
267
            if not self.minerid:
268
                self.minerid = info.minerid
269
270
    def updatefrom(self, updatedminer):
271
        if self.minerid != updatedminer.minerid and self.name != updatedminer.name:
272
            return
273
        if self.minerid == updatedminer.minerid and self.name != updatedminer.name:
274
            self.name = updatedminer.name
275
        self.setminerinfo(updatedminer.minerinfo)
276
277
        #self.minerid = updatedminer.minerid
278
        fields = ['lastmonitor', 'status', 'ipaddress', 'port', 'username', 'password', 'clientid']
279
        fields.append('offlinecount')
280
        fields.append('defaultpool')
281
        fields.append('minerpool')
282
        fields.append('minerstats')
283
        fields.append('networkid')
284
        fields.append('location')
285
        fields.append('in_service_date')
286
        for fld in fields:
287
            val = getattr(updatedminer, fld)
288
            if val:
289
                setattr(self, fld, val)
290
291
class AvailablePool(object):
292
    """A pool available on a miner
293
    pool_type is the miner type (e.g. Antminer S9)
294
    """
295
296
    def __init__(self, pool_type, named_pool=None, url='', user='', password='x', priority=None):
297
        self.pool_type = pool_type
298
        self.named_pool = named_pool
299
        self.url = url
300
        self.user = user
301
        self.password = password
302
        self.priority = priority
303
304
    @property
305
    def key(self):
306
        return '{0}|{1}'.format(self.url, self.user)
307
308
class MinerApiCall(object):
309
    '''info about one call to miner'''
310
    def __init__(self, miner: Miner):
311
        self.miner = miner
312
        self.when = datetime.now()
313
        self.start_time = None
314
        self.stop_time = None
315
316
    def start(self):
317
        self.start_time = time.perf_counter()
318
    def stop(self):
319
        self.stop_time = time.perf_counter()
320
    def elapsed(self):
321
        return self.stop_time - self.start_time
322
323
class Pool(object):
324
    """A configured (Named) Pool.
325
    Does not have to be attached to miner yet
326
    """
327
328
    def __init__(self, pool_type, name, url, user, priority, password='x'):
329
        self.pool_type = pool_type
330
        self.name = name
331
        self.url = url
332
        self.user = user
333
        self.priority = priority
334
        self.password = password
335
336
    @classmethod
337
    def create(cls, values):
338
        '''create entity from values dict'''
339
        entity = Pool('', '', '', '', '')
340
        #todo: find pythonic way to do this
341
        for pair in values:
342
            if 'pool_type' in pair:
343
                entity.pool_type = pair['pool_type']
344
            if 'name' in pair:
345
                entity.name = pair['name']
346
            if 'url' in pair:
347
                entity.url = pair['url']
348
            if 'user' in pair:
349
                entity.user = pair['user']
350
            if 'priority' in pair:
351
                entity.priority = pair['priority']
352
            if 'password' in pair:
353
                entity.password = pair['password']
354
        return entity
355
356
357
    def is_same_as(self, available_pool: AvailablePool):
358
        return available_pool.url == self.url and available_pool.user.startswith(self.user)
359
360
class MinerCurrentPool(object):
361
    '''The current pool where a miner is mining'''
362
    def __init__(self, miner, currentpool=None, currentworker=None, allpools=None):
363
        self.miner = miner
364
        self.poolname = '?'
365
        self.currentpool = currentpool
366
        self.currentworker = currentworker
367
        #allpools is a json object
368
        self.allpools = allpools
369
370
    def findpoolnumberforpool(self, url, worker):
371
        jpools = self.allpools["POOLS"]
372
        for pool in jpools:
373
            thisurl = pool["URL"]
374
            thisworker = pool["User"]
375
            if thisurl == url and thisworker.startswith(worker):
376
                return pool["POOL"]
377
        return None
378