Refactor hashrate normalization and improve error handling

- Refactored `normalizeHashrate` function to use a lookup table for unit conversion, enhancing error handling and logging for unrecognized units.
- Updated `ArrowIndicator` class to utilize the new global `normalizeHashrate` function for consistency.
- Enhanced `updateChartWithNormalizedData` with better error handling for 24-hour averages and current hashrate data, including checks for unreasonable values.
- Improved historical data handling with validation and anomaly detection.
- Encapsulated single data point display logic in `useSingleDataPoint` for better error management.
- Refined low hashrate indicator display logic to show only when necessary and updated its appearance based on the current theme.
- Overall improvements enhance robustness, maintainability, and user experience.
This commit is contained in:
DJObleezy 2025-04-26 06:39:55 -07:00
parent 24c46f058b
commit e9825c9006

View File

@ -227,27 +227,8 @@ class ArrowIndicator {
// Normalize hashrate to a common unit (TH/s) // Normalize hashrate to a common unit (TH/s)
normalizeHashrate(value, unit) { normalizeHashrate(value, unit) {
if (!value || isNaN(value)) return 0; // Use the enhanced global normalizeHashrate function
return window.normalizeHashrate(value, unit);
unit = (unit || 'th/s').toLowerCase();
if (unit.includes('ph/s')) {
return value * 1000; // Convert PH/s to TH/s
} else if (unit.includes('eh/s')) {
return value * 1000000; // Convert EH/s to TH/s
} else if (unit.includes('gh/s')) {
return value / 1000; // Convert GH/s to TH/s
} else if (unit.includes('mh/s')) {
return value / 1000000; // Convert MH/s to TH/s
} else if (unit.includes('kh/s')) {
return value / 1000000000; // Convert KH/s to TH/s
} else if (unit.includes('h/s') && !unit.includes('th/s') && !unit.includes('ph/s') &&
!unit.includes('eh/s') && !unit.includes('gh/s') && !unit.includes('mh/s') &&
!unit.includes('kh/s')) {
return value / 1000000000000; // Convert H/s to TH/s
} else {
// Assume TH/s if unit is not recognized
return value;
}
} }
// Save current state to localStorage // Save current state to localStorage
@ -363,30 +344,99 @@ if (window['chartjs-plugin-annotation']) {
} }
// Hashrate Normalization Utilities // Hashrate Normalization Utilities
// Helper function to normalize hashrate to TH/s for consistent graphing // Enhanced normalizeHashrate function with better error handling for units
function normalizeHashrate(value, unit) { function normalizeHashrate(value, unit) {
if (!value || isNaN(value)) return 0; if (!value || isNaN(value)) return 0;
unit = (unit || 'th/s').toLowerCase(); // Validate and normalize input value
if (unit.includes('ph/s')) { value = parseFloat(value);
return value * 1000; // Convert PH/s to TH/s
} else if (unit.includes('eh/s')) { // Standardize unit handling with a lookup table
return value * 1000000; // Convert EH/s to TH/s const unit_normalized = (unit || 'th/s').toLowerCase().trim();
} else if (unit.includes('gh/s')) {
return value / 1000; // Convert GH/s to TH/s // Lookup table for conversion factors (all relative to TH/s)
} else if (unit.includes('mh/s')) { const unitConversions = {
return value / 1000000; // Convert MH/s to TH/s 'ph/s': 1000,
} else if (unit.includes('kh/s')) { 'p/s': 1000,
return value / 1000000000; // Convert KH/s to TH/s 'p': 1000,
} else if (unit.includes('h/s') && !unit.includes('th/s') && !unit.includes('ph/s') && 'petahash': 1000,
!unit.includes('eh/s') && !unit.includes('gh/s') && !unit.includes('mh/s') && 'petahash/s': 1000,
!unit.includes('kh/s')) { 'peta': 1000,
return value / 1000000000000; // Convert H/s to TH/s
'eh/s': 1000000,
'e/s': 1000000,
'e': 1000000,
'exahash': 1000000,
'exahash/s': 1000000,
'exa': 1000000,
'th/s': 1,
't/s': 1,
't': 1,
'terahash': 1,
'terahash/s': 1,
'tera': 1,
'gh/s': 1 / 1000,
'g/s': 1 / 1000,
'g': 1 / 1000,
'gigahash': 1 / 1000,
'gigahash/s': 1 / 1000,
'giga': 1 / 1000,
'mh/s': 1 / 1000000,
'm/s': 1 / 1000000,
'm': 1 / 1000000,
'megahash': 1 / 1000000,
'megahash/s': 1 / 1000000,
'mega': 1 / 1000000,
'kh/s': 1 / 1000000000,
'k/s': 1 / 1000000000,
'k': 1 / 1000000000,
'kilohash': 1 / 1000000000,
'kilohash/s': 1 / 1000000000,
'kilo': 1 / 1000000000,
'h/s': 1 / 1000000000000,
'h': 1 / 1000000000000,
'hash': 1 / 1000000000000,
'hash/s': 1 / 1000000000000
};
// Try to find the conversion factor
let conversionFactor = null;
// First try direct lookup
if (unitConversions.hasOwnProperty(unit_normalized)) {
conversionFactor = unitConversions[unit_normalized];
} else { } else {
// Assume TH/s if unit is not recognized // If direct lookup fails, try a fuzzy match
return value; for (const knownUnit in unitConversions) {
if (unit_normalized.includes(knownUnit) || knownUnit.includes(unit_normalized)) {
// Log the unit correction for debugging
console.log(`Fuzzy matching unit: "${unit}" → interpreted as "${knownUnit}" (conversion: ${unitConversions[knownUnit]})`);
conversionFactor = unitConversions[knownUnit];
break;
} }
} }
}
// If no conversion factor found, assume TH/s but log a warning
if (conversionFactor === null) {
console.warn(`Unrecognized hashrate unit: "${unit}", assuming TH/s. Value: ${value}`);
// Add additional info to help diagnose incorrect units
if (value > 1000) {
console.warn(`NOTE: Value ${value} is quite large for TH/s. Could it be PH/s?`);
} else if (value < 0.001) {
console.warn(`NOTE: Value ${value} is quite small for TH/s. Could it be GH/s or MH/s?`);
}
return value; // Assume TH/s
}
// Apply conversion and return normalized value
return value * conversionFactor;
}
// Helper function to format hashrate values for display // Helper function to format hashrate values for display
function formatHashrateForDisplay(value, unit) { function formatHashrateForDisplay(value, unit) {
@ -1078,7 +1128,7 @@ function showCongrats(message) {
}); });
} }
// Enhanced Chart Update Function with Dynamic Hashrate Selection // Enhanced Chart Update Function with better error handling for unit conversion
function updateChartWithNormalizedData(chart, data) { function updateChartWithNormalizedData(chart, data) {
if (!chart || !data) { if (!chart || !data) {
console.warn("Cannot update chart - chart or data is null"); console.warn("Cannot update chart - chart or data is null");
@ -1086,12 +1136,24 @@ function updateChartWithNormalizedData(chart, data) {
} }
try { try {
// Always update the 24hr average line even if we don't have data points yet // Process and validate 24hr average line data
let normalizedAvg = 0;
try {
const avg24hr = parseFloat(data.hashrate_24hr || 0); const avg24hr = parseFloat(data.hashrate_24hr || 0);
const avg24hrUnit = data.hashrate_24hr_unit ? data.hashrate_24hr_unit.toLowerCase() : 'th/s'; const avg24hrUnit = data.hashrate_24hr_unit ? data.hashrate_24hr_unit.toLowerCase() : 'th/s';
const normalizedAvg = normalizeHashrate(avg24hr, avg24hrUnit); normalizedAvg = normalizeHashrate(avg24hr, avg24hrUnit);
// Update the 24HR AVG line using the existing formatHashrateForDisplay function // Sanity check - if the value seems unreasonable, log a warning
if (normalizedAvg > 100000) { // Extremely large value (100,000+ TH/s)
console.warn(`WARNING: 24hr avg hashrate seems unreasonably high: ${normalizedAvg} TH/s`);
console.warn(`Original value: ${avg24hr} ${avg24hrUnit}`);
}
} catch (err) {
console.error("Error processing 24hr avg hashrate:", err);
normalizedAvg = 0;
}
// Update the 24HR AVG line annotation if available
if (!isNaN(normalizedAvg) && if (!isNaN(normalizedAvg) &&
chart.options.plugins.annotation && chart.options.plugins.annotation &&
chart.options.plugins.annotation.annotations && chart.options.plugins.annotation.annotations &&
@ -1105,14 +1167,37 @@ function updateChartWithNormalizedData(chart, data) {
annotation.label.content = '24HR AVG: ' + formattedAvg.toUpperCase(); annotation.label.content = '24HR AVG: ' + formattedAvg.toUpperCase();
} }
// Detect low hashrate devices (Bitaxe < 2 TH/s) // Process and validate current hashrates with better error handling
let normalizedHashrate60sec = 0;
let normalizedHashrate3hr = 0;
try {
const hashrate60sec = parseFloat(data.hashrate_60sec || 0); const hashrate60sec = parseFloat(data.hashrate_60sec || 0);
const hashrate60secUnit = data.hashrate_60sec_unit ? data.hashrate_60sec_unit.toLowerCase() : 'th/s'; const hashrate60secUnit = data.hashrate_60sec_unit ? data.hashrate_60sec_unit.toLowerCase() : 'th/s';
const normalizedHashrate60sec = normalizeHashrate(hashrate60sec, hashrate60secUnit); normalizedHashrate60sec = normalizeHashrate(hashrate60sec, hashrate60secUnit);
const hashrate3hr = parseFloat(data.hashrate_3hr || 0); const hashrate3hr = parseFloat(data.hashrate_3hr || 0);
const hashrate3hrUnit = data.hashrate_3hr_unit ? data.hashrate_3hr_unit.toLowerCase() : 'th/s'; const hashrate3hrUnit = data.hashrate_3hr_unit ? data.hashrate_3hr_unit.toLowerCase() : 'th/s';
const normalizedHashrate3hr = normalizeHashrate(hashrate3hr, hashrate3hrUnit); normalizedHashrate3hr = normalizeHashrate(hashrate3hr, hashrate3hrUnit);
// Check for inconsistency between 60sec and 3hr values (could indicate unit issues)
const ratioThreshold = 100; // Maximum reasonable difference
if (normalizedHashrate60sec > 0 && normalizedHashrate3hr > 0) {
const ratio = Math.max(
normalizedHashrate60sec / normalizedHashrate3hr,
normalizedHashrate3hr / normalizedHashrate60sec
);
if (ratio > ratioThreshold) {
console.warn(`WARNING: Large discrepancy between 60sec and 3hr hashrates. Possible unit error!`);
console.warn(`60sec: ${hashrate60sec} ${hashrate60secUnit}${normalizedHashrate60sec} TH/s`);
console.warn(`3hr: ${hashrate3hr} ${hashrate3hrUnit}${normalizedHashrate3hr} TH/s`);
console.warn(`Ratio: ${ratio.toFixed(2)}x difference`);
}
}
} catch (err) {
console.error("Error processing current hashrates:", err);
}
// Choose which hashrate average to display based on device characteristics // Choose which hashrate average to display based on device characteristics
let useHashrate3hr = false; let useHashrate3hr = false;
@ -1127,8 +1212,10 @@ function updateChartWithNormalizedData(chart, data) {
} }
} }
// Process history data if available // Process history data with enhanced validation and error handling
if (data.arrow_history && data.arrow_history.hashrate_60sec) { if (data.arrow_history && data.arrow_history.hashrate_60sec) {
// Validate history data
try {
console.log("History data received:", data.arrow_history.hashrate_60sec); console.log("History data received:", data.arrow_history.hashrate_60sec);
// If we're using 3hr average, try to use that history if available // If we're using 3hr average, try to use that history if available
@ -1136,14 +1223,11 @@ function updateChartWithNormalizedData(chart, data) {
data.arrow_history.hashrate_3hr : data.arrow_history.hashrate_60sec; data.arrow_history.hashrate_3hr : data.arrow_history.hashrate_60sec;
if (historyData && historyData.length > 0) { if (historyData && historyData.length > 0) {
// Add day info to labels if they cross midnight // Format time labels
let prevHour = -1;
let dayCount = 0;
chart.data.labels = historyData.map(item => { chart.data.labels = historyData.map(item => {
const timeStr = item.time; const timeStr = item.time;
try {
// Convert the time string to a Date object in Los Angeles timezone // Parse and format the time (existing code)...
let timeParts; let timeParts;
if (timeStr.length === 8 && timeStr.indexOf(':') !== -1) { if (timeStr.length === 8 && timeStr.indexOf(':') !== -1) {
// Format: HH:MM:SS // Format: HH:MM:SS
@ -1153,20 +1237,16 @@ function updateChartWithNormalizedData(chart, data) {
timeParts = timeStr.split(':'); timeParts = timeStr.split(':');
timeParts.push('00'); // Add seconds timeParts.push('00'); // Add seconds
} else { } else {
// Use current date if format is unexpected return timeStr; // Use as-is if format is unexpected
return timeStr;
} }
// Create a date object for today with the time // Format in 12-hour time with timezone support
const now = new Date(); const now = new Date();
const timeDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), const timeDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(),
parseInt(timeParts[0]), parseInt(timeParts[1]), parseInt(timeParts[2] || 0)); parseInt(timeParts[0]), parseInt(timeParts[1]), parseInt(timeParts[2] || 0));
// Format in 12-hour time for Los Angeles (Pacific Time)
// The options define Pacific Time and 12-hour format without AM/PM
try {
let formattedTime = timeDate.toLocaleTimeString('en-US', { let formattedTime = timeDate.toLocaleTimeString('en-US', {
timeZone: dashboardTimezone, timeZone: dashboardTimezone || 'America/Los_Angeles',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
hour12: true hour12: true
@ -1174,30 +1254,70 @@ function updateChartWithNormalizedData(chart, data) {
// Remove the AM/PM part // Remove the AM/PM part
formattedTime = formattedTime.replace(/\s[AP]M$/i, ''); formattedTime = formattedTime.replace(/\s[AP]M$/i, '');
return formattedTime; return formattedTime;
} catch (e) { } catch (e) {
console.error("Error formatting time:", e); console.error("Error formatting time:", e, timeStr);
return timeStr.substring(0, 5); // Fallback to original format return timeStr; // Use original on error
} }
}); });
chart.data.datasets[0].data = historyData.map(item => { // Process and normalize hashrate values with validation
const val = parseFloat(item.value); const hashrateValues = [];
const unit = item.unit || 'th/s'; // Ensure unit is assigned const validatedData = historyData.map((item, index) => {
return normalizeHashrate(val, unit); try {
// Safety conversion in case value is a string
const val = parseFloat(item.value || 0);
if (isNaN(val)) {
console.warn(`Invalid value at index ${index}: ${item.value}`);
return 0;
}
// Validate the unit
const unit = item.unit || 'th/s';
const normalizedValue = normalizeHashrate(val, unit);
// Collect valid values for statistics
if (normalizedValue > 0) {
hashrateValues.push(normalizedValue);
}
return normalizedValue;
} catch (err) {
console.error(`Error processing hashrate at index ${index}:`, err);
return 0;
}
}); });
chart.data.datasets[0].data = validatedData;
// Calculate statistics for anomaly detection
if (hashrateValues.length > 1) {
const mean = hashrateValues.reduce((sum, val) => sum + val, 0) / hashrateValues.length;
const max = Math.max(...hashrateValues);
const min = Math.min(...hashrateValues);
// Check for outliers that might indicate incorrect units
if (max > mean * 10 || min < mean / 10) {
console.warn("WARNING: Wide hashrate variance detected in chart data. Possible unit inconsistency.");
console.warn(`Min: ${min.toFixed(2)} TH/s, Max: ${max.toFixed(2)} TH/s, Mean: ${mean.toFixed(2)} TH/s`);
}
}
// Update chart dataset label to indicate which average we're displaying // Update chart dataset label to indicate which average we're displaying
chart.data.datasets[0].label = useHashrate3hr ? chart.data.datasets[0].label = useHashrate3hr ?
'Hashrate Trend (3HR AVG)' : 'Hashrate Trend (60SEC AVG)'; 'Hashrate Trend (3HR AVG)' : 'Hashrate Trend (60SEC AVG)';
const values = chart.data.datasets[0].data.filter(v => !isNaN(v) && v !== null); // Calculate appropriate y-axis range with safeguards for outliers
const values = chart.data.datasets[0].data.filter(v => !isNaN(v) && v !== null && v > 0);
if (values.length > 0) { if (values.length > 0) {
const max = Math.max(...values); const max = Math.max(...values);
const min = Math.min(...values.filter(v => v > 0)) || 0; const min = Math.min(...values) || 0;
// Use a more reasonable range if we have outliers
chart.options.scales.y.min = min * 0.8; chart.options.scales.y.min = min * 0.8;
chart.options.scales.y.max = max * 1.2; chart.options.scales.y.max = max * 1.2;
// Set appropriate step size based on range
const range = max - min; const range = max - min;
if (range > 1000) { if (range > 1000) {
chart.options.scales.y.ticks.stepSize = 500; chart.options.scales.y.ticks.stepSize = 500;
@ -1209,16 +1329,28 @@ function updateChartWithNormalizedData(chart, data) {
chart.options.scales.y.ticks.stepSize = 1; chart.options.scales.y.ticks.stepSize = 1;
} }
} }
} else {
console.warn("No valid history data items available");
}
} catch (historyError) {
console.error("Error processing hashrate history data:", historyError);
// Fall back to single datapoint if history processing fails
useSingleDataPoint();
} }
} else { } else {
// No history data, just use the current point // No history data, use single datapoint
// Format current time in 12-hour format for Los Angeles timezone without AM/PM useSingleDataPoint();
}
// Handle single datapoint display when no history is available
function useSingleDataPoint() {
try {
// Format current time
const now = new Date(); const now = new Date();
let currentTime; let currentTime;
try { try {
currentTime = now.toLocaleTimeString('en-US', { currentTime = now.toLocaleTimeString('en-US', {
timeZone: dashboardTimezone, timeZone: dashboardTimezone || 'America/Los_Angeles',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
hour12: true hour12: true
@ -1228,24 +1360,37 @@ function updateChartWithNormalizedData(chart, data) {
currentTime = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }); currentTime = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
} }
// Choose which current hashrate to display based on our earlier logic // Choose which current hashrate to display with validation
let currentValue, currentUnit; let currentValue, currentUnit, normalizedValue;
if (useHashrate3hr) { if (useHashrate3hr) {
currentValue = parseFloat(data.hashrate_3hr || 0); currentValue = parseFloat(data.hashrate_3hr || 0);
currentUnit = data.hashrate_3hr_unit ? data.hashrate_3hr_unit.toLowerCase() : 'th/s'; currentUnit = data.hashrate_3hr_unit || 'th/s';
chart.data.datasets[0].label = 'Hashrate Trend (3HR AVG)'; chart.data.datasets[0].label = 'Hashrate Trend (3HR AVG)';
} else { } else {
currentValue = parseFloat(data.hashrate_60sec || 0); currentValue = parseFloat(data.hashrate_60sec || 0);
currentUnit = data.hashrate_60sec_unit ? data.hashrate_60sec_unit.toLowerCase() : 'th/s'; currentUnit = data.hashrate_60sec_unit || 'th/s';
chart.data.datasets[0].label = 'Hashrate Trend (60SEC AVG)'; chart.data.datasets[0].label = 'Hashrate Trend (60SEC AVG)';
} }
const normalizedValue = normalizeHashrate(currentValue, currentUnit); // Guard against invalid values
chart.data.labels = [currentTime]; if (isNaN(currentValue)) {
chart.data.datasets[0].data = [normalizedValue]; console.warn("Invalid hashrate value, using 0");
normalizedValue = 0;
} else {
normalizedValue = normalizeHashrate(currentValue, currentUnit);
} }
// In updateChartWithNormalizedData function chart.data.labels = [currentTime];
chart.data.datasets[0].data = [normalizedValue];
} catch (err) {
console.error("Error setting up single datapoint:", err);
chart.data.labels = ["Now"];
chart.data.datasets[0].data = [0];
}
}
// Show low hashrate indicator as needed
if (useHashrate3hr) { if (useHashrate3hr) {
// Add indicator text to the chart // Add indicator text to the chart
if (!chart.lowHashrateIndicator) { if (!chart.lowHashrateIndicator) {
@ -1256,11 +1401,8 @@ function updateChartWithNormalizedData(chart, data) {
const indicator = document.createElement('div'); const indicator = document.createElement('div');
indicator.id = 'lowHashrateIndicator'; indicator.id = 'lowHashrateIndicator';
indicator.style.position = 'absolute'; indicator.style.position = 'absolute';
indicator.style.top = '10px';
// Change position from bottom to top right
indicator.style.top = '10px'; // Changed from bottom to top
indicator.style.right = '10px'; indicator.style.right = '10px';
indicator.style.background = 'rgba(0,0,0,0.7)'; indicator.style.background = 'rgba(0,0,0,0.7)';
indicator.style.color = theme.PRIMARY; indicator.style.color = theme.PRIMARY;
indicator.style.padding = '5px 10px'; indicator.style.padding = '5px 10px';
@ -1273,16 +1415,14 @@ function updateChartWithNormalizedData(chart, data) {
chart.lowHashrateIndicator = indicator; chart.lowHashrateIndicator = indicator;
} }
} else { } else {
// Update color based on current theme
chart.lowHashrateIndicator.style.color = getCurrentTheme().PRIMARY; chart.lowHashrateIndicator.style.color = getCurrentTheme().PRIMARY;
// Show the indicator if it already exists
chart.lowHashrateIndicator.style.display = 'block'; chart.lowHashrateIndicator.style.display = 'block';
} }
} else if (chart.lowHashrateIndicator) { } else if (chart.lowHashrateIndicator) {
// Hide the indicator when not in low hashrate mode
chart.lowHashrateIndicator.style.display = 'none'; chart.lowHashrateIndicator.style.display = 'none';
} }
// Finally update the chart with a safe non-animating update
chart.update('none'); chart.update('none');
} catch (chartError) { } catch (chartError) {
console.error("Error updating chart:", chartError); console.error("Error updating chart:", chartError);