Passed
Push — master ( ed280a...13e7ac )
by Dave
01:13
created

domain.mining.MinerStatistics.stats_summary()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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
        if updatedminer.lastmonitor:
241
            self.lastmonitor = updatedminer.lastmonitor
242
        if updatedminer.status:
243
            self.status = updatedminer.status
244
        if updatedminer.ipaddress:
245
            self.ipaddress = updatedminer.ipaddress
246
        if updatedminer.port:
247
            self.port = updatedminer.port
248
        if updatedminer.username:
249
            self.username = updatedminer.username
250
        if updatedminer.password:
251
            self.password = updatedminer.password
252
        if updatedminer.clientid:
253
            self.clientid = updatedminer.clientid
254
        if updatedminer.networkid:
255
            self.networkid = updatedminer.networkid
256
        #self.minerid = updatedminer.minerid
257
        if updatedminer.offlinecount:
258
            self.offlinecount = updatedminer.offlinecount
259
        if updatedminer.defaultpool:
260
            self.defaultpool = updatedminer.defaultpool
261
        if updatedminer.minerpool is not None:
262
            self.minerpool = updatedminer.minerpool
263
        if updatedminer.minerstats is not None:
264
            self.minerstats = updatedminer.minerstats
265
266
          #"Pool Stale%": 0,
267
          #"Discarded": 86497,
268
          #"Diff": "65.5K",
269
          #"Rejected": 15,
270
          #"Proxy Type": "",
271
          #"Getworks": 3311,
272
          #"Last Share Time": "0:00:20",
273
          #"Pool Rejected%": 0.1838,
274
          #"Accepted": 8148,
275
          #"Last Share Difficulty": 65536,
276
          #"Difficulty Accepted": 533987328,
277
          #"Has Stratum": true,
278
          #"Priority": 1,
279
          #"Stale": 3,
280
          #"Long Poll": "N",
281
          #"Quota": 1,
282
          #"URL": "stratum+tcp://solo.antpool.com:3333",
283
          #"Proxy": "",
284
          #"Get Failures": 1,
285
          #"Diff1 Shares": 0,
286
          #"Best Share": 255598083,
287
          #"Stratum Active": true,
288
          #"POOL": 0,
289
          #"Has GBT": false,
290
          #"User": "antminer_1",
291
          #"Status": "Alive",
292
          #"Stratum URL": "solo.antpool.com",
293
          #"Remote Failures": 1,
294
          #"Difficulty Rejected": 983040,
295
          #"Difficulty Stale": 0
296
class AvailablePool(object):
297
    """A pool available on a miner
298
    pool_type is the miner type (e.g. Antminer S9)
299
    """
300
301
    def __init__(self, pool_type, named_pool=None, url='', user='', password='x', priority=None):
302
        self.pool_type = pool_type
303
        self.named_pool = named_pool
304
        self.url = url
305
        self.user = user
306
        self.password = password
307
        self.priority = priority
308
309
    @property
310
    def key(self):
311
        return '{0}|{1}'.format(self.url, self.user)
312
313
class MinerApiCall(object):
314
    '''info about one call to miner'''
315
    def __init__(self, miner: Miner):
316
        self.miner = miner
317
        self.when = datetime.now()
318
        self.start_time = None
319
        self.stop_time = None
320
321
    def start(self):
322
        self.start_time = time.perf_counter()
323
    def stop(self):
324
        self.stop_time = time.perf_counter()
325
    def elapsed(self):
326
        return self.stop_time - self.start_time
327
328
class Pool(object):
329
    """A configured (Named) Pool.
330
    Does not have to be attached to miner yet
331
    """
332
333
    def __init__(self, pool_type, name, url, user, priority, password='x'):
334
        self.pool_type = pool_type
335
        self.name = name
336
        self.url = url
337
        self.user = user
338
        self.priority = priority
339
        self.password = password
340
341
    def is_same_as(self, available_pool: AvailablePool):
342
        return available_pool.url == self.url and available_pool.user.startswith(self.user)
343
344
class MinerCurrentPool(object):
345
    '''The current pool where a miner is mining'''
346
    def __init__(self, miner, currentpool=None, currentworker=None, allpools=None):
347
        self.miner = miner
348
        self.poolname = '?'
349
        self.currentpool = currentpool
350
        self.currentworker = currentworker
351
        #allpools is a json object
352
        self.allpools = allpools
353
354
    def findpoolnumberforpool(self, url, worker):
355
        jpools = self.allpools["POOLS"]
356
        for pool in jpools:
357
            thisurl = pool["URL"]
358
            thisworker = pool["User"]
359
            if thisurl == url and thisworker.startswith(worker):
360
                return pool["POOL"]
361
        return None
362
363