Skip to content

feat: add Enphase Envoy powermeter backend#245

Open
CoRfr wants to merge 1 commit intotomquist:developfrom
CoRfr:feat-enphase-envoy
Open

feat: add Enphase Envoy powermeter backend#245
CoRfr wants to merge 1 commit intotomquist:developfrom
CoRfr:feat-enphase-envoy

Conversation

@CoRfr
Copy link
Copy Markdown

@CoRfr CoRfr commented Mar 16, 2026

Summary

  • Add new Envoy powermeter backend that reads net grid power from the Enphase Envoy local API (/production.json?details=1)
  • Supports single-phase (total) and three-phase (per-line) reporting
  • Supports static JWT token auth and automatic token renewal via Enphase Enlighten cloud credentials (username/password/serial)
  • Falls back from net-consumption to total-consumption for Envoys without CTs on the grid

Files changed

  • powermeter/envoy.pyEnvoy class + obtain_token() helper
  • powermeter/envoy_test.py — 11 unit tests
  • powermeter/__init__.py — export Envoy
  • config/config_loader.pyENVOY config section support
  • config.ini.example — documented configuration options

Configuration example

[ENVOY]
HOST = 192.168.1.200
TOKEN = your_envoy_jwt_token
# Or use Enlighten credentials for automatic token renewal:
USERNAME = [email protected]
PASSWORD = your_password
SERIAL = 122219012345
PHASES = 1
VERIFY_SSL = False
THROTTLE_INTERVAL = 2

Test plan

  • All 11 unit tests pass (find_measurement, single/three phase, fallback, no data error, 401 refresh, auto-obtain, obtain_token success/failure)
  • Integration test with real Envoy hardware

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for Enphase Envoy as a power-meter source with cloud authentication, automatic token refresh, and single‑ and three‑phase readings.
  • Configuration

    • New Envoy options (host, token or credentials, serial, phases, SSL verify, per-device throttle) and optional GENERAL DEVICE_IDS example entry.
  • Enhancement

    • Device IDs can be loaded from config when not provided on the command line.
  • Security

    • Printed config now masks sensitive keys (passwords/tokens).
  • Tests

    • Comprehensive tests for Envoy parsing, token flows, phases, and error cases.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Enphase Envoy support: new ENVOY config example and GENERAL:DEVICE_IDS option, config loader wiring to instantiate Envoy powermeters, a new powermeter.envoy implementation with Enlighten token handling and multi‑phase parsing, tests, and masking of sensitive values in ha_addon/run.sh.

Changes

Cohort / File(s) Summary
Configuration & CLI
config.ini.example, config/config_loader.py, main.py
Adds an ENVOY config block (HOST, TOKEN, USERNAME, PASSWORD, SERIAL, PHASES, VERIFY_SSL, per-meter THROTTLE_INTERVAL) and optional GENERAL:DEVICE_IDS; introduces ENVOY_SECTION, create_envoy_powermeter, and dispatching for ENVOY; main.py loads device IDs from GENERAL:DEVICE_IDS when CLI IDs are absent.
Envoy Backend
powermeter/envoy.py
New Envoy powermeter backend: Enlighten login/token obtain flow, token refresh on 401, local /production.json?details=1 fetch, parsing net/total consumption, single/three‑phase watt reporting, and helper _find_measurement.
Package Exports
powermeter/__init__.py
Exports Envoy in the package public API.
Tests
powermeter/envoy_test.py
Extensive unit tests for obtain_token, token-obtain/refresh flows, _find_measurement, single/three‑phase parsing, fallback behavior, 401 handling, and mocked network interactions.
HA Add-on Script
ha_addon/run.sh
Prints a redacted/masked configuration output, replacing sensitive keys (PASSWORD, TOKEN, ACCESSTOKEN, SECRET) with redacted placeholders.

Sequence Diagram(s)

sequenceDiagram
    participant Envoy as Envoy Class
    participant Enlighten as Enlighten Cloud
    participant EnvoyDevice as Envoy Device (local)
    participant Session as requests.Session

    rect rgba(100, 150, 200, 0.5)
    note over Envoy,Enlighten: Token obtain / refresh
    Envoy->>Enlighten: POST /login (username, password)
    Enlighten-->>Envoy: session_id
    Envoy->>Enlighten: POST /token (session_id, serial)
    Enlighten-->>Envoy: JWT token
    Envoy->>Envoy: Store token
    end

    rect rgba(150, 150, 100, 0.5)
    note over Envoy,EnvoyDevice: Production data fetch (with retry on 401)
    Envoy->>Session: GET https://host/production.json?details=1 (Authorization: Bearer token)
    alt 401 Response
        Session-->>Envoy: 401 Unauthorized
        Envoy->>Enlighten: obtain_token(...)
        Enlighten-->>Envoy: new JWT token
        Envoy->>Session: GET https://host/production.json?details=1 (Authorization: Bearer new_token)
        Session->>EnvoyDevice: GET request
        EnvoyDevice-->>Session: 200 + production.json
        Session-->>Envoy: 200 + JSON
    else 200 Response
        Session->>EnvoyDevice: GET request
        EnvoyDevice-->>Session: 200 + production.json
        Session-->>Envoy: 200 + JSON
    end
    Envoy->>Envoy: Parse consumption (net or total fallback), build phase watts
    Envoy-->>Caller: Return phase watts array
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being added: a new Enphase Envoy powermeter backend with support for authentication, multi-phase reporting, and configuration options.

✏️ 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 and usage tips.

@CoRfr CoRfr changed the title Add Enphase Envoy powermeter backend feat: add Enphase Envoy powermeter backend Mar 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@powermeter/envoy.py`:
- Around line 121-129: The current logic in powermeter/envoy.py silently falls
back from "net-consumption" to "total-consumption" via the
_find_measurement(consumption_list, "total-consumption") call; remove that
unconditional fallback so that if _find_measurement(consumption_list,
"net-consumption") returns None the code logs and raises immediately (preserve
the existing logger.error and raise ValueError) OR implement an explicit
configuration flag (e.g., allow_total_fallback or USE_TOTAL_CONSUMPTION) that
must be true to perform the fallback; update the code paths around net_meter and
the uses of net_meter to rely only on the configured behavior and ensure the
error path remains when fallback is not enabled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 67ff73f8-36af-462f-a166-f8d6ed7c8c71

📥 Commits

Reviewing files that changed from the base of the PR and between 55b8bd0 and 825025b.

📒 Files selected for processing (5)
  • config.ini.example
  • config/config_loader.py
  • powermeter/__init__.py
  • powermeter/envoy.py
  • powermeter/envoy_test.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main.py`:
- Line 207: When parsing cfg_device_ids into device_ids, filter out empty tokens
after splitting and stripping to avoid blank device IDs from trailing or
consecutive commas; change the comprehension that builds device_ids (currently
using cfg_device_ids.split(",") and strip) to include a truthiness check so only
non-empty strings are kept (mirror the filtering used in
config/config_loader.py) so variable device_ids contains only valid IDs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f2c31e5-f085-489e-91e7-6b875a140cdc

📥 Commits

Reviewing files that changed from the base of the PR and between 825025b and 8255ec9.

📒 Files selected for processing (2)
  • config.ini.example
  • main.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • config.ini.example

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
powermeter/envoy.py (1)

86-87: Global warning suppression affects entire process.

urllib3.disable_warnings() is a process-wide operation that will suppress InsecureRequestWarning for all HTTP clients, not just this Envoy instance. If other parts of the application rely on these warnings for debugging, they'll be silently suppressed.

Consider using a context manager or suppressing warnings at the call site instead:

♻️ Suggested approach using warnings filter
-        if not verify_ssl:
-            urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+        self._suppress_ssl_warnings = not verify_ssl

Then in _fetch, wrap the request:

import warnings

def _fetch(self):
    # ...
    with warnings.catch_warnings():
        if self._suppress_ssl_warnings:
            warnings.filterwarnings("ignore", category=urllib3.exceptions.InsecureRequestWarning)
        response = self.session.get(url, headers=headers, verify=self.verify_ssl, timeout=10)
    # ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@powermeter/envoy.py` around lines 86 - 87, The current use of
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) in the
module-wide scope suppresses warnings process-wide; instead, remove that global
call and limit suppression to the request site: update the Envoy class to track
the per-instance flag (e.g., verify_ssl or self._suppress_ssl_warnings) and
inside the _fetch method wrap the session.get call with
warnings.catch_warnings() and warnings.filterwarnings("ignore",
category=urllib3.exceptions.InsecureRequestWarning) only when the instance-level
flag is set, while passing verify=self.verify_ssl to session.get so only this
instance’s requests have suppressed warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@powermeter/envoy.py`:
- Around line 134-138: The current branch for self.phases > 1 builds watts as
[int(line.get("wNow", 0)) for line in lines[:3]] which can produce fewer than
self.phases elements if net_meter.get("lines") is short; update the method that
reads net_meter (use the existing self.phases, net_meter, and lines symbols) to
ensure the returned list has exactly self.phases entries by taking up to
self.phases values from lines, converting each to int with a default of 0, and
then padding with zeros (or truncating) as necessary so callers always receive a
list of length self.phases.

---

Nitpick comments:
In `@powermeter/envoy.py`:
- Around line 86-87: The current use of
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) in the
module-wide scope suppresses warnings process-wide; instead, remove that global
call and limit suppression to the request site: update the Envoy class to track
the per-instance flag (e.g., verify_ssl or self._suppress_ssl_warnings) and
inside the _fetch method wrap the session.get call with
warnings.catch_warnings() and warnings.filterwarnings("ignore",
category=urllib3.exceptions.InsecureRequestWarning) only when the instance-level
flag is set, while passing verify=self.verify_ssl to session.get so only this
instance’s requests have suppressed warnings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62792794-e9bd-4b55-bdf7-cf0756abda67

📥 Commits

Reviewing files that changed from the base of the PR and between 8255ec9 and b7b18d5.

📒 Files selected for processing (1)
  • powermeter/envoy.py

@CoRfr
Copy link
Copy Markdown
Author

CoRfr commented Mar 16, 2026

Addressed all CodeRabbit review feedback in b84cab4:

  1. Removed total-consumption fallback (envoy.py:132) — Now raises ValueError when net-consumption is unavailable, since total-consumption measures house load, not grid import/export.
  2. Filter empty DEVICE_IDS entries (main.py:207) — Added if did.strip() to handle trailing/double commas.
  3. Pad three-phase return (envoy.py:138) — Now slices to self.phases and pads with zeros if the Envoy returns fewer lines than configured phases.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
powermeter/envoy_test.py (1)

81-96: Add a regression test for short lines arrays with multi-phase config.

The new padding behavior in Envoy.get_powermeter_watts() should be explicitly asserted (e.g., phases=3 with only 1 line returns [x, 0, 0]).

Suggested test addition
 class TestEnvoyThreePhase(unittest.TestCase):
@@
     def test_get_powermeter_watts_three_phase(self, mock_session_class):
@@
         self.assertEqual(result, [-100, -80, -120])
+
+    `@patch`("powermeter.envoy.requests.Session")
+    def test_three_phase_pads_missing_lines(self, mock_session_class):
+        data = {
+            "production": [],
+            "consumption": [
+                {
+                    "measurementType": "net-consumption",
+                    "lines": [{"wNow": -150.0}],
+                }
+            ],
+        }
+        mock_response = MagicMock()
+        mock_response.json.return_value = data
+        mock_response.status_code = 200
+        mock_response.raise_for_status = MagicMock()
+        mock_session = MagicMock()
+        mock_session.get.return_value = mock_response
+        mock_session_class.return_value = mock_session
+
+        envoy = Envoy(host="192.168.1.200", token="test-token", phases=3)
+        self.assertEqual(envoy.get_powermeter_watts(), [-150, 0, 0])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@powermeter/envoy_test.py` around lines 81 - 96, Add a regression test in
envoy_test.py that exercises Envoy.get_powermeter_watts() with phases=3 and a
mocked API response whose "lines" array contains only a single entry; assert the
method returns a 3-element list where the first element is the measured value
and the remaining two elements are zeros (e.g., [x, 0, 0]). Use the existing
patch of requests.Session and MagicMock response setup like in
TestEnvoyThreePhase to return SAMPLE_RESPONSE modified to have a single-line
payload, and name the test to indicate it verifies padding behavior for
Envoy.get_powermeter_watts().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@powermeter/envoy.py`:
- Around line 74-90: Validate the phases parameter in the Envoy class __init__:
ensure phases is an integer greater than 0 and raise a ValueError with a clear
message if not (e.g., "phases must be a positive integer"); update the __init__
(constructor) to perform this check before assigning self.phases so invalid
configs fail fast and cannot produce malformed outputs.
- Around line 128-132: The code assumes data.get("consumption") is a list before
calling _find_measurement, which will raise if it's None or another type; update
the parsing after data = self._fetch() to validate and coerce the consumption
payload into a safe list (e.g., assign consumption_list =
data.get("consumption") and if it's not a list replace with []), then call
_find_measurement(consumption_list, "net-consumption") so net_meter handling is
robust against null or unexpected shapes.

---

Nitpick comments:
In `@powermeter/envoy_test.py`:
- Around line 81-96: Add a regression test in envoy_test.py that exercises
Envoy.get_powermeter_watts() with phases=3 and a mocked API response whose
"lines" array contains only a single entry; assert the method returns a
3-element list where the first element is the measured value and the remaining
two elements are zeros (e.g., [x, 0, 0]). Use the existing patch of
requests.Session and MagicMock response setup like in TestEnvoyThreePhase to
return SAMPLE_RESPONSE modified to have a single-line payload, and name the test
to indicate it verifies padding behavior for Envoy.get_powermeter_watts().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eea9ef88-9283-4534-bfac-56df79cd7d92

📥 Commits

Reviewing files that changed from the base of the PR and between b7b18d5 and 94cbfcf.

📒 Files selected for processing (3)
  • main.py
  • powermeter/envoy.py
  • powermeter/envoy_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • main.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ha_addon/run.sh`:
- Line 65: Update the sed masking command in run.sh that operates on "$CONFIG"
to accept optional whitespace around the equals sign so keys like "TOKEN =
value" are masked; adjust the regex used in the sed invocation that currently
targets (PASSWORD|TOKEN|ACCESSTOKEN|SECRET) to allow spaces around "=" and also
consider adding USERNAME to the matched keys for masking PII. Ensure the
modified sed still preserves case-insensitive matching and replaces the
right-hand side with a fixed mask while keeping the key name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8913ae65-8bce-4564-903f-05bb98cc9f5a

📥 Commits

Reviewing files that changed from the base of the PR and between 94cbfcf and 877a162.

📒 Files selected for processing (1)
  • ha_addon/run.sh

@CoRfr CoRfr force-pushed the feat-enphase-envoy branch from 877a162 to c727605 Compare March 16, 2026 18:13
@tomquist
Copy link
Copy Markdown
Owner

Thanks for the PR! This looks like a solid addition.

One thing — it looks like this is hitting the Enphase cloud API for meter reads. Have you considered using the documented local API instead? The Envoy exposes the same data directly on the LAN, which would avoid the cloud dependency and latency for every read and would continue to work if the internet connection cuts out.

According to the "Accessing IQ Gateway local APIs or UI using token" technical brief, there are several local endpoints that would work:

  • GET https://{envoy_ip}/ivp/meters/gridReading (Appendix J): returns per-phase active power at the grid connection point (negative = export, positive = import). This seems like the most direct fit.
  • /ivp/livedata/status (Appendix H): more real-time, has a grid section with per-phase milliwatt values.
  • /ivp/meters/readings (Appendix D): detailed CT measurements, but only updates every 5 minutes.

The cloud login would still be needed to obtain/renew the JWT token, but the actual meter reads could all happen locally. What do you think?

@tomquist tomquist changed the base branch from main to develop March 28, 2026 20:21
@CoRfr
Copy link
Copy Markdown
Author

CoRfr commented Apr 1, 2026

Thanks for the PR! This looks like a solid addition.

One thing — it looks like this is hitting the Enphase cloud API for meter reads. Have you considered using the documented local API instead? The Envoy exposes the same data directly on the LAN, which would avoid the cloud dependency and latency for every read and would continue to work if the internet connection cuts out.

According to the "Accessing IQ Gateway local APIs or UI using token" technical brief, there are several local endpoints that would work:

* `GET https://{envoy_ip}/ivp/meters/gridReading` (Appendix J): returns per-phase active power at the grid connection point (negative = export, positive = import). This seems like the most direct fit.

* `/ivp/livedata/status` (Appendix H): more real-time, has a `grid` section with per-phase milliwatt values.

* `/ivp/meters/readings` (Appendix D): detailed CT measurements, but only updates every 5 minutes.

The cloud login would still be needed to obtain/renew the JWT token, but the actual meter reads could all happen locally. What do you think?

Thanks for your suggestion, but, actually this is using the local API already for meter reading.
The cloud API is only used in obtain_token() and _refresh_token() to get/renew the JWT, which is needed to access the gateway locally, and then the actual power readings are all local (see _fetch()) to https://{self.host}/production.json?details=1

Reads grid power from the Envoy's local API (/production.json?details=1)
using net-consumption data. Supports single and three-phase reporting.

JWT token is obtained from the Enphase Enlighten cloud API and refreshed
automatically on 401. Actual meter reads are always local.

- New powermeter: src/b2500_meter/powermeter/envoy.py
- Config loader: ENVOY section with lazy import to avoid circular dependency
- config.ini.example: document ENVOY section parameters
- Unit tests for token flow, single/three-phase parsing, error handling

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@CoRfr CoRfr force-pushed the feat-enphase-envoy branch from f932036 to 9ccc3ef Compare April 1, 2026 19:37
@@ -0,0 +1,152 @@
from .base import Powermeter
import requests
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The codebase migrated to asyncio after this PR was opened. All powermeter I/O now uses aiohttp.ClientSession. synchronous requests will block the event loop. This affects both the local Envoy calls and the Enlighten token flow. See shelly.py for the pattern.

self.username = username
self.password = password
self.serial = serial
self.session = requests.Session()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

HTTP-based powermeters need async def start() / async def stop() lifecycle methods now. The session should be created in start() and closed in stop(). The main loop calls these. Creating it in __init__ skips that contract.

response.raise_for_status()
return response.json()

def get_powermeter_watts(self):
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This needs to be async def to match the base class signature (async def get_powermeter_watts(self) -> list[float]).

self.serial = serial
self.session = requests.Session()
if not verify_ssl:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This suppresses SSL warnings globally for the entire process, affecting all other HTTP clients. Needs to be scoped to this powermeter only.

)

if self.phases == 1:
return [int(net_meter.get("wNow", 0))]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: base class return type is list[float]. Other powermeters return floats. Consider using float() for consistency.



class TestEnvoySinglePhase(unittest.TestCase):
@patch("powermeter.envoy.requests.Session")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Import path is wrong, should be b2500_meter.powermeter.envoy.requests.Session. Same issue in all test patches throughout this file.

SHELLY_SECTION = "SHELLY"
TASMOTA_SECTION = "TASMOTA"
SHRDZM_SECTION = "SHRDZM"
ENVOY_SECTION = "ENVOY"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The __init__.py export is missing from this PR. Needs from .envoy import Envoy and an "Envoy" entry in __all__. Also missing a README configuration subsection.

"grid CTs are required"
)

if self.phases == 1:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Can we infer this from the response? For example, check if three lines are available; if not, fallback to single phase. I'm not sure if lines can be unavailable, but I see no reason to use the net power if individual phases are accessible.

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.

2 participants