SolarManager unter Versionsverwaltung

Erster Stand der Hintergrundprozesse, die auf der Synology unter
/volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler
je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der
AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins
Web-Verzeichnis.

Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die
nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie
von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot),
zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das
Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat.

Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py,
gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso
gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte.

Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein
pip, sie muessen neben den Skripten liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 20:46:59 +02:00
co-authored by Claude Opus 5
commit 79843aa2ae
968 changed files with 261182 additions and 0 deletions
@@ -0,0 +1,257 @@
import base64
import hashlib
import re
import socketserver
from collections import OrderedDict
import paho.mqtt.client as client
import pytest
from paho.mqtt.client import WebsocketConnectionError
from tests.testsupport.broker import fake_websocket_broker # noqa: F401
@pytest.fixture
def init_response_headers():
# "Normal" websocket response from server
response_headers = OrderedDict([
("Upgrade", "websocket"),
("Connection", "Upgrade"),
("Sec-WebSocket-Accept", "testwebsocketkey"),
("Sec-WebSocket-Protocol", "chat"),
])
return response_headers
def get_websocket_response(response_headers):
""" Takes headers and constructs HTTP response
'HTTP/1.1 101 Switching Protocols' is the headers for the response,
as expected in client.py
"""
response = "\r\n".join([
"HTTP/1.1 101 Switching Protocols",
"\r\n".join(f"{i}: {j}" for i, j in response_headers.items()),
"\r\n",
]).encode("utf8")
return response
@pytest.mark.parametrize("proto_ver,proto_name", [
(client.MQTTv31, "MQIsdp"),
(client.MQTTv311, "MQTT"),
])
class TestInvalidWebsocketResponse:
def test_unexpected_response(self, proto_ver, proto_name, fake_websocket_broker):
""" Server responds with a valid code, but it's not what the client expected """
mqttc = client.Client(
client.CallbackAPIVersion.VERSION1,
"test_unexpected_response",
protocol=proto_ver,
transport="websockets"
)
class WebsocketHandler(socketserver.BaseRequestHandler):
def handle(_self):
# Respond with data passed in to serve()
_self.request.sendall(b"200 OK")
with fake_websocket_broker.serve(WebsocketHandler), pytest.raises(WebsocketConnectionError) as exc:
mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10)
assert str(exc.value) == "WebSocket handshake error"
@pytest.mark.parametrize("proto_ver,proto_name", [
(client.MQTTv31, "MQIsdp"),
(client.MQTTv311, "MQTT"),
])
class TestBadWebsocketHeaders:
""" Testing for basic functionality in checking for headers """
def _get_basic_handler(self, response_headers):
""" Get a basic BaseRequestHandler which returns the information in
self._response_headers
"""
response = get_websocket_response(response_headers)
class WebsocketHandler(socketserver.BaseRequestHandler):
def handle(_self):
self.data = _self.request.recv(1024).strip()
print('Received', self.data.decode('utf8'))
# Respond with data passed in to serve()
_self.request.sendall(response)
return WebsocketHandler
def test_no_upgrade(self, proto_ver, proto_name, fake_websocket_broker,
init_response_headers):
""" Server doesn't respond with 'connection: upgrade' """
mqttc = client.Client(
client.CallbackAPIVersion.VERSION1,
"test_no_upgrade",
protocol=proto_ver,
transport="websockets"
)
init_response_headers["Connection"] = "bad"
response = self._get_basic_handler(init_response_headers)
with fake_websocket_broker.serve(response), pytest.raises(WebsocketConnectionError) as exc:
mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10)
assert str(exc.value) == "WebSocket handshake error, connection not upgraded"
def test_bad_secret_key(self, proto_ver, proto_name, fake_websocket_broker,
init_response_headers):
""" Server doesn't give anything after connection: upgrade """
mqttc = client.Client(
client.CallbackAPIVersion.VERSION1,
"test_bad_secret_key",
protocol=proto_ver,
transport="websockets"
)
response = self._get_basic_handler(init_response_headers)
with fake_websocket_broker.serve(response), pytest.raises(WebsocketConnectionError) as exc:
mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10)
assert str(exc.value) == "WebSocket handshake error, invalid secret key"
@pytest.mark.parametrize("proto_ver,proto_name", [
(client.MQTTv31, "MQIsdp"),
(client.MQTTv311, "MQTT"),
])
class TestValidHeaders:
""" Testing for functionality in request/response headers """
def _get_callback_handler(self, response_headers, check_request=None):
""" Get a basic BaseRequestHandler which returns the information in
self._response_headers
"""
class WebsocketHandler(socketserver.BaseRequestHandler):
def handle(_self):
self.data = _self.request.recv(1024).strip()
print('Received', self.data.decode('utf8'))
decoded = self.data.decode("utf8")
if check_request is not None:
check_request(decoded)
# Create server hash
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
key = re.search("sec-websocket-key: ([A-Za-z0-9+/=]*)", decoded, re.IGNORECASE).group(1)
to_hash = f"{key:s}{GUID:s}"
hashed = hashlib.sha1(to_hash.encode("utf8")) # noqa: S324
encoded = base64.b64encode(hashed.digest()).decode("utf8")
response_headers["Sec-WebSocket-Accept"] = encoded
# Respond with the correct hash
response = get_websocket_response(response_headers)
_self.request.sendall(response)
return WebsocketHandler
def test_successful_connection(self, proto_ver, proto_name,
fake_websocket_broker,
init_response_headers):
""" Connect successfully, on correct path """
mqttc = client.Client(
client.CallbackAPIVersion.VERSION1,
"test_successful_connection",
protocol=proto_ver,
transport="websockets"
)
response = self._get_callback_handler(init_response_headers)
with fake_websocket_broker.serve(response):
mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10)
mqttc.disconnect()
@pytest.mark.parametrize("mqtt_path", [
"/mqtt"
"/special",
None,
])
def test_correct_path(self, proto_ver, proto_name, fake_websocket_broker,
mqtt_path, init_response_headers):
""" Make sure it can connect on user specified paths """
mqttc = client.Client(
client.CallbackAPIVersion.VERSION1,
"test_correct_path",
protocol=proto_ver,
transport="websockets"
)
mqttc.ws_set_options(
path=mqtt_path,
)
def check_path_correct(decoded):
# Make sure it connects to the right path
if mqtt_path:
assert re.search(f"GET {mqtt_path} HTTP/1.1", decoded, re.IGNORECASE) is not None
response = self._get_callback_handler(
init_response_headers,
check_request=check_path_correct,
)
with fake_websocket_broker.serve(response):
mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10)
mqttc.disconnect()
@pytest.mark.parametrize("auth_headers", [
{"Authorization": "test123"},
{"Authorization": "test123", "auth2": "abcdef"},
# Won't be checked, but make sure it still works even if the user passes it
None,
])
def test_correct_auth(self, proto_ver, proto_name, fake_websocket_broker,
auth_headers, init_response_headers):
""" Make sure it sends the right auth headers """
mqttc = client.Client(
client.CallbackAPIVersion.VERSION1,
"test_correct_path",
protocol=proto_ver,
transport="websockets"
)
mqttc.ws_set_options(
headers=auth_headers,
)
def check_headers_used(decoded):
# Make sure it connects to the right path
if auth_headers:
for k, v in auth_headers.items():
assert f"{k}: {v}" in decoded
response = self._get_callback_handler(
init_response_headers,
check_request=check_headers_used,
)
with fake_websocket_broker.serve(response):
mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10)
mqttc.disconnect()