Compare commits

..

14 Commits

Author SHA1 Message Date
6481572981 fix(version): read current version from deno.json and latest version from Gitea API
Some checks failed
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Publish to npm / npm-publish (push) Failing after 27s
Release / build-and-release (push) Successful in 54s
CI / Build All Platforms (push) Successful in 1m8s
- Replace static commitinfo with dynamic deno.json import
- Change version check from npm registry to Gitea releases API
- Delete obsolete ts/00_commitinfo_data.ts
- Ensures version consistency across updates
2025-10-23 16:17:30 +00:00
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
20 changed files with 1059 additions and 287 deletions

View File

@@ -0,0 +1,31 @@
## NUPST {{VERSION}}
Pre-compiled binaries for multiple platforms.
### Installation
#### Option 1: Via npm (recommended)
```bash
npm install -g @serve.zone/nupst
```
#### Option 2: Via installer script
```bash
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
```
#### Option 3: Direct binary download
Download the appropriate binary for your platform from the assets below and make it executable.
### Supported Platforms
- Linux x86_64 (x64)
- Linux ARM64 (aarch64)
- macOS x86_64 (Intel)
- macOS ARM64 (Apple Silicon)
- Windows x86_64
### Checksums
SHA256 checksums are provided in `SHA256SUMS.txt` for binary verification.
### npm Package
The npm package includes automatic binary detection and installation for your platform.

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

View File

@@ -1,183 +0,0 @@
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"

View File

@@ -1,5 +1,19 @@
# 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

View File

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

View File

@@ -249,7 +249,7 @@ echo ""
# Restart service if it was running before update
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
echo "Restarting NUPST service..."
systemctl start nupst
systemctl restart nupst
echo "Service restarted successfully."
echo ""
fi

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/nupst",
"version": "5.1.0",
"version": "5.1.8",
"description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies",
"keywords": [
"ups",
@@ -34,7 +34,8 @@
"scripts": {
"postinstall": "node scripts/install-binary.js",
"prepublishOnly": "echo 'Publishing NUPST binaries to npm...'",
"test": "echo 'Tests are run with Deno: deno task test'"
"test": "echo 'Tests are run with Deno: deno task test'",
"build": "echo 'no build needed'"
},
"files": [
"bin/",
@@ -58,5 +59,6 @@
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
}

316
readme.md
View File

@@ -1,8 +1,12 @@
# ⚡ NUPST - Network UPS Shutdown Tool
**Keep your systems safe when the power goes out.** NUPST is a lightweight, battle-tested command-line tool that monitors SNMP-enabled UPS devices and orchestrates graceful system shutdowns during power emergencies. Distributed as self-contained binaries with zero runtime dependencies for maximum reliability.
**Keep your systems safe when the power goes out.** NUPST is a lightweight, battle-tested
command-line tool that monitors SNMP-enabled UPS devices and orchestrates graceful system shutdowns
during power emergencies. Distributed as self-contained binaries with zero runtime dependencies for
maximum reliability.
**Version 5.0+** is powered by Deno and distributed as single pre-compiled binaries—no installation, no setup, just run.
**Version 5.0+** is powered by Deno and distributed as single pre-compiled binaries—no installation,
no setup, just run.
## ✨ Features
@@ -15,12 +19,18 @@
- Runtime threshold triggers
- Power status change triggers
- Configurable shutdown delays
- **🌐 Universal SNMP Support**: Full support for SNMP v1, v2c, and v3 with authentication and encryption
- **🏭 Multiple UPS Brands**: Works with CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv, and custom OID configurations
- **🌐 Universal SNMP Support**: Full support for SNMP v1, v2c, and v3 with authentication and
encryption
- **🏭 Multiple UPS Brands**: Works with CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv, and
custom OID configurations
- **🚀 Systemd Integration**: Simple service installation and management
- **📊 Real-time Monitoring**: Live status updates with detailed action and group information
- **📦 Self-Contained Binary**: Single executable with zero runtime dependencies—just download and run
- **🖥️ Cross-Platform**: Binaries available for Linux (x64, ARM64), macOS (Intel, Apple Silicon), and Windows
- **🌐 HTTP API**: Optional HTTP server for JSON status export with authentication
- **⚡ Power Metrics**: Monitor output load, power (watts), voltage, and current for all UPS devices
- **📦 Self-Contained Binary**: Single executable with zero runtime dependencies—just download and
run
- **🖥️ Cross-Platform**: Binaries available for Linux (x64, ARM64), macOS (Intel, Apple Silicon),
and Windows
## 🚀 Quick Start
@@ -61,13 +71,15 @@ npm install -g @serve.zone/nupst
```
**Benefits:**
- Automatic platform detection and binary download
- Downloads only the binary for your platform (~400-500MB)
- Easy updates via `npm update -g @serve.zone/nupst`
- Version management with npm
- Works with Node.js >=14
**Note:** The installation will download the appropriate binary from GitHub releases during the postinstall step.
**Note:** The installation will download the appropriate binary from GitHub releases during the
postinstall step.
### Automated Installer Script
@@ -78,6 +90,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
```
**What it does:**
1. Detects your platform (OS and architecture)
2. Downloads the latest pre-compiled binary
3. Installs to `/opt/nupst/nupst`
@@ -101,15 +114,16 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
### Manual Installation
Download the appropriate binary for your platform from [releases](https://code.foss.global/serve.zone/nupst/releases):
Download the appropriate binary for your platform from
[releases](https://code.foss.global/serve.zone/nupst/releases):
| Platform | Binary |
|----------|--------|
| Linux x64 | `nupst-linux-x64` |
| Linux ARM64 | `nupst-linux-arm64` |
| macOS Intel | `nupst-macos-x64` |
| macOS Apple Silicon | `nupst-macos-arm64` |
| Windows x64 | `nupst-windows-x64.exe` |
| Platform | Binary |
| ------------------- | ----------------------- |
| Linux x64 | `nupst-linux-x64` |
| Linux ARM64 | `nupst-linux-arm64` |
| macOS Intel | `nupst-macos-x64` |
| macOS Apple Silicon | `nupst-macos-arm64` |
| Windows x64 | `nupst-windows-x64.exe` |
```bash
# Download binary (replace with your platform)
@@ -175,7 +189,8 @@ nupst group list # List all groups
### Action Management 🆕
Actions define what happens when UPS conditions are met. Actions can be attached to individual UPS devices or to groups.
Actions define what happens when UPS conditions are met. Actions can be attached to individual UPS
devices or to groups.
```bash
# Add an action to a UPS device or group
@@ -215,6 +230,78 @@ Add Action to UPS Main Server UPS
Changes saved and will be applied automatically
```
### Feature Management 🆕
Optional features like the HTTP server for JSON status export:
```bash
# Configure HTTP server feature (interactive)
nupst feature httpServer
```
**Example: Enabling HTTP Server**
```bash
$ sudo nupst feature httpServer
HTTP Server Feature Configuration
Configure the HTTP server to expose UPS status as JSON
HTTP Server is currently: DISABLED
Enable or disable HTTP server? (enable/disable/cancel): enable
HTTP Server Port [8080]: 8080
URL Path [/ups-status]: /ups-status
Generated new authentication token
✓ HTTP Server Configuration
Status: ENABLED
Port: 8080
Path: /ups-status
Auth Token: abc123xyz789def456
Usage examples:
curl -H "Authorization: Bearer abc123xyz789def456" http://localhost:8080/ups-status
curl "http://localhost:8080/ups-status?token=abc123xyz789def456"
⚠ IMPORTANT: Save the authentication token securely!
Service is running. Restart to apply changes? (Y/n): Y
```
**Query UPS Status via HTTP:**
```bash
# Using Bearer token in header
curl -H "Authorization: Bearer abc123xyz789def456" \
http://localhost:8080/ups-status
# Using token as query parameter
curl "http://localhost:8080/ups-status?token=abc123xyz789def456"
```
**JSON Response:**
```json
[
{
"id": "ups-main",
"name": "Main Server UPS",
"powerStatus": "online",
"batteryCapacity": 100,
"batteryRuntime": 45,
"outputLoad": 23,
"outputPower": 115,
"outputVoltage": 230.5,
"outputCurrent": 0.5,
"lastStatusChange": 1729685123456,
"lastCheckTime": 1729685153456
}
]
```
### Configuration
```bash
@@ -231,14 +318,21 @@ nupst config show # Display current configuration
## ⚙️ Configuration
NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to configure is through interactive commands, but you can also edit the JSON directly.
NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to configure is through
interactive commands, but you can also edit the JSON directly.
### Example Configuration (v4.1+)
```json
{
"version": "4.1",
"version": "4.2",
"checkInterval": 30000,
"httpServer": {
"enabled": true,
"port": 8080,
"path": "/ups-status",
"authToken": "abc123xyz789def456"
},
"upsDevices": [
{
"id": "ups-main",
@@ -315,8 +409,9 @@ NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to confi
#### Global Settings
- **`version`**: Config format version (current: "4.1")
- **`version`**: Config format version (current: "4.2")
- **`checkInterval`**: Polling interval in milliseconds (default: 30000)
- **`httpServer`**: Optional HTTP server configuration (see HTTP Server Configuration below)
#### UPS Device Settings
@@ -327,25 +422,25 @@ NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to confi
**SNMP Configuration:**
| Field | Description | Values |
|-------|-------------|--------|
| `host` | IP address or hostname | e.g., "192.168.1.100" |
| `port` | SNMP port | Default: 161 |
| `version` | SNMP version | 1, 2, or 3 |
| `timeout` | Timeout in milliseconds | Default: 5000 |
| `upsModel` | UPS brand/model | 'cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', 'custom' |
| `community` | SNMP community (v1/v2c) | Default: "public" |
| Field | Description | Values |
| ----------- | ----------------------- | -------------------------------------------------------------- |
| `host` | IP address or hostname | e.g., "192.168.1.100" |
| `port` | SNMP port | Default: 161 |
| `version` | SNMP version | 1, 2, or 3 |
| `timeout` | Timeout in milliseconds | Default: 5000 |
| `upsModel` | UPS brand/model | 'cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', 'custom' |
| `community` | SNMP community (v1/v2c) | Default: "public" |
**SNMPv3 Security:**
| Field | Description |
|-------|-------------|
| Field | Description |
| --------------- | ------------------------------------------- |
| `securityLevel` | 'noAuthNoPriv', 'authNoPriv', or 'authPriv' |
| `username` | SNMPv3 username |
| `authProtocol` | 'MD5' or 'SHA' |
| `authKey` | Authentication password |
| `privProtocol` | 'DES' or 'AES' (for authPriv) |
| `privKey` | Privacy/encryption password |
| `username` | SNMPv3 username |
| `authProtocol` | 'MD5' or 'SHA' |
| `authKey` | Authentication password |
| `privProtocol` | 'DES' or 'AES' (for authPriv) |
| `privKey` | Privacy/encryption password |
#### Action Configuration
@@ -365,21 +460,21 @@ Actions define automated responses to UPS conditions:
**Action Fields:**
| Field | Description | Values |
|-------|-------------|--------|
| `type` | Action type | Currently only 'shutdown' |
| `thresholds` | Battery and runtime limits | `{ battery: 0-100, runtime: minutes }` |
| `triggerMode` | When to trigger action | See Trigger Modes below |
| `shutdownDelay` | Delay before executing (seconds) | Default: 5 |
| Field | Description | Values |
| --------------- | -------------------------------- | -------------------------------------- |
| `type` | Action type | Currently only 'shutdown' |
| `thresholds` | Battery and runtime limits | `{ battery: 0-100, runtime: minutes }` |
| `triggerMode` | When to trigger action | See Trigger Modes below |
| `shutdownDelay` | Delay before executing (seconds) | Default: 5 |
**Trigger Modes:**
| Mode | Description |
|------|-------------|
| `onlyPowerChanges` | Trigger only when power status changes (on battery → online or vice versa) |
| `onlyThresholds` | Trigger only when battery or runtime thresholds are violated |
| `powerChangesAndThresholds` | Trigger only when power changes AND thresholds are violated |
| `anyChange` | Trigger on any status change |
| Mode | Description |
| --------------------------- | -------------------------------------------------------------------------- |
| `onlyPowerChanges` | Trigger only when power status changes (on battery → online or vice versa) |
| `onlyThresholds` | Trigger only when battery or runtime thresholds are violated |
| `powerChangesAndThresholds` | Trigger only when power changes AND thresholds are violated |
| `anyChange` | Trigger on any status change |
#### Group Settings
@@ -397,8 +492,53 @@ Groups allow coordinated management of multiple UPS devices:
**Group Modes:**
- **`redundant`**: System shuts down only when ALL UPS devices in the group are critical. Perfect for setups with backup UPS units.
- **`nonRedundant`**: System shuts down when ANY UPS device in the group is critical. Used when all UPS devices must be operational.
- **`redundant`**: System shuts down only when ALL UPS devices in the group are critical. Perfect
for setups with backup UPS units.
- **`nonRedundant`**: System shuts down when ANY UPS device in the group is critical. Used when all
UPS devices must be operational.
#### HTTP Server Configuration 🆕
Enable optional HTTP server for JSON status export with authentication:
```json
{
"enabled": true,
"port": 8080,
"path": "/ups-status",
"authToken": "abc123xyz789def456"
}
```
**HTTP Server Fields:**
| Field | Description | Default |
| ----------- | ------------------------------------------------ | -------------- |
| `enabled` | Whether HTTP server is enabled | `false` |
| `port` | TCP port for HTTP server | `8080` |
| `path` | URL path for status endpoint | `/ups-status` |
| `authToken` | Authentication token (required for all requests) | Auto-generated |
**Authentication Methods:**
The HTTP server supports two authentication methods:
1. **Bearer Token** (Header): `Authorization: Bearer <token>`
2. **Query Parameter**: `?token=<token>`
**Security Features:**
- Token-based authentication required for all requests
- Returns 401 Unauthorized for invalid/missing tokens
- Serves cached data from monitoring loop (no extra SNMP queries)
- No CORS headers (local network only)
**Use Cases:**
- Integration with monitoring systems (Prometheus, Grafana, etc.)
- Custom dashboards and visualizations
- Mobile apps and web interfaces
- Home automation systems
### Supported UPS Models
@@ -425,7 +565,8 @@ For custom UPS models, specify `customOIDs`:
### Status Display
The status command shows comprehensive information about your UPS devices, groups, and configured actions:
The status command shows comprehensive information about your UPS devices, groups, and configured
actions:
```bash
$ nupst service status
@@ -457,6 +598,7 @@ nupst service logs
```
Example output:
```
[2025-01-15 10:30:15] NUPST daemon started
[2025-01-15 10:30:15] ✓ Connected to Main Server UPS (192.168.1.100)
@@ -481,11 +623,11 @@ NUPST is designed with security as a priority:
Full SNMPv3 support with authentication and encryption:
| Security Level | Description |
|----------------|-------------|
| `noAuthNoPriv` | No authentication, no encryption (not recommended) |
| `authNoPriv` | MD5/SHA authentication without encryption |
| `authPriv` | Full authentication + DES/AES encryption (recommended) |
| Security Level | Description |
| -------------- | ------------------------------------------------------ |
| `noAuthNoPriv` | No authentication, no encryption (not recommended) |
| `authNoPriv` | MD5/SHA authentication without encryption |
| `authPriv` | Full authentication + DES/AES encryption (recommended) |
**Example SNMPv3 Configuration:**
@@ -506,6 +648,12 @@ Full SNMPv3 support with authentication and encryption:
- **Local-Only Communication**: Only connects to UPS devices on local network
- **No Telemetry**: No data sent to external servers
- **No Auto-Updates**: Manual update process only
- **HTTP Server** (optional):
- Disabled by default
- Token-based authentication required
- Local network access only (no CORS)
- Serves cached data (no additional SNMP queries)
- Configurable port and path
### Verifying Downloads
@@ -531,6 +679,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
```
The installer will:
- Download the latest binary
- Replace the existing installation
- Preserve your configuration
@@ -557,7 +706,8 @@ sudo nupst service start
nupst --version
```
Visit the [releases page](https://code.foss.global/serve.zone/nupst/releases) for the latest version.
Visit the [releases page](https://code.foss.global/serve.zone/nupst/releases) for the latest
version.
## 🗑️ Uninstallation
@@ -655,11 +805,11 @@ When installed, NUPST makes the following changes:
### File System
| Path | Description |
|------|-------------|
| `/opt/nupst/nupst` | Pre-compiled binary |
| `/usr/local/bin/nupst` | Symlink to binary |
| `/etc/nupst/config.json` | Configuration file |
| Path | Description |
| ----------------------------------- | -------------------- |
| `/opt/nupst/nupst` | Pre-compiled binary |
| `/usr/local/bin/nupst` | Symlink to binary |
| `/etc/nupst/config.json` | Configuration file |
| `/etc/systemd/system/nupst.service` | Systemd service unit |
### Services
@@ -670,7 +820,7 @@ When installed, NUPST makes the following changes:
### Network
- Outbound SNMP to UPS devices (default port 161)
- No inbound connections required
- Optional inbound HTTP server (disabled by default, configurable port)
- No external internet connections
## 🚀 Migration from v3.x
@@ -683,6 +833,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
```
**The installer automatically:**
- Detects v3.x installation
- Stops the service
- Replaces Node.js version with Deno binary
@@ -691,21 +842,22 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
### Key Changes in v4.x
| Aspect | v3.x | v4.x |
|--------|------|------|
| **Runtime** | Node.js + npm | Deno |
| **Distribution** | Git repo + npm install | Pre-compiled binaries |
| **Runtime Dependencies** | node_modules required | Zero (self-contained) |
| **Size** | ~150MB (with node_modules) | ~80MB (single binary) |
| **Startup** | Seconds | Milliseconds |
| **Commands** | Flat (`nupst add`) | Subcommands (`nupst ups add`) |
| **Configuration** | UPS-level thresholds | Action-based thresholds |
| Aspect | v3.x | v4.x |
| ------------------------ | -------------------------- | ----------------------------- |
| **Runtime** | Node.js + npm | Deno |
| **Distribution** | Git repo + npm install | Pre-compiled binaries |
| **Runtime Dependencies** | node_modules required | Zero (self-contained) |
| **Size** | ~150MB (with node_modules) | ~80MB (single binary) |
| **Startup** | Seconds | Milliseconds |
| **Commands** | Flat (`nupst add`) | Subcommands (`nupst ups add`) |
| **Configuration** | UPS-level thresholds | Action-based thresholds |
### Configuration Compatibility
Your v3.x configuration is **fully compatible**. The migration system automatically converts:
**v4.0 format** (UPS-level thresholds):
```json
{
"version": "4.0",
@@ -717,6 +869,7 @@ Your v3.x configuration is **fully compatible**. The migration system automatica
```
**v4.1 format** (action-based thresholds):
```json
{
"version": "4.1",
@@ -788,19 +941,28 @@ nupst/
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT
License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks,
service marks, or product names of the project, except as required for reasonable and customary use
in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated
with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture
Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these
trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be
approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
Task Venture Capital GmbH Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
For any legal inquiries or if you require further information, please contact us via email at
hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its
terms, and understand that the licensing of the code does not imply endorsement by Task Venture
Capital GmbH of any derivative works.

View File

@@ -1,8 +0,0 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@serve.zone/nupst',
version: '5.1.0',
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
}

View File

@@ -223,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';
@@ -294,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) => ({
@@ -466,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)'));
@@ -509,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');
@@ -632,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

@@ -1,12 +1,13 @@
import { NupstSnmp } from './snmp/manager.ts';
import { NupstDaemon } from './daemon.ts';
import { NupstSystemd } from './systemd.ts';
import { commitinfo } from './00_commitinfo_data.ts';
import denoConfig from '../deno.json' with { type: 'json' };
import { logger } from './logger.ts';
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,12 +93,19 @@ 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
*/
public getVersion(): string {
return commitinfo.version;
return denoConfig.version;
}
/**
@@ -143,8 +153,8 @@ export class Nupst {
private getLatestVersion(): Promise<string> {
return new Promise<string>((resolve, reject) => {
const options = {
hostname: 'registry.npmjs.org',
path: '/@serve.zone/nupst',
hostname: 'code.foss.global',
path: '/api/v1/repos/serve.zone/nupst/releases/latest',
method: 'GET',
headers: {
'Accept': 'application/json',
@@ -162,10 +172,14 @@ export class Nupst {
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response['dist-tags'] && response['dist-tags'].latest) {
resolve(response['dist-tags'].latest);
if (response.tag_name) {
// Strip 'v' prefix from tag name (e.g., "v5.1.7" -> "5.1.7")
const version = response.tag_name.startsWith('v')
? response.tag_name.substring(1)
: response.tag_name;
resolve(version);
} else {
reject(new Error('Failed to parse version from npm registry response'));
reject(new Error('Failed to parse version from Gitea API response'));
}
} catch (error) {
reject(error);

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

@@ -355,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}`)}`);