Skip to content

Commit dd78b55

Browse files
authored
Merge pull request #279 from jasonacox/feat/cli-v1r-tedapi-v0.15.4
v0.15.4 - CLI Enhancements and Safety Guards
2 parents 9cdf51a + a8d7bf7 commit dd78b55

7 files changed

Lines changed: 185 additions & 33 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,12 @@ networkingMode=mirrored
124124
```
125125

126126
```bash
127-
# Test
128-
python3 -m pypowerwall tedapi
127+
# Test WiFi TEDAPI
128+
python3 -m pypowerwall tedapi -gw_pwd ABCDEXXXXX
129+
130+
# Test v1r LAN TEDAPI
131+
python3 -m pypowerwall tedapi -host 10.42.1.40 -v1r -gw_pwd ABCDEXXXXX \
132+
-rsa_key_path /path/to/tedapi_rsa_private.pem
129133
```
130134

131135
#### TEDAPI Troubleshooting

RELEASE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# RELEASE NOTES
22

3+
## v0.15.4 - CLI Enhancements and Safety Guards
4+
5+
* Feat: `pypowerwall tedapi` CLI now accepts `-host HOST`, `-gw_pwd GW_PWD`, `-v1r`, `-password PASSWORD`, `-rsa_key_path RSA_KEY_PATH`, and `-wifi_host WIFI_HOST` flags, enabling full PW3 wired LAN (v1r) access directly from the command line
6+
* Feat: `go_off_grid()` now requires `confirm=True` to prevent accidental islanding — calling without the flag logs an error and returns `None`
7+
* Fix: Unsupported-method error logs in `go_off_grid()` and `reconnect_grid()` now include the backend class name for easier diagnosis
8+
* Add: Unit tests for `go_off_grid()` confirm guard and CLI v1r argument forwarding/password derivation
9+
* Release prep:
10+
* Bump library version to `0.15.4`
11+
* Update proxy pinned dependency to `pypowerwall==0.15.4`
12+
313
## v0.15.3 - PW3 No-Solar None Handling
414

515
* Update scanner to use cidr by @Nexarian in https://github.com/jasonacox/pypowerwall/pull/266

proxy/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
pypowerwall==0.15.3
1+
pypowerwall==0.15.4
22
bs4==0.0.2

pypowerwall/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
from json import JSONDecodeError
9090
from typing import Optional, Union
9191

92-
version_tuple = (0, 15, 3)
92+
version_tuple = (0, 15, 4)
9393
version = __version__ = '%d.%d.%d' % version_tuple
9494
__author__ = 'jasonacox'
9595

pypowerwall/__main__.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ def main():
4242
fleetapi_args = subparsers.add_parser("fleetapi", help='Setup Tesla FleetAPI for Cloud Mode access')
4343

4444
tedapi_args = subparsers.add_parser("tedapi", help='Test TEDAPI connection to Powerwall Gateway')
45+
tedapi_args.add_argument("gw_pwd", type=str, nargs="?", default=None,
46+
help="Powerwall Gateway Password")
47+
tedapi_args.add_argument("-gw_pwd", dest="gw_pwd_option", metavar="GW_PWD", type=str, default=None,
48+
help="Powerwall Gateway Password")
49+
tedapi_args.add_argument("-host", type=str, default=None,
50+
help="IP address of Powerwall Gateway")
51+
tedapi_args.add_argument("-v1r", action="store_true", default=False,
52+
help="Use v1r LAN TEDAPI mode")
53+
tedapi_args.add_argument("-password", type=str, default=None,
54+
help="Customer password for v1r mode (defaults to last 5 of gw_pwd)")
55+
tedapi_args.add_argument("-rsa_key_path", type=str, default=None,
56+
help="Path to RSA private key PEM for v1r mode")
57+
tedapi_args.add_argument("-wifi_host", type=str, default=None,
58+
help="Optional WiFi TEDAPI host for v1r follower fallback")
4559

4660
register_args = subparsers.add_parser("register",
4761
help='Register RSA key with Powerwall via Tesla Owner API or Fleet API (for v1r LAN mode)')
@@ -145,7 +159,23 @@ def main():
145159
# TEDAPI Test
146160
elif command == 'tedapi':
147161
from pypowerwall.tedapi.__main__ import run_tedapi_test
148-
run_tedapi_test(auto=True, debug=args.debug)
162+
tedapi_argv = []
163+
gw_pwd = args.gw_pwd_option or args.gw_pwd
164+
if gw_pwd:
165+
tedapi_argv.extend(['-gw_pwd', gw_pwd])
166+
if args.host:
167+
tedapi_argv.extend(['-host', args.host])
168+
if args.v1r:
169+
tedapi_argv.append('-v1r')
170+
if args.password:
171+
tedapi_argv.extend(['-password', args.password])
172+
if args.rsa_key_path:
173+
tedapi_argv.extend(['-rsa_key_path', args.rsa_key_path])
174+
if args.wifi_host:
175+
tedapi_argv.extend(['-wifi_host', args.wifi_host])
176+
if args.debug:
177+
tedapi_argv.append('--debug')
178+
run_tedapi_test(argv=tedapi_argv, debug=args.debug)
149179

150180
# Fleet API RSA Key Registration (v1r LAN mode)
151181
elif command == 'register':

pypowerwall/tedapi/__main__.py

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,39 @@
11
# pyPowerwall - Tesla TEDAPI Class Main
22
# -*- coding: utf-8 -*-
33
"""
4-
Tesla TEADAPI Class - Command Line Test
5-
4+
Tesla TEDAPI Class - Command Line Test
5+
66
This script tests the TEDAPI class by connecting to a Tesla Powerwall Gateway
77
"""
88

9-
def run_tedapi_test(auto=False, debug=False):
9+
10+
def _build_tedapi_arg_parser(default_host):
11+
"""Build the CLI parser used by the TEDAPI test command."""
12+
import argparse
13+
14+
parser = argparse.ArgumentParser(description='Tesla Powerwall Gateway TEDAPI Reader')
15+
parser.add_argument('gw_pwd', nargs='?', help='Powerwall Gateway Password')
16+
parser.add_argument('-gw_pwd', dest='gw_pwd_option', metavar='GW_PWD', default=None,
17+
help='Powerwall Gateway Password')
18+
parser.add_argument('-host', '--gw_ip', dest='host', default=default_host,
19+
help='Powerwall Gateway IP Address')
20+
parser.add_argument('-v1r', action='store_true', help='Use v1r LAN TEDAPI mode')
21+
parser.add_argument('-password', default=None,
22+
help='Customer password for v1r mode (defaults to last 5 of gw_pwd)')
23+
parser.add_argument('-rsa_key_path', default=None,
24+
help='Path to RSA private key PEM for v1r mode')
25+
parser.add_argument('-wifi_host', default=None,
26+
help='Optional WiFi TEDAPI host for v1r follower fallback')
27+
parser.add_argument('--debug', action='store_true', help='Enable Debug Output')
28+
return parser
29+
30+
31+
def run_tedapi_test(argv=None, debug=False):
1032
# Imports
1133
from pypowerwall.tedapi import TEDAPI, GW_IP
1234
from pypowerwall import __version__
1335
import json
1436
import sys
15-
import argparse
1637
import requests
1738
import logging
1839

@@ -35,30 +56,29 @@ def set_debug(toggle=True, color=True):
3556
else:
3657
log.setLevel(logging.NOTSET)
3758

38-
# Load arguments if invoked from pypowerwall
39-
if auto:
40-
argv = ['pypowerwall']
41-
if debug:
42-
argv.append('--debug')
43-
sys.argv = argv
44-
4559
# Check for arguments using argparse
46-
parser = argparse.ArgumentParser(description='Tesla Powerwall Gateway TEDAPI Reader')
47-
parser.add_argument('gw_pwd', nargs='?', help='Powerwall Gateway Password')
48-
parser.add_argument('--gw_ip', default=GW_IP, help='Powerwall Gateway IP Address')
49-
parser.add_argument('--debug', action='store_true', help='Enable Debug Output')
50-
# Parse arguments
51-
args = parser.parse_args()
52-
if args.gw_pwd:
53-
gw_pwd = args.gw_pwd
54-
else:
55-
gw_pwd = None
60+
parser = _build_tedapi_arg_parser(GW_IP)
61+
args = parser.parse_args(argv)
62+
gw_pwd = args.gw_pwd_option or args.gw_pwd
5663
if args.debug:
5764
set_debug(True)
58-
GW_IP = args.gw_ip
65+
elif debug:
66+
set_debug(True)
67+
host = args.host
68+
69+
if args.v1r:
70+
if not args.rsa_key_path:
71+
parser.error('-v1r requires -rsa_key_path')
72+
if not args.password and not gw_pwd:
73+
parser.error('-v1r requires -password or -gw_pwd')
74+
password = args.password or gw_pwd[-5:]
75+
if gw_pwd is None:
76+
gw_pwd = ""
77+
else:
78+
password = None
5979

60-
# Check that GW_IP is listening to port 443
61-
url = f'https://{GW_IP}'
80+
# Check that host is listening to port 443
81+
url = f'https://{host}'
6282
log.debug(f"Checking Powerwall Gateway at {url}")
6383
print(f" - Connecting to {url}...", end="")
6484
try:
@@ -68,8 +88,8 @@ def set_debug(toggle=True, color=True):
6888
except Exception as e:
6989
print(" FAILED")
7090
print()
71-
print(f"ERROR: Unable to connect to Powerwall Gateway {GW_IP} on port 443.")
72-
print("Please verify your your host has a route to the Gateway.")
91+
print(f"ERROR: Unable to connect to Powerwall Gateway {host} on port 443.")
92+
print("Please verify your host has a route to the Gateway.")
7393
print(f"\nError details: {e}")
7494
sys.exit(1)
7595

@@ -89,8 +109,13 @@ def set_debug(toggle=True, color=True):
89109

90110
# Create TEDAPI Object and get Configuration and Status
91111
print()
92-
print(f"Connecting to Powerwall Gateway {GW_IP}")
93-
ted = TEDAPI(gw_pwd, host=GW_IP)
112+
print(f"Connecting to Powerwall Gateway {host}")
113+
if args.v1r:
114+
ted = TEDAPI(gw_pwd=gw_pwd or "", host=host, v1r=True,
115+
password=password, rsa_key_path=args.rsa_key_path,
116+
wifi_host=args.wifi_host)
117+
else:
118+
ted = TEDAPI(gw_pwd, host=host)
94119
if ted.din is None:
95120
print("\nERROR: Unable to connect to Powerwall Gateway. Check your password and try again")
96121
sys.exit(1)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
4+
def test_main_forwards_tedapi_v1r_args():
5+
from pypowerwall.__main__ import main
6+
7+
argv = [
8+
'pypowerwall', 'tedapi',
9+
'-host', '10.42.1.40',
10+
'-v1r',
11+
'-gw_pwd', 'ABCDEXXXXX',
12+
'-rsa_key_path', '/tmp/test.pem',
13+
]
14+
15+
with patch('sys.argv', argv), \
16+
patch('pypowerwall.tedapi.__main__.run_tedapi_test') as mock_run:
17+
main()
18+
19+
mock_run.assert_called_once_with(
20+
argv=['-gw_pwd', 'ABCDEXXXXX', '-host', '10.42.1.40', '-v1r', '-rsa_key_path', '/tmp/test.pem'],
21+
debug=False,
22+
)
23+
24+
25+
def test_run_tedapi_test_v1r_derives_password_from_gw_pwd(tmp_path, monkeypatch):
26+
from pypowerwall.tedapi.__main__ import run_tedapi_test
27+
28+
mock_ted = MagicMock()
29+
mock_ted.din = 'DIN123'
30+
mock_ted.get_config.return_value = {}
31+
mock_ted.get_status.return_value = {}
32+
monkeypatch.chdir(tmp_path)
33+
34+
with patch('requests.get') as mock_get, \
35+
patch('pypowerwall.tedapi.TEDAPI', return_value=mock_ted) as mock_tedapi:
36+
mock_get.return_value.status_code = 200
37+
run_tedapi_test([
38+
'-host', '10.42.1.40',
39+
'-v1r',
40+
'-gw_pwd', 'ABCDEXXXXX',
41+
'-rsa_key_path', '/tmp/test.pem',
42+
])
43+
44+
mock_tedapi.assert_called_once_with(
45+
gw_pwd='ABCDEXXXXX',
46+
host='10.42.1.40',
47+
v1r=True,
48+
password='XXXXX',
49+
rsa_key_path='/tmp/test.pem',
50+
wifi_host=None,
51+
)
52+
53+
54+
def test_run_tedapi_test_v1r_password_only_no_gw_pwd(tmp_path, monkeypatch):
55+
"""v1r with -password but no -gw_pwd must not hang on interactive input."""
56+
from pypowerwall.tedapi.__main__ import run_tedapi_test
57+
58+
mock_ted = MagicMock()
59+
mock_ted.din = 'DIN123'
60+
mock_ted.get_config.return_value = {}
61+
mock_ted.get_status.return_value = {}
62+
monkeypatch.chdir(tmp_path)
63+
64+
with patch('requests.get') as mock_get, \
65+
patch('pypowerwall.tedapi.TEDAPI', return_value=mock_ted) as mock_tedapi, \
66+
patch('builtins.input') as mock_input:
67+
mock_get.return_value.status_code = 200
68+
run_tedapi_test([
69+
'-host', '10.42.1.40',
70+
'-v1r',
71+
'-password', 'mypass',
72+
'-rsa_key_path', '/tmp/test.pem',
73+
])
74+
75+
mock_input.assert_not_called()
76+
mock_tedapi.assert_called_once_with(
77+
gw_pwd='',
78+
host='10.42.1.40',
79+
v1r=True,
80+
password='mypass',
81+
rsa_key_path='/tmp/test.pem',
82+
wifi_host=None,
83+
)

0 commit comments

Comments
 (0)