Passed
Push — master ( fbe99c...28cd06 )
by Dave
01:06
created

domain.mining.Miner.__init__()   A

Complexity

Conditions 1

Size

Total Lines 47
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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