init repo
This commit is contained in:
4
.env
Normal file
4
.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
PORT=3000
|
||||||
|
MINER_IP=192.168.31.174
|
||||||
|
MINER_PORT=4028
|
||||||
|
NODE_ENV=production
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/node_modules
|
||||||
352
app.js
Normal file
352
app.js
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
// server.js
|
||||||
|
const express = require('express')
|
||||||
|
const cors = require('cors')
|
||||||
|
const { WhatsMiner, ResponseError } = require('@thermocline-labs/whats-miner')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const PORT = process.env.PORT || 3000
|
||||||
|
const MINER_IP = process.env.MINER_IP || '192.168.31.174'
|
||||||
|
const MINER_PORT = parseInt(process.env.MINER_PORT || '4028')
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors())
|
||||||
|
app.use(express.json())
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
// Логирование запросов
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`)
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Инициализация клиента майнера
|
||||||
|
const client = new WhatsMiner(MINER_IP, MINER_PORT)
|
||||||
|
|
||||||
|
// Обработчик ошибок
|
||||||
|
const errorHandler = (err, res, customMessage = 'Internal server error') => {
|
||||||
|
console.error('Error:', err)
|
||||||
|
|
||||||
|
if (err instanceof ResponseError) {
|
||||||
|
return res.status(502).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: 'ResponseError',
|
||||||
|
message: err.message,
|
||||||
|
code: err.code
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
return res.status(504).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: 'TimeoutError',
|
||||||
|
message: 'Request timeout'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: err.name || 'UnknownError',
|
||||||
|
message: customMessage,
|
||||||
|
details: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Эндпоинт: получение общей информации
|
||||||
|
app.get('/api/summary', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 5000) // 5 сек таймаут
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = await client.summary(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: summary
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to fetch summary')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: статистика майнера
|
||||||
|
app.get('/api/stats', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await client.stats(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: stats
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to fetch stats')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: информация об устройствах (чипах)
|
||||||
|
app.get('/api/devs', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const devs = await client.devs(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: devs
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to fetch devices')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: информация о пулах
|
||||||
|
app.get('/api/pools', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pools = await client.pools(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: pools
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to fetch pools')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: температура и вентиляторы
|
||||||
|
app.get('/api/temperature', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = await client.summary(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
// Извлечение температурных данных
|
||||||
|
const temperatureData = {
|
||||||
|
board_temp: summary.BoardTemp || summary.temp || null,
|
||||||
|
chip_temp: summary.ChipTemp || summary.temp2 || null,
|
||||||
|
fan_speed: summary.FanSpeed || summary.fan || null,
|
||||||
|
fan_percent: summary.FanPercent || null
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: temperatureData
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to fetch temperature data')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: хешрейт
|
||||||
|
app.get('/api/hashrate', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 5000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = await client.summary(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
const hashrateData = {
|
||||||
|
current: summary.GHS_5s || summary.hashrate || 0,
|
||||||
|
average: summary.GHS_av || summary.hashrate_avg || 0,
|
||||||
|
realtime: summary.GHS_rt || summary.hashrate_rt || 0,
|
||||||
|
unit: 'GH/s'
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: hashrateData
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to fetch hashrate')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: перезагрузка майнера
|
||||||
|
app.post('/api/reboot', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 10000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.reboot(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Miner reboot initiated',
|
||||||
|
data: result
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to reboot miner')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: проверка состояния
|
||||||
|
app.get('/api/health', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 3000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = await client.summary(abortController.signal)
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
status: 'healthy',
|
||||||
|
miner: {
|
||||||
|
ip: MINER_IP,
|
||||||
|
port: MINER_PORT,
|
||||||
|
connected: true,
|
||||||
|
uptime: summary.Uptime || null,
|
||||||
|
hashrate: summary.GHS_5s || 0
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
status: 'unhealthy',
|
||||||
|
miner: {
|
||||||
|
ip: MINER_IP,
|
||||||
|
port: MINER_PORT,
|
||||||
|
connected: false
|
||||||
|
},
|
||||||
|
error: err.message,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Эндпоинт: все данные (агрегированный)
|
||||||
|
app.get('/api/all', async (req, res) => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
const timeout = setTimeout(() => abortController.abort(), 10000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [summary, stats, devs, pools] = await Promise.all([
|
||||||
|
client.summary(abortController.signal),
|
||||||
|
client.stats(abortController.signal),
|
||||||
|
client.devs(abortController.signal),
|
||||||
|
client.pools(abortController.signal)
|
||||||
|
])
|
||||||
|
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
const aggregatedData = {
|
||||||
|
summary,
|
||||||
|
stats,
|
||||||
|
devs,
|
||||||
|
pools,
|
||||||
|
quick_stats: {
|
||||||
|
hashrate: summary.GHS_5s || 0,
|
||||||
|
temperature: summary.BoardTemp || summary.temp || null,
|
||||||
|
uptime: summary.Uptime || null,
|
||||||
|
chips_active: devs?.DEVS?.reduce((sum, dev) => sum + (dev.ChipsActive || 0), 0) || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: aggregatedData
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
errorHandler(err, res, 'Failed to fetch all data')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Главная страница (документация)
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
api: 'WhatsMiner REST API',
|
||||||
|
version: '1.0.0',
|
||||||
|
miner: {
|
||||||
|
ip: MINER_IP,
|
||||||
|
port: MINER_PORT
|
||||||
|
},
|
||||||
|
endpoints: {
|
||||||
|
'GET /api/summary': 'Общая информация о майнере',
|
||||||
|
'GET /api/stats': 'Статистика майнера',
|
||||||
|
'GET /api/devs': 'Информация об устройствах (чипах)',
|
||||||
|
'GET /api/pools': 'Информация о пулах',
|
||||||
|
'GET /api/temperature': 'Температура и вентиляторы',
|
||||||
|
'GET /api/hashrate': 'Текущий хешрейт',
|
||||||
|
'GET /api/health': 'Проверка состояния',
|
||||||
|
'GET /api/all': 'Все данные (агрегированные)',
|
||||||
|
'POST /api/reboot': 'Перезагрузка майнера'
|
||||||
|
},
|
||||||
|
documentation: 'https://github.com/thermocline-labs/whats-miner'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Обработчик 404
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: 'NotFoundError',
|
||||||
|
message: 'Endpoint not found',
|
||||||
|
path: req.path
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Запуск сервера
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 WhatsMiner API запущен на порту ${PORT}`)
|
||||||
|
console.log(`📍 IP майнера: ${MINER_IP}:${MINER_PORT}`)
|
||||||
|
console.log(`📚 Документация: http://localhost:${PORT}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Обработка завершения
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM received, shutting down gracefully')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('SIGINT received, shutting down gracefully')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
1240
package-lock.json
generated
Normal file
1240
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "whatsminer-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "REST API для мониторинга WhatsMiner",
|
||||||
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node app.js",
|
||||||
|
"dev": "nodemon app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"@thermocline-labs/whats-miner": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user