fsantini / python-e3dc

Python API for querying E3/DC systems through the manufacturer's portal
MIT License
71 stars 22 forks source link

problem when using with briefcase to make an android app to show data && problem running in Termux on android #119

Open eltoro0815 opened 2 weeks ago

eltoro0815 commented 2 weeks ago

Hi:)

There is a popular python library briefcase.

You can make all Sort of Apps with python code based on this library.

Today I tried to make an Android app:

"""
shows actual data from your e3dc system
"""

import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW

from e3dc import E3DC

USERNAME = 'user@domain.com'
PASS = 'md5hashofpassword'
SERIALNUMBER = 'S10-xxxxxxxxxxxxxxxxxx'
CONFIG = {}

class eltoro0815e3dcapp(toga.App):

    def startup(self):

        main_box = toga.Box(style=Pack(direction=COLUMN))

        label = toga.Label("App gestartet.",
                           style=Pack(padding=10, font_size=14))

        def show_data(*args):
            e3dc_obj = E3DC(E3DC.CONNECT_WEB,
                        username=USERNAME,
                        password=PASS,
                        serialNumber=SERIALNUMBER,
                        isPasswordMd5=False,
                        configuration=CONFIG)
            label.text = "Warten auf Antwort ..."
            yield 0.1  # needed to redraw the label

            data = e3dc_obj.poll(keepAlive=True)

            label.text = f"""Hausverbrauch: {data['consumption']['house']}
Wallbox: {data['consumption']['wallbox']}
Batterie: {data['consumption']['battery']}
Battery %: {data['stateOfCharge']}%
PV Wechselrichter: {data['production']['solar']}
ext. Quelle: {data['production']['add']}
Netz: {data['production']['grid']}
"""

        button = toga.Button(
            "Status abrufen",
            on_press=show_data,
            style=Pack(padding=5),
        )

        main_box.add(button)
        main_box.add(label)

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.main_window.show()

def main():
    return eltoro0815e3dcapp()

When I run this on an android device the app hangs if I push the button. The following line causes this.

e3dc_obj = E3DC(E3DC.CONNECT_WEB,
                        username=USERNAME,
                        password=PASS,
                        serialNumber=SERIALNUMBER,
                        isPasswordMd5=False,
                        configuration=CONFIG)

Seems like there is a problem with some network traffic.

Are there known limitations on this library caused by network limitations?

eltoro0815 commented 2 weeks ago

I tried to run a minimal version in termux on my android phone. test.py

from e3dc import E3DC

USERNAME = 'user@domain.com'
PASS = 'md5password'
SERIALNUMBER = 'S10-number'
CONFIG = {}

print("web connection")
e3dc_obj = E3DC(E3DC.CONNECT_WEB, username=USERNAME, password=PASS, serialNumber = SERIALNUMBER, isPasswordMd5=False, configuration = CONFIG)
# connect to the portal and poll the status. This might raise an exception in case of failed login. This operation is performed with Ajax
print(e3dc_obj.poll(keepAlive=True))
#print(e3dc_obj.get_pvi_data(keepAlive=True))
print(e3dc_obj.get_battery_data()['rsoc'])
e3dc_obj.disconnect()

When i run this I get:

 web connection
Traceback (most recent call last):
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/e3dc/_e3dc.py", line 226, in sendRequest
    self.rscp.connect()
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/e3dc/_e3dc_rscp_web.py", line 417, in connect
    raise RequestTimeoutError
e3dc._e3dc_rscp_web.RequestTimeoutError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/storage/emulated/0/acode/test.py", line 9, in <module>
    e3dc_obj = E3DC(E3DC.CONNECT_WEB, username=USERNAME, password=PASS, serialNumber = SERIALNUMBER, isPasswordMd5=False, configuration = CONFIG)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/e3dc/e3dc.py", line 141, in __init_
    self.get_system_info_static(keepAlive=True)
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/e3dc/_e3dc.py", line 817, in get_system_info_static
    self.sendRequestTag(RscpTag.EMS_REQ_DERATE_AT_PERCENT_VALUE, keepAlive=True)
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/e3dc/_e3dc.py", line 264, in sendRequestTag
    return self.sendRequest(
           ^^^^^^^^^^^^^^^^^
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/e3dc/_e3dc.py", line 238, in sendRequest
    raise SendError("Max retries reached")
e3dc._e3dc.SendError: Max retries reached
eltoro0815 commented 2 weeks ago

Running test_websocket.py in Termux works as expected:

import websocket
ws = websocket.WebSocket()
ws.connect("ws://echo.websocket.events")
ws.send("Hello, Server")
print(ws.recv())
ws.close()

Output:

echo.websocket.events sponsored by Lob.com
eltoro0815 commented 2 weeks ago

I did some research with the new portal (my.e3dc.com)

Here is a script which logs in to get the Bearer Token and listens to a Websocket to get the actual data of your system.

import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, parse_qs
import websocket

# Start einer Session, um Cookies beizubehalten
session = requests.Session()

# Erste Anfrage an die SAML-Service-Provider-Login-URL:
# https://e3dc.e3dc.com/auth-saml/service-providers/customer/login?app=e3dc
# Gibt 302 als Antwort

initial_url = 'https://e3dc.e3dc.com/auth-saml/service-providers/customer/login?app=e3dc'
response = session.get(initial_url, allow_redirects=False)
print('1) [GET] Erste Anfrage an: ', initial_url)

# Weiterleitung zu ->
# https://customer.sso.e3dc.com/saml2/idp/SSOService.php?SAMLRequest=...
# Gibt 302 als Antwort
redirect_url = response.headers['Location']
response = session.get(redirect_url, allow_redirects=False)
print('2) [GET] Weiterleitung zu: ', redirect_url)

# Weiterleitung zu ->
# https://customer.sso.e3dc.com/module.php/core/loginuserpass.php?AuthState=...
# Gibt 200 als Antwort
login_page_url = response.headers['Location']
response = session.get(login_page_url)
print('3) [GET] Weiterleitung zur Login Seite: ', login_page_url)

if response.status_code == 200:

    # Extrahieren des AuthState-Werts
    soup = BeautifulSoup(response.text, 'html.parser')
    auth_state_input = soup.find('input', {'name': 'AuthState'})
    auth_state_value = auth_state_input['value'] if auth_state_input else None

    if auth_state_value:
        # Senden der Login-Daten
        login_data = {
            'username': 'YourUserName',  # Ersetzen Sie dies durch den tatsächlichen Benutzernamen
            'password': 'YourPasswordInPlainText',      # Ersetzen Sie dies durch das tatsächliche Passwort
            'AuthState': auth_state_value
        }

        # Submit Button des Formular klicken -->
        # https://customer.sso.e3dc.com/module.php/core/loginuserpass.php?
        login_url = 'https://customer.sso.e3dc.com/module.php/core/loginuserpass.php'
        response = session.post(login_url, data=login_data, allow_redirects=False)
        print("4) [POST] Abschicken des Anmeldeformulars mit Username und Passwort: ", login_url)

        if response.status_code == 200:
            # Extrahieren des SAMLResponse-Werts
            soup = BeautifulSoup(response.text, 'html.parser')
            saml_response_input = soup.find('input', {'name': 'SAMLResponse'})
            saml_response_value = saml_response_input['value'] if saml_response_input else None

            if saml_response_value:
                # Senden des SAMLResponse-Werts
                post_data = {
                    'SAMLResponse': saml_response_value
                }

                # Assert aufrufen
                #
                assert_url = 'https://e3dc.e3dc.com/auth-saml/service-providers/customer/assert'
                response = session.post(assert_url, data=post_data, allow_redirects=False)
                print("5) [POST] Abschicken des Dummy Formulars das SAMLResponse enthält: ", assert_url)

                redirect_url = response.headers['Location']

                parsed_url = urlparse(redirect_url)
                query_params = parse_qs(parsed_url.query)
                token_value = query_params.get('token', [None])[0]
                print("------------------------------------------------------------------------------")
                print("Bearer Token: ", token_value)
                print("------------------------------------------------------------------------------")

            else:
                print("SAMLResponse-Wert konnte nicht extrahiert werden.")
        else:
            print("Fehler beim Laden der SAML-Response-Seite:", response.status_code, response.text)
    else:
        print("AuthState-Wert konnte nicht extrahiert werden.")
else:
    print("Fehler beim Laden der Login-Seite:", response.status_code, response.text)

def on_message(ws, message):
    print(f"Received message: {message}")

def on_error(ws, error):
    print(f"Error: {error}")

def on_close(ws, close_status_code, close_msg):
    print("Connection closed")

def on_open(ws):
    # Hier können Sie Nachrichten senden, wenn die Verbindung geöffnet ist
    ws.send("Ihre Nachricht hier")

# Ersetzen Sie die URL durch die URL Ihres WebSocket-Servers
websocket_url = f"wss://e3dc.e3dc.com/storages/YOUR-SERIAL-NUMBER-WITHOUT-THE-PREFIX/status/ws?authorization={token_value}"

ws = websocket.WebSocketApp(websocket_url,
                                on_open=on_open,
                                on_message=on_message,
                                on_error=on_error,
                                on_close=on_close)

ws.run_forever()
+ THIS works on my Phone with Termux :) :) :)