mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00

Updated the application to use a configurable timezone instead of hardcoding "America/Los_Angeles". This change impacts the dashboard, API endpoints, and worker services. Timezone is now fetched from a configuration file or environment variable, enhancing flexibility in time display. New API endpoints for available timezones and the current configured timezone have been added. The frontend now allows users to select their timezone from a dropdown menu, which is stored in local storage for future use. Timestamps in the UI have been updated to reflect the selected timezone.
802 lines
38 KiB
Python
802 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,
|
|
"avg_acceptance_rate": 0.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),
|
|
"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(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 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(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 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
|