unny-Portal-Login automatisieren und Fork dokumentieren
- Portal-URL und Pfade für Zugangsdaten und Cookies fest hinterlegen - SMA-Anmeldung mit Zugangsdaten aus JSON automatisieren - Cookies dauerhaft speichern und Demo-Funktionen entfernen - Startskript in sma-screensaver umbenennen - README mit Einrichtung und Testanleitung ergänzen - Fork-Hinweise ergänzen und sensible Dateien ignorieren
This commit is contained in:
+266
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
SMA-Screensaver - Sunny Portal screensaver
|
||||
Fork of WebScreensaver: https://github.com/lmartinking/webscreensaver
|
||||
Fork repository: https://gitea.diehanis.de/Winz/SMA-Screensaver
|
||||
Modified 2026-09-29: fixed Sunny Portal URL, automatic SMA login,
|
||||
local JSON credentials and persistent cookies; removed demo selection.
|
||||
Copyright (C) 2012-2017 Lucas Martin-King & Thomas Reifenberger
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
"""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import json
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('GdkX11', '3.0')
|
||||
gi.require_version('WebKit2', '4.1')
|
||||
|
||||
from gi.repository import Gtk, GdkX11
|
||||
from gi.repository import WebKit2 as WebKit
|
||||
|
||||
|
||||
SUNNY_PORTAL_URL = "https://ennexos.sunnyportal.com/login"
|
||||
CREDENTIALS_FILE = Path.home() / ".config/sma-screensaver/credentials.json"
|
||||
COOKIE_FILE = Path.home() / ".local/share/sma-screensaver/cookies.txt"
|
||||
|
||||
SMA_LOGIN_SCRIPT = """
|
||||
(credentials => {
|
||||
if (location.origin !== 'https://login.sma.energy') return false;
|
||||
const username = document.getElementById('username');
|
||||
const password = document.getElementById('password');
|
||||
const form = password && password.form;
|
||||
const button = form && form.querySelector('button[name="login"][type="submit"]');
|
||||
if (!username || !form || username.form !== form || !button || button.disabled ||
|
||||
new URL(form.action, location.href).origin !== location.origin) return false;
|
||||
|
||||
for (const [field, value] of [[username, credentials.username], [password, credentials.password]]) {
|
||||
field.value = value;
|
||||
field.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
field.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
if (!form.checkValidity()) return false;
|
||||
button.click();
|
||||
return true;
|
||||
})
|
||||
"""
|
||||
|
||||
|
||||
def load_credentials():
|
||||
try:
|
||||
credentials = json.loads(CREDENTIALS_FILE.read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
raise ValueError(f"Zugangsdaten fehlen oder JSON ist nicht lesbar: {CREDENTIALS_FILE}") from None
|
||||
if not isinstance(credentials, dict) or any(
|
||||
not isinstance(credentials.get(key), str) or not credentials[key].strip()
|
||||
for key in ('username', 'password')
|
||||
):
|
||||
raise ValueError('credentials.json muss username und password als nicht leere Zeichenketten enthalten.')
|
||||
return {key: credentials[key] for key in ('username', 'password')}
|
||||
|
||||
# The portal renders its login button asynchronously through Angular.
|
||||
PORTAL_LOGIN_SCRIPT = """
|
||||
(() => {
|
||||
if (location.origin !== 'https://ennexos.sunnyportal.com' ||
|
||||
location.pathname.replace(/\\/$/, '') !== '/login') return;
|
||||
|
||||
let attempts = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (location.pathname.replace(/\\/$/, '') !== '/login') {
|
||||
clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
const button = document.querySelector('ennexos-button#login button, button#login');
|
||||
if (button && !button.disabled &&
|
||||
button.getAttribute('aria-disabled') !== 'true' &&
|
||||
button.getClientRects().length > 0) {
|
||||
clearInterval(timer);
|
||||
button.click();
|
||||
} else if (++attempts >= 120) {
|
||||
clearInterval(timer);
|
||||
console.warn('Sunny Portal: login button unavailable after 60 seconds.');
|
||||
}
|
||||
}, 500);
|
||||
})();
|
||||
"""
|
||||
|
||||
|
||||
class WebScreensaver(object):
|
||||
'''
|
||||
A simple wrapper for WebKit which works as an XScreensaver Hack
|
||||
'''
|
||||
|
||||
def __init__(self, window_id=None, disk_cache=True):
|
||||
self.window_id = window_id
|
||||
self.disk_cache = disk_cache
|
||||
self.login_attempted = False
|
||||
|
||||
self.w = 640
|
||||
self.h = 480
|
||||
|
||||
def setup_window(self):
|
||||
'''Perform some magic (if needed) to set up a Gtk window'''
|
||||
if self.window_id:
|
||||
self.win = Gtk.Window(Gtk.WindowType.POPUP)
|
||||
|
||||
gdk_display = GdkX11.X11Display.get_default()
|
||||
self.gdk_win = GdkX11.X11Window.foreign_new_for_display(gdk_display, self.window_id)
|
||||
|
||||
# We show the window so we get a Gdk Window,
|
||||
# then we we can reparent it...
|
||||
self.win.show()
|
||||
self.win.get_window().reparent(self.gdk_win, 0, 0)
|
||||
|
||||
x, y, w, h = self.gdk_win.get_geometry()
|
||||
|
||||
# Make us cover our parent window
|
||||
self.win.move(0, 0)
|
||||
self.win.set_default_size(w, h)
|
||||
self.win.set_size_request(w, h)
|
||||
|
||||
self.w, self.h = w, h
|
||||
else:
|
||||
self.win = Gtk.Window()
|
||||
self.win.set_default_size(self.w, self.h)
|
||||
|
||||
def setup_browser(self):
|
||||
'''Sets up WebKit in our window'''
|
||||
self.browser = WebKit.WebView()
|
||||
self.browser.connect('load-changed', self.handle_login)
|
||||
self.browser.get_user_content_manager().add_script(WebKit.UserScript.new(
|
||||
PORTAL_LOGIN_SCRIPT,
|
||||
WebKit.UserContentInjectedFrames.TOP_FRAME,
|
||||
WebKit.UserScriptInjectionTime.END,
|
||||
None,
|
||||
None,
|
||||
))
|
||||
|
||||
settings = self.browser.get_settings()
|
||||
|
||||
# Try to enable webgl
|
||||
try:
|
||||
settings.set_enable_webgl(True)
|
||||
except Exception as err:
|
||||
print("Could not enable WebGL: {}".format(err))
|
||||
|
||||
# Enable disk caching
|
||||
if self.disk_cache:
|
||||
context = self.browser.get_context()
|
||||
data_manager = context.get_website_data_manager()
|
||||
context.set_cache_model(WebKit.CacheModel.WEB_BROWSER)
|
||||
print("Cache directory:", data_manager.props.disk_cache_directory)
|
||||
|
||||
# Take a stab at guessing whether we are running in the
|
||||
# XScreensaver preview window...
|
||||
if self.w < 320 and self.h < 240:
|
||||
self.browser.set_zoom_level(0.4)
|
||||
|
||||
self.browser.set_size_request(self.w, self.h)
|
||||
|
||||
def handle_login(self, view, event):
|
||||
if event != WebKit.LoadEvent.FINISHED or self.login_attempted:
|
||||
return
|
||||
uri = urlsplit(view.get_uri() or '')
|
||||
if uri.scheme != 'https' or uri.netloc != 'login.sma.energy':
|
||||
return
|
||||
# Never resubmit automatically after a rejected login or an MFA challenge.
|
||||
self.login_attempted = True
|
||||
try:
|
||||
credentials = load_credentials()
|
||||
except ValueError as error:
|
||||
print(error)
|
||||
return
|
||||
script = SMA_LOGIN_SCRIPT + '(' + json.dumps(credentials) + ');'
|
||||
view.run_javascript(script, None, self.handle_login_result)
|
||||
|
||||
def handle_login_result(self, view, result):
|
||||
try:
|
||||
submitted = view.run_javascript_finish(result).get_js_value().to_boolean()
|
||||
except Exception:
|
||||
print('Automatischer SMA-Login konnte nicht bestätigt werden.')
|
||||
return
|
||||
if submitted:
|
||||
print('SMA-Anmeldeformular automatisch abgesendet.')
|
||||
else:
|
||||
print('Kein verwendbares SMA-Anmeldeformular gefunden; bitte im Fenster prüfen.')
|
||||
|
||||
def setup_cookie_jar(self):
|
||||
COOKIE_FILE.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
COOKIE_FILE.touch(mode=0o600, exist_ok=True)
|
||||
COOKIE_FILE.chmod(0o600)
|
||||
context = self.browser.get_context()
|
||||
cookie_manager = context.get_cookie_manager()
|
||||
cookie_manager.set_accept_policy(WebKit.CookieAcceptPolicy.ALWAYS)
|
||||
cookie_manager.set_persistent_storage(str(COOKIE_FILE), WebKit.CookiePersistentStorage.TEXT)
|
||||
|
||||
def setup_layout(self):
|
||||
'''Make sure the browser can expand without affecting the window'''
|
||||
sw = Gtk.Layout()
|
||||
sw.put(self.browser, 0, 0)
|
||||
self.win.add(sw)
|
||||
|
||||
def setup(self):
|
||||
'''Do all the things!'''
|
||||
self.setup_window()
|
||||
self.setup_browser()
|
||||
self.setup_layout()
|
||||
self.setup_cookie_jar()
|
||||
|
||||
def terminate(*args):
|
||||
Gtk.main_quit()
|
||||
|
||||
self.win.connect('destroy', terminate)
|
||||
self.win.connect('delete-event', terminate)
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
signal.signal(signal.SIGTERM, terminate)
|
||||
|
||||
self.win.show_all()
|
||||
|
||||
self.browser.load_uri(SUNNY_PORTAL_URL)
|
||||
|
||||
@classmethod
|
||||
def determine_window_id(cls, win_id=None):
|
||||
'''Try and get an XID to use as our parent window'''
|
||||
if not win_id:
|
||||
win_id = os.getenv('XSCREENSAVER_WINDOW')
|
||||
|
||||
if win_id:
|
||||
win_id = int(win_id, 16)
|
||||
|
||||
return win_id
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='SMA-Screensaver: Sunny Portal screensaver (fork of WebScreensaver)')
|
||||
parser.add_argument('-window-id', '--window-id', help='XID of Window to draw on')
|
||||
parser.add_argument('-no-cache', action='store_true', help='Disable disk caching of website data')
|
||||
args = parser.parse_args()
|
||||
|
||||
saver = WebScreensaver(
|
||||
window_id=WebScreensaver.determine_window_id(args.window_id),
|
||||
disk_cache=not args.no_cache
|
||||
)
|
||||
saver.setup()
|
||||
|
||||
Gtk.main()
|
||||
Reference in New Issue
Block a user