initial commit

This commit is contained in:
root
2026-06-28 14:27:20 -04:00
commit ae0f1f559e
115 changed files with 30411 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"dashboard_view": "cards",
"theme": "system",
"navigator_visible": true,
"device_editor_layout": "both",
"secrets_editor_layout": "visual",
"table_page_size": 25,
"table_column_visibility": {},
"table_sort_column": null,
"table_sort_direction": null,
"experience_level": "expert",
"remote_compute_only": false,
"onboarding_completed_version": 0
}
+485
View File
@@ -0,0 +1,485 @@
{
"_board_id_user_set_migrated": true,
"airqualitysensor-3.yaml": {
"mac_address": "1C:DB:D4:5A:D6:38"
},
"_dashboard_identity": {
"dashboard_id": "tEjFIsUFc3OUB8auNqhhKLKr6NoX9POx"
},
"master_bedroom_remote.yaml": {
"mac_address": "B0:CB:D8:D6:CB:2C"
},
"esphome-web-0fdcf4.yaml": {
"mac_address": "A0:85:E3:0F:DC:F4"
},
"_firmware_jobs": [
{
"job_id": "454c2dcf9ffc",
"configuration": "airqualitysensor-2.yaml",
"job_type": "upload",
"status": "failed",
"created_at": "2026-06-23T15:59:03.820028+00:00",
"started_at": "2026-06-23T15:59:03.820176+00:00",
"completed_at": "2026-06-23T15:59:24.360620+00:00",
"exit_code": 1,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "b0b33805fea9",
"configuration": "airqualitysensor-2.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-23T15:58:51.081966+00:00",
"started_at": "2026-06-23T15:58:51.082171+00:00",
"completed_at": "2026-06-23T15:59:03.816937+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "a3b449b55379",
"configuration": "airqualitysensor-1.yaml",
"job_type": "upload",
"status": "failed",
"created_at": "2026-06-23T12:12:15.609235+00:00",
"started_at": "2026-06-23T12:12:15.609406+00:00",
"completed_at": "2026-06-23T12:12:36.129802+00:00",
"exit_code": 1,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "959592b15d5d",
"configuration": "airqualitysensor-1.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-23T12:12:02.678379+00:00",
"started_at": "2026-06-23T12:12:02.678577+00:00",
"completed_at": "2026-06-23T12:12:15.596683+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "f210d3e38469",
"configuration": "home-energy-monitor.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-06-23T12:10:06.335084+00:00",
"started_at": "2026-06-23T12:10:06.335247+00:00",
"completed_at": "2026-06-23T12:10:16.319195+00:00",
"exit_code": 0,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "e7f4e049b845",
"configuration": "home-energy-monitor.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-23T12:08:13.748861+00:00",
"started_at": "2026-06-23T12:08:13.749101+00:00",
"completed_at": "2026-06-23T12:10:06.331091+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "49018100eb10",
"configuration": "esphome-web-0fdcf4.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-06-22T07:42:25.069069+00:00",
"started_at": "2026-06-22T07:42:25.069205+00:00",
"completed_at": "2026-06-22T07:42:35.367662+00:00",
"exit_code": 0,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "b018ef39d4ae",
"configuration": "esphome-web-0fdcf4.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-22T07:40:28.776647+00:00",
"started_at": "2026-06-22T07:40:28.776800+00:00",
"completed_at": "2026-06-22T07:42:25.066345+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "a6fe4e12b390",
"configuration": "esp32-h2c-camera.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-06-22T07:38:05.100640+00:00",
"started_at": "2026-06-22T07:38:05.100779+00:00",
"completed_at": "2026-06-22T07:38:20.875311+00:00",
"exit_code": 0,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "89e9be54a789",
"configuration": "esp32-h2c-camera.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-22T07:36:19.527708+00:00",
"started_at": "2026-06-22T07:36:19.527864+00:00",
"completed_at": "2026-06-22T07:38:05.097814+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "6375c915454b",
"configuration": "master_bedroom_remote.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-06-22T07:34:04.950629+00:00",
"started_at": "2026-06-22T07:34:04.950767+00:00",
"completed_at": "2026-06-22T07:34:15.205602+00:00",
"exit_code": 0,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "b78f643229b3",
"configuration": "master_bedroom_remote.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-22T07:32:02.047324+00:00",
"started_at": "2026-06-22T07:32:02.047531+00:00",
"completed_at": "2026-06-22T07:34:04.947621+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "2ce572f89e47",
"configuration": "laundry-litter-box-camera.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-06-22T07:29:16.588267+00:00",
"started_at": "2026-06-22T07:29:16.588413+00:00",
"completed_at": "2026-06-22T07:29:56.324221+00:00",
"exit_code": 0,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "e416079bc9ee",
"configuration": "laundry-litter-box-camera.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-22T07:27:37.929185+00:00",
"started_at": "2026-06-22T07:27:37.929410+00:00",
"completed_at": "2026-06-22T07:29:16.583896+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "e87fc4673ed1",
"configuration": "airqualitysensor-3.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-06-22T07:06:34.226565+00:00",
"started_at": "2026-06-22T07:06:34.226708+00:00",
"completed_at": "2026-06-22T07:06:45.682261+00:00",
"exit_code": 0,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "564fcb99c4f1",
"configuration": "airqualitysensor-3.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-22T07:04:34.798415+00:00",
"started_at": "2026-06-22T07:04:34.798571+00:00",
"completed_at": "2026-06-22T07:06:34.223520+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "49799549d47e",
"configuration": "cat-medication-tracker.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-06-22T07:02:21.611975+00:00",
"started_at": "2026-06-22T07:02:21.612142+00:00",
"completed_at": "2026-06-22T07:02:30.511802+00:00",
"exit_code": 0,
"error": null,
"port": "OTA",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
},
{
"job_id": "a46e164b02dd",
"configuration": "cat-medication-tracker.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-06-22T07:00:00.362587+00:00",
"started_at": "2026-06-22T07:00:00.362775+00:00",
"completed_at": "2026-06-22T07:02:21.608428+00:00",
"exit_code": 0,
"error": null,
"port": "",
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": ""
}
],
"attic-climate-sensor.yaml": {
"mac_address": "CC:8D:A2:C0:70:C4"
},
"cat-medication-tracker.yaml": {
"mac_address": "B0:CB:D8:03:5E:14"
},
"declan-a1-camera.yaml": {
"mac_address": "D4:E9:F4:A3:59:E8"
},
"esp32-h2c-camera.yaml": {
"mac_address": "E8:F6:0A:87:12:B0"
},
"laundry-litter-box-camera.yaml": {
"mac_address": "D4:E9:F4:A3:59:E8"
},
"home-energy-monitor.yaml": {
"mac_address": "8C:4F:00:AD:42:00"
}
}
View File
+5
View File
@@ -0,0 +1,5 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml
+47
View File
@@ -0,0 +1,47 @@
#include "esphome.h"
#define MAX17048_ADDRESS 0x36
#define MAX17048_VCELL 0x02 // voltage
#define MAX17048_SOC 0x04 // percentage
#define MAX17048_MODE 0x06
#define MAX17048_VERSION 0x08
#define MAX17048_CONFIG 0x0c
#define MAX17048_COMMAND 0xfe
class MAX17048Sensor : public PollingComponent, public Sensor {
public:
Sensor *voltage_sensor = new Sensor();
Sensor *percentage_sensor = new Sensor();
MAX17048Sensor() : PollingComponent(10000) {}
void setup() override {
// Initialize the device here. Usually Wire.begin() will be called in here,
// though that call is unnecessary if you have an 'i2c:' entry in your config
ESP_LOGD("custom", "Starting up MAX17048 sensor");
Wire.begin();
}
uint16_t read16(uint8_t reg) {
uint16_t temp;
Wire.begin();
Wire.beginTransmission(MAX17048_ADDRESS);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom(MAX17048_ADDRESS, 2);
temp = (uint16_t)Wire.read() << 8;
temp |= (uint16_t)Wire.read();
Wire.endTransmission();
return temp;
}
void update() override {
float voltage = (float)(read16(MAX17048_VCELL)) * 78.125 / 1000000;
voltage_sensor->publish_state(voltage);
uint16_t percentage_tmp = read16(MAX17048_SOC);
float percentage = (float)(percentage_tmp) / 256;
percentage_sensor->publish_state(percentage);
}
};
+824
View File
@@ -0,0 +1,824 @@
substitutions: #substitute your own values in this section
internal_temp_sensor: sensor.meat_heater_current_temperature #entity from Home Assistant
outside_temp_sensor: sensor.home_realfeel_temperature #entity from Home Assistant
weather_entity: weather.home #entity from Home Assistant
todays_forecast_high_entity: sensor.home_realfeel_temperature_max_day_0 #entity from Home Assistant
todays_forecast_low_entity: sensor.home_realfeel_temperature_min_day_0 #entity from Home Assistant
device_name: airqualitysensor-basement
friendly_name: "Air Sensor - Basement"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
on_boot:
priority: -100
then:
- light.turn_on:
id: led
brightness: 0.4
effect: "Rainbow Effect"
esp32:
board: esp32-s3-devkitc-1
cpu_frequency: 240MHz
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
# Enable logging
logger:
level: INFO
# Enable Home Assistant API
api:
encryption:
key: "gnQzjc98wQKUP3qX6+FeU9JchMPtjJwlKQejOB/mQDU="
ota:
- platform: esphome
password: "d4d36e1c98d4b7e2069295540a20a1aa"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Airqualitysensor-1"
password: "MpBryi70smN4"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if using static IPs and don't need discovery
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
captive_portal:
web_server:
port: 80
script:
# SCRIPT 1: Runs when the mode is changed from the select
- id: update_led_state
then:
- lambda: |-
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
return; // Exit script, leave rainbow alone
}
std::string quality = id(iaq_reading).state;
float lux = id(ambient_light).state;
std::string mode = id(led_mode).state;
float brightness = 0.15;
float r = 0.0, g = 0.0, b = 0.0;
std::string selected_effect = "None";
// --- 1. Determine desired color ---
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
else if (quality == "Unhealthy") {
r = 1.0; g = 0.0; b = 1.0;
selected_effect = "Alert Flash"; // Alert overrides mode
}
// --- 2. Determine desired effect based on mode (if not alerting) ---
if (selected_effect != "Alert Flash") {
if (mode == "Breathing") {
selected_effect = "Breathing";
} else if (mode == "Scanner") {
selected_effect = "Scanner";
}
}
// --- 3. Apply changes ---
auto call = id(led).turn_on();
call.set_rgb(r, g, b); // Always set color
if (selected_effect == "None") {
// We want a solid light (Auto/Manual)
if (mode == "Auto") {
if (isnan(lux)) lux = 0;
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
if (brightness > 0.7) brightness = 0.7;
if (brightness < 0.15) brightness = 0.15;
} else { // Manual
brightness = id(iaq_led_brightness).state;
}
call.set_brightness(brightness);
}
// This script *always* sets the effect
call.set_effect(selected_effect);
call.perform();
# SCRIPT 2: Runs when IAQ *value* changes (every 10s)
- id: update_led_color
then:
- lambda: |-
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
return; // Exit script, leave rainbow alone
}
std::string quality = id(iaq_reading).state;
std::string mode = id(led_mode).state;
float lux = id(ambient_light).state;
float r = 0.0, g = 0.0, b = 0.0;
// --- 1. Determine desired color ---
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
else if (quality == "Unhealthy") {
r = 1.0; g = 0.0; b = 1.0;
// Force Alert Flash and EXIT
id(led).turn_on().set_rgb(r, g, b).set_effect("Alert Flash").perform();
return;
}
// --- 2. If we are here, AQ is OK. Reset Effect! ---
auto call = id(led).turn_on();
call.set_rgb(r, g, b);
// Determine which effect to restore based on the Select Mode
std::string restore_effect = "None";
if (mode == "Breathing") restore_effect = "Breathing";
else if (mode == "Scanner") restore_effect = "Scanner";
// Explicitly set the effect (This stops the Alert Flash)
call.set_effect(restore_effect);
// --- 3. Apply Brightness ---
if (mode == "Auto") {
float brightness = 0.15;
if (isnan(lux)) lux = 0;
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
if (brightness > 0.7) brightness = 0.7;
if (brightness < 0.15) brightness = 0.15;
call.set_brightness(brightness);
} else if (mode == "Manual") {
call.set_brightness(id(iaq_led_brightness).state);
}
call.perform();
globals:
- id: iaq_index
type: int
restore_value: no
initial_value: '0'
- id: computed_brightness
type: float
restore_value: no
initial_value: '0.15'
uart:
tx_pin: GPIO17
rx_pin: GPIO18
baud_rate: 9600
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
chipset: WS2812
pin: GPIO16
num_leds: 5
name: "LED"
id: led
icon: mdi:led-on
default_transition_length: 0s
disabled_by_default: False
effects:
- pulse:
name: "Breathing"
min_brightness: 0.15
max_brightness: 0.45
transition_length: 3000ms
update_interval: 3000ms
- addressable_scan:
name: "Scanner"
scan_width: 2
move_interval: 100ms
- flicker:
name: "Alert Flash"
alpha: 95%
intensity: 1.5%
- addressable_rainbow:
name: "Rainbow Effect"
speed: 10
width: 50
i2c:
sda: GPIO8
scl: GPIO9
scan: true
id: bus_a
frequency: 100kHz
switch:
- platform: template
name: "OLED Power"
id: oled_power
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
turn_on_action:
- logger.log: "OLED turned ON"
turn_off_action:
- logger.log: "OLED turned OFF"
select:
- platform: template
name: "LED Mode"
id: led_mode
options:
- "Auto"
- "Manual"
- "Breathing"
- "Scanner"
optimistic: true
restore_value: true
on_value:
then:
- script.execute: update_led_state
number:
- platform: template
name: "IAQ LED Brightness"
id: iaq_led_brightness
min_value: 0.15
max_value: 1.0
step: 0.01
optimistic: true
restore_value: true
sensor:
- platform: uptime
name: Uptime Sensor
- platform: homeassistant
id: inside_temperature
entity_id: $internal_temp_sensor
internal: true
- platform: homeassistant
id: outside_temperature
entity_id: $outside_temp_sensor
internal: true
- platform: homeassistant
id: todays_forecast_high
entity_id: $todays_forecast_high_entity
internal: true
- platform: homeassistant
id: todays_forecast_low
entity_id: $todays_forecast_low_entity
internal: true
- platform: pmsx003
type: PMSX003
update_interval: 60s
pm_1_0:
id: pm1
name: "PM <1.0µm Concentration"
pm_2_5:
id: pm25
name: "PM <2.5µm Concentration"
pm_10_0:
id: pm10
name: "PM <10.0µm Concentration"
- platform: bme680
temperature:
name: "BME680 Temperature"
id: bmetemp
oversampling: 16x
pressure:
name: "BME680 Pressure"
id: bmepressure
humidity:
name: "BME680 Humidity"
id: bmehum
gas_resistance:
name: "BME680 Gas Resistance"
id: bmegas
address: 0x77
update_interval: 60s
## CO²/VOC Sensor
- platform: ccs811
eco2:
name: "CCS811 CO²"
accuracy_decimals: 0
id: eco2
tvoc:
name: "CCS811 T-VOC"
accuracy_decimals: 0
id: tvoc
address: 0x5A
update_interval: 60s
temperature: bmetemp
humidity: bmehum
## After Calibration, Uncomment and change "baseline:"
baseline: 0x9CB1
- platform: template
name: "Humidity Sensor"
id: humi
unit_of_measurement: "%"
accuracy_decimals: 1
lambda: |-
return id(bmehum).state;
update_interval: 60s
- platform: wifi_signal
name: AQ WiFi Signal
update_interval: 60s
internal: true
- platform: veml7700
address: 0x10
update_interval: 60s
# short variant of sensor definition:
ambient_light:
name: "Ambient Light"
id: ambient_light
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
font:
- file: "fonts/Roboto-Regular.ttf"
id: robotto
size: 10
- file: "fonts/Roboto-Regular.ttf"
id: font2
size: 12
- file: "fonts/Poppins-Regular.ttf"
id: font1
size: 10
- file: "fonts/Poppins-Regular.ttf"
id: poppinslarger
size: 12
- file: "fonts/Poppins-SemiBold.ttf"
id: poppinsbold
size: 10
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font3
size: 18
glyphs:
- "\U000F13D5" #mdi:home-minus-outline
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font4
size: 40
glyphs:
- "\U000F0594" #"clear-night"
- "\U000F0590" #"cloudy"
- "\U000F0591" #"fog"
- "\U000F0592" #"hail"
- "\U000F0593" #"lightning"
- "\U000F067E" #"lightning-rainy"
- "\U000F0595" #"partlycloudy"
- "\U000F0596" #"pouring"
- "\U000F0597" #"rainy"
- "\U000F0598" #"snowy"
- "\U000F067F" #"snowy-rainy"
- "\U000F0599" #"sunny"
- "\U000F059D" #"windy"
- "\U000F059E" #"windy-variant"
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
id: oled_display
rotation: 0
i2c_id: bus_a
pages:
- id: page_air_quality
lambda: |-
if (id(oled_power).state) { // only draw if display is ON
{
// Combine the final string first
char full_text[64];
if (id(iaq_reading).has_state()) {
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
} else {
sprintf(full_text, "Air Quality: Booting...");
}
int x, y, w, h;
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
int center_x = (128 - w) / 2; // OLED is 128px wide
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
}
// --- Temp + Humidity on same line ---
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
// --- Other sensor lines ---
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(eco2).state);
it.printf(0, 36, id(font1), "TVOC: %.0f ppb", id(tvoc).state);
if (!isnan(id(pm25).state)) {
it.printf(0, 46, id(font1), "PM2.5: %.0f", id(pm25).state);
} else {
it.printf(0, 46, id(font1), "PM2.5: ---");
}
}
- id: page_environment
lambda: |-
if (id(oled_power).state) { // only draw if display is ON
{
// Combine the final string first
char full_text[64];
if (id(iaq_reading).has_state()) {
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
} else {
sprintf(full_text, "Air Quality: Booting...");
}
int x, y, w, h;
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
int center_x = (128 - w) / 2; // OLED is 128px wide
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
}
// --- Temp + Humidity on same line ---
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
// --- Other sensor lines ---
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(ambient_light).state);
}
- id: page_weather
lambda: |-
if (id(weather_state).has_state()) {
std::map<std::string, std::string> weather_icon_map
{
{"clear-night", "\U000F0594"},
{"cloudy", "\U000F0590"},
{"fog", "\U000F0591"},
{"hail", "\U000F0592"},
{"lightning", "\U000F0593"},
{"lightning-rainy", "\U000F067E"},
{"partlycloudy", "\U000F0595"},
{"pouring", "\U000F0596"},
{"rainy", "\U000F0597"},
{"snowy", "\U000F0598"},
{"snowy-rainy", "\U000F067F"},
{"sunny", "\U000F0599"},
{"windy", "\U000F059D"},
{"windy-variant", "\U000F059E"},
};
it.printf(0, it.get_height(), id(font4), TextAlign::BASELINE_LEFT, weather_icon_map[id(weather_state).state.c_str()].c_str());
}
// Print time in HH:MM format
it.strftime(0, 0, id(font1), TextAlign::TOP_LEFT, "%H:%M %a", id(homeassistant_time).now());
//Print day of week
//it.strftime(40, 0, id(font1), TextAlign::TOP_LEFT, "%a", id(homeassistant_time).now());
it.line(0, 20, it.get_width(), 20);
//Print home icon
//it.printf(70, 0, id(font3), "\U000F13D5");
// Print inside temperature (from homeassistant sensor)
if (id(inside_temperature).has_state()) {
it.printf(it.get_width(), 0, id(font1), TextAlign::TOP_RIGHT , "%7.1f°", id(inside_temperature).state);
}
// Print outside temperature (from homeassistant sensor)
if (id(outside_temperature).has_state()) {
it.printf(42, 32, id(font2), "%.1f°", id(outside_temperature).state);
}
// Print Forecast High
if (id(todays_forecast_high).has_state()) {
it.printf(it.get_width(), 32, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_high).state);
}
// Print Forecast Low
if (id(todays_forecast_low).has_state()) {
it.printf(it.get_width(), 48, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_low).state);
}
text_sensor:
- platform: homeassistant
id: weather_state
name: "Current Weather Icon"
entity_id: $weather_entity
internal: true
- platform: template
name: "PM 2.5 Air Quality"
icon: mdi:air-filter
id: aq_reading
lambda: |-
if (id(pm25).state <= 12) {
return {"Good"};
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
return {"Moderate"};
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
return {"Unhealthy(SG)"};
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
return {"Unhealthy"};
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
return {"Very Unhealthy"};
}
else if (id(pm25).state >= 250.5) {
return {"Hazardous"};
}
return {};
update_interval: 60s
- platform: template
name: "PM 10 Air Quality"
icon: mdi:air-filter
id: aq_10_reading
lambda: |-
if (id(pm10).state <= 54) {
return {"Good"};
}
else if ((id(pm10).state >= 55) && (id(pm10).state <= 154)) {
return {"Moderate"};
}
else if ((id(pm10).state >= 155) && (id(pm10).state <= 254)) {
return {"Unhealthy(SG)"};
}
else if ((id(pm10).state >= 255) && (id(pm10).state <= 354)) {
return {"Unhealthy"};
}
else if ((id(pm10).state >= 355) && (id(pm10).state <= 424)) {
return {"Very Unhealthy"};
}
else if (id(pm10).state >= 425) {
return {"Hazardous"};
}
return {};
update_interval: 60s
- platform: template
name: "Livingroom IAQ"
icon: "mdi:air-filter"
id: iaq_reading
lambda: |-
// 1. Safety Check: Ensure all sensors have valid numbers
if (isnan(id(humi).state) || isnan(id(pm25).state) || isnan(id(eco2).state) || isnan(id(tvoc).state)) {
return {"Booting..."};
}
id(iaq_index) = 0;
if (id(humi).state < 10 or id(humi).state > 90) {
id(iaq_index) += 1;
}
else if (id(humi).state < 20 or id(humi).state > 80) {
id(iaq_index) += 2;
}
else if (id(humi).state < 30 or id(humi).state > 70) {
id(iaq_index) += 3;
}
else if (id(humi).state < 40 or id(humi).state > 60) {
id(iaq_index) += 4;
}
else if (id(humi).state >= 40 and id(humi).state <= 60) {
id(iaq_index) += 5;
}
if (id(pm25).state <= 12) {
id(iaq_index) += 6;
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
id(iaq_index) += 5;
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
id(iaq_index) += 4;
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
id(iaq_index) += 3;
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
id(iaq_index) += 2;
}
else if (id(pm25).state >= 250.5) {
id(iaq_index) += 1;
}
if (id(eco2).state <= 600) {
id(iaq_index) += 5;
}
else if (id(eco2).state <= 800) {
id(iaq_index) += 4;
}
else if (id(eco2).state <= 1500) {
id(iaq_index) += 3;
}
else if (id(eco2).state <= 1800) {
id(iaq_index) += 2;
}
else if (id(eco2).state > 1800) {
id(iaq_index) += 1;
}
if (id(tvoc).state <= 65) {
id(iaq_index) += 5;
}
else if (id(tvoc).state <= 220) {
id(iaq_index) += 4;
}
else if (id(tvoc).state <= 660) {
id(iaq_index) += 3;
}
else if (id(tvoc).state <= 2200) {
id(iaq_index) += 2;
}
else if (id(tvoc).state > 2200) {
id(iaq_index) += 1;
}
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
if (id(iaq_index) <= 11) {
return {"Unhealthy"};
}
else if (id(iaq_index) <= 14) {
return {"Poor"};
}
else if (id(iaq_index) <= 17) {
return {"Moderate"};
}
else if (id(iaq_index) <= 19) {
return {"Good"};
}
else if (id(iaq_index) > 19) {
return {"Excellent"};
}
return {};
update_interval: 60s
on_value:
then:
- script.execute: update_led_color
- platform: template
name: "Livingroom IAQ Calculation"
icon: "mdi:air-filter"
id: iaq_reading_calculation
lambda: |-
id(iaq_index) = 0;
if (id(humi).state < 10 or id(humi).state > 90) {
id(iaq_index) += 1;
}
else if (id(humi).state < 20 or id(humi).state > 80) {
id(iaq_index) += 2;
}
else if (id(humi).state < 30 or id(humi).state > 70) {
id(iaq_index) += 3;
}
else if (id(humi).state < 40 or id(humi).state > 60) {
id(iaq_index) += 4;
}
else if (id(humi).state >= 40 and id(humi).state <= 60) {
id(iaq_index) += 5;
}
if (id(pm25).state <= 12) {
id(iaq_index) += 6;
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
id(iaq_index) += 5;
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
id(iaq_index) += 4;
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
id(iaq_index) += 3;
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
id(iaq_index) += 2;
}
else if (id(pm25).state >= 250.5) {
id(iaq_index) += 1;
}
if (id(eco2).state <= 600) {
id(iaq_index) += 5;
}
else if (id(eco2).state <= 800) {
id(iaq_index) += 4;
}
else if (id(eco2).state <= 1500) {
id(iaq_index) += 3;
}
else if (id(eco2).state <= 1800) {
id(iaq_index) += 2;
}
else if (id(eco2).state > 1800) {
id(iaq_index) += 1;
}
if (id(tvoc).state <= 65) {
id(iaq_index) += 5;
}
else if (id(tvoc).state <= 220) {
id(iaq_index) += 4;
}
else if (id(tvoc).state <= 660) {
id(iaq_index) += 3;
}
else if (id(tvoc).state <= 2200) {
id(iaq_index) += 2;
}
else if (id(tvoc).state > 2200) {
id(iaq_index) += 1;
}
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
return std::to_string(id(iaq_index));
update_interval: 60s
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: 0
hours: 6 # 6 AM
then:
- logger.log: "Automatic morning turn ON OLED"
- switch.turn_on: oled_power
- seconds: 0
minutes: 0
hours: 23 # 11 PM
then:
- logger.log: "Automatic evening turn OFF OLED"
- switch.turn_off: oled_power
+825
View File
@@ -0,0 +1,825 @@
substitutions: #substitute your own values in this section
internal_temp_sensor: sensor.meat_heater_current_temperature #entity from Home Assistant
outside_temp_sensor: sensor.home_realfeel_temperature #entity from Home Assistant
weather_entity: weather.home #entity from Home Assistant
todays_forecast_high_entity: sensor.home_realfeel_temperature_max_day_0 #entity from Home Assistant
todays_forecast_low_entity: sensor.home_realfeel_temperature_min_day_0 #entity from Home Assistant
device_name: airqualitysensor-mainfloor
friendly_name: "Air Sensor - Main Floor"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
on_boot:
priority: -100
then:
- light.turn_on:
id: led
brightness: 0.4
effect: "Rainbow Effect"
esp32:
board: esp32-s3-devkitc-1
cpu_frequency: 240MHz
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
# Enable logging
logger:
level: INFO
# Enable Home Assistant API
api:
encryption:
key: "JPYycQ6Uxuubxrfu66M/xAyUPGEl2DiJBCbpCCs9w/4="
ota:
- platform: esphome
password: "42087a27f07378b5499426cf544a6fa6"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Airqualitysensor-2"
password: "uhEm8rcU8sMF"
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: false
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if using static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: 0
hours: 6 # 6 AM
then:
- logger.log: "Automatic morning turn ON OLED"
- switch.turn_on: oled_power
- seconds: 0
minutes: 0
hours: 23 # 11 PM
then:
- logger.log: "Automatic evening turn OFF OLED"
- switch.turn_off: oled_power
script:
# SCRIPT 1: Runs when the mode is changed from the select
- id: update_led_state
then:
- lambda: |-
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
return; // Exit script, leave rainbow alone
}
std::string quality = id(iaq_reading).state;
float lux = id(ambient_light).state;
std::string mode = id(led_mode).state;
float brightness = 0.15;
float r = 0.0, g = 0.0, b = 0.0;
std::string selected_effect = "None";
// --- 1. Determine desired color ---
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
else if (quality == "Unhealthy") {
r = 1.0; g = 0.0; b = 1.0;
selected_effect = "Alert Flash"; // Alert overrides mode
}
// --- 2. Determine desired effect based on mode (if not alerting) ---
if (selected_effect != "Alert Flash") {
if (mode == "Breathing") {
selected_effect = "Breathing";
} else if (mode == "Scanner") {
selected_effect = "Scanner";
}
}
// --- 3. Apply changes ---
auto call = id(led).turn_on();
call.set_rgb(r, g, b); // Always set color
if (selected_effect == "None") {
// We want a solid light (Auto/Manual)
if (mode == "Auto") {
if (isnan(lux)) lux = 0;
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
if (brightness > 0.7) brightness = 0.7;
if (brightness < 0.15) brightness = 0.15;
} else { // Manual
brightness = id(iaq_led_brightness).state;
}
call.set_brightness(brightness);
}
// This script *always* sets the effect
call.set_effect(selected_effect);
call.perform();
# SCRIPT 2: Runs when IAQ *value* changes (every 10s)
- id: update_led_color
then:
- lambda: |-
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
return; // Exit script, leave rainbow alone
}
std::string quality = id(iaq_reading).state;
std::string mode = id(led_mode).state;
float lux = id(ambient_light).state;
float r = 0.0, g = 0.0, b = 0.0;
// --- 1. Determine desired color ---
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
else if (quality == "Unhealthy") {
r = 1.0; g = 0.0; b = 1.0;
// Force Alert Flash and EXIT
id(led).turn_on().set_rgb(r, g, b).set_effect("Alert Flash").perform();
return;
}
// --- 2. If we are here, AQ is OK. Reset Effect! ---
auto call = id(led).turn_on();
call.set_rgb(r, g, b);
// Determine which effect to restore based on the Select Mode
std::string restore_effect = "None";
if (mode == "Breathing") restore_effect = "Breathing";
else if (mode == "Scanner") restore_effect = "Scanner";
// Explicitly set the effect (This stops the Alert Flash)
call.set_effect(restore_effect);
// --- 3. Apply Brightness ---
if (mode == "Auto") {
float brightness = 0.15;
if (isnan(lux)) lux = 0;
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
if (brightness > 0.7) brightness = 0.7;
if (brightness < 0.15) brightness = 0.15;
call.set_brightness(brightness);
} else if (mode == "Manual") {
call.set_brightness(id(iaq_led_brightness).state);
}
call.perform();
globals:
- id: iaq_index
type: int
restore_value: no
initial_value: '0'
- id: computed_brightness
type: float
restore_value: no
initial_value: '0.15'
uart:
tx_pin: GPIO17
rx_pin: GPIO18
baud_rate: 9600
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
chipset: WS2812
pin: GPIO16
num_leds: 5
name: "LED"
id: led
icon: mdi:led-on
default_transition_length: 0s
disabled_by_default: False
effects:
- pulse:
name: "Breathing"
min_brightness: 0.15
max_brightness: 0.45
transition_length: 3000ms
update_interval: 3000ms
- addressable_scan:
name: "Scanner"
scan_width: 2
move_interval: 100ms
- flicker:
name: "Alert Flash"
alpha: 95%
intensity: 1.5%
- addressable_rainbow:
name: "Rainbow Effect"
speed: 10
width: 50
i2c:
sda: GPIO8
scl: GPIO9
scan: true
id: bus_a
frequency: 100kHz
switch:
- platform: template
name: "OLED Power"
id: oled_power
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
turn_on_action:
- logger.log: "OLED turned ON"
turn_off_action:
- logger.log: "OLED turned OFF"
select:
- platform: template
name: "LED Mode"
id: led_mode
options:
- "Auto"
- "Manual"
- "Breathing"
- "Scanner"
optimistic: true
restore_value: true
on_value:
then:
- script.execute: update_led_state
number:
- platform: template
name: "IAQ LED Brightness"
id: iaq_led_brightness
min_value: 0.15
max_value: 1.0
step: 0.01
optimistic: true
restore_value: true
sensor:
- platform: uptime
name: Uptime Sensor
- platform: homeassistant
id: inside_temperature
entity_id: $internal_temp_sensor
internal: true
- platform: homeassistant
id: outside_temperature
entity_id: $outside_temp_sensor
internal: true
- platform: homeassistant
id: todays_forecast_high
entity_id: $todays_forecast_high_entity
internal: true
- platform: homeassistant
id: todays_forecast_low
entity_id: $todays_forecast_low_entity
internal: true
- platform: pmsx003
type: PMSX003
update_interval: 60s
pm_1_0:
id: pm1
name: "PM <1.0µm Concentration"
pm_2_5:
id: pm25
name: "PM <2.5µm Concentration"
pm_10_0:
id: pm10
name: "PM <10.0µm Concentration"
- platform: bme680
temperature:
name: "BME680 Temperature"
id: bmetemp
oversampling: 16x
pressure:
name: "BME680 Pressure"
id: bmepressure
humidity:
name: "BME680 Humidity"
id: bmehum
gas_resistance:
name: "BME680 Gas Resistance"
id: bmegas
address: 0x77
update_interval: 60s
## CO²/VOC Sensor
- platform: ccs811
eco2:
name: "CCS811 CO²"
accuracy_decimals: 0
id: eco2
tvoc:
name: "CCS811 T-VOC"
accuracy_decimals: 0
id: tvoc
address: 0x5A
update_interval: 60s
temperature: bmetemp
humidity: bmehum
## After Calibration, Uncomment and change "baseline:"
baseline: 0x9CB1
- platform: template
name: "Humidity Sensor"
id: humi
unit_of_measurement: "%"
accuracy_decimals: 1
lambda: |-
return id(bmehum).state;
update_interval: 60s
- platform: wifi_signal
name: AQ WiFi Signal
update_interval: 60s
internal: true
- platform: veml7700
address: 0x10
update_interval: 60s
# short variant of sensor definition:
ambient_light:
name: "Ambient Light"
id: ambient_light
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
font:
- file: "fonts/Roboto-Regular.ttf"
id: robotto
size: 10
- file: "fonts/Roboto-Regular.ttf"
id: font2
size: 12
- file: "fonts/Poppins-Regular.ttf"
id: font1
size: 10
- file: "fonts/Poppins-Regular.ttf"
id: poppinslarger
size: 12
- file: "fonts/Poppins-SemiBold.ttf"
id: poppinsbold
size: 10
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font3
size: 18
glyphs:
- "\U000F13D5" #mdi:home-minus-outline
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font4
size: 40
glyphs:
- "\U000F0594" #"clear-night"
- "\U000F0590" #"cloudy"
- "\U000F0591" #"fog"
- "\U000F0592" #"hail"
- "\U000F0593" #"lightning"
- "\U000F067E" #"lightning-rainy"
- "\U000F0595" #"partlycloudy"
- "\U000F0596" #"pouring"
- "\U000F0597" #"rainy"
- "\U000F0598" #"snowy"
- "\U000F067F" #"snowy-rainy"
- "\U000F0599" #"sunny"
- "\U000F059D" #"windy"
- "\U000F059E" #"windy-variant"
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
id: oled_display
rotation: 0
i2c_id: bus_a
pages:
- id: page_air_quality
lambda: |-
if (id(oled_power).state) { // only draw if display is ON
{
// Combine the final string first
char full_text[64];
if (id(iaq_reading).has_state()) {
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
} else {
sprintf(full_text, "Air Quality: Booting...");
}
int x, y, w, h;
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
int center_x = (128 - w) / 2; // OLED is 128px wide
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
}
// --- Temp + Humidity on same line ---
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
// --- Other sensor lines ---
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(eco2).state);
it.printf(0, 36, id(font1), "TVOC: %.0f ppb", id(tvoc).state);
if (!isnan(id(pm25).state)) {
it.printf(0, 46, id(font1), "PM2.5: %.0f", id(pm25).state);
} else {
it.printf(0, 46, id(font1), "PM2.5: ---");
}
}
- id: page_environment
lambda: |-
if (id(oled_power).state) { // only draw if display is ON
{
// Combine the final string first
char full_text[64];
if (id(iaq_reading).has_state()) {
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
} else {
sprintf(full_text, "Air Quality: Booting...");
}
int x, y, w, h;
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
int center_x = (128 - w) / 2; // OLED is 128px wide
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
}
// --- Temp + Humidity on same line ---
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
// --- Other sensor lines ---
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(ambient_light).state);
}
- id: page_weather
lambda: |-
if (id(weather_state).has_state()) {
std::map<std::string, std::string> weather_icon_map
{
{"clear-night", "\U000F0594"},
{"cloudy", "\U000F0590"},
{"fog", "\U000F0591"},
{"hail", "\U000F0592"},
{"lightning", "\U000F0593"},
{"lightning-rainy", "\U000F067E"},
{"partlycloudy", "\U000F0595"},
{"pouring", "\U000F0596"},
{"rainy", "\U000F0597"},
{"snowy", "\U000F0598"},
{"snowy-rainy", "\U000F067F"},
{"sunny", "\U000F0599"},
{"windy", "\U000F059D"},
{"windy-variant", "\U000F059E"},
};
it.printf(0, it.get_height(), id(font4), TextAlign::BASELINE_LEFT, weather_icon_map[id(weather_state).state.c_str()].c_str());
}
// Print time in HH:MM format
it.strftime(0, 0, id(font1), TextAlign::TOP_LEFT, "%H:%M %a", id(homeassistant_time).now());
//Print day of week
//it.strftime(40, 0, id(font1), TextAlign::TOP_LEFT, "%a", id(homeassistant_time).now());
it.line(0, 20, it.get_width(), 20);
//Print home icon
//it.printf(70, 0, id(font3), "\U000F13D5");
// Print inside temperature (from homeassistant sensor)
if (id(inside_temperature).has_state()) {
it.printf(it.get_width(), 0, id(font1), TextAlign::TOP_RIGHT , "%7.1f°", id(inside_temperature).state);
}
// Print outside temperature (from homeassistant sensor)
if (id(outside_temperature).has_state()) {
it.printf(42, 32, id(font2), "%.1f°", id(outside_temperature).state);
}
// Print Forecast High
if (id(todays_forecast_high).has_state()) {
it.printf(it.get_width(), 32, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_high).state);
}
// Print Forecast Low
if (id(todays_forecast_low).has_state()) {
it.printf(it.get_width(), 48, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_low).state);
}
text_sensor:
- platform: homeassistant
id: weather_state
name: "Current Weather Icon"
entity_id: $weather_entity
internal: true
- platform: template
name: "PM 2.5 Air Quality"
icon: mdi:air-filter
id: aq_reading
lambda: |-
if (id(pm25).state <= 12) {
return {"Good"};
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
return {"Moderate"};
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
return {"Unhealthy(SG)"};
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
return {"Unhealthy"};
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
return {"Very Unhealthy"};
}
else if (id(pm25).state >= 250.5) {
return {"Hazardous"};
}
return {};
update_interval: 60s
- platform: template
name: "PM 10 Air Quality"
icon: mdi:air-filter
id: aq_10_reading
lambda: |-
if (id(pm10).state <= 54) {
return {"Good"};
}
else if ((id(pm10).state >= 55) && (id(pm10).state <= 154)) {
return {"Moderate"};
}
else if ((id(pm10).state >= 155) && (id(pm10).state <= 254)) {
return {"Unhealthy(SG)"};
}
else if ((id(pm10).state >= 255) && (id(pm10).state <= 354)) {
return {"Unhealthy"};
}
else if ((id(pm10).state >= 355) && (id(pm10).state <= 424)) {
return {"Very Unhealthy"};
}
else if (id(pm10).state >= 425) {
return {"Hazardous"};
}
return {};
update_interval: 60s
- platform: template
name: "Livingroom IAQ"
icon: "mdi:air-filter"
id: iaq_reading
lambda: |-
// 1. Safety Check: Ensure all sensors have valid numbers
if (isnan(id(humi).state) || isnan(id(pm25).state) || isnan(id(eco2).state) || isnan(id(tvoc).state)) {
return {"Booting..."};
}
id(iaq_index) = 0;
if (id(humi).state < 10 or id(humi).state > 90) {
id(iaq_index) += 1;
}
else if (id(humi).state < 20 or id(humi).state > 80) {
id(iaq_index) += 2;
}
else if (id(humi).state < 30 or id(humi).state > 70) {
id(iaq_index) += 3;
}
else if (id(humi).state < 40 or id(humi).state > 60) {
id(iaq_index) += 4;
}
else if (id(humi).state >= 40 and id(humi).state <= 60) {
id(iaq_index) += 5;
}
if (id(pm25).state <= 12) {
id(iaq_index) += 6;
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
id(iaq_index) += 5;
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
id(iaq_index) += 4;
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
id(iaq_index) += 3;
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
id(iaq_index) += 2;
}
else if (id(pm25).state >= 250.5) {
id(iaq_index) += 1;
}
if (id(eco2).state <= 600) {
id(iaq_index) += 5;
}
else if (id(eco2).state <= 800) {
id(iaq_index) += 4;
}
else if (id(eco2).state <= 1500) {
id(iaq_index) += 3;
}
else if (id(eco2).state <= 1800) {
id(iaq_index) += 2;
}
else if (id(eco2).state > 1800) {
id(iaq_index) += 1;
}
if (id(tvoc).state <= 65) {
id(iaq_index) += 5;
}
else if (id(tvoc).state <= 220) {
id(iaq_index) += 4;
}
else if (id(tvoc).state <= 660) {
id(iaq_index) += 3;
}
else if (id(tvoc).state <= 2200) {
id(iaq_index) += 2;
}
else if (id(tvoc).state > 2200) {
id(iaq_index) += 1;
}
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
if (id(iaq_index) <= 11) {
return {"Unhealthy"};
}
else if (id(iaq_index) <= 14) {
return {"Poor"};
}
else if (id(iaq_index) <= 17) {
return {"Moderate"};
}
else if (id(iaq_index) <= 19) {
return {"Good"};
}
else if (id(iaq_index) > 19) {
return {"Excellent"};
}
return {};
update_interval: 60s
on_value:
then:
- script.execute: update_led_color
- platform: template
name: "Livingroom IAQ Calculation"
icon: "mdi:air-filter"
id: iaq_reading_calculation
lambda: |-
id(iaq_index) = 0;
if (id(humi).state < 10 or id(humi).state > 90) {
id(iaq_index) += 1;
}
else if (id(humi).state < 20 or id(humi).state > 80) {
id(iaq_index) += 2;
}
else if (id(humi).state < 30 or id(humi).state > 70) {
id(iaq_index) += 3;
}
else if (id(humi).state < 40 or id(humi).state > 60) {
id(iaq_index) += 4;
}
else if (id(humi).state >= 40 and id(humi).state <= 60) {
id(iaq_index) += 5;
}
if (id(pm25).state <= 12) {
id(iaq_index) += 6;
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
id(iaq_index) += 5;
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
id(iaq_index) += 4;
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
id(iaq_index) += 3;
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
id(iaq_index) += 2;
}
else if (id(pm25).state >= 250.5) {
id(iaq_index) += 1;
}
if (id(eco2).state <= 600) {
id(iaq_index) += 5;
}
else if (id(eco2).state <= 800) {
id(iaq_index) += 4;
}
else if (id(eco2).state <= 1500) {
id(iaq_index) += 3;
}
else if (id(eco2).state <= 1800) {
id(iaq_index) += 2;
}
else if (id(eco2).state > 1800) {
id(iaq_index) += 1;
}
if (id(tvoc).state <= 65) {
id(iaq_index) += 5;
}
else if (id(tvoc).state <= 220) {
id(iaq_index) += 4;
}
else if (id(tvoc).state <= 660) {
id(iaq_index) += 3;
}
else if (id(tvoc).state <= 2200) {
id(iaq_index) += 2;
}
else if (id(tvoc).state > 2200) {
id(iaq_index) += 1;
}
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
return std::to_string(id(iaq_index));
update_interval: 60s
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min; auto-reboot temporarily disabled for debugging"
- delay: 10s
+825
View File
@@ -0,0 +1,825 @@
substitutions: #substitute your own values in this section
internal_temp_sensor: sensor.meat_heater_current_temperature #entity from Home Assistant
outside_temp_sensor: sensor.home_realfeel_temperature #entity from Home Assistant
weather_entity: weather.home #entity from Home Assistant
todays_forecast_high_entity: sensor.home_realfeel_temperature_max_day_0 #entity from Home Assistant
todays_forecast_low_entity: sensor.home_realfeel_temperature_min_day_0 #entity from Home Assistant
device_name: airqualitysensor-upstairs
friendly_name: "Air Sensor - Upstairs"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
on_boot:
priority: -100
then:
- light.turn_on:
id: led
brightness: 0.4
effect: "Rainbow Effect"
esp32:
board: esp32-s3-devkitc-1
cpu_frequency: 240MHz
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
# Enable logging
logger:
level: INFO
# Enable Home Assistant API
api:
encryption:
key: "p7SGIY1i+hwzZCQMliScoY2B98Qva1nPGUnzTr+8knE="
ota:
- platform: esphome
password: "81bd35b0916ed26675b611274e652689"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Airqualitysensor-3"
password: "a8CsGahAGljj"
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: false
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if using static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: 0
hours: 6 # 6 AM
then:
- logger.log: "Automatic morning turn ON OLED"
- switch.turn_on: oled_power
- seconds: 0
minutes: 0
hours: 19 # 7 PM
then:
- logger.log: "Automatic evening turn OFF OLED"
- switch.turn_off: oled_power
script:
# SCRIPT 1: Runs when the mode is changed from the select
- id: update_led_state
then:
- lambda: |-
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
return; // Exit script, leave rainbow alone
}
std::string quality = id(iaq_reading).state;
float lux = id(ambient_light).state;
std::string mode = id(led_mode).state;
float brightness = 0.15;
float r = 0.0, g = 0.0, b = 0.0;
std::string selected_effect = "None";
// --- 1. Determine desired color ---
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
else if (quality == "Unhealthy") {
r = 1.0; g = 0.0; b = 1.0;
selected_effect = "Alert Flash"; // Alert overrides mode
}
// --- 2. Determine desired effect based on mode (if not alerting) ---
if (selected_effect != "Alert Flash") {
if (mode == "Breathing") {
selected_effect = "Breathing";
} else if (mode == "Scanner") {
selected_effect = "Scanner";
}
}
// --- 3. Apply changes ---
auto call = id(led).turn_on();
call.set_rgb(r, g, b); // Always set color
if (selected_effect == "None") {
// We want a solid light (Auto/Manual)
if (mode == "Auto") {
if (isnan(lux)) lux = 0;
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
if (brightness > 0.7) brightness = 0.7;
if (brightness < 0.15) brightness = 0.15;
} else { // Manual
brightness = id(iaq_led_brightness).state;
}
call.set_brightness(brightness);
}
// This script *always* sets the effect
call.set_effect(selected_effect);
call.perform();
# SCRIPT 2: Runs when IAQ *value* changes (every 10s)
- id: update_led_color
then:
- lambda: |-
if (!id(iaq_reading).has_state() || id(iaq_reading).state == "Booting...") {
return; // Exit script, leave rainbow alone
}
std::string quality = id(iaq_reading).state;
std::string mode = id(led_mode).state;
float lux = id(ambient_light).state;
float r = 0.0, g = 0.0, b = 0.0;
// --- 1. Determine desired color ---
if (quality == "Excellent") { r = 0.0; g = 1.0; b = 0.0; }
else if (quality == "Good") { r = 0.3; g = 0.6; b = 0.3; }
else if (quality == "Moderate") { r = 1.0; g = 0.85; b = 0.38; }
else if (quality == "Poor") { r = 1.0; g = 0.0; b = 0.0; }
else if (quality == "Unhealthy") {
r = 1.0; g = 0.0; b = 1.0;
// Force Alert Flash and EXIT
id(led).turn_on().set_rgb(r, g, b).set_effect("Alert Flash").perform();
return;
}
// --- 2. If we are here, AQ is OK. Reset Effect! ---
auto call = id(led).turn_on();
call.set_rgb(r, g, b);
// Determine which effect to restore based on the Select Mode
std::string restore_effect = "None";
if (mode == "Breathing") restore_effect = "Breathing";
else if (mode == "Scanner") restore_effect = "Scanner";
// Explicitly set the effect (This stops the Alert Flash)
call.set_effect(restore_effect);
// --- 3. Apply Brightness ---
if (mode == "Auto") {
float brightness = 0.15;
if (isnan(lux)) lux = 0;
brightness = 0.15 + (lux / 200.0) * (0.7 - 0.15);
if (brightness > 0.7) brightness = 0.7;
if (brightness < 0.15) brightness = 0.15;
call.set_brightness(brightness);
} else if (mode == "Manual") {
call.set_brightness(id(iaq_led_brightness).state);
}
call.perform();
globals:
- id: iaq_index
type: int
restore_value: no
initial_value: '0'
- id: computed_brightness
type: float
restore_value: no
initial_value: '0.15'
uart:
tx_pin: GPIO17
rx_pin: GPIO18
baud_rate: 9600
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
chipset: WS2812
pin: GPIO16
num_leds: 5
name: "LED"
id: led
icon: mdi:led-on
default_transition_length: 0s
disabled_by_default: False
effects:
- pulse:
name: "Breathing"
min_brightness: 0.15
max_brightness: 0.45
transition_length: 3000ms
update_interval: 3000ms
- addressable_scan:
name: "Scanner"
scan_width: 2
move_interval: 100ms
- flicker:
name: "Alert Flash"
alpha: 95%
intensity: 1.5%
- addressable_rainbow:
name: "Rainbow Effect"
speed: 10
width: 50
i2c:
sda: GPIO8
scl: GPIO9
scan: true
id: bus_a
frequency: 100kHz
switch:
- platform: template
name: "OLED Power"
id: oled_power
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
turn_on_action:
- logger.log: "OLED turned ON"
turn_off_action:
- logger.log: "OLED turned OFF"
select:
- platform: template
name: "LED Mode"
id: led_mode
options:
- "Auto"
- "Manual"
- "Breathing"
- "Scanner"
optimistic: true
restore_value: true
on_value:
then:
- script.execute: update_led_state
number:
- platform: template
name: "IAQ LED Brightness"
id: iaq_led_brightness
min_value: 0.15
max_value: 1.0
step: 0.01
optimistic: true
restore_value: true
sensor:
- platform: uptime
name: Uptime Sensor
- platform: homeassistant
id: inside_temperature
entity_id: $internal_temp_sensor
internal: true
- platform: homeassistant
id: outside_temperature
entity_id: $outside_temp_sensor
internal: true
- platform: homeassistant
id: todays_forecast_high
entity_id: $todays_forecast_high_entity
internal: true
- platform: homeassistant
id: todays_forecast_low
entity_id: $todays_forecast_low_entity
internal: true
- platform: pmsx003
type: PMSX003
update_interval: 60s
pm_1_0:
id: pm1
name: "PM <1.0µm Concentration"
pm_2_5:
id: pm25
name: "PM <2.5µm Concentration"
pm_10_0:
id: pm10
name: "PM <10.0µm Concentration"
- platform: bme680
temperature:
name: "BME680 Temperature"
id: bmetemp
oversampling: 16x
pressure:
name: "BME680 Pressure"
id: bmepressure
humidity:
name: "BME680 Humidity"
id: bmehum
gas_resistance:
name: "BME680 Gas Resistance"
id: bmegas
address: 0x77
update_interval: 60s
## CO²/VOC Sensor
- platform: ccs811
eco2:
name: "CCS811 CO²"
accuracy_decimals: 0
id: eco2
tvoc:
name: "CCS811 T-VOC"
accuracy_decimals: 0
id: tvoc
address: 0x5A
update_interval: 60s
temperature: bmetemp
humidity: bmehum
## After Calibration, Uncomment and change "baseline:"
baseline: 0x9CB1
- platform: template
name: "Humidity Sensor"
id: humi
unit_of_measurement: "%"
accuracy_decimals: 1
lambda: |-
return id(bmehum).state;
update_interval: 60s
- platform: wifi_signal
name: AQ WiFi Signal
update_interval: 60s
internal: true
- platform: veml7700
address: 0x10
update_interval: 60s
# short variant of sensor definition:
ambient_light:
name: "Ambient Light"
id: ambient_light
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1
# Diagnostic sensors
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
font:
- file: "fonts/Roboto-Regular.ttf"
id: robotto
size: 10
- file: "fonts/Roboto-Regular.ttf"
id: font2
size: 12
- file: "fonts/Poppins-Regular.ttf"
id: font1
size: 10
- file: "fonts/Poppins-Regular.ttf"
id: poppinslarger
size: 12
- file: "fonts/Poppins-SemiBold.ttf"
id: poppinsbold
size: 10
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font3
size: 18
glyphs:
- "\U000F13D5" #mdi:home-minus-outline
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font4
size: 40
glyphs:
- "\U000F0594" #"clear-night"
- "\U000F0590" #"cloudy"
- "\U000F0591" #"fog"
- "\U000F0592" #"hail"
- "\U000F0593" #"lightning"
- "\U000F067E" #"lightning-rainy"
- "\U000F0595" #"partlycloudy"
- "\U000F0596" #"pouring"
- "\U000F0597" #"rainy"
- "\U000F0598" #"snowy"
- "\U000F067F" #"snowy-rainy"
- "\U000F0599" #"sunny"
- "\U000F059D" #"windy"
- "\U000F059E" #"windy-variant"
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
id: oled_display
rotation: 0
i2c_id: bus_a
pages:
- id: page_air_quality
lambda: |-
if (id(oled_power).state) { // only draw if display is ON
{
// Combine the final string first
char full_text[64];
if (id(iaq_reading).has_state()) {
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
} else {
sprintf(full_text, "Air Quality: Booting...");
}
int x, y, w, h;
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
int center_x = (128 - w) / 2; // OLED is 128px wide
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
}
// --- Temp + Humidity on same line ---
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
// --- Other sensor lines ---
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(eco2).state);
it.printf(0, 36, id(font1), "TVOC: %.0f ppb", id(tvoc).state);
if (!isnan(id(pm25).state)) {
it.printf(0, 46, id(font1), "PM2.5: %.0f", id(pm25).state);
} else {
it.printf(0, 46, id(font1), "PM2.5: ---");
}
}
- id: page_environment
lambda: |-
if (id(oled_power).state) { // only draw if display is ON
{
// Combine the final string first
char full_text[64];
if (id(iaq_reading).has_state()) {
sprintf(full_text, "Air Quality: %s", id(iaq_reading).state.c_str());
} else {
sprintf(full_text, "Air Quality: Booting...");
}
int x, y, w, h;
it.get_text_bounds(0, 0, full_text, id(poppinsbold), TextAlign::TOP_LEFT, &x, &y, &w, &h);
int center_x = (128 - w) / 2; // OLED is 128px wide
it.printf(center_x, 0, id(poppinsbold), "%s", full_text);
}
// --- Temp + Humidity on same line ---
it.printf(0, 16, id(font1), "TEMP: %.1f°C", id(bmetemp).state);
it.printf(70, 16, id(font1), "HUM: %.0f%%", id(bmehum).state);
// --- Other sensor lines ---
it.printf(0, 26, id(font1), "CO2: %.0f ppm", id(ambient_light).state);
}
- id: page_weather
lambda: |-
if (id(weather_state).has_state()) {
std::map<std::string, std::string> weather_icon_map
{
{"clear-night", "\U000F0594"},
{"cloudy", "\U000F0590"},
{"fog", "\U000F0591"},
{"hail", "\U000F0592"},
{"lightning", "\U000F0593"},
{"lightning-rainy", "\U000F067E"},
{"partlycloudy", "\U000F0595"},
{"pouring", "\U000F0596"},
{"rainy", "\U000F0597"},
{"snowy", "\U000F0598"},
{"snowy-rainy", "\U000F067F"},
{"sunny", "\U000F0599"},
{"windy", "\U000F059D"},
{"windy-variant", "\U000F059E"},
};
it.printf(0, it.get_height(), id(font4), TextAlign::BASELINE_LEFT, weather_icon_map[id(weather_state).state.c_str()].c_str());
}
// Print time in HH:MM format
it.strftime(0, 0, id(font1), TextAlign::TOP_LEFT, "%H:%M %a", id(homeassistant_time).now());
//Print day of week
//it.strftime(40, 0, id(font1), TextAlign::TOP_LEFT, "%a", id(homeassistant_time).now());
it.line(0, 20, it.get_width(), 20);
//Print home icon
//it.printf(70, 0, id(font3), "\U000F13D5");
// Print inside temperature (from homeassistant sensor)
if (id(inside_temperature).has_state()) {
it.printf(it.get_width(), 0, id(font1), TextAlign::TOP_RIGHT , "%7.1f°", id(inside_temperature).state);
}
// Print outside temperature (from homeassistant sensor)
if (id(outside_temperature).has_state()) {
it.printf(42, 32, id(font2), "%.1f°", id(outside_temperature).state);
}
// Print Forecast High
if (id(todays_forecast_high).has_state()) {
it.printf(it.get_width(), 32, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_high).state);
}
// Print Forecast Low
if (id(todays_forecast_low).has_state()) {
it.printf(it.get_width(), 48, id(font1), TextAlign::TOP_RIGHT, "%7.1f°", id(todays_forecast_low).state);
}
text_sensor:
- platform: homeassistant
id: weather_state
name: "Current Weather Icon"
entity_id: $weather_entity
internal: true
- platform: template
name: "PM 2.5 Air Quality"
icon: mdi:air-filter
id: aq_reading
lambda: |-
if (id(pm25).state <= 12) {
return {"Good"};
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
return {"Moderate"};
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
return {"Unhealthy(SG)"};
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
return {"Unhealthy"};
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
return {"Very Unhealthy"};
}
else if (id(pm25).state >= 250.5) {
return {"Hazardous"};
}
return {};
update_interval: 60s
- platform: template
name: "PM 10 Air Quality"
icon: mdi:air-filter
id: aq_10_reading
lambda: |-
if (id(pm10).state <= 54) {
return {"Good"};
}
else if ((id(pm10).state >= 55) && (id(pm10).state <= 154)) {
return {"Moderate"};
}
else if ((id(pm10).state >= 155) && (id(pm10).state <= 254)) {
return {"Unhealthy(SG)"};
}
else if ((id(pm10).state >= 255) && (id(pm10).state <= 354)) {
return {"Unhealthy"};
}
else if ((id(pm10).state >= 355) && (id(pm10).state <= 424)) {
return {"Very Unhealthy"};
}
else if (id(pm10).state >= 425) {
return {"Hazardous"};
}
return {};
update_interval: 60s
- platform: template
name: "Livingroom IAQ"
icon: "mdi:air-filter"
id: iaq_reading
lambda: |-
// 1. Safety Check: Ensure all sensors have valid numbers
if (isnan(id(humi).state) || isnan(id(pm25).state) || isnan(id(eco2).state) || isnan(id(tvoc).state)) {
return {"Booting..."};
}
id(iaq_index) = 0;
if (id(humi).state < 10 or id(humi).state > 90) {
id(iaq_index) += 1;
}
else if (id(humi).state < 20 or id(humi).state > 80) {
id(iaq_index) += 2;
}
else if (id(humi).state < 30 or id(humi).state > 70) {
id(iaq_index) += 3;
}
else if (id(humi).state < 40 or id(humi).state > 60) {
id(iaq_index) += 4;
}
else if (id(humi).state >= 40 and id(humi).state <= 60) {
id(iaq_index) += 5;
}
if (id(pm25).state <= 12) {
id(iaq_index) += 6;
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
id(iaq_index) += 5;
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
id(iaq_index) += 4;
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
id(iaq_index) += 3;
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
id(iaq_index) += 2;
}
else if (id(pm25).state >= 250.5) {
id(iaq_index) += 1;
}
if (id(eco2).state <= 600) {
id(iaq_index) += 5;
}
else if (id(eco2).state <= 800) {
id(iaq_index) += 4;
}
else if (id(eco2).state <= 1500) {
id(iaq_index) += 3;
}
else if (id(eco2).state <= 1800) {
id(iaq_index) += 2;
}
else if (id(eco2).state > 1800) {
id(iaq_index) += 1;
}
if (id(tvoc).state <= 65) {
id(iaq_index) += 5;
}
else if (id(tvoc).state <= 220) {
id(iaq_index) += 4;
}
else if (id(tvoc).state <= 660) {
id(iaq_index) += 3;
}
else if (id(tvoc).state <= 2200) {
id(iaq_index) += 2;
}
else if (id(tvoc).state > 2200) {
id(iaq_index) += 1;
}
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
if (id(iaq_index) <= 11) {
return {"Unhealthy"};
}
else if (id(iaq_index) <= 14) {
return {"Poor"};
}
else if (id(iaq_index) <= 17) {
return {"Moderate"};
}
else if (id(iaq_index) <= 19) {
return {"Good"};
}
else if (id(iaq_index) > 19) {
return {"Excellent"};
}
return {};
update_interval: 60s
on_value:
then:
- script.execute: update_led_color
- platform: template
name: "Livingroom IAQ Calculation"
icon: "mdi:air-filter"
id: iaq_reading_calculation
lambda: |-
id(iaq_index) = 0;
if (id(humi).state < 10 or id(humi).state > 90) {
id(iaq_index) += 1;
}
else if (id(humi).state < 20 or id(humi).state > 80) {
id(iaq_index) += 2;
}
else if (id(humi).state < 30 or id(humi).state > 70) {
id(iaq_index) += 3;
}
else if (id(humi).state < 40 or id(humi).state > 60) {
id(iaq_index) += 4;
}
else if (id(humi).state >= 40 and id(humi).state <= 60) {
id(iaq_index) += 5;
}
if (id(pm25).state <= 12) {
id(iaq_index) += 6;
}
else if ((id(pm25).state >= 12.1) && (id(pm25).state <= 35.4)) {
id(iaq_index) += 5;
}
else if ((id(pm25).state >= 35.5) && (id(pm25).state <= 55.4)) {
id(iaq_index) += 4;
}
else if ((id(pm25).state >= 55.5) && (id(pm25).state <= 150.4)) {
id(iaq_index) += 3;
}
else if ((id(pm25).state >= 150.5) && (id(pm25).state <= 250.4)) {
id(iaq_index) += 2;
}
else if (id(pm25).state >= 250.5) {
id(iaq_index) += 1;
}
if (id(eco2).state <= 600) {
id(iaq_index) += 5;
}
else if (id(eco2).state <= 800) {
id(iaq_index) += 4;
}
else if (id(eco2).state <= 1500) {
id(iaq_index) += 3;
}
else if (id(eco2).state <= 1800) {
id(iaq_index) += 2;
}
else if (id(eco2).state > 1800) {
id(iaq_index) += 1;
}
if (id(tvoc).state <= 65) {
id(iaq_index) += 5;
}
else if (id(tvoc).state <= 220) {
id(iaq_index) += 4;
}
else if (id(tvoc).state <= 660) {
id(iaq_index) += 3;
}
else if (id(tvoc).state <= 2200) {
id(iaq_index) += 2;
}
else if (id(tvoc).state > 2200) {
id(iaq_index) += 1;
}
ESP_LOGD("main", "Current IAQ index %d", id(iaq_index));
return std::to_string(id(iaq_index));
update_interval: 60s
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min; auto-reboot temporarily disabled for debugging"
- delay: 10s
+26
View File
@@ -0,0 +1,26 @@
- type: grid
column_span: 3
cards:
- type: heading
heading: Attic Climate
- type: markdown
content: >
🌡️ **Temperature**
{{ states('sensor.attic_climate_sensor_attic_climate_sensor_temperature') | float(0) | round(1) }} °F
💧 **Humidity**
{{ states('sensor.attic_climate_sensor_attic_climate_sensor_humidity') | float(0) | round(1) }} %
🌀 **Pressure**
{{ states('sensor.attic_climate_sensor_attic_climate_sensor_barometric_pressure') | float(0) | round(2) }} inHg
🔋 **Battery**
{{ states('sensor.attic_climate_sensor_attic_climate_sensor_battery_percentage') | int(0) }}%
🕐 **Last updated**
{{ states.sensor.attic_climate_sensor_attic_climate_sensor_temperature.last_changed | as_timestamp | timestamp_custom('%-I:%M %p, %b %-d') }}
+246
View File
@@ -0,0 +1,246 @@
# ============================================================
# Attic Climate Sensor — ESP32-C3 SuperMini (Battery-Powered)
# Board: ESP32-C3 SuperMini (RISC-V, single-core, 160MHz)
# Sensor: AHT20 + BMP280 combo (I2C)
# Battery: 18650 Li-Ion
# Schedule: Wake every 6 hours, report to HA, deep sleep
# OTA: Hold GPIO3 LOW (jumper to GND) to prevent deep sleep
# ============================================================
#
# WIRING:
# AHT20/BMP280 Combo Board:
# SDA → GPIO4
# SCL → GPIO5
# VCC → 3.3V
# GND → GND
#
# Battery Voltage Monitor (voltage divider):
# BAT+ → 100kΩ → GPIO0 (ADC) → 100kΩ → GND
#
# OTA Stay-Awake Jumper:
# GPIO3 → 2-pin header → GND (bridge to stay awake)
#
# NOTES:
# - GPIO8 = onboard blue LED (active LOW / inverted)
# - GPIO9 = BOOT button (avoid for general use)
# - GPIO2, GPIO10 = strapping pins (avoid)
# - All GPIO are 3.3V logic only
# ============================================================
substitutions:
device_name: "attic-climate-sensor"
friendly_name: "Attic Climate Sensor"
# Deep sleep duration: 6 hours = 21600 seconds
sleep_duration: "21600s"
# Max awake time before forced sleep (safety net)
run_duration: "60s"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
on_boot:
priority: -10
then:
- wait_until:
condition:
api.connected:
timeout: 45s
# Give sensors time to stabilize and publish
- delay: 5s
- if:
condition:
binary_sensor.is_on: ota_stay_awake
then:
- logger.log: "OTA jumper detected — staying awake for maintenance"
- deep_sleep.prevent: deep_sleep_control
- light.turn_on: onboard_led
else:
- logger.log: "Publishing complete — entering deep sleep"
- deep_sleep.enter: deep_sleep_control
esp32:
board: esp32-c3-devkitm-1
variant: esp32c3
framework:
type: arduino
# ── Logging ────────────────────────────────────────────────
logger:
level: DEBUG
# The C3 SuperMini uses native USB — hardware UART is on GPIO20/21
# For production, reduce to save power:
# level: WARN
# ── Network ────────────────────────────────────────────────
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Static IP dramatically reduces WiFi connect time (~3-5s saved per wake)
manual_ip:
static_ip: !secret attic_sensor_ip
gateway: !secret gateway
subnet: !secret subnet
dns1: !secret dns1
fast_connect: true
power_save_mode: none
ap:
ssid: "${device_name}-fallback"
password: !secret wifi_password
captive_portal:
# ── API & OTA ──────────────────────────────────────────────
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
# ── I2C Bus ────────────────────────────────────────────────
# AHT20: 0x38 | BMP280: 0x77 (or 0x76 depending on board)
# Using GPIO4 (SDA) and GPIO5 (SCL) — safe general-purpose pins
i2c:
sda: GPIO4
scl: GPIO5
scan: true
# ── Deep Sleep ─────────────────────────────────────────────
deep_sleep:
id: deep_sleep_control
run_duration: ${run_duration}
sleep_duration: ${sleep_duration}
# ── Status LED ─────────────────────────────────────────────
# Onboard blue LED on GPIO8 (active LOW / inverted)
output:
- platform: gpio
pin:
number: GPIO8
inverted: true
id: blue_led_output
light:
- platform: binary
name: "${friendly_name} Status LED"
output: blue_led_output
id: onboard_led
entity_category: diagnostic
# ── Binary Sensors ─────────────────────────────────────────
binary_sensor:
# OTA Stay-Awake Jumper
# Wire a 2-pin header from GPIO3 → GND
# Bridge jumper = stay awake for OTA | Remove = normal deep sleep
- platform: gpio
pin:
number: GPIO3
mode:
input: true
pullup: true
inverted: true # LOW (jumper bridged) = ON = stay awake
name: "${friendly_name} OTA Stay Awake"
id: ota_stay_awake
entity_category: diagnostic
on_press:
- logger.log: "OTA jumper enabled — preventing deep sleep"
- deep_sleep.prevent: deep_sleep_control
- light.turn_on: onboard_led
on_release:
- logger.log: "OTA jumper removed — allowing deep sleep"
- deep_sleep.allow: deep_sleep_control
- light.turn_off: onboard_led
# ── Sensors ────────────────────────────────────────────────
sensor:
# ── AHT20: Temperature & Humidity ──
- platform: aht10
variant: AHT20
temperature:
name: "${friendly_name} Temperature"
unit_of_measurement: "°F"
accuracy_decimals: 1
filters:
- lambda: return x * 9.0f / 5.0f + 32.0f;
- sliding_window_moving_average:
window_size: 3
send_every: 1
humidity:
name: "${friendly_name} Humidity"
accuracy_decimals: 1
filters:
- sliding_window_moving_average:
window_size: 3
send_every: 1
update_interval: 5s
# ── BMP280: Barometric Pressure (+ backup temp) ──
- platform: bmp280_i2c
address: 0x77 # Change to 0x76 if your board uses that
temperature:
name: "${friendly_name} BMP280 Temperature"
unit_of_measurement: "°F"
accuracy_decimals: 1
filters:
- lambda: return x * 9.0f / 5.0f + 32.0f;
disabled_by_default: true
pressure:
name: "${friendly_name} Barometric Pressure"
accuracy_decimals: 1
unit_of_measurement: "inHg"
filters:
- multiply: 0.0295299831
update_interval: 5s
# ── Battery Voltage via ADC ──
# Voltage divider: 100kΩ + 100kΩ from BAT+ to GND, midpoint → GPIO0
# GPIO0 is ADC1_CH0 on the C3 — safe for ADC use
# 2:1 divider ratio — multiply by 2 to get actual battery voltage
- platform: adc
pin: GPIO0
name: "${friendly_name} Battery Voltage"
id: battery_voltage
accuracy_decimals: 2
update_interval: 5s
attenuation: 11db
filters:
- multiply: 2.0
# ── Battery Percentage (estimated) ──
# 18650 range: ~3.0V (empty) to ~4.2V (full)
- platform: template
name: "${friendly_name} Battery Percentage"
unit_of_measurement: "%"
device_class: battery
accuracy_decimals: 0
update_interval: 5s
lambda: |-
float voltage = id(battery_voltage).state;
if (voltage >= 4.2) return 100.0;
if (voltage <= 3.0) return 0.0;
return (voltage - 3.0) / (4.2 - 3.0) * 100.0;
# ── WiFi Signal ──
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 10s
entity_category: diagnostic
# ── Text Sensors ───────────────────────────────────────────
text_sensor:
- platform: wifi_info
ip_address:
name: "${friendly_name} IP Address"
entity_category: diagnostic
# ── Switches ───────────────────────────────────────────────
switch:
# Manual deep sleep trigger from HA (for testing)
- platform: template
name: "${friendly_name} Force Sleep"
icon: "mdi:sleep"
entity_category: config
turn_on_action:
- deep_sleep.enter: deep_sleep_control
+347
View File
@@ -0,0 +1,347 @@
substitutions:
device_name: cat-medication-tracker
friendly_name: "Cat Medication Tracker"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
includes:
- spi_helper.h
on_boot:
- priority: 100
then:
- light.turn_on: backlight
- priority: -100
then:
- lambda: |-
// MADCTL 0x48: MY=0, MX=1, BGR=1 — correct portrait, no mirror for ESP32-2432S035R
// ESP-IDF SPI master API used to bypass ESPHome's buffered display layer
spi_device_handle_t disp_fix;
spi_device_interface_config_t cfg = {};
cfg.clock_speed_hz = 1000000;
cfg.mode = 0;
cfg.spics_io_num = -1; // manual CS
cfg.queue_size = 1;
if (spi_bus_add_device(SPI2_HOST, &cfg, &disp_fix) == ESP_OK) {
gpio_set_level((gpio_num_t)15, 0); // CS low
gpio_set_level((gpio_num_t)2, 0); // DC = command
spi_transaction_t t = {};
t.length = 8;
t.flags = SPI_TRANS_USE_TXDATA;
t.tx_data[0] = 0x36; // MADCTL register
spi_device_polling_transmit(disp_fix, &t);
gpio_set_level((gpio_num_t)2, 1); // DC = data
t.tx_data[0] = 0x48; // MY=0, MX=1, BGR=1
spi_device_polling_transmit(disp_fix, &t);
gpio_set_level((gpio_num_t)15, 1); // CS high
spi_bus_remove_device(disp_fix);
}
- component.update: my_display
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: INFO
api:
encryption:
key: !secret api_encryption_key
ota:
platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.230
gateway: 192.168.69.1
subnet: 255.255.255.0
ap:
ssid: "${device_name}-fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if you use static IPs and don't need discovery
captive_portal:
web_server:
port: 80
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: 0
hours: 0
then:
- switch.turn_off: penelope_medicated
- switch.turn_off: tess_medicated
- script.execute: update_display
spi:
- id: tft_spi
clk_pin: GPIO14
mosi_pin: GPIO13
miso_pin: GPIO12
display:
- platform: mipi_spi
model: ILI9488
spi_id: tft_spi
cs_pin: GPIO15
dc_pin: GPIO2
reset_pin: GPIO4
rotation: 0
invert_colors: false
color_order: bgr
data_rate: 10MHz
dimensions:
width: 320
height: 480
id: my_display
auto_clear_enabled: false
update_interval: 2s
color_depth: 16
buffer_size: 25%
lambda: |-
// Colors
auto red = Color(255, 0, 0);
auto green = Color(0, 200, 0);
auto light_grey = Color(200, 200, 200);
auto white = Color(255, 255, 255);
auto black = Color(0, 0, 0);
auto dark_grey = Color(80, 80, 80);
// Fill background
it.fill(light_grey);
// Border: green if all done, red otherwise
bool all_done = id(penelope_medicated).state && id(tess_medicated).state;
auto border_color = all_done ? green : red;
int border = 10;
it.filled_rectangle(0, 0, 320, border, border_color);
it.filled_rectangle(0, 480 - border, 320, border, border_color);
it.filled_rectangle(0, 0, border, 480, border_color);
it.filled_rectangle(320 - border, 0, border, 480, border_color);
// Title
it.printf(160, 30, id(title_font), black, TextAlign::TOP_CENTER, "Cat Meds");
// Penelope button
int btn_x = 40;
int btn_y = 90;
int btn_w = 240;
int btn_h = 120;
auto penelope_color = id(penelope_medicated).state ? green : red;
it.filled_rectangle(btn_x, btn_y, btn_w, btn_h, penelope_color);
it.rectangle(btn_x, btn_y, btn_w, btn_h, dark_grey);
it.printf(btn_x + btn_w/2, btn_y + btn_h/2, id(button_font), white, TextAlign::CENTER, "Penelope");
if (id(penelope_medicated).state) {
it.printf(btn_x + btn_w/2, btn_y + btn_h - 20, id(status_font), white, TextAlign::CENTER, "DONE");
}
// Tess button
btn_y = 230;
auto tess_color = id(tess_medicated).state ? green : red;
it.filled_rectangle(btn_x, btn_y, btn_w, btn_h, tess_color);
it.rectangle(btn_x, btn_y, btn_w, btn_h, dark_grey);
it.printf(btn_x + btn_w/2, btn_y + btn_h/2, id(button_font), white, TextAlign::CENTER, "Tess");
if (id(tess_medicated).state) {
it.printf(btn_x + btn_w/2, btn_y + btn_h - 20, id(status_font), white, TextAlign::CENTER, "DONE");
}
// Reset button
int reset_x = 110;
int reset_y = 395;
int reset_w = 100;
int reset_h = 55;
it.filled_rectangle(reset_x, reset_y, reset_w, reset_h, dark_grey);
it.rectangle(reset_x, reset_y, reset_w, reset_h, black);
it.printf(reset_x + reset_w/2, reset_y + reset_h/2, id(status_font), white, TextAlign::CENTER, "RESET");
# XPT2046 Touchscreen
touchscreen:
- platform: xpt2046
id: my_touchscreen
spi_id: tft_spi
cs_pin: GPIO33
update_interval: 250ms
threshold: 1200
calibration:
x_min: 280
x_max: 3850
y_min: 340
y_max: 3860
transform:
mirror_y: false
# Touch buttons as binary sensors
binary_sensor:
- platform: touchscreen
touchscreen_id: my_touchscreen
name: "Penelope Button"
id: penelope_button
x_min: 40
x_max: 280
y_min: 230
y_max: 350
on_press:
then:
- switch.toggle: penelope_medicated
- platform: touchscreen
touchscreen_id: my_touchscreen
name: "Tess Button"
id: tess_button
x_min: 40
x_max: 280
y_min: 90
y_max: 210
on_press:
then:
- switch.toggle: tess_medicated
- platform: touchscreen
touchscreen_id: my_touchscreen
name: "Reset Button"
id: reset_button
x_min: 110
x_max: 210
y_min: 10 #395
y_max: 50 #450
on_press:
then:
- switch.turn_off: penelope_medicated
- switch.turn_off: tess_medicated
- platform: template
name: "Penelope Medication Status"
lambda: 'return id(penelope_medicated).state;'
device_class: running
- platform: template
name: "Tess Medication Status"
lambda: 'return id(tess_medicated).state;'
device_class: running
- platform: template
name: "All Cats Medicated"
lambda: 'return id(penelope_medicated).state && id(tess_medicated).state;'
device_class: running
# Backlight control
output:
- platform: gpio
pin: GPIO27
id: backlight_pwm
inverted: false
light:
- platform: binary
output: backlight_pwm
name: "${friendly_name} Backlight"
id: backlight
restore_mode: ALWAYS_ON
# Medication state switches (exposed to Home Assistant)
switch:
- platform: template
name: "Penelope Medicated"
id: penelope_medicated
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
on_turn_on:
- script.execute: update_display
on_turn_off:
- script.execute: update_display
- platform: template
name: "Tess Medicated"
id: tess_medicated
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
on_turn_on:
- script.execute: update_display
on_turn_off:
- script.execute: update_display
- platform: template
name: "Reset All Medications"
id: reset_all
optimistic: false
turn_on_action:
- switch.turn_off: penelope_medicated
- switch.turn_off: tess_medicated
# Script to update display
script:
- id: update_display
then:
- component.update: my_display
# Fonts
font:
- file: "gfonts://Roboto"
id: title_font
size: 28
- file: "gfonts://Roboto"
id: button_font
size: 24
- file: "gfonts://Roboto"
id: status_font
size: 14
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
################################################################################
# chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml)
# Kids: Jordyn, Declan, Chloe
################################################################################
title: "Chore Tracker"
views:
- title: Today
icon: mdi:checkbox-marked-circle
path: chores
cards:
- type: horizontal-stack
cards:
- type: markdown
content: >
### 󰄛 Jordyn
{{
'All done!' if states('sensor.jordyn_all_chores_done') == 'True'
else states('sensor.jordyn_chores_done_today') ~ '/4 chores done'
}}
- type: markdown
content: >
### 󱑷 Declan
{{
'All done!' if states('sensor.declan_all_chores_done') == 'True'
else states('sensor.declan_chores_done_today') ~ '/2 chores done'
}}
- type: markdown
content: >
### 󱚣 Chloe
{{
'All done!' if states('sensor.chloe_all_chores_done') == 'True'
else states('sensor.chloe_chores_done_today') ~ '/3 chores done'
}}
- type: button
name: "Reset ALL Chores"
icon: mdi:restart-alert
tap_action:
action: call-service
service: input_button.press
target:
entity_id: input_button.reset_all_chores
- type: entities
title: "󰄛 Jordyn's Chores"
entities:
- entity: switch.chore_tracker_jordyn_put_on_underwear
name: "󰋣 Put on underwear"
- entity: switch.chore_tracker_jordyn_brush_teeth
name: "󰦩 Brush Teeth"
- entity: switch.chore_tracker_jordyn_fill_water_bowls
name: "󰆫 Fill water bowls"
- entity: switch.chore_tracker_jordyn_restock_cat_food_cans
name: "󱜜 Restock Cat Food Cans"
- type: divider
- entity: sensor.jordyn_chores_done_today
name: "Chores done today"
- entity: sensor.jordyn_all_chores_done
name: "All Done?"
- type: button
name: "Reset Jordyn's Chores"
tap_action:
action: call-service
service: input_button.press
target:
entity_id: input_button.jordyn_reset_chores
- type: entities
title: "󱑷 Declan's Chores"
entities:
- entity: switch.chore_tracker_declan_take_morning_pill
name: "󰐂 Take morning pill"
- entity: switch.chore_tracker_declan_scoop_dog_poop
name: "󰇷 Scoop Dog Poop"
- type: divider
- entity: sensor.declan_chores_done_today
name: "Chores done today"
- entity: sensor.declan_all_chores_done
name: "All Done?"
- type: button
name: "Reset Declan's Chores"
tap_action:
action: call-service
service: input_button.press
target:
entity_id: input_button.declan_reset_chores
- type: entities
title: "󱚣 Chloe's Chores"
entities:
- entity: switch.chore_tracker_chloe_fill_kitty_feeders
name: "󰊩 Fill kitty feeders"
- entity: switch.chore_tracker_chloe_scoop_kitty_litter
name: "󰇷 Scoop Kitty Litter"
- entity: switch.chore_tracker_chloe_replace_kitty_litter_bags
name: "󰇷 Replace Kitty Litter Bags"
- type: divider
- entity: sensor.chloe_chores_done_today
name: "Chores done today"
- entity: sensor.chloe_all_chores_done
name: "All Done?"
- type: button
name: "Reset Chloe's Chores"
tap_action:
action: call-service
service: input_button.press
target:
entity_id: input_button.chloe_reset_chores
+81
View File
@@ -0,0 +1,81 @@
################################################################################
# chores_config.yaml — YOUR SINGLE SOURCE OF TRUTH
#
# Edit this file to change kids, chores, and settings.
# Then run: python3 generate.py
#
# ICONS: Use MDI codepoints from https://pictogrammers.com/library/mdi/
# Format: "\U000FXXXX" e.g. mdi:bed = "\U000F02E3"
# The kid avatar field also uses MDI codepoints.
#
# Common chore icons:
# Bed: "\U000F02E3" mdi:bed
# Brush teeth: "\U000F09A9" mdi:toothbrush
# Broom: "\U000F00A8" mdi:broom
# Book: "\U000F0219" mdi:book-open-variant
# Dog: "\U000F01F9" mdi:dog
# Paw: "\U000F0265" mdi:paw
# Silverware: "\U000F03E7" mdi:silverware-fork-knife
# Trash: "\U000F05B8" mdi:trash-can
# Watering can: "\U000F1B25" mdi:watering-can
# Piano: "\U000F0F9E" mdi:piano
# Dumbbell: "\U000F0F1E" mdi:dumbbell
# Shower: "\U000F0467" mdi:shower
################################################################################
settings:
device_name: chore-tracker
friendly_name: "Chore Tracker"
wifi_ssid: !secret wifi_iot_ssid
wifi_password: !secret wifi_password
api_key: !secret api_encryption_key
ota_password: !secret ota_password
reset_time: "00:00:00"
reminder_time: "18:00"
notify_service: notify.notify
static_ip: 192.168.69.115
gateway: 192.168.69.1
subnet: 255.255.255.0
# ── KIDS ──────────────────────────────────────────────────────────────────────
# avatar: MDI codepoint shown in the sidebar on the kid's chore page
# color / color_dark: hex colours for that kid's sidebar
#
kids:
- name: Jordyn
avatar: "\U000F011B" # mdi:cat
color: "5F5980" # #5F5980 is a nice light blue, but it's a bit hard to read when used as a background colour for the sidebar. So I use a darker version of the same colour for the sidebar background.
color_dark: "2F3061" # #2F3061 is a darker version of #5F5980, and is easier to read when used as a background colour for the sidebar.
chores:
- name: Put on underwear
icon: "\U000F02E3" # mdi:bed
- name: Brush Teeth
icon: "\U000F09A9" # mdi:toothbrush
- name: Fill water bowls
icon: "\U000F01AB" # mdi:cup-water
- name: Restock Cat Food Cans
icon: "\U000F171C" # mdi:food-turkey
- name: Declan
avatar: "\U000F1477" # mdi:wizard-hat
color: "720026" # #720026 is a deep red, used for the kid's sidebar background.
color_dark: "4F000B" # #4F000B is a darker version of #720026, easier to read when used as a background colour for the sidebar.
chores:
- name: Take morning pill
icon: "\U000F0402" # mdi:pill
- name: Scoop Dog Poop
icon: "\U000F01F7" # mdi:emoticon-poop
- name: Chloe
avatar: "\U000F16A3" # mdi:robot-excited
color: "73683B" # #73683B is a light brown, used for the kid's sidebar background.
color_dark: "583E23" # #583E23 is a darker version of #73683B, easier to read when used as a background colour for the sidebar.
chores:
- name: Fill kitty feeders
icon: "\U000F02A9" # mdi:bowl-outline
- name: Scoop Kitty Litter
icon: "\U000F01F7" # mdi:emoticon-poop
- name: Replace Kitty Litter Bags
icon: "\U000F01F7" # mdi:emoticon-poop
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@blacknell"]
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "esphome/core/automation.h"
#include "max17043.h"
namespace esphome {
namespace max17043 {
template<typename... Ts> class SleepAction : public Action<Ts...> {
public:
explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {}
void play(Ts... x) override { this->max17043_->sleep_mode(); }
protected:
MAX17043Component *max17043_;
};
} // namespace max17043
} // namespace esphome
+96
View File
@@ -0,0 +1,96 @@
#include "max17043.h"
#include "esphome/core/log.h"
namespace esphome {
namespace max17043 {
// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
// Consult the datasheet at https://www.analog.com/en/products/max17043.html
static const char *const TAG = "max17043";
static const uint8_t MAX17043_VCELL = 0x02;
static const uint8_t MAX17043_SOC = 0x04;
static const uint8_t MAX17043_CONFIG = 0x0c;
static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C;
static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4)
static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080;
void MAX17043Component::update() {
uint16_t raw_voltage, raw_percent;
if (this->voltage_sensor_ != nullptr) {
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
this->status_set_warning("Unable to read MAX17043_VCELL");
} else {
float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0;
this->voltage_sensor_->publish_state(voltage);
this->status_clear_warning();
}
}
if (this->battery_remaining_sensor_ != nullptr) {
if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) {
this->status_set_warning("Unable to read MAX17043_SOC");
} else {
float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff));
this->battery_remaining_sensor_->publish_state(percent);
this->status_clear_warning();
}
}
}
void MAX17043Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up MAX17043...");
uint16_t config_reg;
if (this->write(&MAX17043_CONFIG, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
if (this->read(reinterpret_cast<uint8_t *>(&config_reg), 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
config_reg = i2c::i2ctohs(config_reg) & MAX17043_CONFIG_SAFE_MASK;
ESP_LOGV(TAG, "MAX17043 CONFIG register reads 0x%X", config_reg);
if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) {
ESP_LOGW(TAG, "Non-standard CONFIG register 0x%X at 0x36; continuing in compatible mode", config_reg);
return;
}
// need to write back to config register to reset the sleep bit
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) {
this->status_set_error("sleep reset failed");
this->mark_failed();
return;
}
}
void MAX17043Component::dump_config() {
ESP_LOGCONFIG(TAG, "MAX17043:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with MAX17043 failed");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Battery Voltage", this->voltage_sensor_);
LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_);
}
float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; }
void MAX17043Component::sleep_mode() {
if (!this->is_failed()) {
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) {
ESP_LOGW(TAG, "Unable to write the sleep bit to config register");
this->status_set_warning();
}
}
}
} // namespace max17043
} // namespace esphome
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace max17043 {
class MAX17043Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void sleep_mode();
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) {
battery_remaining_sensor_ = battery_remaining_sensor;
}
protected:
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *battery_remaining_sensor_{nullptr};
};
} // namespace max17043
} // namespace esphome
+77
View File
@@ -0,0 +1,77 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_LEVEL,
CONF_BATTERY_VOLTAGE,
CONF_ID,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_VOLTAGE,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_VOLT,
)
DEPENDENCIES = ["i2c"]
max17043_ns = cg.esphome_ns.namespace("max17043")
MAX17043Component = max17043_ns.class_(
"MAX17043Component", cg.PollingComponent, i2c.I2CDevice
)
# Actions
SleepAction = max17043_ns.class_("SleepAction", automation.Action)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MAX17043Component),
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_BATTERY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x36))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
if CONF_BATTERY_LEVEL in config:
sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
cg.add(var.set_battery_remaining_sensor(sens))
MAX17043_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(MAX17043Component),
}
)
@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA)
async def max17043_sleep_mode_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
+122
View File
@@ -0,0 +1,122 @@
substitutions:
device_name: declan-a1-camera
friendly_name: "Declan's A1 Camera"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.220
gateway: 192.168.69.1
subnet: 255.255.255.0
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if you use static IPs and don't need discovery
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
captive_portal:
i2c:
- id: camera_i2c
sda: GPIO26
scl: GPIO27
psram:
mode: quad
speed: 80MHz
esp32_camera:
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_id: camera_i2c
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: 1024x768
jpeg_quality: 15
vertical_flip: true
horizontal_mirror: false
max_framerate: 20 fps
idle_framerate: 0.05 fps
name: ${device_name}
esp32_camera_web_server:
- port: 8080
mode: STREAM
- port: 8081
mode: SNAPSHOT
switch:
- platform: gpio
name: "${device_name}-flash"
pin: 4
+146
View File
@@ -0,0 +1,146 @@
substitutions:
device_name: esp32-h2c-camera
friendly_name: "H2C Camera"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
# Required for ESP32 camera buffering.
psram:
mode: octal
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.90
gateway: 192.168.69.1
subnet: 255.255.255.0
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if you use static IPs and don't need discovery
# captive_portal:
web_server:
port: 80
i2c:
- id: camera_i2c
sda: GPIO8
scl: GPIO9
scan: false
esp32_camera:
name: "${friendly_name}"
external_clock:
pin: GPIO5
frequency: 20MHz
i2c_id: camera_i2c
data_pins: [GPIO16, GPIO18, GPIO21, GPIO17, GPIO14, GPIO7, GPIO6, GPIO4]
vsync_pin: GPIO1
href_pin: GPIO2
pixel_clock_pin: GPIO15
# Start conservative for reliability; increase later if stable.
resolution: 800X600
jpeg_quality: 15
max_framerate: 20 fps
idle_framerate: 0.2 fps
frame_buffer_count: 1
# Common orientation defaults for this module.
vertical_flip: true
horizontal_mirror: false
brightness: 1
# Camera endpoints:
# Stream: http://<ip>:8080
# Snapshot: http://<ip>:8081
esp32_camera_web_server:
- port: 8080
mode: stream
- port: 8081
mode: snapshot
switch:
# DFRobot board IR illumination control pin.
- platform: gpio
name: "${friendly_name} IR LED"
pin: GPIO47
restore_mode: ALWAYS_OFF
sensor:
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${friendly_name} Uptime"
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
button:
- platform: restart
name: "${friendly_name} Restart"
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
+140
View File
@@ -0,0 +1,140 @@
substitutions:
device_name: saturn4-printer-cam
hostname: "Saturn 4 Printer Cam"
friendly_name: "Saturn 4 Printer Cam"
device_description: ESP32-CAM module in Saturn 4 Ultra Resin printer.
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "3Zxo91fAfA7wpZ4DAYFxGarCRIGQr+0rUJH2taJo7ds="
ota:
- platform: esphome
password: "d2e50b2ad36ed7de4ecbfd90725765e3"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Esp32-Saturn4-Cam"
password: "klRzTYuqPJXm"
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
captive_portal:
# Example configuration entry
i2c:
- id: camera_i2c
sda: GPIO26
scl: GPIO27
psram:
mode: quad
speed: 80MHz
# Flashlight # you can control this flashlight within Homeassistant or mqtt to shine on the birds. use it with an timer!
output:
- platform: ledc
pin: GPIO4
id: gpio_4
channel: 2
## GPIO_4 is the flash light pin
light:
- platform: monochromatic
output: gpio_4
name: $hostname light
restore_mode: RESTORE_AND_OFF
icon: mdi:flash
esp32_camera:
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_id: camera_i2c
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: 640x480 # you can use: [1600x1200 , 1280x1024 , 1024x768 , 800x600, 640x480 ] for best performance (FPS) use 1024 of lower resolution.
jpeg_quality: 12 #The JPEG quality that the camera should encode images with. From 10 (best) to 63 (worst)
aec2: true
ae_level: 2
brightness: 2 #The brightness to apply to the picture, default 0
contrast: 2
saturation: -2
vertical_flip: False
# Image settings
name: ${device_name}
# ...
esp32_camera_web_server:
- port: 80
mode: STREAM
- port: 8080
mode: SNAPSHOT
+152
View File
@@ -0,0 +1,152 @@
substitutions:
device_name: salt-sensor
friendly_name: "Salt Level Sensor"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "EaifDkFuB9N+qDKHW6B4k/n495FWLVmcp6DBODKzK10="
ota:
- platform: esphome
password: "7ce613915c51252c2810c07c889b4a5e"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Salt-Sensor Fallback Hotspot"
password: "ZoBRDcj54rwW"
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
captive_portal:
# Enable Web server.
web_server:
port: 80
# Sync time with Home Assistant.
time:
- platform: homeassistant
id: homeassistant_time
# Text sensors with general information.
text_sensor:
# Expose ESPHome version as sensor.
- platform: version
name: salt_level_sensor ESPHome Version
# Expose WiFi information as sensors.
- platform: wifi_info
ip_address:
name: salt_level_sensor IP
ssid:
name: salt_level_sensor SSID
bssid:
name: salt_level_sensor BSSID
sensor:
# Uptime sensor.
- platform: uptime
name: salt_level_sensor Uptime
# WiFi Signal sensor.
- platform: wifi_signal
name: salt_level_sensor WiFi Signal
update_interval: 60s
# Diagnostic sensors
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
# Ultrasonic sensor to measure salt level.
# - platform: ultrasonic
# trigger_pin: GPIO6
# echo_pin: GPIO7
# name: "Salt level in percent"
# update_interval: 1h
# unit_of_measurement: "%"
# # pulse_time: 20us
# # accuracy_decimals: 4
# filters:
# - lambda: return(0.42-x)*(100/0.42);
# # - multiply: 100
# - exponential_moving_average:
# alpha: 0.001
# send_every: 1
# - clamp:
# min_value: 20
# max_value: 100
# on_value_range:
# above: 200
# then:
# - homeassistant.service:
# service: notify.mailgun
# data:
# target:
# -"joshua@cnjmail.com"
# title: "Add Salt"
# message: "Add Salt to the Water Softener Tank"
# - homeassistant.service:
# service: notify.mobile_app_joshuas_iphone_of_pain
# data:
# message: Water softener is low on salt, please add salt.
# Ultrasonic sensor to measure salt level.
- platform: ultrasonic
trigger_pin: GPIO6
echo_pin: GPIO7
name: "Salt level in cm"
update_interval: .5h
unit_of_measurement: "cm"
# pulse_time: 20us
# accuracy_decimals: 4
filters:
- lambda: return(0.83-x)*(100/0.83);
+38
View File
@@ -0,0 +1,38 @@
substitutions:
name: family-room-remote
friendly_name: "Family Room Remote"
packages:
base: !include family-room-remote/family-room-remote.base.yaml
ui: !include family-room-remote/family-room-remote.ui.yaml
bindings: !include family-room-remote/family-room-remote.bindings.yaml
esphome:
name: ${name}
friendly_name: ${friendly_name}
name_add_mac_suffix: false
esp32:
board: esp32-s3-devkitc-1
variant: ESP32S3
flash_size: 16MB
framework:
type: esp-idf
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
logger:
baud_rate: 0
@@ -0,0 +1,173 @@
psram:
mode: octal
speed: 80MHz
i2c:
id: i2c_main
sda: GPIO19
scl: GPIO20
frequency: 400kHz
scan: false
# --- Backlight control (GPIO2 direct) ---
switch:
- platform: gpio
name: "LCD Backlight Raw"
id: lcd_backlight_raw
restore_mode: ALWAYS_ON
pin:
number: GPIO2
mode:
output: true
light:
- platform: binary
name: "Family Room Remote Backlight"
output: lcd_backlight_out
id: ha_remote_backlight
restore_mode: ALWAYS_ON
output:
- platform: template
id: lcd_backlight_out
type: binary
write_action:
- if:
condition:
lambda: return state;
then:
- switch.turn_on: lcd_backlight_raw
else:
- switch.turn_off: lcd_backlight_raw
# --- Inactivity tracking (off after 2 minutes, wake on touch) ---
globals:
- id: last_activity_ms
type: uint32_t
restore_value: no
initial_value: '0'
interval:
- interval: 1s
then:
- lambda: |-
if (id(last_activity_ms) == 0) id(last_activity_ms) = millis();
- if:
condition:
lambda: |-
const uint32_t idle_s = (millis() - id(last_activity_ms)) / 1000;
return idle_s == 120;
then:
- light.turn_off: ha_remote_backlight
# --- Display (ST7262 parallel RGB, 800x480) ---
display:
- platform: rpi_dpi_rgb
id: main_display
dimensions:
width: 800
height: 480
de_pin: GPIO40
hsync_pin: GPIO39
vsync_pin: GPIO41
pclk_pin: GPIO42
pclk_frequency: 16MHz
color_order: BGR
data_pins:
red:
- GPIO45
- GPIO48
- GPIO47
- GPIO21
- GPIO14
green:
- GPIO5
- GPIO6
- GPIO7
- GPIO15
- GPIO16
- GPIO4
blue:
- GPIO8
- GPIO3
- GPIO46
- GPIO9
- GPIO1
hsync_back_porch: 48
hsync_front_porch: 40
hsync_pulse_width: 48
vsync_back_porch: 32
vsync_front_porch: 13
vsync_pulse_width: 3
update_interval: never
auto_clear_enabled: false
# --- Touch (GT911 capacitive) ---
# NOTE: interrupt_pin GPIO18 requires soldering the R17 bridge on the board.
# If not soldered, remove the interrupt_pin line (falls back to polling).
touchscreen:
platform: gt911
id: touch_panel
i2c_id: i2c_main
reset_pin: GPIO38
on_touch:
then:
- lambda: |-
id(last_activity_ms) = millis();
- light.turn_on: ha_remote_backlight
# --- MDI Icon Font ---
font:
- file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf"
id: mdi_icons
size: 24
bpp: 4
glyphs:
- "\U000F0335" # mdi:lightbulb
- "\U000F0425" # mdi:power
- "\U000F0079" # mdi:battery
- "\U000F12A1" # mdi:battery-low
- "\U000F12A2" # mdi:battery-medium
- "\U000F12A3" # mdi:battery-high
- "\U000F12A4" # mdi:battery-charging-low
- "\U000F12A5" # mdi:battery-charging-medium
- "\U000F12A6" # mdi:battery-charging-high
- "\U000F0091" # mdi:battery-unknown
- "\U000F10CD" # mdi:battery-alert-variant-outline
# --- Battery fuel gauge (MAX17048 via I2C) ---
# DISABLED for testing - suspected display interference
# sensor:
# - platform: max17043
# id: max17048_battery
# i2c_id: i2c_main
# update_interval: 120s
# battery_voltage:
# name: "Remote Battery Voltage"
# id: remote_battery_voltage
# entity_category: diagnostic
# device_class: voltage
# state_class: measurement
# accuracy_decimals: 3
# battery_level:
# name: "Remote Battery Level"
# id: remote_battery_level
# entity_category: diagnostic
# device_class: battery
# state_class: measurement
# accuracy_decimals: 0
# on_value:
# then:
# - lambda: |-
# ESP_LOGI("battery", "Level: %.0f%%", x);
# // Update the LVGL battery icon based on level
# lv_obj_t *lbl = id(battery_icon);
# if (x < 10)
# lv_label_set_text(lbl, "\U000F10CD"); // battery-alert-variant-outline
# else if (x < 30)
# lv_label_set_text(lbl, "\U000F12A1"); // battery-low
# else if (x < 70)
# lv_label_set_text(lbl, "\U000F12A2"); // battery-medium
# else
# lv_label_set_text(lbl, "\U000F12A3"); // battery-high
@@ -0,0 +1,59 @@
text_sensor:
- platform: wifi_info
ip_address:
name: "Family Room Remote IP"
id: ip_addr
- platform: homeassistant
id: ts_family_room_tv_stand
entity_id: light.family_room_tv_stand
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_family_room_tv_stand
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_family_room_tv_stand);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_small_family_room_lamp
entity_id: light.small_family_room_lamp
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_small_family_room_lamp
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_small_family_room_lamp);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_big_family_room_lamp
entity_id: switch.big_family_room_lamp
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_big_family_room_lamp
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_big_family_room_lamp);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
@@ -0,0 +1,231 @@
lvgl:
displays:
- main_display
touchscreens:
- touch_panel
disp_bg_color: 0x141218
style_definitions:
# ── Top App Bar ──
- id: md3_header
bg_color: 0x1D1B20
bg_opa: 100%
border_width: 0
radius: 0
pad_all: 0
shadow_width: 0
# ── Status Chip ──
- id: md3_chip
bg_color: 0x2B2930
bg_opa: 100%
border_color: 0x49454F
border_width: 1
border_opa: 100%
radius: 8
pad_all: 0
# ── Text Styles ──
- id: md3_title
text_color: 0xE3E2E6
text_opa: 100%
- id: md3_meta
text_color: 0xC4C6CF
text_opa: 100%
- id: md3_icon
text_color: 0xC4C6CF
text_opa: 100%
# ── Card Variants ──
- id: md3_card_blue
bg_color: 0x2B2930
bg_opa: 100%
border_color: 0x49454F
border_width: 1
border_opa: 100%
radius: 16
shadow_width: 0
- id: md3_card_pink
bg_color: 0x2B2930
bg_opa: 100%
border_color: 0x49454F
border_width: 1
border_opa: 100%
radius: 16
shadow_width: 0
- id: md3_card_green
bg_color: 0x2B2930
bg_opa: 100%
border_color: 0x49454F
border_width: 1
border_opa: 100%
radius: 16
shadow_width: 0
theme:
label:
text_color: 0xE3E2E6
button:
text_color: 0xE3E2E6
pages:
- id: home
widgets:
# ── Header ──
- obj:
x: 0
y: 0
width: 800
height: 64
styles: md3_header
widgets:
- label:
x: 24
y: 20
text_color: 0xE3E2E6
text: "Family Room"
# ── TV Stand ──
- button:
id: btn_family_room_tv_stand
x: 24
y: 80
width: 234
height: 300
checkable: true
styles: md3_card_blue
checked:
bg_color: 0xA8C7FA
bg_opa: 100%
border_color: 0xA8C7FA
border_opa: 100%
text_color: 0x001A33
on_click:
- homeassistant.service:
service: light.toggle
data:
entity_id: light.family_room_tv_stand
widgets:
- label:
x: 194
y: 16
styles: md3_icon
text_font: mdi_icons
text: "\U000F0335" # mdi:lightbulb
- label:
x: 18
y: 16
styles: md3_meta
text: "LIGHT"
- label:
x: 18
y: 56
styles: md3_title
text: "TV Stand"
- label:
x: 18
y: 90
styles: md3_meta
text: "Tap to toggle"
# ── Small Lamp ──
- button:
id: btn_small_family_room_lamp
x: 282
y: 80
width: 234
height: 300
checkable: true
styles: md3_card_pink
checked:
bg_color: 0xFFD7F1
bg_opa: 100%
border_color: 0xFFD7F1
border_opa: 100%
text_color: 0x22001F
on_click:
- homeassistant.service:
service: light.toggle
data:
entity_id: light.small_family_room_lamp
widgets:
- label:
x: 194
y: 16
styles: md3_icon
text_font: mdi_icons
text: "\U000F0335" # mdi:lightbulb
- label:
x: 18
y: 16
styles: md3_meta
text: "LIGHT"
- label:
x: 18
y: 56
styles: md3_title
text: "Small Lamp"
- label:
x: 18
y: 90
styles: md3_meta
text: "Tap to toggle"
# ── Standing Lamp ──
- button:
id: btn_big_family_room_lamp
x: 540
y: 80
width: 236
height: 300
checkable: true
styles: md3_card_green
checked:
bg_color: 0xB5CCBA
bg_opa: 100%
border_color: 0xB5CCBA
border_opa: 100%
text_color: 0x001F15
on_click:
- homeassistant.service:
service: light.toggle
data:
entity_id: light.living_room_lamp_1
widgets:
- label:
x: 196
y: 16
styles: md3_icon
text_font: mdi_icons
text: "\U000F0335" # mdi:lightbulb
- label:
x: 18
y: 16
styles: md3_meta
text: "LIGHT"
- label:
x: 18
y: 56
styles: md3_title
text: "Standing Lamp"
- label:
x: 18
y: 90
styles: md3_meta
text: "Tap to toggle"
# ── Status Bar ──
- obj:
x: 0
y: 400
width: 800
height: 80
styles: md3_header
widgets:
- obj:
x: 8
y: 20
width: 84
height: 40
styles: md3_chip
widgets:
- label:
id: battery_icon
align: center
text_font: mdi_icons
text_color: 0xC4C6CF
text: "\U000F10CD" # mdi:battery-alert-variant-outline
+132
View File
@@ -0,0 +1,132 @@
substitutions:
device_name: fdm-exhaust-fan-control
friendly_name: "FDM Exhaust Fan Control"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: seeed_xiao_esp32c3
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.110
gateway: 192.168.69.1
subnet: 255.255.255.0
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if you use static IPs and don't need discovery
captive_portal:
web_server:
port: 80
# GPIO Outputs for fan control
# IMPORTANT: inverted: true because NPN transistor driver inverts the logic
# GPIO HIGH -> NPN ON -> Gate LOW -> Fan OFF
# GPIO LOW -> NPN OFF -> Gate HIGH (5V via pull-up) -> Fan ON
output:
- platform: gpio
pin:
number: GPIO3 # D1 on XIAO
inverted: true
id: fan_80mm_output
- platform: gpio
pin:
number: GPIO4 # D2 on XIAO
inverted: true
id: fan_120mm_output
# Fan entities for Home Assistant
fan:
- platform: binary
name: "80mm Exhaust Fan"
id: fan_80mm
output: fan_80mm_output
- platform: binary
name: "120mm Exhaust Fan"
id: fan_120mm
output: fan_120mm_output
# Optional: Add a switch to control both fans together
switch:
- platform: template
name: "All Fans"
id: all_fans
turn_on_action:
- fan.turn_on: fan_80mm
- fan.turn_on: fan_120mm
turn_off_action:
- fan.turn_off: fan_80mm
- fan.turn_off: fan_120mm
lambda: |-
return id(fan_80mm).state && id(fan_120mm).state;
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
CODEOWNERS = ["@blacknell"]
@@ -0,0 +1,20 @@
#pragma once
#include "esphome/core/automation.h"
#include "max17043.h"
namespace esphome {
namespace max17043 {
template<typename... Ts> class SleepAction : public Action<Ts...> {
public:
explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {}
void play(Ts... x) override { this->max17043_->sleep_mode(); }
protected:
MAX17043Component *max17043_;
};
} // namespace max17043
} // namespace esphome
@@ -0,0 +1,72 @@
#include "max17043.h"
#include "esphome/core/log.h"
namespace esphome {
namespace max17043 {
// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
// Consult the datasheet at https://www.analog.com/en/products/max17043.html
static const char *const TAG = "max17043";
static const uint8_t MAX17043_VCELL = 0x02;
static const uint8_t MAX17043_SOC = 0x04;
static const uint8_t MAX17043_CONFIG = 0x0c;
static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C;
static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4)
static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080;
void MAX17043Component::update() {
uint16_t raw_voltage, raw_percent;
if (this->voltage_sensor_ != nullptr) {
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
this->status_set_warning("Unable to read MAX17043_VCELL");
} else {
float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0;
this->voltage_sensor_->publish_state(voltage);
this->status_clear_warning();
}
}
if (this->battery_remaining_sensor_ != nullptr) {
if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) {
this->status_set_warning("Unable to read MAX17043_SOC");
} else {
float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff));
this->battery_remaining_sensor_->publish_state(percent);
this->status_clear_warning();
}
}
}
void MAX17043Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up MAX17043...");
// Compatible mode for MAX17048/variant boards:
// avoid setup-time register writes on the shared touch I2C bus.
}
void MAX17043Component::dump_config() {
ESP_LOGCONFIG(TAG, "MAX17043:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with MAX17043 failed");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Battery Voltage", this->voltage_sensor_);
LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_);
}
float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; }
void MAX17043Component::sleep_mode() {
if (!this->is_failed()) {
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) {
ESP_LOGW(TAG, "Unable to write the sleep bit to config register");
this->status_set_warning();
}
}
}
} // namespace max17043
} // namespace esphome
@@ -0,0 +1,29 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace max17043 {
class MAX17043Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void sleep_mode();
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) {
battery_remaining_sensor_ = battery_remaining_sensor;
}
protected:
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *battery_remaining_sensor_{nullptr};
};
} // namespace max17043
} // namespace esphome
@@ -0,0 +1,77 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_LEVEL,
CONF_BATTERY_VOLTAGE,
CONF_ID,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_VOLTAGE,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_VOLT,
)
DEPENDENCIES = ["i2c"]
max17043_ns = cg.esphome_ns.namespace("max17043")
MAX17043Component = max17043_ns.class_(
"MAX17043Component", cg.PollingComponent, i2c.I2CDevice
)
# Actions
SleepAction = max17043_ns.class_("SleepAction", automation.Action)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MAX17043Component),
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_BATTERY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x36))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
if CONF_BATTERY_LEVEL in config:
sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
cg.add(var.set_battery_remaining_sensor(sens))
MAX17043_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(MAX17043Component),
}
)
@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA)
async def max17043_sleep_mode_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
+176
View File
@@ -0,0 +1,176 @@
psram:
mode: octal
speed: 80MHz
i2c:
id: i2c_main
sda: 8
scl: 9
frequency: 400kHz
scan: false
# CH422G I/O expander (Waveshare uses it for LCD reset/backlight/touch reset)
ch422g:
- id: ch422g_hub
i2c_id: i2c_main
# --- Backlight control (CH422G IO2 is common for Waveshare LCD BL) ---
switch:
- platform: gpio
name: "LCD Backlight Raw"
id: lcd_backlight_raw
restore_mode: ALWAYS_ON
pin:
ch422g: ch422g_hub
number: 2
mode:
output: true
# A nicer HA-exposed control (so you can also automate it from HA)
light:
- platform: binary
name: "HA Remote Backlight"
output: lcd_backlight_out
id: ha_remote_backlight
output:
- platform: template
id: lcd_backlight_out
type: binary
write_action:
- if:
condition:
lambda: return state;
then:
- switch.turn_on: lcd_backlight_raw
else:
- switch.turn_off: lcd_backlight_raw
# --- Inactivity tracking (dim/off + wake on touch) ---
globals:
- id: last_activity_ms
type: uint32_t
restore_value: no
initial_value: '0'
interval:
- interval: 1s
then:
- lambda: |-
if (id(last_activity_ms) == 0) id(last_activity_ms) = millis();
# Turn off backlight ONCE at 2 minutes idle.
- if:
condition:
lambda: |-
const uint32_t idle_s = (millis() - id(last_activity_ms)) / 1000;
return idle_s == 120;
then:
- light.turn_off: ha_remote_backlight
# --- Display ---
display:
- platform: mipi_rgb
model: ESP32-S3-TOUCH-LCD-7-800X480
id: main_display
update_interval: never
auto_clear_enabled: false
reset_pin:
ch422g: ch422g_hub
number: 3
mode:
output: true
# --- Touch ---
touchscreen:
platform: gt911
id: touch_panel
i2c_id: i2c_main
interrupt_pin: 4
reset_pin:
ch422g: ch422g_hub
number: 1
mode:
output: true
on_touch:
then:
- lambda: |-
id(last_activity_ms) = millis();
- light.turn_on: ha_remote_backlight
# --- MDI Icon Font ---
font:
- file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf"
id: mdi_icons
size: 24
bpp: 4
glyphs:
# Tile icons
- "\U000F0335" # mdi:lightbulb
- "\U000F0425" # mdi:power
- "\U000F0426" # mdi:power-plug
- "\U000F07E9" # mdi:power-socket-us
- "\U000F1A26" # mdi:toggle-switch-variant-off
# Battery status icons
- "\U000F0079" # mdi:battery
- "\U000F12A1" # mdi:battery-low
- "\U000F12A2" # mdi:battery-medium
- "\U000F12A3" # mdi:battery-high
- "\U000F12A4" # mdi:battery-charging-low
- "\U000F12A5" # mdi:battery-charging-medium
- "\U000F12A6" # mdi:battery-charging-high
- "\U000F0091" # mdi:battery-unknown
- "\U000F10CD" # mdi:battery-alert-variant-outline
# Navigation bar icons
- "\U000F04B9" # mdi:sofa
- "\U000F06B5" # mdi:lamp
- "\U000F04DE" # mdi:stove
- "\U000F12BD" # mdi:stairs-up
- "\U000F1239" # mdi:desk
- "\U000F06D9" # mdi:garage
- "\U000F0531" # mdi:tree
# --- LVGL UI ---
# Note: On ESP32-S3-Touch-LCD-7, GPIO14 is used by the RGB display bus,
# so it cannot be reused as ADC for battery telemetry in this display mode.
# --- Battery fuel gauge (Adafruit MAX17048 via compatible driver) ---
sensor:
- platform: max17043
id: max17048_battery
i2c_id: i2c_main
update_interval: 120s
battery_voltage:
name: "Remote Battery Voltage"
id: remote_battery_voltage
entity_category: diagnostic
device_class: voltage
state_class: measurement
accuracy_decimals: 3
on_value:
then:
- lambda: |-
ESP_LOGI("battery", "Voltage: %.3f V", x);
battery_level:
name: "Remote Battery Level"
id: remote_battery_level
entity_category: diagnostic
device_class: battery
state_class: measurement
accuracy_decimals: 0
on_value:
then:
- lambda: |-
const float pct = x;
ESP_LOGI("battery", "Level: %.0f%%", pct);
# - platform: custom
# lambda: |-
# auto max17048_sensor = new MAX17048Sensor();
# App.register_component(max17048_sensor);
# return {max17048_sensor->voltage_sensor, max17048_sensor->percentage_sensor};
# sensors:
# - name: "Voltage"
# unit_of_measurement: V
# accuracy_decimals: 2
# - name: "Percentage"
# unit_of_measurement: '%'
+383
View File
@@ -0,0 +1,383 @@
text_sensor:
- platform: wifi_info
ip_address:
name: "HA Remote IP"
id: ip_addr
- platform: homeassistant
id: ts_family_room_tv_stand
entity_id: light.family_room_tv_stand
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_family_room_tv_stand
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_family_room_tv_stand);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_small_family_room_lamp
entity_id: light.small_family_room_lamp
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_small_family_room_lamp
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_small_family_room_lamp);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_living_room_lamp_1
entity_id: light.living_room_lamp_1
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_family_room_standing_lamp
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x3D0002) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_family_room_standing_lamp);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_living_room_light_2
entity_id: light.living_room_light_2
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_living_room_main_light
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_living_room_main_light);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_patio_light_1
entity_id: light.patio_light_1
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_living_room_patio_light
state:
checked: !lambda return x == "on";
- lvgl.widget.update:
id: btn_outside_patio_light
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t off_color = lv_color_hex(0xE3E2E6);
auto *btn1 = id(btn_living_room_patio_light);
auto *btn2 = id(btn_outside_patio_light);
if (x == "on") {
lv_obj_set_style_text_color(btn1, lv_color_hex(0x1A0C00), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), lv_color_hex(0x1A0C00), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
lv_obj_set_style_text_color(btn2, lv_color_hex(0x001A33), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), lv_color_hex(0x001A33), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
} else {
lv_obj_set_style_text_color(btn1, off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
lv_obj_set_style_text_color(btn2, off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
}
- platform: homeassistant
id: ts_kitchen_sink_light
entity_id: light.kitchen_sink_light
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_kitchen_sink_light
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_kitchen_sink_light);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_office_lamp_2
entity_id: light.office_lamp_2
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_office_lamp
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_office_lamp);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_office_led_strip
entity_id: light.led_strip_controller_led_strip_controller
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_office_led_strip
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_office_led_strip);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_garage_cam_light
entity_id: light.esp32_saturn4_cam_esp32_saturn4_cam_light
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_garage_cam_light
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_garage_cam_light);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_office_echo_plug
entity_id: switch.office_echo_plug
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_office_echo_plug
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_office_echo_plug);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_office_end_table_lamp_outlet
entity_id: switch.office_end_table_lamp_socket_1
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_office_end_table_lamp_outlet
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_office_end_table_lamp_outlet);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_office_wax_warmer_outlet
entity_id: switch.office_wax_warmer_socket_1
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_office_wax_warmer_outlet
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_office_wax_warmer_outlet);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_outside_lamppost_outlet_1
entity_id: switch.lamppost_outlets_socket_1
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_outside_lamppost_outlet_1
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_outside_lamppost_outlet_1);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_outside_lamppost_outlet_2
entity_id: switch.lamppost_outlets_socket_2
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_outside_lamppost_outlet_2
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_outside_lamppost_outlet_2);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_outside_porch_decor_outlet
entity_id: switch.washing_machine_socket_1
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_outside_porch_decor_outlet
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_outside_porch_decor_outlet);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_living_room_garland_switch
entity_id: switch.big_family_room_lamp
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_living_room_garland_switch
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_living_room_garland_switch);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_living_room_calendar_switch
entity_id: switch.digital_calendar
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_living_room_calendar_switch
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_living_room_calendar_switch);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_upstairs_airquality_oled
entity_id: switch.airqualitysensor_3_oled_power
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_upstairs_airquality_oled
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_upstairs_airquality_oled);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_upstairs_camera_motion
entity_id: switch.upstairs_camera_motion_detection
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_upstairs_camera_motion
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_upstairs_camera_motion);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_garage_fume_exhaust_fan
entity_id: switch.fume_exhaust_fan
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_garage_fume_exhaust_fan
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_garage_fume_exhaust_fan);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
- platform: homeassistant
id: ts_garage_resin_printer_heater
entity_id: switch.resin_printer_heater
internal: true
on_value:
then:
- lvgl.widget.update:
id: btn_garage_resin_printer_heater
state:
checked: !lambda return x == "on";
- lambda: |-
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
auto *btn = id(btn_garage_resin_printer_heater);
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
}
File diff suppressed because it is too large Load Diff
+202
View File
@@ -0,0 +1,202 @@
substitutions:
friendly_name: "Home Energy Monitor"
device_name: home-energy-monitor
update_time: 59s
# SCT-013-000 (100A/50mA) calibration value
current_cal: '27518'
# Jameco 9VAC Transformer (board v1.3+)
voltage_cal: '7305'
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: nodemcu-32s
framework:
type: esp-idf
# Enable logging
logger:
level: INFO
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
# Enable OTA updates
ota:
- platform: esphome
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Fallback hotspot if Wi-Fi fails
ap:
ssid: ${device_name}
password: !secret wifi_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if using static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
# SPI bus configuration
spi:
clk_pin: 18
miso_pin: 19
mosi_pin: 23
sensor:
# ──────────────────────────────────
# IC1: Channels 1-3 (CS pin 5)
# ──────────────────────────────────
- platform: atm90e32
cs_pin: 5
phase_a:
voltage:
name: ${device_name} Volts
id: ic1Volts
accuracy_decimals: 1
current:
name: ${device_name} CT1 Amps
id: ct1Amps
power:
name: ${device_name} CT1 Watts
id: ct1Watts
gain_voltage: ${voltage_cal}
gain_ct: ${current_cal}
phase_b:
current:
name: ${device_name} CT2 Amps
id: ct2Amps
power:
name: ${device_name} CT2 Watts
id: ct2Watts
gain_voltage: ${voltage_cal}
gain_ct: ${current_cal}
phase_c:
current:
name: ${device_name} CT3 Amps
id: ct3Amps
power:
name: ${device_name} CT3 Watts
id: ct3Watts
gain_voltage: ${voltage_cal}
gain_ct: ${current_cal}
frequency:
name: ${device_name} Frequency
line_frequency: 60Hz
gain_pga: 1X
update_interval: ${update_time}
# ──────────────────────────────────
# IC2: Channels 4-6 (CS pin 4)
# ──────────────────────────────────
- platform: atm90e32
cs_pin: 4
phase_a:
current:
name: ${device_name} CT4 Amps
id: ct4Amps
power:
name: ${device_name} CT4 Watts
id: ct4Watts
gain_voltage: ${voltage_cal}
gain_ct: ${current_cal}
phase_b:
current:
name: ${device_name} CT5 Amps
id: ct5Amps
power:
name: ${device_name} CT5 Watts
id: ct5Watts
gain_voltage: ${voltage_cal}
gain_ct: ${current_cal}
phase_c:
current:
name: ${device_name} CT6 Amps
id: ct6Amps
power:
name: ${device_name} CT6 Watts
id: ct6Watts
gain_voltage: ${voltage_cal}
gain_ct: ${current_cal}
line_frequency: 60Hz
gain_pga: 1X
update_interval: ${update_time}
# ──────────────────────────────────
# Calculated: Total Home Power
# (CT1 = Main Leg 1, CT2 = Main Leg 2)
# ──────────────────────────────────
- platform: template
name: ${device_name} Total Watts
id: totalWatts
lambda: "return id(ct1Watts).state + id(ct2Watts).state;"
accuracy_decimals: 1
unit_of_measurement: W
device_class: power
update_interval: ${update_time}
- platform: total_daily_energy
name: ${device_name} Total Daily Energy
power_id: totalWatts
unit_of_measurement: kWh
accuracy_decimals: 2
filters:
- multiply: 0.001
# Diagnostic sensors
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
time:
- platform: homeassistant
id: homeassistant_time
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
@@ -0,0 +1,49 @@
# ===== I2S Audio =====
i2s_audio:
- id: i2s_in
i2s_lrclk_pin: ${i2s_in_lrclk_pin}
i2s_bclk_pin: ${i2s_in_bclk_pin}
- id: i2s_out
i2s_lrclk_pin: ${i2s_out_lrclk_pin}
i2s_bclk_pin: ${i2s_out_bclk_pin}
microphone:
- platform: i2s_audio
id: mic
i2s_audio_id: i2s_in
i2s_din_pin: ${mic_din_pin}
adc_type: external
pdm: false
bits_per_sample: 32bit
channel: left
speaker:
- platform: i2s_audio
id: spk
i2s_audio_id: i2s_out
i2s_dout_pin: ${speaker_dout_pin}
dac_type: external
bits_per_sample: 32bit
channel: mono
sample_rate: 16000
# Voice Assistant needs the speaker to fully stop after playback so it can
# leave RESPONSE_FINISHED and return to IDLE for the next wake word.
timeout: 500ms
rtttl:
id: volume_beep
speaker: spk
# ===== Wake Word =====
micro_wake_word:
id: mww
microphone: mic
models:
- model: okay_nabu
id: wake_word_model
on_wake_word_detected:
- voice_assistant.start:
wake_word: !lambda return wake_word;
- light.turn_on:
id: led_bar
effect: "Listening Pulse"
@@ -0,0 +1,125 @@
packages:
jarvis-satellite-led: !include jarvis-satellite-led.yaml
jarvis-satellite-voice: !include jarvis-satellite-voice.yaml
jarvis-satellite-audio: !include jarvis-satellite-audio.yaml
jarvis-satellite-ui: !include jarvis-satellite-ui.yaml
substitutions:
# Flash & PSRAM
flash_size: "8MB"
psram_mode: "quad"
psram_speed: "80MHz"
# CPU Frequency
cpu_frequency: "240MHz"
# I2S Microphone pins
i2s_in_lrclk_pin: GPIO6
i2s_in_bclk_pin: GPIO7
mic_din_pin: GPIO4
# I2S Speaker pins
i2s_out_lrclk_pin: GPIO45
i2s_out_bclk_pin: GPIO46
speaker_dout_pin: GPIO8
# LED bar
led_pin: GPIO16
led_num_leds: "8"
# Microphone sensitivity (tuned for INMP441)
mic_noise_suppression_level: "2"
mic_auto_gain: "10dBFS"
mic_volume_multiplier: "4.0"
# Button pins
btn_vol_up_pin: GPIO1
btn_vol_down_pin: GPIO2
btn_mute_pin: GPIO3
btn_wake_pin: GPIO10
# Change this to true in case you have a hidden SSID at home.
hidden_ssid: "true"
globals:
- id: volume_level
type: float
restore_value: true
initial_value: '0.8'
esphome:
platformio_options:
board_build.flash_mode: dio
on_boot:
- priority: -100
then:
- lambda: id(spk).set_volume(id(volume_level));
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
cpu_frequency: ${cpu_frequency}
flash_size: ${flash_size}
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y"
# Moves instructions and read only data from flash into PSRAM on boot.
# Both enabled allows instructions to execute while a flash operation is in progress without needing to be placed in IRAM.
# Considerably speeds up mWW at the cost of using more PSRAM.
CONFIG_SPIRAM_RODATA: "y"
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y"
CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y"
CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y"
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y"
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y"
wifi:
id: wifi_id
fast_connect: ${hidden_ssid}
network:
enable_ipv6: true
http_request:
web_server:
port: 80
psram:
mode: ${psram_mode}
speed: ${psram_speed}
captive_portal:
api:
encryption:
key: !secret api_encryption_key
on_client_connected:
- delay: 50ms
- if:
condition:
not:
lambda: 'return id(mww).is_running();'
then:
- micro_wake_word.start:
on_client_disconnected:
- micro_wake_word.stop:
- voice_assistant.stop:
logger:
level: DEBUG
logs:
micro_wake_word: DEBUG
voice_assistant: DEBUG
ota:
- platform: esphome
password: !secret ota_password
@@ -0,0 +1,111 @@
# ===== Physical Buttons =====
# Note: esp32_touch is not supported on ESP32-S3 with ESP-IDF.
# Wire physical momentary buttons to these pins (active LOW with internal pullup).
binary_sensor:
- platform: gpio
pin:
number: ${btn_vol_up_pin}
mode: INPUT_PULLUP
inverted: true
name: "Volume Up"
on_press:
- lambda: |-
float vol = min(1.0f, id(volume_level) + 0.1f);
id(volume_level) = vol;
id(spk).set_volume(vol);
- rtttl.play:
id: volume_beep
rtttl: "beep:d=32,o=6,b=180:c"
- light.turn_on:
id: led_bar
brightness: 100%
effect: "none"
- delay: 150ms
- light.turn_on:
id: led_bar
effect: "Idle Breathe"
- platform: gpio
pin:
number: ${btn_vol_down_pin}
mode: INPUT_PULLUP
inverted: true
name: "Volume Down"
on_press:
- lambda: |-
float vol = max(0.0f, id(volume_level) - 0.1f);
id(volume_level) = vol;
id(spk).set_volume(vol);
- rtttl.play:
id: volume_beep
rtttl: "beep:d=32,o=5,b=180:c"
- light.turn_on:
id: led_bar
brightness: 30%
effect: "none"
- delay: 150ms
- light.turn_on:
id: led_bar
effect: "Idle Breathe"
- platform: gpio
pin:
number: ${btn_mute_pin}
mode: INPUT_PULLUP
inverted: true
name: "Mic Mute"
on_press:
- switch.toggle: mic_mute
- platform: gpio
pin:
number: ${btn_wake_pin}
mode: INPUT_PULLUP
inverted: true
name: "Wake"
on_press:
- if:
condition:
switch.is_off: mic_mute
then:
- voice_assistant.start:
- light.turn_on:
id: led_bar
effect: "Listening Pulse"
else:
- light.turn_on:
id: led_bar
effect: "Error Flash"
- delay: 500ms
- light.turn_on:
id: led_bar
red: 100%
green: 30%
blue: 0%
brightness: 50%
effect: "none"
# ===== Mic Mute Switch =====
switch:
- platform: template
id: mic_mute
name: "Microphone Mute"
icon: "mdi:microphone-off"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
on_turn_on:
- voice_assistant.stop:
- light.turn_on:
id: led_bar
effect: "none"
- light.turn_on:
id: led_bar
red: 100%
green: 30%
blue: 0%
brightness: 50%
on_turn_off:
- light.turn_on:
id: led_bar
effect: "Idle Breathe"
- voice_assistant.start:
@@ -0,0 +1,67 @@
# ===== LED Bar (WS2812B) =====
light:
- platform: esp32_rmt_led_strip
id: led_bar
chipset: WS2812
rgb_order: GRB
pin: ${led_pin}
num_leds: ${led_num_leds}
name: "Voice Satellite LEDs"
default_transition_length: 200ms
effects:
- addressable_lambda:
name: "Idle Breathe"
update_interval: 50ms
lambda: |-
static float brightness = 0;
static int direction = 1;
brightness += direction * 0.02;
if (brightness >= 1.0) { brightness = 1.0; direction = -1; }
if (brightness <= 0.1) { brightness = 0.1; direction = 1; }
for (int i = 0; i < it.size(); i++) {
it[i] = Color(0, 0, (int)(40 * brightness));
}
- addressable_lambda:
name: "Listening Pulse"
update_interval: 30ms
lambda: |-
static int pos = 0;
pos = (pos + 1) % (it.size() * 2);
for (int i = 0; i < it.size(); i++) {
int dist = abs(pos - i);
if (pos >= it.size()) dist = abs((it.size() * 2 - pos) - i);
int bright = max(0, 255 - dist * 60);
it[i] = Color(0, bright, bright);
}
- addressable_lambda:
name: "Processing"
update_interval: 80ms
lambda: |-
static int offset = 0;
offset = (offset + 1) % it.size();
for (int i = 0; i < it.size(); i++) {
if ((i + offset) % 2 == 0) {
it[i] = Color(80, 0, 180);
} else {
it[i] = Color(0, 0, 0);
}
}
- addressable_lambda:
name: "Speaking"
update_interval: 40ms
lambda: |-
static int wave = 0;
wave = (wave + 1) % 16;
for (int i = 0; i < it.size(); i++) {
int bright = (sin((wave + i * 2) * 0.4) + 1) * 127;
it[i] = Color(0, bright, 0);
}
- addressable_lambda:
name: "Error Flash"
update_interval: 100ms
lambda: |-
static bool on = false;
on = !on;
for (int i = 0; i < it.size(); i++) {
it[i] = on ? Color(255, 0, 0) : Color(0, 0, 0);
}
@@ -0,0 +1,53 @@
# ===== Web UI / HA Entities =====
script:
- id: publish_current_time
then:
- lambda: |-
auto time_now = id(homeassistant_time).now();
id(current_time_sensor).publish_state(time_now.strftime("%H:%M"));
time:
- platform: homeassistant
id: homeassistant_time
on_time_sync:
- script.execute: publish_current_time
on_time:
- seconds: 0
minutes: /1
then:
- script.execute: publish_current_time
text_sensor:
- platform: template
name: "Current device time"
id: current_time_sensor
icon: mdi:clock
number:
- platform: template
name: "Volume"
id: volume_number
icon: mdi:volume-high
entity_category: config
min_value: 0
max_value: 100
step: 5
unit_of_measurement: "%"
lambda: return id(volume_level) * 100.0f;
set_action:
- lambda: |-
float vol = x / 100.0f;
id(volume_level) = vol;
id(spk).set_volume(vol);
select:
- platform: logger
name: "Logger Level"
disabled_by_default: true
button:
- platform: restart
name: "Restart"
icon: mdi:restart
entity_category: config
@@ -0,0 +1,97 @@
# ===== Wake Word Sensitivity =====
select:
- platform: template
name: "Wake Word Sensitivity"
optimistic: true
initial_option: "Slightly sensitive"
restore_value: true
entity_category: config
icon: mdi:ear-hearing
options:
- Slightly sensitive
- Moderately sensitive
- Very sensitive
on_value:
lambda: |-
if (x == "Slightly sensitive") {
id(wake_word_model).set_probability_cutoff(247); // 0.97
} else if (x == "Moderately sensitive") {
id(wake_word_model).set_probability_cutoff(235); // 0.92
} else if (x == "Very sensitive") {
id(wake_word_model).set_probability_cutoff(212); // 0.83
}
# ===== Voice Assistant =====
voice_assistant:
id: va
microphone: mic
speaker: spk
micro_wake_word: mww
noise_suppression_level: ${mic_noise_suppression_level}
auto_gain: ${mic_auto_gain}
volume_multiplier: ${mic_volume_multiplier}
on_listening:
- logger.log: "[VA] on_listening — mic open, sending audio to HA"
- light.turn_on:
id: led_bar
effect: "Listening Pulse"
on_stt_vad_end:
- logger.log: "[VA] on_stt_vad_end — speech detected, processing"
- light.turn_on:
id: led_bar
effect: "Processing"
on_stt_end:
- lambda: |-
ESP_LOGI("va", "STT result: %s", x.c_str());
- light.turn_off:
id: led_bar
on_tts_start:
- lambda: |-
ESP_LOGI("va", "TTS start: %s", x.c_str());
- light.turn_on:
id: led_bar
effect: "Speaking"
on_tts_end:
- logger.log: "[VA] on_tts_end"
on_tts_stream_end:
- logger.log: "[VA] on_tts_stream_end"
on_error:
- lambda: |-
ESP_LOGE("va", "VA error: code=%s, message=%s", code.c_str(), message.c_str());
- if:
condition:
lambda: 'return code != "duplicate_wake_up_detected";'
then:
- light.turn_on:
id: led_bar
effect: "Error Flash"
- delay: 2s
- micro_wake_word.start:
on_end:
- logger.log: "[VA] on_end"
- wait_until:
not:
voice_assistant.is_running:
- light.turn_off:
id: led_bar
- if:
condition:
and:
- voice_assistant.connected:
- not:
micro_wake_word.is_running:
then:
- micro_wake_word.start:
on_idle:
- logger.log: "[VA] on_idle"
- light.turn_on:
id: led_bar
effect: "Idle Breathe"
- if:
condition:
and:
- voice_assistant.connected:
- not:
micro_wake_word.is_running:
then:
- micro_wake_word.start:
+13
View File
@@ -0,0 +1,13 @@
{% set ns = namespace(lines=[]) %}
{% for domain, entities in states | groupby('domain') %}
{% set ns.lines = ns.lines + ['[' ~ domain ~ ']'] %}
{% for s in entities | sort(attribute='entity_id') %}
{% set area = area_name(s.entity_id) if area_name(s.entity_id) else 'No area' %}
{% set friendly = state_attr(s.entity_id, 'friendly_name') if state_attr(s.entity_id, 'friendly_name') else s.object_id %}
{% set ns.lines = ns.lines + [
s.entity_id ~ ' | ' ~ friendly ~ ' | ' ~ area
] %}
{% endfor %}
{% set ns.lines = ns.lines + [''] %}
{% endfor %}
{{ ns.lines | join('\n') }}
+135
View File
@@ -0,0 +1,135 @@
substitutions:
device_name: jordyn-a1-camera
friendly_name: "Jordyn A1 Camera"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: esp-idf
#cpu_frequency: 240MHZ
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "PDB5LTKAcNWMC+MeE03/SKpqX42pcnITzmfwu761LjI="
ota:
- platform: esphome
password: "d207583702e4b979e28a43f6f52d19ae"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Jordyn-A1-Camera"
password: "jN4rzGokp5mt"
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
i2c:
- id: camera_i2c
sda: GPIO26
scl: GPIO27
psram:
mode: quad
speed: 80MHz
esp32_camera:
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_id: camera_i2c
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: 1024x768
jpeg_quality: 20
vertical_flip: False
horizontal_mirror: False
max_framerate: 40 fps
idle_framerate: 0.05 fps
# Image settings
name: esp32-jordyn-a1-cam
# brightness: 1
# #contrast: 0
# agc_gain_ceiling: 2X
# agc_mode: MANUAL
# agc_value: 5
# ...
esp32_camera_web_server:
- port: 80
mode: STREAM
- port: 8080
mode: SNAPSHOT
switch:
- platform: gpio
name: "jordyn-a1-cam-flash"
pin: 4
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
+122
View File
@@ -0,0 +1,122 @@
substitutions:
device_name: laundry-litter-box-camera
friendly_name: "Laundry Litter Box Camera"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.220
gateway: 192.168.69.1
subnet: 255.255.255.0
ap:
ssid: "laundry-litter-cam-fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if you use static IPs and don't need discovery
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
captive_portal:
i2c:
- id: camera_i2c
sda: GPIO26
scl: GPIO27
psram:
mode: quad
speed: 80MHz
esp32_camera:
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_id: camera_i2c
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: 1024x768
jpeg_quality: 15
vertical_flip: true
horizontal_mirror: false
max_framerate: 20 fps
idle_framerate: 0.05 fps
name: ${device_name}
esp32_camera_web_server:
- port: 8080
mode: STREAM
- port: 8081
mode: SNAPSHOT
switch:
- platform: gpio
name: "${device_name}-flash"
pin: 4
+202
View File
@@ -0,0 +1,202 @@
substitutions:
device_name: led-controller-jordyn-a1
friendly_name: "Jordyn's LED Controller"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-c3-devkitm-1
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
# Enable over-the-air updates
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.80
gateway: 192.168.69.1
subnet: 255.255.255.0
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
# LED Light configuration
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
chipset: WS2812
pin: GPIO7
num_leds: 60
name: "${friendly_name}"
id: led_strip
# Default color on boot
restore_mode: RESTORE_DEFAULT_OFF
# Color correction for more accurate colors (optional)
# Uncomment and adjust if your LEDs look too blue/green
# color_correct: [100%, 100%, 100%]
# Effects - you can enable these for more features!
effects:
# Basic effects
- random:
name: "Random"
transition_length: 5s
update_interval: 7s
- strobe:
name: "Strobe"
colors:
- state: true
brightness: 100%
red: 100%
green: 100%
blue: 100%
duration: 500ms
- state: false
duration: 250ms
- flicker:
name: "Flicker"
alpha: 95%
intensity: 1.5%
# Rainbow effects
- addressable_rainbow:
name: "Rainbow"
speed: 10
width: 50
- addressable_color_wipe:
name: "Color Wipe"
colors:
- red: 100%
green: 0%
blue: 0%
num_leds: 1
- red: 0%
green: 100%
blue: 0%
num_leds: 1
- red: 0%
green: 0%
blue: 100%
num_leds: 1
add_led_interval: 100ms
reverse: false
- addressable_scan:
name: "Scan"
move_interval: 100ms
scan_width: 3
- addressable_twinkle:
name: "Twinkle"
twinkle_probability: 5%
progress_interval: 4ms
- addressable_fireworks:
name: "Fireworks"
update_interval: 32ms
spark_probability: 10%
use_random_color: true
fade_out_rate: 120
# Pulse effect
- pulse:
name: "Pulse"
transition_length: 1s
update_interval: 1s
# Sensors for monitoring
sensor:
# WiFi Signal Strength
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
# Uptime
- platform: uptime
name: "${friendly_name} Uptime"
# Diagnostic sensors
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
# Text sensors
text_sensor:
# ESPHome version
- platform: version
name: "${friendly_name} ESPHome Version"
# WiFi info
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Button to restart ESP32
button:
- platform: restart
name: "${friendly_name} Restart"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
+198
View File
@@ -0,0 +1,198 @@
substitutions:
device_name: led-controller-polly-a1m
friendly_name: "Polly's LED Controller"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-c3-devkitm-1
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
# Enable over-the-air updates
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.181
gateway: 192.168.69.1
subnet: 255.255.255.0
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
# LED Light configuration
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
chipset: WS2812
pin: GPIO7
num_leds: 10
name: "${friendly_name}"
id: led_strip
# Default color on boot
restore_mode: RESTORE_DEFAULT_OFF
# Effects - you can enable these for more features!
effects:
# Basic effects
- random:
name: "Random"
transition_length: 5s
update_interval: 7s
- strobe:
name: "Strobe"
colors:
- state: true
brightness: 100%
red: 100%
green: 100%
blue: 100%
duration: 500ms
- state: false
duration: 250ms
- flicker:
name: "Flicker"
alpha: 95%
intensity: 1.5%
# Rainbow effects
- addressable_rainbow:
name: "Rainbow"
speed: 10
width: 50
- addressable_color_wipe:
name: "Color Wipe"
colors:
- red: 100%
green: 0%
blue: 0%
num_leds: 1
- red: 0%
green: 100%
blue: 0%
num_leds: 1
- red: 0%
green: 0%
blue: 100%
num_leds: 1
add_led_interval: 100ms
reverse: false
- addressable_scan:
name: "Scan"
move_interval: 100ms
scan_width: 3
- addressable_twinkle:
name: "Twinkle"
twinkle_probability: 5%
progress_interval: 4ms
- addressable_fireworks:
name: "Fireworks"
update_interval: 32ms
spark_probability: 10%
use_random_color: true
fade_out_rate: 120
# Pulse effect
- pulse:
name: "Pulse"
transition_length: 1s
update_interval: 1s
# Sensors for monitoring
sensor:
# WiFi Signal Strength
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
# Uptime
- platform: uptime
name: "${friendly_name} Uptime"
# Diagnostic sensors
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
# Text sensors
text_sensor:
# ESPHome version
- platform: version
name: "${friendly_name} ESPHome Version"
# WiFi info
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Button to restart ESP32
button:
- platform: restart
name: "${friendly_name} Restart"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
+340
View File
@@ -0,0 +1,340 @@
substitutions:
name: mbr-ha-remote
friendly_name: "MBR HA Remote"
# Home Assistant entity IDs - UPDATE THESE TO MATCH YOUR SETUP
light_1_entity: switch.pollys_light
light_2_entity: switch.joshuas_light
fan_entity: switch.parents_ceiling_fan
esphome:
name: ${name}
friendly_name: ${friendly_name}
on_boot:
priority: -100
then:
- lambda: |-
id(last_interaction_ms) = millis();
id(backlight_is_on) = true;
- component.update: my_display
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
ap:
ssid: "${name} Fallback"
password: !secret wifi_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
# SPI for display and touchscreen (CYD uses two separate SPI buses)
spi:
- id: tft_spi
clk_pin: GPIO14
mosi_pin: GPIO13
miso_pin: GPIO12
- id: touch_spi
clk_pin: GPIO25
mosi_pin: GPIO32
miso_pin: GPIO39
# ILI9341 Display (2.8" 320x240)
display:
- platform: ili9xxx
model: ili9341
spi_id: tft_spi
cs_pin: GPIO15
dc_pin: GPIO2
dimensions:
width: 320
height: 240
# Use palette mode to lower display buffer memory usage.
color_palette: 8BIT
data_rate: 20MHz
update_interval: never
rotation: 0
invert_colors: false
id: my_display
lambda: |-
it.fill(Color(0x1A1A2E));
it.print(160, 20, id(title_font), Color(0xFFFFFF), TextAlign::TOP_CENTER, "Room Remote");
// Button 1: All Toggle (top-left)
if (id(all_state)) {
it.filled_rectangle(20, 50, 130, 80, Color(0x4CAF50));
} else {
it.filled_rectangle(20, 50, 130, 80, Color(0x424242));
}
it.print(85, 90, id(button_font), Color(0x000000), TextAlign::CENTER, "Ceiling Fan");
// Button 2: Light 1 (top-right)
if (id(light1_state)) {
it.filled_rectangle(170, 50, 130, 80, Color(0x4CAF50));
} else {
it.filled_rectangle(170, 50, 130, 80, Color(0x424242));
}
it.print(235, 90, id(button_font), Color(0x000000), TextAlign::CENTER, "Polly's Light");
// Button 3: Light 2 (bottom-left)
if (id(light2_state)) {
it.filled_rectangle(20, 150, 130, 80, Color(0x4CAF50));
} else {
it.filled_rectangle(20, 150, 130, 80, Color(0x424242));
}
it.print(85, 190, id(button_font), Color(0x000000), TextAlign::CENTER, "Joshua's Light");
// Button 4: Fan (bottom-right)
if (id(fan_state)) {
it.filled_rectangle(170, 150, 130, 80, Color(0x4CAF50));
} else {
it.filled_rectangle(170, 150, 130, 80, Color(0x424242));
}
it.print(235, 190, id(button_font), Color(0x000000), TextAlign::CENTER, "Toggle All");
# XPT2046 Touchscreen
touchscreen:
- platform: xpt2046
spi_id: touch_spi
cs_pin: GPIO33
interrupt_pin: GPIO36
calibration:
x_min: 280
x_max: 3860
y_min: 340
y_max: 3860
on_touch:
- lambda: |-
uint32_t now = millis();
id(last_interaction_ms) = now;
// Wake backlight on first touch only, don't also trigger a button
if (!id(backlight_is_on)) {
auto call = id(backlight).turn_on();
call.perform();
id(backlight_is_on) = true;
id(last_button_press_ms) = now;
return;
}
// Debounce: ignore repeated touch events within 600ms
if ((uint32_t)(now - id(last_button_press_ms)) < 600) return;
id(last_button_press_ms) = now;
ESP_LOGD("touch", "Touch at x=%d, y=%d", touch.x, touch.y);
// Button 1: Toggle All (top-left)
if (touch.x >= 20 && touch.x <= 150 && touch.y >= 50 && touch.y <= 130) {
ESP_LOGD("touch", "Toggle All pressed");
id(light1_state) = !id(light1_state);
id(light2_state) = !id(light2_state);
id(fan_state) = !id(fan_state);
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
id(svc_toggle_all).execute();
// Button 2: Polly's Light (top-right)
} else if (touch.x >= 170 && touch.x <= 300 && touch.y >= 50 && touch.y <= 130) {
ESP_LOGD("touch", "Polly's Light pressed");
id(light1_state) = !id(light1_state);
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
id(svc_toggle_polly).execute();
// Button 3: Joshua's Light (bottom-left)
} else if (touch.x >= 20 && touch.x <= 150 && touch.y >= 150 && touch.y <= 230) {
ESP_LOGD("touch", "Joshua's Light pressed");
id(light2_state) = !id(light2_state);
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
id(svc_toggle_joshua).execute();
// Button 4: Ceiling Fan (bottom-right)
} else if (touch.x >= 170 && touch.x <= 300 && touch.y >= 150 && touch.y <= 230) {
ESP_LOGD("touch", "Ceiling Fan pressed");
id(fan_state) = !id(fan_state);
id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);
id(svc_toggle_fan).execute();
}
id(my_display).update();
# Backlight control
output:
- platform: ledc
pin: GPIO21
id: backlight_output
light:
- platform: monochromatic
output: backlight_output
name: "Display Backlight"
id: backlight
restore_mode: ALWAYS_ON
# Fonts
font:
- file: "gfonts://Nunito"
id: title_font
size: 24
- file: "gfonts://Nunito"
id: button_font
size: 20
# Scripts to call HA services (lambdas can't call homeassistant.service directly)
script:
- id: svc_toggle_all
then:
- homeassistant.service:
service: switch.toggle
data:
entity_id: ${light_1_entity}
- homeassistant.service:
service: switch.toggle
data:
entity_id: ${light_2_entity}
- homeassistant.service:
service: switch.toggle
data:
entity_id: ${fan_entity}
- id: svc_toggle_polly
then:
- homeassistant.service:
service: switch.toggle
data:
entity_id: ${light_1_entity}
- id: svc_toggle_joshua
then:
- homeassistant.service:
service: switch.toggle
data:
entity_id: ${light_2_entity}
- id: svc_toggle_fan
then:
- homeassistant.service:
service: switch.toggle
data:
entity_id: ${fan_entity}
# Global state tracking
globals:
- id: all_state
type: bool
initial_value: "false"
- id: light1_state
type: bool
initial_value: "false"
- id: light2_state
type: bool
initial_value: "false"
- id: fan_state
type: bool
initial_value: "false"
- id: last_interaction_ms
type: uint32_t
initial_value: "0"
- id: last_button_press_ms
type: uint32_t
initial_value: "0"
- id: backlight_is_on
type: bool
initial_value: "true"
# Backlight timeout
interval:
- interval: 100ms
then:
- if:
condition:
lambda: 'return id(backlight_is_on) && ((uint32_t)(millis() - id(last_interaction_ms)) > 5000);'
then:
- light.turn_off: backlight
- lambda: 'id(backlight_is_on) = false;'
# Watchdog to auto-reboot if things go wrong
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
# Import states from Home Assistant
text_sensor:
- platform: homeassistant
entity_id: ${light_1_entity}
id: ha_light1_state
on_value:
then:
- lambda: 'id(light1_state) = (x == "on");'
- lambda: 'id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);'
- component.update: my_display
- platform: homeassistant
entity_id: ${light_2_entity}
id: ha_light2_state
on_value:
then:
- lambda: 'id(light2_state) = (x == "on");'
- lambda: 'id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);'
- component.update: my_display
- platform: homeassistant
entity_id: ${fan_entity}
id: ha_fan_state
on_value:
then:
- lambda: 'id(fan_state) = (x == "on");'
- lambda: 'id(all_state) = id(light1_state) && id(light2_state) && id(fan_state);'
- component.update: my_display
# ESPHome version
- platform: version
name: "${friendly_name} ESPHome Version"
# WiFi info
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
+33
View File
@@ -0,0 +1,33 @@
packages:
jarvis-satellite: !include jarvis-satellite/jarvis-satellite-base.yaml
### Change the following for each satellite device, e.g. "jarvis-satellite-office" and "jarvis-satellite-bedroom" and to match each ESP board pinout
substitutions:
flash_size: "8MB"
psram_mode: "quad"
psram_speed: "80MHz"
i2s_in_lrclk_pin: GPIO6
i2s_in_bclk_pin: GPIO7
mic_din_pin: GPIO4
i2s_out_lrclk_pin: GPIO45
i2s_out_bclk_pin: GPIO46
speaker_dout_pin: GPIO8
led_pin: GPIO16
led_num_leds: "8"
btn_vol_up_pin: GPIO1
btn_vol_down_pin: GPIO2
btn_mute_pin: GPIO3
btn_wake_pin: GPIO10
cpu_frequency: "240MHz"
esphome:
name: office-jarvis-satellite
friendly_name: "Office Jarvis"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
ap:
ssid: "Jarvis-Satellite-AP"
password: !secret wifi_password
+133
View File
@@ -0,0 +1,133 @@
substitutions:
device_name: polly-a1-mini-camera
friendly_name: "Polly's A1 Camera"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: esp-idf
# minimum_chip_revision: 3.1
# Enable logging
logger:
level: INFO
# Enable Home Assistant API
api:
encryption:
key: "iLF5QCgTVCwgaA5vjTGez03Dfjvns8JvkcwmUN4NXR8="
ota:
- platform: esphome
password: "58f86a5e60abe60886cec2f65a1d3da5"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.194
gateway: 192.168.69.1
subnet: 255.255.255.0
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${friendly_name} Fallback"
password: !secret wifi_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if you use static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
i2c:
- id: camera_i2c
sda: GPIO26
scl: GPIO27
psram:
mode: quad
speed: 80MHz
esp32_camera:
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_id: camera_i2c
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: 1024X768
jpeg_quality: 20
vertical_flip: False
horizontal_mirror: True
max_framerate: 20 fps
idle_framerate: 0.05 fps
# Image settings
name: ${device_name}
esp32_camera_web_server:
- port: 8080
mode: STREAM
- port: 8081
mode: SNAPSHOT
switch:
- platform: gpio
name: "${device_name}-flash"
pin: 4
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
+10
View File
@@ -0,0 +1,10 @@
packages:
respeaker-satellite: !include respeaker-satellite/respeaker-satellite-base.yaml
esphome:
name: respeaker-satellite-fr
friendly_name: "Respeaker Satellite Family Room"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
+10
View File
@@ -0,0 +1,10 @@
packages:
respeaker-satellite: !include respeaker-satellite/respeaker-satellite-base.yaml
esphome:
name: respeaker-satellite-lr
friendly_name: "Respeaker Satellite Living Room"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
+10
View File
@@ -0,0 +1,10 @@
packages:
respeaker-satellite: !include respeaker-satellite/respeaker-satellite-base.yaml
esphome:
name: respeaker-satellite-mbr
friendly_name: "Respeaker Satellite Master Bedroom"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#include "driver/spi_master.h"
#include "driver/gpio.h"