-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
124 lines (104 loc) · 4.08 KB
/
Copy pathcli.py
File metadata and controls
124 lines (104 loc) · 4.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Pure-function CLI helpers for dbus-evcc.py.
Kept separate so they can be unit-tested on macOS without importing the
hyphenated dbus-evcc.py entry script (which pulls dbus + gi at runtime).
"""
from __future__ import annotations
import argparse
import configparser
import ipaddress
from pathlib import Path
from typing import NamedTuple
class Settings(NamedTuple):
host: str
poll_seconds: int
di_lo: int
di_hi: int
def parse_args(argv):
parser = argparse.ArgumentParser(
prog="dbus-evcc-multi",
description="Auto-discovery bridge from EVCC to Victron D-Bus.",
)
parser.add_argument(
"--debug", action="store_true", help="DEBUG log level (default INFO)",
)
parser.add_argument(
"--config", default=None,
help="Path to config.ini (default: next to this script)",
)
return parser.parse_args(argv)
def read_config(path: Path) -> configparser.ConfigParser:
cp = configparser.ConfigParser()
if path.exists():
cp.read(path)
return cp
def resolve_settings(cp: configparser.ConfigParser) -> Settings:
host = cp.get("ONPREMISE", "Host", fallback="").strip()
poll_s = cp.getint("DEFAULT", "PollSeconds", fallback=15)
di_lo = cp.getint("DEFAULT", "DeviceInstanceRangeStart", fallback=40)
di_hi = cp.getint("DEFAULT", "DeviceInstanceRangeEnd", fallback=59)
if poll_s < 1:
raise ValueError("PollSeconds must be >= 1, got %d" % poll_s)
if di_lo > di_hi:
raise ValueError(
"DeviceInstanceRangeStart (%d) > End (%d)" % (di_lo, di_hi)
)
if di_lo < 0 or di_hi > 255:
raise ValueError(
"DeviceInstance range must lie within [0, 255]; got [%d, %d]"
% (di_lo, di_hi)
)
return Settings(host=host, poll_seconds=poll_s, di_lo=di_lo, di_hi=di_hi)
class TunnelSettings(NamedTuple):
enabled: bool
advertise_ip: str
evcc_target: str # "host:port" the rewrite proxy forwards to
proxy_port: int
def _is_loopback(ip: str) -> bool:
try:
return ipaddress.ip_address(ip).is_loopback
except ValueError:
# Not a literal IP (e.g. a hostname). Loopback check does not apply.
return False
def resolve_tunnel_settings(cp: configparser.ConfigParser) -> TunnelSettings:
enabled = cp.getboolean("VRM_TUNNEL", "Enabled", fallback=False)
advertise_ip = cp.get("VRM_TUNNEL", "AdvertiseIp", fallback="").strip()
evcc_target = cp.get(
"VRM_TUNNEL", "EvccTarget",
fallback="127.0.0.1:7070", # proxy -> local EVCC default
).strip()
proxy_port = cp.getint("VRM_TUNNEL", "ProxyPort", fallback=8099)
if enabled:
if not advertise_ip:
raise ValueError(
"VRM_TUNNEL enabled but AdvertiseIp is empty - set it to the "
"non-loopback IP VRM should tunnel to."
)
if _is_loopback(advertise_ip):
raise ValueError(
"AdvertiseIp must be a non-loopback IP (got %s); a loopback "
"address would require the fake-Modbus fallback which is out "
"of scope. Use the Cerbo LAN IP (on-Cerbo) or the EVCC host "
"IP (remote)." % advertise_ip
)
host, sep, port = evcc_target.rpartition(":")
if not sep or not host or not port.isdigit():
raise ValueError(
"EvccTarget must be host:port (got %r)" % evcc_target
)
if not (1 <= proxy_port <= 65535):
raise ValueError(
"ProxyPort must be in 1..65535, got %d" % proxy_port
)
return TunnelSettings(
enabled=enabled,
advertise_ip=advertise_ip,
evcc_target=evcc_target,
proxy_port=proxy_port,
)
def mgmt_connection_string(tunnel: TunnelSettings) -> str:
"""The /Mgmt/Connection value each loadpoint advertises. With the tunnel
on, the 'Modbus TCP <ip>' prefix is what makes generate_authorized_keys.sh
whitelist <ip>:80 so the VRM 'Control panel' button works."""
if tunnel.enabled:
return "Modbus TCP %s" % tunnel.advertise_ip
return "EVCC REST API"