Adding esphome display
This commit is contained in:
parent
1501d7562e
commit
26a68a8962
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Wi-Fi credentials for ESPHome devices
|
||||||
|
WIFI_SSID="your_wifi_ssid"
|
||||||
|
WIFI_PASSWORD="your_wifi_password"
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
.esphome/
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# AGENTS.md (ESPHome Workspace)
|
||||||
|
|
||||||
|
This directory is a self-contained ESPHome project space utilizing dynamic configuration loaders and an interactive flashing CLI.
|
||||||
|
|
||||||
|
## Rules & Design Standards
|
||||||
|
|
||||||
|
### Secret Management
|
||||||
|
* **NEVER** write plaintext Wi-Fi passwords, tokens, or API keys directly in the YAML configurations.
|
||||||
|
* **DO NOT** modify the `.gitignore` to allow checking in `.env` files.
|
||||||
|
* Use ESPHome's `!env_var VAR_NAME` to inject environment variables at compile-time. Any new secrets must be declared in `.env.template` and configured locally in `.env`.
|
||||||
|
|
||||||
|
### Configuration Standards
|
||||||
|
* Default board architecture is ESP32 (`nodemcu-32s` with `arduino` framework) unless specified.
|
||||||
|
* Naming conventions for configurations: `esdisplay_<name>.yaml` or `sensor_<name>.yaml`.
|
||||||
|
* All displays should use the `time` component with SNTP sync for accurate time rendering.
|
||||||
|
* Prefer importing sensors from Home Assistant over Native API client integrations, as it decouples rendering layouts from automation scripts.
|
||||||
|
|
||||||
|
### Build and Run Tools
|
||||||
|
* Use `uv run flash.py` to compile, validate, clean, or flash ESPHome configurations.
|
||||||
|
* Avoid triggering direct `esphome` cli calls unless doing isolated validation tests.
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# ESPHome Use-Cases & e-Paper Display
|
||||||
|
|
||||||
|
This directory contains ESPHome configuration files for various IoT and monitoring devices, using environment variables to keep credentials local and gitignored.
|
||||||
|
|
||||||
|
## Get Started
|
||||||
|
|
||||||
|
### 1. Configure Credentials
|
||||||
|
Copy `.env.template` to `.env` and fill in your Wi-Fi credentials:
|
||||||
|
```bash
|
||||||
|
cp .env.template .env
|
||||||
|
```
|
||||||
|
Inside `.env`, configure:
|
||||||
|
- `WIFI_SSID`: Your local Wi-Fi Name.
|
||||||
|
- `WIFI_PASSWORD`: Your local Wi-Fi Password.
|
||||||
|
|
||||||
|
### 2. Run the Flashing Wizard
|
||||||
|
Use the interactive Python script to flash your microcontroller:
|
||||||
|
```bash
|
||||||
|
uv run flash.py
|
||||||
|
```
|
||||||
|
This wizard will prompt you to:
|
||||||
|
1. Select which `.yaml` configuration file to use.
|
||||||
|
2. Select the command to run (such as `Run`, `Compile`, `Upload`, `Logs`, `Validate`, or `Clean`).
|
||||||
|
|
||||||
|
*Note: `uv run` handles the required dependencies (`questionary` and `python-dotenv`) automatically inside an isolated ephemeral environment, so there is no need to manually manage virtualenvs or pip installs.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Home Assistant Integration
|
||||||
|
|
||||||
|
The configurations in this directory connect directly to Home Assistant using the **ESPHome Native API**.
|
||||||
|
|
||||||
|
### Dynamic Sensor Subscriptions
|
||||||
|
Instead of pushing raw screens from Home Assistant, we pull the required entities dynamically into ESPHome's internal variables.
|
||||||
|
|
||||||
|
1. **Outdoor Temperature (`sensor.regensburg_regensburg_temperatur`)**:
|
||||||
|
Pulled automatically via the `homeassistant` platform sensor.
|
||||||
|
2. **Kitchen Temperature (`sensor.home_kuche_wandthermostat_temperature`)**:
|
||||||
|
Pulled automatically from the kitchen wall thermostat.
|
||||||
|
3. **Kitchen Message (`input_text.epaper_message`)**:
|
||||||
|
A Home Assistant `input_text` helper. When the state changes, Home Assistant pushes the new text to the display.
|
||||||
|
|
||||||
|
### Sending Messages from Command Line
|
||||||
|
You can push text warnings or messages directly to the display from the terminal using the helper script:
|
||||||
|
```bash
|
||||||
|
./send_message.sh "Your custom display message here"
|
||||||
|
```
|
||||||
|
This script reads the long-lived API token from the `k8s/home-assistant` secrets folder to authenticate against the Home Assistant API automatically.
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
esphome:
|
||||||
|
name: esdisplay-kitchen
|
||||||
|
comment: "e-Paper display in the kitchen connected to Home Assistant"
|
||||||
|
platformio_options:
|
||||||
|
lib_deps:
|
||||||
|
- bblanchon/ArduinoJson@7.4.2
|
||||||
|
includes:
|
||||||
|
- prometheus_client.h
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: prometheus_connected
|
||||||
|
type: bool
|
||||||
|
restore_value: no
|
||||||
|
initial_value: 'true'
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: nodemcu-32s
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logger:
|
||||||
|
|
||||||
|
# Enable Home Assistant API (auto-discovery)
|
||||||
|
api:
|
||||||
|
password: ""
|
||||||
|
|
||||||
|
# Over-The-Air updates
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: !env_var WIFI_SSID
|
||||||
|
password: !env_var WIFI_PASSWORD
|
||||||
|
|
||||||
|
# Enable fallback hotspot in case wifi fails
|
||||||
|
ap:
|
||||||
|
ssid: "Kitchen Display Fallback"
|
||||||
|
password: "fallbackpassword"
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
# Time synchronization to get current time on display
|
||||||
|
time:
|
||||||
|
- platform: sntp
|
||||||
|
id: sntp_time
|
||||||
|
timezone: "Europe/Berlin"
|
||||||
|
|
||||||
|
# Define local and imported sensors
|
||||||
|
sensor:
|
||||||
|
- platform: homeassistant
|
||||||
|
id: ha_outdoor_temp
|
||||||
|
entity_id: sensor.regensburg_regensburg_temperatur
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: ha_kitchen_temp
|
||||||
|
entity_id: sensor.home_kuche_wandthermostat_temperature
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
# Local template sensors scraped from Prometheus
|
||||||
|
- platform: template
|
||||||
|
id: local_cellar_temp
|
||||||
|
name: "Kitchen Display Cellar Temperature Indoor"
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
- platform: template
|
||||||
|
id: local_cellar_humi
|
||||||
|
name: "Kitchen Display Cellar Humidity Indoor"
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
- platform: template
|
||||||
|
id: local_cellar_dp
|
||||||
|
name: "Kitchen Display Cellar Dewpoint Indoor"
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
- platform: template
|
||||||
|
id: local_cellar_out_temp
|
||||||
|
name: "Kitchen Display Cellar Temperature Outdoor"
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
- platform: template
|
||||||
|
id: local_cellar_out_humi
|
||||||
|
name: "Kitchen Display Cellar Humidity Outdoor"
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
- platform: template
|
||||||
|
id: local_cellar_out_dp
|
||||||
|
name: "Kitchen Display Cellar Dewpoint Outdoor"
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
- platform: template
|
||||||
|
id: local_fan_status
|
||||||
|
name: "Kitchen Display Cellar Fan Status"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
- platform: template
|
||||||
|
id: local_mold_critical
|
||||||
|
name: "Kitchen Display Cellar Mold Critical"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
- platform: template
|
||||||
|
id: local_fan_would_activate
|
||||||
|
name: "Kitchen Display Cellar Fan Activation Criteria"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
- platform: template
|
||||||
|
id: local_mold_threshold
|
||||||
|
name: "Kitchen Display Cellar Mold Threshold"
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
- platform: template
|
||||||
|
id: local_shelly_online
|
||||||
|
name: "Kitchen Display Cellar Shelly Online"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
|
||||||
|
# Interval to scrape metrics from Prometheus every 30 seconds
|
||||||
|
interval:
|
||||||
|
- interval: 30s
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
fetch_prometheus();
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: homeassistant
|
||||||
|
id: ha_message
|
||||||
|
entity_id: input_text.epaper_message
|
||||||
|
internal: true
|
||||||
|
- platform: wifi_info
|
||||||
|
ip_address:
|
||||||
|
name: "IP Address"
|
||||||
|
id: wifi_ip
|
||||||
|
|
||||||
|
# Font configuration (uses Google Fonts Roboto)
|
||||||
|
font:
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: font_title
|
||||||
|
size: 16
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: font_body
|
||||||
|
size: 12
|
||||||
|
|
||||||
|
# SPI bus configuration for e-Paper display
|
||||||
|
spi:
|
||||||
|
clk_pin: GPIO18
|
||||||
|
mosi_pin: GPIO23
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: waveshare_epaper
|
||||||
|
id: epaper_display
|
||||||
|
model: 2.13inv3
|
||||||
|
cs_pin: GPIO5
|
||||||
|
dc_pin: GPIO17
|
||||||
|
reset_pin: GPIO16
|
||||||
|
busy_pin: GPIO4
|
||||||
|
rotation: 90
|
||||||
|
update_interval: 60s
|
||||||
|
lambda: |-
|
||||||
|
// Fill background with white
|
||||||
|
it.fill(COLOR_OFF);
|
||||||
|
|
||||||
|
// Check if Prometheus is reachable
|
||||||
|
if (!id(prometheus_connected)) {
|
||||||
|
it.print(125, 20, id(font_title), TextAlign::CENTER, "PROMETHEUS ERROR");
|
||||||
|
it.print(125, 55, id(font_body), TextAlign::CENTER, "Cannot reach server");
|
||||||
|
it.print(125, 75, id(font_body), TextAlign::CENTER, "Check network or auth");
|
||||||
|
|
||||||
|
// Show time at the bottom if synced
|
||||||
|
it.line(5, 96, 245, 96);
|
||||||
|
char time_str[30];
|
||||||
|
auto time_now = id(sntp_time).now();
|
||||||
|
if (time_now.is_valid()) {
|
||||||
|
it.strftime(5, 100, id(font_body), "Time: %a %H:%M", time_now);
|
||||||
|
} else {
|
||||||
|
it.print(5, 100, id(font_body), "Time: --:--");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SECTION 1: HEADER ---
|
||||||
|
it.print(5, 2, id(font_title), "Cellar Fan (TAUPI)");
|
||||||
|
|
||||||
|
// Time and weekday on the right
|
||||||
|
auto time_now = id(sntp_time).now();
|
||||||
|
if (time_now.is_valid()) {
|
||||||
|
it.strftime(245, 2, id(font_body), TextAlign::TOP_RIGHT, "%a %H:%M", time_now);
|
||||||
|
} else {
|
||||||
|
it.print(245, 2, id(font_body), TextAlign::TOP_RIGHT, "Time: --:--");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw header divider
|
||||||
|
it.line(5, 18, 245, 18);
|
||||||
|
|
||||||
|
// --- SECTION 2: CLIMATE CELLS ---
|
||||||
|
// Vertical separator
|
||||||
|
it.line(122, 22, 122, 82);
|
||||||
|
|
||||||
|
// CELLAR INDOOR (Left)
|
||||||
|
it.print(5, 22, id(font_body), "CELLAR INDOOR");
|
||||||
|
|
||||||
|
if (!isnan(id(local_cellar_temp).state)) {
|
||||||
|
it.printf(5, 36, id(font_body), "Temp: %.1f C", id(local_cellar_temp).state);
|
||||||
|
} else {
|
||||||
|
it.print(5, 36, id(font_body), "Temp: -- C");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isnan(id(local_cellar_humi).state)) {
|
||||||
|
it.printf(5, 49, id(font_body), "Humi: %.0f%%", id(local_cellar_humi).state);
|
||||||
|
} else {
|
||||||
|
it.print(5, 49, id(font_body), "Humi: --%%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isnan(id(local_cellar_dp).state)) {
|
||||||
|
it.printf(5, 62, id(font_body), "Dewp: %.1f C", id(local_cellar_dp).state);
|
||||||
|
} else {
|
||||||
|
it.print(5, 62, id(font_body), "Dewp: -- C");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isnan(id(local_mold_threshold).state)) {
|
||||||
|
it.printf(5, 75, id(font_body), "Limit: %.0f%%", id(local_mold_threshold).state);
|
||||||
|
} else {
|
||||||
|
it.print(5, 75, id(font_body), "Limit: --%%");
|
||||||
|
}
|
||||||
|
|
||||||
|
// CELLAR OUTDOOR (Right)
|
||||||
|
it.print(128, 22, id(font_body), "CELLAR OUTDOOR");
|
||||||
|
|
||||||
|
if (!isnan(id(local_cellar_out_temp).state)) {
|
||||||
|
it.printf(128, 36, id(font_body), "Temp: %.1f C", id(local_cellar_out_temp).state);
|
||||||
|
} else {
|
||||||
|
it.print(128, 36, id(font_body), "Temp: -- C");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isnan(id(local_cellar_out_humi).state)) {
|
||||||
|
it.printf(128, 49, id(font_body), "Humi: %.0f%%", id(local_cellar_out_humi).state);
|
||||||
|
} else {
|
||||||
|
it.print(128, 49, id(font_body), "Humi: --%%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isnan(id(local_cellar_out_dp).state)) {
|
||||||
|
it.printf(128, 62, id(font_body), "Dewp: %.1f C", id(local_cellar_out_dp).state);
|
||||||
|
} else {
|
||||||
|
it.print(128, 62, id(font_body), "Dewp: -- C");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SECTION 3: STATUS BAR (Bottom) ---
|
||||||
|
it.line(5, 86, 245, 86);
|
||||||
|
|
||||||
|
// Fan status
|
||||||
|
const char* fan_state = "OFF";
|
||||||
|
if (!isnan(id(local_fan_status).state) && id(local_fan_status).state > 0.5) {
|
||||||
|
fan_state = "ON";
|
||||||
|
}
|
||||||
|
it.printf(5, 90, id(font_body), "Fan: %s", fan_state);
|
||||||
|
|
||||||
|
// Mold status
|
||||||
|
const char* mold_state = "SAFE";
|
||||||
|
if (!isnan(id(local_mold_critical).state) && id(local_mold_critical).state > 0.5) {
|
||||||
|
mold_state = "CRITICAL";
|
||||||
|
}
|
||||||
|
it.printf(128, 90, id(font_body), "Mold: %s", mold_state);
|
||||||
|
|
||||||
|
// Criteria status
|
||||||
|
const char* crit_state = "NO";
|
||||||
|
if (!isnan(id(local_fan_would_activate).state) && id(local_fan_would_activate).state > 0.5) {
|
||||||
|
crit_state = "YES";
|
||||||
|
}
|
||||||
|
it.printf(5, 104, id(font_body), "Criteria: %s", crit_state);
|
||||||
|
|
||||||
|
// Shelly Status
|
||||||
|
const char* shelly_state = "OFFLINE";
|
||||||
|
if (!isnan(id(local_shelly_online).state) && id(local_shelly_online).state > 0.5) {
|
||||||
|
shelly_state = "ONLINE";
|
||||||
|
}
|
||||||
|
it.printf(128, 104, id(font_body), "Shelly: %s", shelly_state);
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.11"
|
||||||
|
# dependencies = [
|
||||||
|
# "python-dotenv",
|
||||||
|
# "questionary",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Ensure we run from the script's directory
|
||||||
|
script_dir = Path(__file__).resolve().parent
|
||||||
|
os.chdir(script_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import questionary
|
||||||
|
except ImportError:
|
||||||
|
print("[Error] Failed to import questionary. Run this script using 'uv run flash.py'.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def run_esphome(args, env):
|
||||||
|
esphome_bin = shutil.which("esphome")
|
||||||
|
if not esphome_bin:
|
||||||
|
print("\n[Error] 'esphome' CLI not found on PATH.")
|
||||||
|
print("Please install it (e.g. 'brew install esphome' or 'pip install esphome').\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = [esphome_bin] + args
|
||||||
|
print(f"\nExecuting: {' '.join(cmd)}\n")
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
env=env,
|
||||||
|
stdout=sys.stdout,
|
||||||
|
stderr=sys.stderr,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
process.wait()
|
||||||
|
return process.returncode
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nExecution interrupted.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def get_device_name(yaml_path):
|
||||||
|
try:
|
||||||
|
with open(yaml_path, "r") as f:
|
||||||
|
in_esphome = False
|
||||||
|
for line in f:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("esphome:"):
|
||||||
|
in_esphome = True
|
||||||
|
continue
|
||||||
|
if in_esphome:
|
||||||
|
if line.strip() and not line.startswith(" ") and not line.startswith("\t"):
|
||||||
|
break
|
||||||
|
if stripped.startswith("name:"):
|
||||||
|
name = stripped.split(":", 1)[1].split("#", 1)[0].strip()
|
||||||
|
return name.replace('"', '').replace("'", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
env_file = script_dir / ".env"
|
||||||
|
template_file = script_dir / ".env.template"
|
||||||
|
|
||||||
|
# Auto-copy template if .env is missing
|
||||||
|
if not env_file.exists():
|
||||||
|
if template_file.exists():
|
||||||
|
print(f"'.env' not found. Copying '.env.template' to '.env'...")
|
||||||
|
shutil.copy(template_file, env_file)
|
||||||
|
print("\n[Action Required] Please open '.env' and fill in your Wi-Fi credentials.")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("[Error] Neither '.env' nor '.env.template' could be found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
load_dotenv(env_file)
|
||||||
|
|
||||||
|
# 1. Select YAML Configuration
|
||||||
|
yaml_files = sorted([f.name for f in script_dir.glob("*.yaml")])
|
||||||
|
if not yaml_files:
|
||||||
|
print("[Error] No ESPHome configuration files (*.yaml) found in this directory.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
selected_yaml = questionary.select(
|
||||||
|
"Select the ESPHome configuration to compile/flash:",
|
||||||
|
choices=yaml_files
|
||||||
|
).ask()
|
||||||
|
|
||||||
|
if not selected_yaml:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 2. Select Command Action
|
||||||
|
commands = [
|
||||||
|
questionary.Choice("Run (Validate, compile, upload, and start logs)", "run"),
|
||||||
|
questionary.Choice("Compile (Build only)", "compile"),
|
||||||
|
questionary.Choice("Upload (Flash compiled binary only)", "upload"),
|
||||||
|
questionary.Choice("Logs (Stream device logs)", "logs"),
|
||||||
|
questionary.Choice("Validate (Check syntax only)", "config"),
|
||||||
|
questionary.Choice("Clean (Clear build artifacts)", "clean"),
|
||||||
|
]
|
||||||
|
|
||||||
|
selected_cmd = questionary.select(
|
||||||
|
"Select ESPHome action to perform:",
|
||||||
|
choices=commands
|
||||||
|
).ask()
|
||||||
|
|
||||||
|
if not selected_cmd:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# If upload is selected, verify build files exist
|
||||||
|
if selected_cmd == "upload":
|
||||||
|
device_name = get_device_name(script_dir / selected_yaml)
|
||||||
|
if device_name:
|
||||||
|
build_dir = script_dir / ".esphome" / "build" / device_name
|
||||||
|
if not build_dir.exists():
|
||||||
|
print(f"\n[Warning] Build directory for '{device_name}' does not exist.")
|
||||||
|
print("You must compile the project before uploading it.")
|
||||||
|
confirm = questionary.confirm(
|
||||||
|
"Would you like to compile + upload the configuration now (Run)?",
|
||||||
|
default=True
|
||||||
|
).ask()
|
||||||
|
if confirm:
|
||||||
|
selected_cmd = "run"
|
||||||
|
else:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Prepare system environment
|
||||||
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
# Execute ESPHome command
|
||||||
|
exit_code = run_esphome([selected_cmd, selected_yaml], env)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "esphome.h"
|
||||||
|
|
||||||
|
// Forward declare the global variables/pointers defined in main.cpp
|
||||||
|
extern template_::TemplateSensor *local_cellar_temp;
|
||||||
|
extern template_::TemplateSensor *local_cellar_humi;
|
||||||
|
extern template_::TemplateSensor *local_cellar_dp;
|
||||||
|
extern template_::TemplateSensor *local_cellar_out_temp;
|
||||||
|
extern template_::TemplateSensor *local_cellar_out_humi;
|
||||||
|
extern template_::TemplateSensor *local_cellar_out_dp;
|
||||||
|
extern template_::TemplateSensor *local_fan_status;
|
||||||
|
extern template_::TemplateSensor *local_mold_critical;
|
||||||
|
extern template_::TemplateSensor *local_fan_would_activate;
|
||||||
|
extern template_::TemplateSensor *local_mold_threshold;
|
||||||
|
extern template_::TemplateSensor *local_shelly_online;
|
||||||
|
extern globals::GlobalsComponent<bool> *prometheus_connected;
|
||||||
|
|
||||||
|
inline void fetch_prometheus() {
|
||||||
|
WiFiClient client;
|
||||||
|
if (!client.connect("prometheus.haumdaucher.de", 80)) {
|
||||||
|
prometheus_connected->value() = false;
|
||||||
|
ESP_LOGW("taupi", "Failed to connect to Prometheus over HTTP");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send HTTP/1.0 request to avoid chunked transfer encoding!
|
||||||
|
client.println("GET /api/v1/query?query=%7B__name__%3D~%22taupi_.*%7Cup%22%2Cjob%3D%22taupi-fan%22%7D HTTP/1.0");
|
||||||
|
client.println("Host: prometheus.haumdaucher.de");
|
||||||
|
client.println("Authorization: Basic bW9yaXR6OlZhZWo2UXVpZXF1NHZvMGphZVJh");
|
||||||
|
client.println("Connection: close");
|
||||||
|
client.println();
|
||||||
|
|
||||||
|
// Read headers
|
||||||
|
bool headers_ended = false;
|
||||||
|
std::string body = "";
|
||||||
|
while (client.connected() || client.available()) {
|
||||||
|
if (client.available()) {
|
||||||
|
if (!headers_ended) {
|
||||||
|
String line = client.readStringUntil('\n');
|
||||||
|
if (line == "\r" || line == "") {
|
||||||
|
headers_ended = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String data = client.readString();
|
||||||
|
body += data.c_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.stop();
|
||||||
|
|
||||||
|
if (body.length() == 0) {
|
||||||
|
prometheus_connected->value() = false;
|
||||||
|
ESP_LOGW("taupi", "Empty response body from Prometheus");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prometheus_connected->value() = true;
|
||||||
|
|
||||||
|
// Parse body using ArduinoJson 7
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, body);
|
||||||
|
if (!error) {
|
||||||
|
JsonObject root = doc.as<JsonObject>();
|
||||||
|
JsonObject data = root["data"];
|
||||||
|
JsonArray result = data["result"];
|
||||||
|
for (JsonObject item : result) {
|
||||||
|
JsonObject metric = item["metric"];
|
||||||
|
std::string name = metric["__name__"].as<std::string>();
|
||||||
|
JsonArray value = item["value"];
|
||||||
|
if (value.size() >= 2) {
|
||||||
|
std::string val_str = value[1].as<std::string>();
|
||||||
|
float val = atof(val_str.c_str());
|
||||||
|
|
||||||
|
if (name == "taupi_temperature_celsius_innen") local_cellar_temp->publish_state(val);
|
||||||
|
else if (name == "taupi_humidity_percent_innen") local_cellar_humi->publish_state(val);
|
||||||
|
else if (name == "taupi_dewpoint_celsius_innen") local_cellar_dp->publish_state(val);
|
||||||
|
else if (name == "taupi_temperature_celsius_aussen") local_cellar_out_temp->publish_state(val);
|
||||||
|
else if (name == "taupi_humidity_percent_aussen") local_cellar_out_humi->publish_state(val);
|
||||||
|
else if (name == "taupi_dewpoint_celsius_aussen") local_cellar_out_dp->publish_state(val);
|
||||||
|
else if (name == "taupi_relay_status") local_fan_status->publish_state(val);
|
||||||
|
else if (name == "taupi_is_critical") local_mold_critical->publish_state(val);
|
||||||
|
else if (name == "taupi_would_fan_activate") local_fan_would_activate->publish_state(val);
|
||||||
|
else if (name == "taupi_critical_humidity_threshold_percent") local_mold_threshold->publish_state(val);
|
||||||
|
else if (name == "up") {
|
||||||
|
std::string job = metric["job"].as<std::string>();
|
||||||
|
if (job == "taupi-fan") {
|
||||||
|
local_shelly_online->publish_state(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ESP_LOGD("taupi", "Successfully parsed Taupi metrics from Prometheus");
|
||||||
|
} else {
|
||||||
|
ESP_LOGW("taupi", "JSON parsing failed: %s", error.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Exit on error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||||
|
|
||||||
|
# Read the token from the home-assistant secrets folder
|
||||||
|
TOKEN_FILE="${CDIR}/../k8s/home-assistant/tmp_long_lived_token.secret.yml"
|
||||||
|
|
||||||
|
if [ ! -f "$TOKEN_FILE" ]; then
|
||||||
|
echo "Error: Home Assistant token file not found at $TOKEN_FILE."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the token (grab the line that is not a comment)
|
||||||
|
TOKEN=$(grep -v '^#' "$TOKEN_FILE" | tr -d '\n' | tr -d '\r' | xargs)
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "Error: Token extracted from $TOKEN_FILE is empty."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the message from arguments
|
||||||
|
MESSAGE="${1:-"Hello from command line!"}"
|
||||||
|
|
||||||
|
echo "Sending message: '$MESSAGE' to Home Assistant (https://hass.moritzgraf.de)..."
|
||||||
|
|
||||||
|
# Call the Home Assistant REST API to set the value of the input_text helper
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"entity_id\": \"input_text.epaper_message\", \"value\": \"$MESSAGE\"}" \
|
||||||
|
https://hass.moritzgraf.de/api/services/input_text/set_value
|
||||||
|
|
||||||
|
echo -e "\nDone!"
|
||||||
Loading…
Reference in New Issue