Compare commits

..

28 Commits

Author SHA1 Message Date
0dc14a6ea1 5.1.7
Some checks failed
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 5s
Publish to npm / npm-publish (push) Failing after 20s
Release / build-and-release (push) Successful in 51s
CI / Build All Platforms (push) Successful in 57s
2025-10-23 13:25:32 +00:00
dea344e6ba feat(status): display power metrics in service status output
Some checks failed
CI / Type Check & Lint (push) Successful in 7s
CI / Build Test (Current Platform) (push) Successful in 5s
CI / Build All Platforms (push) Has been cancelled
2025-10-23 13:24:55 +00:00
f81f5957ab 5.1.6
Some checks failed
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 6s
CI / Build All Platforms (push) Successful in 53s
Publish to npm / npm-publish (push) Failing after 53s
Release / build-and-release (push) Successful in 1m7s
2025-10-23 13:18:22 +00:00
281d3fbbeb fix(ci): correct setup-deno action version to install Deno 2.x
Some checks failed
CI / Type Check & Lint (push) Successful in 9s
CI / Build Test (Current Platform) (push) Successful in 8s
CI / Build All Platforms (push) Has been cancelled
2025-10-23 13:17:56 +00:00
c1cb136a7d 5.1.5
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Failing after 3s
CI / Build All Platforms (push) Failing after 3s
Release / build-and-release (push) Failing after 4s
2025-10-23 13:15:44 +00:00
b80275a594 5.1.3
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Build All Platforms (push) Failing after 4s
Release / build-and-release (push) Failing after 4s
2025-10-23 13:03:35 +00:00
b64a515c94 set deno version 2025-10-23 13:03:29 +00:00
68c4eb6480 5.1.2
Some checks failed
CI / Type Check & Lint (push) Failing after 3s
CI / Build Test (Current Platform) (push) Failing after 3s
CI / Build All Platforms (push) Failing after 3s
Release / build-and-release (push) Failing after 3s
2025-10-23 13:00:24 +00:00
6c8f6ac33f fix(scripts): Add build script to package.json and include local dev tool settings 2025-10-23 13:00:24 +00:00
ffa491c7a1 5.1.1
Some checks failed
Release / build-and-release (push) Failing after 3s
2025-10-23 12:57:58 +00:00
777d48d82e fix(tooling): better oids and more power metrics. Also new json httpServer feature support. 2025-10-23 12:57:58 +00:00
b7a0bbcf6d fix(snmp): Update current handling for Tripplite and Liebert models; add APC current logging 2025-10-23 12:45:29 +00:00
fbe1cd64cb feat(snmp): Enhance SNMP metrics with output load, power, voltage, and current readings 2025-10-23 12:25:59 +00:00
9ba50da73c 5.1.0
Some checks failed
Release / build-and-release (push) Failing after 3s
2025-10-22 14:18:09 +00:00
684319983d feat(packaging): Add npm packaging and installer: wrapper, postinstall downloader, publish workflow, and packaging files 2025-10-22 14:18:09 +00:00
18bd9f6cda fix(install): add error checking for binary move and chmod operations
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 47s
CI / Build All Platforms (push) Successful in 50s
- Check if mv command succeeds
- Verify binary exists after move
- Check if chmod succeeds
- Exit with error instead of continuing on failure
2025-10-20 13:33:00 +00:00
f03c683d02 fix(install): correct installation order for updates
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 42s
CI / Build All Platforms (push) Successful in 47s
- Stop service first
- Remove /opt/nupst
- Create fresh directory
- Download binary
- Ensures clean installation without leaving empty directories
2025-10-20 13:28:56 +00:00
f750299780 fix(install): simplify installation to only binary in /opt/nupst
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 46s
CI / Build All Platforms (push) Successful in 48s
- Remove all conditional migration logic
- Always completely clean /opt/nupst before installation
- Ensures only NUPST binary exists in installation directory
- Simplified service restart logic
2025-10-20 13:24:03 +00:00
ca1039408d chore(release): bump version to 5.0.2
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 42s
CI / Build All Platforms (push) Successful in 47s
2025-10-20 13:09:20 +00:00
df3e0b9424 fix: import process from node:process in script-action
Some checks failed
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
CI / Build All Platforms (push) Has been cancelled
Fixes TS2580 error where process was undefined
2025-10-20 13:08:43 +00:00
c8e5960abd chore(release): bump version to 5.0.1
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 45s
CI / Build All Platforms (push) Successful in 48s
2025-10-20 13:07:07 +00:00
7304a62357 chore(release): bump version to 5.0.0
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 41s
CI / Build All Platforms (push) Successful in 47s
BREAKING CHANGE: Deprecated CLI commands removed

This is a major version bump due to breaking changes:
- Removed all deprecated flat command structure
- Users must now use modern subcommand structure:
  - nupst service <subcommand>
  - nupst ups <subcommand>
  - nupst group <subcommand>
  - nupst action <subcommand>

New in v5.0:
- Enhanced status display showing actions and groups (v4.3.3)
- Action management system (v4.3.0)
- Improved type safety (v4.2.5)
- Config auto-reload (v4.3.2)

Updated readme version references to v5.0.0
2025-10-20 13:00:42 +00:00
a5a88e53ba docs: improve accuracy of dependency claims
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 5s
CI / Build All Platforms (push) Successful in 45s
- Clarify 'zero runtime dependencies' means for compiled binary
- Change 'no npm' to 'no installation required'
- Update 'Zero Dependencies' to 'Self-Contained Binary'
- Improve migration table accuracy (Runtime Dependencies vs build deps)
- More honest about supply chain (reduced vs eliminated)
2025-10-20 12:59:14 +00:00
73bc271c59 docs: remove deprecated command migration documentation
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 4s
CI / Build All Platforms (push) Successful in 45s
- Remove 'Command Migration' section showing old commands
- Users must now use modern subcommand structure
- Clean up migration guide to focus on actual changes
2025-10-20 12:57:48 +00:00
1e98181e71 feat: remove deprecated CLI commands
BREAKING CHANGE: Old flat command structure no longer supported

Removed deprecated commands:
- nupst add → use 'nupst ups add'
- nupst edit → use 'nupst ups edit'
- nupst delete → use 'nupst ups remove'
- nupst list → use 'nupst ups list'
- nupst test → use 'nupst ups test'
- nupst setup → use 'nupst ups edit'
- nupst enable → use 'nupst service enable'
- nupst disable → use 'nupst service disable'
- nupst start → use 'nupst service start'
- nupst stop → use 'nupst service stop'
- nupst status → use 'nupst service status'
- nupst logs → use 'nupst service logs'
- nupst daemon-start → use 'nupst service start-daemon'

Also removed 'delete' as alias for 'remove' (use 'rm' instead)

Modern command structure is now required:
- nupst service <subcommand>
- nupst ups <subcommand>
- nupst group <subcommand>
- nupst action <subcommand>

Kept modern aliases: rm, ls
2025-10-20 12:57:23 +00:00
eb5a8185ae docs: create comprehensive readme with v4.3 features
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Successful in 6s
CI / Build All Platforms (push) Successful in 45s
- Add action management documentation (new feature)
- Update configuration examples to v4.1+ action-based format
- Document trigger modes and action system
- Update status command output examples with groups and actions
- Remove outdated contributing section
- Add modern emojis and engaging tone
- Update all version references to v4.3.3
- Maintain Task Venture Capital GmbH legal section
2025-10-20 12:52:26 +00:00
ef3d3f3fa3 chore(release): bump version to 4.3.3
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 43s
CI / Build All Platforms (push) Successful in 48s
2025-10-20 12:48:26 +00:00
34e6e850ad feat(status): display actions and groups in status command
- Add action display to UPS status showing trigger mode, thresholds, and delays
- Create displayGroupsStatus() method to show group information
- Display group mode, member UPS devices, and group actions
- Integrate groups section into status command output
2025-10-20 12:48:14 +00:00
26 changed files with 2294 additions and 590 deletions

View File

@@ -21,7 +21,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
deno-version: v2.x
- name: Check TypeScript types
run: deno check mod.ts
@@ -45,7 +45,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
deno-version: v2.x
- name: Compile for current platform
run: |
@@ -71,7 +71,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
deno-version: v2.x
- name: Compile all platform binaries
run: bash scripts/compile-all.sh

View File

@@ -0,0 +1,129 @@
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
npm-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org/'
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
echo "Publishing version: $VERSION"
- name: Verify deno.json version matches tag
run: |
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
TAG_VERSION="${{ steps.version.outputs.version_number }}"
echo "deno.json version: $DENO_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
echo "ERROR: Version mismatch!"
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
exit 1
fi
- name: Compile binaries for npm package
run: |
echo "Compiling binaries for npm package..."
deno task compile
echo ""
echo "Binary sizes:"
ls -lh dist/binaries/
- name: Generate SHA256 checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS
cat SHA256SUMS
cd ../..
- name: Sync package.json version
run: |
VERSION="${{ steps.version.outputs.version_number }}"
echo "Syncing package.json to version ${VERSION}..."
npm version ${VERSION} --no-git-tag-version --allow-same-version
echo "package.json version: $(grep '"version"' package.json | head -1)"
- name: Create npm package
run: |
echo "Creating npm package..."
npm pack
echo ""
echo "Package created:"
ls -lh *.tgz
- name: Test local installation
run: |
echo "Testing local package installation..."
PACKAGE_FILE=$(ls *.tgz)
npm install -g ${PACKAGE_FILE}
echo ""
echo "Testing nupst command:"
nupst --version || echo "Note: Binary execution may fail in CI environment"
echo ""
echo "Checking installed files:"
npm ls -g @serve.zone/nupst || true
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "Publishing to npm registry..."
npm publish --access public
echo ""
echo "✅ Successfully published @serve.zone/nupst to npm!"
echo ""
echo "Package info:"
npm view @serve.zone/nupst
- name: Verify npm package
run: |
echo "Waiting for npm propagation..."
sleep 30
echo ""
echo "Verifying published package..."
npm view @serve.zone/nupst
echo ""
echo "Testing installation from npm:"
npm install -g @serve.zone/nupst
echo ""
echo "Package installed successfully!"
which nupst || echo "Binary location check skipped"
- name: Publish Summary
run: |
echo "================================================"
echo " npm Publish Complete!"
echo "================================================"
echo ""
echo "✅ Package: @serve.zone/nupst"
echo "✅ Version: ${{ steps.version.outputs.version }}"
echo ""
echo "Installation:"
echo " npm install -g @serve.zone/nupst"
echo ""
echo "Registry:"
echo " https://www.npmjs.com/package/@serve.zone/nupst"
echo ""

View File

@@ -18,7 +18,7 @@ jobs:
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
deno-version: v2.x
- name: Get version from tag
id: version

183
.github/workflows/npm-publish.yml vendored Normal file
View File

@@ -0,0 +1,183 @@
name: Publish to npm
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g., 5.0.6)'
required: true
type: string
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
# Checkout the repository
- name: Checkout code
uses: actions/checkout@v4
# Setup Deno
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
# Setup Node.js for npm publishing
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org/'
# Compile binaries for all platforms
- name: Compile binaries
run: |
echo "Compiling binaries for all platforms..."
deno task compile
echo ""
echo "Binary sizes:"
ls -lh dist/binaries/
# Update version in package.json if triggered manually
- name: Update version in package.json
if: github.event_name == 'workflow_dispatch'
run: |
VERSION=${{ github.event.inputs.version }}
echo "Updating package.json to version ${VERSION}"
npm version ${VERSION} --no-git-tag-version
# Extract version from tag if triggered by tag push
- name: Extract version from tag
if: startsWith(github.ref, 'refs/tags/')
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "Extracted version: ${VERSION}"
# Ensure versions are synchronized
- name: Sync versions
run: |
if [ -n "${VERSION}" ]; then
echo "Syncing version ${VERSION} across files..."
# Update deno.json
sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" deno.json
# Update package.json
npm version ${VERSION} --no-git-tag-version --allow-same-version
echo "Updated versions:"
echo "deno.json: $(grep '"version"' deno.json)"
echo "package.json: $(grep '"version"' package.json | head -1)"
fi
# Generate SHA256 checksums for binaries
- name: Generate checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS
echo "Checksums generated:"
cat SHA256SUMS
cd ../..
# Create npm package
- name: Create npm package
run: |
echo "Creating npm package..."
npm pack
echo ""
echo "Package created:"
ls -lh *.tgz
# Test package installation locally
- name: Test local installation
run: |
echo "Testing local package installation..."
PACKAGE_FILE=$(ls *.tgz)
npm install -g ${PACKAGE_FILE}
echo ""
echo "Testing nupst command:"
nupst --version || echo "Note: Binary execution may fail in CI environment"
echo ""
echo "Checking installed files:"
npm ls -g @serve.zone/nupst
# Publish to npm (only on tag push or manual trigger)
- name: Publish to npm
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "Publishing to npm registry..."
npm publish --access public
echo ""
echo "✅ Successfully published @serve.zone/nupst to npm!"
echo ""
echo "Package info:"
npm view @serve.zone/nupst
# Create GitHub Release (only on tag push)
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: |
dist/binaries/nupst-*
dist/binaries/SHA256SUMS
*.tgz
generate_release_notes: true
body: |
## NUPST ${{ env.VERSION }}
### Installation
#### Via npm (recommended)
```bash
npm install -g @serve.zone/nupst
```
#### Direct download
Download the appropriate binary for your platform from the assets below.
### Platform Support
- Linux x64 / ARM64
- macOS x64 / ARM64 (Apple Silicon)
- Windows x64
### Checksums
SHA256 checksums are available in `SHA256SUMS` file.
# Verify the published package
verify:
needs: build-and-publish
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
- name: Wait for npm propagation
run: sleep 30
- name: Verify npm package
run: |
echo "Verifying published package..."
npm view @serve.zone/nupst
echo ""
echo "Testing installation from npm:"
npm install -g @serve.zone/nupst
echo ""
echo "Package installed successfully!"
which nupst || echo "Binary location check skipped"

54
.npmignore Normal file
View File

@@ -0,0 +1,54 @@
# Source code (not needed for binary distribution)
/ts/
/test/
mod.ts
*.ts
# Development files
.git/
.gitea/
.claude/
.serena/
.nogit/
.github/
deno.json
deno.lock
tsconfig.json
# Scripts not needed for npm
/scripts/compile-all.sh
install.sh
uninstall.sh
example-action.sh
# Documentation files not needed for npm package
readme.plan.md
readme.hints.md
npm-publish-instructions.md
docs/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Keep only the install-binary.js in scripts/
/scripts/*
!/scripts/install-binary.js
# Exclude all dist directory (binaries will be downloaded during install)
/dist/
# Logs and temporary files
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Other
node_modules/
.env
.env.*

108
bin/nupst-wrapper.js Normal file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* NUPST npm wrapper
* This script executes the appropriate pre-compiled binary based on the current platform
*/
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
import { platform, arch } from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Get the binary name for the current platform
*/
function getBinaryName() {
const plat = platform();
const architecture = arch();
// Map Node's platform/arch to our binary naming
const platformMap = {
'darwin': 'macos',
'linux': 'linux',
'win32': 'windows'
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64'
};
const mappedPlatform = platformMap[plat];
const mappedArch = archMap[architecture];
if (!mappedPlatform || !mappedArch) {
console.error(`Error: Unsupported platform/architecture: ${plat}/${architecture}`);
console.error('Supported platforms: Linux, macOS, Windows');
console.error('Supported architectures: x64, arm64');
process.exit(1);
}
// Construct binary name
let binaryName = `nupst-${mappedPlatform}-${mappedArch}`;
if (plat === 'win32') {
binaryName += '.exe';
}
return binaryName;
}
/**
* Execute the binary
*/
function executeBinary() {
const binaryName = getBinaryName();
const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName);
// Check if binary exists
if (!existsSync(binaryPath)) {
console.error(`Error: Binary not found at ${binaryPath}`);
console.error('This might happen if:');
console.error('1. The postinstall script failed to run');
console.error('2. The platform is not supported');
console.error('3. The package was not installed correctly');
console.error('');
console.error('Try reinstalling the package:');
console.error(' npm uninstall -g @serve.zone/nupst');
console.error(' npm install -g @serve.zone/nupst');
process.exit(1);
}
// Spawn the binary with all arguments passed through
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
shell: false
});
// Handle child process events
child.on('error', (err) => {
console.error(`Error executing nupst: ${err.message}`);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
});
// Forward signals to child process
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
signals.forEach(signal => {
process.on(signal, () => {
if (!child.killed) {
child.kill(signal);
}
});
});
}
// Execute
executeBinary();

View File

@@ -1,5 +1,30 @@
# Changelog
## 2025-10-23 - 5.1.2 - fix(scripts)
Add build script to package.json and include local dev tool settings
- Add a 'build' script to package.json (no-op placeholder) to provide an explicit build step
- Minor scripts section formatting tidy in package.json
- Add a hidden local settings file for development tooling permissions to the repository (local-only configuration)
## 2025-10-23 - 5.1.1 - fix(tooling)
Add .claude/settings.local.json with local automation permissions
- Add .claude/settings.local.json to specify allowed permissions for local automated tasks
- Grants permissions for various developer/CI actions (deno check/lint/fmt, npm/npm pack, selective Bash commands, WebFetch to docs.deno.com and code.foss.global, and file/read/replace helpers)
- This is a developer/local tooling config only and does not change runtime code or package behavior
## 2025-10-22 - 5.1.0 - feat(packaging)
Add npm packaging and installer: wrapper, postinstall downloader, publish workflow, and packaging files
- Add package.json (v5.0.5) and npm packaging metadata to publish @serve.zone/nupst
- Include a small Node.js wrapper (bin/nupst-wrapper.js) to execute platform-specific precompiled binaries
- Add postinstall script (scripts/install-binary.js) that downloads the correct binary for the current platform and sets executable permissions
- Add GitHub Actions workflow (.github/workflows/npm-publish.yml) to build binaries, pack and publish to npm, and create releases
- Add .npmignore to keep source, tests and dev files out of npm package; keep only runtime installer and wrapper
- Move example action script into docs (docs/example-action.sh) and remove the top-level example-action.sh
- Include generated npm package artifact (serve.zone-nupst-5.0.5.tgz) and npmextra.json
## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime
**MAJOR RELEASE: NUPST v4.0 is a complete rewrite powered by Deno**

View File

@@ -1,7 +1,8 @@
{
"name": "@serve.zone/nupst",
"version": "4.3.2",
"version": "5.1.7",
"exports": "./mod.ts",
"nodeModulesDir": "auto",
"tasks": {
"dev": "deno run --allow-all mod.ts",
"compile": "deno task compile:all",

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# NUPST Installer Script (v4.0+)
# NUPST Installer Script (v5.0+)
# Downloads and installs pre-compiled NUPST binary from Gitea releases
#
# Usage:
@@ -8,7 +8,7 @@
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
#
# With version specification:
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0
#
# Options:
# -h, --help Show this help message
@@ -48,14 +48,14 @@ while [[ $# -gt 0 ]]; do
done
if [ $SHOW_HELP -eq 1 ]; then
echo "NUPST Installer Script (v4.0+)"
echo "NUPST Installer Script (v5.0+)"
echo "Downloads and installs pre-compiled NUPST binary"
echo ""
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --version VERSION Install specific version (e.g., v4.0.0)"
echo " --version VERSION Install specific version (e.g., v5.0.0)"
echo " --install-dir DIR Installation directory (default: /opt/nupst)"
echo ""
echo "Examples:"
@@ -63,7 +63,7 @@ if [ $SHOW_HELP -eq 1 ]; then
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
echo ""
echo " # Install specific version"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0"
exit 0
fi
@@ -145,7 +145,7 @@ get_latest_version() {
# Main installation process
echo "================================================"
echo " NUPST Installation Script (v4.0+)"
echo " NUPST Installation Script (v5.0+)"
echo "================================================"
echo ""
@@ -169,51 +169,26 @@ DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BIN
echo "Download URL: $DOWNLOAD_URL"
echo ""
# Check if installation directory exists
# Check if service is running and stop it
SERVICE_WAS_RUNNING=0
OLD_NODE_INSTALL=0
if [ -d "$INSTALL_DIR" ]; then
# Check if this is an old Node.js-based installation
if [ -f "$INSTALL_DIR/package.json" ] || [ -d "$INSTALL_DIR/node_modules" ]; then
OLD_NODE_INSTALL=1
echo "Detected old Node.js-based NUPST installation (v3.x or earlier)"
echo "This installer will migrate to the new Deno-based binary version (v4.0+)"
echo ""
if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
SERVICE_WAS_RUNNING=1
if systemctl is-active --quiet nupst 2>/dev/null; then
echo "Stopping NUPST service..."
systemctl stop nupst
fi
echo "Updating existing installation at $INSTALL_DIR..."
# Check if service exists (enabled or running) and stop it if active
if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
SERVICE_WAS_RUNNING=1
if systemctl is-active --quiet nupst 2>/dev/null; then
echo "Stopping NUPST service..."
systemctl stop nupst
else
echo "Service is installed but not currently running (will be updated)..."
fi
fi
# Clean up old Node.js installation files
if [ $OLD_NODE_INSTALL -eq 1 ]; then
echo "Cleaning up old Node.js installation files..."
rm -rf "$INSTALL_DIR/node_modules" 2>/dev/null || true
rm -rf "$INSTALL_DIR/vendor" 2>/dev/null || true
rm -rf "$INSTALL_DIR/dist_ts" 2>/dev/null || true
rm -f "$INSTALL_DIR/package.json" 2>/dev/null || true
rm -f "$INSTALL_DIR/package-lock.json" 2>/dev/null || true
rm -f "$INSTALL_DIR/pnpm-lock.yaml" 2>/dev/null || true
rm -f "$INSTALL_DIR/tsconfig.json" 2>/dev/null || true
rm -f "$INSTALL_DIR/setup.sh" 2>/dev/null || true
rm -rf "$INSTALL_DIR/bin" 2>/dev/null || true
echo "Old installation files removed."
fi
else
echo "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
fi
# Clean installation directory - ensure only binary exists
if [ -d "$INSTALL_DIR" ]; then
echo "Cleaning installation directory: $INSTALL_DIR"
rm -rf "$INSTALL_DIR"
fi
# Create fresh installation directory
echo "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# Download binary
echo "Downloading NUPST binary..."
TEMP_FILE="$INSTALL_DIR/nupst.download"
@@ -241,9 +216,20 @@ fi
BINARY_PATH="$INSTALL_DIR/nupst"
mv "$TEMP_FILE" "$BINARY_PATH"
if [ $? -ne 0 ] || [ ! -f "$BINARY_PATH" ]; then
echo "Error: Failed to move binary to $BINARY_PATH"
rm -f "$TEMP_FILE" 2>/dev/null
exit 1
fi
# Make executable
chmod +x "$BINARY_PATH"
if [ $? -ne 0 ]; then
echo "Error: Failed to make binary executable"
exit 1
fi
echo "Binary installed successfully to: $BINARY_PATH"
echo ""
@@ -260,14 +246,6 @@ echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH"
echo ""
# Update systemd service file if migrating from v3
if [ $SERVICE_WAS_RUNNING -eq 1 ] && [ $OLD_NODE_INSTALL -eq 1 ]; then
echo "Updating systemd service file for v4..."
$BINARY_PATH service enable > /dev/null 2>&1
echo "Service file updated."
echo ""
fi
# Restart service if it was running before update
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
echo "Restarting NUPST service..."
@@ -280,20 +258,6 @@ echo "================================================"
echo " NUPST Installation Complete!"
echo "================================================"
echo ""
if [ $OLD_NODE_INSTALL -eq 1 ]; then
echo "Migration from v3.x to v4.0 successful!"
echo ""
echo "What changed:"
echo " • Node.js runtime removed (now a self-contained binary)"
echo " • Faster startup and lower memory usage"
echo " • CLI commands now use subcommand structure"
echo " (old commands still work with deprecation warnings)"
echo ""
echo "See readme for migration details: https://code.foss.global/serve.zone/nupst#migration-from-v3x"
echo ""
fi
echo "Installation details:"
echo " Binary location: $BINARY_PATH"
echo " Symlink location: $BIN_DIR/nupst"

1
npmextra.json Normal file
View File

@@ -0,0 +1 @@
{}

64
package.json Normal file
View File

@@ -0,0 +1,64 @@
{
"name": "@serve.zone/nupst",
"version": "5.1.7",
"description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies",
"keywords": [
"ups",
"snmp",
"power",
"shutdown",
"monitoring",
"cyberpower",
"apc",
"eaton",
"tripplite",
"liebert",
"vertiv",
"battery",
"backup"
],
"homepage": "https://code.foss.global/serve.zone/nupst",
"bugs": {
"url": "https://code.foss.global/serve.zone/nupst/issues"
},
"repository": {
"type": "git",
"url": "git+https://code.foss.global/serve.zone/nupst.git"
},
"author": "Serve Zone",
"license": "MIT",
"type": "module",
"bin": {
"nupst": "./bin/nupst-wrapper.js"
},
"scripts": {
"postinstall": "node scripts/install-binary.js",
"prepublishOnly": "echo 'Publishing NUPST binaries to npm...'",
"test": "echo 'Tests are run with Deno: deno task test'",
"build": "echo 'no build needed'"
},
"files": [
"bin/",
"scripts/install-binary.js",
"readme.md",
"license",
"changelog.md"
],
"engines": {
"node": ">=14.0.0"
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
}

1168
readme.md

File diff suppressed because it is too large Load Diff

231
scripts/install-binary.js Normal file
View File

@@ -0,0 +1,231 @@
#!/usr/bin/env node
/**
* NUPST npm postinstall script
* Downloads the appropriate binary for the current platform from GitHub releases
*/
import { platform, arch } from 'os';
import { existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import https from 'https';
import { pipeline } from 'stream';
import { promisify } from 'util';
import { createWriteStream } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const streamPipeline = promisify(pipeline);
// Configuration
const REPO_BASE = 'https://code.foss.global/serve.zone/nupst';
const VERSION = process.env.npm_package_version || '5.0.5';
function getBinaryInfo() {
const plat = platform();
const architecture = arch();
const platformMap = {
'darwin': 'macos',
'linux': 'linux',
'win32': 'windows'
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64'
};
const mappedPlatform = platformMap[plat];
const mappedArch = archMap[architecture];
if (!mappedPlatform || !mappedArch) {
return { supported: false, platform: plat, arch: architecture };
}
let binaryName = `nupst-${mappedPlatform}-${mappedArch}`;
if (plat === 'win32') {
binaryName += '.exe';
}
return {
supported: true,
platform: mappedPlatform,
arch: mappedArch,
binaryName,
originalPlatform: plat
};
}
function downloadFile(url, destination) {
return new Promise((resolve, reject) => {
console.log(`Downloading from: ${url}`);
// Follow redirects
const download = (url, redirectCount = 0) => {
if (redirectCount > 5) {
reject(new Error('Too many redirects'));
return;
}
https.get(url, (response) => {
if (response.statusCode === 301 || response.statusCode === 302) {
console.log(`Following redirect to: ${response.headers.location}`);
download(response.headers.location, redirectCount + 1);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`));
return;
}
const totalSize = parseInt(response.headers['content-length'], 10);
let downloadedSize = 0;
let lastProgress = 0;
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const progress = Math.round((downloadedSize / totalSize) * 100);
// Only log every 10% to reduce noise
if (progress >= lastProgress + 10) {
console.log(`Download progress: ${progress}%`);
lastProgress = progress;
}
});
const file = createWriteStream(destination);
pipeline(response, file, (err) => {
if (err) {
reject(err);
} else {
console.log('Download complete!');
resolve();
}
});
}).on('error', reject);
};
download(url);
});
}
async function main() {
console.log('===========================================');
console.log(' NUPST - Binary Installation');
console.log('===========================================');
console.log('');
const binaryInfo = getBinaryInfo();
if (!binaryInfo.supported) {
console.error(`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`);
console.error('');
console.error('Supported platforms:');
console.error(' • Linux (x64, arm64)');
console.error(' • macOS (x64, arm64)');
console.error(' • Windows (x64)');
console.error('');
console.error('If you believe your platform should be supported, please file an issue:');
console.error(' https://code.foss.global/serve.zone/nupst/issues');
process.exit(1);
}
console.log(`Platform: ${binaryInfo.platform} (${binaryInfo.originalPlatform})`);
console.log(`Architecture: ${binaryInfo.arch}`);
console.log(`Binary: ${binaryInfo.binaryName}`);
console.log(`Version: ${VERSION}`);
console.log('');
// Create dist/binaries directory if it doesn't exist
const binariesDir = join(__dirname, '..', 'dist', 'binaries');
if (!existsSync(binariesDir)) {
console.log('Creating binaries directory...');
mkdirSync(binariesDir, { recursive: true });
}
const binaryPath = join(binariesDir, binaryInfo.binaryName);
// Check if binary already exists and skip download
if (existsSync(binaryPath)) {
console.log('✓ Binary already exists, skipping download');
} else {
// Construct download URL
// Try release URL first, fall back to raw branch if needed
const releaseUrl = `${REPO_BASE}/releases/download/v${VERSION}/${binaryInfo.binaryName}`;
const fallbackUrl = `${REPO_BASE}/raw/branch/main/dist/binaries/${binaryInfo.binaryName}`;
console.log('Downloading platform-specific binary...');
console.log('This may take a moment depending on your connection speed.');
console.log('');
try {
// Try downloading from release
await downloadFile(releaseUrl, binaryPath);
} catch (err) {
console.log(`Release download failed: ${err.message}`);
console.log('Trying fallback URL...');
try {
// Try fallback URL
await downloadFile(fallbackUrl, binaryPath);
} catch (fallbackErr) {
console.error(`❌ Error: Failed to download binary`);
console.error(` Primary URL: ${releaseUrl}`);
console.error(` Fallback URL: ${fallbackUrl}`);
console.error('');
console.error('This might be because:');
console.error('1. The release has not been created yet');
console.error('2. Network connectivity issues');
console.error('3. The version specified does not exist');
console.error('');
console.error('You can try:');
console.error('1. Installing from source: https://code.foss.global/serve.zone/nupst');
console.error('2. Downloading the binary manually from the releases page');
console.error('3. Using the install script: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash');
// Clean up partial download
if (existsSync(binaryPath)) {
unlinkSync(binaryPath);
}
process.exit(1);
}
}
console.log(`✓ Binary downloaded successfully`);
}
// On Unix-like systems, ensure the binary is executable
if (binaryInfo.originalPlatform !== 'win32') {
try {
console.log('Setting executable permissions...');
chmodSync(binaryPath, 0o755);
console.log('✓ Binary permissions updated');
} catch (err) {
console.error(`⚠️ Warning: Could not set executable permissions: ${err.message}`);
console.error(' You may need to manually run:');
console.error(` chmod +x ${binaryPath}`);
}
}
console.log('');
console.log('✅ NUPST installation completed successfully!');
console.log('');
console.log('You can now use NUPST by running:');
console.log(' nupst --help');
console.log('');
console.log('For initial setup, run:');
console.log(' sudo nupst ups add');
console.log('');
console.log('===========================================');
}
// Run the installation
main().catch(err => {
console.error(`❌ Installation failed: ${err.message}`);
process.exit(1);
});

BIN
serve.zone-nupst-5.0.5.tgz Normal file

Binary file not shown.

View File

@@ -1,10 +1,8 @@
/**
* commitinfo - reads version from deno.json
* autocreated commitinfo by @push.rocks/commitinfo
*/
import denoConfig from '../deno.json' with { type: 'json' };
export const commitinfo = {
name: denoConfig.name,
version: denoConfig.version,
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)',
};
name: '@serve.zone/nupst',
version: '5.1.2',
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
}

View File

@@ -1,5 +1,6 @@
import * as path from 'node:path';
import * as fs from 'node:fs';
import process from 'node:process';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';

141
ts/cli.ts
View File

@@ -127,8 +127,7 @@ export class NupstCli {
break;
}
case 'remove':
case 'rm': // Alias
case 'delete': { // Backward compatibility
case 'rm': {
const upsIdToRemove = subcommandArgs[0];
if (!upsIdToRemove) {
logger.error('UPS ID is required for remove command');
@@ -172,8 +171,7 @@ export class NupstCli {
break;
}
case 'remove':
case 'rm': // Alias
case 'delete': { // Backward compatibility
case 'rm': {
const groupIdToRemove = subcommandArgs[0];
if (!groupIdToRemove) {
logger.error('Group ID is required for remove command');
@@ -206,8 +204,7 @@ export class NupstCli {
break;
}
case 'remove':
case 'rm': // Alias
case 'delete': { // Backward compatibility
case 'rm': {
const upsId = subcommandArgs[0];
const actionIndex = subcommandArgs[1];
await actionHandler.remove(upsId, actionIndex);
@@ -226,6 +223,24 @@ export class NupstCli {
return;
}
// Handle feature subcommands
if (command === 'feature') {
const subcommand = commandArgs[0];
const featureHandler = this.nupst.getFeatureHandler();
switch (subcommand) {
case 'httpServer':
case 'http-server':
case 'http':
await featureHandler.configureHttpServer();
break;
default:
this.showFeatureHelp();
break;
}
return;
}
// Handle config subcommand
if (command === 'config') {
const subcommand = commandArgs[0] || 'show';
@@ -242,72 +257,8 @@ export class NupstCli {
return;
}
// Handle top-level commands and backward compatibility
// Handle top-level commands
switch (command) {
// Backward compatibility - old UPS commands
case 'add':
logger.log("Note: 'nupst add' is deprecated. Use 'nupst ups add' instead.");
await upsHandler.add();
break;
case 'edit':
logger.log("Note: 'nupst edit' is deprecated. Use 'nupst ups edit' instead.");
await upsHandler.edit(commandArgs[0]);
break;
case 'delete':
logger.log("Note: 'nupst delete' is deprecated. Use 'nupst ups remove' instead.");
if (!commandArgs[0]) {
logger.error('UPS ID is required for delete command');
this.showHelp();
return;
}
await upsHandler.remove(commandArgs[0]);
break;
case 'list':
logger.log("Note: 'nupst list' is deprecated. Use 'nupst ups list' instead.");
await upsHandler.list();
break;
case 'test':
logger.log("Note: 'nupst test' is deprecated. Use 'nupst ups test' instead.");
await upsHandler.test(debugMode);
break;
case 'setup':
logger.log("Note: 'nupst setup' is deprecated. Use 'nupst ups edit' instead.");
await upsHandler.edit(undefined);
break;
// Backward compatibility - old service commands
case 'enable':
logger.log("Note: 'nupst enable' is deprecated. Use 'nupst service enable' instead.");
await serviceHandler.enable();
break;
case 'disable':
logger.log("Note: 'nupst disable' is deprecated. Use 'nupst service disable' instead.");
await serviceHandler.disable();
break;
case 'start':
logger.log("Note: 'nupst start' is deprecated. Use 'nupst service start' instead.");
await serviceHandler.start();
break;
case 'stop':
logger.log("Note: 'nupst stop' is deprecated. Use 'nupst service stop' instead.");
await serviceHandler.stop();
break;
case 'status':
logger.log("Note: 'nupst status' is deprecated. Use 'nupst service status' instead.");
await serviceHandler.status();
break;
case 'logs':
logger.log("Note: 'nupst logs' is deprecated. Use 'nupst service logs' instead.");
await serviceHandler.logs();
break;
case 'daemon-start':
logger.log(
"Note: 'nupst daemon-start' is deprecated. Use 'nupst service start-daemon' instead.",
);
await serviceHandler.daemonStart(debugMode);
break;
// Top-level commands (no changes)
case 'update':
await serviceHandler.update();
break;
@@ -361,6 +312,26 @@ export class NupstCli {
` ${theme.path('/etc/nupst/config.json')}`,
], 60, 'info');
// HTTP Server Status (if configured)
if (config.httpServer) {
const serverStatus = config.httpServer.enabled
? theme.success('Enabled')
: theme.dim('Disabled');
logger.log('');
logger.logBox('HTTP Server', [
`Status: ${serverStatus}`,
...(config.httpServer.enabled ? [
`Port: ${theme.highlight(String(config.httpServer.port))}`,
`Path: ${theme.highlight(config.httpServer.path)}`,
`Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`,
'',
theme.dim('Usage:'),
` curl -H "Authorization: Bearer TOKEN" http://localhost:${config.httpServer.port}${config.httpServer.path}`,
] : []),
], 70, config.httpServer.enabled ? 'success' : 'default');
}
// UPS Devices Table
if (config.upsDevices.length > 0) {
const upsRows = config.upsDevices.map((ups) => ({
@@ -533,6 +504,7 @@ export class NupstCli {
this.printCommand('ups <subcommand>', 'Manage UPS devices');
this.printCommand('group <subcommand>', 'Manage UPS groups');
this.printCommand('action <subcommand>', 'Manage UPS actions');
this.printCommand('feature <subcommand>', 'Manage optional features');
this.printCommand('config [show]', 'Display current configuration');
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
@@ -576,6 +548,11 @@ export class NupstCli {
this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)');
console.log('');
// Feature subcommands
logger.log(theme.info('Feature Subcommands:'));
this.printCommand('nupst feature httpServer', 'Configure HTTP server for JSON status export');
console.log('');
// Options
logger.log(theme.info('Options:'));
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
@@ -589,11 +566,6 @@ export class NupstCli {
logger.dim(' nupst group list # Show all configured groups');
logger.dim(' nupst config # Display current configuration');
console.log('');
// Note about deprecated commands
logger.warn('Note: Old command format (e.g., \'nupst add\') still works but is deprecated.');
logger.dim(' Use the new format (e.g., \'nupst ups add\') going forward.');
console.log('');
}
/**
@@ -692,7 +664,7 @@ Usage:
Subcommands:
add <ups-id|group-id> - Add a new action to a UPS or group interactively
remove <ups-id|group-id> <index> - Remove an action by index (alias: rm, delete)
remove <ups-id|group-id> <index> - Remove an action by index (alias: rm)
list [ups-id|group-id] - List all actions (optionally for specific target) (alias: ls)
Options:
@@ -704,6 +676,21 @@ Examples:
nupst action add default - Add a new action to UPS or group 'default'
nupst action remove default 0 - Remove action at index 0 from UPS or group 'default'
nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1'
`);
}
private showFeatureHelp(): void {
logger.log(`
NUPST - Feature Management Commands
Usage:
nupst feature <subcommand>
Subcommands:
httpServer - Configure HTTP server for JSON status export
Examples:
nupst feature httpServer - Enable/disable HTTP server with interactive setup
`);
}
}

213
ts/cli/feature-handler.ts Normal file
View File

@@ -0,0 +1,213 @@
import process from 'node:process';
import { execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts';
/**
* Class for handling feature-related CLI commands
* Provides interface for managing optional features like HTTP server
*/
export class FeatureHandler {
private readonly nupst: Nupst;
/**
* Create a new feature handler
* @param nupst Reference to the main Nupst instance
*/
constructor(nupst: Nupst) {
this.nupst = nupst;
}
/**
* Configure HTTP server feature
*/
public async configureHttpServer(): Promise<void> {
try {
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
try {
await this.runHttpServerConfig(prompt);
} finally {
rl.close();
process.stdin.destroy();
}
} catch (error) {
logger.error(`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Run the interactive HTTP server configuration process
* @param prompt Function to prompt for user input
*/
private async runHttpServerConfig(prompt: (question: string) => Promise<string>): Promise<void> {
logger.log('');
logger.logBoxTitle('HTTP Server Feature Configuration', 60);
logger.logBoxLine('Configure the HTTP server to expose UPS status as JSON');
logger.logBoxEnd();
logger.log('');
// Load config
let config;
try {
await this.nupst.getDaemon().loadConfig();
config = this.nupst.getDaemon().getConfig();
} catch (error) {
logger.error('No configuration found. Please run "nupst ups add" first.');
return;
}
// Show current status
if (config.httpServer?.enabled) {
logger.info('HTTP Server is currently: ' + theme.success('ENABLED'));
logger.log(` Port: ${theme.highlight(String(config.httpServer.port))}`);
logger.log(` Path: ${theme.highlight(config.httpServer.path)}`);
logger.log(` Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`);
logger.log('');
} else {
logger.info('HTTP Server is currently: ' + theme.dim('DISABLED'));
logger.log('');
}
// Ask enable/disable
const action = await prompt('Enable or disable HTTP server? (enable/disable/cancel): ');
if (action.toLowerCase() === 'cancel' || action.toLowerCase() === 'c') {
logger.log('Cancelled.');
return;
}
if (action.toLowerCase() === 'disable' || action.toLowerCase() === 'd') {
// Disable HTTP server
config.httpServer = {
enabled: false,
port: config.httpServer?.port || 8080,
path: config.httpServer?.path || '/ups-status',
authToken: config.httpServer?.authToken || '',
};
this.nupst.getDaemon().saveConfig(config);
logger.log('');
logger.success('HTTP Server disabled');
logger.log('');
await this.restartServiceIfRunning();
return;
}
if (action.toLowerCase() !== 'enable' && action.toLowerCase() !== 'e') {
logger.error('Invalid option. Please enter "enable", "disable", or "cancel".');
return;
}
// Enable - gather configuration
logger.log('');
const portInput = await prompt(`HTTP Server Port [${config.httpServer?.port || 8080}]: `);
const port = portInput ? parseInt(portInput, 10) : (config.httpServer?.port || 8080);
if (isNaN(port) || port < 1 || port > 65535) {
logger.error('Invalid port number. Must be between 1 and 65535.');
return;
}
const pathInput = await prompt(`URL Path [${config.httpServer?.path || '/ups-status'}]: `);
const path = pathInput || config.httpServer?.path || '/ups-status';
// Ensure path starts with /
const finalPath = path.startsWith('/') ? path : `/${path}`;
// Generate or reuse auth token
let authToken = config.httpServer?.authToken;
if (!authToken) {
// Generate new random token
authToken = helpers.shortId() + helpers.shortId() + helpers.shortId();
logger.log('');
logger.info('Generated new authentication token');
} else {
const regenerate = await prompt('Regenerate authentication token? (y/N): ');
if (regenerate.toLowerCase() === 'y' || regenerate.toLowerCase() === 'yes') {
authToken = helpers.shortId() + helpers.shortId() + helpers.shortId();
logger.info('Generated new authentication token');
}
}
// Save configuration
config.httpServer = {
enabled: true,
port,
path: finalPath,
authToken,
};
this.nupst.getDaemon().saveConfig(config);
// Display summary
logger.log('');
logger.logBoxTitle('HTTP Server Configuration', 70, 'success');
logger.logBoxLine(`Status: ${theme.success('ENABLED')}`);
logger.logBoxLine(`Port: ${theme.highlight(String(port))}`);
logger.logBoxLine(`Path: ${theme.highlight(finalPath)}`);
logger.logBoxLine(`Auth Token: ${theme.warning(authToken)}`);
logger.logBoxLine('');
logger.logBoxLine(theme.dim('Usage examples:'));
logger.logBoxLine(` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`);
logger.logBoxLine(` curl "http://localhost:${port}${finalPath}?token=${authToken}"`);
logger.logBoxEnd();
logger.log('');
logger.warn('IMPORTANT: Save the authentication token securely!');
logger.log('');
await this.restartServiceIfRunning();
}
/**
* Restart the service if it's currently running
*/
private async restartServiceIfRunning(): Promise<void> {
try {
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isActive) {
logger.log('');
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question('Service is running. Restart to apply changes? (Y/n): ', resolve);
});
rl.close();
if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
logger.info('Restarting service...');
execSync('sudo systemctl restart nupst.service');
logger.success('Service restarted successfully');
} else {
logger.warn('Changes will take effect on next service restart');
}
}
} catch (error) {
// Ignore errors - service might not be installed
}
}
}

View File

@@ -10,6 +10,7 @@ import { MigrationRunner } from './migrations/index.ts';
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
import type { IActionConfig } from './actions/base-action.ts';
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
import { NupstHttpServer } from './http-server.ts';
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
@@ -46,6 +47,20 @@ export interface IGroupConfig {
actions?: IActionConfig[];
}
/**
* HTTP Server configuration interface
*/
export interface IHttpServerConfig {
/** Whether HTTP server is enabled */
enabled: boolean;
/** Port to listen on */
port: number;
/** URL path for the endpoint */
path: string;
/** Authentication token */
authToken: string;
}
/**
* Configuration interface for the daemon
*/
@@ -58,6 +73,8 @@ export interface INupstConfig {
groups: IGroupConfig[];
/** Check interval in milliseconds */
checkInterval: number;
/** HTTP Server configuration */
httpServer?: IHttpServerConfig;
// Legacy fields for backward compatibility (will be migrated away)
/** UPS list (v3 format - legacy) */
@@ -82,6 +99,10 @@ export interface IUpsStatus {
powerStatus: 'online' | 'onBattery' | 'unknown';
batteryCapacity: number;
batteryRuntime: number;
outputLoad: number; // Load percentage (0-100%)
outputPower: number; // Power in watts
outputVoltage: number; // Voltage in volts
outputCurrent: number; // Current in amps
lastStatusChange: number;
lastCheckTime: number;
}
@@ -139,6 +160,7 @@ export class NupstDaemon {
private snmp: NupstSnmp;
private isRunning: boolean = false;
private upsStatus: Map<string, IUpsStatus> = new Map();
private httpServer?: NupstHttpServer;
/**
* Create a new daemon instance with the given SNMP manager
@@ -278,6 +300,21 @@ export class NupstDaemon {
// Initialize UPS status tracking
this.initializeUpsStatus();
// Start HTTP server if configured
if (this.config.httpServer?.enabled && this.config.httpServer.authToken) {
try {
this.httpServer = new NupstHttpServer(
this.config.httpServer.port,
this.config.httpServer.path,
this.config.httpServer.authToken,
() => this.upsStatus
);
this.httpServer.start();
} catch (error) {
logger.error(`Failed to start HTTP server: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Start UPS monitoring
this.isRunning = true;
await this.monitor();
@@ -304,6 +341,10 @@ export class NupstDaemon {
powerStatus: 'unknown',
batteryCapacity: 100,
batteryRuntime: 999, // High value as default
outputLoad: 0,
outputPower: 0,
outputVoltage: 0,
outputCurrent: 0,
lastStatusChange: Date.now(),
lastCheckTime: 0,
});
@@ -377,6 +418,12 @@ export class NupstDaemon {
*/
public stop(): void {
logger.log('Stopping NUPST daemon...');
// Stop HTTP server if running
if (this.httpServer) {
this.httpServer.stop();
}
this.isRunning = false;
}
@@ -437,6 +484,10 @@ export class NupstDaemon {
powerStatus: 'unknown',
batteryCapacity: 100,
batteryRuntime: 999,
outputLoad: 0,
outputPower: 0,
outputVoltage: 0,
outputCurrent: 0,
lastStatusChange: Date.now(),
lastCheckTime: 0,
});
@@ -456,6 +507,10 @@ export class NupstDaemon {
powerStatus: status.powerStatus,
batteryCapacity: status.batteryCapacity,
batteryRuntime: status.batteryRuntime,
outputLoad: status.outputLoad,
outputPower: status.outputPower,
outputVoltage: status.outputVoltage,
outputCurrent: status.outputCurrent,
lastCheckTime: currentTime,
lastStatusChange: currentStatus?.lastStatusChange || currentTime,
};

113
ts/http-server.ts Normal file
View File

@@ -0,0 +1,113 @@
import * as http from 'node:http';
import { URL } from 'node:url';
import { logger } from './logger.ts';
import type { IUpsStatus } from './daemon.ts';
/**
* HTTP Server for exposing UPS status as JSON
* Serves cached data from the daemon's monitoring loop
*/
export class NupstHttpServer {
private server?: http.Server;
private port: number;
private path: string;
private authToken: string;
private getUpsStatus: () => Map<string, IUpsStatus>;
/**
* Create a new HTTP server instance
* @param port Port to listen on
* @param path URL path for the endpoint
* @param authToken Authentication token required for access
* @param getUpsStatus Function to retrieve cached UPS status
*/
constructor(
port: number,
path: string,
authToken: string,
getUpsStatus: () => Map<string, IUpsStatus>
) {
this.port = port;
this.path = path;
this.authToken = authToken;
this.getUpsStatus = getUpsStatus;
}
/**
* Verify authentication token from request
* Supports both Bearer token in Authorization header and token query parameter
* @param req HTTP request
* @returns True if authenticated, false otherwise
*/
private isAuthenticated(req: http.IncomingMessage): boolean {
// Check Authorization header (Bearer token)
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.substring(7);
return token === this.authToken;
}
// Check token query parameter
if (req.url) {
const url = new URL(req.url, `http://localhost:${this.port}`);
const tokenParam = url.searchParams.get('token');
return tokenParam === this.authToken;
}
return false;
}
/**
* Start the HTTP server
*/
public start(): void {
this.server = http.createServer((req, res) => {
// Parse URL
const reqUrl = new URL(req.url || '/', `http://localhost:${this.port}`);
if (reqUrl.pathname === this.path && req.method === 'GET') {
// Check authentication
if (!this.isAuthenticated(req)) {
res.writeHead(401, {
'Content-Type': 'application/json',
'WWW-Authenticate': 'Bearer'
});
res.end(JSON.stringify({ error: 'Unauthorized' }));
return;
}
// Get cached status (no refresh)
const statusMap = this.getUpsStatus();
const statusArray = Array.from(statusMap.values());
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
});
res.end(JSON.stringify(statusArray, null, 2));
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
}
});
this.server.listen(this.port, () => {
logger.success(`HTTP server started on port ${this.port} at ${this.path}`);
});
this.server.on('error', (error: any) => {
logger.error(`HTTP server error: ${error.message}`);
});
}
/**
* Stop the HTTP server
*/
public stop(): void {
if (this.server) {
this.server.close(() => {
logger.log('HTTP server stopped');
});
}
}
}

View File

@@ -7,6 +7,7 @@ import { UpsHandler } from './cli/ups-handler.ts';
import { GroupHandler } from './cli/group-handler.ts';
import { ServiceHandler } from './cli/service-handler.ts';
import { ActionHandler } from './cli/action-handler.ts';
import { FeatureHandler } from './cli/feature-handler.ts';
import * as https from 'node:https';
/**
@@ -21,6 +22,7 @@ export class Nupst {
private readonly groupHandler: GroupHandler;
private readonly serviceHandler: ServiceHandler;
private readonly actionHandler: ActionHandler;
private readonly featureHandler: FeatureHandler;
private updateAvailable: boolean = false;
private latestVersion: string = '';
@@ -39,6 +41,7 @@ export class Nupst {
this.groupHandler = new GroupHandler(this);
this.serviceHandler = new ServiceHandler(this);
this.actionHandler = new ActionHandler(this);
this.featureHandler = new FeatureHandler(this);
}
/**
@@ -90,6 +93,13 @@ export class Nupst {
return this.actionHandler;
}
/**
* Get the Feature handler for feature management
*/
public getFeatureHandler(): FeatureHandler {
return this.featureHandler;
}
/**
* Get the current version of NUPST
* @returns The current version string

View File

@@ -1,4 +1,4 @@
import * as snmp from 'npm:net-snmp@3.20.0';
import * as snmp from 'npm:net-snmp@3.26.0';
import { Buffer } from 'node:buffer';
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
import { UpsOidSets } from './oid-sets.ts';
@@ -304,6 +304,10 @@ export class NupstSnmp {
console.log(' Power Status:', this.activeOIDs.POWER_STATUS);
console.log(' Battery Capacity:', this.activeOIDs.BATTERY_CAPACITY);
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
console.log(' Output Load:', this.activeOIDs.OUTPUT_LOAD);
console.log(' Output Power:', this.activeOIDs.OUTPUT_POWER);
console.log(' Output Voltage:', this.activeOIDs.OUTPUT_VOLTAGE);
console.log(' Output Current:', this.activeOIDs.OUTPUT_CURRENT);
console.log('---------------------------------------');
}
@@ -324,20 +328,65 @@ export class NupstSnmp {
config,
) || 0;
// Get power draw metrics
const outputLoad = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_LOAD,
'output load',
config,
) || 0;
const outputPower = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_POWER,
'output power',
config,
) || 0;
const outputVoltage = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_VOLTAGE,
'output voltage',
config,
) || 0;
const outputCurrent = await this.getSNMPValueWithRetry(
this.activeOIDs.OUTPUT_CURRENT,
'output current',
config,
) || 0;
// Determine power status - handle different values for different UPS models
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
// Convert to minutes for UPS models with different time units
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
// Process power metrics with vendor-specific scaling
const processedVoltage = this.processVoltageValue(config.upsModel, outputVoltage);
const processedCurrent = this.processCurrentValue(config.upsModel, outputCurrent);
// Calculate power from voltage × current if not provided by UPS
let processedPower = outputPower;
if (outputPower === 0 && processedVoltage > 0 && processedCurrent > 0) {
processedPower = Math.round(processedVoltage * processedCurrent);
if (this.debug) {
console.log(
`Calculated power from V×I: ${processedVoltage}V × ${processedCurrent}A = ${processedPower}W`,
);
}
}
const result = {
powerStatus,
batteryCapacity,
batteryRuntime: processedRuntime,
outputLoad,
outputPower: processedPower,
outputVoltage: processedVoltage,
outputCurrent: processedCurrent,
raw: {
powerStatus: powerStatusValue,
batteryCapacity,
batteryRuntime,
outputLoad,
outputPower,
outputVoltage,
outputCurrent,
},
};
@@ -347,6 +396,10 @@ export class NupstSnmp {
console.log(' Power Status:', result.powerStatus);
console.log(' Battery Capacity:', result.batteryCapacity + '%');
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
console.log(' Output Load:', result.outputLoad + '%');
console.log(' Output Power:', result.outputPower, 'watts');
console.log(' Output Voltage:', result.outputVoltage, 'volts');
console.log(' Output Current:', result.outputCurrent, 'amps');
console.log('---------------------------------------');
}
@@ -602,4 +655,74 @@ export class NupstSnmp {
return batteryRuntime;
}
/**
* Process voltage value based on UPS model
* @param upsModel UPS model
* @param outputVoltage Raw output voltage value
* @returns Processed voltage in volts
*/
private processVoltageValue(
upsModel: TUpsModel | undefined,
outputVoltage: number,
): number {
if (this.debug) {
console.log('Raw voltage value:', outputVoltage);
}
if (upsModel === 'cyberpower' && outputVoltage > 0) {
// CyberPower: Voltage is in 0.1V, convert to volts
const volts = outputVoltage / 10;
if (this.debug) {
console.log(
`Converting CyberPower voltage from ${outputVoltage} (0.1V) to ${volts} volts`,
);
}
return volts;
}
return outputVoltage;
}
/**
* Process current value based on UPS model
* @param upsModel UPS model
* @param outputCurrent Raw output current value
* @returns Processed current in amps
*/
private processCurrentValue(
upsModel: TUpsModel | undefined,
outputCurrent: number,
): number {
if (this.debug) {
console.log('Raw current value:', outputCurrent);
}
if (upsModel === 'cyberpower' && outputCurrent > 0) {
// CyberPower: Current is in 0.1A, convert to amps
const amps = outputCurrent / 10;
if (this.debug) {
console.log(
`Converting CyberPower current from ${outputCurrent} (0.1A) to ${amps} amps`,
);
}
return amps;
} else if ((upsModel === 'tripplite' || upsModel === 'liebert') && outputCurrent > 0) {
// RFC 1628 standard: Current is in 0.1A, convert to amps
const amps = outputCurrent / 10;
if (this.debug) {
console.log(
`Converting RFC 1628 current from ${outputCurrent} (0.1A) to ${amps} amps`,
);
}
return amps;
}
// Eaton XUPS-MIB and APC PowerNet report current directly in RMS Amps (no scaling needed)
if ((upsModel === 'eaton' || upsModel === 'apc') && this.debug && outputCurrent > 0) {
console.log(`${upsModel.toUpperCase()} current already in RMS Amps: ${outputCurrent}A`);
}
return outputCurrent;
}
}

View File

@@ -14,28 +14,40 @@ export class UpsOidSets {
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
OUTPUT_LOAD: '1.3.6.1.4.1.3808.1.1.1.4.2.3.0', // upsAdvanceOutputLoad (percentage)
OUTPUT_POWER: '1.3.6.1.4.1.3808.1.1.1.4.2.5.0', // upsAdvanceOutputPower (watts)
OUTPUT_VOLTAGE: '1.3.6.1.4.1.3808.1.1.1.4.2.1.0', // upsAdvanceOutputVoltage (0.1V scale)
OUTPUT_CURRENT: '1.3.6.1.4.1.3808.1.1.1.4.2.4.0', // upsAdvanceOutputCurrent (0.1A scale)
POWER_STATUS_VALUES: {
online: 2, // upsBaseOutputStatus: 2=onLine
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
},
},
// APC OIDs
// APC OIDs (PowerNet MIB)
apc: {
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
OUTPUT_LOAD: '1.3.6.1.4.1.318.1.1.1.4.2.3.0', // upsAdvOutputLoad (percentage)
OUTPUT_POWER: '1.3.6.1.4.1.318.1.1.1.4.2.8.0', // upsAdvOutputActivePower (watts)
OUTPUT_VOLTAGE: '1.3.6.1.4.1.318.1.1.1.4.2.1.0', // upsAdvOutputVoltage
OUTPUT_CURRENT: '1.3.6.1.4.1.318.1.1.1.4.2.4.0', // upsAdvOutputCurrent
POWER_STATUS_VALUES: {
online: 2, // upsBasicOutputStatus: 2=onLine
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
},
},
// Eaton OIDs
// Eaton OIDs (XUPS-MIB)
eaton: {
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
OUTPUT_LOAD: '1.3.6.1.4.1.534.1.4.4.1.8.1', // xupsOutputPercentLoad (phase 1)
OUTPUT_POWER: '1.3.6.1.4.1.534.1.4.4.1.4.1', // xupsOutputWatts (phase 1)
OUTPUT_VOLTAGE: '1.3.6.1.4.1.534.1.4.4.1.2.1', // xupsOutputVoltage (phase 1)
OUTPUT_CURRENT: '1.3.6.1.4.1.534.1.4.4.1.3.1', // xupsOutputCurrent (phase 1)
POWER_STATUS_VALUES: {
online: 3, // xupsOutputSource: 3=normal (mains power)
onBattery: 5, // xupsOutputSource: 5=battery
@@ -47,6 +59,10 @@ export class UpsOidSets {
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
OUTPUT_LOAD: '1.3.6.1.2.1.33.1.4.4.1.5.1', // RFC 1628: upsOutputPercentLoad
OUTPUT_POWER: '1.3.6.1.2.1.33.1.4.4.1.4.1', // RFC 1628: upsOutputPower (watts)
OUTPUT_VOLTAGE: '1.3.6.1.2.1.33.1.4.4.1.2.1', // RFC 1628: upsOutputVoltage
OUTPUT_CURRENT: '1.3.6.1.2.1.33.1.4.4.1.3.1', // RFC 1628: upsOutputCurrent (0.1A scale)
POWER_STATUS_VALUES: {
online: 2, // tlUpsOutputSource: 2=normal (mains power)
onBattery: 3, // tlUpsOutputSource: 3=onBattery
@@ -58,6 +74,10 @@ export class UpsOidSets {
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
OUTPUT_LOAD: '1.3.6.1.2.1.33.1.4.4.1.5.1', // RFC 1628: upsOutputPercentLoad
OUTPUT_POWER: '1.3.6.1.2.1.33.1.4.4.1.4.1', // RFC 1628: upsOutputPower (watts)
OUTPUT_VOLTAGE: '1.3.6.1.2.1.33.1.4.4.1.2.1', // RFC 1628: upsOutputVoltage
OUTPUT_CURRENT: '1.3.6.1.2.1.33.1.4.4.1.3.1', // RFC 1628: upsOutputCurrent (0.1A scale)
POWER_STATUS_VALUES: {
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
@@ -69,6 +89,10 @@ export class UpsOidSets {
POWER_STATUS: '',
BATTERY_CAPACITY: '',
BATTERY_RUNTIME: '',
OUTPUT_LOAD: '',
OUTPUT_POWER: '',
OUTPUT_VOLTAGE: '',
OUTPUT_CURRENT: '',
},
};
@@ -90,6 +114,10 @@ export class UpsOidSets {
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
'output load': '1.3.6.1.2.1.33.1.4.4.1.5.1', // upsOutputPercentLoad (indexed by line)
'output power': '1.3.6.1.2.1.33.1.4.4.1.4.1', // upsOutputPower in watts (indexed by line)
'output voltage': '1.3.6.1.2.1.33.1.4.4.1.2.1', // upsOutputVoltage (indexed by line)
'output current': '1.3.6.1.2.1.33.1.4.4.1.3.1', // upsOutputCurrent in 0.1A (indexed by line)
};
}
}

View File

@@ -14,6 +14,14 @@ export interface IUpsStatus {
batteryCapacity: number;
/** Remaining runtime in minutes */
batteryRuntime: number;
/** Output load percentage (0-100) */
outputLoad: number;
/** Output power in watts */
outputPower: number;
/** Output voltage in volts */
outputVoltage: number;
/** Output current in amps */
outputCurrent: number;
/** Raw values from SNMP responses */
raw: Record<string, any>;
}
@@ -28,6 +36,14 @@ export interface IOidSet {
BATTERY_CAPACITY: string;
/** OID for battery runtime */
BATTERY_RUNTIME: string;
/** OID for output load percentage */
OUTPUT_LOAD: string;
/** OID for output power in watts */
OUTPUT_POWER: string;
/** OID for output voltage */
OUTPUT_VOLTAGE: string;
/** OID for output current */
OUTPUT_CURRENT: string;
/** Power status value mappings */
POWER_STATUS_VALUES?: {
/** SNMP value that indicates UPS is online (on AC power) */

View File

@@ -277,6 +277,9 @@ WantedBy=multi-user.target
for (const ups of config.upsDevices) {
await this.displaySingleUpsStatus(ups, snmp);
}
// Display groups after UPS devices
this.displayGroupsStatus();
} else if (config.snmp) {
// Legacy single UPS configuration (v1/v2 format)
logger.info('UPS Devices (1):');
@@ -352,6 +355,9 @@ WantedBy=multi-user.target
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
// Display power metrics
logger.log(` Load: ${theme.highlight(status.outputLoad + '%')} Power: ${theme.highlight(status.outputPower + 'W')} Voltage: ${theme.highlight(status.outputVoltage + 'V')} Current: ${theme.highlight(status.outputCurrent + 'A')}`);
// Display host info
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
@@ -365,6 +371,27 @@ WantedBy=multi-user.target
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
}
// Display actions if any
if (ups.actions && ups.actions.length > 0) {
for (const action of ups.actions) {
let actionDesc = `${action.type}`;
if (action.thresholds) {
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
} else {
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
}
logger.log(` ${theme.dim('Action:')} ${theme.info(actionDesc)}`);
}
}
logger.log('');
} catch (error) {
@@ -376,6 +403,69 @@ WantedBy=multi-user.target
}
}
/**
* Display status of all groups
* @private
*/
private displayGroupsStatus(): void {
const config = this.daemon.getConfig();
if (!config.groups || config.groups.length === 0) {
return; // No groups to display
}
logger.log('');
logger.info(`Groups (${config.groups.length}):`);
for (const group of config.groups) {
// Display group name and mode
const modeColor = group.mode === 'redundant' ? theme.success : theme.warning;
logger.log(
` ${symbols.info} ${theme.highlight(group.name)} ${theme.dim(`(${modeColor(group.mode)})`)}`,
);
// Display description if present
if (group.description) {
logger.log(` ${theme.dim(group.description)}`);
}
// Display UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(group.id)
);
if (upsInGroup.length > 0) {
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
logger.log(` ${theme.dim(`UPS Devices (${upsInGroup.length}):`)} ${upsNames}`);
} else {
logger.log(` ${theme.dim('UPS Devices: None')}`);
}
// Display actions if any
if (group.actions && group.actions.length > 0) {
for (const action of group.actions) {
let actionDesc = `${action.type}`;
if (action.thresholds) {
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
} else {
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
}
logger.log(` ${theme.dim('Action:')} ${theme.info(actionDesc)}`);
}
}
logger.log('');
}
}
/**
* Disable and uninstall the systemd service
* @throws Error if disabling fails