Add solar charge controller integration and firmware

App:
- New DeviceRole.solar with its own BLE session/protocol/state
  (SolarSession, VanAlignSolarProtocol, SolarState), mirroring the
  leveling sensor but deliberately not wired into the Live Activity.
- BluetoothManager now keeps an in-memory history per metric (voltage,
  current, power) instead of a single primary-metric series; cleared on
  app restart by design, not persisted to disk.
- DeviceDetailView shows a channel picker above the history chart when a
  device has more than one chartable metric.
- AddDeviceView recognizes the solar service UUID during setup.
- DemoData gets a simulated solar device so the integration can be
  checked in the simulator without hardware.

Firmware:
- Add esp32_ble_solar.yaml (Votronic solar charge controller over BLE)
  and simulated variants (esp32_ble_sim.yaml, esp32_ble_solar_sim.yaml)
  for testing without a vehicle.
- esp32_ble.yaml: set flash_size/psram for the ESP32-S3 N16R8 module.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
fototeddy
2026-09-05 00:04:20 +02:00
co-authored by Claude Sonnet 5
parent 9a3b5cb472
commit 6d7b32d622
14 changed files with 1257 additions and 22 deletions
+6
View File
@@ -22,9 +22,15 @@ esphome:
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
framework:
type: esp-idf
# N16R8: 16 MB Flash + 8 MB PSRAM, beim S3 als Octal-PSRAM angebunden.
psram:
mode: octal
speed: 80MHz
logger:
level: WARN
+332
View File
@@ -0,0 +1,332 @@
# VanAlign Pro - Neigungsmessung über BLE (SIMULATION)
#
# Kopie von esp32_ble.yaml für den Fall, dass gerade kein MPU6050 zum
# Anschliessen vorhanden ist. Der `platform: mpu6050`-Sensor sowie der
# i2c-Bus wurden entfernt und durch Template-Sensoren ersetzt, die
# plausible, sich langsam ändernde Beschleunigungswerte erzeugen (ein
# gedachter Sensor, der gemütlich hin- und herschaukelt). Pitch/Roll,
# Kalibrierung und die BLE-Charakteristiken funktionieren dadurch exakt wie
# im Original - nur eben ohne angeschlossene Hardware.
#
# Name und Friendly Name sind bewusst auf "-sim" abgeändert, damit dieses
# Gerät im Netzwerk/BLE nicht mit einem echten VanAlign-Gerät kollidiert.
#
# Sobald wieder ein echter MPU6050 verfügbar ist, einfach esp32_ble.yaml
# weiterverwenden - diese Datei ist nur zum Testen der App/BLE-Anbindung.
esphome:
name: vanalign-sim
friendly_name: "VanAlign Pro (Sim)"
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
framework:
type: esp-idf
# N16R8: 16 MB Flash + 8 MB PSRAM, beim S3 als Octal-PSRAM angebunden.
psram:
mode: octal
speed: 80MHz
logger:
level: WARN
espnow:
channel: 1
sensor:
# Simulierte Rohwerte anstelle des physischen MPU6050. Die Sensor-Lage
# (Pitch/Roll) wandert langsam und stetig, wie es ein tatsächlich leicht
# schaukelndes Fahrzeug/Werkstück tun würde (Perioden ~75s/~113s).
- platform: template
name: "MPU6050 Accel X (Sim)"
id: accel_x
internal: true
update_interval: 0.1s
lambda: |-
float t = millis() / 1000.0f;
float pitch_rad = (15.0f * sin(t / 12.0f)) * 3.14159265f / 180.0f;
float roll_rad = (10.0f * sin(t / 18.0f + 1.0f)) * 3.14159265f / 180.0f;
return -9.80665f * sin(roll_rad) * cos(pitch_rad);
- platform: template
name: "MPU6050 Accel Y (Sim)"
id: accel_y
internal: true
update_interval: 0.1s
lambda: |-
float t = millis() / 1000.0f;
float pitch_rad = (15.0f * sin(t / 12.0f)) * 3.14159265f / 180.0f;
return 9.80665f * sin(pitch_rad);
- platform: template
name: "MPU6050 Accel Z (Sim)"
id: accel_z
internal: true
update_interval: 0.1s
lambda: |-
float t = millis() / 1000.0f;
float pitch_rad = (15.0f * sin(t / 12.0f)) * 3.14159265f / 180.0f;
float roll_rad = (10.0f * sin(t / 18.0f + 1.0f)) * 3.14159265f / 180.0f;
return 9.80665f * cos(roll_rad) * cos(pitch_rad);
# Gyro-Werte werden nur zur Anzeige simuliert (kleine Winkelgeschwindigkeit
# passend zur Schaukelbewegung oben, kein realer Bezug nötig).
- platform: template
name: "MPU6050 Gyro X-Achse"
id: mpu_gyro_x
update_interval: 0.1s
lambda: |-
float t = millis() / 1000.0f;
return (10.0f / 18.0f) * cos(t / 18.0f + 1.0f) * 3.14159265f / 180.0f;
- platform: template
name: "MPU6050 Gyro Y-Achse"
id: mpu_gyro_y
update_interval: 0.1s
lambda: |-
float t = millis() / 1000.0f;
return (15.0f / 12.0f) * cos(t / 12.0f) * 3.14159265f / 180.0f;
- platform: template
name: "MPU6050 Gyro Z-Achse"
id: mpu_gyro_z
update_interval: 0.1s
lambda: |-
return 0.0f;
- platform: template
name: "Neigung Pitch"
id: pitch
icon: mdi:caravan
unit_of_measurement: "°"
accuracy_decimals: 1
update_interval: 0.1s
lambda: |-
if (isnan(id(accel_x).state) || isnan(id(accel_y).state) || isnan(id(accel_z).state)) {
return NAN;
}
float raw = atan2(id(accel_y).state, sqrt(pow(id(accel_x).state, 2) + pow(id(accel_z).state, 2))) * (180.0 / 3.14159265);
return raw - id(pitch_offset); // Offset wird hier subtrahiert
filters:
- sliding_window_moving_average:
window_size: 8
send_every: 1
- exponential_moving_average:
alpha: 0.2
- platform: template
name: "Neigung Roll"
id: roll
icon: mdi:axis-x-rotate-clockwise
unit_of_measurement: "°"
accuracy_decimals: 1
update_interval: 0.1s
lambda: |-
if (isnan(id(accel_x).state) || isnan(id(accel_z).state)) {
return NAN;
}
float raw = atan2(-id(accel_x).state, id(accel_z).state) * (180.0 / 3.14159265);
return raw - id(roll_offset); // Offset wird hier subtrahiert
filters:
- sliding_window_moving_average:
window_size: 8
send_every: 1
- exponential_moving_average:
alpha: 0.2
globals:
- id: pitch_offset
type: float
restore_value: yes
initial_value: '0.0'
- id: roll_offset
type: float
restore_value: yes
initial_value: '0.0'
# Die Einbaulage liegt im Gerät, nicht in den Apps: Sie beschreibt, wie der
# Sensor im Fahrzeug sitzt eine Eigenschaft des Einbaus, nicht des Telefons.
# Damit sehen iPhone, Uhr und Android dasselbe, ohne sie je einzeln zu
# bestimmen. Angewandt wird sie weiterhin in den Apps; das Gerät verwahrt sie
# nur, sonst rechneten ältere Clients die Korrektur ein zweites Mal.
- id: orientation_version
type: uint8_t
restore_value: yes
initial_value: '0'
- id: orientation_source
type: uint8_t
restore_value: yes
initial_value: '0'
- id: orientation_invert_long
type: bool
restore_value: yes
initial_value: 'false'
- id: orientation_invert_lat
type: bool
restore_value: yes
initial_value: 'false'
- id: orientation_twist
type: float
restore_value: yes
initial_value: '0.0'
- id: enable_captive
type: bool
restore_value: yes
initial_value: 'false'
esp32_ble_server:
services:
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc42d
advertise: true
characteristics:
- id: pitch_ble
uuid: cad48e28-7fbe-41cf-bae9-d77a6c233424
description: "Pitch"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pitch).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: roll_ble
uuid: cad48e28-7fbe-41cf-bae9-d77a6c233425
description: "Roll"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(roll).state;
memcpy(v.data(), &val, sizeof(float));
return v;
# Die gespeicherten Nullpunkte, zwei Floats. Daran erkennen die Apps,
# ob überhaupt schon kalibriert wurde.
- id: offsets_ble
uuid: cad48e28-7fbe-41cf-bae9-d77a6c233426
description: "Kalibrier-Offsets"
read: true
value: !lambda |-
std::vector<unsigned char> v(2 * sizeof(float));
float p = id(pitch_offset);
float r = id(roll_offset);
memcpy(v.data(), &p, sizeof(float));
memcpy(v.data() + sizeof(float), &r, sizeof(float));
return v;
# Die Einbaulage, acht Byte:
#
# 0 Version, 1 = gültig gesetzt, 0 = nie geschrieben
# 1 Längsachse: 0 = Pitch des Sensors, 1 = Roll des Sensors
# 2 längs umgekehrt (0/1)
# 3 quer umgekehrt (0/1)
# 4..7 Verdrehung um die Hochachse, float32, Grad
- id: orientation_ble
uuid: cad48e28-7fbe-41cf-bae9-d77a6c233428
description: "Einbaulage"
read: true
write: true
value: !lambda |-
std::vector<unsigned char> v(8, 0);
v[0] = id(orientation_version);
v[1] = id(orientation_source);
v[2] = id(orientation_invert_long) ? 1 : 0;
v[3] = id(orientation_invert_lat) ? 1 : 0;
float t = id(orientation_twist);
memcpy(v.data() + 4, &t, sizeof(float));
return v;
on_write:
then:
- lambda: |-
if (x.size() < 8 || x[0] != 1) {
ESP_LOGW("vanalign", "Einbaulage verworfen: %d Byte, Version %d",
(int) x.size(), x.empty() ? -1 : (int) x[0]);
return;
}
float t;
memcpy(&t, x.data() + 4, sizeof(float));
if (!std::isfinite(t) || fabsf(t) > 180.0f) {
ESP_LOGW("vanalign", "Einbaulage verworfen: Verdrehung %.1f", t);
return;
}
id(orientation_version) = 1;
id(orientation_source) = x[1];
id(orientation_invert_long) = x[2] != 0;
id(orientation_invert_lat) = x[3] != 0;
id(orientation_twist) = t;
ESP_LOGI("vanalign", "Einbaulage gespeichert: Quelle=%d laengs=%d quer=%d verdreht=%.1f",
(int) x[1], (int) x[2], (int) x[3], t);
- id: calib_ble
uuid: cad48e28-7fbe-41cf-bae9-d77a6c233427
description: "Kalibriere Neigung"
write: true
on_write:
then:
- lambda: |-
bool reset = !x.empty() && (x[0] == 0x00 || x[0] == '0');
if (reset) {
id(reset_calibration).execute();
} else {
id(calibrate_level).execute();
}
#web_server:
# port: 80
#ota:
# platform: web_server
#wifi:
# ap:
# ssid: "VanAlign-Setup"
# password: "kalibrierung"
script:
# Die aktuelle Lage wird zur neuen Null. Knopf und Bluetooth laufen hier
# zusammen, damit sie nicht auseinanderdriften.
- id: calibrate_level
then:
- lambda: |-
if (isnan(id(accel_x).state) || isnan(id(accel_y).state) || isnan(id(accel_z).state)) {
ESP_LOGW("vanalign", "Kalibrierung abgebrochen: keine Sensorwerte");
return;
}
id(pitch_offset) = atan2(id(accel_y).state, sqrt(pow(id(accel_x).state, 2) + pow(id(accel_z).state, 2))) * (180.0 / 3.14159265);
id(roll_offset) = atan2(-id(accel_x).state, id(accel_z).state) * (180.0 / 3.14159265);
ESP_LOGI("vanalign", "Kalibriert: pitch_offset=%.2f roll_offset=%.2f", id(pitch_offset), id(roll_offset));
- id: reset_calibration
then:
- lambda: |-
id(pitch_offset) = 0.0f;
id(roll_offset) = 0.0f;
ESP_LOGI("vanalign", "Kalibrierung zurückgesetzt");
button:
- platform: template
name: "Kalibriere Neigung"
id: calib_button
on_press:
- script.execute: calibrate_level
- platform: template
name: "Kalibrierung zurücksetzen"
id: calib_reset_button
on_press:
- script.execute: reset_calibration
- platform: restart
name: "ESP Restart"
text_sensor:
- platform: template
name: "Firmware Version"
id: firmware_version
icon: mdi:tag
lambda: |-
return {"v1.0.2-sim"};
+233
View File
@@ -0,0 +1,233 @@
# VanAlign Solar - Votronic Solarladeregler über BLE
#
# Zweiter ESP32 im Fahrzeug, unabhängig vom Neigungssensor (esp32_ble.yaml).
# Liest den Votronic-Solarladeregler über den Displaylink-Port (UART) mit der
# externen Komponente github://syssi/esphome-votronic aus und stellt die
# Werte - genau wie beim Neigungssensor - über einen eigenen BLE-Service
# bereit. Zusätzlich (für Debug-Zwecke) läuft WLAN mit und die Werte werden
# auch auf dem eingebauten Webserver (Port 80) angezeigt - MQTT und die API
# sind weiterhin nicht enthalten. WLAN-Zugangsdaten liegen in secrets.yaml
# (lokal anzulegen, ist per .gitignore ausgeschlossen).
#
# Verkabelung: UART TX=GPIO4, RX=GPIO5 an den Displaylink-Port des Reglers,
# Baudrate 1000 (kein Tippfehler - das Votronic-Protokoll nutzt diese
# ungewöhnlich niedrige Rate). Board/Pins ggf. an die tatsächlich verbaute
# Hardware anpassen, hier als ESP32-S3-DevKitC-1 wie beim Neigungssensor
# angenommen.
#
# BLE-Service 05c9a349-2b8e-4b1d-9c9d-c247e9a6a001 (wird beworben):
#
# Charakteristik UUID (Ende) Inhalt
# Batteriespg. ...a101 Float32 LE, Volt
# PV-Spannung ...a102 Float32 LE, Volt
# PV-Strom ...a103 Float32 LE, Ampere
# PV-Leistung ...a104 Float32 LE, Watt
# Reglertemp. ...a105 Float32 LE, °C
# Statusflags ...a106 1 Byte, Bitmaske: Bit0 Batterie lädt,
# Bit1 Batterie entlädt, Bit2 PV-Regler
# aktiv, Bit3 PV-Strombegrenzung, Bit4 AES
# PV-Modus-ID ...a107 1 Byte, roher Wert aus pv_mode_setting_id
# Batteriestatus ...a108 1 Byte, rohe Bitmaske aus dem Regler
# Reglerstatus ...a109 1 Byte, rohe Bitmaske aus dem Regler
#
# Alle Charakteristiken sind reine Lesewerte, wie beim Neigungssensor fragt
# der Client sie im Takt ab.
esphome:
name: vanalign-solar
friendly_name: "VanAlign Solar"
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
framework:
type: esp-idf
# N16R8: 16 MB Flash + 8 MB PSRAM, beim S3 als Octal-PSRAM angebunden.
psram:
mode: octal
speed: 80MHz
logger:
level: WARN
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
web_server:
port: 80
external_components:
- source: github://syssi/esphome-votronic@main
refresh: 0s
uart:
- id: uart_0
baud_rate: 1000
tx_pin: GPIO4
rx_pin: GPIO5
votronic:
- id: votronic0
uart_id: uart_0
rx_timeout: 150ms
throttle: 2s
sensor:
- platform: votronic
votronic_id: votronic0
battery_computer_battery_voltage:
name: "Batteriespannung"
id: battery_voltage
pv_voltage:
name: "PV Spannung"
id: pv_voltage
pv_current:
name: "PV Strom"
id: pv_current
pv_power:
name: "PV Leistung"
id: pv_power
pv_controller_temperature:
name: "Reglertemperatur"
id: pv_temperature
pv_mode_setting_id:
name: "PV Modus-ID"
id: pv_mode_id
pv_battery_status_bitmask:
name: "PV Batteriestatus (Bitmaske)"
id: pv_battery_status_bitmask
pv_controller_status_bitmask:
name: "PV Reglerstatus (Bitmaske)"
id: pv_controller_status_bitmask
binary_sensor:
- platform: votronic
votronic_id: votronic0
battery_computer_charging:
name: "Batterie lädt"
id: battery_charging
battery_computer_discharging:
name: "Batterie entlädt"
id: battery_discharging
pv_controller_active:
name: "PV Regler aktiv"
id: pv_active
pv_current_reduction:
name: "PV Strombegrenzung"
id: pv_current_reduction
pv_aes_active:
name: "PV AES aktiv"
id: pv_aes_active
text_sensor:
- platform: votronic
votronic_id: votronic0
pv_mode_setting:
name: "PV Modus"
pv_battery_status:
name: "PV Batteriestatus"
pv_controller_status:
name: "PV Reglerstatus"
- platform: template
name: "Firmware Version"
id: firmware_version
icon: mdi:tag
lambda: |-
return {"v1.0.0"};
esp32_ble_server:
services:
- uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a001
advertise: true
characteristics:
- id: battery_voltage_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a101
description: "Batteriespannung"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(battery_voltage).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_voltage_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a102
description: "PV Spannung"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_voltage).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_current_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a103
description: "PV Strom"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_current).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_power_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a104
description: "PV Leistung"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_power).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_temperature_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a105
description: "Reglertemperatur"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_temperature).state;
memcpy(v.data(), &val, sizeof(float));
return v;
# Bit0 Batterie lädt, Bit1 Batterie entlädt, Bit2 PV-Regler aktiv,
# Bit3 PV-Strombegrenzung, Bit4 AES aktiv.
- id: status_flags_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a106
description: "Statusflags"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
uint8_t flags = 0;
if (id(battery_charging).state) flags |= (1 << 0);
if (id(battery_discharging).state) flags |= (1 << 1);
if (id(pv_active).state) flags |= (1 << 2);
if (id(pv_current_reduction).state) flags |= (1 << 3);
if (id(pv_aes_active).state) flags |= (1 << 4);
v[0] = flags;
return v;
- id: pv_mode_id_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a107
description: "PV Modus-ID"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
v[0] = (uint8_t) id(pv_mode_id).state;
return v;
- id: pv_battery_status_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a108
description: "PV Batteriestatus (Bitmaske)"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
v[0] = (uint8_t) id(pv_battery_status_bitmask).state;
return v;
- id: pv_controller_status_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a109
description: "PV Reglerstatus (Bitmaske)"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
v[0] = (uint8_t) id(pv_controller_status_bitmask).state;
return v;
button:
- platform: restart
name: "ESP Restart"
+265
View File
@@ -0,0 +1,265 @@
# VanAlign Solar - Votronic Solarladeregler über BLE (SIMULATION)
#
# Kopie von esp32_ble_solar.yaml für den Fall, dass gerade kein Votronic-
# Regler zum Anschliessen vorhanden ist. Die `platform: votronic`-Sensoren
# sowie UART/external_components wurden entfernt und durch Template-Sensoren
# ersetzt, die einen plausiblen Tagesverlauf simulieren: PV-Spannung/-Strom
# folgen einem "Sonnenstand" (ein gedachter, ca. 6 Minuten langer Tag), die
# Batteriespannung schwankt gemütlich mit, und die Statusflags/Bitmasken
# leiten sich aus diesen Werten ab. Die BLE-Charakteristiken funktionieren
# dadurch exakt wie im Original - nur eben ohne angeschlossene Hardware.
#
# Name und Friendly Name sind bewusst auf "-sim" abgeändert, damit dieses
# Gerät im BLE-Umfeld nicht mit einem echten VanAlign-Solar-Gerät kollidiert.
#
# Für Debug-Zwecke läuft WLAN mit und die Werte werden zusätzlich auf dem
# eingebauten Webserver (Port 80) angezeigt; WLAN-Zugangsdaten liegen in
# secrets.yaml (lokal anzulegen, ist per .gitignore ausgeschlossen). Alle
# simulierten Werte aktualisieren sich höchstens jede Sekunde, damit sich
# Änderungen beim Debuggen zügig zeigen.
#
# Sobald wieder ein echter Votronic-Regler verfügbar ist, einfach
# esp32_ble_solar.yaml weiterverwenden - diese Datei ist nur zum Testen der
# App/BLE-Anbindung.
esphome:
name: vanalign-solar-sim
friendly_name: "VanAlign Solar (Sim)"
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
framework:
type: esp-idf
# N16R8: 16 MB Flash + 8 MB PSRAM, beim S3 als Octal-PSRAM angebunden.
psram:
mode: octal
speed: 80MHz
logger:
level: WARN
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
web_server:
port: 80
sensor:
# Simulierter "Sonnenstand": 0 nachts, sanfter Buckel tagsüber, Periode
# ca. 6 Minuten - reicht zum Beobachten eines vollen Auf/Ab in der App.
- platform: template
name: "Sonnenstand (Sim)"
id: sun_level
internal: true
update_interval: 1s
lambda: |-
float t = millis() / 1000.0f;
return std::max(0.0f, sinf(t / 180.0f));
- platform: template
name: "Batteriespannung (Sim)"
id: battery_voltage
unit_of_measurement: "V"
accuracy_decimals: 2
update_interval: 1s
lambda: |-
float t = millis() / 1000.0f;
return 12.6f + 0.8f * id(sun_level).state + 0.05f * sin(t / 20.0f);
- platform: template
name: "PV Spannung (Sim)"
id: pv_voltage
unit_of_measurement: "V"
accuracy_decimals: 2
update_interval: 1s
lambda: |-
float t = millis() / 1000.0f;
return id(sun_level).state > 0.01f ? (19.0f + 1.0f * sin(t / 25.0f)) : 0.0f;
- platform: template
name: "PV Strom (Sim)"
id: pv_current
unit_of_measurement: "A"
accuracy_decimals: 2
update_interval: 1s
lambda: |-
float t = millis() / 1000.0f;
return id(sun_level).state * (6.0f + 1.5f * sin(t / 17.0f));
- platform: template
name: "PV Leistung (Sim)"
id: pv_power
unit_of_measurement: "W"
accuracy_decimals: 1
update_interval: 1s
lambda: |-
return id(pv_voltage).state * id(pv_current).state;
- platform: template
name: "Reglertemperatur (Sim)"
id: pv_temperature
unit_of_measurement: "°C"
accuracy_decimals: 1
update_interval: 1s
lambda: |-
float t = millis() / 1000.0f;
return 22.0f + 10.0f * id(sun_level).state + 1.0f * sin(t / 11.0f);
- platform: template
name: "PV Modus-ID (Sim)"
id: pv_mode_id
update_interval: 1s
lambda: |-
return 3.0f; // fester simulierter Modus (MPPT)
- platform: template
name: "PV Batteriestatus (Bitmaske, Sim)"
id: pv_battery_status_bitmask
update_interval: 1s
lambda: |-
return id(sun_level).state > 0.01f ? 1.0f : 0.0f;
- platform: template
name: "PV Reglerstatus (Bitmaske, Sim)"
id: pv_controller_status_bitmask
update_interval: 1s
lambda: |-
return id(sun_level).state > 0.01f ? 2.0f : 0.0f;
binary_sensor:
- platform: template
name: "Batterie lädt (Sim)"
id: battery_charging
lambda: |-
return id(pv_current).state > 0.2f;
- platform: template
name: "Batterie entlädt (Sim)"
id: battery_discharging
lambda: |-
return id(pv_current).state <= 0.2f;
- platform: template
name: "PV Regler aktiv (Sim)"
id: pv_active
lambda: |-
return id(sun_level).state > 0.01f;
- platform: template
name: "PV Strombegrenzung (Sim)"
id: pv_current_reduction
lambda: |-
float t = millis() / 1000.0f;
return id(pv_active).state && sin(t / 60.0f) > 0.9f;
- platform: template
name: "PV AES aktiv (Sim)"
id: pv_aes_active
lambda: |-
return id(battery_voltage).state > 13.3f;
text_sensor:
- platform: template
name: "Firmware Version"
id: firmware_version
icon: mdi:tag
lambda: |-
return {"v1.0.0-sim"};
esp32_ble_server:
services:
- uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a001
advertise: true
characteristics:
- id: battery_voltage_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a101
description: "Batteriespannung"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(battery_voltage).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_voltage_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a102
description: "PV Spannung"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_voltage).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_current_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a103
description: "PV Strom"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_current).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_power_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a104
description: "PV Leistung"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_power).state;
memcpy(v.data(), &val, sizeof(float));
return v;
- id: pv_temperature_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a105
description: "Reglertemperatur"
read: true
value: !lambda |-
std::vector<unsigned char> v(sizeof(float));
float val = id(pv_temperature).state;
memcpy(v.data(), &val, sizeof(float));
return v;
# Bit0 Batterie lädt, Bit1 Batterie entlädt, Bit2 PV-Regler aktiv,
# Bit3 PV-Strombegrenzung, Bit4 AES aktiv.
- id: status_flags_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a106
description: "Statusflags"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
uint8_t flags = 0;
if (id(battery_charging).state) flags |= (1 << 0);
if (id(battery_discharging).state) flags |= (1 << 1);
if (id(pv_active).state) flags |= (1 << 2);
if (id(pv_current_reduction).state) flags |= (1 << 3);
if (id(pv_aes_active).state) flags |= (1 << 4);
v[0] = flags;
return v;
- id: pv_mode_id_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a107
description: "PV Modus-ID"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
v[0] = (uint8_t) id(pv_mode_id).state;
return v;
- id: pv_battery_status_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a108
description: "PV Batteriestatus (Bitmaske)"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
v[0] = (uint8_t) id(pv_battery_status_bitmask).state;
return v;
- id: pv_controller_status_ble
uuid: 05c9a349-2b8e-4b1d-9c9d-c247e9a6a109
description: "PV Reglerstatus (Bitmaske)"
read: true
value: !lambda |-
std::vector<unsigned char> v(1, 0);
v[0] = (uint8_t) id(pv_controller_status_bitmask).state;
return v;
button:
- platform: restart
name: "ESP Restart"