mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 11:10:44 +02:00
801 lines
38 KiB
Python
801 lines
38 KiB
Python
"""
|
|
Worker service module for managing workers data.
|
|
"""
|
|
import logging
|
|
import random
|
|
from datetime import datetime, timedelta
|
|
from zoneinfo import ZoneInfo
|
|
|
|
class WorkerService:
|
|
"""Service for retrieving and managing worker data."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the worker service."""
|
|
self.worker_data_cache = None
|
|
self.last_worker_data_update = None
|
|
self.WORKER_DATA_CACHE_TIMEOUT = 60 # Cache worker data for 60 seconds
|
|
self.dashboard_service = None # Will be set by App.py during initialization
|
|
self.sats_per_btc = 100_000_000 # Constant for conversion
|
|
|
|
def set_dashboard_service(self, dashboard_service):
|
|
"""
|
|
Set the dashboard service instance - to be called from App.py
|
|
|
|
Args:
|
|
dashboard_service (MiningDashboardService): The initialized dashboard service
|
|
"""
|
|
self.dashboard_service = dashboard_service
|
|
# Immediately access the wallet from dashboard_service when it's set
|
|
if hasattr(dashboard_service, 'wallet'):
|
|
self.wallet = dashboard_service.wallet
|
|
logging.info(f"Worker service updated with new wallet: {self.wallet}")
|
|
logging.info("Dashboard service connected to worker service")
|
|
|
|
def generate_default_workers_data(self):
|
|
"""
|
|
Generate default worker data when no metrics are available.
|
|
|
|
Returns:
|
|
dict: Default worker data structure
|
|
"""
|
|
return {
|
|
"workers": [],
|
|
"workers_total": 0,
|
|
"workers_online": 0,
|
|
"workers_offline": 0,
|
|
"total_hashrate": 0.0,
|
|
"hashrate_unit": "TH/s",
|
|
"total_earnings": 0.0,
|
|
"daily_sats": 0,
|
|
"avg_acceptance_rate": 0.0,
|
|
"hashrate_history": [],
|
|
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
|
}
|
|
|
|
def get_workers_data(self, cached_metrics, force_refresh=False):
|
|
"""
|
|
Get worker data with caching for better performance.
|
|
|
|
Args:
|
|
cached_metrics (dict): Cached metrics from the dashboard
|
|
force_refresh (bool): Whether to force a refresh of cached data
|
|
|
|
Returns:
|
|
dict: Worker data
|
|
"""
|
|
current_time = datetime.now().timestamp()
|
|
|
|
# Return cached data if it's still fresh and not forced to refresh
|
|
if not force_refresh and self.worker_data_cache and self.last_worker_data_update and \
|
|
(current_time - self.last_worker_data_update) < self.WORKER_DATA_CACHE_TIMEOUT:
|
|
# Even when using cached data, sync worker count with main dashboard
|
|
if cached_metrics and cached_metrics.get("workers_hashing") is not None:
|
|
self.sync_worker_counts_with_dashboard(self.worker_data_cache, cached_metrics)
|
|
|
|
logging.info("Using cached worker data")
|
|
return self.worker_data_cache
|
|
|
|
try:
|
|
# First try to get actual worker data from the dashboard service
|
|
if self.dashboard_service:
|
|
logging.info("Attempting to fetch real worker data from Ocean.xyz")
|
|
real_worker_data = self.dashboard_service.get_worker_data()
|
|
|
|
if real_worker_data and real_worker_data.get('workers') and len(real_worker_data['workers']) > 0:
|
|
# Validate that worker names are not just "Online" or "Offline"
|
|
valid_names = False
|
|
for worker in real_worker_data['workers']:
|
|
name = worker.get('name', '').lower()
|
|
if name and name not in ['online', 'offline', 'total', 'worker', 'status']:
|
|
valid_names = True
|
|
break
|
|
|
|
if valid_names:
|
|
logging.info(f"Successfully retrieved {len(real_worker_data['workers'])} real workers from Ocean.xyz")
|
|
|
|
# Add hashrate history if available in cached metrics
|
|
if cached_metrics and cached_metrics.get("arrow_history") and cached_metrics["arrow_history"].get("hashrate_3hr"):
|
|
real_worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
|
|
|
# Sync with dashboard metrics to ensure consistency
|
|
if cached_metrics:
|
|
self.sync_worker_counts_with_dashboard(real_worker_data, cached_metrics)
|
|
|
|
# Update cache
|
|
self.worker_data_cache = real_worker_data
|
|
self.last_worker_data_update = current_time
|
|
|
|
return real_worker_data
|
|
else:
|
|
logging.warning("Real worker data had invalid names (like 'online'/'offline'), falling back to simulated data")
|
|
else:
|
|
logging.warning("Real worker data fetch returned no workers, falling back to simulated data")
|
|
else:
|
|
logging.warning("Dashboard service not available, cannot fetch real worker data")
|
|
|
|
# Fallback to simulated data if real data fetch fails or returns no workers
|
|
logging.info("Generating fallback simulated worker data")
|
|
worker_data = self.generate_fallback_data(cached_metrics)
|
|
|
|
# Add hashrate history if available in cached metrics
|
|
if cached_metrics and cached_metrics.get("arrow_history") and cached_metrics["arrow_history"].get("hashrate_3hr"):
|
|
worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
|
|
|
# Ensure worker counts match dashboard metrics
|
|
if cached_metrics:
|
|
self.sync_worker_counts_with_dashboard(worker_data, cached_metrics)
|
|
|
|
# Update cache
|
|
self.worker_data_cache = worker_data
|
|
self.last_worker_data_update = current_time
|
|
|
|
logging.info(f"Successfully generated fallback worker data: {worker_data['workers_total']} workers")
|
|
return worker_data
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error getting worker data: {e}")
|
|
fallback_data = self.generate_fallback_data(cached_metrics)
|
|
|
|
# Even on error, try to sync with dashboard metrics
|
|
if cached_metrics:
|
|
self.sync_worker_counts_with_dashboard(fallback_data, cached_metrics)
|
|
|
|
return fallback_data
|
|
|
|
def sync_worker_counts_with_dashboard(self, worker_data, dashboard_metrics):
|
|
"""
|
|
Synchronize worker counts and other metrics between worker data and dashboard metrics.
|
|
|
|
Args:
|
|
worker_data (dict): Worker data to be updated
|
|
dashboard_metrics (dict): Dashboard metrics with worker count and other data
|
|
"""
|
|
if not worker_data or not dashboard_metrics:
|
|
return
|
|
|
|
# Sync worker count
|
|
dashboard_worker_count = dashboard_metrics.get("workers_hashing")
|
|
|
|
# Only proceed if dashboard has valid worker count
|
|
if dashboard_worker_count is not None:
|
|
current_worker_count = worker_data.get("workers_total", 0)
|
|
|
|
# If counts already match, no need to sync workers count
|
|
if current_worker_count != dashboard_worker_count:
|
|
logging.info(f"Syncing worker count: worker page({current_worker_count}) → dashboard({dashboard_worker_count})")
|
|
|
|
# Update the total count
|
|
worker_data["workers_total"] = dashboard_worker_count
|
|
|
|
# Adjust online/offline counts proportionally
|
|
current_online = worker_data.get("workers_online", 0)
|
|
current_total = max(1, current_worker_count) # Avoid division by zero
|
|
|
|
# Calculate ratio of online workers
|
|
online_ratio = current_online / current_total
|
|
|
|
# Recalculate online and offline counts
|
|
new_online_count = round(dashboard_worker_count * online_ratio)
|
|
new_offline_count = dashboard_worker_count - new_online_count
|
|
|
|
# Update the counts
|
|
worker_data["workers_online"] = new_online_count
|
|
worker_data["workers_offline"] = new_offline_count
|
|
|
|
logging.info(f"Updated worker counts - Total: {dashboard_worker_count}, Online: {new_online_count}, Offline: {new_offline_count}")
|
|
|
|
# If we have worker instances, try to adjust them as well
|
|
if "workers" in worker_data and isinstance(worker_data["workers"], list):
|
|
self.adjust_worker_instances(worker_data, dashboard_worker_count)
|
|
|
|
# Sync daily sats - critical for fixing the daily sats discrepancy
|
|
if dashboard_metrics.get("daily_mined_sats") is not None:
|
|
daily_sats_value = dashboard_metrics.get("daily_mined_sats")
|
|
if daily_sats_value != worker_data.get("daily_sats"):
|
|
worker_data["daily_sats"] = daily_sats_value
|
|
logging.info(f"Synced daily sats: {worker_data['daily_sats']}")
|
|
|
|
# Sync other important metrics
|
|
if dashboard_metrics.get("total_hashrate") is not None:
|
|
worker_data["total_hashrate"] = dashboard_metrics.get("total_hashrate")
|
|
|
|
if dashboard_metrics.get("unpaid_earnings") is not None:
|
|
# Attempt to convert string to float if needed
|
|
unpaid_value = dashboard_metrics.get("unpaid_earnings")
|
|
if isinstance(unpaid_value, str):
|
|
try:
|
|
unpaid_value = float(unpaid_value.split()[0].replace(',', ''))
|
|
except (ValueError, IndexError):
|
|
pass
|
|
worker_data["total_earnings"] = unpaid_value
|
|
|
|
def adjust_worker_instances(self, worker_data, target_count):
|
|
"""
|
|
Adjust the number of worker instances to match the target count.
|
|
|
|
Args:
|
|
worker_data (dict): Worker data containing worker instances
|
|
target_count (int): Target number of worker instances
|
|
"""
|
|
current_workers = worker_data.get("workers", [])
|
|
current_count = len(current_workers)
|
|
|
|
if current_count == target_count:
|
|
return
|
|
|
|
if current_count < target_count:
|
|
# Need to add more workers
|
|
workers_to_add = target_count - current_count
|
|
|
|
# Get existing online/offline worker counts
|
|
online_workers = [w for w in current_workers if w["status"] == "online"]
|
|
offline_workers = [w for w in current_workers if w["status"] == "offline"]
|
|
|
|
# Use the same online/offline ratio for new workers
|
|
online_ratio = len(online_workers) / max(1, current_count)
|
|
new_online = round(workers_to_add * online_ratio)
|
|
new_offline = workers_to_add - new_online
|
|
|
|
# Copy and adjust existing workers to create new ones
|
|
if online_workers and new_online > 0:
|
|
for i in range(new_online):
|
|
# Pick a random online worker as template
|
|
template = random.choice(online_workers).copy()
|
|
# Give it a new name to avoid duplicates
|
|
template["name"] = f"{template['name']}_{current_count + i + 1}"
|
|
current_workers.append(template)
|
|
|
|
if offline_workers and new_offline > 0:
|
|
for i in range(new_offline):
|
|
# Pick a random offline worker as template
|
|
template = random.choice(offline_workers).copy()
|
|
# Give it a new name to avoid duplicates
|
|
template["name"] = f"{template['name']}_{current_count + new_online + i + 1}"
|
|
current_workers.append(template)
|
|
|
|
# If no existing workers of either type, create new ones from scratch
|
|
if not online_workers and new_online > 0:
|
|
for i in range(new_online):
|
|
worker = self.create_default_worker(f"Miner_{current_count + i + 1}", "online")
|
|
current_workers.append(worker)
|
|
|
|
if not offline_workers and new_offline > 0:
|
|
for i in range(new_offline):
|
|
worker = self.create_default_worker(f"Miner_{current_count + new_online + i + 1}", "offline")
|
|
current_workers.append(worker)
|
|
|
|
elif current_count > target_count:
|
|
# Need to remove some workers
|
|
workers_to_remove = current_count - target_count
|
|
|
|
# Remove workers from the end of the list to preserve earlier ones
|
|
worker_data["workers"] = current_workers[:target_count]
|
|
|
|
# Update the worker data
|
|
worker_data["workers"] = current_workers
|
|
|
|
def create_default_worker(self, name, status):
|
|
"""
|
|
Create a default worker with given name and status.
|
|
|
|
Args:
|
|
name (str): Worker name
|
|
status (str): Worker status ('online' or 'offline')
|
|
|
|
Returns:
|
|
dict: Default worker data
|
|
"""
|
|
is_online = status == "online"
|
|
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
|
|
|
|
# Generate some reasonable hashrate and other values
|
|
hashrate = round(random.uniform(50, 100), 2) if is_online else 0
|
|
last_share = current_time.strftime("%Y-%m-%d %H:%M") if is_online else (
|
|
(current_time - timedelta(hours=random.uniform(1, 24))).strftime("%Y-%m-%d %H:%M")
|
|
)
|
|
|
|
return {
|
|
"name": name,
|
|
"status": status,
|
|
"type": "ASIC",
|
|
"model": "Default Miner",
|
|
"hashrate_60sec": hashrate if is_online else 0,
|
|
"hashrate_60sec_unit": "TH/s",
|
|
"hashrate_3hr": hashrate if is_online else round(random.uniform(30, 80), 2),
|
|
"hashrate_3hr_unit": "TH/s",
|
|
"efficiency": round(random.uniform(80, 95), 1) if is_online else 0,
|
|
"last_share": last_share,
|
|
"earnings": round(random.uniform(0.0001, 0.001), 8),
|
|
"acceptance_rate": round(random.uniform(95, 99), 1),
|
|
"power_consumption": round(random.uniform(2000, 3500)) if is_online else 0,
|
|
"temperature": round(random.uniform(55, 75)) if is_online else 0
|
|
}
|
|
|
|
def generate_fallback_data(self, cached_metrics):
|
|
"""
|
|
Generate fallback worker data from cached metrics when real data can't be fetched.
|
|
Try to preserve real worker names if available.
|
|
|
|
Args:
|
|
cached_metrics (dict): Cached metrics from the dashboard
|
|
|
|
Returns:
|
|
dict: Generated worker data
|
|
"""
|
|
# If metrics aren't available yet, return default data
|
|
if not cached_metrics:
|
|
logging.warning("No cached metrics available for worker fallback data")
|
|
return self.generate_default_workers_data()
|
|
|
|
# Check if we have workers_hashing information
|
|
workers_count = cached_metrics.get("workers_hashing", 0)
|
|
|
|
# Force at least 1 worker if the count is 0
|
|
if workers_count <= 0:
|
|
logging.warning("No workers reported in metrics, forcing 1 worker")
|
|
workers_count = 1
|
|
|
|
# Get hashrate from cached metrics
|
|
original_hashrate_3hr = float(cached_metrics.get("hashrate_3hr", 0) or 0)
|
|
hashrate_unit = cached_metrics.get("hashrate_3hr_unit", "TH/s")
|
|
|
|
# If hashrate is 0, set a minimum value to avoid empty display
|
|
if original_hashrate_3hr <= 0:
|
|
original_hashrate_3hr = 50.0
|
|
logging.warning(f"Hashrate was 0, setting minimum value of {original_hashrate_3hr} {hashrate_unit}")
|
|
|
|
# Check if we have any previously cached real worker names
|
|
real_worker_names = []
|
|
if self.worker_data_cache and self.worker_data_cache.get('workers'):
|
|
for worker in self.worker_data_cache['workers']:
|
|
name = worker.get('name', '')
|
|
# Only use names that don't look like status indicators
|
|
if name and name.lower() not in ['online', 'offline', 'total']:
|
|
real_worker_names.append(name)
|
|
|
|
# Generate worker data
|
|
workers_data = []
|
|
|
|
# If we have real worker names, use them
|
|
if real_worker_names:
|
|
logging.info(f"Using {len(real_worker_names)} real worker names from cache")
|
|
workers_data = self.generate_simulated_workers(
|
|
workers_count,
|
|
original_hashrate_3hr,
|
|
hashrate_unit,
|
|
real_worker_names=real_worker_names
|
|
)
|
|
else:
|
|
# Otherwise use sequential names
|
|
logging.info("No real worker names available, using sequential names")
|
|
workers_data = self.generate_sequential_workers(
|
|
workers_count,
|
|
original_hashrate_3hr,
|
|
hashrate_unit
|
|
)
|
|
|
|
# Calculate basic statistics
|
|
workers_online = len([w for w in workers_data if w['status'] == 'online'])
|
|
workers_offline = len(workers_data) - workers_online
|
|
|
|
# Use unpaid_earnings from main dashboard
|
|
unpaid_earnings = cached_metrics.get("unpaid_earnings", 0)
|
|
# Handle case where unpaid_earnings might be a string
|
|
if isinstance(unpaid_earnings, str):
|
|
try:
|
|
# Handle case where it might include "BTC" or other text
|
|
unpaid_earnings = float(unpaid_earnings.split()[0].replace(',', ''))
|
|
except (ValueError, IndexError):
|
|
unpaid_earnings = 0.001
|
|
|
|
# Ensure we have a minimum value for unpaid earnings
|
|
if unpaid_earnings <= 0:
|
|
unpaid_earnings = 0.001
|
|
|
|
# Use unpaid_earnings as total_earnings
|
|
total_earnings = unpaid_earnings
|
|
|
|
# ---- IMPORTANT FIX: Daily sats calculation ----
|
|
# Get daily_mined_sats directly from cached metrics
|
|
daily_sats = cached_metrics.get("daily_mined_sats", 0)
|
|
|
|
# If daily_sats is missing or zero, try to calculate it from other available metrics
|
|
if daily_sats is None or daily_sats == 0:
|
|
logging.warning("daily_mined_sats is missing or zero, attempting alternative calculations")
|
|
|
|
# Try to calculate from daily_btc_net
|
|
if cached_metrics.get("daily_btc_net") is not None:
|
|
daily_btc_net = cached_metrics.get("daily_btc_net")
|
|
daily_sats = int(round(daily_btc_net * self.sats_per_btc))
|
|
logging.info(f"Calculated daily_sats from daily_btc_net: {daily_sats}")
|
|
|
|
# Alternative calculation from estimated_earnings_per_day
|
|
elif cached_metrics.get("estimated_earnings_per_day") is not None:
|
|
daily_btc = cached_metrics.get("estimated_earnings_per_day")
|
|
daily_sats = int(round(daily_btc * self.sats_per_btc))
|
|
logging.info(f"Calculated daily_sats from estimated_earnings_per_day: {daily_sats}")
|
|
|
|
# If still zero, try to use estimated_earnings_per_day_sats directly
|
|
elif cached_metrics.get("estimated_earnings_per_day_sats") is not None:
|
|
daily_sats = cached_metrics.get("estimated_earnings_per_day_sats")
|
|
logging.info(f"Using estimated_earnings_per_day_sats as fallback: {daily_sats}")
|
|
|
|
logging.info(f"Final daily_sats value: {daily_sats}")
|
|
|
|
# Create hashrate history based on arrow_history if available
|
|
hashrate_history = []
|
|
if cached_metrics.get("arrow_history") and cached_metrics["arrow_history"].get("hashrate_3hr"):
|
|
hashrate_history = cached_metrics["arrow_history"]["hashrate_3hr"]
|
|
|
|
result = {
|
|
"workers": workers_data,
|
|
"workers_total": len(workers_data),
|
|
"workers_online": workers_online,
|
|
"workers_offline": workers_offline,
|
|
"total_hashrate": original_hashrate_3hr,
|
|
"hashrate_unit": hashrate_unit,
|
|
"total_earnings": total_earnings,
|
|
"daily_sats": daily_sats, # Fixed daily_sats value
|
|
"avg_acceptance_rate": 98.8, # Default value
|
|
"hashrate_history": hashrate_history,
|
|
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
|
}
|
|
|
|
# Update cache
|
|
self.worker_data_cache = result
|
|
self.last_worker_data_update = datetime.now().timestamp()
|
|
|
|
logging.info(f"Generated fallback data with {len(workers_data)} workers")
|
|
return result
|
|
|
|
def generate_sequential_workers(self, num_workers, total_hashrate, hashrate_unit, total_unpaid_earnings=None):
|
|
"""
|
|
Generate workers with sequential names when other methods fail.
|
|
|
|
Args:
|
|
num_workers (int): Number of workers
|
|
total_hashrate (float): Total hashrate
|
|
hashrate_unit (str): Hashrate unit
|
|
total_unpaid_earnings (float, optional): Total unpaid earnings
|
|
|
|
Returns:
|
|
list: List of worker data dictionaries
|
|
"""
|
|
logging.info(f"Generating {num_workers} workers with sequential names")
|
|
|
|
# Ensure we have at least 1 worker
|
|
num_workers = max(1, num_workers)
|
|
|
|
# Worker model types for simulation
|
|
models = [
|
|
{"type": "ASIC", "model": "Bitmain Antminer S19 Pro", "max_hashrate": 110, "power": 3250},
|
|
{"type": "ASIC", "model": "Bitmain Antminer T21", "max_hashrate": 130, "power": 3276},
|
|
{"type": "ASIC", "model": "Bitmain Antminer S19j Pro", "max_hashrate": 104, "power": 3150},
|
|
{"type": "Bitaxe", "model": "Bitaxe Gamma 601", "max_hashrate": 3.2, "power": 35}
|
|
]
|
|
|
|
# Calculate hashrate distribution - majority of hashrate to online workers
|
|
online_count = max(1, int(num_workers * 0.8)) # At least 1 online worker
|
|
offline_count = num_workers - online_count
|
|
|
|
# Average hashrate per online worker (ensure it's at least 0.5 TH/s)
|
|
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
|
|
|
|
workers = []
|
|
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
|
|
|
|
# Default total unpaid earnings if not provided
|
|
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
|
|
total_unpaid_earnings = 0.001 # Default small amount
|
|
|
|
# Generate online workers with sequential names
|
|
for i in range(online_count):
|
|
# Select a model based on hashrate
|
|
model_info = models[0] if avg_hashrate > 50 else models[-1] if avg_hashrate < 5 else random.choice(models)
|
|
|
|
# For Antminers and regular ASICs, use ASIC model
|
|
if i < online_count - 1 or avg_hashrate > 5:
|
|
model_idx = random.randint(0, len(models) - 2) # Exclude Bitaxe for most workers
|
|
else:
|
|
model_idx = len(models) - 1 # Bitaxe for last worker if small hashrate
|
|
|
|
model_info = models[model_idx]
|
|
|
|
# Generate hashrate with some random variation
|
|
base_hashrate = min(model_info["max_hashrate"], avg_hashrate * random.uniform(0.5, 1.5))
|
|
hashrate_60sec = round(base_hashrate * random.uniform(0.9, 1.1), 2)
|
|
hashrate_3hr = round(base_hashrate * random.uniform(0.85, 1.0), 2)
|
|
|
|
# Generate last share time (within last 5 minutes)
|
|
minutes_ago = random.randint(0, 5)
|
|
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
|
|
|
|
# Generate acceptance rate (95-100%)
|
|
acceptance_rate = round(random.uniform(95, 100), 1)
|
|
|
|
# Generate temperature (normal operating range)
|
|
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
|
|
|
|
# Create a sequential name
|
|
name = f"Miner_{i+1}"
|
|
|
|
workers.append({
|
|
"name": name,
|
|
"status": "online",
|
|
"type": model_info["type"],
|
|
"model": model_info["model"],
|
|
"hashrate_60sec": hashrate_60sec,
|
|
"hashrate_60sec_unit": hashrate_unit,
|
|
"hashrate_3hr": hashrate_3hr,
|
|
"hashrate_3hr_unit": hashrate_unit,
|
|
"efficiency": round(random.uniform(65, 95), 1),
|
|
"last_share": last_share,
|
|
"earnings": 0, # Will be set after all workers are generated
|
|
"acceptance_rate": acceptance_rate,
|
|
"power_consumption": model_info["power"],
|
|
"temperature": temperature
|
|
})
|
|
|
|
# Generate offline workers
|
|
for i in range(offline_count):
|
|
# Select a model - more likely to be Bitaxe for offline
|
|
if random.random() > 0.6:
|
|
model_info = models[-1] # Bitaxe
|
|
else:
|
|
model_info = random.choice(models[:-1]) # ASIC
|
|
|
|
# Generate last share time (0.5 to 8 hours ago)
|
|
hours_ago = random.uniform(0.5, 8)
|
|
last_share = (current_time - timedelta(hours=hours_ago)).strftime("%Y-%m-%d %H:%M")
|
|
|
|
# Generate hashrate (historical before going offline)
|
|
if model_info["type"] == "Bitaxe":
|
|
hashrate_3hr = round(random.uniform(1, 3), 2)
|
|
else:
|
|
hashrate_3hr = round(random.uniform(20, 90), 2)
|
|
|
|
# Create a sequential name
|
|
idx = i + online_count # Index for offline workers starts after online workers
|
|
name = f"Miner_{idx+1}"
|
|
|
|
workers.append({
|
|
"name": name,
|
|
"status": "offline",
|
|
"type": model_info["type"],
|
|
"model": model_info["model"],
|
|
"hashrate_60sec": 0,
|
|
"hashrate_60sec_unit": hashrate_unit,
|
|
"hashrate_3hr": hashrate_3hr,
|
|
"hashrate_3hr_unit": hashrate_unit,
|
|
"efficiency": 0,
|
|
"last_share": last_share,
|
|
"earnings": 0, # Minimal earnings for offline workers
|
|
"acceptance_rate": round(random.uniform(95, 99), 1),
|
|
"power_consumption": 0,
|
|
"temperature": 0
|
|
})
|
|
|
|
# Distribute earnings based on hashrate proportion
|
|
# Reserve a small portion (5%) of earnings for offline workers
|
|
online_earnings_pool = total_unpaid_earnings * 0.95
|
|
offline_earnings_pool = total_unpaid_earnings * 0.05
|
|
|
|
# Distribute earnings based on hashrate proportion for online workers
|
|
total_effective_hashrate = sum(w["hashrate_3hr"] for w in workers if w["status"] == "online")
|
|
if total_effective_hashrate > 0:
|
|
for worker in workers:
|
|
if worker["status"] == "online":
|
|
hashrate_proportion = worker["hashrate_3hr"] / total_effective_hashrate
|
|
worker["earnings"] = round(online_earnings_pool * hashrate_proportion, 8)
|
|
|
|
# Distribute minimal earnings to offline workers
|
|
if offline_count > 0:
|
|
offline_per_worker = offline_earnings_pool / offline_count
|
|
for worker in workers:
|
|
if worker["status"] == "offline":
|
|
worker["earnings"] = round(offline_per_worker, 8)
|
|
|
|
logging.info(f"Generated {len(workers)} workers with sequential names")
|
|
return workers
|
|
|
|
def generate_simulated_workers(self, num_workers, total_hashrate, hashrate_unit, total_unpaid_earnings=None, real_worker_names=None):
|
|
"""
|
|
Generate simulated worker data based on total hashrate.
|
|
This is a fallback method used when real data can't be fetched.
|
|
|
|
Args:
|
|
num_workers (int): Number of workers
|
|
total_hashrate (float): Total hashrate
|
|
hashrate_unit (str): Hashrate unit
|
|
total_unpaid_earnings (float, optional): Total unpaid earnings
|
|
real_worker_names (list, optional): List of real worker names to use instead of random names
|
|
|
|
Returns:
|
|
list: List of worker data dictionaries
|
|
"""
|
|
# Ensure we have at least 1 worker
|
|
num_workers = max(1, num_workers)
|
|
|
|
# Worker model types for simulation
|
|
models = [
|
|
{"type": "ASIC", "model": "Bitmain Antminer S19k Pro", "max_hashrate": 110, "power": 3250},
|
|
{"type": "ASIC", "model": "Bitmain Antminer T21", "max_hashrate": 130, "power": 3276},
|
|
{"type": "ASIC", "model": "Bitmain Antminer S19j Pro", "max_hashrate": 104, "power": 3150},
|
|
{"type": "Bitaxe", "model": "Bitaxe Gamma 601", "max_hashrate": 3.2, "power": 35}
|
|
]
|
|
|
|
# Worker names for simulation - only used if no real worker names are provided
|
|
prefixes = ["Antminer", "Miner", "Rig", "Node", "Worker", "BitAxe", "BTC"]
|
|
|
|
# Calculate hashrate distribution - majority of hashrate to online workers
|
|
online_count = max(1, int(num_workers * 0.8)) # At least 1 online worker
|
|
offline_count = num_workers - online_count
|
|
|
|
# Average hashrate per online worker (ensure it's at least 0.5 TH/s)
|
|
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
|
|
|
|
workers = []
|
|
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
|
|
|
|
# Default total unpaid earnings if not provided
|
|
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
|
|
total_unpaid_earnings = 0.001 # Default small amount
|
|
|
|
# Prepare name list - use real names if available, otherwise will generate random names
|
|
# If we have real names but not enough, we'll reuse them or generate additional random ones
|
|
name_list = []
|
|
if real_worker_names and len(real_worker_names) > 0:
|
|
logging.info(f"Using {len(real_worker_names)} real worker names")
|
|
# Ensure we have enough names by cycling through the list if needed
|
|
name_list = real_worker_names * (num_workers // len(real_worker_names) + 1)
|
|
name_list = name_list[:num_workers] # Truncate to exact number needed
|
|
|
|
# Generate online workers
|
|
for i in range(online_count):
|
|
# Select a model based on hashrate
|
|
model_info = models[0] if avg_hashrate > 50 else models[-1] if avg_hashrate < 5 else random.choice(models)
|
|
|
|
# For Antminers and regular ASICs, use ASIC model
|
|
if i < online_count - 1 or avg_hashrate > 5:
|
|
model_idx = random.randint(0, len(models) - 2) # Exclude Bitaxe for most workers
|
|
else:
|
|
model_idx = len(models) - 1 # Bitaxe for last worker if small hashrate
|
|
|
|
model_info = models[model_idx]
|
|
|
|
# Generate hashrate with some random variation
|
|
base_hashrate = min(model_info["max_hashrate"], avg_hashrate * random.uniform(0.5, 1.5))
|
|
hashrate_60sec = round(base_hashrate * random.uniform(0.9, 1.1), 2)
|
|
hashrate_3hr = round(base_hashrate * random.uniform(0.85, 1.0), 2)
|
|
|
|
# Generate last share time (within last 3 minutes)
|
|
minutes_ago = random.randint(0, 3)
|
|
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
|
|
|
|
# Generate acceptance rate (95-100%)
|
|
acceptance_rate = round(random.uniform(95, 100), 1)
|
|
|
|
# Generate temperature (normal operating range)
|
|
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
|
|
|
|
# Use a real name if available, otherwise generate a random name
|
|
if name_list and i < len(name_list):
|
|
name = name_list[i]
|
|
else:
|
|
# Create a unique name
|
|
if model_info["type"] == "Bitaxe":
|
|
name = f"{prefixes[-1]}{random.randint(1, 99):02d}"
|
|
else:
|
|
name = f"{random.choice(prefixes[:-1])}{random.randint(1, 99):02d}"
|
|
|
|
workers.append({
|
|
"name": name,
|
|
"status": "online",
|
|
"type": model_info["type"],
|
|
"model": model_info["model"],
|
|
"hashrate_60sec": hashrate_60sec,
|
|
"hashrate_60sec_unit": hashrate_unit,
|
|
"hashrate_3hr": hashrate_3hr,
|
|
"hashrate_3hr_unit": hashrate_unit,
|
|
"efficiency": round(random.uniform(65, 95), 1),
|
|
"last_share": last_share,
|
|
"earnings": 0, # Will be set after all workers are generated
|
|
"acceptance_rate": acceptance_rate,
|
|
"power_consumption": model_info["power"],
|
|
"temperature": temperature
|
|
})
|
|
|
|
# Generate offline workers
|
|
for i in range(offline_count):
|
|
# Select a model - more likely to be Bitaxe for offline
|
|
if random.random() > 0.6:
|
|
model_info = models[-1] # Bitaxe
|
|
else:
|
|
model_info = random.choice(models[:-1]) # ASIC
|
|
|
|
# Generate last share time (0.5 to 8 hours ago)
|
|
hours_ago = random.uniform(0.5, 8)
|
|
last_share = (current_time - timedelta(hours=hours_ago)).strftime("%Y-%m-%d %H:%M")
|
|
|
|
# Generate hashrate (historical before going offline)
|
|
if model_info["type"] == "Bitaxe":
|
|
hashrate_3hr = round(random.uniform(1, 3), 2)
|
|
else:
|
|
hashrate_3hr = round(random.uniform(20, 90), 2)
|
|
|
|
# Use a real name if available, otherwise generate a random name
|
|
idx = i + online_count # Index for offline workers starts after online workers
|
|
if name_list and idx < len(name_list):
|
|
name = name_list[idx]
|
|
else:
|
|
# Create a unique name
|
|
if model_info["type"] == "Bitaxe":
|
|
name = f"{prefixes[-1]}{random.randint(1, 99):02d}"
|
|
else:
|
|
name = f"{random.choice(prefixes[:-1])}{random.randint(1, 99):02d}"
|
|
|
|
workers.append({
|
|
"name": name,
|
|
"status": "offline",
|
|
"type": model_info["type"],
|
|
"model": model_info["model"],
|
|
"hashrate_60sec": 0,
|
|
"hashrate_60sec_unit": hashrate_unit,
|
|
"hashrate_3hr": hashrate_3hr,
|
|
"hashrate_3hr_unit": hashrate_unit,
|
|
"efficiency": 0,
|
|
"last_share": last_share,
|
|
"earnings": 0, # Minimal earnings for offline workers
|
|
"acceptance_rate": round(random.uniform(95, 99), 1),
|
|
"power_consumption": 0,
|
|
"temperature": 0
|
|
})
|
|
|
|
# Calculate the current sum of online worker hashrates
|
|
current_total = sum(w["hashrate_3hr"] for w in workers if w["status"] == "online")
|
|
|
|
# If we have online workers and the total doesn't match, apply a scaling factor
|
|
if online_count > 0 and abs(current_total - total_hashrate) > 0.01 and current_total > 0:
|
|
scaling_factor = total_hashrate / current_total
|
|
|
|
# Apply scaling to all online workers
|
|
for worker in workers:
|
|
if worker["status"] == "online":
|
|
# Scale the 3hr hashrate to exactly match total
|
|
worker["hashrate_3hr"] = round(worker["hashrate_3hr"] * scaling_factor, 2)
|
|
|
|
# Scale the 60sec hashrate proportionally
|
|
if worker["hashrate_60sec"] > 0:
|
|
worker["hashrate_60sec"] = round(worker["hashrate_60sec"] * scaling_factor, 2)
|
|
|
|
# Reserve a small portion (5%) of earnings for offline workers
|
|
online_earnings_pool = total_unpaid_earnings * 0.95
|
|
offline_earnings_pool = total_unpaid_earnings * 0.05
|
|
|
|
# Distribute earnings based on hashrate proportion for online workers
|
|
total_effective_hashrate = sum(w["hashrate_3hr"] for w in workers if w["status"] == "online")
|
|
if total_effective_hashrate > 0:
|
|
for worker in workers:
|
|
if worker["status"] == "online":
|
|
hashrate_proportion = worker["hashrate_3hr"] / total_effective_hashrate
|
|
worker["earnings"] = round(online_earnings_pool * hashrate_proportion, 8)
|
|
|
|
# Distribute minimal earnings to offline workers
|
|
if offline_count > 0:
|
|
offline_per_worker = offline_earnings_pool / offline_count
|
|
for worker in workers:
|
|
if worker["status"] == "offline":
|
|
worker["earnings"] = round(offline_per_worker, 8)
|
|
|
|
# Final verification - ensure total earnings match
|
|
current_total_earnings = sum(w["earnings"] for w in workers)
|
|
if abs(current_total_earnings - total_unpaid_earnings) > 0.00000001:
|
|
# Adjust the first worker to account for any rounding errors
|
|
adjustment = total_unpaid_earnings - current_total_earnings
|
|
for worker in workers:
|
|
if worker["status"] == "online":
|
|
worker["earnings"] = round(worker["earnings"] + adjustment, 8)
|
|
break
|
|
|
|
return workers
|