Skip to content

feat: parse power measurements in http-power and add a power read CLI command#790

Merged
mangelajo merged 1 commit into
jumpstarter-dev:mainfrom
mmahut:mmahut/http-power-read
Jun 24, 2026
Merged

feat: parse power measurements in http-power and add a power read CLI command#790
mangelajo merged 1 commit into
jumpstarter-dev:mainfrom
mmahut:mmahut/http-power-read

Conversation

@mmahut

@mmahut mmahut commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Previously the http-power driver's read() always returned dummy (0.0, 0.0) values.

read() now parses the JSON the read endpoint returns and pulls voltage/current out of it. By default it looks for top-level voltage/current keys, but you can point voltage_path/current_path at a dotted path (emeter.voltage, StatusSNS.ENERGY.Voltage, meters.0.voltage) for devices that nest them.

Tested against a real Shelly Plug S Gen3.

j-power

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

HTTP Power driver's dummy read() implementation is replaced with real JSON parsing using configurable dotted-path fields, proper error handling for missing config/invalid JSON/non-numeric values, and a new CLI read subcommand that streams formatted power measurements.

Changes

HTTP Power JSON Parsing and CLI Integration

Layer / File(s) Summary
HTTP Power config and JSON path utility
python/packages/jumpstarter-driver-http-power/jumpstarter_driver_http_power/driver.py
JSON import added. _json_path() utility traverses nested dicts/lists using dotted notation. HttpEndpointConfig extended with optional voltage_path and current_path fields to specify where voltage/current values are located in JSON responses.
HTTP Power read() parsing and extraction
python/packages/jumpstarter-driver-http-power/jumpstarter_driver_http_power/driver.py
HttpPower.read() now performs HTTP request, parses response as JSON, extracts voltage/current via configured paths, and yields PowerReading with parsed floats. Raises ValueError for missing power_read config, invalid JSON, missing configured paths, or non-numeric values. _extract_reading() static method centralizes path traversal and float coercion.
HTTP Power read() unit and integration tests
python/packages/jumpstarter-driver-http-power/jumpstarter_driver_http_power/driver_test.py
Existing test updated to validate parsed values from mock JSON instead of dummy zeros. New focused unit tests cover nested path parsing, missing-field defaulting, missing-path errors, non-numeric-index errors, invalid JSON, and missing endpoint configuration. Includes pytest import and _power() test helper.
HTTP Power README documentation
python/packages/jumpstarter-driver-http-power/README.md
Added Shelly Smart Plug Gen1 and Gen2/Gen3 configuration sections. Config parameters table documents voltage_path and current_path for dotted-path JSON extraction. "Reading measurements" section describes JSON parsing behavior, missing-field defaulting to 0.0, and configured-path error handling, replacing prior note claiming parsing was not implemented.
Power client read subcommand and tests
python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py, python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client_test.py
PowerClient CLI extended with read subcommand that streams power measurements and prints voltage, current, and apparent power for each reading. Tests verify read() method yields expected values and CLI invocation produces correctly formatted output.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • jumpstarter-dev/jumpstarter#526: Extends the HTTP power driver implementation by replacing dummy HttpPower.read() behavior with real JSON/dotted-path parsing and updating tests and configuration fields.

Suggested reviewers

  • mangelajo
  • NickCao
  • bennyz

Poem

🐰 A driver once slept, returning zeros in jest,
Now JSON paths wake it to parse with its best,
Volt and current dance through the dotted-path flight,
The CLI brings them to echo so bright! 📊✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main changes: implementing real power measurement parsing in the http-power driver and adding a CLI command for reading power values.
Description check ✅ Passed The description is directly related to the changeset, explaining the shift from dummy values to real JSON parsing of voltage/current data with configurable dotted paths.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@raballew raballew self-requested a review June 16, 2026 15:46
url: "http://192.168.1.65/cm?cmnd=Status%2010" # Tasmota
voltage_path: "StatusSNS.ENERGY.Voltage"
current_path: "StatusSNS.ENERGY.Current"
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide the examples for the shelly? I have one and would love to test it :)

@base.command()
def read():
"""Read power measurements"""
for reading in self.read():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the CLI we should probably perform a single read. Otherwise this will remain in a loop reading powers without any throttling.

Another option is to provide a number of readings, and the interval between them, may be default to 1s ?

@mangelajo mangelajo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a couple of comments but looks great :) thank you so much!

try:
return float(value)
except (TypeError, ValueError):
raise ValueError(f"value at {key!r} is not numeric: {value!r}") from None

@raballew raballew Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message includes {value!r}, which dumps the full Python repr of whatever sits at the targeted path. Can a device response contain sensitive fields that should not leak into logs or client-visible error messages? If so consider including only the type name instead:

raise ValueError(f"value at {key!r} is not numeric (got {type(value).__name__})") from None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good comment, I did not think of sensitive data, but we can never be sure.

from jumpstarter.driver import Driver, export


def _json_path(data, path: str):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotations missing:

def _json_path(data: Any, path: str) -> Any:
def _extract_reading(data: Any, path: Optional[str], default_key: str) -> float:

@mmahut mmahut force-pushed the mmahut/http-power-read branch from d0897d8 to e2fd76c Compare June 23, 2026 21:05
@mmahut mmahut requested review from mangelajo and raballew June 23, 2026 21:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py (1)

67-75: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

count/interval don’t control individual readings when the stream is continuous.

At Line 68, for reading in self.read() can consume an unbounded stream, so the outer counter (i) may never increment. That makes --count and --interval ineffective for live streams.

Suggested fix
-        `@click.option`("--count", "-n", default=1, help="Number of readings (0 = infinite)", show_default=True)
-        `@click.option`("--interval", "-i", default=1.0, help="Seconds between readings", show_default=True)
+        `@click.option`(
+            "--count",
+            "-n",
+            type=click.IntRange(min=0),
+            default=1,
+            help="Number of readings (0 = infinite)",
+            show_default=True,
+        )
+        `@click.option`(
+            "--interval",
+            "-i",
+            type=click.FloatRange(min=0.0),
+            default=1.0,
+            help="Seconds between readings",
+            show_default=True,
+        )
         def read(count, interval):
             """Read power measurements"""
-            i = 0
-            while count == 0 or i < count:
-                for reading in self.read():
-                    click.echo(
-                        f"voltage={reading.voltage} V  current={reading.current} A  "
-                        f"apparent_power={reading.apparent_power} VA"
-                    )
-                i += 1
-                if (count == 0 or i < count) and interval > 0:
-                    time.sleep(interval)
+            for i, reading in enumerate(self.read(), start=1):
+                click.echo(
+                    f"voltage={reading.voltage} V  current={reading.current} A  "
+                    f"apparent_power={reading.apparent_power} VA"
+                )
+                if count > 0 and i >= count:
+                    break
+                if interval > 0:
+                    time.sleep(interval)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py`
around lines 67 - 75, The while loop at line 67 uses a counter `i` to control
iterations, but the inner `for reading in self.read()` consumes an unbounded
stream without respecting the count limit, making the `--count` and `--interval`
parameters ineffective for continuous streams. Add a break condition inside the
for loop (right after the click.echo call) to check if we have reached the count
limit by comparing `i` with `count`, and increment `i` for each individual
reading processed rather than only after the entire stream is consumed. This
ensures each reading respects the count constraint.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py`:
- Around line 67-75: The while loop at line 67 uses a counter `i` to control
iterations, but the inner `for reading in self.read()` consumes an unbounded
stream without respecting the count limit, making the `--count` and `--interval`
parameters ineffective for continuous streams. Add a break condition inside the
for loop (right after the click.echo call) to check if we have reached the count
limit by comparing `i` with `count`, and increment `i` for each individual
reading processed rather than only after the entire stream is consumed. This
ensures each reading respects the count constraint.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a64bfb55-d9dd-42d6-bbec-e3520a4ac85e

📥 Commits

Reviewing files that changed from the base of the PR and between d0897d8 and e2fd76c.

📒 Files selected for processing (6)
  • python/packages/jumpstarter-driver-http-power/README.md
  • python/packages/jumpstarter-driver-http-power/examples/exporter.yaml
  • python/packages/jumpstarter-driver-http-power/jumpstarter_driver_http_power/driver.py
  • python/packages/jumpstarter-driver-http-power/jumpstarter_driver_http_power/driver_test.py
  • python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py
  • python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client_test.py
💤 Files with no reviewable changes (1)
  • python/packages/jumpstarter-driver-http-power/examples/exporter.yaml
✅ Files skipped from review due to trivial changes (1)
  • python/packages/jumpstarter-driver-http-power/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/packages/jumpstarter-driver-power/jumpstarter_driver_power/client_test.py
  • python/packages/jumpstarter-driver-http-power/jumpstarter_driver_http_power/driver.py

@mangelajo mangelajo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks a lot @mmahut

@mangelajo mangelajo added this pull request to the merge queue Jun 24, 2026
Merged via the queue into jumpstarter-dev:main with commit 9671082 Jun 24, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants