-
Notifications
You must be signed in to change notification settings - Fork 121
Fix Tesla/Powerwall data handling and improve robustness of control loop #3232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
e2d1824
15bae70
38fbc46
2d479e0
d5b572d
d0b3498
2346e01
5cfe1c7
71f4e41
2932b51
b4be565
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| #!/usr/bin/env python3 | ||
| import logging | ||
| import math | ||
| from requests import HTTPError | ||
|
|
||
| from modules.common.abstract_device import AbstractCounter | ||
|
|
@@ -21,29 +22,138 @@ | |
| self.store = get_counter_value_store(self.component_config.id) | ||
| self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) | ||
|
|
||
| @staticmethod | ||
| def _safe_float(val, default: float = 0.0) -> float: | ||
| try: | ||
| if val is None: | ||
| return default | ||
| return float(val) | ||
| except (TypeError, ValueError): | ||
| return default | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Die Methode und die Aufrufe kann entfallen, das ist zentral umgesetzt. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Danke, ich entferne _safe_float() und verlasse mich auf die zentrale Behandlung. Mein ursprünglicher Gedanke war, unvollständige Tesla-Antworten robuster abzufangen. |
||
|
|
||
| @staticmethod | ||
| def _nearly_zero(x: float, eps: float = 1e-9) -> bool: | ||
| return abs(x) < eps | ||
|
|
||
| def _calc_currents_and_pf_from_pqu( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Diese Berechnung erfolgt zentral im CounterState, wenn keine Ströme gesetzt sind, aber Phasenspannungen und Phasenleistungen vorhanden sind. Die Berechnung soll nicht in jedem Modul erfolgen.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im CounterState sind jedoch nur die Wirkleistungen vorhanden. Reale Ströme können dann nicht berechnet werden. Das sollte in einem anderen PR grundlegend überarbeitet werden.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Danke für den Hinweis. Dann würde ich für diesen PR die lokale Stromberechnung im Tesla-Counter beibehalten. In meinem Setup liefert Tesla/Neurio für die Phase currents nur 0-Werte, und ohne diese Ableitung funktioniert das Lastmanagement nicht zuverlässig. Den generischen Ansatz zur Berechnung realer currents im CounterState würde ich nicht in diesem PR lösen. Das wäre aus meiner Sicht ein separates Thema, wie von dir vorgeschlagen. Ich reduziere den PR daher weiter auf: reduzierte /api/status-Nutzung Ist das in eurem Interesse?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ich habe das bei mir umgesetzt und teste die Änderungen über Ostern. Anschließend würde ich den PR aktualisieren... |
||
| self, voltages: list[float], p_list: list[float], q_list: list[float] | ||
| ) -> tuple[list[float], list[float]]: | ||
| """ | ||
| Calculates signed currents (A) and signed power factors per phase from P/Q/U. | ||
|
|
||
| Convention: | ||
| - sign of current follows sign of active power P (import +, export -) | ||
| - PF = P / S (signed) | ||
| - S = sqrt(P^2 + Q^2) | ||
| - I = S / U (signed via P) | ||
| """ | ||
| currents: list[float] = [0.0, 0.0, 0.0] | ||
| pfs: list[float] = [0.0, 0.0, 0.0] | ||
|
|
||
| for i in range(3): | ||
| u = self._safe_float(voltages[i], 0.0) | ||
| p = self._safe_float(p_list[i], 0.0) | ||
| q = self._safe_float(q_list[i], 0.0) | ||
|
|
||
| if self._nearly_zero(u): | ||
| currents[i] = 0.0 | ||
| pfs[i] = 0.0 | ||
| continue | ||
|
|
||
| s = math.sqrt(p * p + q * q) | ||
|
|
||
| if self._nearly_zero(s): | ||
| currents[i] = 0.0 | ||
| pfs[i] = 0.0 | ||
| continue | ||
|
|
||
| pfs[i] = p / s | ||
| i_mag = s / u | ||
| currents[i] = i_mag if p >= 0 else -i_mag | ||
|
|
||
| return currents, pfs | ||
|
|
||
| def update(self, client: PowerwallHttpClient, aggregate): | ||
| # read firmware version | ||
| status = client.get_json("/api/status") | ||
| log.debug('Firmware: ' + status["version"]) | ||
| # - only once after startup (no firmware known yet) | ||
| # - and whenever a new auth cookie was negotiated (startup or reauth) | ||
| need_status = False | ||
| if getattr(client, "cookie_renewed", False): | ||
| need_status = True | ||
| elif not getattr(self.store, "firmware", ""): | ||
| need_status = True | ||
|
|
||
| if need_status: | ||
| try: | ||
| status = client.get_json("/api/status", fail_fast=False) | ||
| self.store.firmware = status.get("version", "") | ||
| log.debug("Firmware: %s", self.store.firmware) | ||
| except Exception: | ||
| # /api/status is non-critical and must not trigger fail-fast | ||
| pass | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ist es nicht sinnvoller, die Firmware-Version in der initializer-Methode der device.py aufzurufen?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ja, das ist aus meiner Sicht sinnvoller. Die Firmware-Version ist nicht regelungsrelevant und sollte daher nicht im regulären Update-Zyklus abgefragt werden. Ich nehme die /api/status-Abfrage aus dem Counter-Update heraus und verschiebe sie in die initializer-Methode.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Danke für den Hinweis. Ich nehme die lokale Default-Wert-Behandlung an dieser Stelle wieder heraus, damit fehlende oder unvollständige Werte konsistent über den bestehenden Exception-/Fallback-Pfad behandelt werden. Mein Ziel war hier vor allem, auf inkonsistente Tesla/Neurio-Antworten robuster zu reagieren. Ich richte mich im PR aber nach der zentralen openWB-Logik und reduziere die lokale Sonderbehandlung wieder. |
||
| try: | ||
| # read additional info if firmware supports | ||
| meters_site = client.get_json("/api/meters/site") | ||
| cached = meters_site[0]["Cached_readings"] | ||
|
|
||
| # --- voltages / powers / reactive powers (per phase) --- | ||
| voltages = [self._safe_float(cached.get(f"v_l{phase}n")) for phase in range(1, 4)] | ||
| p_list = [self._safe_float(cached.get(f"real_power_{ph}")) for ph in ["a", "b", "c"]] | ||
| q_list = [self._safe_float(cached.get(f"reactive_power_{ph}")) for ph in ["a", "b", "c"]] | ||
|
|
||
| # --- currents from API (often all 0 on Neurio/Tesla) --- | ||
| api_currents = [self._safe_float(cached.get(f"i_{ph}_current")) for ph in ["a", "b", "c"]] | ||
|
|
||
| # --- energy counters: use aggregate site values as sole source --- | ||
| imported = self._safe_float(aggregate["site"]["energy_imported"]) | ||
| exported = self._safe_float(aggregate["site"]["energy_exported"]) | ||
|
|
||
| # --- local fallback for Tesla/Neurio setups with missing phase currents --- | ||
| calculated_currents, power_factors = self._calc_currents_and_pf_from_pqu( | ||
| voltages=voltages, | ||
| p_list=p_list, | ||
| q_list=q_list, | ||
| ) | ||
|
|
||
|
|
||
|
Check warning on line 118 in packages/modules/devices/tesla/tesla/counter.py
|
||
| if all(self._nearly_zero(i) for i in api_currents): | ||
| currents = calculated_currents | ||
| log.debug( | ||
| "Tesla/Neurio phase currents missing (all 0). " | ||
| "Calculated currents locally from P/Q and U." | ||
| ) | ||
| else: | ||
| currents = api_currents | ||
| log.debug("Using phase currents from Tesla/Neurio API.") | ||
|
|
||
| freq = self._safe_float(aggregate["site"].get("frequency", 50.0), 50.0) | ||
|
|
||
| serial = cached.get("serial_number") | ||
| serial_number = str(serial) if serial else None | ||
|
|
||
| powerwall_state = CounterState( | ||
| imported=aggregate["site"]["energy_imported"], | ||
| exported=aggregate["site"]["energy_exported"], | ||
| power=aggregate["site"]["instant_power"], | ||
| voltages=[meters_site[0]["Cached_readings"]["v_l" + str(phase) + "n"] for phase in range(1, 4)], | ||
| currents=[meters_site[0]["Cached_readings"]["i_" + phase + "_current"] for phase in ["a", "b", "c"]], | ||
| powers=[meters_site[0]["Cached_readings"]["real_power_" + phase] for phase in ["a", "b", "c"]] | ||
| imported=imported, | ||
| exported=exported, | ||
| power=self._safe_float(aggregate["site"]["instant_power"]), | ||
| voltages=voltages, | ||
| currents=currents, | ||
| powers=p_list, | ||
| power_factors=power_factors, | ||
| frequency=round(freq, 2), | ||
| serial_number=serial_number, | ||
| ) | ||
| except (KeyError, HTTPError): | ||
|
|
||
| except (KeyError, HTTPError, IndexError, TypeError) as e: | ||
| log.debug( | ||
| "Firmware seems not to provide detailed phase measurements. Fallback to total power only.") | ||
| "Firmware seems not to provide detailed phase measurements. Fallback to total power only. (%s)", | ||
| str(e), | ||
| ) | ||
| powerwall_state = CounterState( | ||
| imported=aggregate["site"]["energy_imported"], | ||
| exported=aggregate["site"]["energy_exported"], | ||
| power=aggregate["site"]["instant_power"] | ||
| imported=self._safe_float(aggregate["site"]["energy_imported"]), | ||
| exported=self._safe_float(aggregate["site"]["energy_exported"]), | ||
| power=self._safe_float(aggregate["site"]["instant_power"]), | ||
| ) | ||
|
|
||
| self.store.set(powerwall_state) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |||||||||||||||||||||
| import logging | ||||||||||||||||||||||
| import requests | ||||||||||||||||||||||
| from requests import HTTPError | ||||||||||||||||||||||
| from typing import Iterable, Union | ||||||||||||||||||||||
| from typing import Iterable, Union, Optional | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from modules.common.abstract_device import DeviceDescriptor | ||||||||||||||||||||||
| from modules.common.component_context import SingleComponentUpdateContext | ||||||||||||||||||||||
|
|
@@ -17,13 +17,20 @@ | |||||||||||||||||||||
| log = logging.getLogger(__name__) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def __update_components(client: PowerwallHttpClient, | ||||||||||||||||||||||
| components: Iterable[Union[TeslaBat, TeslaCounter, TeslaInverter]]): | ||||||||||||||||||||||
| def __update_components( | ||||||||||||||||||||||
| client: PowerwallHttpClient, | ||||||||||||||||||||||
| components: Iterable[Union[TeslaBat, TeslaCounter, TeslaInverter]], | ||||||||||||||||||||||
| ): | ||||||||||||||||||||||
| aggregate = client.get_json("/api/meters/aggregates") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for component in components: | ||||||||||||||||||||||
| with SingleComponentUpdateContext(component.fault_state): | ||||||||||||||||||||||
| component.update(client, aggregate) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # FAIL-FAST: abort remaining components if any critical request failed in this cycle | ||||||||||||||||||||||
| if client.cycle_failed: | ||||||||||||||||||||||
| break | ||||||||||||||||||||||
|
Comment on lines
+27
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Das bricht mit der bestehenden Logik. Wenn eine Komponente nicht auslesbar ist, müssen alle anderen trotzdem ausgelesen werden, sonst kommen sie nicht in den Fehlerzustand oder wenn sie Werte liefern, braucht die Regelung diese. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ich verstehe den Einwand zum break im Komponentenloop. Ich würde das Verhalten gern präzisieren: Bei Parsing-/Datenfehlern innerhalb einer einzelnen Komponente soll kein fail-fast erfolgen; dann soll die jeweilige Komponente über den bestehenden Fehlerpfad fehlschlagen und die übrigen Komponenten sollen weiter verarbeitet werden. Fail-fast soll nur bei Gateway-/Transportfehlern greifen, also wenn die Powerwall/Gateway-Kommunikation selbst fehlschlägt. In diesem Fall hängen alle Tesla-Komponenten an derselben nicht erreichbaren Quelle, weitere Abfragen im selben Zyklus erzeugen nur zusätzliche Timeouts und verzögern die Regelschleife. Ich würde die Änderung entsprechend anpassen, sodass nur Verbindungs-/Gatewayfehler den Tesla-Zyklus abbrechen, nicht aber komponentenspezifische Parsing- oder Datenfehler. Ist das in eurem Interesse? |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _authenticate(session: requests.Session, url: str, email: str, password: str): | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
|
|
@@ -33,15 +40,16 @@ def _authenticate(session: requests.Session, url: str, email: str, password: str | |||||||||||||||||||||
| "https://" + url + "/api/login/Basic", | ||||||||||||||||||||||
| json={"username": "customer", "email": email, "password": password, "force_sm_off": False}, | ||||||||||||||||||||||
| verify=False, | ||||||||||||||||||||||
| timeout=5 | ||||||||||||||||||||||
| timeout=5, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| log.debug("Authentication endpoint send cookies %s", str(response.cookies)) | ||||||||||||||||||||||
| response.raise_for_status() | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Wird über einen Hook aufgerufen. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Werde ich entfernen! |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return {"AuthCookie": response.cookies["AuthCookie"], "UserRecord": response.cookies["UserRecord"]} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def create_device(device_config: Tesla): | ||||||||||||||||||||||
| http_client = None | ||||||||||||||||||||||
| session = None | ||||||||||||||||||||||
| http_client: Optional[PowerwallHttpClient] = None | ||||||||||||||||||||||
| session: Optional[requests.Session] = None | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def create_bat_component(component_config: TeslaBatSetup): | ||||||||||||||||||||||
| return TeslaBat(component_config) | ||||||||||||||||||||||
|
|
@@ -52,27 +60,48 @@ def create_counter_component(component_config: TeslaCounterSetup): | |||||||||||||||||||||
| def create_inverter_component(component_config: TeslaInverterSetup): | ||||||||||||||||||||||
| return TeslaInverter(component_config) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _ensure_initialized(): | ||||||||||||||||||||||
| nonlocal http_client, session | ||||||||||||||||||||||
| if session is None or http_client is None: | ||||||||||||||||||||||
| initializer() | ||||||||||||||||||||||
| if session is None or http_client is None: | ||||||||||||||||||||||
| raise Exception("Powerwall device initializer did not create session/http_client") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bitte entfernen, wird zentral gelöst.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Danke für den Hinweis. Ich entferne die lokale Initialisierungs-Hilfslogik wieder, damit das Modul näher an der zentralen openWB-Struktur bleibt. Falls eine Reinitialisierung bei Fehlern weiterhin nötig ist, binde ich das wie vorgeschlagen über den error_handler mit initializer an, statt es lokal im Modul zu behandeln. |
||||||||||||||||||||||
| def update_components(components: Iterable[Union[TeslaBat, TeslaCounter, TeslaInverter]]): | ||||||||||||||||||||||
| log.debug("Beginning update") | ||||||||||||||||||||||
| nonlocal http_client, session | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| address = device_config.configuration.ip_address | ||||||||||||||||||||||
| email = device_config.configuration.email | ||||||||||||||||||||||
| password = device_config.configuration.password | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _ensure_initialized() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Reset per-cycle flags (fail-fast + cookie-renewed marker) | ||||||||||||||||||||||
| http_client.reset_cycle() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # First run after process start: no cookies -> authenticate once | ||||||||||||||||||||||
| if http_client.cookies is None: | ||||||||||||||||||||||
| http_client.cookies = _authenticate(session, address, email, password) | ||||||||||||||||||||||
| http_client.mark_cookie_renewed() | ||||||||||||||||||||||
| __update_components(http_client, components) | ||||||||||||||||||||||
| return | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Normal operation: reuse cookie. If it fails with 401/403 -> re-auth | ||||||||||||||||||||||
| try: | ||||||||||||||||||||||
| __update_components(http_client, components) | ||||||||||||||||||||||
| return | ||||||||||||||||||||||
| except HTTPError as e: | ||||||||||||||||||||||
| if e.response.status_code != 401 and e.response.status_code != 403: | ||||||||||||||||||||||
| raise e | ||||||||||||||||||||||
| log.warning("Login to powerwall with existing cookie failed. Will retry with new cookie...") | ||||||||||||||||||||||
| status = getattr(getattr(e, "response", None), "status_code", None) | ||||||||||||||||||||||
| if status not in (401, 403): | ||||||||||||||||||||||
| raise | ||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||
| "Login to powerwall with existing cookie failed (status=%s). Will retry with new cookie...", | ||||||||||||||||||||||
| status, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| http_client.cookies = _authenticate(session, address, email, password) | ||||||||||||||||||||||
| http_client.mark_cookie_renewed() | ||||||||||||||||||||||
| __update_components(http_client, components) | ||||||||||||||||||||||
| log.debug("Update completed successfully") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def initializer(): | ||||||||||||||||||||||
| nonlocal http_client, session | ||||||||||||||||||||||
|
|
@@ -87,7 +116,7 @@ def initializer(): | |||||||||||||||||||||
| counter=create_counter_component, | ||||||||||||||||||||||
| inverter=create_inverter_component, | ||||||||||||||||||||||
| ), | ||||||||||||||||||||||
| component_updater=MultiComponentUpdater(update_components) | ||||||||||||||||||||||
| component_updater=MultiComponentUpdater(update_components), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,53 @@ | ||
| import logging | ||
|
|
||
| import requests | ||
| from requests.exceptions import HTTPError, ConnectionError, RequestException, SSLError, Timeout | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class PowerwallHttpClient: | ||
| """ | ||
| HTTP client wrapper for Tesla Powerwall Gateway local API calls. | ||
| """ | ||
|
|
||
| def __init__(self, host: str, session: requests.Session, cookies): | ||
| self.__base_url = "https://" + host | ||
| self.cookies = cookies | ||
| self.__session = session | ||
|
|
||
| def get_json(self, relative_url: str): | ||
| # Fail-fast marker for the current polling cycle | ||
| self.cycle_failed = False | ||
|
|
||
| # Marker: set when a new auth cookie was negotiated (startup or reauth) | ||
| self.cookie_renewed = False | ||
|
|
||
| def reset_cycle(self): | ||
| """Reset per update-cycle flags.""" | ||
| self.cycle_failed = False | ||
| self.cookie_renewed = False | ||
|
|
||
| def mark_cookie_renewed(self): | ||
| """Mark that cookies have been freshly negotiated (start or reauth).""" | ||
| self.cookie_renewed = True | ||
|
|
||
| def get_json(self, relative_url: str, *, fail_fast: bool = True): | ||
| """ | ||
| :param fail_fast: | ||
| True -> errors mark this cycle as failed (device aborts remaining components) | ||
| False -> errors do NOT mark cycle_failed (for non-critical endpoints like /api/status) | ||
| """ | ||
| url = self.__base_url + relative_url | ||
| return self.__session.get(url, cookies=self.cookies, verify=False, timeout=5).json() | ||
|
|
||
| try: | ||
| response = self.__session.get( | ||
| url, | ||
| cookies=self.cookies, | ||
| verify=False, | ||
| timeout=5, | ||
| ) | ||
| return response.json() | ||
| except (HTTPError, Timeout, ConnectionError, SSLError, RequestException, ValueError): | ||
| if fail_fast: | ||
| self.cycle_failed = True | ||
| raise | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bitte die Änderungen entfernen, diese blähen den Code stark auf, ohne einen wirklichen Mehrwert zu bieten.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verstanden. Ich reduziere die Änderungen an dieser Stelle wieder deutlich. Mein Ziel war hier, konkrete Probleme im Tesla-/Powerwall-Modul zu adressieren. Für den PR lasse ich aber nur die Teile stehen, die dafür funktional wirklich notwendig sind, und nehme die übrigen Erweiterungen wieder heraus. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -333,7 +333,7 @@ function teslaLogin () { | |
| ?> | ||
| <div class="alert alert-success"> | ||
| Anmeldung erfolgreich!<br> | ||
| Die erhaltenen Token wurden gespeichert. Du kannst diese Seite jetzt schließen. | ||
| Die erhaltenen Token wurden gespeichert. Sie können diese Seite jetzt schließen. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im UI wird Du als Ansprache verwendet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Werde ich ändern! |
||
| </div> | ||
| <?php | ||
| } else { | ||
|
|
@@ -354,7 +354,7 @@ function teslaLogin () { | |
| } | ||
| ?> | ||
| <div class="alert alert-success"> | ||
| Gespeicherte Anmeldedaten wurden entfernt. Du kannst diese Seite jetzt schließen. | ||
| Gespeicherte Anmeldedaten wurden entfernt. Sie können diese Seite jetzt schließen. | ||
| </div> | ||
| <?php | ||
| break; | ||
|
|
@@ -369,4 +369,4 @@ function teslaLogin () { | |
|
|
||
| </div> <!-- container --> | ||
| </body> | ||
| </html> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default Werte werden zentral im CounterState gesetzt, damit sie konsistent sind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bitte die Methode entfernen. Wenn Werte in einem Zyklus fehlen, wird eine Exception geworfen.
Es wird inkonsistent und der Code in jedem Modul sehr umfangreich, wenn überall default-Werte gesetzt werden.