custom-ocean.xyz-dashboard/worker_service.py
DJObleezy 81f79e2792 Add boot sequence bypass and improve earnings check
Introduces a script in `boot.html` to bypass the boot sequence if the user has completed it previously, checking for a valid wallet configuration before redirecting to the dashboard. Additionally, updates the earnings check in `worker_service.py` to handle `None` values more robustly, ensuring a minimum unpaid earnings value of `0.001`.
2025-04-24 07:17:36 -07:00

793 lines
38 KiB
Python

"""
Worker service module for managing workers data.
"""
import logging
import random
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from config import get_timezone
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,
"hashrate_history": [],
"timestamp": datetime.now(ZoneInfo(get_timezone())).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(get_timezone()))
# 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),
"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")
# Handle None value for workers_count
if workers_count is None:
logging.warning("No workers_hashing value in cached metrics, defaulting to 1 worker")
workers_count = 1
# Force at least 1 worker if the count is 0
elif 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 is None or 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
"hashrate_history": hashrate_history,
"timestamp": datetime.now(ZoneInfo(get_timezone())).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(get_timezone()))
# 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 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
"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
"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(get_timezone()))
# 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 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
"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
"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