domain.mining.Miner.__init__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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