Passed
Push — master ( 87c632...227015 )
by Dave
01:20
created

domain.mining.Miner.__init__()   A

Complexity

Conditions 1

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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