Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit da5f51e

Browse files
authored
Merge pull request #445 from jumpstarter-dev/backport-358-to-release-0.6
[Backport release-0.6] Add drivers for EnerGenie Power Management System (PMS) products
2 parents 9b95455 + f091b6d commit da5f51e

11 files changed

Lines changed: 329 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../../packages/jumpstarter-driver-energenie/README.md

docs/source/reference/package-apis/drivers/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Drivers that control the power state and basic operation of devices:
2121
control
2222
* **[DUT Link](dutlink.md)** (`jumpstarter-driver-dutlink`) - [DUT Link
2323
Board](https://github.com/jumpstarter-dev/dutlink-board) hardware control
24+
* **[Energenie PDU](energenie.md)** (`jumpstarter-driver-energenie`) - Energenie PDUs
2425

2526
### Communication Drivers
2627

@@ -79,6 +80,7 @@ General-purpose utility drivers:
7980
can.md
8081
corellium.md
8182
dutlink.md
83+
energenie.md
8284
flashers.md
8385
http.md
8486
network.md
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/
2+
.coverage
3+
coverage.xml
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# EnerGenie
2+
3+
Drivers for EnerGenie products.
4+
5+
## EnerGenie driver
6+
7+
This driver provides a client for the [EnerGenie Programmable power switch](https://energenie.com/products.aspx?sg=239). The driver was tested on EG-PMS2-LAN device only but should be easy to support other devices.
8+
9+
**driver**: `jumpstarter_driver_energenie.driver.EnerGenie`
10+
11+
## Installation
12+
13+
```shell
14+
pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-energenie
15+
```
16+
17+
### Configuration
18+
19+
```yaml
20+
export:
21+
power:
22+
type: jumpstarter_driver_energenie.driver.EnerGenie
23+
config:
24+
host: "192.168.0.1"
25+
password: "password"
26+
slot: "1"
27+
```
28+
29+
### Config parameters
30+
31+
| Parameter | Description | Type | Required | Default |
32+
|-----------|-------------|------|----------|---------|
33+
| host | The ip address of the EnerGenie system | string | yes | None |
34+
| password | The password of the EnerGenie system | string | no | None |
35+
| slot | The slot number to be managed, 1, 2, 3, 4 | int | yes | 1 |
36+
37+
### PowerClient API
38+
39+
The EnerGenie driver provides a `PowerClient` with the following API:
40+
41+
```{eval-rst}
42+
.. autoclass:: jumpstarter_driver_power.client.PowerClient()
43+
:no-index:
44+
:members: on, off
45+
```
46+
47+
### Examples
48+
49+
Powering on and off a device
50+
51+
```{testcode}
52+
:skipif: True
53+
client.power.on()
54+
time.sleep(1)
55+
client.power.off()
56+
```
57+
58+
### CLI
59+
60+
```bash
61+
$ sudo uv run jmp exporter shell -c ./packages/jumpstarter-driver-energenie/examples/exporter.yaml
62+
63+
$$ j
64+
Usage: j [OPTIONS] COMMAND [ARGS]...
65+
66+
Generic composite device
67+
68+
Options:
69+
--help Show this message and exit.
70+
71+
Commands:
72+
power Generic power
73+
74+
$$ j power on
75+
76+
77+
$$ exit
78+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
metadata:
4+
namespace: default
5+
name: demo
6+
endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082
7+
token: "<token>"
8+
export:
9+
power:
10+
type: jumpstarter_driver_energenie.driver.EnerGenie
11+
config:
12+
host: "192.168.1.51"
13+
password: "1"
14+
slot: 1

packages/jumpstarter-driver-energenie/jumpstarter_driver_energenie/__init__.py

Whitespace-only changes.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
2+
from collections.abc import AsyncGenerator
3+
from dataclasses import dataclass, field
4+
5+
import requests
6+
from jumpstarter_driver_power.driver import PowerInterface, PowerReading
7+
8+
from jumpstarter.driver import Driver, export
9+
10+
11+
@dataclass(kw_only=True)
12+
class EnerGenie(PowerInterface, Driver):
13+
"""
14+
driver for the EnerGenie Programmable surge protector with LAN interface.
15+
16+
This driver was tested on EG-PMS2-LAN device only but should be easy to support other devices.
17+
"""
18+
19+
host: str | None = field(default=None)
20+
password: str | None = field(default="1")
21+
slot: int = 1
22+
23+
def login(self):
24+
"""
25+
Log in to the programmable power switch.
26+
27+
:return: True if login is successful, False otherwise.
28+
"""
29+
login_url = f"{self.base_url}/login.html"
30+
try:
31+
response = requests.post(login_url, data={"pw": self.password}, timeout=10)
32+
return response.status_code == 200
33+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,
34+
requests.exceptions.RequestException) as e:
35+
self.logger.error(f"Login failed: {str(e)}")
36+
return False
37+
38+
def __post_init__(self):
39+
if hasattr(super(), "__post_init__"):
40+
super().__post_init__()
41+
# Programmable power switch initialitzation. The EG-PMS2-LAN device has up to 4 slots.
42+
if self.slot < 1 or self.slot > 4:
43+
raise ValueError("Slot must be between 1 and 4")
44+
if self.host is None:
45+
raise ValueError("Host must be specified")
46+
self.logger.debug(f"Using Host: {self.host}, Slot: {self.slot}")
47+
self.base_url = f"http://{self.host}"
48+
49+
50+
def set_switch(self, switch_number, state):
51+
"""
52+
Set the state of a specific switch.
53+
54+
:param switch_number: The switch number (1, 2, etc.).
55+
:param state: The state to set (1 for ON, 0 for OFF).
56+
:return: True if the operation is successful, False otherwise.
57+
"""
58+
if state not in [0, 1]:
59+
self.logger.error(f"Invalid state: {state}")
60+
return False
61+
62+
if self.login():
63+
self.logger.debug("Login successful!")
64+
else:
65+
self.logger.debug("Login failed!")
66+
return False
67+
data = {f"cte{switch_number}": state}
68+
try:
69+
response = requests.post(self.base_url, data=data, timeout=10)
70+
if response.status_code != 200:
71+
self.logger.error(f"Set switch {switch_number} to {state} state failed!")
72+
return False
73+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,
74+
requests.exceptions.RequestException) as e:
75+
self.logger.error(f"Set switch failed: {str(e)}")
76+
return False
77+
78+
self.logger.debug(f"Set switch {switch_number} to {state} state")
79+
80+
return True
81+
82+
@export
83+
def on(self) -> None:
84+
self.set_switch(self.slot, 1)
85+
86+
@export
87+
def off(self) -> None:
88+
self.set_switch(self.slot, 0)
89+
90+
@export
91+
def read(self) -> AsyncGenerator[PowerReading, None]:
92+
raise NotImplementedError
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pytest_httpserver import HTTPServer
2+
3+
from .driver import EnerGenie
4+
from jumpstarter.common.utils import serve
5+
6+
7+
def test_drivers_energenie(httpserver: HTTPServer):
8+
# Configure mock responses
9+
# 1. Login response - Match raw data string
10+
httpserver.expect_request(
11+
"/login.html",
12+
method="POST",
13+
data="pw=1"
14+
).respond_with_data("Login successful") # Defaults to status 200
15+
16+
# 2. Response for turning ON switch 1 - Match raw data string
17+
httpserver.expect_request(
18+
"/",
19+
method="POST",
20+
data="cte1=1"
21+
).respond_with_data("Switch turned ON") # Defaults to status 200
22+
23+
# 3. Response for turning OFF switch 1 - Match raw data string
24+
httpserver.expect_request(
25+
"/",
26+
method="POST",
27+
data="cte1=0"
28+
).respond_with_data("Switch turned OFF") # Defaults to status 200
29+
30+
# Get the mock server's host and port
31+
host = f"{httpserver.host}:{httpserver.port}"
32+
33+
# Create EnerGenie instance with the mock server's URL
34+
instance = EnerGenie(host=host)
35+
36+
with serve(instance) as client:
37+
client.on()
38+
client.off()
39+
40+
# check_assertions will verify that all expected requests were received
41+
# in the correct order and that no unexpected requests arrived.
42+
try:
43+
httpserver.check_assertions()
44+
except AssertionError as e:
45+
print(f"httpserver assertions FAILED: {e}")
46+
raise # Re-raise the assertion error to fail the test
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[project]
2+
name = "jumpstarter-driver-energenie"
3+
dynamic = ["version", "urls"]
4+
description = "Energenie is an advanced surge protector with power management features"
5+
readme = "README.md"
6+
license = { text = "Apache-2.0" }
7+
authors = [
8+
{ name = "Enric Balletbo i Serra", email = "eballetbo@redhat.com" }
9+
]
10+
requires-python = ">=3.11"
11+
dependencies = [
12+
"anyio>=4.6.2.post1",
13+
"jumpstarter",
14+
"jumpstarter-driver-power"
15+
]
16+
17+
[project.entry-points."jumpstarter.drivers"]
18+
EnerGenie = "jumpstarter_driver_energenie.driver:EnerGenie"
19+
20+
[dependency-groups]
21+
dev = [
22+
"pytest-cov>=6.0.0",
23+
"pytest>=8.3.3",
24+
"pytest-httpserver>=1.0.0",
25+
]
26+
27+
[tool.hatch.metadata.hooks.vcs.urls]
28+
Homepage = "https://jumpstarter.dev"
29+
source_archive = "https://github.com/jumpstarter-dev/repo/archive/{commit_hash}.zip"
30+
31+
[tool.hatch.version]
32+
source = "vcs"
33+
raw-options = { 'root' = '../../'}
34+
35+
[build-system]
36+
requires = ["hatchling", "hatch-vcs"]
37+
build-backend = "hatchling.build"

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jumpstarter-driver-can = { workspace = true }
1111
jumpstarter-driver-composite = { workspace = true }
1212
jumpstarter-driver-corellium = { workspace = true }
1313
jumpstarter-driver-dutlink = { workspace = true }
14+
jumpstarter-driver-energenie = { workspace = true }
1415
jumpstarter-driver-flashers = { workspace = true }
1516
jumpstarter-driver-http = { workspace = true }
1617
jumpstarter-driver-raspberrypi = { workspace = true }

0 commit comments

Comments
 (0)