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