diff --git a/.gitignore b/.gitignore index c76879f..c56efa7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,11 @@ # Whitelist specific folders (uncomment as needed) !automations/ !scripts/ -# !packages/ -#!themes/ !blueprints/ +!esphome/ +!custom_components/ +# !packages/ +# !themes/ # !node-red/ # ============================================= diff --git a/esphome/.gitignore b/esphome/.gitignore new file mode 100644 index 0000000..d8b4157 --- /dev/null +++ b/esphome/.gitignore @@ -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 diff --git a/esphome/airqualitysensor-1.yaml b/esphome/airqualitysensor-1.yaml new file mode 100644 index 0000000..fdd42a2 --- /dev/null +++ b/esphome/airqualitysensor-1.yaml @@ -0,0 +1,781 @@ +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 + +esphome: + name: airqualitysensor-1 + friendly_name: AirQualitySensor-1 + + 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: + +# Enable Home Assistant API +api: + encryption: + key: "gnQzjc98wQKUP3qX6+FeU9JchMPtjJwlKQejOB/mQDU=" + +ota: + - platform: esphome + password: "d4d36e1c98d4b7e2069295540a20a1aa" + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # manual_ip: + # static_ip: 192.168.1.61 + # gateway: 192.168.1.1 + # subnet: 255.255.255.0 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Airqualitysensor-1" + password: "MpBryi70smN4" + +captive_portal: + +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' + + +bluetooth_proxy: + active: true + connection_slots: 3 + +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 + 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: 10s + + ## 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: 10s + 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: 10s + + - platform: wifi_signal + name: AQ WiFi Signal + update_interval: 60s + + - platform: veml7700 + address: 0x10 + update_interval: 10s + # short variant of sensor definition: + ambient_light: + name: "Ambient Light" + id: ambient_light + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 1 + +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 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: 30s + + - 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: 30s + + - 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: 10s + 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: 10s \ No newline at end of file diff --git a/esphome/airqualitysensor-2.yaml b/esphome/airqualitysensor-2.yaml new file mode 100644 index 0000000..bed59e5 --- /dev/null +++ b/esphome/airqualitysensor-2.yaml @@ -0,0 +1,776 @@ +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 + +esphome: + name: airqualitysensor-2 + friendly_name: AirQualitySensor-2 + + 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: + +# Enable Home Assistant API +api: + encryption: + key: "JPYycQ6Uxuubxrfu66M/xAyUPGEl2DiJBCbpCCs9w/4=" + +ota: + - platform: esphome + password: "42087a27f07378b5499426cf544a6fa6" + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Airqualitysensor-2" + password: "uhEm8rcU8sMF" + +captive_portal: + +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' + + +bluetooth_proxy: + active: true + connection_slots: 3 + +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 + 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: 10s + + ## 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: 10s + 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: 10s + + - platform: wifi_signal + name: AQ WiFi Signal + update_interval: 60s + + - platform: veml7700 + address: 0x10 + update_interval: 10s + # short variant of sensor definition: + ambient_light: + name: "Ambient Light" + id: ambient_light + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 1 + +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 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: 30s + + - 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: 30s + + - 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: 10s + 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: 10s \ No newline at end of file diff --git a/esphome/airqualitysensor-3.yaml b/esphome/airqualitysensor-3.yaml new file mode 100644 index 0000000..86135b9 --- /dev/null +++ b/esphome/airqualitysensor-3.yaml @@ -0,0 +1,776 @@ +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 + +esphome: + name: airqualitysensor-3 + friendly_name: AirQualitySensor-3 + + 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: + +# Enable Home Assistant API +api: + encryption: + key: "p7SGIY1i+hwzZCQMliScoY2B98Qva1nPGUnzTr+8knE=" + +ota: + - platform: esphome + password: "81bd35b0916ed26675b611274e652689" + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Airqualitysensor-3" + password: "a8CsGahAGljj" + +captive_portal: + +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' + + +bluetooth_proxy: + active: true + connection_slots: 3 + +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 + 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: 10s + + ## 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: 10s + 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: 10s + + - platform: wifi_signal + name: AQ WiFi Signal + update_interval: 60s + + - platform: veml7700 + address: 0x10 + update_interval: 10s + # short variant of sensor definition: + ambient_light: + name: "Ambient Light" + id: ambient_light + filters: + - sliding_window_moving_average: + window_size: 5 + send_every: 1 + +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 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: 30s + + - 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: 30s + + - 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: 10s + 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: 10s \ No newline at end of file diff --git a/esphome/declan-a1-camera.yaml b/esphome/declan-a1-camera.yaml new file mode 100644 index 0000000..67b0ee6 --- /dev/null +++ b/esphome/declan-a1-camera.yaml @@ -0,0 +1,83 @@ +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 + #cpu_frequency: 240MHZ + +# 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 + ap: + ssid: "${friendly_name} Fallback" + password: !secret fallback_password + +web_server: + port: 80 + +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: 640x480 + jpeg_quality: 20 + vertical_flip: False + horizontal_mirror: False + max_framerate: 22 fps + idle_framerate: 0.1 fps + + # Image settings + name: ${device_name} + #brightness: 1 + #contrast: 0 + # agc_gain_ceiling: 2X + # agc_mode: MANUAL + # agc_value: 5 + # ... + +esp32_camera_web_server: + - port: 8080 + mode: STREAM + - port: 8081 + mode: SNAPSHOT + +switch: + - platform: gpio + name: "${device_name}-flash" + pin: 4 \ No newline at end of file diff --git a/esphome/esp32-h2c-camera.yaml b/esphome/esp32-h2c-camera.yaml new file mode 100644 index 0000000..54a5aa6 --- /dev/null +++ b/esphome/esp32-h2c-camera.yaml @@ -0,0 +1,95 @@ +substitutions: + device_name: esp32-h2c-camera + friendly_name: "3D Printer 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 + ap: + ssid: "${friendly_name} Fallback" + password: !secret fallback_password + +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://:8080 +# Snapshot: http://: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" + +button: + - platform: restart + name: "${friendly_name} Restart" diff --git a/esphome/esp32-saturn4-cam.yaml b/esphome/esp32-saturn4-cam.yaml new file mode 100644 index 0000000..2340144 --- /dev/null +++ b/esphome/esp32-saturn4-cam.yaml @@ -0,0 +1,91 @@ +esphome: + name: esp32-saturn4-cam + friendly_name: esp32-saturn4-cam + +esp32: + board: esp32dev + framework: + type: esp-idf + +substitutions: + devicename: "esp32-saturn4-cam" + hostname: "esp32-saturn4-cam" + #friendly_name: + device_description: ESP32-CAM module in Saturn 4 Ultra Resin printer. + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: "3Zxo91fAfA7wpZ4DAYFxGarCRIGQr+0rUJH2taJo7ds=" + +ota: + - platform: esphome + password: "d2e50b2ad36ed7de4ecbfd90725765e3" + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Esp32-Saturn4-Cam" + password: "klRzTYuqPJXm" + +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: $devicename + # ... + +esp32_camera_web_server: + - port: 80 + mode: STREAM + - port: 8080 + mode: SNAPSHOT \ No newline at end of file diff --git a/esphome/esphome-web-0fdcf4.yaml b/esphome/esphome-web-0fdcf4.yaml new file mode 100644 index 0000000..297312c --- /dev/null +++ b/esphome/esphome-web-0fdcf4.yaml @@ -0,0 +1,107 @@ +esphome: + name: salt-sensor + friendly_name: Salt-Sensor + +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_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Salt-Sensor Fallback Hotspot" + password: "ZoBRDcj54rwW" + +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 + +# 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); \ No newline at end of file diff --git a/esphome/ha-remote-1.base.yml b/esphome/ha-remote-1.base.yml new file mode 100644 index 0000000..64f507a --- /dev/null +++ b/esphome/ha-remote-1.base.yml @@ -0,0 +1,99 @@ +psram: + mode: octal + speed: 80MHz + +i2c: + sda: 8 + scl: 9 + +# CH422G I/O expander (Waveshare uses it for LCD reset/backlight/touch reset) +ch422g: + - id: ch422g_hub + +# --- 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 + update_interval: 120ms + 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 + +# --- 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. + diff --git a/esphome/ha-remote-1.bindings.yml b/esphome/ha-remote-1.bindings.yml new file mode 100644 index 0000000..c65912b --- /dev/null +++ b/esphome/ha-remote-1.bindings.yml @@ -0,0 +1,354 @@ +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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_family_room_tv_stand); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_small_family_room_lamp); + lv_obj_set_style_text_color(btn, text_color, static_cast(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"; + - lvgl.widget.update: + id: btn_living_room_standing_lamp + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn1 = id(btn_family_room_standing_lamp); + auto *btn2 = id(btn_living_room_standing_lamp); + lv_obj_set_style_text_color(btn1, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + lv_obj_set_style_text_color(btn2, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_living_room_main_light); + lv_obj_set_style_text_color(btn, text_color, static_cast(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 text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn1 = id(btn_living_room_patio_light); + auto *btn2 = id(btn_outside_patio_light); + lv_obj_set_style_text_color(btn1, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + lv_obj_set_style_text_color(btn2, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_kitchen_sink_light); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_lamp); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_led_strip); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_garage_cam_light); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + - platform: homeassistant + id: ts_family_room_standing_fan_outlet + entity_id: switch.standing_fan_socket_1 + internal: true + on_value: + then: + - lvgl.widget.update: + id: btn_family_room_standing_fan_outlet + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_family_room_standing_fan_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_echo_plug); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_end_table_lamp_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_wax_warmer_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_outside_lamppost_outlet_1); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_outside_lamppost_outlet_2); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_outside_porch_decor_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_living_room_garland_switch); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_living_room_calendar_switch); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + - platform: homeassistant + id: ts_kitchen_dnd_switch + entity_id: switch.kitchen_do_not_disturb_switch + internal: true + on_value: + then: + - lvgl.widget.update: + id: btn_kitchen_dnd_switch + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_kitchen_dnd_switch); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + - platform: homeassistant + id: ts_kitchen_camera_motion + entity_id: switch.kitchen_camera_motion_detection + internal: true + on_value: + then: + - lvgl.widget.update: + id: btn_kitchen_camera_motion + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_kitchen_camera_motion); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_upstairs_airquality_oled); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_upstairs_camera_motion); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_garage_fume_exhaust_fan); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_garage_resin_printer_heater); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + diff --git a/esphome/ha-remote-1.ui.yml b/esphome/ha-remote-1.ui.yml new file mode 100644 index 0000000..bdc9420 --- /dev/null +++ b/esphome/ha-remote-1.ui.yml @@ -0,0 +1,2094 @@ +lvgl: + displays: + - main_display + touchscreens: + - touch_panel + disp_bg_color: 0x0A1020 + style_definitions: + - id: glass_orb_cool + bg_color: 0x7EC6FF + bg_opa: 0% + radius: 120 + border_width: 0 + shadow_color: 0x7EC6FF + shadow_width: 42 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 0 + - id: glass_orb_warm + bg_color: 0xFFB8D0 + bg_opa: 0% + radius: 120 + border_width: 0 + shadow_color: 0xFFB8D0 + shadow_width: 42 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 0 + - id: glass_header + bg_color: 0x1A2740 + bg_opa: 94% + border_color: 0xA7C8FF + border_width: 1 + border_opa: 35% + radius: 22 + pad_all: 0 + shadow_color: 0x061022 + shadow_width: 22 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_chip + bg_color: 0xCFE1FF + bg_opa: 92% + border_color: 0xFFFFFF + border_width: 1 + border_opa: 45% + radius: 18 + pad_all: 0 + - id: glass_tile_title + text_opa: 100% + - id: glass_tile_meta + text_opa: 96% + - id: glass_tile_icon + text_opa: 100% + - id: glass_toggle_blue + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x091529 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_pink + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A0E2B + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_red + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x2A0A12 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_amber + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x2A1A08 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_green + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x0B2018 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_violet + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A1230 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_tile_blue + bg_color: 0x7BB8FF + bg_opa: 92% + border_color: 0xE6F1FF + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x091529 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xF4F8FF + - id: glass_tile_pink + bg_color: 0xD99CFF + bg_opa: 92% + border_color: 0xF7E7FF + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A0E2B + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xFFF8FF + - id: glass_tile_amber + bg_color: 0xFFD29A + bg_opa: 92% + border_color: 0xFFF0D9 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x2A1A08 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xFFF9EE + - id: glass_tile_green + bg_color: 0x9BE2C6 + bg_opa: 92% + border_color: 0xE4FFF3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x0B2018 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xF3FFF9 + - id: glass_tile_red + bg_color: 0xFF9FAF + bg_opa: 92% + border_color: 0xFFE8EC + border_width: 1 + border_opa: 38% + radius: 24 + shadow_color: 0x2A0A12 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xFFF5F7 + - id: glass_tile_violet + bg_color: 0xB8A8FF + bg_opa: 92% + border_color: 0xF0EAFF + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A1230 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xF7F3FF + - id: room_nav_btn + bg_color: 0x22324A + bg_opa: 96% + border_color: 0x8FB3E8 + border_width: 1 + border_opa: 35% + radius: 12 + text_color: 0xF2F7FF + - id: room_stub_card + bg_color: 0x1A2740 + bg_opa: 96% + border_color: 0xA7C8FF + border_width: 1 + border_opa: 35% + radius: 20 + pad_all: 0 + theme: + label: + text_color: 0xEAF2FF + button: + text_color: 0xF6FAFF + + pages: + - id: home + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + id: status_label + x: 22 + y: 12 + text: "Family Room" + - label: + x: 22 + y: 42 + text: "Liquid Glass Control" + - label: + x: 22 + y: 62 + text: "Quick scenes and lighting controls" + - obj: + x: 560 + y: 12 + width: 194 + height: 58 + styles: glass_chip + widgets: + - label: + id: battery_label + align: center + text: " n/a" + - label: + x: 22 + y: 108 + text: "Tap any card. Screen sleeps after 60s idle." + + - button: + id: btn_family_room_tv_stand + x: 24 + y: 138 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.family_room_tv_stand + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "TV Stand" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + + - button: + id: btn_small_family_room_lamp + x: 278 + y: 138 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.small_family_room_lamp + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Small Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + + - button: + id: btn_family_room_standing_lamp + x: 532 + y: 138 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_red + checked: + bg_color: 0xD83452 + bg_opa: 100% + border_color: 0xFFE2E8 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.living_room_lamp_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Standing Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + + - button: + x: 24 + y: 286 + width: 244 + height: 136 + styles: glass_tile_amber + on_click: + - homeassistant.service: + service: scene.turn_on + data: + entity_id: scene.movie_time + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SCENE" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Movie Time" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Dim and set mood" + + - button: + x: 278 + y: 286 + width: 244 + height: 136 + styles: glass_tile_green + on_click: + - homeassistant.service: + service: scene.turn_on + data: + entity_id: scene.good_night + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SCENE" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Good Night" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Evening shutdown" + + - button: + id: btn_family_room_standing_fan_outlet + x: 532 + y: 286 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_violet + checked: + bg_color: 0x6C55D9 + bg_opa: 100% + border_color: 0xEEE8FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.standing_fan_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Standing Fan" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: living_room + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Living Room" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your lights, scenes, and media controls here" + - button: + id: btn_living_room_main_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.living_room_light_2 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Main Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_standing_lamp + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.living_room_lamp_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Standing Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_patio_light + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.patio_light_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Patio Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_garland_switch + x: 24 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_green + checked: + bg_color: 0x2EAF7E + bg_opa: 100% + border_color: 0xE5FFF5 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.big_family_room_lamp + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Front Garland" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_calendar_switch + x: 278 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_violet + checked: + bg_color: 0x6C55D9 + bg_opa: 100% + border_color: 0xEEE8FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.digital_calendar + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Calendar" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: kitchen_page + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Kitchen" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your kitchen controls here" + - button: + id: btn_kitchen_sink_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.kitchen_sink_light + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Sink Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_kitchen_dnd_switch + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.kitchen_do_not_disturb_switch + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Kitchen DND" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_kitchen_camera_motion + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.kitchen_camera_motion_detection + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Cam Motion" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: upstairs + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Upstairs" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your upstairs controls here" + - button: + id: btn_upstairs_airquality_oled + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.airqualitysensor_3_oled_power + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "AQ OLED" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_upstairs_camera_motion + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.upstairs_camera_motion_detection + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Cam Motion" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: office + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Office (Basement)" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your office controls here" + - button: + id: btn_office_lamp + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.office_lamp_2 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Office Lamp 2" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_led_strip + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.led_strip_controller_led_strip_controller + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "LED Strip" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_echo_plug + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.office_echo_plug + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Echo Plug" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_end_table_lamp_outlet + x: 24 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_green + checked: + bg_color: 0x2EAF7E + bg_opa: 100% + border_color: 0xE5FFF5 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.office_end_table_lamp_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "End Table Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_wax_warmer_outlet + x: 278 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_violet + checked: + bg_color: 0x6C55D9 + bg_opa: 100% + border_color: 0xEEE8FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.office_wax_warmer_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Wax Warmer" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: garage + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Garage" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your garage controls here" + - button: + id: btn_garage_cam_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.esp32_saturn4_cam_esp32_saturn4_cam_light + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Saturn Cam Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_garage_fume_exhaust_fan + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.fume_exhaust_fan + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Fume Fan" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_garage_resin_printer_heater + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.resin_printer_heater + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Resin Heater" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: outside + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Outside" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your outdoor controls here" + - button: + id: btn_outside_patio_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.patio_light_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Patio Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_outside_lamppost_outlet_1 + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.lamppost_outlets_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Lamppost 1" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_outside_lamppost_outlet_2 + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.lamppost_outlets_socket_2 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Lamppost 2" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_outside_porch_decor_outlet + x: 24 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_green + checked: + bg_color: 0x2EAF7E + bg_opa: 100% + border_color: 0xE5FFF5 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.washing_machine_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Porch Decor 1" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" diff --git a/esphome/ha-remote-1.yaml b/esphome/ha-remote-1.yaml new file mode 100644 index 0000000..f9ad8ff --- /dev/null +++ b/esphome/ha-remote-1.yaml @@ -0,0 +1,2593 @@ +substitutions: + device_name: ha-family-room-remote + friendly_name: "Family Room HA Remote" + +esphome: + name: ${device_name} + friendly_name: ${friendly_name} + on_boot: + priority: -10 + then: + - light.turn_on: ha_remote_backlight + - lvgl.page.show: home + - lambda: |- + id(last_activity_ms) = millis(); + +esp32: + board: esp32-s3-devkitc-1 + framework: + type: esp-idf + +logger: + level: WARN + +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 + power_save_mode: NONE + fast_connect: true + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${friendly_name} Fallback" + password: !secret fallback_password + +captive_portal: + + +psram: + mode: octal + speed: 80MHz + +i2c: + sda: 8 + scl: 9 + +# CH422G I/O expander (Waveshare uses it for LCD reset/backlight/touch reset) +ch422g: + - id: ch422g_hub + +# --- 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 + update_interval: 120ms + 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 + +# --- 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. + +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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_family_room_tv_stand); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_small_family_room_lamp); + lv_obj_set_style_text_color(btn, text_color, static_cast(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"; + - lvgl.widget.update: + id: btn_living_room_standing_lamp + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn1 = id(btn_family_room_standing_lamp); + auto *btn2 = id(btn_living_room_standing_lamp); + lv_obj_set_style_text_color(btn1, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + lv_obj_set_style_text_color(btn2, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_living_room_main_light); + lv_obj_set_style_text_color(btn, text_color, static_cast(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 text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn1 = id(btn_living_room_patio_light); + auto *btn2 = id(btn_outside_patio_light); + lv_obj_set_style_text_color(btn1, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + lv_obj_set_style_text_color(btn2, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_kitchen_sink_light); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_lamp); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_led_strip); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_garage_cam_light); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + - platform: homeassistant + id: ts_family_room_standing_fan_outlet + entity_id: switch.standing_fan_socket_1 + internal: true + on_value: + then: + - lvgl.widget.update: + id: btn_family_room_standing_fan_outlet + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_family_room_standing_fan_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_echo_plug); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_end_table_lamp_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_office_wax_warmer_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_outside_lamppost_outlet_1); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_outside_lamppost_outlet_2); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_outside_porch_decor_outlet); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_living_room_garland_switch); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_living_room_calendar_switch); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + - platform: homeassistant + id: ts_kitchen_dnd_switch + entity_id: switch.kitchen_do_not_disturb_switch + internal: true + on_value: + then: + - lvgl.widget.update: + id: btn_kitchen_dnd_switch + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_kitchen_dnd_switch); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + - platform: homeassistant + id: ts_kitchen_camera_motion + entity_id: switch.kitchen_camera_motion_detection + internal: true + on_value: + then: + - lvgl.widget.update: + id: btn_kitchen_camera_motion + state: + checked: !lambda return x == "on"; + - lambda: |- + const lv_color_t text_color = (x == "on") ? lv_color_hex(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_kitchen_camera_motion); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_upstairs_airquality_oled); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_upstairs_camera_motion); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_garage_fume_exhaust_fan); + lv_obj_set_style_text_color(btn, text_color, static_cast(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(0x000000) : lv_color_hex(0xEAF2FF); + auto *btn = id(btn_garage_resin_printer_heater); + lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); + +lvgl: + displays: + - main_display + touchscreens: + - touch_panel + disp_bg_color: 0x0A1020 + style_definitions: + - id: glass_orb_cool + bg_color: 0x7EC6FF + bg_opa: 0% + radius: 120 + border_width: 0 + shadow_color: 0x7EC6FF + shadow_width: 42 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 0 + - id: glass_orb_warm + bg_color: 0xFFB8D0 + bg_opa: 0% + radius: 120 + border_width: 0 + shadow_color: 0xFFB8D0 + shadow_width: 42 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 0 + - id: glass_header + bg_color: 0x1A2740 + bg_opa: 94% + border_color: 0xA7C8FF + border_width: 1 + border_opa: 35% + radius: 22 + pad_all: 0 + shadow_color: 0x061022 + shadow_width: 22 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_chip + bg_color: 0xCFE1FF + bg_opa: 92% + border_color: 0xFFFFFF + border_width: 1 + border_opa: 45% + radius: 18 + pad_all: 0 + - id: glass_tile_title + text_opa: 100% + - id: glass_tile_meta + text_opa: 96% + - id: glass_tile_icon + text_opa: 100% + - id: glass_toggle_blue + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x091529 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_pink + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A0E2B + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_red + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x2A0A12 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_amber + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x2A1A08 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_green + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x0B2018 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_toggle_violet + bg_color: 0x2C364A + bg_opa: 92% + border_color: 0x90A4C3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A1230 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + - id: glass_tile_blue + bg_color: 0x7BB8FF + bg_opa: 92% + border_color: 0xE6F1FF + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x091529 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xF4F8FF + - id: glass_tile_pink + bg_color: 0xD99CFF + bg_opa: 92% + border_color: 0xF7E7FF + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A0E2B + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xFFF8FF + - id: glass_tile_amber + bg_color: 0xFFD29A + bg_opa: 92% + border_color: 0xFFF0D9 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x2A1A08 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xFFF9EE + - id: glass_tile_green + bg_color: 0x9BE2C6 + bg_opa: 92% + border_color: 0xE4FFF3 + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x0B2018 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xF3FFF9 + - id: glass_tile_red + bg_color: 0xFF9FAF + bg_opa: 92% + border_color: 0xFFE8EC + border_width: 1 + border_opa: 38% + radius: 24 + shadow_color: 0x2A0A12 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xFFF5F7 + - id: glass_tile_violet + bg_color: 0xB8A8FF + bg_opa: 92% + border_color: 0xF0EAFF + border_width: 1 + border_opa: 35% + radius: 24 + shadow_color: 0x1A1230 + shadow_width: 24 + shadow_opa: 0% + shadow_ofs_x: 0 + shadow_ofs_y: 8 + text_color: 0xF7F3FF + - id: room_nav_btn + bg_color: 0x22324A + bg_opa: 96% + border_color: 0x8FB3E8 + border_width: 1 + border_opa: 35% + radius: 12 + text_color: 0xF2F7FF + - id: room_stub_card + bg_color: 0x1A2740 + bg_opa: 96% + border_color: 0xA7C8FF + border_width: 1 + border_opa: 35% + radius: 20 + pad_all: 0 + theme: + label: + text_color: 0xEAF2FF + button: + text_color: 0xF6FAFF + + pages: + - id: home + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + id: status_label + x: 22 + y: 12 + text: "Family Room" + - label: + x: 22 + y: 42 + text: "Liquid Glass Control" + - label: + x: 22 + y: 62 + text: "Quick scenes and lighting controls" + - obj: + x: 560 + y: 12 + width: 194 + height: 58 + styles: glass_chip + widgets: + - label: + id: battery_label + align: center + text: " n/a" + - label: + x: 22 + y: 108 + text: "Tap any card. Screen sleeps after 60s idle." + + - button: + id: btn_family_room_tv_stand + x: 24 + y: 138 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.family_room_tv_stand + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "TV Stand" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + + - button: + id: btn_small_family_room_lamp + x: 278 + y: 138 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.small_family_room_lamp + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Small Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + + - button: + id: btn_family_room_standing_lamp + x: 532 + y: 138 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_red + checked: + bg_color: 0xD83452 + bg_opa: 100% + border_color: 0xFFE2E8 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.living_room_lamp_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Standing Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + + - button: + x: 24 + y: 286 + width: 244 + height: 136 + styles: glass_tile_amber + on_click: + - homeassistant.service: + service: scene.turn_on + data: + entity_id: scene.movie_time + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SCENE" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Movie Time" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Dim and set mood" + + - button: + x: 278 + y: 286 + width: 244 + height: 136 + styles: glass_tile_green + on_click: + - homeassistant.service: + service: scene.turn_on + data: + entity_id: scene.good_night + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SCENE" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Good Night" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Evening shutdown" + + - button: + id: btn_family_room_standing_fan_outlet + x: 532 + y: 286 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_violet + checked: + bg_color: 0x6C55D9 + bg_opa: 100% + border_color: 0xEEE8FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.standing_fan_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Standing Fan" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: living_room + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Living Room" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your lights, scenes, and media controls here" + - button: + id: btn_living_room_main_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.living_room_light_2 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Main Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_standing_lamp + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.living_room_lamp_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Standing Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_patio_light + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.patio_light_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Patio Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_garland_switch + x: 24 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_green + checked: + bg_color: 0x2EAF7E + bg_opa: 100% + border_color: 0xE5FFF5 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.big_family_room_lamp + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Front Garland" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_living_room_calendar_switch + x: 278 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_violet + checked: + bg_color: 0x6C55D9 + bg_opa: 100% + border_color: 0xEEE8FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.digital_calendar + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Calendar" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: kitchen_page + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Kitchen" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your kitchen controls here" + - button: + id: btn_kitchen_sink_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.kitchen_sink_light + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Sink Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_kitchen_dnd_switch + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.kitchen_do_not_disturb_switch + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Kitchen DND" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_kitchen_camera_motion + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.kitchen_camera_motion_detection + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Cam Motion" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: upstairs + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Upstairs" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your upstairs controls here" + - button: + id: btn_upstairs_airquality_oled + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.airqualitysensor_3_oled_power + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "AQ OLED" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_upstairs_camera_motion + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.upstairs_camera_motion_detection + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Cam Motion" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: office + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Office (Basement)" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your office controls here" + - button: + id: btn_office_lamp + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.office_lamp_2 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Office Lamp 2" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_led_strip + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.led_strip_controller_led_strip_controller + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "LED Strip" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_echo_plug + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.office_echo_plug + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Echo Plug" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_end_table_lamp_outlet + x: 24 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_green + checked: + bg_color: 0x2EAF7E + bg_opa: 100% + border_color: 0xE5FFF5 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.office_end_table_lamp_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "End Table Lamp" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_office_wax_warmer_outlet + x: 278 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_violet + checked: + bg_color: 0x6C55D9 + bg_opa: 100% + border_color: 0xEEE8FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.office_wax_warmer_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Wax Warmer" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: garage + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Garage" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your garage controls here" + - button: + id: btn_garage_cam_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.esp32_saturn4_cam_esp32_saturn4_cam_light + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Saturn Cam Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_garage_fume_exhaust_fan + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.fume_exhaust_fan + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Fume Fan" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_garage_resin_printer_heater + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.resin_printer_heater + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "SWITCH" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Resin Heater" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" + + - id: outside + widgets: + - obj: + x: 14 + y: 14 + width: 772 + height: 84 + styles: glass_header + widgets: + - label: + x: 22 + y: 12 + text: "Outside" + - label: + x: 22 + y: 42 + text: "Room Controls" + - label: + x: 22 + y: 62 + text: "Add your outdoor controls here" + - button: + id: btn_outside_patio_light + x: 24 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_blue + checked: + bg_color: 0x0B7DFA + bg_opa: 100% + border_color: 0xEAF4FF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: light.toggle + data: + entity_id: light.patio_light_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "LIGHT" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Patio Light" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_outside_lamppost_outlet_1 + x: 278 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_pink + checked: + bg_color: 0xB22FD9 + bg_opa: 100% + border_color: 0xFFDEFF + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.lamppost_outlets_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Lamppost 1" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_outside_lamppost_outlet_2 + x: 532 + y: 122 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_amber + checked: + bg_color: 0xC97700 + bg_opa: 100% + border_color: 0xFFF1D6 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.lamppost_outlets_socket_2 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Lamppost 2" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + id: btn_outside_porch_decor_outlet + x: 24 + y: 270 + width: 244 + height: 136 + checkable: true + styles: glass_toggle_green + checked: + bg_color: 0x2EAF7E + bg_opa: 100% + border_color: 0xE5FFF5 + border_opa: 90% + text_color: 0x000000 + on_click: + - homeassistant.service: + service: switch.toggle + data: + entity_id: switch.washing_machine_socket_1 + widgets: + - label: + x: 204 + y: 16 + styles: glass_tile_icon + text: "" + - label: + x: 18 + y: 16 + styles: glass_tile_meta + text: "OUTLET" + - label: + x: 18 + y: 56 + styles: glass_tile_title + text: "Porch Decor 1" + - label: + x: 18 + y: 90 + styles: glass_tile_meta + text: "Tap to toggle" + - button: + x: 20 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: home + widgets: + - label: + align: center + text: "Family" + - button: + x: 130 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: living_room + widgets: + - label: + align: center + text: "Living" + - button: + x: 240 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: kitchen_page + widgets: + - label: + align: center + text: "Kitchen" + - button: + x: 350 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: upstairs + widgets: + - label: + align: center + text: "Upstairs" + - button: + x: 460 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: office + widgets: + - label: + align: center + text: "Office" + - button: + x: 570 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: garage + widgets: + - label: + align: center + text: "Garage" + - button: + x: 680 + y: 430 + width: 100 + height: 40 + styles: room_nav_btn + on_click: + - lvgl.page.show: outside + widgets: + - label: + align: center + text: "Outside" \ No newline at end of file diff --git a/esphome/ha-remote-1.yml b/esphome/ha-remote-1.yml new file mode 100644 index 0000000..868bb13 --- /dev/null +++ b/esphome/ha-remote-1.yml @@ -0,0 +1,51 @@ +substitutions: + device_name: ha-family-room-remote + friendly_name: "Family Room HA Remote" + +esphome: + name: ${device_name} + friendly_name: ${friendly_name} + on_boot: + priority: -10 + then: + - light.turn_on: ha_remote_backlight + - lvgl.page.show: home + - lambda: |- + id(last_activity_ms) = millis(); + +esp32: + board: esp32-s3-devkitc-1 + framework: + type: esp-idf + +logger: + level: WARN + +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 + power_save_mode: NONE + fast_connect: true + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${friendly_name} Fallback" + password: !secret fallback_password + +captive_portal: + + + +packages: + base: !include ha-remote-1.base.yml + bindings: !include ha-remote-1.bindings.yml + ui: !include ha-remote-1.ui.yml diff --git a/esphome/home-energy-monitor.yaml b/esphome/home-energy-monitor.yaml new file mode 100644 index 0000000..db19468 --- /dev/null +++ b/esphome/home-energy-monitor.yaml @@ -0,0 +1,161 @@ +substitutions: + friendly_name: "Home Energy Monitor" + device_name: home-energy-monitor + update_time: 10s + # 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 + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: !secret api_encryption_key + +# Enable OTA updates +ota: + - platform: esphome + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + # Optional: static IP for reliability + # manual_ip: + # static_ip: 192.168.1.50 + # gateway: 192.168.1.1 + # subnet: 255.255.255.0 + + # Fallback hotspot if Wi-Fi fails + ap: + ssid: ${device_name} + password: !secret wifi_password + +captive_portal: + +web_server: + port: 80 + +# SPI bus configuration +spi: + clk_pin: 18 + miso_pin: 19 + mosi_pin: 23 + +sensor: + # Wi-Fi signal strength + - platform: wifi_signal + name: ${device_name} WiFi Signal + update_interval: 60s + + # ────────────────────────────────── + # 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 + +time: + - platform: homeassistant + id: homeassistant_time diff --git a/esphome/jordyn-a1-camera.yaml b/esphome/jordyn-a1-camera.yaml new file mode 100644 index 0000000..c246125 --- /dev/null +++ b/esphome/jordyn-a1-camera.yaml @@ -0,0 +1,78 @@ +esphome: + name: jordyn-a1-camera + friendly_name: jordyn-a1-camera + +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_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Jordyn-A1-Camera" + password: "jN4rzGokp5mt" + +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: 800x600 + jpeg_quality: 15 + vertical_flip: False + horizontal_mirror: False + max_framerate: 20 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 \ No newline at end of file diff --git a/esphome/led-controller-declan-a1.yaml b/esphome/led-controller-declan-a1.yaml new file mode 100644 index 0000000..4df3a6d --- /dev/null +++ b/esphome/led-controller-declan-a1.yaml @@ -0,0 +1,158 @@ +substitutions: + device_name: led-controller-declan-a1 + friendly_name: "Declan'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 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${friendly_name} Fallback" + password: !secret fallback_password + +captive_portal: + +# Web server for standalone control (optional, but useful) +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" + +# Text sensors +text_sensor: + # ESPHome version + - platform: version + name: "${friendly_name} ESPHome Version" + + # WiFi info + - platform: wifi_info + ip_address: + name: "${friendly_name} IP Address" + ssid: + name: "${friendly_name} Connected SSID" + +# Button to restart ESP32 +button: + - platform: restart + name: "${friendly_name} Restart" diff --git a/esphome/led-controller-jordyn-a1.yaml b/esphome/led-controller-jordyn-a1.yaml new file mode 100644 index 0000000..8c6bd6d --- /dev/null +++ b/esphome/led-controller-jordyn-a1.yaml @@ -0,0 +1,158 @@ +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 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${friendly_name} Fallback" + password: !secret fallback_password + +captive_portal: + +# Web server for standalone control (optional, but useful) +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" + +# Text sensors +text_sensor: + # ESPHome version + - platform: version + name: "${friendly_name} ESPHome Version" + + # WiFi info + - platform: wifi_info + ip_address: + name: "${friendly_name} IP Address" + ssid: + name: "${friendly_name} Connected SSID" + +# Button to restart ESP32 +button: + - platform: restart + name: "${friendly_name} Restart" diff --git a/esphome/led-controller-polly-a1mini.yaml b/esphome/led-controller-polly-a1mini.yaml new file mode 100644 index 0000000..9d69088 --- /dev/null +++ b/esphome/led-controller-polly-a1mini.yaml @@ -0,0 +1,155 @@ +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 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${friendly_name} Fallback" + password: !secret fallback_password + +captive_portal: + +# Web server for standalone control (optional, but useful) +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" + +# Text sensors +text_sensor: + # ESPHome version + - platform: version + name: "${friendly_name} ESPHome Version" + + # WiFi info + - platform: wifi_info + ip_address: + name: "${friendly_name} IP Address" + ssid: + name: "${friendly_name} Connected SSID" + +# Button to restart ESP32 +button: + - platform: restart + name: "${friendly_name} Restart" diff --git a/esphome/polly-a1-mini-camera.yaml b/esphome/polly-a1-mini-camera.yaml new file mode 100644 index 0000000..2e7a574 --- /dev/null +++ b/esphome/polly-a1-mini-camera.yaml @@ -0,0 +1,78 @@ +esphome: + name: polly-a1-mini-camera + friendly_name: polly-a1-mini-camera + +esp32: + board: esp32dev + framework: + type: esp-idf +# minimum_chip_revision: 3.1 + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: "iLF5QCgTVCwgaA5vjTGez03Dfjvns8JvkcwmUN4NXR8=" + +ota: + - platform: esphome + password: "58f86a5e60abe60886cec2f65a1d3da5" + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Polly-A1-Mini-Camera" + password: "7so1xpjNMIMr" + +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: 800x600 + jpeg_quality: 20 + #horizontal_mirror: False + max_framerate: 20 fps + idle_framerate: 0.05 fps + + # Image settings + name: esp32-polly-a1-mini-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: "polly-a1-mini-cam-flash" + pin: 4 + \ No newline at end of file