eaton 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 42cf738c337ad3fae01953c5e78003e8ed0c789146ee7b71bacaf8f3162b98ae
4
+ data.tar.gz: 0ebf36ddf22c38dea0a216777f8b04fb5da841648cbc1ccc0e4a83a8f45427ff
5
+ SHA512:
6
+ metadata.gz: 5d342a6f0162da2fc073ccdb52f768a98daa7a6486b1c327c261b0a4b88e41c3d4d3763f55b02a14fb24e4393ff89d95f4d88f5c49ce728ad95c5fd78e98adbb
7
+ data.tar.gz: 26622a5ed0fcd33eb9249b25f92550b887f6844c528fd9641380782aabdbe3f4de5717020d6554d0212640b1aeffd6853963e6b3038c5757e3962e6245fbceed
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-01-20
9
+
10
+ ### Added
11
+ - Initial release
12
+ - OAuth2 bearer token authentication with Eaton PDU G4 devices
13
+ - Power monitoring commands:
14
+ - `eaton power` - Overall power consumption
15
+ - `eaton outlets` - Per-outlet power monitoring
16
+ - `eaton branches` - Branch power distribution
17
+ - `eaton detailed` - Detailed power metrics
18
+ - `eaton info` - PDU device information
19
+ - `eaton auth` - Authentication testing
20
+ - Smart filtering: text mode shows only active outlets/branches, JSON shows all
21
+ - SSH tunneling support via custom host headers
22
+ - Dual output formats: human-friendly text and machine-readable JSON
23
+ - Ruby API for programmatic access
24
+ - Support for 42 outlets and 6 branches
25
+ - Metrics include: watts, voltage, current, power factor, frequency, load percentage
26
+
27
+ ### Features
28
+ - Zero-dependency HTTP client (uses Ruby's Net::HTTP)
29
+ - Automatic token management and session cleanup
30
+ - Comprehensive error handling
31
+ - CLI built with Thor
32
+ - Compatible with Ruby 3.0+
33
+
34
+ [0.1.0]: https://github.com/usiegj00/eaton/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jonathan Siegel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # Eaton PDU Manager
2
+
3
+ A Ruby gem and CLI for managing Eaton Rack PDU G4 devices via REST API. Provides comprehensive power monitoring and management capabilities.
4
+
5
+ ## Features
6
+
7
+ - 🔌 **Power Monitoring**: Overall, per-outlet, and per-branch power consumption
8
+ - 📊 **Detailed Metrics**: Voltage, current, power factor, frequency, load percentage
9
+ - 🔄 **Smart Filtering**: Text mode shows only active outlets/branches, JSON mode shows all
10
+ - 🔐 **OAuth2 Authentication**: Secure bearer token authentication
11
+ - 🌐 **SSH Tunneling**: Support for SSH tunneled connections via custom host headers
12
+ - 📝 **Dual Output**: Human-friendly text or machine-readable JSON
13
+
14
+ ## Installation
15
+
16
+ Add to your Gemfile:
17
+
18
+ ```ruby
19
+ gem 'eaton'
20
+ ```
21
+
22
+ Or install directly:
23
+
24
+ ```bash
25
+ gem install eaton
26
+ ```
27
+
28
+ For development:
29
+
30
+ ```bash
31
+ git clone https://github.com/usiegj00/eaton.git
32
+ cd eaton
33
+ bundle install
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ # Get overall power consumption
40
+ eaton power \
41
+ --host pdu.example.com \
42
+ --username admin \
43
+ --password your_password
44
+
45
+ # Get active outlets only (text mode)
46
+ eaton outlets \
47
+ --host pdu.example.com \
48
+ --username admin \
49
+ --password your_password
50
+
51
+ # Get all outlets as JSON
52
+ eaton outlets \
53
+ --host pdu.example.com \
54
+ --username admin \
55
+ --password your_password \
56
+ --format json
57
+ ```
58
+
59
+ ## Commands
60
+
61
+ All commands support these options:
62
+ - `--host` - PDU hostname or IP (required)
63
+ - `--port` - PDU port (default: 443)
64
+ - `--username` - PDU username (required)
65
+ - `--password` - PDU password (required)
66
+ - `--host-header` - Custom Host header for SSH tunneling (optional)
67
+ - `--verify-ssl` - Verify SSL certificates (default: false)
68
+ - `--format` - Output format: `text` or `json` (default: text)
69
+
70
+ ### Available Commands
71
+
72
+ | Command | Description |
73
+ |---------|-------------|
74
+ | `eaton auth` | Test authentication |
75
+ | `eaton info` | Display PDU device information |
76
+ | `eaton power` | Get overall power consumption (watts) |
77
+ | `eaton outlets` | Get per-outlet power consumption |
78
+ | `eaton branches` | Get per-branch power distribution |
79
+ | `eaton detailed` | Get detailed power metrics |
80
+
81
+ ### Output Filtering
82
+
83
+ **Text Mode** (default):
84
+ - Shows only outlets/branches with active power draw
85
+ - Clean, focused output for human reading
86
+
87
+ **JSON Mode** (`--format json`):
88
+ - Shows all outlets/branches regardless of state
89
+ - Complete data for automation and monitoring systems
90
+
91
+ ## Usage Examples
92
+
93
+ ### Get PDU Information
94
+
95
+ ```bash
96
+ eaton info --host pdu.example.com --username admin --password secret
97
+ ```
98
+
99
+ Output:
100
+ ```
101
+ PDU Device Information:
102
+ ============================================================
103
+ id: 1
104
+ name: PDU
105
+ model: Eaton Rack PDU G4
106
+ serial_number: ABC123
107
+ vendor: Eaton
108
+ firmware_version: 2.9.2
109
+ status: in service
110
+ health: ok
111
+ nominal_power: 19800
112
+ nominal_current: 55
113
+ nominal_voltage: 208
114
+ ```
115
+
116
+ ### Monitor Overall Power
117
+
118
+ ```bash
119
+ eaton power --host pdu.example.com --username admin --password secret
120
+ ```
121
+
122
+ Output:
123
+ ```
124
+ Overall Power:
125
+ ============================================================
126
+ watts: 1542.3
127
+ ```
128
+
129
+ ### View Active Outlets
130
+
131
+ ```bash
132
+ eaton outlets --host pdu.example.com --username admin --password secret
133
+ ```
134
+
135
+ Shows only outlets currently drawing power.
136
+
137
+ ### Export All Outlets to JSON
138
+
139
+ ```bash
140
+ eaton outlets \
141
+ --host pdu.example.com \
142
+ --username admin \
143
+ --password secret \
144
+ --format json > outlets.json
145
+ ```
146
+
147
+ ### SSH Tunneling
148
+
149
+ When connecting through an SSH tunnel:
150
+
151
+ ```bash
152
+ # SSH tunnel to remote PDU
153
+ ssh -L 5000:192.168.1.100:443 user@jumphost
154
+
155
+ # Connect via tunnel with custom host header
156
+ eaton power \
157
+ --host localhost \
158
+ --port 5000 \
159
+ --username admin \
160
+ --password secret \
161
+ --host-header 192.168.1.100
162
+ ```
163
+
164
+ ## Ruby API
165
+
166
+ Use the gem programmatically in your Ruby code:
167
+
168
+ ```ruby
169
+ require 'eaton'
170
+
171
+ # Create client
172
+ client = Eaton::Client.new(
173
+ host: 'pdu.example.com',
174
+ username: 'admin',
175
+ password: 'secret',
176
+ verify_ssl: true
177
+ )
178
+
179
+ # Extend with power monitoring
180
+ client.extend(Eaton::Power)
181
+
182
+ # Get PDU info
183
+ info = client.pdu_info
184
+ puts "#{info[:model]} - #{info[:serial_number]}"
185
+
186
+ # Get overall power
187
+ power = client.overall_power
188
+ puts "Current draw: #{power} watts"
189
+
190
+ # Get active outlets
191
+ outlets = client.outlet_power
192
+ outlets.select { |o| o[:watts] > 0 }.each do |outlet|
193
+ puts "#{outlet[:name]}: #{outlet[:watts]}W"
194
+ end
195
+
196
+ # Get branch distribution
197
+ branches = client.branch_power
198
+ branches.each do |branch|
199
+ puts "#{branch[:name]}: #{branch[:current]}A @ #{branch[:voltage]}V"
200
+ end
201
+
202
+ # Clean up
203
+ client.logout
204
+ ```
205
+
206
+ ## API Endpoints
207
+
208
+ The gem interfaces with these REST API endpoints:
209
+
210
+ - `/powerDistributions/1` - PDU device information
211
+ - `/powerDistributions/1/inputs/1` - Overall power input
212
+ - `/powerDistributions/1/outlets/{id}` - Individual outlet data
213
+ - `/powerDistributions/1/branches/{id}` - Branch distribution data
214
+
215
+ ## Supported Devices
216
+
217
+ Tested and verified with:
218
+ - **Eaton Rack PDU G4**
219
+ - Firmware: 2.9.2+
220
+ - API: `/rest/mbdetnrs/2.0`
221
+
222
+ Should work with other Eaton PDU models using the same API version.
223
+
224
+ ## Authentication
225
+
226
+ Uses OAuth2 bearer token authentication:
227
+
228
+ 1. POST credentials to `/oauth2/token/`
229
+ 2. Receive access token
230
+ 3. Include token in `Authorization: Bearer {token}` header
231
+ 4. Token automatically managed and refreshed
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ # Clone repository
237
+ git clone https://github.com/usiegj00/eaton.git
238
+ cd eaton
239
+
240
+ # Install dependencies
241
+ bundle install
242
+
243
+ # Run tests
244
+ bundle exec rspec
245
+
246
+ # Install locally
247
+ bundle exec rake install
248
+ ```
249
+
250
+ ## Contributing
251
+
252
+ 1. Fork it
253
+ 2. Create your feature branch (`git checkout -b feature/my-feature`)
254
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
255
+ 4. Push to the branch (`git push origin feature/my-feature`)
256
+ 5. Create a Pull Request
257
+
258
+ ## License
259
+
260
+ MIT License - see LICENSE file for details
261
+
262
+ ## Support
263
+
264
+ - Issues: https://github.com/usiegj00/eaton/issues
265
+ - Documentation: https://github.com/usiegj00/eaton
266
+
267
+ ## Credits
268
+
269
+ Developed for managing Eaton Rack PDU G4 devices via REST API.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/exe/eaton ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "eaton"
4
+
5
+ Eaton::CLI.start(ARGV)
data/lib/eaton/cli.rb ADDED
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+
6
+ module Eaton
7
+ class CLI < Thor
8
+ class_option :host, type: :string, required: true, desc: "PDU hostname or IP address"
9
+ class_option :port, type: :numeric, default: 443, desc: "PDU port (default: 443)"
10
+ class_option :username, type: :string, required: true, desc: "PDU username"
11
+ class_option :password, type: :string, required: true, desc: "PDU password"
12
+ class_option :verify_ssl, type: :boolean, default: false, desc: "Verify SSL certificates"
13
+ class_option :host_header, type: :string, desc: "Custom Host header (for SSH tunneling)"
14
+ class_option :format, type: :string, default: "text", enum: ["text", "json"], desc: "Output format"
15
+
16
+ desc "power", "Get overall power consumption in watts"
17
+ def power
18
+ with_client do |client|
19
+ power = client.overall_power
20
+ output_result("Overall Power", { watts: power })
21
+ end
22
+ end
23
+
24
+ desc "outlets", "Get per-outlet power consumption"
25
+ def outlets
26
+ with_client do |client|
27
+ outlets = client.outlet_power
28
+ # Filter out zero-power outlets in text mode
29
+ if options[:format] == "text"
30
+ outlets = outlets.select { |o| o[:watts] && o[:watts] > 0 }
31
+ end
32
+ output_result("Outlet Power", outlets)
33
+ end
34
+ end
35
+
36
+ desc "detailed", "Get detailed power information"
37
+ def detailed
38
+ with_client do |client|
39
+ info = client.detailed_power_info
40
+ # Filter outlets in text mode
41
+ if options[:format] == "text" && info[:outlets]
42
+ info[:outlets] = info[:outlets].select { |o| o[:watts] && o[:watts] > 0 }
43
+ end
44
+ output_result("Detailed Power Information", info)
45
+ end
46
+ end
47
+
48
+ desc "branches", "Get power consumption per branch"
49
+ def branches
50
+ with_client do |client|
51
+ branches = client.branch_power
52
+ # Filter out zero-current branches in text mode
53
+ if options[:format] == "text"
54
+ branches = branches.select { |b| b[:current] && b[:current] > 0 }
55
+ end
56
+ output_result("Branch Power", branches)
57
+ end
58
+ end
59
+
60
+ desc "info", "Display PDU device information"
61
+ def info
62
+ with_client do |client|
63
+ info = client.pdu_info
64
+ output_result("PDU Device Information", info)
65
+ end
66
+ end
67
+
68
+ desc "auth", "Test authentication with the PDU"
69
+ def auth
70
+ with_client do |client|
71
+ token = client.authenticate!
72
+ output_result("Authentication Test", {
73
+ status: "success",
74
+ token_present: !token.nil?,
75
+ token_length: token&.length
76
+ })
77
+ end
78
+ end
79
+
80
+ no_commands do
81
+ def with_client
82
+ client = Client.new(
83
+ host: options[:host],
84
+ port: options[:port],
85
+ username: options[:username],
86
+ password: options[:password],
87
+ verify_ssl: options[:verify_ssl],
88
+ host_header: options[:host_header]
89
+ )
90
+
91
+ # Mix in the Power module to add power monitoring methods
92
+ client.extend(Power)
93
+
94
+ yield client
95
+ rescue Client::AuthenticationError => e
96
+ error("Authentication failed: #{e.message}")
97
+ rescue Client::APIError => e
98
+ error("API error: #{e.message}")
99
+ rescue StandardError => e
100
+ error("Unexpected error: #{e.message}")
101
+ ensure
102
+ client&.logout
103
+ end
104
+
105
+ def output_result(title, data)
106
+ if options[:format] == "json"
107
+ puts JSON.pretty_generate(data)
108
+ else
109
+ output_text(title, data)
110
+ end
111
+ end
112
+
113
+ def output_text(title, data)
114
+ puts "\n#{title}:"
115
+ puts "=" * 60
116
+
117
+ case data
118
+ when Hash
119
+ output_hash(data)
120
+ when Array
121
+ data.each_with_index do |item, index|
122
+ puts "\n[#{index + 1}]"
123
+ output_hash(item) if item.is_a?(Hash)
124
+ end
125
+ else
126
+ puts data
127
+ end
128
+
129
+ puts
130
+ end
131
+
132
+ def output_hash(hash, indent = 0)
133
+ hash.each do |key, value|
134
+ prefix = " " * indent
135
+ case value
136
+ when Hash
137
+ puts "#{prefix}#{key}:"
138
+ output_hash(value, indent + 1)
139
+ when Array
140
+ puts "#{prefix}#{key}:"
141
+ value.each_with_index do |item, index|
142
+ if item.is_a?(Hash)
143
+ puts "#{prefix} [#{index}]:"
144
+ output_hash(item, indent + 2)
145
+ else
146
+ puts "#{prefix} - #{item}"
147
+ end
148
+ end
149
+ else
150
+ puts "#{prefix}#{key}: #{value}"
151
+ end
152
+ end
153
+ end
154
+
155
+ def error(message)
156
+ STDERR.puts "ERROR: #{message}"
157
+ exit 1
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "openssl"
7
+
8
+ module Eaton
9
+ class Client
10
+ class AuthenticationError < StandardError; end
11
+ class APIError < StandardError; end
12
+
13
+ attr_reader :host, :username, :base_url
14
+
15
+ def initialize(host:, username:, password:, port: 443, verify_ssl: false, host_header: nil)
16
+ @host = host
17
+ @username = username
18
+ @password = password
19
+ @port = port
20
+ @verify_ssl = verify_ssl
21
+ @host_header = host_header || host
22
+ @base_path = "/rest/mbdetnrs/2.0"
23
+ @token = nil
24
+ @session = nil
25
+ end
26
+
27
+ def authenticate!
28
+ request = Net::HTTP::Post.new("#{@base_path}/oauth2/token/")
29
+ add_browser_headers(request)
30
+ request.body = JSON.generate(username: @username, password: @password)
31
+
32
+ response = execute_request(request)
33
+
34
+ if response.code.to_i.between?(200, 299)
35
+ data = JSON.parse(response.body)
36
+ @token = data["access_token"]
37
+ @session = data["session"]
38
+ @token
39
+ else
40
+ raise AuthenticationError, "Authentication failed: #{response.body}"
41
+ end
42
+ rescue JSON::ParserError => e
43
+ raise AuthenticationError, "Invalid response from server: #{e.message}"
44
+ end
45
+
46
+ def authenticated?
47
+ !@token.nil?
48
+ end
49
+
50
+ def get(path)
51
+ authenticate! unless authenticated?
52
+
53
+ request = Net::HTTP::Get.new("#{@base_path}#{path}")
54
+ add_auth_headers(request)
55
+
56
+ handle_response(execute_request(request))
57
+ end
58
+
59
+ def post(path, data = {})
60
+ authenticate! unless authenticated?
61
+
62
+ request = Net::HTTP::Post.new("#{@base_path}#{path}")
63
+ add_auth_headers(request)
64
+ request.body = data.to_json
65
+
66
+ handle_response(execute_request(request))
67
+ end
68
+
69
+ def logout
70
+ return unless authenticated?
71
+
72
+ begin
73
+ delete(@session) if @session
74
+ rescue APIError
75
+ # Session might already be expired or deleted, ignore
76
+ ensure
77
+ @token = nil
78
+ @session = nil
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def http_connection
85
+ @http_connection ||= begin
86
+ http = Net::HTTP.new(@host, @port)
87
+ http.use_ssl = true
88
+ http.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
89
+ http
90
+ end
91
+ end
92
+
93
+ def execute_request(request)
94
+ http_connection.request(request)
95
+ end
96
+
97
+ def add_browser_headers(request)
98
+ request["Content-Type"] = "application/json"
99
+ request["Host"] = @host_header
100
+ request["Sec-Fetch-Mode"] = "cors"
101
+ request["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"
102
+ request["Origin"] = "https://#{@host_header}"
103
+ request["Sec-Fetch-Site"] = "same-origin"
104
+ end
105
+
106
+ def add_auth_headers(request)
107
+ request["Content-Type"] = "application/json"
108
+ request["Authorization"] = "Bearer #{@token}"
109
+ request["Host"] = @host_header
110
+ end
111
+
112
+ def delete(path)
113
+ request = Net::HTTP::Delete.new("#{@base_path}#{path}")
114
+ add_auth_headers(request)
115
+
116
+ handle_response(execute_request(request))
117
+ end
118
+
119
+ def handle_response(response)
120
+ status_code = response.code.to_i
121
+
122
+ case status_code
123
+ when 200..299
124
+ begin
125
+ JSON.parse(response.body)
126
+ rescue JSON::ParserError
127
+ response.body
128
+ end
129
+ when 401, 403
130
+ # Token might have expired, clear it and let caller retry
131
+ @token = nil
132
+ raise AuthenticationError, "Authentication failed or token expired"
133
+ else
134
+ error_message = begin
135
+ data = JSON.parse(response.body)
136
+ data["description"] || response.body
137
+ rescue
138
+ response.body
139
+ end
140
+ raise APIError, "API error (#{status_code}): #{error_message}"
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eaton
4
+ module Power
5
+ # Get overall power consumption for the PDU
6
+ # Returns power in watts
7
+ def overall_power
8
+ data = get("/powerDistributions/1/inputs/1")
9
+ data.dig("measures", "activePower")
10
+ end
11
+
12
+ # Get per-outlet power consumption
13
+ # Returns an array of hashes with outlet info and power in watts
14
+ def outlet_power
15
+ # Get list of outlets
16
+ outlets_list = get("/powerDistributions/1/outlets")
17
+ member_count = outlets_list["members@count"] || 0
18
+
19
+ return [] if member_count.zero?
20
+
21
+ # Get data for each outlet
22
+ outlets = []
23
+ outlets_list["members"].each do |member|
24
+ outlet_id = member["@id"].split("/").last
25
+ outlet_data = get("/powerDistributions/1/outlets/#{outlet_id}")
26
+
27
+ outlets << {
28
+ id: outlet_data["id"],
29
+ name: outlet_data.dig("identification", "friendlyName") || "Outlet #{outlet_id}",
30
+ physical_name: outlet_data.dig("identification", "physicalName"),
31
+ watts: outlet_data.dig("measures", "activePower"),
32
+ current: outlet_data.dig("measures", "current"),
33
+ voltage: nil, # Outlets don't report voltage individually
34
+ power_factor: outlet_data.dig("measures", "powerFactor"),
35
+ state: outlet_data.dig("status", "switchedOn") ? "on" : "off",
36
+ switched_on: outlet_data.dig("status", "switchedOn")
37
+ }
38
+ end
39
+
40
+ outlets
41
+ end
42
+
43
+ # Get detailed power information including voltage, current, and power factor
44
+ def detailed_power_info
45
+ input_data = get("/powerDistributions/1/inputs/1")
46
+
47
+ {
48
+ overall: {
49
+ watts: input_data.dig("measures", "activePower"),
50
+ apparent_power: input_data.dig("measures", "apparentPower"),
51
+ reactive_power: input_data.dig("measures", "reactivePower"),
52
+ frequency: input_data.dig("measures", "frequency"),
53
+ power_factor: input_data.dig("measures", "powerFactor"),
54
+ percent_load: input_data.dig("measures", "percentLoad"),
55
+ cumulated_energy: input_data.dig("measures", "cumulatedEnergy"),
56
+ partial_energy: input_data.dig("measures", "partialEnergy")
57
+ },
58
+ outlets: outlet_power
59
+ }
60
+ end
61
+
62
+ # Get branch power information
63
+ # Returns an array of branch power data
64
+ def branch_power
65
+ branches_list = get("/powerDistributions/1/branches")
66
+ member_count = branches_list["members@count"] || 0
67
+
68
+ return [] if member_count.zero?
69
+
70
+ branches = []
71
+ branches_list["members"].each do |member|
72
+ branch_id = member["@id"].split("/").last
73
+ branch_data = get("/powerDistributions/1/branches/#{branch_id}")
74
+
75
+ branches << {
76
+ id: branch_data["id"],
77
+ name: branch_data.dig("identification", "friendlyName") || "Branch #{branch_id}",
78
+ physical_name: branch_data.dig("identification", "physicalName"),
79
+ watts: branch_data.dig("measures", "activePower"),
80
+ current: branch_data.dig("measures", "current"),
81
+ voltage: branch_data.dig("measures", "voltage"),
82
+ power_factor: branch_data.dig("measures", "powerFactor")
83
+ }
84
+ end
85
+
86
+ branches
87
+ end
88
+
89
+ # Get PDU information
90
+ def pdu_info
91
+ data = get("/powerDistributions/1")
92
+
93
+ {
94
+ id: data["id"],
95
+ name: data.dig("identification", "friendlyName"),
96
+ model: data.dig("identification", "model"),
97
+ serial_number: data.dig("identification", "serialNumber"),
98
+ part_number: data.dig("identification", "partNumber"),
99
+ vendor: data.dig("identification", "vendor"),
100
+ firmware_version: data.dig("identification", "firmwareVersion"),
101
+ status: data.dig("status", "operating"),
102
+ health: data.dig("status", "health"),
103
+ nominal_power: data.dig("specifications", "activePower", "nominal"),
104
+ nominal_current: data.dig("specifications", "current", "nominal"),
105
+ nominal_voltage: data.dig("specifications", "voltage", "nominal")
106
+ }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eaton
4
+ VERSION = "0.1.0"
5
+ end
data/lib/eaton.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "eaton/version"
4
+ require_relative "eaton/client"
5
+ require_relative "eaton/power"
6
+ require_relative "eaton/cli"
7
+
8
+ module Eaton
9
+ class Error < StandardError; end
10
+ end
@@ -0,0 +1,4 @@
1
+ module PduManager
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eaton
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Siegel
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-10-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ description: Comprehensive power monitoring and management for Eaton Rack PDU G4 devices.
28
+ Features include overall power consumption, per-outlet monitoring, branch distribution,
29
+ detailed metrics (voltage, current, power factor), OAuth2 authentication, and SSH
30
+ tunneling support.
31
+ email:
32
+ - jonathan@example.com
33
+ executables:
34
+ - eaton
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - CHANGELOG.md
39
+ - LICENSE
40
+ - README.md
41
+ - Rakefile
42
+ - exe/eaton
43
+ - lib/eaton.rb
44
+ - lib/eaton/cli.rb
45
+ - lib/eaton/client.rb
46
+ - lib/eaton/power.rb
47
+ - lib/eaton/version.rb
48
+ - sig/pdu_manager.rbs
49
+ homepage: https://github.com/usiegj00/eaton
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/usiegj00/eaton
54
+ source_code_uri: https://github.com/usiegj00/eaton
55
+ bug_tracker_uri: https://github.com/usiegj00/eaton/issues
56
+ changelog_uri: https://github.com/usiegj00/eaton/blob/main/CHANGELOG.md
57
+ documentation_uri: https://github.com/usiegj00/eaton
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 3.0.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.5.22
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Ruby gem and CLI for managing Eaton Rack PDU G4 devices via REST API
77
+ test_files: []