diff --git a/dashboards/at-a-glance.yaml b/dashboards/at-a-glance.yaml deleted file mode 100644 index 298d9e8..0000000 --- a/dashboards/at-a-glance.yaml +++ /dev/null @@ -1,5 +0,0 @@ -mode: yaml -title: At-a-Glance -icon: mdi:eye-outline -show_in_sidebar: false -filename: dashboards/views/at-a-glance-dashboard.yaml diff --git a/dashboards/dashboards.yaml b/dashboards/dashboards.yaml index 3446c55..33db90e 100644 --- a/dashboards/dashboards.yaml +++ b/dashboards/dashboards.yaml @@ -1,7 +1,3 @@ -voice-alarms: !include voice-alarms.yaml -family-hub: !include family-hub.yaml -at-a-glance: !include at-a-glance.yaml -lunch-menus: !include lunch-menus.yaml # Each key is the dashboard's url_path, and HA requires it to contain a hyphen # (see homeassistant/components/lovelace: url_path validation). A hyphen-less # key fails validation for the whole `lovelace:` block, not just its own entry, diff --git a/dashboards/views/at-a-glance-dashboard.yaml b/dashboards/views/at-a-glance-dashboard.yaml deleted file mode 100644 index c96ccc0..0000000 --- a/dashboards/views/at-a-glance-dashboard.yaml +++ /dev/null @@ -1,294 +0,0 @@ -# At-a-Glance — wall panel -# -# Styled with the CubeCraft Creations display system. The tokens live in -# themes/cubecraft.yaml, which mirrors the `:host` block of the production -# card's stylesheet (custom_components/cubecraft_production/www/ -# cubecraft-production-card.js). Anything referenced here as --cc-* comes from -# that theme, so this file never hard-codes a brand hex. -# -# Three motifs carry the style, all lifted from the production card: -# .title → gradient title bar, white 800-weight type, brand radius -# .column header → uppercase 700/.05em muted section labels -# article → 8px radius, 4px brand left rule, shadow-sm on each tile -# -# Colour is deliberately restrained: brand indigo for anything informational, -# and green/amber/red kept in reserve so they still mean something when they -# show up. That is the same discipline the production card uses, where only a -# blocked order goes red. -title: At-a-Glance -theme: cubecraft -views: - - title: At-a-Glance - path: at-a-glance - icon: mdi:eye-outline - theme: cubecraft - type: masonry - cards: - # ── Brand title bar ── - # Mirrors `.shell.kiosk .title`: gradient ground, white type at 1.75rem, - # brand radius and the brand shadow. ha-markdown supplies its own 16px - # inset, so ha-card only tops that up to roughly the card's 20px/28px. - - type: markdown - content: | - # {{ now().strftime("%A, %B %d, %Y") }} - ## {{ now().strftime("%I:%M %p") }} - card_mod: - style: | - ha-card { - background: var(--cc-gradient); - color: #fff; - border: none; - border-radius: var(--cc-radius-brand); - box-shadow: var(--cc-shadow-brand); - padding: 6px 12px; - text-align: center; - } - ha-card h1 { - margin: 0; - font-size: 1rem; - font-weight: 600; - letter-spacing: 0.02em; - opacity: 0.85; - color: #fff; - } - ha-card h2 { - margin: 2px 0 0 0; - font-size: 1.75rem; - font-weight: 800; - letter-spacing: -0.01em; - color: #fff; - } - - # ── Weather Now ── - - type: weather-forecast - entity: weather.home - name: Now - show_forecast: false - show_current: true - round_temperature: true - - # ── Indoor Climate ── - # First use of the section-label treatment; the rest of the file aliases - # it as *cc_section. Selectors are listed broadly on purpose — a heading - # card's internals are not a stable API, and any selector that misses - # simply leaves that bit at the theme default rather than breaking. - - type: heading - heading: Indoor Climate - heading_style: title - icon: mdi:home-thermometer-outline - card_mod: &cc_section - style: | - ha-card { - background: none; - border: none; - box-shadow: none; - padding: 0; - margin: 4px 0 -4px 0; - } - ha-card .heading, - ha-card .content p, - ha-card p { - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 700; - font-size: 0.9375rem; - color: var(--cc-text-muted, var(--secondary-text-color)); - } - ha-card ha-icon, - ha-card ha-svg-icon { - color: var(--cc-accent, var(--primary-color)); - } - - - type: grid - columns: 3 - square: false - cards: - # First use of the `article` treatment; aliased as *cc_tile below. - - type: tile - entity: sensor.living_room_temperature - name: Living Room - icon: mdi:sofa - color: primary - card_mod: &cc_tile - style: | - ha-card { - border-radius: var(--cc-radius-md); - border-left: 4px solid var(--cc-accent); - box-shadow: var(--cc-shadow-sm); - } - - type: tile - entity: sensor.office_temperature - name: Office - icon: mdi:desk - color: primary - card_mod: *cc_tile - - type: tile - entity: sensor.bedroom_temperature - name: Master Bedroom - icon: mdi:bed - color: primary - card_mod: *cc_tile - - type: tile - entity: sensor.upstairs_temperature - name: Upstairs - icon: mdi:stairs - color: primary - card_mod: *cc_tile - - type: tile - entity: sensor.blink_family_room_temperature - name: Family Room - icon: mdi:television - color: primary - card_mod: *cc_tile - - type: tile - entity: sensor.blink_kitchen_temperature - name: Kitchen - icon: mdi:stove - color: primary - card_mod: *cc_tile - - # ── 3D Printer Status ── - - type: heading - heading: 3D Printers - heading_style: title - icon: mdi:printer-3d-nozzle - card_mod: *cc_section - - - type: grid - columns: 2 - square: false - cards: - # Online/offline is a genuine success signal, so these keep green and - # take the success-coloured left rule. - - type: tile - entity: binary_sensor.p1s_01p09c470102673_online - name: PrintsCharming - icon: mdi:printer-3d-nozzle - show_entity_picture: false - color: green - card_mod: &cc_tile_success - style: | - ha-card { - border-radius: var(--cc-radius-md); - border-left: 4px solid var(--success-color); - box-shadow: var(--cc-shadow-sm); - } - - type: tile - entity: binary_sensor.a1_03919d540806387_online - name: Ds-Turd-Flinger - icon: mdi:printer-3d-nozzle - show_entity_picture: false - color: green - card_mod: *cc_tile_success - - type: tile - entity: binary_sensor.a1mini_0309ca580403653_online - name: Polly-Pocket - icon: mdi:printer-3d-nozzle - show_entity_picture: false - color: green - card_mod: *cc_tile_success - - type: tile - entity: binary_sensor.constipation_orion_online - name: Constipation-Orion - icon: mdi:printer-3d-nozzle - show_entity_picture: false - color: green - card_mod: *cc_tile_success - - type: tile - entity: binary_sensor.jordyns_layer_slayer_online - name: Jordyn's Layer Slayer - icon: mdi:printer-3d-nozzle - show_entity_picture: false - color: green - card_mod: *cc_tile_success - - type: tile - entity: binary_sensor.centauri_carbon_sdcp_status - name: Centauri Carbon - icon: mdi:printer-3d-nozzle - show_entity_picture: false - color: green - card_mod: *cc_tile_success - - # ── Litter Boxes ── - - type: heading - heading: Litter Boxes - heading_style: title - icon: mdi:cat - card_mod: *cc_section - - - type: grid - columns: 2 - square: false - cards: - # These are the "something needs doing" tiles — the equivalent of a - # blocked order in the production card, so they take the error rule. - - type: tile - entity: binary_sensor.cat_crapper_3000_wastebin_filled - name: Cat Crapper 3000 - icon: mdi:delete-empty - show_entity_picture: false - color: red - card_mod: &cc_tile_alert - style: | - ha-card { - border-radius: var(--cc-radius-md); - border-left: 4px solid var(--error-color); - box-shadow: var(--cc-shadow-sm); - } - - type: tile - entity: binary_sensor.cat_crapper_3000_sand_lack - name: CC3000 Litter - icon: mdi:grain - show_entity_picture: false - color: amber - card_mod: - style: | - ha-card { - border-radius: var(--cc-radius-md); - border-left: 4px solid var(--warning-color); - box-shadow: var(--cc-shadow-sm); - } - - type: tile - entity: binary_sensor.poop_box_wastebin_filled - name: Poop Box - icon: mdi:delete-empty - show_entity_picture: false - color: red - card_mod: *cc_tile_alert - - type: tile - entity: binary_sensor.triple_threat_wastebin_filled - name: Triple Threat - icon: mdi:delete-empty - show_entity_picture: false - color: red - card_mod: *cc_tile_alert - - # ── Security ── - - type: heading - heading: Security - heading_style: title - icon: mdi:shield-home-outline - badges: - - type: entity - entity: alarm_control_panel.blink_front - name: Front - - type: entity - entity: alarm_control_panel.blink_inside - name: Inside - - type: entity - entity: alarm_control_panel.blink_garage - name: Garage - - type: entity - entity: alarm_control_panel.blink_rear - name: Rear - card_mod: *cc_section - - # ── Recent Activity ── - - type: logbook - entities: - - automation.announce_day_camp_preparation_and_pause_tv - - automation.announce_time_to_leave_for_day_camp_and_turn_off_tv - - automation.bedtime_snack_notification - - automation.chore_tracker_evening_reminder - hours_to_show: 24 - title: Recent Family Activity diff --git a/dashboards/voice-alarms.yaml b/dashboards/voice-alarms.yaml deleted file mode 100644 index 500b99d..0000000 --- a/dashboards/voice-alarms.yaml +++ /dev/null @@ -1,5 +0,0 @@ -mode: yaml -title: Voice Alarms -icon: mdi:alarm-panel -show_in_sidebar: false -filename: dashboards/views/voice-alarms-dashboard.yaml diff --git a/esphome/MAX17048_component.h b/esphome/MAX17048_component.h deleted file mode 100644 index 99f278a..0000000 --- a/esphome/MAX17048_component.h +++ /dev/null @@ -1,47 +0,0 @@ -#include "esphome.h" - -#define MAX17048_ADDRESS 0x36 -#define MAX17048_VCELL 0x02 // voltage -#define MAX17048_SOC 0x04 // percentage -#define MAX17048_MODE 0x06 -#define MAX17048_VERSION 0x08 -#define MAX17048_CONFIG 0x0c -#define MAX17048_COMMAND 0xfe - -class MAX17048Sensor : public PollingComponent, public Sensor { - public: - Sensor *voltage_sensor = new Sensor(); - Sensor *percentage_sensor = new Sensor(); - - MAX17048Sensor() : PollingComponent(10000) {} - - void setup() override { - // Initialize the device here. Usually Wire.begin() will be called in here, - // though that call is unnecessary if you have an 'i2c:' entry in your config - ESP_LOGD("custom", "Starting up MAX17048 sensor"); - - Wire.begin(); - } - - uint16_t read16(uint8_t reg) { - uint16_t temp; - Wire.begin(); - Wire.beginTransmission(MAX17048_ADDRESS); - Wire.write(reg); - Wire.endTransmission(); - Wire.requestFrom(MAX17048_ADDRESS, 2); - temp = (uint16_t)Wire.read() << 8; - temp |= (uint16_t)Wire.read(); - Wire.endTransmission(); - return temp; - } - - void update() override { - float voltage = (float)(read16(MAX17048_VCELL)) * 78.125 / 1000000; - voltage_sensor->publish_state(voltage); - - uint16_t percentage_tmp = read16(MAX17048_SOC); - float percentage = (float)(percentage_tmp) / 256; - percentage_sensor->publish_state(percentage); - } -}; \ No newline at end of file diff --git a/esphome/chore-tracker-esphome.yaml b/esphome/chore-tracker-esphome.yaml index 4b16519..cd5b337 100644 --- a/esphome/chore-tracker-esphome.yaml +++ b/esphome/chore-tracker-esphome.yaml @@ -1,1448 +1,1448 @@ -################################################################################ -# chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml) -# Kids: Jordyn, Declan, Chloe -# -# HOME SCREEN BEHAVIOUR: -# Red outline = chores incomplete -# Solid green = all chores done -# Resets to red automatically at midnight -# -# Hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) -################################################################################ -substitutions: - device_name: chore-tracker - friendly_name: "Chore Tracker" +# ################################################################################ +# # chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml) +# # Kids: Jordyn, Declan, Chloe +# # +# # HOME SCREEN BEHAVIOUR: +# # Red outline = chores incomplete +# # Solid green = all chores done +# # Resets to red automatically at midnight +# # +# # Hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) +# ################################################################################ +# substitutions: +# device_name: chore-tracker +# friendly_name: "Chore Tracker" -esphome: - name: ${device_name} - friendly_name: ${friendly_name} - on_boot: - priority: -10 - then: - - light.turn_on: backlight - - lvgl.page.show: page_home +# esphome: +# name: ${device_name} +# friendly_name: ${friendly_name} +# on_boot: +# priority: -10 +# then: +# - light.turn_on: backlight +# - lvgl.page.show: page_home -esp32: - board: esp32-s3-devkitc-1 - framework: - type: esp-idf +# esp32: +# board: esp32-s3-devkitc-1 +# framework: +# type: esp-idf -psram: - mode: octal - speed: 80MHz +# psram: +# mode: octal +# speed: 80MHz -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password - manual_ip: - static_ip: 192.168.69.115 - gateway: 192.168.69.1 - subnet: 255.255.255.0 - ap: - ssid: "Chore Tracker Hotspot" - password: "choretracker" +# wifi: +# ssid: !secret wifi_iot_ssid +# password: !secret wifi_password +# manual_ip: +# static_ip: 192.168.69.115 +# gateway: 192.168.69.1 +# subnet: 255.255.255.0 +# ap: +# ssid: "Chore Tracker Hotspot" +# password: "choretracker" - # Prevents aggressive reconnection attempts - power_save_mode: none # Use 'light' for battery devices +# # Prevents aggressive reconnection attempts +# power_save_mode: none # Use 'light' for battery devices - # Slower but more reliable connection - fast_connect: true +# # Slower but more reliable connection +# fast_connect: true - # Handle connection failures gracefully - on_connect: - - logger.log: "Wi-Fi connected!" - on_disconnect: - - logger.log: "Wi-Fi disconnected!" +# # Handle connection failures gracefully +# on_connect: +# - logger.log: "Wi-Fi connected!" +# on_disconnect: +# - logger.log: "Wi-Fi disconnected!" -mdns: - disabled: true +# mdns: +# disabled: true -safe_mode: - disabled: true +# safe_mode: +# disabled: true -#captive_portal: +# #captive_portal: -logger: - level: DEBUG +# logger: +# level: DEBUG -api: - encryption: - key: !secret api_encryption_key - reboot_timeout: 0s +# api: +# encryption: +# key: !secret api_encryption_key +# reboot_timeout: 0s -ota: - - platform: esphome - password: !secret ota_password +# ota: +# - platform: esphome +# password: !secret ota_password -web_server: - port: 8080 +# web_server: +# port: 8080 -# ── I2C ─────────────────────────────────────────────────────────────────────── -i2c: - sda: 8 - scl: 9 +# # ── I2C ─────────────────────────────────────────────────────────────────────── +# i2c: +# sda: 8 +# scl: 9 -# ── CH422G IO Expander (controls LCD reset, touch reset, backlight) ─────────── -ch422g: - - id: ch422g_hub +# # ── CH422G IO Expander (controls LCD reset, touch reset, backlight) ─────────── +# ch422g: +# - id: ch422g_hub -# ── 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 +# # ── 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 -# ── Touchscreen ─────────────────────────────────────────────────────────────── -touchscreen: - platform: gt911 - id: touch - update_interval: 120ms - reset_pin: - ch422g: ch422g_hub - number: 1 - mode: - output: true +# # ── Touchscreen ─────────────────────────────────────────────────────────────── +# touchscreen: +# platform: gt911 +# id: touch +# update_interval: 120ms +# reset_pin: +# ch422g: ch422g_hub +# number: 1 +# mode: +# output: true -# ── 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 +# # ── 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 -light: - - platform: binary - name: "Display Backlight" - output: lcd_backlight_out - id: backlight +# light: +# - platform: binary +# name: "Display Backlight" +# output: lcd_backlight_out +# id: backlight -# ── Midnight reset ──────────────────────────────────────────────────────────── -time: - - platform: homeassistant - id: ha_time - on_time: - - hours: 0 - minutes: 0 - seconds: 0 - then: - - lambda: |- - id(reset_jordyn_chores).execute(); - id(reset_declan_chores).execute(); - id(reset_chloe_chores).execute(); - - lvgl.page.show: page_home +# # ── Midnight reset ──────────────────────────────────────────────────────────── +# time: +# - platform: homeassistant +# id: ha_time +# on_time: +# - hours: 0 +# minutes: 0 +# seconds: 0 +# then: +# - lambda: |- +# id(reset_jordyn_chores).execute(); +# id(reset_declan_chores).execute(); +# id(reset_chloe_chores).execute(); +# - lvgl.page.show: page_home -# ── Fonts (place files in /config/esphome/fonts/) ──────────────────────────── -font: - - file: "fonts/Nunito-Black.ttf" - id: font_title - size: 40 - - file: "fonts/Nunito-ExtraBold.ttf" - id: font_name - size: 30 - - file: "fonts/Nunito-Bold.ttf" - id: font_med - size: 22 - - file: "fonts/Nunito-SemiBold.ttf" - id: font_small - size: 17 - - file: "fonts/Nunito-SemiBold.ttf" - id: font_tiny - size: 13 - # MDI icon font — used for chore icons and kid avatars - - file: "fonts/materialdesignicons-webfont.ttf" - id: font_mdi_large - size: 48 - bpp: 4 - glyphs: 󰄛󰆫󰇷󰊩󰋣󰐂󰦩󱑷󱚣󱜜 - - file: "fonts/materialdesignicons-webfont.ttf" - id: font_mdi_small - size: 32 - bpp: 4 - glyphs: 󰄛󰆫󰇷󰊩󰋣󰐂󰦩󱑷󱚣󱜜 +# # ── Fonts (place files in /config/esphome/fonts/) ──────────────────────────── +# font: +# - file: "fonts/Nunito-Black.ttf" +# id: font_title +# size: 40 +# - file: "fonts/Nunito-ExtraBold.ttf" +# id: font_name +# size: 30 +# - file: "fonts/Nunito-Bold.ttf" +# id: font_med +# size: 22 +# - file: "fonts/Nunito-SemiBold.ttf" +# id: font_small +# size: 17 +# - file: "fonts/Nunito-SemiBold.ttf" +# id: font_tiny +# size: 13 +# # MDI icon font — used for chore icons and kid avatars +# - file: "fonts/materialdesignicons-webfont.ttf" +# id: font_mdi_large +# size: 48 +# bpp: 4 +# glyphs: 󰄛󰆫󰇷󰊩󰋣󰐂󰦩󱑷󱚣󱜜 +# - file: "fonts/materialdesignicons-webfont.ttf" +# id: font_mdi_small +# size: 32 +# bpp: 4 +# glyphs: 󰄛󰆫󰇷󰊩󰋣󰐂󰦩󱑷󱚣󱜜 -# ── Switches — backlight raw + one per chore per kid ───────────────────────── -switch: - - platform: gpio - id: lcd_backlight_raw - name: "LCD Backlight Raw" - restore_mode: ALWAYS_ON - pin: - ch422g: ch422g_hub - number: 2 - mode: - output: true +# # ── Switches — backlight raw + one per chore per kid ───────────────────────── +# switch: +# - platform: gpio +# id: lcd_backlight_raw +# name: "LCD Backlight Raw" +# restore_mode: ALWAYS_ON +# pin: +# ch422g: ch422g_hub +# number: 2 +# mode: +# output: true - # ── Jordyn's chores ────────────────────────── - - platform: template - name: "Jordyn - Put on underwear" - id: jordyn_put_on_underwear - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_jordyn_put_on_underwear - on_turn_off: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_jordyn_put_on_underwear - - platform: template - name: "Jordyn - Brush Teeth" - id: jordyn_brush_teeth - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_jordyn_brush_teeth - on_turn_off: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_jordyn_brush_teeth - - platform: template - name: "Jordyn - Fill water bowls" - id: jordyn_fill_water_bowls - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_jordyn_fill_water_bowls - on_turn_off: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_jordyn_fill_water_bowls - - platform: template - name: "Jordyn - Restock Cat Food Cans" - id: jordyn_restock_cat_food_cans - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans - on_turn_off: - then: - - script.execute: update_jordyn_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans +# # ── Jordyn's chores ────────────────────────── +# - platform: template +# name: "Jordyn - Put on underwear" +# id: jordyn_put_on_underwear +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_jordyn_put_on_underwear +# on_turn_off: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_jordyn_put_on_underwear +# - platform: template +# name: "Jordyn - Brush Teeth" +# id: jordyn_brush_teeth +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_jordyn_brush_teeth +# on_turn_off: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_jordyn_brush_teeth +# - platform: template +# name: "Jordyn - Fill water bowls" +# id: jordyn_fill_water_bowls +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_jordyn_fill_water_bowls +# on_turn_off: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_jordyn_fill_water_bowls +# - platform: template +# name: "Jordyn - Restock Cat Food Cans" +# id: jordyn_restock_cat_food_cans +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans +# on_turn_off: +# then: +# - script.execute: update_jordyn_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_jordyn_restock_cat_food_cans - # ── Declan's chores ────────────────────────── - - platform: template - name: "Declan - Take morning pill" - id: declan_take_morning_pill - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_declan_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_declan_take_morning_pill - on_turn_off: - then: - - script.execute: update_declan_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_declan_take_morning_pill - - platform: template - name: "Declan - Scoop Dog Poop" - id: declan_scoop_dog_poop - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_declan_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_declan_scoop_dog_poop - on_turn_off: - then: - - script.execute: update_declan_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_declan_scoop_dog_poop +# # ── Declan's chores ────────────────────────── +# - platform: template +# name: "Declan - Take morning pill" +# id: declan_take_morning_pill +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_declan_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_declan_take_morning_pill +# on_turn_off: +# then: +# - script.execute: update_declan_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_declan_take_morning_pill +# - platform: template +# name: "Declan - Scoop Dog Poop" +# id: declan_scoop_dog_poop +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_declan_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_declan_scoop_dog_poop +# on_turn_off: +# then: +# - script.execute: update_declan_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_declan_scoop_dog_poop - # ── Chloe's chores ────────────────────────── - - platform: template - name: "Chloe - Fill kitty feeders" - id: chloe_fill_kitty_feeders - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_chloe_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_chloe_fill_kitty_feeders - on_turn_off: - then: - - script.execute: update_chloe_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_chloe_fill_kitty_feeders - - platform: template - name: "Chloe - Scoop Kitty Litter" - id: chloe_scoop_kitty_litter - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_chloe_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_chloe_scoop_kitty_litter - on_turn_off: - then: - - script.execute: update_chloe_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_chloe_scoop_kitty_litter - - platform: template - name: "Chloe - Replace Kitty Litter Bags" - id: chloe_replace_kitty_litter_bags - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_chloe_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags - on_turn_off: - then: - - script.execute: update_chloe_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags +# # ── Chloe's chores ────────────────────────── +# - platform: template +# name: "Chloe - Fill kitty feeders" +# id: chloe_fill_kitty_feeders +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_chloe_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_chloe_fill_kitty_feeders +# on_turn_off: +# then: +# - script.execute: update_chloe_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_chloe_fill_kitty_feeders +# - platform: template +# name: "Chloe - Scoop Kitty Litter" +# id: chloe_scoop_kitty_litter +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_chloe_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_chloe_scoop_kitty_litter +# on_turn_off: +# then: +# - script.execute: update_chloe_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_chloe_scoop_kitty_litter +# - platform: template +# name: "Chloe - Replace Kitty Litter Bags" +# id: chloe_replace_kitty_litter_bags +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_chloe_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags +# on_turn_off: +# then: +# - script.execute: update_chloe_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_chloe_replace_kitty_litter_bags -# ── Sensors — reported to HA ───────────────────────────────────────────────── -sensor: - - platform: template - name: "Jordyn Chores Done" - id: jordyn_chores_done - accuracy_decimals: 0 - update_interval: 2s - lambda: |- - int done = 0; - if (id(jordyn_put_on_underwear).state) done++; - if (id(jordyn_brush_teeth).state) done++; - if (id(jordyn_fill_water_bowls).state) done++; - if (id(jordyn_restock_cat_food_cans).state) done++; - return done; +# # ── Sensors — reported to HA ───────────────────────────────────────────────── +# sensor: +# - platform: template +# name: "Jordyn Chores Done" +# id: jordyn_chores_done +# accuracy_decimals: 0 +# update_interval: 2s +# lambda: |- +# int done = 0; +# if (id(jordyn_put_on_underwear).state) done++; +# if (id(jordyn_brush_teeth).state) done++; +# if (id(jordyn_fill_water_bowls).state) done++; +# if (id(jordyn_restock_cat_food_cans).state) done++; +# return done; - - platform: template - name: "Declan Chores Done" - id: declan_chores_done - accuracy_decimals: 0 - update_interval: 2s - lambda: |- - int done = 0; - if (id(declan_take_morning_pill).state) done++; - if (id(declan_scoop_dog_poop).state) done++; - return done; +# - platform: template +# name: "Declan Chores Done" +# id: declan_chores_done +# accuracy_decimals: 0 +# update_interval: 2s +# lambda: |- +# int done = 0; +# if (id(declan_take_morning_pill).state) done++; +# if (id(declan_scoop_dog_poop).state) done++; +# return done; - - platform: template - name: "Chloe Chores Done" - id: chloe_chores_done - accuracy_decimals: 0 - update_interval: 2s - lambda: |- - int done = 0; - if (id(chloe_fill_kitty_feeders).state) done++; - if (id(chloe_scoop_kitty_litter).state) done++; - if (id(chloe_replace_kitty_litter_bags).state) done++; - return done; +# - platform: template +# name: "Chloe Chores Done" +# id: chloe_chores_done +# accuracy_decimals: 0 +# update_interval: 2s +# lambda: |- +# int done = 0; +# if (id(chloe_fill_kitty_feeders).state) done++; +# if (id(chloe_scoop_kitty_litter).state) done++; +# if (id(chloe_replace_kitty_litter_bags).state) done++; +# return done; -# Diagnostic sensors - - platform: wifi_signal - name: "${device_name} WiFi Signal" - update_interval: 60s +# # Diagnostic sensors +# - platform: wifi_signal +# name: "${device_name} WiFi Signal" +# update_interval: 60s - - platform: uptime - name: "${device_name} Uptime" - update_interval: 60s +# - platform: uptime +# name: "${device_name} Uptime" +# update_interval: 60s -text_sensor: - - platform: wifi_info - ip_address: - name: "${device_name} IP Address" - ssid: - name: "${device_name} Connected SSID" - bssid: - name: "${device_name} BSSID" +# text_sensor: +# - platform: wifi_info +# ip_address: +# name: "${device_name} IP Address" +# ssid: +# name: "${device_name} Connected SSID" +# bssid: +# name: "${device_name} BSSID" -# Watchdog to auto-reboot if things go wrong -# Note: interval fires on first tick (t≈0), so guard with millis() to ensure -# the device has actually been running for 5+ minutes before triggering. -interval: - - interval: 5min - then: - - if: - condition: - and: - - lambda: 'return millis() > 300000;' # >5 min uptime - - not: - wifi.connected: - then: - - logger.log: "Wi-Fi not connected for 5 min, rebooting..." - - delay: 10s - - lambda: 'App.safe_reboot();' +# # Watchdog to auto-reboot if things go wrong +# # Note: interval fires on first tick (t≈0), so guard with millis() to ensure +# # the device has actually been running for 5+ minutes before triggering. +# interval: +# - interval: 5min +# then: +# - if: +# condition: +# and: +# - lambda: 'return millis() > 300000;' # >5 min uptime +# - not: +# wifi.connected: +# then: +# - logger.log: "Wi-Fi not connected for 5 min, rebooting..." +# - delay: 10s +# - lambda: 'App.safe_reboot();' -# ── Scripts — reset + UI update ─────────────────────────────────────────────── -script: - - id: reset_jordyn_chores - mode: single - then: - - lambda: |- - id(jordyn_put_on_underwear).turn_off(); - id(jordyn_brush_teeth).turn_off(); - id(jordyn_fill_water_bowls).turn_off(); - id(jordyn_restock_cat_food_cans).turn_off(); - - script.execute: update_jordyn_ui +# # ── Scripts — reset + UI update ─────────────────────────────────────────────── +# script: +# - id: reset_jordyn_chores +# mode: single +# then: +# - lambda: |- +# id(jordyn_put_on_underwear).turn_off(); +# id(jordyn_brush_teeth).turn_off(); +# id(jordyn_fill_water_bowls).turn_off(); +# id(jordyn_restock_cat_food_cans).turn_off(); +# - script.execute: update_jordyn_ui - - id: update_jordyn_ui - mode: single - then: - - lambda: |- - int done = (id(jordyn_put_on_underwear).state ? 1 : 0) + (id(jordyn_brush_teeth).state ? 1 : 0) + (id(jordyn_fill_water_bowls).state ? 1 : 0) + (id(jordyn_restock_cat_food_cans).state ? 1 : 0); - int total = 4; - char buf[24]; +# - id: update_jordyn_ui +# mode: single +# then: +# - lambda: |- +# int done = (id(jordyn_put_on_underwear).state ? 1 : 0) + (id(jordyn_brush_teeth).state ? 1 : 0) + (id(jordyn_fill_water_bowls).state ? 1 : 0) + (id(jordyn_restock_cat_food_cans).state ? 1 : 0); +# int total = 4; +# char buf[24]; - // Progress bar + label - lv_bar_set_value(id(progress_bar_jordyn), done, LV_ANIM_ON); - snprintf(buf, sizeof(buf), "%d / 4", done); - lv_label_set_text(id(progress_label_jordyn), buf); +# // Progress bar + label +# lv_bar_set_value(id(progress_bar_jordyn), done, LV_ANIM_ON); +# snprintf(buf, sizeof(buf), "%d / 4", done); +# lv_label_set_text(id(progress_label_jordyn), buf); - // All-done message in sidebar - lv_label_set_text(id(all_done_label_jordyn), done == total ? "All done!" : ""); +# // All-done message in sidebar +# lv_label_set_text(id(all_done_label_jordyn), done == total ? "All done!" : ""); - // Home button: RED outline = incomplete, SOLID GREEN = all done - if (done == total) { - lv_obj_set_style_bg_opa(id(home_btn_jordyn), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_jordyn), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_jordyn), 0, LV_PART_MAIN); - lv_label_set_text(id(home_status_jordyn), "\u2713 all done!"); - lv_obj_set_style_text_color(id(home_status_jordyn), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - } else { - lv_obj_set_style_bg_opa(id(home_btn_jordyn), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_jordyn), lv_color_hex(0xFF4757), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_jordyn), 0, LV_PART_MAIN); - snprintf(buf, sizeof(buf), "%d left", total - done); - lv_label_set_text(id(home_status_jordyn), buf); - lv_obj_set_style_text_color(id(home_status_jordyn), lv_color_hex(0xFF4757), LV_PART_MAIN); - } +# // Home button: RED outline = incomplete, SOLID GREEN = all done +# if (done == total) { +# lv_obj_set_style_bg_opa(id(home_btn_jordyn), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_jordyn), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_jordyn), 0, LV_PART_MAIN); +# lv_label_set_text(id(home_status_jordyn), "\u2713 all done!"); +# lv_obj_set_style_text_color(id(home_status_jordyn), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# } else { +# lv_obj_set_style_bg_opa(id(home_btn_jordyn), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_jordyn), lv_color_hex(0xFF4757), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_jordyn), 0, LV_PART_MAIN); +# snprintf(buf, sizeof(buf), "%d left", total - done); +# lv_label_set_text(id(home_status_jordyn), buf); +# lv_obj_set_style_text_color(id(home_status_jordyn), lv_color_hex(0xFF4757), LV_PART_MAIN); +# } - // Card colours - if (id(jordyn_put_on_underwear).state) { - lv_obj_set_style_bg_color(id(card_jordyn_put_on_underwear), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_put_on_underwear), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_put_on_underwear), 3, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_put_on_underwear), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_jordyn_put_on_underwear), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_put_on_underwear), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_put_on_underwear), 2, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_put_on_underwear), ""); - } - if (id(jordyn_brush_teeth).state) { - lv_obj_set_style_bg_color(id(card_jordyn_brush_teeth), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_brush_teeth), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_brush_teeth), 3, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_brush_teeth), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_jordyn_brush_teeth), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_brush_teeth), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_brush_teeth), 2, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_brush_teeth), ""); - } - if (id(jordyn_fill_water_bowls).state) { - lv_obj_set_style_bg_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_fill_water_bowls), 3, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_fill_water_bowls), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_fill_water_bowls), 2, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_fill_water_bowls), ""); - } - if (id(jordyn_restock_cat_food_cans).state) { - lv_obj_set_style_bg_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_restock_cat_food_cans), 3, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_restock_cat_food_cans), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_jordyn_restock_cat_food_cans), 2, LV_PART_MAIN); - lv_label_set_text(id(check_jordyn_restock_cat_food_cans), ""); - } +# // Card colours +# if (id(jordyn_put_on_underwear).state) { +# lv_obj_set_style_bg_color(id(card_jordyn_put_on_underwear), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_put_on_underwear), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_put_on_underwear), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_put_on_underwear), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_jordyn_put_on_underwear), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_put_on_underwear), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_put_on_underwear), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_put_on_underwear), ""); +# } +# if (id(jordyn_brush_teeth).state) { +# lv_obj_set_style_bg_color(id(card_jordyn_brush_teeth), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_brush_teeth), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_brush_teeth), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_brush_teeth), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_jordyn_brush_teeth), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_brush_teeth), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_brush_teeth), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_brush_teeth), ""); +# } +# if (id(jordyn_fill_water_bowls).state) { +# lv_obj_set_style_bg_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_fill_water_bowls), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_fill_water_bowls), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_fill_water_bowls), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_fill_water_bowls), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_fill_water_bowls), ""); +# } +# if (id(jordyn_restock_cat_food_cans).state) { +# lv_obj_set_style_bg_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_restock_cat_food_cans), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_restock_cat_food_cans), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_jordyn_restock_cat_food_cans), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_jordyn_restock_cat_food_cans), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_jordyn_restock_cat_food_cans), ""); +# } - - id: reset_declan_chores - mode: single - then: - - lambda: |- - id(declan_take_morning_pill).turn_off(); - id(declan_scoop_dog_poop).turn_off(); - - script.execute: update_declan_ui +# - id: reset_declan_chores +# mode: single +# then: +# - lambda: |- +# id(declan_take_morning_pill).turn_off(); +# id(declan_scoop_dog_poop).turn_off(); +# - script.execute: update_declan_ui - - id: update_declan_ui - mode: single - then: - - lambda: |- - int done = (id(declan_take_morning_pill).state ? 1 : 0) + (id(declan_scoop_dog_poop).state ? 1 : 0); - int total = 2; - char buf[24]; +# - id: update_declan_ui +# mode: single +# then: +# - lambda: |- +# int done = (id(declan_take_morning_pill).state ? 1 : 0) + (id(declan_scoop_dog_poop).state ? 1 : 0); +# int total = 2; +# char buf[24]; - // Progress bar + label - lv_bar_set_value(id(progress_bar_declan), done, LV_ANIM_ON); - snprintf(buf, sizeof(buf), "%d / 2", done); - lv_label_set_text(id(progress_label_declan), buf); +# // Progress bar + label +# lv_bar_set_value(id(progress_bar_declan), done, LV_ANIM_ON); +# snprintf(buf, sizeof(buf), "%d / 2", done); +# lv_label_set_text(id(progress_label_declan), buf); - // All-done message in sidebar - lv_label_set_text(id(all_done_label_declan), done == total ? "All done!" : ""); +# // All-done message in sidebar +# lv_label_set_text(id(all_done_label_declan), done == total ? "All done!" : ""); - // Home button: RED outline = incomplete, SOLID GREEN = all done - if (done == total) { - lv_obj_set_style_bg_opa(id(home_btn_declan), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_declan), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_declan), 0, LV_PART_MAIN); - lv_label_set_text(id(home_status_declan), "\u2713 all done!"); - lv_obj_set_style_text_color(id(home_status_declan), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - } else { - lv_obj_set_style_bg_opa(id(home_btn_declan), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_declan), lv_color_hex(0xFF4757), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_declan), 0, LV_PART_MAIN); - snprintf(buf, sizeof(buf), "%d left", total - done); - lv_label_set_text(id(home_status_declan), buf); - lv_obj_set_style_text_color(id(home_status_declan), lv_color_hex(0xFF4757), LV_PART_MAIN); - } +# // Home button: RED outline = incomplete, SOLID GREEN = all done +# if (done == total) { +# lv_obj_set_style_bg_opa(id(home_btn_declan), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_declan), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_declan), 0, LV_PART_MAIN); +# lv_label_set_text(id(home_status_declan), "\u2713 all done!"); +# lv_obj_set_style_text_color(id(home_status_declan), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# } else { +# lv_obj_set_style_bg_opa(id(home_btn_declan), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_declan), lv_color_hex(0xFF4757), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_declan), 0, LV_PART_MAIN); +# snprintf(buf, sizeof(buf), "%d left", total - done); +# lv_label_set_text(id(home_status_declan), buf); +# lv_obj_set_style_text_color(id(home_status_declan), lv_color_hex(0xFF4757), LV_PART_MAIN); +# } - // Card colours - if (id(declan_take_morning_pill).state) { - lv_obj_set_style_bg_color(id(card_declan_take_morning_pill), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_declan_take_morning_pill), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_declan_take_morning_pill), 3, LV_PART_MAIN); - lv_label_set_text(id(check_declan_take_morning_pill), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_declan_take_morning_pill), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_declan_take_morning_pill), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_declan_take_morning_pill), 2, LV_PART_MAIN); - lv_label_set_text(id(check_declan_take_morning_pill), ""); - } - if (id(declan_scoop_dog_poop).state) { - lv_obj_set_style_bg_color(id(card_declan_scoop_dog_poop), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_declan_scoop_dog_poop), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_declan_scoop_dog_poop), 3, LV_PART_MAIN); - lv_label_set_text(id(check_declan_scoop_dog_poop), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_declan_scoop_dog_poop), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_declan_scoop_dog_poop), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_declan_scoop_dog_poop), 2, LV_PART_MAIN); - lv_label_set_text(id(check_declan_scoop_dog_poop), ""); - } +# // Card colours +# if (id(declan_take_morning_pill).state) { +# lv_obj_set_style_bg_color(id(card_declan_take_morning_pill), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_declan_take_morning_pill), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_declan_take_morning_pill), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_declan_take_morning_pill), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_declan_take_morning_pill), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_declan_take_morning_pill), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_declan_take_morning_pill), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_declan_take_morning_pill), ""); +# } +# if (id(declan_scoop_dog_poop).state) { +# lv_obj_set_style_bg_color(id(card_declan_scoop_dog_poop), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_declan_scoop_dog_poop), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_declan_scoop_dog_poop), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_declan_scoop_dog_poop), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_declan_scoop_dog_poop), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_declan_scoop_dog_poop), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_declan_scoop_dog_poop), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_declan_scoop_dog_poop), ""); +# } - - id: reset_chloe_chores - mode: single - then: - - lambda: |- - id(chloe_fill_kitty_feeders).turn_off(); - id(chloe_scoop_kitty_litter).turn_off(); - id(chloe_replace_kitty_litter_bags).turn_off(); - - script.execute: update_chloe_ui +# - id: reset_chloe_chores +# mode: single +# then: +# - lambda: |- +# id(chloe_fill_kitty_feeders).turn_off(); +# id(chloe_scoop_kitty_litter).turn_off(); +# id(chloe_replace_kitty_litter_bags).turn_off(); +# - script.execute: update_chloe_ui - - id: update_chloe_ui - mode: single - then: - - lambda: |- - int done = (id(chloe_fill_kitty_feeders).state ? 1 : 0) + (id(chloe_scoop_kitty_litter).state ? 1 : 0) + (id(chloe_replace_kitty_litter_bags).state ? 1 : 0); - int total = 3; - char buf[24]; +# - id: update_chloe_ui +# mode: single +# then: +# - lambda: |- +# int done = (id(chloe_fill_kitty_feeders).state ? 1 : 0) + (id(chloe_scoop_kitty_litter).state ? 1 : 0) + (id(chloe_replace_kitty_litter_bags).state ? 1 : 0); +# int total = 3; +# char buf[24]; - // Progress bar + label - lv_bar_set_value(id(progress_bar_chloe), done, LV_ANIM_ON); - snprintf(buf, sizeof(buf), "%d / 3", done); - lv_label_set_text(id(progress_label_chloe), buf); +# // Progress bar + label +# lv_bar_set_value(id(progress_bar_chloe), done, LV_ANIM_ON); +# snprintf(buf, sizeof(buf), "%d / 3", done); +# lv_label_set_text(id(progress_label_chloe), buf); - // All-done message in sidebar - lv_label_set_text(id(all_done_label_chloe), done == total ? "All done!" : ""); +# // All-done message in sidebar +# lv_label_set_text(id(all_done_label_chloe), done == total ? "All done!" : ""); - // Home button: RED outline = incomplete, SOLID GREEN = all done - if (done == total) { - lv_obj_set_style_bg_opa(id(home_btn_chloe), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_chloe), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_chloe), 0, LV_PART_MAIN); - lv_label_set_text(id(home_status_chloe), "\u2713 all done!"); - lv_obj_set_style_text_color(id(home_status_chloe), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - } else { - lv_obj_set_style_bg_opa(id(home_btn_chloe), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_chloe), lv_color_hex(0xFF4757), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_chloe), 0, LV_PART_MAIN); - snprintf(buf, sizeof(buf), "%d left", total - done); - lv_label_set_text(id(home_status_chloe), buf); - lv_obj_set_style_text_color(id(home_status_chloe), lv_color_hex(0xFF4757), LV_PART_MAIN); - } +# // Home button: RED outline = incomplete, SOLID GREEN = all done +# if (done == total) { +# lv_obj_set_style_bg_opa(id(home_btn_chloe), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_chloe), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_chloe), 0, LV_PART_MAIN); +# lv_label_set_text(id(home_status_chloe), "\u2713 all done!"); +# lv_obj_set_style_text_color(id(home_status_chloe), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# } else { +# lv_obj_set_style_bg_opa(id(home_btn_chloe), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_chloe), lv_color_hex(0xFF4757), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_chloe), 0, LV_PART_MAIN); +# snprintf(buf, sizeof(buf), "%d left", total - done); +# lv_label_set_text(id(home_status_chloe), buf); +# lv_obj_set_style_text_color(id(home_status_chloe), lv_color_hex(0xFF4757), LV_PART_MAIN); +# } - // Card colours - if (id(chloe_fill_kitty_feeders).state) { - lv_obj_set_style_bg_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_chloe_fill_kitty_feeders), 3, LV_PART_MAIN); - lv_label_set_text(id(check_chloe_fill_kitty_feeders), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_chloe_fill_kitty_feeders), 2, LV_PART_MAIN); - lv_label_set_text(id(check_chloe_fill_kitty_feeders), ""); - } - if (id(chloe_scoop_kitty_litter).state) { - lv_obj_set_style_bg_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_chloe_scoop_kitty_litter), 3, LV_PART_MAIN); - lv_label_set_text(id(check_chloe_scoop_kitty_litter), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_chloe_scoop_kitty_litter), 2, LV_PART_MAIN); - lv_label_set_text(id(check_chloe_scoop_kitty_litter), ""); - } - if (id(chloe_replace_kitty_litter_bags).state) { - lv_obj_set_style_bg_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_chloe_replace_kitty_litter_bags), 3, LV_PART_MAIN); - lv_label_set_text(id(check_chloe_replace_kitty_litter_bags), "\u2705"); - } else { - lv_obj_set_style_bg_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_chloe_replace_kitty_litter_bags), 2, LV_PART_MAIN); - lv_label_set_text(id(check_chloe_replace_kitty_litter_bags), ""); - } +# // Card colours +# if (id(chloe_fill_kitty_feeders).state) { +# lv_obj_set_style_bg_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_chloe_fill_kitty_feeders), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_chloe_fill_kitty_feeders), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_chloe_fill_kitty_feeders), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_chloe_fill_kitty_feeders), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_chloe_fill_kitty_feeders), ""); +# } +# if (id(chloe_scoop_kitty_litter).state) { +# lv_obj_set_style_bg_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_chloe_scoop_kitty_litter), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_chloe_scoop_kitty_litter), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_chloe_scoop_kitty_litter), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_chloe_scoop_kitty_litter), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_chloe_scoop_kitty_litter), ""); +# } +# if (id(chloe_replace_kitty_litter_bags).state) { +# lv_obj_set_style_bg_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_chloe_replace_kitty_litter_bags), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_chloe_replace_kitty_litter_bags), "\u2705"); +# } else { +# lv_obj_set_style_bg_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_chloe_replace_kitty_litter_bags), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_chloe_replace_kitty_litter_bags), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_chloe_replace_kitty_litter_bags), ""); +# } -# ── LVGL UI ─────────────────────────────────────────────────────────────────── -lvgl: - displays: - - main_display - touchscreens: - - touch - theme: - button: - radius: 20 - border_width: 0 - pages: - - id: page_home - bg_color: 0xFFF9F0 - widgets: - - label: - x: 0 - y: 36 - width: 776 - align: TOP_MID - text: "Chore Tracker" - text_font: font_title - text_color: 0x2D3436 - - label: - x: 0 - y: 90 - width: 776 - align: TOP_MID - text: "Tap a name to check off chores" - text_font: font_small - text_color: 0xB2BEC3 - - button: - x: 640 - y: 428 - width: 148 - height: 40 - bg_color: 0xFF4757 - radius: 12 - on_click: - then: - - script.execute: reset_jordyn_chores - - script.execute: reset_declan_chores - - script.execute: reset_chloe_chores - widgets: - - label: - align: CENTER - text: "Reset All" - text_color: 0xFFFFFF - text_font: font_tiny - - button: - id: home_btn_jordyn - x: 80 - y: 130 - width: 200 - height: 210 - bg_color: 0xFF4757 - bg_opa: COVER - border_width: 0 - radius: 24 - on_click: - then: - - lvgl.page.show: page_jordyn - widgets: - - label: - align: CENTER - y: -45 - text: "󰄛" - text_font: font_mdi_large - - label: - align: CENTER - y: 22 - text: "Jordyn" - text_font: font_name - text_color: 0xFFFFFF - - label: - id: home_status_jordyn - align: CENTER - y: 66 - text: "not done" - text_font: font_tiny - text_color: 0xFFFFFF +# # ── LVGL UI ─────────────────────────────────────────────────────────────────── +# lvgl: +# displays: +# - main_display +# touchscreens: +# - touch +# theme: +# button: +# radius: 20 +# border_width: 0 +# pages: +# - id: page_home +# bg_color: 0xFFF9F0 +# widgets: +# - label: +# x: 0 +# y: 36 +# width: 776 +# align: TOP_MID +# text: "Chore Tracker" +# text_font: font_title +# text_color: 0x2D3436 +# - label: +# x: 0 +# y: 90 +# width: 776 +# align: TOP_MID +# text: "Tap a name to check off chores" +# text_font: font_small +# text_color: 0xB2BEC3 +# - button: +# x: 640 +# y: 428 +# width: 148 +# height: 40 +# bg_color: 0xFF4757 +# radius: 12 +# on_click: +# then: +# - script.execute: reset_jordyn_chores +# - script.execute: reset_declan_chores +# - script.execute: reset_chloe_chores +# widgets: +# - label: +# align: CENTER +# text: "Reset All" +# text_color: 0xFFFFFF +# text_font: font_tiny +# - button: +# id: home_btn_jordyn +# x: 80 +# y: 130 +# width: 200 +# height: 210 +# bg_color: 0xFF4757 +# bg_opa: COVER +# border_width: 0 +# radius: 24 +# on_click: +# then: +# - lvgl.page.show: page_jordyn +# widgets: +# - label: +# align: CENTER +# y: -45 +# text: "󰄛" +# text_font: font_mdi_large +# - label: +# align: CENTER +# y: 22 +# text: "Jordyn" +# text_font: font_name +# text_color: 0xFFFFFF +# - label: +# id: home_status_jordyn +# align: CENTER +# y: 66 +# text: "not done" +# text_font: font_tiny +# text_color: 0xFFFFFF - - button: - id: home_btn_declan - x: 300 - y: 130 - width: 200 - height: 210 - bg_color: 0xFF4757 - bg_opa: COVER - border_width: 0 - radius: 24 - on_click: - then: - - lvgl.page.show: page_declan - widgets: - - label: - align: CENTER - y: -45 - text: "󱑷" - text_font: font_mdi_large - - label: - align: CENTER - y: 22 - text: "Declan" - text_font: font_name - text_color: 0xFFFFFF - - label: - id: home_status_declan - align: CENTER - y: 66 - text: "not done" - text_font: font_tiny - text_color: 0xFFFFFF +# - button: +# id: home_btn_declan +# x: 300 +# y: 130 +# width: 200 +# height: 210 +# bg_color: 0xFF4757 +# bg_opa: COVER +# border_width: 0 +# radius: 24 +# on_click: +# then: +# - lvgl.page.show: page_declan +# widgets: +# - label: +# align: CENTER +# y: -45 +# text: "󱑷" +# text_font: font_mdi_large +# - label: +# align: CENTER +# y: 22 +# text: "Declan" +# text_font: font_name +# text_color: 0xFFFFFF +# - label: +# id: home_status_declan +# align: CENTER +# y: 66 +# text: "not done" +# text_font: font_tiny +# text_color: 0xFFFFFF - - button: - id: home_btn_chloe - x: 520 - y: 130 - width: 200 - height: 210 - bg_color: 0xFF4757 - bg_opa: COVER - border_width: 0 - radius: 24 - on_click: - then: - - lvgl.page.show: page_chloe - widgets: - - label: - align: CENTER - y: -45 - text: "󱚣" - text_font: font_mdi_large - - label: - align: CENTER - y: 22 - text: "Chloe" - text_font: font_name - text_color: 0xFFFFFF - - label: - id: home_status_chloe - align: CENTER - y: 66 - text: "not done" - text_font: font_tiny - text_color: 0xFFFFFF +# - button: +# id: home_btn_chloe +# x: 520 +# y: 130 +# width: 200 +# height: 210 +# bg_color: 0xFF4757 +# bg_opa: COVER +# border_width: 0 +# radius: 24 +# on_click: +# then: +# - lvgl.page.show: page_chloe +# widgets: +# - label: +# align: CENTER +# y: -45 +# text: "󱚣" +# text_font: font_mdi_large +# - label: +# align: CENTER +# y: 22 +# text: "Chloe" +# text_font: font_name +# text_color: 0xFFFFFF +# - label: +# id: home_status_chloe +# align: CENTER +# y: 66 +# text: "not done" +# text_font: font_tiny +# text_color: 0xFFFFFF - - id: page_jordyn - bg_color: 0xFFF9F0 - widgets: +# - id: page_jordyn +# bg_color: 0xFFF9F0 +# widgets: - # ── Sidebar ────────────────────────────────────────────────────────── - - obj: - x: 0 - y: 0 - width: 172 - height: 480 - bg_color: 0x5F5980 - border_width: 0 - radius: 0 - pad_all: 0 - widgets: - - label: - x: 0 - y: 22 - width: 172 - align: TOP_MID - text: "󰄛" - text_font: font_mdi_large - - label: - x: 0 - y: 76 - width: 172 - align: TOP_MID - text: "Jordyn" - text_font: font_name - text_color: 0xFFFFFF - - bar: - id: progress_bar_jordyn - x: 14 - y: 132 - width: 144 - height: 16 - bg_color: 0x2F3061 - indicator: - bg_color: 0xFFFFFF - value: 0 - min_value: 0 - max_value: 4 - - label: - id: progress_label_jordyn - x: 0 - y: 156 - width: 172 - align: TOP_MID - text: "0 / 4" - text_font: font_small - text_color: 0xDDEEFF - - label: - id: all_done_label_jordyn - x: 0 - y: 192 - width: 172 - align: TOP_MID - text: "" - text_font: font_small - text_color: 0xFFFFFF - - button: - x: 14 - y: 374 - width: 144 - height: 44 - bg_color: 0xFF4757 - radius: 14 - on_click: - then: - - script.execute: reset_jordyn_chores - widgets: - - label: - align: CENTER - text: "Reset" - text_color: 0xFFFFFF - text_font: font_small - - button: - x: 14 - y: 424 - width: 144 - height: 44 - bg_color: 0x2F3061 - radius: 14 - on_click: - then: - - lvgl.page.show: page_home - widgets: - - label: - align: CENTER - text: "Home" - text_color: 0xFFFFFF - text_font: font_small +# # ── Sidebar ────────────────────────────────────────────────────────── +# - obj: +# x: 0 +# y: 0 +# width: 172 +# height: 480 +# bg_color: 0x5F5980 +# border_width: 0 +# radius: 0 +# pad_all: 0 +# widgets: +# - label: +# x: 0 +# y: 22 +# width: 172 +# align: TOP_MID +# text: "󰄛" +# text_font: font_mdi_large +# - label: +# x: 0 +# y: 76 +# width: 172 +# align: TOP_MID +# text: "Jordyn" +# text_font: font_name +# text_color: 0xFFFFFF +# - bar: +# id: progress_bar_jordyn +# x: 14 +# y: 132 +# width: 144 +# height: 16 +# bg_color: 0x2F3061 +# indicator: +# bg_color: 0xFFFFFF +# value: 0 +# min_value: 0 +# max_value: 4 +# - label: +# id: progress_label_jordyn +# x: 0 +# y: 156 +# width: 172 +# align: TOP_MID +# text: "0 / 4" +# text_font: font_small +# text_color: 0xDDEEFF +# - label: +# id: all_done_label_jordyn +# x: 0 +# y: 192 +# width: 172 +# align: TOP_MID +# text: "" +# text_font: font_small +# text_color: 0xFFFFFF +# - button: +# x: 14 +# y: 374 +# width: 144 +# height: 44 +# bg_color: 0xFF4757 +# radius: 14 +# on_click: +# then: +# - script.execute: reset_jordyn_chores +# widgets: +# - label: +# align: CENTER +# text: "Reset" +# text_color: 0xFFFFFF +# text_font: font_small +# - button: +# x: 14 +# y: 424 +# width: 144 +# height: 44 +# bg_color: 0x2F3061 +# radius: 14 +# on_click: +# then: +# - lvgl.page.show: page_home +# widgets: +# - label: +# align: CENTER +# text: "Home" +# text_color: 0xFFFFFF +# text_font: font_small - # ── Chore cards ─────────────────────────────────────────────────────── - - button: - id: card_jordyn_put_on_underwear - x: 182 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: jordyn_put_on_underwear - widgets: - - label: - id: check_jordyn_put_on_underwear - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰋣" - text_font: font_mdi_small - text_color: 0x5F5980 - - label: - align: CENTER - y: 17 - text: "Put on underwear" - text_font: font_small - text_color: 0x2D3436 +# # ── Chore cards ─────────────────────────────────────────────────────── +# - button: +# id: card_jordyn_put_on_underwear +# x: 182 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: jordyn_put_on_underwear +# widgets: +# - label: +# id: check_jordyn_put_on_underwear +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰋣" +# text_font: font_mdi_small +# text_color: 0x5F5980 +# - label: +# align: CENTER +# y: 17 +# text: "Put on underwear" +# text_font: font_small +# text_color: 0x2D3436 - - button: - id: card_jordyn_brush_teeth - x: 387 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: jordyn_brush_teeth - widgets: - - label: - id: check_jordyn_brush_teeth - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰦩" - text_font: font_mdi_small - text_color: 0x5F5980 - - label: - align: CENTER - y: 17 - text: "Brush Teeth" - text_font: font_small - text_color: 0x2D3436 +# - button: +# id: card_jordyn_brush_teeth +# x: 387 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: jordyn_brush_teeth +# widgets: +# - label: +# id: check_jordyn_brush_teeth +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰦩" +# text_font: font_mdi_small +# text_color: 0x5F5980 +# - label: +# align: CENTER +# y: 17 +# text: "Brush Teeth" +# text_font: font_small +# text_color: 0x2D3436 - - button: - id: card_jordyn_fill_water_bowls - x: 592 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: jordyn_fill_water_bowls - widgets: - - label: - id: check_jordyn_fill_water_bowls - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰆫" - text_font: font_mdi_small - text_color: 0x5F5980 - - label: - align: CENTER - y: 17 - text: "Fill water bowls" - text_font: font_small - text_color: 0x2D3436 +# - button: +# id: card_jordyn_fill_water_bowls +# x: 592 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: jordyn_fill_water_bowls +# widgets: +# - label: +# id: check_jordyn_fill_water_bowls +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰆫" +# text_font: font_mdi_small +# text_color: 0x5F5980 +# - label: +# align: CENTER +# y: 17 +# text: "Fill water bowls" +# text_font: font_small +# text_color: 0x2D3436 - - button: - id: card_jordyn_restock_cat_food_cans - x: 182 - y: 160 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: jordyn_restock_cat_food_cans - widgets: - - label: - id: check_jordyn_restock_cat_food_cans - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󱜜" - text_font: font_mdi_small - text_color: 0x5F5980 - - label: - align: CENTER - y: 17 - text: "Restock Cat Food Cans" - text_font: font_small - text_color: 0x2D3436 +# - button: +# id: card_jordyn_restock_cat_food_cans +# x: 182 +# y: 160 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: jordyn_restock_cat_food_cans +# widgets: +# - label: +# id: check_jordyn_restock_cat_food_cans +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󱜜" +# text_font: font_mdi_small +# text_color: 0x5F5980 +# - label: +# align: CENTER +# y: 17 +# text: "Restock Cat Food Cans" +# text_font: font_small +# text_color: 0x2D3436 - - id: page_declan - bg_color: 0xFFF9F0 - widgets: +# - id: page_declan +# bg_color: 0xFFF9F0 +# widgets: - # ── Sidebar ────────────────────────────────────────────────────────── - - obj: - x: 0 - y: 0 - width: 172 - height: 480 - bg_color: 0x720026 - border_width: 0 - radius: 0 - pad_all: 0 - widgets: - - label: - x: 0 - y: 22 - width: 172 - align: TOP_MID - text: "󱑷" - text_font: font_mdi_large - - label: - x: 0 - y: 76 - width: 172 - align: TOP_MID - text: "Declan" - text_font: font_name - text_color: 0xFFFFFF - - bar: - id: progress_bar_declan - x: 14 - y: 132 - width: 144 - height: 16 - bg_color: 0x4F000B - indicator: - bg_color: 0xFFFFFF - value: 0 - min_value: 0 - max_value: 2 - - label: - id: progress_label_declan - x: 0 - y: 156 - width: 172 - align: TOP_MID - text: "0 / 2" - text_font: font_small - text_color: 0xDDEEFF - - label: - id: all_done_label_declan - x: 0 - y: 192 - width: 172 - align: TOP_MID - text: "" - text_font: font_small - text_color: 0xFFFFFF - - button: - x: 14 - y: 374 - width: 144 - height: 44 - bg_color: 0xFF4757 - radius: 14 - on_click: - then: - - script.execute: reset_declan_chores - widgets: - - label: - align: CENTER - text: "Reset" - text_color: 0xFFFFFF - text_font: font_small - - button: - x: 14 - y: 424 - width: 144 - height: 44 - bg_color: 0x4F000B - radius: 14 - on_click: - then: - - lvgl.page.show: page_home - widgets: - - label: - align: CENTER - text: "Home" - text_color: 0xFFFFFF - text_font: font_small +# # ── Sidebar ────────────────────────────────────────────────────────── +# - obj: +# x: 0 +# y: 0 +# width: 172 +# height: 480 +# bg_color: 0x720026 +# border_width: 0 +# radius: 0 +# pad_all: 0 +# widgets: +# - label: +# x: 0 +# y: 22 +# width: 172 +# align: TOP_MID +# text: "󱑷" +# text_font: font_mdi_large +# - label: +# x: 0 +# y: 76 +# width: 172 +# align: TOP_MID +# text: "Declan" +# text_font: font_name +# text_color: 0xFFFFFF +# - bar: +# id: progress_bar_declan +# x: 14 +# y: 132 +# width: 144 +# height: 16 +# bg_color: 0x4F000B +# indicator: +# bg_color: 0xFFFFFF +# value: 0 +# min_value: 0 +# max_value: 2 +# - label: +# id: progress_label_declan +# x: 0 +# y: 156 +# width: 172 +# align: TOP_MID +# text: "0 / 2" +# text_font: font_small +# text_color: 0xDDEEFF +# - label: +# id: all_done_label_declan +# x: 0 +# y: 192 +# width: 172 +# align: TOP_MID +# text: "" +# text_font: font_small +# text_color: 0xFFFFFF +# - button: +# x: 14 +# y: 374 +# width: 144 +# height: 44 +# bg_color: 0xFF4757 +# radius: 14 +# on_click: +# then: +# - script.execute: reset_declan_chores +# widgets: +# - label: +# align: CENTER +# text: "Reset" +# text_color: 0xFFFFFF +# text_font: font_small +# - button: +# x: 14 +# y: 424 +# width: 144 +# height: 44 +# bg_color: 0x4F000B +# radius: 14 +# on_click: +# then: +# - lvgl.page.show: page_home +# widgets: +# - label: +# align: CENTER +# text: "Home" +# text_color: 0xFFFFFF +# text_font: font_small - # ── Chore cards ─────────────────────────────────────────────────────── - - button: - id: card_declan_take_morning_pill - x: 182 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: declan_take_morning_pill - widgets: - - label: - id: check_declan_take_morning_pill - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰐂" - text_font: font_mdi_small - text_color: 0x720026 - - label: - align: CENTER - y: 17 - text: "Take morning pill" - text_font: font_small - text_color: 0x2D3436 +# # ── Chore cards ─────────────────────────────────────────────────────── +# - button: +# id: card_declan_take_morning_pill +# x: 182 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: declan_take_morning_pill +# widgets: +# - label: +# id: check_declan_take_morning_pill +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰐂" +# text_font: font_mdi_small +# text_color: 0x720026 +# - label: +# align: CENTER +# y: 17 +# text: "Take morning pill" +# text_font: font_small +# text_color: 0x2D3436 - - button: - id: card_declan_scoop_dog_poop - x: 387 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: declan_scoop_dog_poop - widgets: - - label: - id: check_declan_scoop_dog_poop - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰇷" - text_font: font_mdi_small - text_color: 0x720026 - - label: - align: CENTER - y: 17 - text: "Scoop Dog Poop" - text_font: font_small - text_color: 0x2D3436 +# - button: +# id: card_declan_scoop_dog_poop +# x: 387 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: declan_scoop_dog_poop +# widgets: +# - label: +# id: check_declan_scoop_dog_poop +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰇷" +# text_font: font_mdi_small +# text_color: 0x720026 +# - label: +# align: CENTER +# y: 17 +# text: "Scoop Dog Poop" +# text_font: font_small +# text_color: 0x2D3436 - - id: page_chloe - bg_color: 0xFFF9F0 - widgets: +# - id: page_chloe +# bg_color: 0xFFF9F0 +# widgets: - # ── Sidebar ────────────────────────────────────────────────────────── - - obj: - x: 0 - y: 0 - width: 172 - height: 480 - bg_color: 0x73683B - border_width: 0 - radius: 0 - pad_all: 0 - widgets: - - label: - x: 0 - y: 22 - width: 172 - align: TOP_MID - text: "󱚣" - text_font: font_mdi_large - - label: - x: 0 - y: 76 - width: 172 - align: TOP_MID - text: "Chloe" - text_font: font_name - text_color: 0xFFFFFF - - bar: - id: progress_bar_chloe - x: 14 - y: 132 - width: 144 - height: 16 - bg_color: 0x583E23 - indicator: - bg_color: 0xFFFFFF - value: 0 - min_value: 0 - max_value: 3 - - label: - id: progress_label_chloe - x: 0 - y: 156 - width: 172 - align: TOP_MID - text: "0 / 3" - text_font: font_small - text_color: 0xDDEEFF - - label: - id: all_done_label_chloe - x: 0 - y: 192 - width: 172 - align: TOP_MID - text: "" - text_font: font_small - text_color: 0xFFFFFF - - button: - x: 14 - y: 374 - width: 144 - height: 44 - bg_color: 0xFF4757 - radius: 14 - on_click: - then: - - script.execute: reset_chloe_chores - widgets: - - label: - align: CENTER - text: "Reset" - text_color: 0xFFFFFF - text_font: font_small - - button: - x: 14 - y: 424 - width: 144 - height: 44 - bg_color: 0x583E23 - radius: 14 - on_click: - then: - - lvgl.page.show: page_home - widgets: - - label: - align: CENTER - text: "Home" - text_color: 0xFFFFFF - text_font: font_small +# # ── Sidebar ────────────────────────────────────────────────────────── +# - obj: +# x: 0 +# y: 0 +# width: 172 +# height: 480 +# bg_color: 0x73683B +# border_width: 0 +# radius: 0 +# pad_all: 0 +# widgets: +# - label: +# x: 0 +# y: 22 +# width: 172 +# align: TOP_MID +# text: "󱚣" +# text_font: font_mdi_large +# - label: +# x: 0 +# y: 76 +# width: 172 +# align: TOP_MID +# text: "Chloe" +# text_font: font_name +# text_color: 0xFFFFFF +# - bar: +# id: progress_bar_chloe +# x: 14 +# y: 132 +# width: 144 +# height: 16 +# bg_color: 0x583E23 +# indicator: +# bg_color: 0xFFFFFF +# value: 0 +# min_value: 0 +# max_value: 3 +# - label: +# id: progress_label_chloe +# x: 0 +# y: 156 +# width: 172 +# align: TOP_MID +# text: "0 / 3" +# text_font: font_small +# text_color: 0xDDEEFF +# - label: +# id: all_done_label_chloe +# x: 0 +# y: 192 +# width: 172 +# align: TOP_MID +# text: "" +# text_font: font_small +# text_color: 0xFFFFFF +# - button: +# x: 14 +# y: 374 +# width: 144 +# height: 44 +# bg_color: 0xFF4757 +# radius: 14 +# on_click: +# then: +# - script.execute: reset_chloe_chores +# widgets: +# - label: +# align: CENTER +# text: "Reset" +# text_color: 0xFFFFFF +# text_font: font_small +# - button: +# x: 14 +# y: 424 +# width: 144 +# height: 44 +# bg_color: 0x583E23 +# radius: 14 +# on_click: +# then: +# - lvgl.page.show: page_home +# widgets: +# - label: +# align: CENTER +# text: "Home" +# text_color: 0xFFFFFF +# text_font: font_small - # ── Chore cards ─────────────────────────────────────────────────────── - - button: - id: card_chloe_fill_kitty_feeders - x: 182 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: chloe_fill_kitty_feeders - widgets: - - label: - id: check_chloe_fill_kitty_feeders - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰊩" - text_font: font_mdi_small - text_color: 0x73683B - - label: - align: CENTER - y: 17 - text: "Fill kitty feeders" - text_font: font_small - text_color: 0x2D3436 +# # ── Chore cards ─────────────────────────────────────────────────────── +# - button: +# id: card_chloe_fill_kitty_feeders +# x: 182 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: chloe_fill_kitty_feeders +# widgets: +# - label: +# id: check_chloe_fill_kitty_feeders +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰊩" +# text_font: font_mdi_small +# text_color: 0x73683B +# - label: +# align: CENTER +# y: 17 +# text: "Fill kitty feeders" +# text_font: font_small +# text_color: 0x2D3436 - - button: - id: card_chloe_scoop_kitty_litter - x: 387 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: chloe_scoop_kitty_litter - widgets: - - label: - id: check_chloe_scoop_kitty_litter - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰇷" - text_font: font_mdi_small - text_color: 0x73683B - - label: - align: CENTER - y: 17 - text: "Scoop Kitty Litter" - text_font: font_small - text_color: 0x2D3436 +# - button: +# id: card_chloe_scoop_kitty_litter +# x: 387 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: chloe_scoop_kitty_litter +# widgets: +# - label: +# id: check_chloe_scoop_kitty_litter +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰇷" +# text_font: font_mdi_small +# text_color: 0x73683B +# - label: +# align: CENTER +# y: 17 +# text: "Scoop Kitty Litter" +# text_font: font_small +# text_color: 0x2D3436 - - button: - id: card_chloe_replace_kitty_litter_bags - x: 592 - y: 12 - width: 195 - height: 138 - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: chloe_replace_kitty_litter_bags - widgets: - - label: - id: check_chloe_replace_kitty_litter_bags - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: -34 - text: "󰇷" - text_font: font_mdi_small - text_color: 0x73683B - - label: - align: CENTER - y: 17 - text: "Replace Kitty Litter Bags" - text_font: font_small - text_color: 0x2D3436 +# - button: +# id: card_chloe_replace_kitty_litter_bags +# x: 592 +# y: 12 +# width: 195 +# height: 138 +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: chloe_replace_kitty_litter_bags +# widgets: +# - label: +# id: check_chloe_replace_kitty_litter_bags +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: -34 +# text: "󰇷" +# text_font: font_mdi_small +# text_color: 0x73683B +# - label: +# align: CENTER +# y: 17 +# text: "Replace Kitty Litter Bags" +# text_font: font_small +# text_color: 0x2D3436 diff --git a/esphome/chore-tracker/chore-tracker-dashboard.yaml b/esphome/chore-tracker/chore-tracker-dashboard.yaml index 5a97329..260fc1f 100644 --- a/esphome/chore-tracker/chore-tracker-dashboard.yaml +++ b/esphome/chore-tracker/chore-tracker-dashboard.yaml @@ -1,115 +1,115 @@ -################################################################################ -# chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml) -# Kids: Jordyn, Declan, Chloe -################################################################################ +# ################################################################################ +# # chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml) +# # Kids: Jordyn, Declan, Chloe +# ################################################################################ -title: "Chore Tracker" -views: - - title: Today - icon: mdi:checkbox-marked-circle - path: chores - cards: - - type: horizontal-stack - cards: - - type: markdown - content: > - ### 󰄛 Jordyn - {{ - 'All done!' if states('sensor.jordyn_all_chores_done') == 'True' - else states('sensor.jordyn_chores_done_today') ~ '/4 chores done' - }} +# title: "Chore Tracker" +# views: +# - title: Today +# icon: mdi:checkbox-marked-circle +# path: chores +# cards: +# - type: horizontal-stack +# cards: +# - type: markdown +# content: > +# ### 󰄛 Jordyn +# {{ +# 'All done!' if states('sensor.jordyn_all_chores_done') == 'True' +# else states('sensor.jordyn_chores_done_today') ~ '/4 chores done' +# }} - - type: markdown - content: > - ### 󱑷 Declan - {{ - 'All done!' if states('sensor.declan_all_chores_done') == 'True' - else states('sensor.declan_chores_done_today') ~ '/2 chores done' - }} +# - type: markdown +# content: > +# ### 󱑷 Declan +# {{ +# 'All done!' if states('sensor.declan_all_chores_done') == 'True' +# else states('sensor.declan_chores_done_today') ~ '/2 chores done' +# }} - - type: markdown - content: > - ### 󱚣 Chloe - {{ - 'All done!' if states('sensor.chloe_all_chores_done') == 'True' - else states('sensor.chloe_chores_done_today') ~ '/3 chores done' - }} +# - type: markdown +# content: > +# ### 󱚣 Chloe +# {{ +# 'All done!' if states('sensor.chloe_all_chores_done') == 'True' +# else states('sensor.chloe_chores_done_today') ~ '/3 chores done' +# }} - - type: button - name: "Reset ALL Chores" - icon: mdi:restart-alert - tap_action: - action: call-service - service: input_button.press - target: - entity_id: input_button.reset_all_chores - - type: entities - title: "󰄛 Jordyn's Chores" - entities: - - entity: switch.chore_tracker_jordyn_put_on_underwear - name: "󰋣 Put on underwear" +# - type: button +# name: "Reset ALL Chores" +# icon: mdi:restart-alert +# tap_action: +# action: call-service +# service: input_button.press +# target: +# entity_id: input_button.reset_all_chores +# - type: entities +# title: "󰄛 Jordyn's Chores" +# entities: +# - entity: switch.chore_tracker_jordyn_put_on_underwear +# name: "󰋣 Put on underwear" - - entity: switch.chore_tracker_jordyn_brush_teeth - name: "󰦩 Brush Teeth" +# - entity: switch.chore_tracker_jordyn_brush_teeth +# name: "󰦩 Brush Teeth" - - entity: switch.chore_tracker_jordyn_fill_water_bowls - name: "󰆫 Fill water bowls" +# - entity: switch.chore_tracker_jordyn_fill_water_bowls +# name: "󰆫 Fill water bowls" - - entity: switch.chore_tracker_jordyn_restock_cat_food_cans - name: "󱜜 Restock Cat Food Cans" - - type: divider - - entity: sensor.jordyn_chores_done_today - name: "Chores done today" - - entity: sensor.jordyn_all_chores_done - name: "All Done?" - - type: button - name: "Reset Jordyn's Chores" - tap_action: - action: call-service - service: input_button.press - target: - entity_id: input_button.jordyn_reset_chores - - type: entities - title: "󱑷 Declan's Chores" - entities: - - entity: switch.chore_tracker_declan_take_morning_pill - name: "󰐂 Take morning pill" +# - entity: switch.chore_tracker_jordyn_restock_cat_food_cans +# name: "󱜜 Restock Cat Food Cans" +# - type: divider +# - entity: sensor.jordyn_chores_done_today +# name: "Chores done today" +# - entity: sensor.jordyn_all_chores_done +# name: "All Done?" +# - type: button +# name: "Reset Jordyn's Chores" +# tap_action: +# action: call-service +# service: input_button.press +# target: +# entity_id: input_button.jordyn_reset_chores +# - type: entities +# title: "󱑷 Declan's Chores" +# entities: +# - entity: switch.chore_tracker_declan_take_morning_pill +# name: "󰐂 Take morning pill" - - entity: switch.chore_tracker_declan_scoop_dog_poop - name: "󰇷 Scoop Dog Poop" - - type: divider - - entity: sensor.declan_chores_done_today - name: "Chores done today" - - entity: sensor.declan_all_chores_done - name: "All Done?" - - type: button - name: "Reset Declan's Chores" - tap_action: - action: call-service - service: input_button.press - target: - entity_id: input_button.declan_reset_chores - - type: entities - title: "󱚣 Chloe's Chores" - entities: - - entity: switch.chore_tracker_chloe_fill_kitty_feeders - name: "󰊩 Fill kitty feeders" +# - entity: switch.chore_tracker_declan_scoop_dog_poop +# name: "󰇷 Scoop Dog Poop" +# - type: divider +# - entity: sensor.declan_chores_done_today +# name: "Chores done today" +# - entity: sensor.declan_all_chores_done +# name: "All Done?" +# - type: button +# name: "Reset Declan's Chores" +# tap_action: +# action: call-service +# service: input_button.press +# target: +# entity_id: input_button.declan_reset_chores +# - type: entities +# title: "󱚣 Chloe's Chores" +# entities: +# - entity: switch.chore_tracker_chloe_fill_kitty_feeders +# name: "󰊩 Fill kitty feeders" - - entity: switch.chore_tracker_chloe_scoop_kitty_litter - name: "󰇷 Scoop Kitty Litter" +# - entity: switch.chore_tracker_chloe_scoop_kitty_litter +# name: "󰇷 Scoop Kitty Litter" - - entity: switch.chore_tracker_chloe_replace_kitty_litter_bags - name: "󰇷 Replace Kitty Litter Bags" - - type: divider - - entity: sensor.chloe_chores_done_today - name: "Chores done today" - - entity: sensor.chloe_all_chores_done - name: "All Done?" - - type: button - name: "Reset Chloe's Chores" - tap_action: - action: call-service - service: input_button.press - target: - entity_id: input_button.chloe_reset_chores +# - entity: switch.chore_tracker_chloe_replace_kitty_litter_bags +# name: "󰇷 Replace Kitty Litter Bags" +# - type: divider +# - entity: sensor.chloe_chores_done_today +# name: "Chores done today" +# - entity: sensor.chloe_all_chores_done +# name: "All Done?" +# - type: button +# name: "Reset Chloe's Chores" +# tap_action: +# action: call-service +# service: input_button.press +# target: +# entity_id: input_button.chloe_reset_chores diff --git a/esphome/chore-tracker/chores_config.yaml b/esphome/chore-tracker/chores_config.yaml index 74fe8a9..af50f02 100644 --- a/esphome/chore-tracker/chores_config.yaml +++ b/esphome/chore-tracker/chores_config.yaml @@ -1,81 +1,81 @@ -################################################################################ -# chores_config.yaml — YOUR SINGLE SOURCE OF TRUTH -# -# Edit this file to change kids, chores, and settings. -# Then run: python3 generate.py -# -# ICONS: Use MDI codepoints from https://pictogrammers.com/library/mdi/ -# Format: "\U000FXXXX" e.g. mdi:bed = "\U000F02E3" -# The kid avatar field also uses MDI codepoints. -# -# Common chore icons: -# Bed: "\U000F02E3" mdi:bed -# Brush teeth: "\U000F09A9" mdi:toothbrush -# Broom: "\U000F00A8" mdi:broom -# Book: "\U000F0219" mdi:book-open-variant -# Dog: "\U000F01F9" mdi:dog -# Paw: "\U000F0265" mdi:paw -# Silverware: "\U000F03E7" mdi:silverware-fork-knife -# Trash: "\U000F05B8" mdi:trash-can -# Watering can: "\U000F1B25" mdi:watering-can -# Piano: "\U000F0F9E" mdi:piano -# Dumbbell: "\U000F0F1E" mdi:dumbbell -# Shower: "\U000F0467" mdi:shower -################################################################################ +# ################################################################################ +# # chores_config.yaml — YOUR SINGLE SOURCE OF TRUTH +# # +# # Edit this file to change kids, chores, and settings. +# # Then run: python3 generate.py +# # +# # ICONS: Use MDI codepoints from https://pictogrammers.com/library/mdi/ +# # Format: "\U000FXXXX" e.g. mdi:bed = "\U000F02E3" +# # The kid avatar field also uses MDI codepoints. +# # +# # Common chore icons: +# # Bed: "\U000F02E3" mdi:bed +# # Brush teeth: "\U000F09A9" mdi:toothbrush +# # Broom: "\U000F00A8" mdi:broom +# # Book: "\U000F0219" mdi:book-open-variant +# # Dog: "\U000F01F9" mdi:dog +# # Paw: "\U000F0265" mdi:paw +# # Silverware: "\U000F03E7" mdi:silverware-fork-knife +# # Trash: "\U000F05B8" mdi:trash-can +# # Watering can: "\U000F1B25" mdi:watering-can +# # Piano: "\U000F0F9E" mdi:piano +# # Dumbbell: "\U000F0F1E" mdi:dumbbell +# # Shower: "\U000F0467" mdi:shower +# ################################################################################ -settings: - device_name: chore-tracker - friendly_name: "Chore Tracker" - wifi_ssid: !secret wifi_iot_ssid - wifi_password: !secret wifi_password - api_key: !secret api_encryption_key - ota_password: !secret ota_password - reset_time: "00:00:00" - reminder_time: "18:00" - notify_service: notify.notify - static_ip: 192.168.69.115 - gateway: 192.168.69.1 - subnet: 255.255.255.0 +# settings: +# device_name: chore-tracker +# friendly_name: "Chore Tracker" +# wifi_ssid: !secret wifi_iot_ssid +# wifi_password: !secret wifi_password +# api_key: !secret api_encryption_key +# ota_password: !secret ota_password +# reset_time: "00:00:00" +# reminder_time: "18:00" +# notify_service: notify.notify +# static_ip: 192.168.69.115 +# gateway: 192.168.69.1 +# subnet: 255.255.255.0 -# ── KIDS ────────────────────────────────────────────────────────────────────── -# avatar: MDI codepoint shown in the sidebar on the kid's chore page -# color / color_dark: hex colours for that kid's sidebar -# -kids: - - name: Jordyn - avatar: "\U000F011B" # mdi:cat - color: "5F5980" # #5F5980 is a nice light blue, but it's a bit hard to read when used as a background colour for the sidebar. So I use a darker version of the same colour for the sidebar background. - color_dark: "2F3061" # #2F3061 is a darker version of #5F5980, and is easier to read when used as a background colour for the sidebar. - chores: - - name: Put on underwear - icon: "\U000F02E3" # mdi:bed - - name: Brush Teeth - icon: "\U000F09A9" # mdi:toothbrush - - name: Fill water bowls - icon: "\U000F01AB" # mdi:cup-water - - name: Restock Cat Food Cans - icon: "\U000F171C" # mdi:food-turkey +# # ── KIDS ────────────────────────────────────────────────────────────────────── +# # avatar: MDI codepoint shown in the sidebar on the kid's chore page +# # color / color_dark: hex colours for that kid's sidebar +# # +# kids: +# - name: Jordyn +# avatar: "\U000F011B" # mdi:cat +# color: "5F5980" # #5F5980 is a nice light blue, but it's a bit hard to read when used as a background colour for the sidebar. So I use a darker version of the same colour for the sidebar background. +# color_dark: "2F3061" # #2F3061 is a darker version of #5F5980, and is easier to read when used as a background colour for the sidebar. +# chores: +# - name: Put on underwear +# icon: "\U000F02E3" # mdi:bed +# - name: Brush Teeth +# icon: "\U000F09A9" # mdi:toothbrush +# - name: Fill water bowls +# icon: "\U000F01AB" # mdi:cup-water +# - name: Restock Cat Food Cans +# icon: "\U000F171C" # mdi:food-turkey - - name: Declan - avatar: "\U000F1477" # mdi:wizard-hat - color: "720026" # #720026 is a deep red, used for the kid's sidebar background. - color_dark: "4F000B" # #4F000B is a darker version of #720026, easier to read when used as a background colour for the sidebar. - chores: - - name: Take morning pill - icon: "\U000F0402" # mdi:pill - - name: Scoop Dog Poop - icon: "\U000F01F7" # mdi:emoticon-poop +# - name: Declan +# avatar: "\U000F1477" # mdi:wizard-hat +# color: "720026" # #720026 is a deep red, used for the kid's sidebar background. +# color_dark: "4F000B" # #4F000B is a darker version of #720026, easier to read when used as a background colour for the sidebar. +# chores: +# - name: Take morning pill +# icon: "\U000F0402" # mdi:pill +# - name: Scoop Dog Poop +# icon: "\U000F01F7" # mdi:emoticon-poop - - name: Chloe - avatar: "\U000F16A3" # mdi:robot-excited - color: "73683B" # #73683B is a light brown, used for the kid's sidebar background. - color_dark: "583E23" # #583E23 is a darker version of #73683B, easier to read when used as a background colour for the sidebar. - chores: - - name: Fill kitty feeders - icon: "\U000F02A9" # mdi:bowl-outline - - name: Scoop Kitty Litter - icon: "\U000F01F7" # mdi:emoticon-poop - - name: Replace Kitty Litter Bags - icon: "\U000F01F7" # mdi:emoticon-poop \ No newline at end of file +# - name: Chloe +# avatar: "\U000F16A3" # mdi:robot-excited +# color: "73683B" # #73683B is a light brown, used for the kid's sidebar background. +# color_dark: "583E23" # #583E23 is a darker version of #73683B, easier to read when used as a background colour for the sidebar. +# chores: +# - name: Fill kitty feeders +# icon: "\U000F02A9" # mdi:bowl-outline +# - name: Scoop Kitty Litter +# icon: "\U000F01F7" # mdi:emoticon-poop +# - name: Replace Kitty Litter Bags +# icon: "\U000F01F7" # mdi:emoticon-poop \ No newline at end of file diff --git a/esphome/chore-tracker/generate.py b/esphome/chore-tracker/generate.py index 736142b..2cde55c 100644 --- a/esphome/chore-tracker/generate.py +++ b/esphome/chore-tracker/generate.py @@ -1,1011 +1,1011 @@ -#!/usr/bin/env python3 -""" -generate.py — Chore Tracker Code Generator -========================================== -Reads chores_config.yaml and generates: - • chore-tracker-esphome.yaml (flash to ESP32 via ESPHome) - • chore-tracker-ha.yaml (Home Assistant config) - • chore-tracker-dashboard.yaml (Lovelace dashboard) +# #!/usr/bin/env python3 +# """ +# generate.py — Chore Tracker Code Generator +# ========================================== +# Reads chores_config.yaml and generates: +# • chore-tracker-esphome.yaml (flash to ESP32 via ESPHome) +# • chore-tracker-ha.yaml (Home Assistant config) +# • chore-tracker-dashboard.yaml (Lovelace dashboard) -Target hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) +# Target hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) -Home screen behaviour: - - Each kid's button has a RED outline while any chores remain undone - - Button turns SOLID GREEN when ALL chores are complete - - Resets to red automatically at midnight +# Home screen behaviour: +# - Each kid's button has a RED outline while any chores remain undone +# - Button turns SOLID GREEN when ALL chores are complete +# - Resets to red automatically at midnight -Each kid must define their own chores list in chores_config.yaml. +# Each kid must define their own chores list in chores_config.yaml. -Usage: - python3 generate.py - python3 generate.py --config my_other_config.yaml - python3 generate.py --out ./output_dir -""" +# Usage: +# python3 generate.py +# python3 generate.py --config my_other_config.yaml +# python3 generate.py --out ./output_dir +# """ -import argparse -import sys -from pathlib import Path +# import argparse +# import sys +# from pathlib import Path -try: - import yaml -except ImportError: - print("PyYAML not found. Install it with: pip install pyyaml") - sys.exit(1) +# try: +# import yaml +# except ImportError: +# print("PyYAML not found. Install it with: pip install pyyaml") +# sys.exit(1) -# ───────────────────────────────────────────────────────────────────────────── -# Helpers -# ───────────────────────────────────────────────────────────────────────────── +# # ───────────────────────────────────────────────────────────────────────────── +# # Helpers +# # ───────────────────────────────────────────────────────────────────────────── -# Screen padding — space kept clear around all edges (pixels) -PAD = 12 +# # Screen padding — space kept clear around all edges (pixels) +# PAD = 12 -def slug(name: str) -> str: - return name.lower().replace(" ", "_").replace("-", "_") +# def slug(name: str) -> str: +# return name.lower().replace(" ", "_").replace("-", "_") -def kid_slug(kid: dict) -> str: - return slug(kid["name"]) +# def kid_slug(kid: dict) -> str: +# return slug(kid["name"]) -def eid(kid: dict, chore: dict) -> str: - return f"{kid_slug(kid)}_{slug(chore['name'])}" +# def eid(kid: dict, chore: dict) -> str: +# return f"{kid_slug(kid)}_{slug(chore['name'])}" -def ha_switch(kid: dict, chore: dict) -> str: - return f"switch.chore_tracker_{eid(kid, chore)}" +# def ha_switch(kid: dict, chore: dict) -> str: +# return f"switch.chore_tracker_{eid(kid, chore)}" -# mdi:checkbox-marked-circle-outline — safe default if icon is omitted -DEFAULT_ICON = "\U000F0133" +# # mdi:checkbox-marked-circle-outline — safe default if icon is omitted +# DEFAULT_ICON = "\U000F0133" -def get_chores(kid: dict) -> list: - chores = kid["chores"] - for c in chores: - if "icon" not in c: - c["icon"] = DEFAULT_ICON - return chores +# def get_chores(kid: dict) -> list: +# chores = kid["chores"] +# for c in chores: +# if "icon" not in c: +# c["icon"] = DEFAULT_ICON +# return chores -# ───────────────────────────────────────────────────────────────────────────── -# ESPHome generator -# ───────────────────────────────────────────────────────────────────────────── +# # ───────────────────────────────────────────────────────────────────────────── +# # ESPHome generator +# # ───────────────────────────────────────────────────────────────────────────── -def gen_esphome(cfg: dict) -> str: - s = cfg["settings"] - kids = cfg["kids"] +# def gen_esphome(cfg: dict) -> str: +# s = cfg["settings"] +# kids = cfg["kids"] - # ── Switches (one per chore per kid) ────────────────────────────────────── - switch_blocks = [] - for kid in kids: - chores = get_chores(kid) - ks = kid_slug(kid) - switch_blocks.append(f"\n # ── {kid['name']}'s chores ──────────────────────────\n") - for chore in chores: - e = eid(kid, chore) - switch_blocks.append(f"""\ - - platform: template - name: "{kid['name']} - {chore['name']}" - id: {e} - icon: mdi:checkbox-marked-circle - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - then: - - script.execute: update_{ks}_ui - - homeassistant.service: - service: switch.turn_on - data: - entity_id: switch.chore_tracker_{e} - on_turn_off: - then: - - script.execute: update_{ks}_ui - - homeassistant.service: - service: switch.turn_off - data: - entity_id: switch.chore_tracker_{e} -""") +# # ── Switches (one per chore per kid) ────────────────────────────────────── +# switch_blocks = [] +# for kid in kids: +# chores = get_chores(kid) +# ks = kid_slug(kid) +# switch_blocks.append(f"\n # ── {kid['name']}'s chores ──────────────────────────\n") +# for chore in chores: +# e = eid(kid, chore) +# switch_blocks.append(f"""\ +# - platform: template +# name: "{kid['name']} - {chore['name']}" +# id: {e} +# icon: mdi:checkbox-marked-circle +# optimistic: true +# restore_mode: RESTORE_DEFAULT_OFF +# on_turn_on: +# then: +# - script.execute: update_{ks}_ui +# - homeassistant.service: +# service: switch.turn_on +# data: +# entity_id: switch.chore_tracker_{e} +# on_turn_off: +# then: +# - script.execute: update_{ks}_ui +# - homeassistant.service: +# service: switch.turn_off +# data: +# entity_id: switch.chore_tracker_{e} +# """) - # ── Sensors (chores-done count only — no stars) ──────────────────────── - sensor_blocks = [] - for kid in kids: - chores = get_chores(kid) - ks = kid_slug(kid) - done_lines = "\n".join( - f" if (id({eid(kid, c)}).state) done++;" for c in chores - ) - sensor_blocks.append(f"""\ - - platform: template - name: "{kid['name']} Chores Done" - id: {ks}_chores_done - accuracy_decimals: 0 - update_interval: 2s - lambda: |- - int done = 0; -{done_lines} - return done; - # Diagnostic sensors - - platform: wifi_signal - name: "${device_name} WiFi Signal" - update_interval: 60s +# # ── Sensors (chores-done count only — no stars) ──────────────────────── +# sensor_blocks = [] +# for kid in kids: +# chores = get_chores(kid) +# ks = kid_slug(kid) +# done_lines = "\n".join( +# f" if (id({eid(kid, c)}).state) done++;" for c in chores +# ) +# sensor_blocks.append(f"""\ +# - platform: template +# name: "{kid['name']} Chores Done" +# id: {ks}_chores_done +# accuracy_decimals: 0 +# update_interval: 2s +# lambda: |- +# int done = 0; +# {done_lines} +# return done; +# # Diagnostic sensors +# - platform: wifi_signal +# name: "${device_name} WiFi Signal" +# update_interval: 60s - - platform: uptime - name: "${device_name} Uptime" - update_interval: 60s -""") - - # ── Midnight reset calls ────────────────────────────────────────────────── - reset_calls = "\n".join( - f" id(reset_{kid_slug(k)}_chores).execute();" - for k in kids - ) - - lvgl_pages = _gen_lvgl_pages(kids) - scripts = _gen_scripts(kids) - - # Collect all unique MDI glyphs used (avatars + chore icons) - all_glyphs = sorted(set( - [k["avatar"] for k in kids] + - [c["icon"] for k in kids for c in get_chores(k)] - )) - glyphs_str = "".join(all_glyphs) - - return f"""\ -################################################################################ -# chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml) -# Kids: {", ".join(k["name"] for k in kids)} -# -# HOME SCREEN BEHAVIOUR: -# Red outline = chores incomplete -# Solid green = all chores done -# Resets to red automatically at midnight -# -# Hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) -################################################################################ -substitutions: - device_name: {s["device_name"]} - friendly_name: "{s["friendly_name"]}" - -esphome: - name: {s["device_name"]} - friendly_name: "{s["friendly_name"]}" - on_boot: - priority: -10 - then: - - light.turn_on: backlight - - lvgl.page.show: page_home - -esp32: - board: esp32-s3-devkitc-1 - framework: - type: esp-idf - -psram: - mode: octal - speed: 80MHz - -wifi: - ssid: {s["wifi_ssid"]} - password: {s["wifi_password"]} - manual_ip: - static_ip: {s["static_ip"]} - gateway: {s["gateway"]} - subnet: {s["subnet"]} - ap: - ssid: "{s['friendly_name']} Hotspot" - password: "choretracker" - - # Prevents aggressive reconnection attempts - power_save_mode: none # Use 'light' for battery devices - - # Slower but more reliable connection - fast_connect: true - - # Handle connection failures gracefully - on_connect: - - logger.log: "Wi-Fi connected!" - on_disconnect: - - logger.log: "Wi-Fi disconnected!" - -mdns: - disabled: true - -safe_mode: - disabled: true - -logger: - level: INFO - -api: - encryption: - key: {s["api_key"]} - reboot_timeout: 0s - -ota: - - platform: esphome - password: {s["ota_password"]} - -web_server: - port: 8080 - -# ── I2C ─────────────────────────────────────────────────────────────────────── -i2c: - sda: 8 - scl: 9 - -# ── CH422G IO Expander (controls LCD reset, touch reset, backlight) ─────────── -ch422g: - - id: ch422g_hub - -# ── 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 - -# ── Touchscreen ─────────────────────────────────────────────────────────────── -touchscreen: - platform: gt911 - id: touch - update_interval: 120ms - reset_pin: - ch422g: ch422g_hub - number: 1 - mode: - output: true - -# ── 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 - -light: - - platform: binary - name: "Display Backlight" - output: lcd_backlight_out - id: backlight - -# ── Midnight reset ──────────────────────────────────────────────────────────── -time: - - platform: homeassistant - id: ha_time - on_time: - - hours: 0 - minutes: 0 - seconds: 0 - then: - - lambda: |- -{reset_calls} - - lvgl.page.show: page_home - -# ── Fonts (place files in /config/esphome/fonts/) ──────────────────────────── -font: - - file: "fonts/Nunito-Black.ttf" - id: font_title - size: 40 - - file: "fonts/Nunito-ExtraBold.ttf" - id: font_name - size: 30 - - file: "fonts/Nunito-Bold.ttf" - id: font_med - size: 22 - - file: "fonts/Nunito-SemiBold.ttf" - id: font_small - size: 17 - - file: "fonts/Nunito-SemiBold.ttf" - id: font_tiny - size: 13 - # MDI icon font — used for chore icons and kid avatars - - file: "fonts/materialdesignicons-webfont.ttf" - id: font_mdi_large - size: 48 - bpp: 4 - glyphs: {glyphs_str} - - file: "fonts/materialdesignicons-webfont.ttf" - id: font_mdi_small - size: 32 - bpp: 4 - glyphs: {glyphs_str} - -# ── Switches — backlight raw + one per chore per kid ───────────────────────── -switch: - - platform: gpio - id: lcd_backlight_raw - name: "LCD Backlight Raw" - restore_mode: ALWAYS_ON - pin: - ch422g: ch422g_hub - number: 2 - mode: - output: true -{"".join(switch_blocks)} - -# ── Sensors — reported to HA ───────────────────────────────────────────────── -sensor: -{"".join(sensor_blocks)} - -# ── Text Sensors — reported to HA ───────────────────────────────────────────────── -text_sensor: - - platform: wifi_info - ip_address: - name: "${device_name} IP Address" - ssid: - name: "${device_name} Connected SSID" - bssid: - name: "${device_name} BSSID" - -# Watchdog to auto-reboot if things go wrong -# Note: interval fires on first tick (t≈0), so guard with millis() to ensure -# the device has actually been running for 5+ minutes before triggering. -interval: - - interval: 5min - then: - - if: - condition: - and: - - lambda: 'return millis() > 300000;' # >5 min uptime - - not: - wifi.connected: - then: - - logger.log: "Wi-Fi not connected for 5 min, rebooting..." - - delay: 10s - - lambda: 'App.safe_reboot();' - -# ── Scripts — reset + UI update ─────────────────────────────────────────────── -script: -{scripts} - -# ── LVGL UI ─────────────────────────────────────────────────────────────────── -lvgl: - displays: - - main_display - touchscreens: - - touch - theme: - button: - radius: 20 - border_width: 0 - pages: -{lvgl_pages} -""" - - -# ───────────────────────────────────────────────────────────────────────────── -# LVGL page builder -# ───────────────────────────────────────────────────────────────────────────── - -def _gen_lvgl_pages(kids: list) -> str: - pages = [] - - # ── HOME PAGE ───────────────────────────────────────────────────────────── - n = len(kids) - usable_w = 800 - PAD * 2 - btn_w = min(200, max(140, (usable_w - 40 - 20 * (n - 1)) // n)) - total_w = btn_w * n + 20 * (n - 1) - start_x = PAD + (usable_w - total_w) // 2 - btn_y = PAD + 118 - btn_h = 210 - - reset_all_calls = "\n".join( - f" - script.execute: reset_{kid_slug(k)}_chores" - for k in kids - ) - - home_btns = "" - for i, kid in enumerate(kids): - x = start_x + i * (btn_w + 20) - ks = kid_slug(kid) - home_btns += f"""\ - - button: - id: home_btn_{ks} - x: {x} - y: {btn_y} - width: {btn_w} - height: {btn_h} - bg_color: 0xFF4757 - bg_opa: COVER - border_width: 0 - radius: 24 - on_click: - then: - - lvgl.page.show: page_{ks} - widgets: - - label: - align: CENTER - y: -45 - text: "{kid['avatar']}" - text_font: font_mdi_large - - label: - align: CENTER - y: 22 - text: "{kid['name']}" - text_font: font_name - text_color: 0xFFFFFF - - label: - id: home_status_{ks} - align: CENTER - y: 66 - text: "not done" - text_font: font_tiny - text_color: 0xFFFFFF - -""" - - pages.append(f"""\ - - id: page_home - bg_color: 0xFFF9F0 - widgets: - - label: - x: 0 - y: 36 - width: {800 - PAD * 2} - align: TOP_MID - text: "Chore Tracker" - text_font: font_title - text_color: 0x2D3436 - - label: - x: 0 - y: 90 - width: {800 - PAD * 2} - align: TOP_MID - text: "Tap a name to check off chores" - text_font: font_small - text_color: 0xB2BEC3 - - button: - x: 640 - y: 428 - width: 148 - height: 40 - bg_color: 0xFF4757 - radius: 12 - on_click: - then: -{reset_all_calls} - widgets: - - label: - align: CENTER - text: "Reset All" - text_color: 0xFFFFFF - text_font: font_tiny -{home_btns}""") - - # ── PER-KID CHORE PAGES ─────────────────────────────────────────────────── - for kid in kids: - chores = get_chores(kid) - ks = kid_slug(kid) - n_chores = len(chores) - - cols = 3 - rows = (n_chores + cols - 1) // cols - sidebar_w = 172 - gap = 10 - content_w = 800 - sidebar_w - gap - 12 # right pad - card_w = (content_w - gap * (cols - 1)) // cols - card_h = min(138, (480 - PAD - gap * (rows + 1)) // rows) - - card_widgets = "" - for idx, chore in enumerate(chores): - row = idx // cols - col = idx % cols - x = sidebar_w + gap + col * (card_w + gap) - y = 12 + row * (card_h + gap) - e = eid(kid, chore) - icon_y = -(card_h // 4) - label_y = card_h // 8 - card_widgets += f"""\ - - button: - id: card_{e} - x: {x} - y: {y} - width: {card_w} - height: {card_h} - bg_color: 0xFFFFFF - border_color: 0xEEEEEE - border_width: 2 - radius: 20 - shadow_color: 0xCCCCCC - shadow_width: 4 - shadow_ofs_y: 3 - on_click: - then: - - switch.toggle: {e} - widgets: - - label: - id: check_{e} - align: TOP_RIGHT - x: -10 - y: 8 - text: "" - text_font: font_med - - label: - align: CENTER - y: {icon_y} - text: "{chore['icon']}" - text_font: font_mdi_small - text_color: 0x{kid['color']} - - label: - align: CENTER - y: {label_y} - text: "{chore['name']}" - text_font: font_small - text_color: 0x2D3436 - -""" - - pages.append(f"""\ - - id: page_{ks} - bg_color: 0xFFF9F0 - widgets: - - # ── Sidebar ────────────────────────────────────────────────────────── - - obj: - x: 0 - y: 0 - width: {sidebar_w} - height: 480 - bg_color: 0x{kid['color']} - border_width: 0 - radius: 0 - pad_all: 0 - widgets: - - label: - x: 0 - y: 22 - width: {sidebar_w} - align: TOP_MID - text: "{kid['avatar']}" - text_font: font_mdi_large - - label: - x: 0 - y: 76 - width: {sidebar_w} - align: TOP_MID - text: "{kid['name']}" - text_font: font_name - text_color: 0xFFFFFF - - bar: - id: progress_bar_{ks} - x: 14 - y: 132 - width: {sidebar_w - 28} - height: 16 - bg_color: 0x{kid['color_dark']} - indicator: - bg_color: 0xFFFFFF - value: 0 - min_value: 0 - max_value: {n_chores} - - label: - id: progress_label_{ks} - x: 0 - y: 156 - width: {sidebar_w} - align: TOP_MID - text: "0 / {n_chores}" - text_font: font_small - text_color: 0xDDEEFF - - label: - id: all_done_label_{ks} - x: 0 - y: 192 - width: {sidebar_w} - align: TOP_MID - text: "" - text_font: font_small - text_color: 0xFFFFFF - - button: - x: 14 - y: {480 - PAD - 44 - 6 - 44} - width: {sidebar_w - 28} - height: 44 - bg_color: 0xFF4757 - radius: 14 - on_click: - then: - - script.execute: reset_{ks}_chores - widgets: - - label: - align: CENTER - text: "Reset" - text_color: 0xFFFFFF - text_font: font_small - - button: - x: 14 - y: {480 - PAD - 44} - width: {sidebar_w - 28} - height: 44 - bg_color: 0x{kid['color_dark']} - radius: 14 - on_click: - then: - - lvgl.page.show: page_home - widgets: - - label: - align: CENTER - text: "Home" - text_color: 0xFFFFFF - text_font: font_small - - # ── Chore cards ─────────────────────────────────────────────────────── -{card_widgets}""") - - return "\n".join(pages) - - -# ───────────────────────────────────────────────────────────────────────────── -# Script builder -# ───────────────────────────────────────────────────────────────────────────── - -def _gen_scripts(kids: list) -> str: - blocks = [] - for kid in kids: - chores = get_chores(kid) - ks = kid_slug(kid) - n = len(chores) - - card_updates = "" - for chore in chores: - e = eid(kid, chore) - card_updates += f"""\ - if (id({e}).state) {{ - lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xF0FFF4), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_{e}), 3, LV_PART_MAIN); - lv_label_set_text(id(check_{e}), "\\u2705"); - }} else {{ - lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0xEEEEEE), LV_PART_MAIN); - lv_obj_set_style_border_width(id(card_{e}), 2, LV_PART_MAIN); - lv_label_set_text(id(check_{e}), ""); - }} -""" - - count_done = " + ".join(f"(id({eid(kid,c)}).state ? 1 : 0)" for c in chores) - reset_lines = "\n".join( - f" id({eid(kid,c)}).turn_off();" for c in chores - ) - - blocks.append(f"""\ - - id: reset_{ks}_chores - mode: single - then: - - lambda: |- -{reset_lines} - - script.execute: update_{ks}_ui - -""") - - blocks.append(f"""\ - - id: update_{ks}_ui - mode: single - then: - - lambda: |- - int done = {count_done}; - int total = {n}; - char buf[24]; - - // Progress bar + label - lv_bar_set_value(id(progress_bar_{ks}), done, LV_ANIM_ON); - snprintf(buf, sizeof(buf), "%d / {n}", done); - lv_label_set_text(id(progress_label_{ks}), buf); - - // All-done message in sidebar - lv_label_set_text(id(all_done_label_{ks}), done == total ? "All done!" : ""); - - // Home button: RED outline = incomplete, SOLID GREEN = all done - if (done == total) {{ - lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0x6BCB77), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN); - lv_label_set_text(id(home_status_{ks}), "\\u2713 all done!"); - lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFFFFFF), LV_PART_MAIN); - }} else {{ - lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN); - lv_obj_set_style_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN); - snprintf(buf, sizeof(buf), "%d left", total - done); - lv_label_set_text(id(home_status_{ks}), buf); - lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN); - }} - - // Card colours -{card_updates} -""") - - return "\n".join(blocks) - - -# ───────────────────────────────────────────────────────────────────────────── -# Home Assistant YAML generator -# ───────────────────────────────────────────────────────────────────────────── - -def gen_ha(cfg: dict) -> str: - s = cfg["settings"] - kids = cfg["kids"] - - sensor_blocks = [] - for kid in kids: - chores = get_chores(kid) - ks = kid_slug(kid) - n = len(chores) - states_list = ", ".join(f"states('{ha_switch(kid,c)}')" for c in chores) - - sensor_blocks.append(f"""\ - - name: "{kid['name']} Chores Done Today" - unique_id: {ks}_chores_done_today - icon: mdi:check-circle - state: > - {{% set chores = [{states_list}] %}} - {{{{ chores | select('equalto', 'on') | list | count }}}} - - - name: "{kid['name']} All Chores Done" - unique_id: {ks}_all_chores_done - icon: mdi:check-all - state: > - {{{{ states('sensor.{ks}_chores_done_today') | int == {n} }}}} - -""") - - input_buttons = "".join(f"""\ - {kid_slug(k)}_reset_chores: - name: "Reset {k['name']}'s Chores" - icon: mdi:restart -""" for k in kids) - input_buttons += """\ - reset_all_chores: - name: "Reset All Chores" - icon: mdi:restart-alert -""" - - auto_blocks = [] - - for kid in kids: - ks = kid_slug(kid) - off_calls = "\n".join( - f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}" - for c in get_chores(kid) - ) - auto_blocks.append(f"""\ - - id: {ks}_reset_from_ha - alias: "Chore Tracker - Reset {kid['name']}'s chores from HA" - trigger: - - platform: state - entity_id: input_button.{ks}_reset_chores - action: -{off_calls} - -""") - - all_off = "\n".join( - f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}" - for kid in kids for c in get_chores(kid) - ) - auto_blocks.append(f"""\ - - id: reset_all_from_ha - alias: "Chore Tracker - Reset ALL chores from HA" - trigger: - - platform: state - entity_id: input_button.reset_all_chores - action: -{all_off} - -""") - - for kid in kids: - ks = kid_slug(kid) - n = len(get_chores(kid)) - auto_blocks.append(f"""\ - - id: {ks}_all_done_notify - alias: "Chore Tracker - {kid['name']} all done!" - trigger: - - platform: state - entity_id: sensor.{ks}_all_chores_done - to: "True" - action: - - service: {s['notify_service']} - data: - title: "{kid['name']} finished all chores!" - message: "{kid['name']} completed all {n} chores today!" - -""") - - reminder_actions = "".join(f"""\ - - if: - condition: template - value_template: "{{{{ states('sensor.{kid_slug(k)}_all_chores_done') != 'True' }}}}" - then: - - service: {s['notify_service']} - data: - title: "{k['name']} has unfinished chores" - message: > - {k['name']} has done - {{{{ states('sensor.{kid_slug(k)}_chores_done_today') }}}}/{len(get_chores(k))} chores today. -""" for k in kids) - - auto_blocks.append(f"""\ - - id: chore_reminder_evening - alias: "Chore Tracker - Evening reminder" - trigger: - - platform: time - at: "{s['reminder_time']}:00" - action: -{reminder_actions} -""") - - return f"""\ -################################################################################ -# chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml) -# Kids: {", ".join(k["name"] for k in kids)} -# -# BIDIRECTIONAL SYNC: -# Screen to HA: Each switch calls homeassistant.service on toggle -# HA to Screen: ESPHome native API handles this automatically -################################################################################ - -input_button: -{input_buttons} -template: - - sensor: -{"".join(sensor_blocks)} -automation: -{"".join(auto_blocks)} -""" - - -# ───────────────────────────────────────────────────────────────────────────── -# Lovelace dashboard generator -# ───────────────────────────────────────────────────────────────────────────── - -def gen_dashboard(cfg: dict) -> str: - kids = cfg["kids"] - - summary_cards = "\n".join(f"""\ - - type: markdown - content: > - ### {kid['avatar']} {kid['name']} - {{{{ - 'All done!' if states('sensor.{kid_slug(kid)}_all_chores_done') == 'True' - else states('sensor.{kid_slug(kid)}_chores_done_today') ~ '/{len(get_chores(kid))} chores done' - }}}} -""" for kid in kids) - - chore_cards = [] - for kid in kids: - chores = get_chores(kid) - ks = kid_slug(kid) - chore_rows = "\n".join(f"""\ - - entity: {ha_switch(kid, c)} - name: "{c['icon']} {c['name']}" -""" for c in chores) - - chore_cards.append(f"""\ - - type: entities - title: "{kid['avatar']} {kid['name']}'s Chores" - entities: -{chore_rows} - type: divider - - entity: sensor.{ks}_chores_done_today - name: "Chores done today" - - entity: sensor.{ks}_all_chores_done - name: "All Done?" - - type: button - name: "Reset {kid['name']}'s Chores" - tap_action: - action: call-service - service: input_button.press - target: - entity_id: input_button.{ks}_reset_chores -""") - - return f"""\ -################################################################################ -# chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml) -# Kids: {", ".join(k["name"] for k in kids)} -################################################################################ - -title: "Chore Tracker" -views: - - title: Today - icon: mdi:checkbox-marked-circle - path: chores - cards: - - type: horizontal-stack - cards: -{summary_cards} - - type: button - name: "Reset ALL Chores" - icon: mdi:restart-alert - tap_action: - action: call-service - service: input_button.press - target: - entity_id: input_button.reset_all_chores -{"".join(chore_cards)} -""" - - -# ───────────────────────────────────────────────────────────────────────────── -# Main -# ───────────────────────────────────────────────────────────────────────────── - -def main(): - parser = argparse.ArgumentParser(description="Chore Tracker config generator") - parser.add_argument("--config", default="chores_config.yaml") - parser.add_argument("--out", default=".") - args = parser.parse_args() - - config_path = Path(args.config) - if not config_path.exists(): - print(f"❌ Config not found: {config_path}") - sys.exit(1) - - out_dir = Path(args.out) - out_dir.mkdir(parents=True, exist_ok=True) - - class SecretLoader(yaml.SafeLoader): - pass - SecretLoader.add_constructor( - "!secret", - lambda loader, node: f"!secret {loader.construct_scalar(node)}" - ) - - with open(config_path) as f: - cfg = yaml.load(f, Loader=SecretLoader) - - kids = cfg.get("kids", []) - if not kids: - print("❌ No kids defined in config!") - sys.exit(1) - - errors = [k["name"] for k in kids if "chores" not in k or not k["chores"]] - if errors: - print(f"❌ These kids have no chores defined: {', '.join(errors)}") - print(" Add a 'chores:' list under each kid in chores_config.yaml") - sys.exit(1) - - # Warn about missing icons (will use default, not crash) - for k in kids: - for c in k.get("chores", []): - if "icon" not in c: - print(f"⚠️ {k['name']} / '{c['name']}' has no icon — using default") - - total_chores = sum(len(get_chores(k)) for k in kids) - print(f"✅ Config loaded: {len(kids)} kid(s), {total_chores} total chores") - for k in kids: - chores = get_chores(k) - print(f" {k['avatar']} {k['name']}: {len(chores)} chores" - f" — {', '.join(c['name'] for c in chores)}") - print() - - files = { - "chore-tracker-esphome.yaml": gen_esphome(cfg), - "chore-tracker-ha.yaml": gen_ha(cfg), - "chore-tracker-dashboard.yaml": gen_dashboard(cfg), - } - - for name, content in files.items(): - path = out_dir / name - path.write_text(content) - print(f"📄 {path}") - - print() - print("✅ Done!") - print(" 1. Flash chore-tracker-esphome.yaml -> ESPHome dashboard") - print(" 2. Merge chore-tracker-ha.yaml -> HA config + restart HA") - print(" 3. Paste chore-tracker-dashboard.yaml -> New HA dashboard") - - -if __name__ == "__main__": - main() \ No newline at end of file +# - platform: uptime +# name: "${device_name} Uptime" +# update_interval: 60s +# """) + +# # ── Midnight reset calls ────────────────────────────────────────────────── +# reset_calls = "\n".join( +# f" id(reset_{kid_slug(k)}_chores).execute();" +# for k in kids +# ) + +# lvgl_pages = _gen_lvgl_pages(kids) +# scripts = _gen_scripts(kids) + +# # Collect all unique MDI glyphs used (avatars + chore icons) +# all_glyphs = sorted(set( +# [k["avatar"] for k in kids] + +# [c["icon"] for k in kids for c in get_chores(k)] +# )) +# glyphs_str = "".join(all_glyphs) + +# return f"""\ +# ################################################################################ +# # chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml) +# # Kids: {", ".join(k["name"] for k in kids)} +# # +# # HOME SCREEN BEHAVIOUR: +# # Red outline = chores incomplete +# # Solid green = all chores done +# # Resets to red automatically at midnight +# # +# # Hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) +# ################################################################################ +# substitutions: +# device_name: {s["device_name"]} +# friendly_name: "{s["friendly_name"]}" + +# esphome: +# name: {s["device_name"]} +# friendly_name: "{s["friendly_name"]}" +# on_boot: +# priority: -10 +# then: +# - light.turn_on: backlight +# - lvgl.page.show: page_home + +# esp32: +# board: esp32-s3-devkitc-1 +# framework: +# type: esp-idf + +# psram: +# mode: octal +# speed: 80MHz + +# wifi: +# ssid: {s["wifi_ssid"]} +# password: {s["wifi_password"]} +# manual_ip: +# static_ip: {s["static_ip"]} +# gateway: {s["gateway"]} +# subnet: {s["subnet"]} +# ap: +# ssid: "{s['friendly_name']} Hotspot" +# password: "choretracker" + +# # Prevents aggressive reconnection attempts +# power_save_mode: none # Use 'light' for battery devices + +# # Slower but more reliable connection +# fast_connect: true + +# # Handle connection failures gracefully +# on_connect: +# - logger.log: "Wi-Fi connected!" +# on_disconnect: +# - logger.log: "Wi-Fi disconnected!" + +# mdns: +# disabled: true + +# safe_mode: +# disabled: true + +# logger: +# level: INFO + +# api: +# encryption: +# key: {s["api_key"]} +# reboot_timeout: 0s + +# ota: +# - platform: esphome +# password: {s["ota_password"]} + +# web_server: +# port: 8080 + +# # ── I2C ─────────────────────────────────────────────────────────────────────── +# i2c: +# sda: 8 +# scl: 9 + +# # ── CH422G IO Expander (controls LCD reset, touch reset, backlight) ─────────── +# ch422g: +# - id: ch422g_hub + +# # ── 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 + +# # ── Touchscreen ─────────────────────────────────────────────────────────────── +# touchscreen: +# platform: gt911 +# id: touch +# update_interval: 120ms +# reset_pin: +# ch422g: ch422g_hub +# number: 1 +# mode: +# output: true + +# # ── 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 + +# light: +# - platform: binary +# name: "Display Backlight" +# output: lcd_backlight_out +# id: backlight + +# # ── Midnight reset ──────────────────────────────────────────────────────────── +# time: +# - platform: homeassistant +# id: ha_time +# on_time: +# - hours: 0 +# minutes: 0 +# seconds: 0 +# then: +# - lambda: |- +# {reset_calls} +# - lvgl.page.show: page_home + +# # ── Fonts (place files in /config/esphome/fonts/) ──────────────────────────── +# font: +# - file: "fonts/Nunito-Black.ttf" +# id: font_title +# size: 40 +# - file: "fonts/Nunito-ExtraBold.ttf" +# id: font_name +# size: 30 +# - file: "fonts/Nunito-Bold.ttf" +# id: font_med +# size: 22 +# - file: "fonts/Nunito-SemiBold.ttf" +# id: font_small +# size: 17 +# - file: "fonts/Nunito-SemiBold.ttf" +# id: font_tiny +# size: 13 +# # MDI icon font — used for chore icons and kid avatars +# - file: "fonts/materialdesignicons-webfont.ttf" +# id: font_mdi_large +# size: 48 +# bpp: 4 +# glyphs: {glyphs_str} +# - file: "fonts/materialdesignicons-webfont.ttf" +# id: font_mdi_small +# size: 32 +# bpp: 4 +# glyphs: {glyphs_str} + +# # ── Switches — backlight raw + one per chore per kid ───────────────────────── +# switch: +# - platform: gpio +# id: lcd_backlight_raw +# name: "LCD Backlight Raw" +# restore_mode: ALWAYS_ON +# pin: +# ch422g: ch422g_hub +# number: 2 +# mode: +# output: true +# {"".join(switch_blocks)} + +# # ── Sensors — reported to HA ───────────────────────────────────────────────── +# sensor: +# {"".join(sensor_blocks)} + +# # ── Text Sensors — reported to HA ───────────────────────────────────────────────── +# text_sensor: +# - platform: wifi_info +# ip_address: +# name: "${device_name} IP Address" +# ssid: +# name: "${device_name} Connected SSID" +# bssid: +# name: "${device_name} BSSID" + +# # Watchdog to auto-reboot if things go wrong +# # Note: interval fires on first tick (t≈0), so guard with millis() to ensure +# # the device has actually been running for 5+ minutes before triggering. +# interval: +# - interval: 5min +# then: +# - if: +# condition: +# and: +# - lambda: 'return millis() > 300000;' # >5 min uptime +# - not: +# wifi.connected: +# then: +# - logger.log: "Wi-Fi not connected for 5 min, rebooting..." +# - delay: 10s +# - lambda: 'App.safe_reboot();' + +# # ── Scripts — reset + UI update ─────────────────────────────────────────────── +# script: +# {scripts} + +# # ── LVGL UI ─────────────────────────────────────────────────────────────────── +# lvgl: +# displays: +# - main_display +# touchscreens: +# - touch +# theme: +# button: +# radius: 20 +# border_width: 0 +# pages: +# {lvgl_pages} +# """ + + +# # ───────────────────────────────────────────────────────────────────────────── +# # LVGL page builder +# # ───────────────────────────────────────────────────────────────────────────── + +# def _gen_lvgl_pages(kids: list) -> str: +# pages = [] + +# # ── HOME PAGE ───────────────────────────────────────────────────────────── +# n = len(kids) +# usable_w = 800 - PAD * 2 +# btn_w = min(200, max(140, (usable_w - 40 - 20 * (n - 1)) // n)) +# total_w = btn_w * n + 20 * (n - 1) +# start_x = PAD + (usable_w - total_w) // 2 +# btn_y = PAD + 118 +# btn_h = 210 + +# reset_all_calls = "\n".join( +# f" - script.execute: reset_{kid_slug(k)}_chores" +# for k in kids +# ) + +# home_btns = "" +# for i, kid in enumerate(kids): +# x = start_x + i * (btn_w + 20) +# ks = kid_slug(kid) +# home_btns += f"""\ +# - button: +# id: home_btn_{ks} +# x: {x} +# y: {btn_y} +# width: {btn_w} +# height: {btn_h} +# bg_color: 0xFF4757 +# bg_opa: COVER +# border_width: 0 +# radius: 24 +# on_click: +# then: +# - lvgl.page.show: page_{ks} +# widgets: +# - label: +# align: CENTER +# y: -45 +# text: "{kid['avatar']}" +# text_font: font_mdi_large +# - label: +# align: CENTER +# y: 22 +# text: "{kid['name']}" +# text_font: font_name +# text_color: 0xFFFFFF +# - label: +# id: home_status_{ks} +# align: CENTER +# y: 66 +# text: "not done" +# text_font: font_tiny +# text_color: 0xFFFFFF + +# """ + +# pages.append(f"""\ +# - id: page_home +# bg_color: 0xFFF9F0 +# widgets: +# - label: +# x: 0 +# y: 36 +# width: {800 - PAD * 2} +# align: TOP_MID +# text: "Chore Tracker" +# text_font: font_title +# text_color: 0x2D3436 +# - label: +# x: 0 +# y: 90 +# width: {800 - PAD * 2} +# align: TOP_MID +# text: "Tap a name to check off chores" +# text_font: font_small +# text_color: 0xB2BEC3 +# - button: +# x: 640 +# y: 428 +# width: 148 +# height: 40 +# bg_color: 0xFF4757 +# radius: 12 +# on_click: +# then: +# {reset_all_calls} +# widgets: +# - label: +# align: CENTER +# text: "Reset All" +# text_color: 0xFFFFFF +# text_font: font_tiny +# {home_btns}""") + +# # ── PER-KID CHORE PAGES ─────────────────────────────────────────────────── +# for kid in kids: +# chores = get_chores(kid) +# ks = kid_slug(kid) +# n_chores = len(chores) + +# cols = 3 +# rows = (n_chores + cols - 1) // cols +# sidebar_w = 172 +# gap = 10 +# content_w = 800 - sidebar_w - gap - 12 # right pad +# card_w = (content_w - gap * (cols - 1)) // cols +# card_h = min(138, (480 - PAD - gap * (rows + 1)) // rows) + +# card_widgets = "" +# for idx, chore in enumerate(chores): +# row = idx // cols +# col = idx % cols +# x = sidebar_w + gap + col * (card_w + gap) +# y = 12 + row * (card_h + gap) +# e = eid(kid, chore) +# icon_y = -(card_h // 4) +# label_y = card_h // 8 +# card_widgets += f"""\ +# - button: +# id: card_{e} +# x: {x} +# y: {y} +# width: {card_w} +# height: {card_h} +# bg_color: 0xFFFFFF +# border_color: 0xEEEEEE +# border_width: 2 +# radius: 20 +# shadow_color: 0xCCCCCC +# shadow_width: 4 +# shadow_ofs_y: 3 +# on_click: +# then: +# - switch.toggle: {e} +# widgets: +# - label: +# id: check_{e} +# align: TOP_RIGHT +# x: -10 +# y: 8 +# text: "" +# text_font: font_med +# - label: +# align: CENTER +# y: {icon_y} +# text: "{chore['icon']}" +# text_font: font_mdi_small +# text_color: 0x{kid['color']} +# - label: +# align: CENTER +# y: {label_y} +# text: "{chore['name']}" +# text_font: font_small +# text_color: 0x2D3436 + +# """ + +# pages.append(f"""\ +# - id: page_{ks} +# bg_color: 0xFFF9F0 +# widgets: + +# # ── Sidebar ────────────────────────────────────────────────────────── +# - obj: +# x: 0 +# y: 0 +# width: {sidebar_w} +# height: 480 +# bg_color: 0x{kid['color']} +# border_width: 0 +# radius: 0 +# pad_all: 0 +# widgets: +# - label: +# x: 0 +# y: 22 +# width: {sidebar_w} +# align: TOP_MID +# text: "{kid['avatar']}" +# text_font: font_mdi_large +# - label: +# x: 0 +# y: 76 +# width: {sidebar_w} +# align: TOP_MID +# text: "{kid['name']}" +# text_font: font_name +# text_color: 0xFFFFFF +# - bar: +# id: progress_bar_{ks} +# x: 14 +# y: 132 +# width: {sidebar_w - 28} +# height: 16 +# bg_color: 0x{kid['color_dark']} +# indicator: +# bg_color: 0xFFFFFF +# value: 0 +# min_value: 0 +# max_value: {n_chores} +# - label: +# id: progress_label_{ks} +# x: 0 +# y: 156 +# width: {sidebar_w} +# align: TOP_MID +# text: "0 / {n_chores}" +# text_font: font_small +# text_color: 0xDDEEFF +# - label: +# id: all_done_label_{ks} +# x: 0 +# y: 192 +# width: {sidebar_w} +# align: TOP_MID +# text: "" +# text_font: font_small +# text_color: 0xFFFFFF +# - button: +# x: 14 +# y: {480 - PAD - 44 - 6 - 44} +# width: {sidebar_w - 28} +# height: 44 +# bg_color: 0xFF4757 +# radius: 14 +# on_click: +# then: +# - script.execute: reset_{ks}_chores +# widgets: +# - label: +# align: CENTER +# text: "Reset" +# text_color: 0xFFFFFF +# text_font: font_small +# - button: +# x: 14 +# y: {480 - PAD - 44} +# width: {sidebar_w - 28} +# height: 44 +# bg_color: 0x{kid['color_dark']} +# radius: 14 +# on_click: +# then: +# - lvgl.page.show: page_home +# widgets: +# - label: +# align: CENTER +# text: "Home" +# text_color: 0xFFFFFF +# text_font: font_small + +# # ── Chore cards ─────────────────────────────────────────────────────── +# {card_widgets}""") + +# return "\n".join(pages) + + +# # ───────────────────────────────────────────────────────────────────────────── +# # Script builder +# # ───────────────────────────────────────────────────────────────────────────── + +# def _gen_scripts(kids: list) -> str: +# blocks = [] +# for kid in kids: +# chores = get_chores(kid) +# ks = kid_slug(kid) +# n = len(chores) + +# card_updates = "" +# for chore in chores: +# e = eid(kid, chore) +# card_updates += f"""\ +# if (id({e}).state) {{ +# lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xF0FFF4), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_{e}), 3, LV_PART_MAIN); +# lv_label_set_text(id(check_{e}), "\\u2705"); +# }} else {{ +# lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0xEEEEEE), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(card_{e}), 2, LV_PART_MAIN); +# lv_label_set_text(id(check_{e}), ""); +# }} +# """ + +# count_done = " + ".join(f"(id({eid(kid,c)}).state ? 1 : 0)" for c in chores) +# reset_lines = "\n".join( +# f" id({eid(kid,c)}).turn_off();" for c in chores +# ) + +# blocks.append(f"""\ +# - id: reset_{ks}_chores +# mode: single +# then: +# - lambda: |- +# {reset_lines} +# - script.execute: update_{ks}_ui + +# """) + +# blocks.append(f"""\ +# - id: update_{ks}_ui +# mode: single +# then: +# - lambda: |- +# int done = {count_done}; +# int total = {n}; +# char buf[24]; + +# // Progress bar + label +# lv_bar_set_value(id(progress_bar_{ks}), done, LV_ANIM_ON); +# snprintf(buf, sizeof(buf), "%d / {n}", done); +# lv_label_set_text(id(progress_label_{ks}), buf); + +# // All-done message in sidebar +# lv_label_set_text(id(all_done_label_{ks}), done == total ? "All done!" : ""); + +# // Home button: RED outline = incomplete, SOLID GREEN = all done +# if (done == total) {{ +# lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0x6BCB77), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN); +# lv_label_set_text(id(home_status_{ks}), "\\u2713 all done!"); +# lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFFFFFF), LV_PART_MAIN); +# }} else {{ +# lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN); +# lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN); +# lv_obj_set_style_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN); +# snprintf(buf, sizeof(buf), "%d left", total - done); +# lv_label_set_text(id(home_status_{ks}), buf); +# lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN); +# }} + +# // Card colours +# {card_updates} +# """) + +# return "\n".join(blocks) + + +# # ───────────────────────────────────────────────────────────────────────────── +# # Home Assistant YAML generator +# # ───────────────────────────────────────────────────────────────────────────── + +# def gen_ha(cfg: dict) -> str: +# s = cfg["settings"] +# kids = cfg["kids"] + +# sensor_blocks = [] +# for kid in kids: +# chores = get_chores(kid) +# ks = kid_slug(kid) +# n = len(chores) +# states_list = ", ".join(f"states('{ha_switch(kid,c)}')" for c in chores) + +# sensor_blocks.append(f"""\ +# - name: "{kid['name']} Chores Done Today" +# unique_id: {ks}_chores_done_today +# icon: mdi:check-circle +# state: > +# {{% set chores = [{states_list}] %}} +# {{{{ chores | select('equalto', 'on') | list | count }}}} + +# - name: "{kid['name']} All Chores Done" +# unique_id: {ks}_all_chores_done +# icon: mdi:check-all +# state: > +# {{{{ states('sensor.{ks}_chores_done_today') | int == {n} }}}} + +# """) + +# input_buttons = "".join(f"""\ +# {kid_slug(k)}_reset_chores: +# name: "Reset {k['name']}'s Chores" +# icon: mdi:restart +# """ for k in kids) +# input_buttons += """\ +# reset_all_chores: +# name: "Reset All Chores" +# icon: mdi:restart-alert +# """ + +# auto_blocks = [] + +# for kid in kids: +# ks = kid_slug(kid) +# off_calls = "\n".join( +# f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}" +# for c in get_chores(kid) +# ) +# auto_blocks.append(f"""\ +# - id: {ks}_reset_from_ha +# alias: "Chore Tracker - Reset {kid['name']}'s chores from HA" +# trigger: +# - platform: state +# entity_id: input_button.{ks}_reset_chores +# action: +# {off_calls} + +# """) + +# all_off = "\n".join( +# f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}" +# for kid in kids for c in get_chores(kid) +# ) +# auto_blocks.append(f"""\ +# - id: reset_all_from_ha +# alias: "Chore Tracker - Reset ALL chores from HA" +# trigger: +# - platform: state +# entity_id: input_button.reset_all_chores +# action: +# {all_off} + +# """) + +# for kid in kids: +# ks = kid_slug(kid) +# n = len(get_chores(kid)) +# auto_blocks.append(f"""\ +# - id: {ks}_all_done_notify +# alias: "Chore Tracker - {kid['name']} all done!" +# trigger: +# - platform: state +# entity_id: sensor.{ks}_all_chores_done +# to: "True" +# action: +# - service: {s['notify_service']} +# data: +# title: "{kid['name']} finished all chores!" +# message: "{kid['name']} completed all {n} chores today!" + +# """) + +# reminder_actions = "".join(f"""\ +# - if: +# condition: template +# value_template: "{{{{ states('sensor.{kid_slug(k)}_all_chores_done') != 'True' }}}}" +# then: +# - service: {s['notify_service']} +# data: +# title: "{k['name']} has unfinished chores" +# message: > +# {k['name']} has done +# {{{{ states('sensor.{kid_slug(k)}_chores_done_today') }}}}/{len(get_chores(k))} chores today. +# """ for k in kids) + +# auto_blocks.append(f"""\ +# - id: chore_reminder_evening +# alias: "Chore Tracker - Evening reminder" +# trigger: +# - platform: time +# at: "{s['reminder_time']}:00" +# action: +# {reminder_actions} +# """) + +# return f"""\ +# ################################################################################ +# # chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml) +# # Kids: {", ".join(k["name"] for k in kids)} +# # +# # BIDIRECTIONAL SYNC: +# # Screen to HA: Each switch calls homeassistant.service on toggle +# # HA to Screen: ESPHome native API handles this automatically +# ################################################################################ + +# input_button: +# {input_buttons} +# template: +# - sensor: +# {"".join(sensor_blocks)} +# automation: +# {"".join(auto_blocks)} +# """ + + +# # ───────────────────────────────────────────────────────────────────────────── +# # Lovelace dashboard generator +# # ───────────────────────────────────────────────────────────────────────────── + +# def gen_dashboard(cfg: dict) -> str: +# kids = cfg["kids"] + +# summary_cards = "\n".join(f"""\ +# - type: markdown +# content: > +# ### {kid['avatar']} {kid['name']} +# {{{{ +# 'All done!' if states('sensor.{kid_slug(kid)}_all_chores_done') == 'True' +# else states('sensor.{kid_slug(kid)}_chores_done_today') ~ '/{len(get_chores(kid))} chores done' +# }}}} +# """ for kid in kids) + +# chore_cards = [] +# for kid in kids: +# chores = get_chores(kid) +# ks = kid_slug(kid) +# chore_rows = "\n".join(f"""\ +# - entity: {ha_switch(kid, c)} +# name: "{c['icon']} {c['name']}" +# """ for c in chores) + +# chore_cards.append(f"""\ +# - type: entities +# title: "{kid['avatar']} {kid['name']}'s Chores" +# entities: +# {chore_rows} - type: divider +# - entity: sensor.{ks}_chores_done_today +# name: "Chores done today" +# - entity: sensor.{ks}_all_chores_done +# name: "All Done?" +# - type: button +# name: "Reset {kid['name']}'s Chores" +# tap_action: +# action: call-service +# service: input_button.press +# target: +# entity_id: input_button.{ks}_reset_chores +# """) + +# return f"""\ +# ################################################################################ +# # chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml) +# # Kids: {", ".join(k["name"] for k in kids)} +# ################################################################################ + +# title: "Chore Tracker" +# views: +# - title: Today +# icon: mdi:checkbox-marked-circle +# path: chores +# cards: +# - type: horizontal-stack +# cards: +# {summary_cards} +# - type: button +# name: "Reset ALL Chores" +# icon: mdi:restart-alert +# tap_action: +# action: call-service +# service: input_button.press +# target: +# entity_id: input_button.reset_all_chores +# {"".join(chore_cards)} +# """ + + +# # ───────────────────────────────────────────────────────────────────────────── +# # Main +# # ───────────────────────────────────────────────────────────────────────────── + +# def main(): +# parser = argparse.ArgumentParser(description="Chore Tracker config generator") +# parser.add_argument("--config", default="chores_config.yaml") +# parser.add_argument("--out", default=".") +# args = parser.parse_args() + +# config_path = Path(args.config) +# if not config_path.exists(): +# print(f"❌ Config not found: {config_path}") +# sys.exit(1) + +# out_dir = Path(args.out) +# out_dir.mkdir(parents=True, exist_ok=True) + +# class SecretLoader(yaml.SafeLoader): +# pass +# SecretLoader.add_constructor( +# "!secret", +# lambda loader, node: f"!secret {loader.construct_scalar(node)}" +# ) + +# with open(config_path) as f: +# cfg = yaml.load(f, Loader=SecretLoader) + +# kids = cfg.get("kids", []) +# if not kids: +# print("❌ No kids defined in config!") +# sys.exit(1) + +# errors = [k["name"] for k in kids if "chores" not in k or not k["chores"]] +# if errors: +# print(f"❌ These kids have no chores defined: {', '.join(errors)}") +# print(" Add a 'chores:' list under each kid in chores_config.yaml") +# sys.exit(1) + +# # Warn about missing icons (will use default, not crash) +# for k in kids: +# for c in k.get("chores", []): +# if "icon" not in c: +# print(f"⚠️ {k['name']} / '{c['name']}' has no icon — using default") + +# total_chores = sum(len(get_chores(k)) for k in kids) +# print(f"✅ Config loaded: {len(kids)} kid(s), {total_chores} total chores") +# for k in kids: +# chores = get_chores(k) +# print(f" {k['avatar']} {k['name']}: {len(chores)} chores" +# f" — {', '.join(c['name'] for c in chores)}") +# print() + +# files = { +# "chore-tracker-esphome.yaml": gen_esphome(cfg), +# "chore-tracker-ha.yaml": gen_ha(cfg), +# "chore-tracker-dashboard.yaml": gen_dashboard(cfg), +# } + +# for name, content in files.items(): +# path = out_dir / name +# path.write_text(content) +# print(f"📄 {path}") + +# print() +# print("✅ Done!") +# print(" 1. Flash chore-tracker-esphome.yaml -> ESPHome dashboard") +# print(" 2. Merge chore-tracker-ha.yaml -> HA config + restart HA") +# print(" 3. Paste chore-tracker-dashboard.yaml -> New HA dashboard") + + +# if __name__ == "__main__": +# main() \ No newline at end of file diff --git a/esphome/components/max17043/__init__.py b/esphome/components/max17043/__init__.py deleted file mode 100644 index 1db25cc..0000000 --- a/esphome/components/max17043/__init__.py +++ /dev/null @@ -1 +0,0 @@ -CODEOWNERS = ["@blacknell"] diff --git a/esphome/components/max17043/automation.h b/esphome/components/max17043/automation.h deleted file mode 100644 index 44729d1..0000000 --- a/esphome/components/max17043/automation.h +++ /dev/null @@ -1,20 +0,0 @@ - -#pragma once -#include "esphome/core/automation.h" -#include "max17043.h" - -namespace esphome { -namespace max17043 { - -template class SleepAction : public Action { - public: - explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {} - - void play(Ts... x) override { this->max17043_->sleep_mode(); } - - protected: - MAX17043Component *max17043_; -}; - -} // namespace max17043 -} // namespace esphome diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp deleted file mode 100644 index bafc148..0000000 --- a/esphome/components/max17043/max17043.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "max17043.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace max17043 { - -// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert -// Consult the datasheet at https://www.analog.com/en/products/max17043.html - -static const char *const TAG = "max17043"; - -static const uint8_t MAX17043_VCELL = 0x02; -static const uint8_t MAX17043_SOC = 0x04; -static const uint8_t MAX17043_CONFIG = 0x0c; - -static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C; -static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4) -static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080; - -void MAX17043Component::update() { - uint16_t raw_voltage, raw_percent; - - if (this->voltage_sensor_ != nullptr) { - if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) { - this->status_set_warning("Unable to read MAX17043_VCELL"); - } else { - float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0; - this->voltage_sensor_->publish_state(voltage); - this->status_clear_warning(); - } - } - if (this->battery_remaining_sensor_ != nullptr) { - if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) { - this->status_set_warning("Unable to read MAX17043_SOC"); - } else { - float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff)); - this->battery_remaining_sensor_->publish_state(percent); - this->status_clear_warning(); - } - } -} - -void MAX17043Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up MAX17043..."); - - uint16_t config_reg; - if (this->write(&MAX17043_CONFIG, 1) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - - if (this->read(reinterpret_cast(&config_reg), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - - config_reg = i2c::i2ctohs(config_reg) & MAX17043_CONFIG_SAFE_MASK; - ESP_LOGV(TAG, "MAX17043 CONFIG register reads 0x%X", config_reg); - - if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) { - ESP_LOGW(TAG, "Non-standard CONFIG register 0x%X at 0x36; continuing in compatible mode", config_reg); - return; - } - - // need to write back to config register to reset the sleep bit - if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) { - this->status_set_error("sleep reset failed"); - this->mark_failed(); - return; - } -} - -void MAX17043Component::dump_config() { - ESP_LOGCONFIG(TAG, "MAX17043:"); - LOG_I2C_DEVICE(this); - if (this->is_failed()) { - ESP_LOGE(TAG, "Communication with MAX17043 failed"); - } - LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Battery Voltage", this->voltage_sensor_); - LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_); -} - -float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; } - -void MAX17043Component::sleep_mode() { - if (!this->is_failed()) { - if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) { - ESP_LOGW(TAG, "Unable to write the sleep bit to config register"); - this->status_set_warning(); - } - } -} - -} // namespace max17043 -} // namespace esphome diff --git a/esphome/components/max17043/max17043.h b/esphome/components/max17043/max17043.h deleted file mode 100644 index 540b977..0000000 --- a/esphome/components/max17043/max17043.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" - -namespace esphome { -namespace max17043 { - -class MAX17043Component : public PollingComponent, public i2c::I2CDevice { - public: - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - void update() override; - void sleep_mode(); - - void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } - void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) { - battery_remaining_sensor_ = battery_remaining_sensor; - } - - protected: - sensor::Sensor *voltage_sensor_{nullptr}; - sensor::Sensor *battery_remaining_sensor_{nullptr}; -}; - -} // namespace max17043 -} // namespace esphome diff --git a/esphome/components/max17043/sensor.py b/esphome/components/max17043/sensor.py deleted file mode 100644 index 3da0f95..0000000 --- a/esphome/components/max17043/sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -from esphome import automation -from esphome.automation import maybe_simple_id -import esphome.codegen as cg -from esphome.components import i2c, sensor -import esphome.config_validation as cv -from esphome.const import ( - CONF_BATTERY_LEVEL, - CONF_BATTERY_VOLTAGE, - CONF_ID, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_VOLTAGE, - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_CLASS_MEASUREMENT, - UNIT_PERCENT, - UNIT_VOLT, -) - -DEPENDENCIES = ["i2c"] - -max17043_ns = cg.esphome_ns.namespace("max17043") -MAX17043Component = max17043_ns.class_( - "MAX17043Component", cg.PollingComponent, i2c.I2CDevice -) - -# Actions -SleepAction = max17043_ns.class_("SleepAction", automation.Action) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MAX17043Component), - cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=3, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=3, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x36)) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) - - if voltage_config := config.get(CONF_BATTERY_VOLTAGE): - sens = await sensor.new_sensor(voltage_config) - cg.add(var.set_voltage_sensor(sens)) - - if CONF_BATTERY_LEVEL in config: - sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_remaining_sensor(sens)) - - -MAX17043_ACTION_SCHEMA = maybe_simple_id( - { - cv.Required(CONF_ID): cv.use_id(MAX17043Component), - } -) - - -@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA) -async def max17043_sleep_mode_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/declan-a1-camera.yaml b/esphome/declan-a1-camera.yaml deleted file mode 100644 index 23d6599..0000000 --- a/esphome/declan-a1-camera.yaml +++ /dev/null @@ -1,122 +0,0 @@ -substitutions: - device_name: declan-a1-camera - friendly_name: "Declan's A1 Camera" - -esphome: - name: ${device_name} - friendly_name: ${friendly_name} - -esp32: - board: esp32dev - framework: - type: esp-idf - -logger: - -api: - encryption: - key: !secret api_encryption_key - -ota: - - platform: esphome - password: !secret ota_password - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password - manual_ip: - static_ip: 192.168.69.220 - gateway: 192.168.69.1 - subnet: 255.255.255.0 - ap: - ssid: "${friendly_name} Fallback" - password: !secret fallback_password - # Prevents aggressive reconnection attempts - power_save_mode: none # Use 'light' for battery devices - - # Slower but more reliable connection - fast_connect: true - - # Handle connection failures gracefully - on_connect: - - logger.log: "Wi-Fi connected!" - on_disconnect: - - logger.log: "Wi-Fi disconnected!" - -# Reduce mDNS traffic -mdns: - disabled: true # Set to true if you use static IPs and don't need discovery - -# Diagnostic sensors -sensor: - - platform: wifi_signal - name: "${device_name} WiFi Signal" - update_interval: 60s - - - platform: uptime - name: "${device_name} Uptime" - update_interval: 60s - -text_sensor: - - platform: wifi_info - ip_address: - name: "${device_name} IP Address" - ssid: - name: "${device_name} Connected SSID" - bssid: - name: "${device_name} BSSID" - -# Watchdog to auto-reboot if things go wrong -interval: - - interval: 5min - then: - - if: - condition: - and: - - lambda: 'return millis() > 300000;' # >5 min uptime - - not: - wifi.connected: - then: - - logger.log: "Wi-Fi not connected for 5 min, rebooting..." - - delay: 10s - - lambda: 'App.safe_reboot();' - -captive_portal: - -i2c: - - id: camera_i2c - sda: GPIO26 - scl: GPIO27 - -psram: - mode: quad - speed: 80MHz - -esp32_camera: - external_clock: - pin: GPIO0 - frequency: 20MHz - i2c_id: camera_i2c - data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35] - vsync_pin: GPIO25 - href_pin: GPIO23 - pixel_clock_pin: GPIO22 - power_down_pin: GPIO32 - resolution: 1024x768 - jpeg_quality: 15 - vertical_flip: true - horizontal_mirror: false - max_framerate: 20 fps - idle_framerate: 0.05 fps - name: ${device_name} - -esp32_camera_web_server: - - port: 8080 - mode: STREAM - - port: 8081 - mode: SNAPSHOT - -switch: - - platform: gpio - name: "${device_name}-flash" - pin: 4 \ No newline at end of file diff --git a/esphome/esp32-h2c-camera.yaml b/esphome/esp32-h2c-camera.yaml index e73cb7d..03e0e99 100644 --- a/esphome/esp32-h2c-camera.yaml +++ b/esphome/esp32-h2c-camera.yaml @@ -60,7 +60,10 @@ esp32_camera: name: "${friendly_name}" external_clock: pin: GPIO5 - frequency: 20MHz + # 10MHz (half the usual 20MHz) slows the sensor's line rate, so each of the + # 1200 exposure lines below collects roughly twice the light. Costs + # framerate, which does not matter for chamber monitoring. + frequency: 10MHz i2c_pins: sda: GPIO8 scl: GPIO9 @@ -87,14 +90,17 @@ esp32_camera: contrast: -2 saturation: 0 - # Auto-Exposure & Gain tuning (AEC2 + AE Level lift shadows away from bright LEDs). - # The gain ceiling is the main lever for the tinted door: 8X was capping how - # far AGC could amplify, leaving everything outside the LED strips black. - aec_mode: auto + # Exposure & gain are MANUAL on purpose. The two LED strips in frame kept the + # auto-exposure meter satisfied, so AEC never lengthened the exposure and the + # tinted-door scene stayed black regardless of gain ceiling or AE level. + # Pinning both to max forces a bright chamber; the strips clip out entirely. + aec_mode: manual + aec_value: 1200 # max integration time (0-1200 lines) aec2: true - ae_level: 2 - agc_mode: auto - agc_gain_ceiling: 128X + ae_level: 2 # only consulted if AEC is flipped back to auto + agc_mode: manual + agc_value: 30 # max sensor gain (0-30) + agc_gain_ceiling: 128X # only consulted if AGC is flipped back to auto wb_mode: auto # Camera endpoints: @@ -113,6 +119,25 @@ switch: pin: GPIO47 restore_mode: ALWAYS_OFF + # Flip exposure/gain back to automatic for A/B comparison without a reflash. + # Off (the default) = manual exposure per the esp32_camera block above. + - platform: template + name: "${friendly_name} Auto Exposure" + optimistic: true + restore_mode: ALWAYS_OFF + turn_on_action: + - lambda: |- + sensor_t *s = esp_camera_sensor_get(); + if (!s) return; + s->set_exposure_ctrl(s, 1); + s->set_gain_ctrl(s, 1); + turn_off_action: + - lambda: |- + sensor_t *s = esp_camera_sensor_get(); + if (!s) return; + s->set_exposure_ctrl(s, 0); + s->set_gain_ctrl(s, 0); + sensor: - platform: wifi_signal name: "${friendly_name} WiFi Signal" @@ -159,6 +184,31 @@ interval: # Dynamic controls exposed to Home Assistant for real-time camera tuning number: + # The two that actually drive image brightness in manual mode. + - platform: template + name: "${friendly_name} Exposure" + min_value: 0 + max_value: 1200 + step: 25 + initial_value: 1200 + optimistic: true + set_action: + - lambda: |- + sensor_t *s = esp_camera_sensor_get(); + if (s) s->set_aec_value(s, (int)x); + + - platform: template + name: "${friendly_name} Gain" + min_value: 0 + max_value: 30 + step: 1 + initial_value: 30 + optimistic: true + set_action: + - lambda: |- + sensor_t *s = esp_camera_sensor_get(); + if (s) s->set_agc_gain(s, (int)x); + - platform: template name: "${friendly_name} Brightness" min_value: -2 diff --git a/esphome/family-room-remote.yaml b/esphome/family-room-remote.yaml deleted file mode 100644 index 8f00b42..0000000 --- a/esphome/family-room-remote.yaml +++ /dev/null @@ -1,38 +0,0 @@ -substitutions: - name: family-room-remote - friendly_name: "Family Room Remote" - -packages: - base: !include family-room-remote/family-room-remote.base.yaml - ui: !include family-room-remote/family-room-remote.ui.yaml - bindings: !include family-room-remote/family-room-remote.bindings.yaml - -esphome: - name: ${name} - friendly_name: ${friendly_name} - name_add_mac_suffix: false - -esp32: - board: esp32-s3-devkitc-1 - variant: ESP32S3 - flash_size: 16MB - framework: - type: esp-idf - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password - ap: - ssid: "${friendly_name} Fallback" - password: !secret fallback_password - -api: - encryption: - key: !secret api_encryption_key - -ota: - - platform: esphome - password: !secret ota_password - -logger: - baud_rate: 0 diff --git a/esphome/family-room-remote/family-room-remote.base.yaml b/esphome/family-room-remote/family-room-remote.base.yaml deleted file mode 100644 index c48af6b..0000000 --- a/esphome/family-room-remote/family-room-remote.base.yaml +++ /dev/null @@ -1,173 +0,0 @@ -psram: - mode: octal - speed: 80MHz - -i2c: - id: i2c_main - sda: GPIO19 - scl: GPIO20 - frequency: 400kHz - scan: false - -# --- Backlight control (GPIO2 direct) --- -switch: - - platform: gpio - name: "LCD Backlight Raw" - id: lcd_backlight_raw - restore_mode: ALWAYS_ON - pin: - number: GPIO2 - mode: - output: true - -light: - - platform: binary - name: "Family Room Remote Backlight" - output: lcd_backlight_out - id: ha_remote_backlight - restore_mode: ALWAYS_ON - -output: - - platform: template - id: lcd_backlight_out - type: binary - write_action: - - if: - condition: - lambda: return state; - then: - - switch.turn_on: lcd_backlight_raw - else: - - switch.turn_off: lcd_backlight_raw - -# --- Inactivity tracking (off after 2 minutes, wake on touch) --- -globals: - - id: last_activity_ms - type: uint32_t - restore_value: no - initial_value: '0' - -interval: - - interval: 1s - then: - - lambda: |- - if (id(last_activity_ms) == 0) id(last_activity_ms) = millis(); - - - if: - condition: - lambda: |- - const uint32_t idle_s = (millis() - id(last_activity_ms)) / 1000; - return idle_s == 120; - then: - - light.turn_off: ha_remote_backlight - -# --- Display (ST7262 parallel RGB, 800x480) --- -display: - - platform: rpi_dpi_rgb - id: main_display - dimensions: - width: 800 - height: 480 - de_pin: GPIO40 - hsync_pin: GPIO39 - vsync_pin: GPIO41 - pclk_pin: GPIO42 - pclk_frequency: 16MHz - color_order: BGR - data_pins: - red: - - GPIO45 - - GPIO48 - - GPIO47 - - GPIO21 - - GPIO14 - green: - - GPIO5 - - GPIO6 - - GPIO7 - - GPIO15 - - GPIO16 - - GPIO4 - blue: - - GPIO8 - - GPIO3 - - GPIO46 - - GPIO9 - - GPIO1 - hsync_back_porch: 48 - hsync_front_porch: 40 - hsync_pulse_width: 48 - vsync_back_porch: 32 - vsync_front_porch: 13 - vsync_pulse_width: 3 - update_interval: never - auto_clear_enabled: false - -# --- Touch (GT911 capacitive) --- -# NOTE: interrupt_pin GPIO18 requires soldering the R17 bridge on the board. -# If not soldered, remove the interrupt_pin line (falls back to polling). -touchscreen: - platform: gt911 - id: touch_panel - i2c_id: i2c_main - reset_pin: GPIO38 - on_touch: - then: - - lambda: |- - id(last_activity_ms) = millis(); - - light.turn_on: ha_remote_backlight - -# --- MDI Icon Font --- -font: - - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" - id: mdi_icons - size: 24 - bpp: 4 - glyphs: - - "\U000F0335" # mdi:lightbulb - - "\U000F0425" # mdi:power - - "\U000F0079" # mdi:battery - - "\U000F12A1" # mdi:battery-low - - "\U000F12A2" # mdi:battery-medium - - "\U000F12A3" # mdi:battery-high - - "\U000F12A4" # mdi:battery-charging-low - - "\U000F12A5" # mdi:battery-charging-medium - - "\U000F12A6" # mdi:battery-charging-high - - "\U000F0091" # mdi:battery-unknown - - "\U000F10CD" # mdi:battery-alert-variant-outline - -# --- Battery fuel gauge (MAX17048 via I2C) --- -# DISABLED for testing - suspected display interference -# sensor: -# - platform: max17043 -# id: max17048_battery -# i2c_id: i2c_main -# update_interval: 120s -# battery_voltage: -# name: "Remote Battery Voltage" -# id: remote_battery_voltage -# entity_category: diagnostic -# device_class: voltage -# state_class: measurement -# accuracy_decimals: 3 -# battery_level: -# name: "Remote Battery Level" -# id: remote_battery_level -# entity_category: diagnostic -# device_class: battery -# state_class: measurement -# accuracy_decimals: 0 -# on_value: -# then: -# - lambda: |- -# ESP_LOGI("battery", "Level: %.0f%%", x); -# // Update the LVGL battery icon based on level -# lv_obj_t *lbl = id(battery_icon); -# if (x < 10) -# lv_label_set_text(lbl, "\U000F10CD"); // battery-alert-variant-outline -# else if (x < 30) -# lv_label_set_text(lbl, "\U000F12A1"); // battery-low -# else if (x < 70) -# lv_label_set_text(lbl, "\U000F12A2"); // battery-medium -# else -# lv_label_set_text(lbl, "\U000F12A3"); // battery-high diff --git a/esphome/family-room-remote/family-room-remote.bindings.yaml b/esphome/family-room-remote/family-room-remote.bindings.yaml deleted file mode 100644 index 162a7be..0000000 --- a/esphome/family-room-remote/family-room-remote.bindings.yaml +++ /dev/null @@ -1,59 +0,0 @@ -text_sensor: - - platform: wifi_info - ip_address: - name: "Family Room Remote IP" - id: ip_addr - - - platform: homeassistant - id: ts_family_room_tv_stand - entity_id: light.family_room_tv_stand - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_family_room_tv_stand - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_family_room_tv_stand); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - - platform: homeassistant - id: ts_small_family_room_lamp - entity_id: light.small_family_room_lamp - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_small_family_room_lamp - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_small_family_room_lamp); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - - platform: homeassistant - id: ts_big_family_room_lamp - entity_id: switch.big_family_room_lamp - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_big_family_room_lamp - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_big_family_room_lamp); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } diff --git a/esphome/family-room-remote/family-room-remote.ui.yaml b/esphome/family-room-remote/family-room-remote.ui.yaml deleted file mode 100644 index 1898afb..0000000 --- a/esphome/family-room-remote/family-room-remote.ui.yaml +++ /dev/null @@ -1,231 +0,0 @@ -lvgl: - displays: - - main_display - touchscreens: - - touch_panel - disp_bg_color: 0x141218 - style_definitions: - # ── Top App Bar ── - - id: md3_header - bg_color: 0x1D1B20 - bg_opa: 100% - border_width: 0 - radius: 0 - pad_all: 0 - shadow_width: 0 - # ── Status Chip ── - - id: md3_chip - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 8 - pad_all: 0 - # ── Text Styles ── - - id: md3_title - text_color: 0xE3E2E6 - text_opa: 100% - - id: md3_meta - text_color: 0xC4C6CF - text_opa: 100% - - id: md3_icon - text_color: 0xC4C6CF - text_opa: 100% - # ── Card Variants ── - - id: md3_card_blue - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - - id: md3_card_pink - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - - id: md3_card_green - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - theme: - label: - text_color: 0xE3E2E6 - button: - text_color: 0xE3E2E6 - pages: - - id: home - widgets: - # ── Header ── - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Family Room" - - # ── TV Stand ── - - button: - id: btn_family_room_tv_stand - x: 24 - y: 80 - width: 234 - height: 300 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.family_room_tv_stand - widgets: - - label: - x: 194 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "TV Stand" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - # ── Small Lamp ── - - button: - id: btn_small_family_room_lamp - x: 282 - y: 80 - width: 234 - height: 300 - checkable: true - styles: md3_card_pink - checked: - bg_color: 0xFFD7F1 - bg_opa: 100% - border_color: 0xFFD7F1 - border_opa: 100% - text_color: 0x22001F - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.small_family_room_lamp - widgets: - - label: - x: 194 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Small Lamp" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - # ── Standing Lamp ── - - button: - id: btn_big_family_room_lamp - x: 540 - y: 80 - width: 236 - height: 300 - checkable: true - styles: md3_card_green - checked: - bg_color: 0xB5CCBA - bg_opa: 100% - border_color: 0xB5CCBA - border_opa: 100% - text_color: 0x001F15 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.living_room_lamp_1 - widgets: - - label: - x: 196 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Standing Lamp" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - # ── Status Bar ── - - obj: - x: 0 - y: 400 - width: 800 - height: 80 - styles: md3_header - widgets: - - obj: - x: 8 - y: 20 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: battery_icon - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline diff --git a/esphome/ha-remote/components/max17043/__init__.py b/esphome/ha-remote/components/max17043/__init__.py deleted file mode 100644 index 1db25cc..0000000 --- a/esphome/ha-remote/components/max17043/__init__.py +++ /dev/null @@ -1 +0,0 @@ -CODEOWNERS = ["@blacknell"] diff --git a/esphome/ha-remote/components/max17043/automation.h b/esphome/ha-remote/components/max17043/automation.h deleted file mode 100644 index 44729d1..0000000 --- a/esphome/ha-remote/components/max17043/automation.h +++ /dev/null @@ -1,20 +0,0 @@ - -#pragma once -#include "esphome/core/automation.h" -#include "max17043.h" - -namespace esphome { -namespace max17043 { - -template class SleepAction : public Action { - public: - explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {} - - void play(Ts... x) override { this->max17043_->sleep_mode(); } - - protected: - MAX17043Component *max17043_; -}; - -} // namespace max17043 -} // namespace esphome diff --git a/esphome/ha-remote/components/max17043/max17043.cpp b/esphome/ha-remote/components/max17043/max17043.cpp deleted file mode 100644 index 23a7e2d..0000000 --- a/esphome/ha-remote/components/max17043/max17043.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "max17043.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace max17043 { - -// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert -// Consult the datasheet at https://www.analog.com/en/products/max17043.html - -static const char *const TAG = "max17043"; - -static const uint8_t MAX17043_VCELL = 0x02; -static const uint8_t MAX17043_SOC = 0x04; -static const uint8_t MAX17043_CONFIG = 0x0c; - -static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C; -static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4) -static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080; - -void MAX17043Component::update() { - uint16_t raw_voltage, raw_percent; - - if (this->voltage_sensor_ != nullptr) { - if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) { - this->status_set_warning("Unable to read MAX17043_VCELL"); - } else { - float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0; - this->voltage_sensor_->publish_state(voltage); - this->status_clear_warning(); - } - } - if (this->battery_remaining_sensor_ != nullptr) { - if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) { - this->status_set_warning("Unable to read MAX17043_SOC"); - } else { - float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff)); - this->battery_remaining_sensor_->publish_state(percent); - this->status_clear_warning(); - } - } -} - -void MAX17043Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up MAX17043..."); - // Compatible mode for MAX17048/variant boards: - // avoid setup-time register writes on the shared touch I2C bus. -} - -void MAX17043Component::dump_config() { - ESP_LOGCONFIG(TAG, "MAX17043:"); - LOG_I2C_DEVICE(this); - if (this->is_failed()) { - ESP_LOGE(TAG, "Communication with MAX17043 failed"); - } - LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Battery Voltage", this->voltage_sensor_); - LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_); -} - -float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; } - -void MAX17043Component::sleep_mode() { - if (!this->is_failed()) { - if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) { - ESP_LOGW(TAG, "Unable to write the sleep bit to config register"); - this->status_set_warning(); - } - } -} - -} // namespace max17043 -} // namespace esphome diff --git a/esphome/ha-remote/components/max17043/max17043.h b/esphome/ha-remote/components/max17043/max17043.h deleted file mode 100644 index 540b977..0000000 --- a/esphome/ha-remote/components/max17043/max17043.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" - -namespace esphome { -namespace max17043 { - -class MAX17043Component : public PollingComponent, public i2c::I2CDevice { - public: - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - void update() override; - void sleep_mode(); - - void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } - void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) { - battery_remaining_sensor_ = battery_remaining_sensor; - } - - protected: - sensor::Sensor *voltage_sensor_{nullptr}; - sensor::Sensor *battery_remaining_sensor_{nullptr}; -}; - -} // namespace max17043 -} // namespace esphome diff --git a/esphome/ha-remote/components/max17043/sensor.py b/esphome/ha-remote/components/max17043/sensor.py deleted file mode 100644 index 3da0f95..0000000 --- a/esphome/ha-remote/components/max17043/sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -from esphome import automation -from esphome.automation import maybe_simple_id -import esphome.codegen as cg -from esphome.components import i2c, sensor -import esphome.config_validation as cv -from esphome.const import ( - CONF_BATTERY_LEVEL, - CONF_BATTERY_VOLTAGE, - CONF_ID, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_VOLTAGE, - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_CLASS_MEASUREMENT, - UNIT_PERCENT, - UNIT_VOLT, -) - -DEPENDENCIES = ["i2c"] - -max17043_ns = cg.esphome_ns.namespace("max17043") -MAX17043Component = max17043_ns.class_( - "MAX17043Component", cg.PollingComponent, i2c.I2CDevice -) - -# Actions -SleepAction = max17043_ns.class_("SleepAction", automation.Action) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MAX17043Component), - cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=3, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=3, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x36)) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) - - if voltage_config := config.get(CONF_BATTERY_VOLTAGE): - sens = await sensor.new_sensor(voltage_config) - cg.add(var.set_voltage_sensor(sens)) - - if CONF_BATTERY_LEVEL in config: - sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_remaining_sensor(sens)) - - -MAX17043_ACTION_SCHEMA = maybe_simple_id( - { - cv.Required(CONF_ID): cv.use_id(MAX17043Component), - } -) - - -@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA) -async def max17043_sleep_mode_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/ha-remote/ha-remote-1.base.yaml b/esphome/ha-remote/ha-remote-1.base.yaml deleted file mode 100644 index cbf8128..0000000 --- a/esphome/ha-remote/ha-remote-1.base.yaml +++ /dev/null @@ -1,176 +0,0 @@ -psram: - mode: octal - speed: 80MHz - -i2c: - id: i2c_main - sda: 8 - scl: 9 - frequency: 400kHz - scan: false - -# CH422G I/O expander (Waveshare uses it for LCD reset/backlight/touch reset) -ch422g: - - id: ch422g_hub - i2c_id: i2c_main - -# --- Backlight control (CH422G IO2 is common for Waveshare LCD BL) --- -switch: - - platform: gpio - name: "LCD Backlight Raw" - id: lcd_backlight_raw - restore_mode: ALWAYS_ON - pin: - ch422g: ch422g_hub - number: 2 - mode: - output: true - -# A nicer HA-exposed control (so you can also automate it from HA) -light: - - platform: binary - name: "HA Remote Backlight" - output: lcd_backlight_out - id: ha_remote_backlight - -output: - - platform: template - id: lcd_backlight_out - type: binary - write_action: - - if: - condition: - lambda: return state; - then: - - switch.turn_on: lcd_backlight_raw - else: - - switch.turn_off: lcd_backlight_raw - -# --- Inactivity tracking (dim/off + wake on touch) --- -globals: - - id: last_activity_ms - type: uint32_t - restore_value: no - initial_value: '0' - -interval: - - interval: 1s - then: - - lambda: |- - if (id(last_activity_ms) == 0) id(last_activity_ms) = millis(); - - # Turn off backlight ONCE at 2 minutes idle. - - if: - condition: - lambda: |- - const uint32_t idle_s = (millis() - id(last_activity_ms)) / 1000; - return idle_s == 120; - then: - - light.turn_off: ha_remote_backlight - -# --- Display --- -display: - - platform: mipi_rgb - model: ESP32-S3-TOUCH-LCD-7-800X480 - id: main_display - update_interval: never - auto_clear_enabled: false - reset_pin: - ch422g: ch422g_hub - number: 3 - mode: - output: true - -# --- Touch --- -touchscreen: - platform: gt911 - id: touch_panel - i2c_id: i2c_main - interrupt_pin: 4 - reset_pin: - ch422g: ch422g_hub - number: 1 - mode: - output: true - on_touch: - then: - - lambda: |- - id(last_activity_ms) = millis(); - - light.turn_on: ha_remote_backlight - -# --- MDI Icon Font --- -font: - - file: "https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf" - id: mdi_icons - size: 24 - bpp: 4 - glyphs: - # Tile icons - - "\U000F0335" # mdi:lightbulb - - "\U000F0425" # mdi:power - - "\U000F0426" # mdi:power-plug - - "\U000F07E9" # mdi:power-socket-us - - "\U000F1A26" # mdi:toggle-switch-variant-off - # Battery status icons - - "\U000F0079" # mdi:battery - - "\U000F12A1" # mdi:battery-low - - "\U000F12A2" # mdi:battery-medium - - "\U000F12A3" # mdi:battery-high - - "\U000F12A4" # mdi:battery-charging-low - - "\U000F12A5" # mdi:battery-charging-medium - - "\U000F12A6" # mdi:battery-charging-high - - "\U000F0091" # mdi:battery-unknown - - "\U000F10CD" # mdi:battery-alert-variant-outline - # Navigation bar icons - - "\U000F04B9" # mdi:sofa - - "\U000F06B5" # mdi:lamp - - "\U000F04DE" # mdi:stove - - "\U000F12BD" # mdi:stairs-up - - "\U000F1239" # mdi:desk - - "\U000F06D9" # mdi:garage - - "\U000F0531" # mdi:tree - -# --- LVGL UI --- -# Note: On ESP32-S3-Touch-LCD-7, GPIO14 is used by the RGB display bus, -# so it cannot be reused as ADC for battery telemetry in this display mode. - -# --- Battery fuel gauge (Adafruit MAX17048 via compatible driver) --- -sensor: - - platform: max17043 - id: max17048_battery - i2c_id: i2c_main - update_interval: 120s - battery_voltage: - name: "Remote Battery Voltage" - id: remote_battery_voltage - entity_category: diagnostic - device_class: voltage - state_class: measurement - accuracy_decimals: 3 - on_value: - then: - - lambda: |- - ESP_LOGI("battery", "Voltage: %.3f V", x); - battery_level: - name: "Remote Battery Level" - id: remote_battery_level - entity_category: diagnostic - device_class: battery - state_class: measurement - accuracy_decimals: 0 - on_value: - then: - - lambda: |- - const float pct = x; - ESP_LOGI("battery", "Level: %.0f%%", pct); - # - platform: custom - # lambda: |- - # auto max17048_sensor = new MAX17048Sensor(); - # App.register_component(max17048_sensor); - # return {max17048_sensor->voltage_sensor, max17048_sensor->percentage_sensor}; - # sensors: - # - name: "Voltage" - # unit_of_measurement: V - # accuracy_decimals: 2 - # - name: "Percentage" - # unit_of_measurement: '%' diff --git a/esphome/ha-remote/ha-remote-1.bindings.yaml b/esphome/ha-remote/ha-remote-1.bindings.yaml deleted file mode 100644 index 58f949d..0000000 --- a/esphome/ha-remote/ha-remote-1.bindings.yaml +++ /dev/null @@ -1,383 +0,0 @@ -text_sensor: - - platform: wifi_info - ip_address: - name: "HA Remote IP" - id: ip_addr - - platform: homeassistant - id: ts_family_room_tv_stand - entity_id: light.family_room_tv_stand - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_family_room_tv_stand - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_family_room_tv_stand); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_small_family_room_lamp - entity_id: light.small_family_room_lamp - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_small_family_room_lamp - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_small_family_room_lamp); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_living_room_lamp_1 - entity_id: light.living_room_lamp_1 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_family_room_standing_lamp - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x3D0002) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_family_room_standing_lamp); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_living_room_light_2 - entity_id: light.living_room_light_2 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_living_room_main_light - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_living_room_main_light); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_patio_light_1 - entity_id: light.patio_light_1 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_living_room_patio_light - state: - checked: !lambda return x == "on"; - - lvgl.widget.update: - id: btn_outside_patio_light - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t off_color = lv_color_hex(0xE3E2E6); - auto *btn1 = id(btn_living_room_patio_light); - auto *btn2 = id(btn_outside_patio_light); - if (x == "on") { - lv_obj_set_style_text_color(btn1, lv_color_hex(0x1A0C00), static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), lv_color_hex(0x1A0C00), static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - lv_obj_set_style_text_color(btn2, lv_color_hex(0x001A33), static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), lv_color_hex(0x001A33), static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - } else { - lv_obj_set_style_text_color(btn1, off_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), off_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - lv_obj_set_style_text_color(btn2, off_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), off_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - } - - platform: homeassistant - id: ts_kitchen_sink_light - entity_id: light.kitchen_sink_light - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_kitchen_sink_light - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_kitchen_sink_light); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_office_lamp_2 - entity_id: light.office_lamp_2 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_office_lamp - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_office_lamp); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_office_led_strip - entity_id: light.led_strip_controller_led_strip_controller - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_office_led_strip - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_office_led_strip); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_garage_cam_light - entity_id: light.esp32_saturn4_cam_esp32_saturn4_cam_light - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_garage_cam_light - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_garage_cam_light); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_office_echo_plug - entity_id: switch.office_echo_plug - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_office_echo_plug - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_office_echo_plug); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_office_end_table_lamp_outlet - entity_id: switch.office_end_table_lamp_socket_1 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_office_end_table_lamp_outlet - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_office_end_table_lamp_outlet); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_office_wax_warmer_outlet - entity_id: switch.office_wax_warmer_socket_1 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_office_wax_warmer_outlet - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_office_wax_warmer_outlet); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_outside_lamppost_outlet_1 - entity_id: switch.lamppost_outlets_socket_1 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_outside_lamppost_outlet_1 - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_outside_lamppost_outlet_1); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_outside_lamppost_outlet_2 - entity_id: switch.lamppost_outlets_socket_2 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_outside_lamppost_outlet_2 - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_outside_lamppost_outlet_2); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_outside_porch_decor_outlet - entity_id: switch.washing_machine_socket_1 - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_outside_porch_decor_outlet - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_outside_porch_decor_outlet); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_living_room_garland_switch - entity_id: switch.big_family_room_lamp - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_living_room_garland_switch - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_living_room_garland_switch); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_living_room_calendar_switch - entity_id: switch.digital_calendar - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_living_room_calendar_switch - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_living_room_calendar_switch); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_upstairs_airquality_oled - entity_id: switch.airqualitysensor_3_oled_power - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_upstairs_airquality_oled - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_upstairs_airquality_oled); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_upstairs_camera_motion - entity_id: switch.upstairs_camera_motion_detection - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_upstairs_camera_motion - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_upstairs_camera_motion); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_garage_fume_exhaust_fan - entity_id: switch.fume_exhaust_fan - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_garage_fume_exhaust_fan - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_garage_fume_exhaust_fan); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - - platform: homeassistant - id: ts_garage_resin_printer_heater - entity_id: switch.resin_printer_heater - internal: true - on_value: - then: - - lvgl.widget.update: - id: btn_garage_resin_printer_heater - state: - checked: !lambda return x == "on"; - - lambda: |- - const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6); - auto *btn = id(btn_garage_resin_printer_heater); - lv_obj_set_style_text_color(btn, text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) { - lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast(LV_PART_MAIN | LV_STATE_DEFAULT)); - } - diff --git a/esphome/ha-remote/ha-remote-1.ui.yaml b/esphome/ha-remote/ha-remote-1.ui.yaml deleted file mode 100644 index ca83be0..0000000 --- a/esphome/ha-remote/ha-remote-1.ui.yaml +++ /dev/null @@ -1,2719 +0,0 @@ -lvgl: - displays: - - main_display - touchscreens: - - touch_panel - disp_bg_color: 0x141218 - style_definitions: - # ── Top App Bar ── - - id: md3_header - bg_color: 0x1D1B20 - bg_opa: 100% - border_width: 0 - radius: 0 - pad_all: 0 - shadow_width: 0 - # ── Status Chip ── - - id: md3_chip - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 8 - pad_all: 0 - # ── Text Styles ── - - id: md3_title - text_color: 0xE3E2E6 - text_opa: 100% - - id: md3_meta - text_color: 0xC4C6CF - text_opa: 100% - - id: md3_icon - text_color: 0xC4C6CF - text_opa: 100% - # ── Card Variants ── - - id: md3_card_blue - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - - id: md3_card_pink - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - - id: md3_card_red - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - - id: md3_card_amber - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - - id: md3_card_green - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - - id: md3_card_violet - bg_color: 0x2B2930 - bg_opa: 100% - border_color: 0x49454F - border_width: 1 - border_opa: 100% - radius: 16 - shadow_width: 0 - # ── Bottom Navigation ── - - id: md3_nav_bar - bg_color: 0x2B2930 - bg_opa: 100% - border_width: 0 - radius: 0 - pad_all: 0 - shadow_width: 0 - - id: md3_nav_item - bg_opa: 0% - border_width: 0 - radius: 0 - text_color: 0xC4C6CF - pad_all: 0 - theme: - label: - text_color: 0xE3E2E6 - button: - text_color: 0xE3E2E6 - pages: -############################################# -# Family Room # -############################################# - - id: home - widgets: - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Family Room" - - button: - id: btn_family_room_tv_stand - x: 24 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.family_room_tv_stand - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "TV Stand" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_small_family_room_lamp - x: 278 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_pink - checked: - bg_color: 0xFFD7F1 - bg_opa: 100% - border_color: 0xFFD7F1 - border_opa: 100% - text_color: 0x22001F - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.small_family_room_lamp - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Small Lamp" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_family_room_standing_lamp - x: 532 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_red - checked: - bg_color: 0xFFB4AB - bg_opa: 100% - border_color: 0xFFB4AB - border_opa: 100% - text_color: 0x3D0002 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.living_room_lamp_1 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Standing Lamp" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - obj: - x: 0 - y: 408 - width: 800 - height: 72 - styles: md3_nav_bar - widgets: - - obj: - x: 8 - y: 16 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: nav_battery_icon_home - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline - - button: - x: 100 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: home - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 100% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xD5E3FF - text: "\U000F04B9" # mdi:sofa - - label: - align: top_mid - y: 46 - text_color: 0xE3E2E6 - text: "Family" - - button: - x: 200 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: living_room - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06B5" # mdi:lamp - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Living" - - button: - x: 300 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: kitchen_page - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04DE" # mdi:stove - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Kitchen" - - button: - x: 400 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: upstairs - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F12BD" # mdi:stairs-up - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Upstairs" - - button: - x: 500 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: office - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F1239" # mdi:desk - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Office" - - button: - x: 600 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: garage - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06D9" # mdi:garage - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Garage" - - button: - x: 700 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: outside - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F0531" # mdi:tree - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Outside" - -############################################# -# Living Room # -############################################# - - id: living_room - widgets: - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Living Room" - - - button: - id: btn_living_room_main_light - x: 24 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.living_room_light_2 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Large Living Room Lamp" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_living_room_patio_light - x: 532 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_amber - checked: - bg_color: 0xFFDCBE - bg_opa: 100% - border_color: 0xFFDCBE - border_opa: 100% - text_color: 0x1A0C00 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.small_living_room_lamp - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Small Living Room Lamp" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_living_room_garland_switch - x: 24 - y: 228 - width: 244 - height: 136 - checkable: true - styles: md3_card_green - checked: - bg_color: 0x66F9B9 - bg_opa: 100% - border_color: 0x66F9B9 - border_opa: 100% - text_color: 0x001F15 - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.big_family_room_lamp - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F1A26" # mdi:toggle-switch-variant-off - - label: - x: 18 - y: 16 - styles: md3_meta - text: "SWITCH" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Front Garland" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_living_room_calendar_switch - x: 278 - y: 228 - width: 244 - height: 136 - checkable: true - styles: md3_card_violet - checked: - bg_color: 0xD0BCFF - bg_opa: 100% - border_color: 0xD0BCFF - border_opa: 100% - text_color: 0x1E0F3F - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.digital_calendar - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F1A26" # mdi:toggle-switch-variant-off - - label: - x: 18 - y: 16 - styles: md3_meta - text: "SWITCH" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Calendar" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - obj: - x: 0 - y: 408 - width: 800 - height: 72 - styles: md3_nav_bar - widgets: - - obj: - x: 8 - y: 16 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: nav_battery_icon_living_room - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline - - button: - x: 100 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: home - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04B9" # mdi:sofa - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Family" - - button: - x: 200 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: living_room - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 100% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xD5E3FF - text: "\U000F06B5" # mdi:lamp - - label: - align: top_mid - y: 46 - text_color: 0xE3E2E6 - text: "Living" - - button: - x: 300 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: kitchen_page - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04DE" # mdi:stove - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Kitchen" - - button: - x: 400 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: upstairs - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F12BD" # mdi:stairs-up - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Upstairs" - - button: - x: 500 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: office - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F1239" # mdi:desk - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Office" - - button: - x: 600 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: garage - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06D9" # mdi:garage - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Garage" - - button: - x: 700 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: outside - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F0531" # mdi:tree - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Outside" - -############################################# -# Kitchen # -############################################# - - id: kitchen_page - widgets: - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Kitchen" - - - button: - id: btn_kitchen_sink_light - x: 24 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.kitchen_sink_light - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Sink Light" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - obj: - x: 0 - y: 408 - width: 800 - height: 72 - styles: md3_nav_bar - widgets: - - obj: - x: 8 - y: 16 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: nav_battery_icon_kitchen - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline - - button: - x: 100 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: home - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04B9" # mdi:sofa - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Family" - - button: - x: 200 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: living_room - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06B5" # mdi:lamp - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Living" - - button: - x: 300 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: kitchen_page - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 100% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xD5E3FF - text: "\U000F04DE" # mdi:stove - - label: - align: top_mid - y: 46 - text_color: 0xE3E2E6 - text: "Kitchen" - - button: - x: 400 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: upstairs - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F12BD" # mdi:stairs-up - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Upstairs" - - button: - x: 500 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: office - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F1239" # mdi:desk - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Office" - - button: - x: 600 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: garage - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06D9" # mdi:garage - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Garage" - - button: - x: 700 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: outside - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F0531" # mdi:tree - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Outside" - -############################################# -# Upstairs # -############################################# - - id: upstairs - widgets: - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Upstairs" - - - button: - id: btn_upstairs_airquality_oled - x: 24 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.airqualitysensor_3_oled_power - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F1A26" # mdi:toggle-switch-variant-off - - label: - x: 18 - y: 16 - styles: md3_meta - text: "SWITCH" - - label: - x: 18 - y: 56 - styles: md3_title - text: "AQ OLED" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_upstairs_camera_motion - x: 278 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_pink - checked: - bg_color: 0xFFD7F1 - bg_opa: 100% - border_color: 0xFFD7F1 - border_opa: 100% - text_color: 0x22001F - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.upstairs_camera_motion_detection - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F1A26" # mdi:toggle-switch-variant-off - - label: - x: 18 - y: 16 - styles: md3_meta - text: "SWITCH" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Cam Motion" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - obj: - x: 0 - y: 408 - width: 800 - height: 72 - styles: md3_nav_bar - widgets: - - obj: - x: 8 - y: 16 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: nav_battery_icon_upstairs - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline - - button: - x: 100 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: home - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04B9" # mdi:sofa - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Family" - - button: - x: 200 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: living_room - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06B5" # mdi:lamp - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Living" - - button: - x: 300 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: kitchen_page - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04DE" # mdi:stove - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Kitchen" - - button: - x: 400 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: upstairs - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 100% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xD5E3FF - text: "\U000F12BD" # mdi:stairs-up - - label: - align: top_mid - y: 46 - text_color: 0xE3E2E6 - text: "Upstairs" - - button: - x: 500 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: office - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F1239" # mdi:desk - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Office" - - button: - x: 600 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: garage - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06D9" # mdi:garage - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Garage" - - button: - x: 700 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: outside - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F0531" # mdi:tree - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Outside" - -############################################# -# Office # -############################################# - - id: office - widgets: - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Office" - - - button: - id: btn_office_lamp - x: 24 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.office_lamp_2 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Office Lamp 2" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_office_led_strip - x: 278 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_pink - checked: - bg_color: 0xFFD7F1 - bg_opa: 100% - border_color: 0xFFD7F1 - border_opa: 100% - text_color: 0x22001F - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.led_strip_controller_led_strip_controller - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "LED Strip" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_office_echo_plug - x: 532 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_amber - checked: - bg_color: 0xFFDCBE - bg_opa: 100% - border_color: 0xFFDCBE - border_opa: 100% - text_color: 0x1A0C00 - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.office_echo_plug - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F07E9" # mdi:power-socket-us - - label: - x: 18 - y: 16 - styles: md3_meta - text: "OUTLET" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Echo Plug" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_office_end_table_lamp_outlet - x: 24 - y: 228 - width: 244 - height: 136 - checkable: true - styles: md3_card_green - checked: - bg_color: 0x66F9B9 - bg_opa: 100% - border_color: 0x66F9B9 - border_opa: 100% - text_color: 0x001F15 - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.office_end_table_lamp_socket_1 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F07E9" # mdi:power-socket-us - - label: - x: 18 - y: 16 - styles: md3_meta - text: "OUTLET" - - label: - x: 18 - y: 56 - styles: md3_title - text: "End Table Lamp" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_office_wax_warmer_outlet - x: 278 - y: 228 - width: 244 - height: 136 - checkable: true - styles: md3_card_violet - checked: - bg_color: 0xD0BCFF - bg_opa: 100% - border_color: 0xD0BCFF - border_opa: 100% - text_color: 0x1E0F3F - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.office_wax_warmer_socket_1 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F07E9" # mdi:power-socket-us - - label: - x: 18 - y: 16 - styles: md3_meta - text: "OUTLET" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Wax Warmer" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - obj: - x: 0 - y: 408 - width: 800 - height: 72 - styles: md3_nav_bar - widgets: - - obj: - x: 8 - y: 16 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: nav_battery_icon_office - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline - - button: - x: 100 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: home - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04B9" # mdi:sofa - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Family" - - button: - x: 200 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: living_room - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06B5" # mdi:lamp - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Living" - - button: - x: 300 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: kitchen_page - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04DE" # mdi:stove - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Kitchen" - - button: - x: 400 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: upstairs - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F12BD" # mdi:stairs-up - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Upstairs" - - button: - x: 500 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: office - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 100% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xD5E3FF - text: "\U000F1239" # mdi:desk - - label: - align: top_mid - y: 46 - text_color: 0xE3E2E6 - text: "Office" - - button: - x: 600 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: garage - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06D9" # mdi:garage - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Garage" - - button: - x: 700 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: outside - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F0531" # mdi:tree - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Outside" - -############################################# -# Garage # -############################################# - - id: garage - widgets: - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Garage" - - - button: - id: btn_garage_cam_light - x: 24 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.esp32_saturn4_cam_esp32_saturn4_cam_light - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Saturn Cam Light" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_garage_fume_exhaust_fan - x: 278 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_pink - checked: - bg_color: 0xFFD7F1 - bg_opa: 100% - border_color: 0xFFD7F1 - border_opa: 100% - text_color: 0x22001F - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.fume_exhaust_fan - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F1A26" # mdi:toggle-switch-variant-off - - label: - x: 18 - y: 16 - styles: md3_meta - text: "SWITCH" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Fume Fan" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_garage_resin_printer_heater - x: 532 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_amber - checked: - bg_color: 0xFFDCBE - bg_opa: 100% - border_color: 0xFFDCBE - border_opa: 100% - text_color: 0x1A0C00 - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.resin_printer_heater - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F1A26" # mdi:toggle-switch-variant-off - - label: - x: 18 - y: 16 - styles: md3_meta - text: "SWITCH" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Resin Heater" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - obj: - x: 0 - y: 408 - width: 800 - height: 72 - styles: md3_nav_bar - widgets: - - obj: - x: 8 - y: 16 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: nav_battery_icon_garage - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline - - button: - x: 100 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: home - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04B9" # mdi:sofa - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Family" - - button: - x: 200 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: living_room - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06B5" # mdi:lamp - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Living" - - button: - x: 300 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: kitchen_page - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04DE" # mdi:stove - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Kitchen" - - button: - x: 400 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: upstairs - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F12BD" # mdi:stairs-up - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Upstairs" - - button: - x: 500 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: office - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F1239" # mdi:desk - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Office" - - button: - x: 600 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: garage - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 100% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xD5E3FF - text: "\U000F06D9" # mdi:garage - - label: - align: top_mid - y: 46 - text_color: 0xE3E2E6 - text: "Garage" - - button: - x: 700 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: outside - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F0531" # mdi:tree - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Outside" - -############################################# -# Outside # -############################################# - - id: outside - widgets: - - obj: - x: 0 - y: 0 - width: 800 - height: 64 - styles: md3_header - widgets: - - label: - x: 24 - y: 20 - text_color: 0xE3E2E6 - text: "Outside" - - - button: - id: btn_outside_patio_light - x: 24 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_blue - checked: - bg_color: 0xA8C7FA - bg_opa: 100% - border_color: 0xA8C7FA - border_opa: 100% - text_color: 0x001A33 - on_click: - - homeassistant.service: - service: light.toggle - data: - entity_id: light.patio_light_1 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F0335" # mdi:lightbulb - - label: - x: 18 - y: 16 - styles: md3_meta - text: "LIGHT" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Patio Light" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_outside_lamppost_outlet_1 - x: 278 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_pink - checked: - bg_color: 0xFFD7F1 - bg_opa: 100% - border_color: 0xFFD7F1 - border_opa: 100% - text_color: 0x22001F - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.lamppost_outlets_socket_1 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F07E9" # mdi:power-socket-us - - label: - x: 18 - y: 16 - styles: md3_meta - text: "OUTLET" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Lamppost 1" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_outside_lamppost_outlet_2 - x: 532 - y: 80 - width: 244 - height: 136 - checkable: true - styles: md3_card_amber - checked: - bg_color: 0xFFDCBE - bg_opa: 100% - border_color: 0xFFDCBE - border_opa: 100% - text_color: 0x1A0C00 - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.lamppost_outlets_socket_2 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F07E9" # mdi:power-socket-us - - label: - x: 18 - y: 16 - styles: md3_meta - text: "OUTLET" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Lamppost 2" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - button: - id: btn_outside_porch_decor_outlet - x: 24 - y: 228 - width: 244 - height: 136 - checkable: true - styles: md3_card_green - checked: - bg_color: 0x66F9B9 - bg_opa: 100% - border_color: 0x66F9B9 - border_opa: 100% - text_color: 0x001F15 - on_click: - - homeassistant.service: - service: switch.toggle - data: - entity_id: switch.washing_machine_socket_1 - widgets: - - label: - x: 204 - y: 16 - styles: md3_icon - text_font: mdi_icons - text: "\U000F07E9" # mdi:power-socket-us - - label: - x: 18 - y: 16 - styles: md3_meta - text: "OUTLET" - - label: - x: 18 - y: 56 - styles: md3_title - text: "Porch Decor 1" - - label: - x: 18 - y: 90 - styles: md3_meta - text: "Tap to toggle" - - - obj: - x: 0 - y: 408 - width: 800 - height: 72 - styles: md3_nav_bar - widgets: - - obj: - x: 8 - y: 16 - width: 84 - height: 40 - styles: md3_chip - widgets: - - label: - id: nav_battery_icon_outside - align: center - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F10CD" # mdi:battery-alert-variant-outline - - button: - x: 100 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: home - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04B9" # mdi:sofa - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Family" - - button: - x: 200 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: living_room - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06B5" # mdi:lamp - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Living" - - button: - x: 300 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: kitchen_page - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F04DE" # mdi:stove - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Kitchen" - - button: - x: 400 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: upstairs - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F12BD" # mdi:stairs-up - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Upstairs" - - button: - x: 500 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: office - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F1239" # mdi:desk - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Office" - - button: - x: 600 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: garage - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 0% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xC4C6CF - text: "\U000F06D9" # mdi:garage - - label: - align: top_mid - y: 46 - text_color: 0xC4C6CF - text: "Garage" - - button: - x: 700 - y: 0 - width: 100 - height: 72 - styles: md3_nav_item - on_click: - - lvgl.page.show: outside - widgets: - - obj: - x: 29 - y: 4 - width: 56 - height: 32 - bg_color: 0x17488B - bg_opa: 100% - radius: 16 - border_width: 0 - - label: - align: top_mid - y: 10 - text_font: mdi_icons - text_color: 0xD5E3FF - text: "\U000F0531" # mdi:tree - - label: - align: top_mid - y: 46 - text_color: 0xE3E2E6 - text: "Outside" diff --git a/esphome/jarvis-satellite/jarvis-satellite-audio.yaml b/esphome/jarvis-satellite/jarvis-satellite-audio.yaml deleted file mode 100644 index 340ec60..0000000 --- a/esphome/jarvis-satellite/jarvis-satellite-audio.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# ===== I2S Audio ===== -i2s_audio: - - id: i2s_in - i2s_lrclk_pin: ${i2s_in_lrclk_pin} - i2s_bclk_pin: ${i2s_in_bclk_pin} - - id: i2s_out - i2s_lrclk_pin: ${i2s_out_lrclk_pin} - i2s_bclk_pin: ${i2s_out_bclk_pin} - -microphone: - - platform: i2s_audio - id: mic - i2s_audio_id: i2s_in - i2s_din_pin: ${mic_din_pin} - adc_type: external - pdm: false - bits_per_sample: 32bit - channel: left - -speaker: - - platform: i2s_audio - id: spk - i2s_audio_id: i2s_out - i2s_dout_pin: ${speaker_dout_pin} - dac_type: external - bits_per_sample: 32bit - channel: mono - sample_rate: 16000 - # Voice Assistant needs the speaker to fully stop after playback so it can - # leave RESPONSE_FINISHED and return to IDLE for the next wake word. - timeout: 500ms - -rtttl: - id: volume_beep - speaker: spk - -# ===== Wake Word ===== -micro_wake_word: - id: mww - microphone: mic - models: - - model: okay_nabu - id: wake_word_model - on_wake_word_detected: - - voice_assistant.start: - wake_word: !lambda return wake_word; - - light.turn_on: - id: led_bar - effect: "Listening Pulse" diff --git a/esphome/jarvis-satellite/jarvis-satellite-base.yaml b/esphome/jarvis-satellite/jarvis-satellite-base.yaml deleted file mode 100644 index 20e862d..0000000 --- a/esphome/jarvis-satellite/jarvis-satellite-base.yaml +++ /dev/null @@ -1,125 +0,0 @@ -packages: - jarvis-satellite-led: !include jarvis-satellite-led.yaml - jarvis-satellite-voice: !include jarvis-satellite-voice.yaml - jarvis-satellite-audio: !include jarvis-satellite-audio.yaml - jarvis-satellite-ui: !include jarvis-satellite-ui.yaml - -substitutions: - # Flash & PSRAM - flash_size: "8MB" - psram_mode: "quad" - psram_speed: "80MHz" - # CPU Frequency - cpu_frequency: "240MHz" - - # I2S Microphone pins - i2s_in_lrclk_pin: GPIO6 - i2s_in_bclk_pin: GPIO7 - mic_din_pin: GPIO4 - - # I2S Speaker pins - i2s_out_lrclk_pin: GPIO45 - i2s_out_bclk_pin: GPIO46 - speaker_dout_pin: GPIO8 - - # LED bar - led_pin: GPIO16 - led_num_leds: "8" - - # Microphone sensitivity (tuned for INMP441) - mic_noise_suppression_level: "2" - mic_auto_gain: "10dBFS" - mic_volume_multiplier: "4.0" - - # Button pins - btn_vol_up_pin: GPIO1 - btn_vol_down_pin: GPIO2 - btn_mute_pin: GPIO3 - btn_wake_pin: GPIO10 - - # Change this to true in case you have a hidden SSID at home. - hidden_ssid: "true" - - -globals: - - id: volume_level - type: float - restore_value: true - initial_value: '0.8' - -esphome: - platformio_options: - board_build.flash_mode: dio - on_boot: - - priority: -100 - then: - - lambda: id(spk).set_volume(id(volume_level)); - -esp32: - board: esp32-s3-devkitc-1 - variant: esp32s3 - cpu_frequency: ${cpu_frequency} - flash_size: ${flash_size} - framework: - type: esp-idf - version: recommended - sdkconfig_options: - CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" - CONFIG_ESP32S3_DATA_CACHE_64KB: "y" - CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" - CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y" - - # Moves instructions and read only data from flash into PSRAM on boot. - # Both enabled allows instructions to execute while a flash operation is in progress without needing to be placed in IRAM. - # Considerably speeds up mWW at the cost of using more PSRAM. - CONFIG_SPIRAM_RODATA: "y" - CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y" - - CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" - CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" - - CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y" - CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y" - -wifi: - id: wifi_id - fast_connect: ${hidden_ssid} - -network: - enable_ipv6: true - -http_request: - -web_server: - port: 80 - -psram: - mode: ${psram_mode} - speed: ${psram_speed} - -captive_portal: - -api: - encryption: - key: !secret api_encryption_key - on_client_connected: - - delay: 50ms - - if: - condition: - not: - lambda: 'return id(mww).is_running();' - then: - - micro_wake_word.start: - on_client_disconnected: - - micro_wake_word.stop: - - voice_assistant.stop: - -logger: - level: DEBUG - logs: - micro_wake_word: DEBUG - voice_assistant: DEBUG - -ota: - - platform: esphome - password: !secret ota_password diff --git a/esphome/jarvis-satellite/jarvis-satellite-buttons.yaml b/esphome/jarvis-satellite/jarvis-satellite-buttons.yaml deleted file mode 100644 index f1fea1e..0000000 --- a/esphome/jarvis-satellite/jarvis-satellite-buttons.yaml +++ /dev/null @@ -1,111 +0,0 @@ -# ===== Physical Buttons ===== -# Note: esp32_touch is not supported on ESP32-S3 with ESP-IDF. -# Wire physical momentary buttons to these pins (active LOW with internal pullup). -binary_sensor: - - platform: gpio - pin: - number: ${btn_vol_up_pin} - mode: INPUT_PULLUP - inverted: true - name: "Volume Up" - on_press: - - lambda: |- - float vol = min(1.0f, id(volume_level) + 0.1f); - id(volume_level) = vol; - id(spk).set_volume(vol); - - rtttl.play: - id: volume_beep - rtttl: "beep:d=32,o=6,b=180:c" - - light.turn_on: - id: led_bar - brightness: 100% - effect: "none" - - delay: 150ms - - light.turn_on: - id: led_bar - effect: "Idle Breathe" - - - platform: gpio - pin: - number: ${btn_vol_down_pin} - mode: INPUT_PULLUP - inverted: true - name: "Volume Down" - on_press: - - lambda: |- - float vol = max(0.0f, id(volume_level) - 0.1f); - id(volume_level) = vol; - id(spk).set_volume(vol); - - rtttl.play: - id: volume_beep - rtttl: "beep:d=32,o=5,b=180:c" - - light.turn_on: - id: led_bar - brightness: 30% - effect: "none" - - delay: 150ms - - light.turn_on: - id: led_bar - effect: "Idle Breathe" - - - platform: gpio - pin: - number: ${btn_mute_pin} - mode: INPUT_PULLUP - inverted: true - name: "Mic Mute" - on_press: - - switch.toggle: mic_mute - - - platform: gpio - pin: - number: ${btn_wake_pin} - mode: INPUT_PULLUP - inverted: true - name: "Wake" - on_press: - - if: - condition: - switch.is_off: mic_mute - then: - - voice_assistant.start: - - light.turn_on: - id: led_bar - effect: "Listening Pulse" - else: - - light.turn_on: - id: led_bar - effect: "Error Flash" - - delay: 500ms - - light.turn_on: - id: led_bar - red: 100% - green: 30% - blue: 0% - brightness: 50% - effect: "none" - -# ===== Mic Mute Switch ===== -switch: - - platform: template - id: mic_mute - name: "Microphone Mute" - icon: "mdi:microphone-off" - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - on_turn_on: - - voice_assistant.stop: - - light.turn_on: - id: led_bar - effect: "none" - - light.turn_on: - id: led_bar - red: 100% - green: 30% - blue: 0% - brightness: 50% - on_turn_off: - - light.turn_on: - id: led_bar - effect: "Idle Breathe" - - voice_assistant.start: \ No newline at end of file diff --git a/esphome/jarvis-satellite/jarvis-satellite-led.yaml b/esphome/jarvis-satellite/jarvis-satellite-led.yaml deleted file mode 100644 index 7911bb1..0000000 --- a/esphome/jarvis-satellite/jarvis-satellite-led.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# ===== LED Bar (WS2812B) ===== -light: - - platform: esp32_rmt_led_strip - id: led_bar - chipset: WS2812 - rgb_order: GRB - pin: ${led_pin} - num_leds: ${led_num_leds} - name: "Voice Satellite LEDs" - default_transition_length: 200ms - effects: - - addressable_lambda: - name: "Idle Breathe" - update_interval: 50ms - lambda: |- - static float brightness = 0; - static int direction = 1; - brightness += direction * 0.02; - if (brightness >= 1.0) { brightness = 1.0; direction = -1; } - if (brightness <= 0.1) { brightness = 0.1; direction = 1; } - for (int i = 0; i < it.size(); i++) { - it[i] = Color(0, 0, (int)(40 * brightness)); - } - - addressable_lambda: - name: "Listening Pulse" - update_interval: 30ms - lambda: |- - static int pos = 0; - pos = (pos + 1) % (it.size() * 2); - for (int i = 0; i < it.size(); i++) { - int dist = abs(pos - i); - if (pos >= it.size()) dist = abs((it.size() * 2 - pos) - i); - int bright = max(0, 255 - dist * 60); - it[i] = Color(0, bright, bright); - } - - addressable_lambda: - name: "Processing" - update_interval: 80ms - lambda: |- - static int offset = 0; - offset = (offset + 1) % it.size(); - for (int i = 0; i < it.size(); i++) { - if ((i + offset) % 2 == 0) { - it[i] = Color(80, 0, 180); - } else { - it[i] = Color(0, 0, 0); - } - } - - addressable_lambda: - name: "Speaking" - update_interval: 40ms - lambda: |- - static int wave = 0; - wave = (wave + 1) % 16; - for (int i = 0; i < it.size(); i++) { - int bright = (sin((wave + i * 2) * 0.4) + 1) * 127; - it[i] = Color(0, bright, 0); - } - - addressable_lambda: - name: "Error Flash" - update_interval: 100ms - lambda: |- - static bool on = false; - on = !on; - for (int i = 0; i < it.size(); i++) { - it[i] = on ? Color(255, 0, 0) : Color(0, 0, 0); - } \ No newline at end of file diff --git a/esphome/jarvis-satellite/jarvis-satellite-ui.yaml b/esphome/jarvis-satellite/jarvis-satellite-ui.yaml deleted file mode 100644 index c4905b9..0000000 --- a/esphome/jarvis-satellite/jarvis-satellite-ui.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# ===== Web UI / HA Entities ===== - -script: - - id: publish_current_time - then: - - lambda: |- - auto time_now = id(homeassistant_time).now(); - id(current_time_sensor).publish_state(time_now.strftime("%H:%M")); - -time: - - platform: homeassistant - id: homeassistant_time - on_time_sync: - - script.execute: publish_current_time - on_time: - - seconds: 0 - minutes: /1 - then: - - script.execute: publish_current_time - -text_sensor: - - platform: template - name: "Current device time" - id: current_time_sensor - icon: mdi:clock - -number: - - platform: template - name: "Volume" - id: volume_number - icon: mdi:volume-high - entity_category: config - min_value: 0 - max_value: 100 - step: 5 - unit_of_measurement: "%" - lambda: return id(volume_level) * 100.0f; - set_action: - - lambda: |- - float vol = x / 100.0f; - id(volume_level) = vol; - id(spk).set_volume(vol); - -select: - - platform: logger - name: "Logger Level" - disabled_by_default: true - -button: - - platform: restart - name: "Restart" - icon: mdi:restart - entity_category: config diff --git a/esphome/jarvis-satellite/jarvis-satellite-voice.yaml b/esphome/jarvis-satellite/jarvis-satellite-voice.yaml deleted file mode 100644 index e65b8f3..0000000 --- a/esphome/jarvis-satellite/jarvis-satellite-voice.yaml +++ /dev/null @@ -1,97 +0,0 @@ -# ===== Wake Word Sensitivity ===== -select: - - platform: template - name: "Wake Word Sensitivity" - optimistic: true - initial_option: "Slightly sensitive" - restore_value: true - entity_category: config - icon: mdi:ear-hearing - options: - - Slightly sensitive - - Moderately sensitive - - Very sensitive - on_value: - lambda: |- - if (x == "Slightly sensitive") { - id(wake_word_model).set_probability_cutoff(247); // 0.97 - } else if (x == "Moderately sensitive") { - id(wake_word_model).set_probability_cutoff(235); // 0.92 - } else if (x == "Very sensitive") { - id(wake_word_model).set_probability_cutoff(212); // 0.83 - } - -# ===== Voice Assistant ===== -voice_assistant: - id: va - microphone: mic - speaker: spk - micro_wake_word: mww - noise_suppression_level: ${mic_noise_suppression_level} - auto_gain: ${mic_auto_gain} - volume_multiplier: ${mic_volume_multiplier} - on_listening: - - logger.log: "[VA] on_listening — mic open, sending audio to HA" - - light.turn_on: - id: led_bar - effect: "Listening Pulse" - on_stt_vad_end: - - logger.log: "[VA] on_stt_vad_end — speech detected, processing" - - light.turn_on: - id: led_bar - effect: "Processing" - on_stt_end: - - lambda: |- - ESP_LOGI("va", "STT result: %s", x.c_str()); - - light.turn_off: - id: led_bar - on_tts_start: - - lambda: |- - ESP_LOGI("va", "TTS start: %s", x.c_str()); - - light.turn_on: - id: led_bar - effect: "Speaking" - on_tts_end: - - logger.log: "[VA] on_tts_end" - on_tts_stream_end: - - logger.log: "[VA] on_tts_stream_end" - on_error: - - lambda: |- - ESP_LOGE("va", "VA error: code=%s, message=%s", code.c_str(), message.c_str()); - - if: - condition: - lambda: 'return code != "duplicate_wake_up_detected";' - then: - - light.turn_on: - id: led_bar - effect: "Error Flash" - - delay: 2s - - micro_wake_word.start: - on_end: - - logger.log: "[VA] on_end" - - wait_until: - not: - voice_assistant.is_running: - - light.turn_off: - id: led_bar - - if: - condition: - and: - - voice_assistant.connected: - - not: - micro_wake_word.is_running: - then: - - micro_wake_word.start: - on_idle: - - logger.log: "[VA] on_idle" - - light.turn_on: - id: led_bar - effect: "Idle Breathe" - - if: - condition: - and: - - voice_assistant.connected: - - not: - micro_wake_word.is_running: - then: - - micro_wake_word.start: diff --git a/esphome/jordyn-a1-camera.yaml b/esphome/jordyn-a1-camera.yaml deleted file mode 100644 index a3e4547..0000000 --- a/esphome/jordyn-a1-camera.yaml +++ /dev/null @@ -1,135 +0,0 @@ -substitutions: - device_name: jordyn-a1-camera - friendly_name: "Jordyn A1 Camera" - -esphome: - name: ${device_name} - friendly_name: ${friendly_name} - -esp32: - board: esp32dev - framework: - type: esp-idf - #cpu_frequency: 240MHZ - -# Enable logging -logger: - -# Enable Home Assistant API -api: - encryption: - key: "PDB5LTKAcNWMC+MeE03/SKpqX42pcnITzmfwu761LjI=" - -ota: - - platform: esphome - password: "d207583702e4b979e28a43f6f52d19ae" - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password - - # Enable fallback hotspot (captive portal) in case wifi connection fails - ap: - ssid: "Jordyn-A1-Camera" - password: "jN4rzGokp5mt" - - # Prevents aggressive reconnection attempts - power_save_mode: none # Use 'light' for battery devices - - # Slower but more reliable connection - fast_connect: true - - # Handle connection failures gracefully - on_connect: - - logger.log: "Wi-Fi connected!" - on_disconnect: - - logger.log: "Wi-Fi disconnected!" - -# Reduce mDNS traffic -mdns: - disabled: false # Set to true if you use static IPs and don't need discovery - -#captive_portal: - -web_server: - port: 80 - -i2c: - - id: camera_i2c - sda: GPIO26 - scl: GPIO27 - -psram: - mode: quad - speed: 80MHz - -esp32_camera: - external_clock: - pin: GPIO0 - frequency: 20MHz - i2c_id: camera_i2c - data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35] - vsync_pin: GPIO25 - href_pin: GPIO23 - pixel_clock_pin: GPIO22 - power_down_pin: GPIO32 - resolution: 1024x768 - jpeg_quality: 20 - vertical_flip: False - horizontal_mirror: False - max_framerate: 40 fps - idle_framerate: 0.05 fps - - # Image settings - name: esp32-jordyn-a1-cam - # brightness: 1 - # #contrast: 0 - # agc_gain_ceiling: 2X - # agc_mode: MANUAL - # agc_value: 5 - # ... - -esp32_camera_web_server: - - port: 80 - mode: STREAM - - port: 8080 - mode: SNAPSHOT - -switch: - - platform: gpio - name: "jordyn-a1-cam-flash" - pin: 4 - -# Diagnostic sensors -sensor: - - platform: wifi_signal - name: "${device_name} WiFi Signal" - update_interval: 60s - - - platform: uptime - name: "${device_name} Uptime" - update_interval: 60s - -text_sensor: - - platform: wifi_info - ip_address: - name: "${device_name} IP Address" - ssid: - name: "${device_name} Connected SSID" - bssid: - name: "${device_name} BSSID" - -# Watchdog to auto-reboot if things go wrong -interval: - - interval: 5min - then: - - if: - condition: - and: - - lambda: 'return millis() > 300000;' # >5 min uptime - - not: - wifi.connected: - then: - - logger.log: "Wi-Fi not connected for 5 min, rebooting..." - - delay: 10s - - lambda: 'App.safe_reboot();' \ No newline at end of file diff --git a/esphome/laundry-litter-box-camera.yaml b/esphome/laundry-litter-box-camera.yaml deleted file mode 100644 index dd9e40e..0000000 --- a/esphome/laundry-litter-box-camera.yaml +++ /dev/null @@ -1,122 +0,0 @@ -substitutions: - device_name: laundry-litter-box-camera - friendly_name: "Laundry Litter Box Camera" - -esphome: - name: ${device_name} - friendly_name: ${friendly_name} - -esp32: - board: esp32dev - framework: - type: esp-idf - -logger: - -api: - encryption: - key: !secret api_encryption_key - -ota: - - platform: esphome - password: !secret ota_password - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password - manual_ip: - static_ip: 192.168.69.220 - gateway: 192.168.69.1 - subnet: 255.255.255.0 - ap: - ssid: "laundry-litter-cam-fallback" - password: !secret fallback_password - # Prevents aggressive reconnection attempts - power_save_mode: none # Use 'light' for battery devices - - # Slower but more reliable connection - fast_connect: true - - # Handle connection failures gracefully - on_connect: - - logger.log: "Wi-Fi connected!" - on_disconnect: - - logger.log: "Wi-Fi disconnected!" - -# Reduce mDNS traffic -mdns: - disabled: true # Set to true if you use static IPs and don't need discovery - -# Diagnostic sensors -sensor: - - platform: wifi_signal - name: "${device_name} WiFi Signal" - update_interval: 60s - - - platform: uptime - name: "${device_name} Uptime" - update_interval: 60s - -text_sensor: - - platform: wifi_info - ip_address: - name: "${device_name} IP Address" - ssid: - name: "${device_name} Connected SSID" - bssid: - name: "${device_name} BSSID" - -# Watchdog to auto-reboot if things go wrong -interval: - - interval: 5min - then: - - if: - condition: - and: - - lambda: 'return millis() > 300000;' # >5 min uptime - - not: - wifi.connected: - then: - - logger.log: "Wi-Fi not connected for 5 min, rebooting..." - - delay: 10s - - lambda: 'App.safe_reboot();' - -captive_portal: - -i2c: - - id: camera_i2c - sda: GPIO26 - scl: GPIO27 - -psram: - mode: quad - speed: 80MHz - -esp32_camera: - external_clock: - pin: GPIO0 - frequency: 20MHz - i2c_id: camera_i2c - data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35] - vsync_pin: GPIO25 - href_pin: GPIO23 - pixel_clock_pin: GPIO22 - power_down_pin: GPIO32 - resolution: 1024x768 - jpeg_quality: 15 - vertical_flip: true - horizontal_mirror: false - max_framerate: 20 fps - idle_framerate: 0.05 fps - name: ${device_name} - -esp32_camera_web_server: - - port: 8080 - mode: STREAM - - port: 8081 - mode: SNAPSHOT - -switch: - - platform: gpio - name: "${device_name}-flash" - pin: 4 \ No newline at end of file diff --git a/esphome/office-jarvis-satellite.yaml b/esphome/office-jarvis-satellite.yaml deleted file mode 100644 index 8330023..0000000 --- a/esphome/office-jarvis-satellite.yaml +++ /dev/null @@ -1,33 +0,0 @@ -packages: - jarvis-satellite: !include jarvis-satellite/jarvis-satellite-base.yaml - -### Change the following for each satellite device, e.g. "jarvis-satellite-office" and "jarvis-satellite-bedroom" and to match each ESP board pinout -substitutions: - flash_size: "8MB" - psram_mode: "quad" - psram_speed: "80MHz" - i2s_in_lrclk_pin: GPIO6 - i2s_in_bclk_pin: GPIO7 - mic_din_pin: GPIO4 - i2s_out_lrclk_pin: GPIO45 - i2s_out_bclk_pin: GPIO46 - speaker_dout_pin: GPIO8 - led_pin: GPIO16 - led_num_leds: "8" - btn_vol_up_pin: GPIO1 - btn_vol_down_pin: GPIO2 - btn_mute_pin: GPIO3 - btn_wake_pin: GPIO10 - cpu_frequency: "240MHz" - - -esphome: - name: office-jarvis-satellite - friendly_name: "Office Jarvis" - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password - ap: - ssid: "Jarvis-Satellite-AP" - password: !secret wifi_password diff --git a/esphome/polly-a1-mini-camera.yaml b/esphome/polly-a1-mini-camera.yaml deleted file mode 100644 index 909e555..0000000 --- a/esphome/polly-a1-mini-camera.yaml +++ /dev/null @@ -1,133 +0,0 @@ -substitutions: - device_name: polly-a1-mini-camera - friendly_name: "Polly's A1 Camera" - -esphome: - name: ${device_name} - friendly_name: ${friendly_name} - -esp32: - board: esp32dev - framework: - type: esp-idf -# minimum_chip_revision: 3.1 - -# Enable logging -logger: - level: INFO - -# Enable Home Assistant API -api: - encryption: - key: "iLF5QCgTVCwgaA5vjTGez03Dfjvns8JvkcwmUN4NXR8=" - -ota: - - platform: esphome - password: "58f86a5e60abe60886cec2f65a1d3da5" - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password - manual_ip: - static_ip: 192.168.69.194 - gateway: 192.168.69.1 - subnet: 255.255.255.0 - # Enable fallback hotspot (captive portal) in case wifi connection fails - ap: - ssid: "${friendly_name} Fallback" - password: !secret wifi_password - - # Prevents aggressive reconnection attempts - power_save_mode: none # Use 'light' for battery devices - - # Slower but more reliable connection - fast_connect: true - - # Handle connection failures gracefully - on_connect: - - logger.log: "Wi-Fi connected!" - on_disconnect: - - logger.log: "Wi-Fi disconnected!" - -# Reduce mDNS traffic -mdns: - disabled: true # Set to true if you use static IPs and don't need discovery - -#captive_portal: - -web_server: - port: 80 - -i2c: - - id: camera_i2c - sda: GPIO26 - scl: GPIO27 - -psram: - mode: quad - speed: 80MHz - -esp32_camera: - external_clock: - pin: GPIO0 - frequency: 20MHz - i2c_id: camera_i2c - data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35] - vsync_pin: GPIO25 - href_pin: GPIO23 - pixel_clock_pin: GPIO22 - power_down_pin: GPIO32 - resolution: 1024X768 - jpeg_quality: 20 - vertical_flip: False - horizontal_mirror: True - max_framerate: 20 fps - idle_framerate: 0.05 fps - - # Image settings - name: ${device_name} - -esp32_camera_web_server: - - port: 8080 - mode: STREAM - - port: 8081 - mode: SNAPSHOT - -switch: - - platform: gpio - name: "${device_name}-flash" - pin: 4 - -# Diagnostic sensors -sensor: - - platform: wifi_signal - name: "${device_name} WiFi Signal" - update_interval: 60s - - - platform: uptime - name: "${device_name} Uptime" - update_interval: 60s - -text_sensor: - - platform: wifi_info - ip_address: - name: "${device_name} IP Address" - ssid: - name: "${device_name} Connected SSID" - bssid: - name: "${device_name} BSSID" - -# Watchdog to auto-reboot if things go wrong -interval: - - interval: 5min - then: - - if: - condition: - and: - - lambda: 'return millis() > 300000;' # >5 min uptime - - not: - wifi.connected: - then: - - logger.log: "Wi-Fi not connected for 5 min, rebooting..." - - delay: 10s - - lambda: 'App.safe_reboot();' \ No newline at end of file diff --git a/esphome/respeaker-satellite-fr.yaml b/esphome/respeaker-satellite-fr.yaml deleted file mode 100644 index fdb0228..0000000 --- a/esphome/respeaker-satellite-fr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -packages: - respeaker-satellite: !include respeaker-satellite/respeaker-satellite-base.yaml - -esphome: - name: respeaker-satellite-fr - friendly_name: "Respeaker Satellite Family Room" - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password \ No newline at end of file diff --git a/esphome/respeaker-satellite-lr.yaml b/esphome/respeaker-satellite-lr.yaml deleted file mode 100644 index ea37fe3..0000000 --- a/esphome/respeaker-satellite-lr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -packages: - respeaker-satellite: !include respeaker-satellite/respeaker-satellite-base.yaml - -esphome: - name: respeaker-satellite-lr - friendly_name: "Respeaker Satellite Living Room" - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password \ No newline at end of file diff --git a/esphome/respeaker-satellite-mbr.yaml b/esphome/respeaker-satellite-mbr.yaml deleted file mode 100644 index f26b4da..0000000 --- a/esphome/respeaker-satellite-mbr.yaml +++ /dev/null @@ -1,10 +0,0 @@ -packages: - respeaker-satellite: !include respeaker-satellite/respeaker-satellite-base.yaml - -esphome: - name: respeaker-satellite-mbr - friendly_name: "Respeaker Satellite Master Bedroom" - -wifi: - ssid: !secret wifi_iot_ssid - password: !secret wifi_password \ No newline at end of file diff --git a/esphome/respeaker-satellite/respeaker-satellite-base.yaml b/esphome/respeaker-satellite/respeaker-satellite-base.yaml deleted file mode 100644 index 6e5ff01..0000000 --- a/esphome/respeaker-satellite/respeaker-satellite-base.yaml +++ /dev/null @@ -1,1608 +0,0 @@ -substitutions: - # Phases of the Voice Assistant - # The voice assistant is ready to be triggered by a wake word - voice_assist_idle_phase_id: '1' - # The voice assistant is waiting for a voice command (after being triggered by the wake word) - voice_assist_waiting_for_command_phase_id: '2' - # The voice assistant is listening for a voice command - voice_assist_listening_for_command_phase_id: '3' - # The voice assistant is currently processing the command - voice_assist_thinking_phase_id: '4' - # The voice assistant is replying to the command - voice_assist_replying_phase_id: '5' - # The voice assistant is not ready - voice_assist_not_ready_phase_id: '10' - # The voice assistant encountered an error - voice_assist_error_phase_id: '11' - # Change this to true in case you ahve a hidden SSID at home. - hidden_ssid: "false" - # Substitutions for audio files - mute_switch_on_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_on.flac - mute_switch_off_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_off.flac - timer_finished_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac - wake_word_triggered_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac - center_button_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_press.flac - center_button_double_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_double_press.flac - center_button_triple_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_triple_press.flac - center_button_long_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_long_press.flac - factory_reset_initiated_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_initiated.mp3 - factory_reset_cancelled_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_cancelled.mp3 - factory_reset_confirmed_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_confirmed.mp3 - error_cloud_expired_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3 - - -esphome: - project: - name: formatbce.Respeaker Lite Satellite - version: 2026.5.0 - min_version: 2026.5.0 - on_boot: - - priority: 375 - then: - - sensor.template.publish: - id: next_timer - state: -1 - # Run the script to refresh the LED status - - script.execute: control_leds - # If after 10 minutes, the device is still initializing (It did not yet connect to Home Assistant), turn off the init_in_progress variable and run the script to refresh the LED status - - delay: 10min - - if: - condition: - lambda: return id(init_in_progress); - then: - - lambda: id(init_in_progress) = false; - - script.execute: control_leds - - priority: -100 - then: - - lambda: |- - auto call = id(alarm_action).make_call(); - call.set_option(id(saved_alarm_action)); - call.perform(); - - lambda: |- - setenv("TZ", id(saved_time_zone).c_str(), 1); - tzset(); - on_shutdown: - then: - # Prevent loud noise on software restart - - lambda: id(respeaker).mute_speaker(); - -esp32: - board: esp32-s3-devkitc-1 - cpu_frequency: 240MHz - variant: esp32s3 - flash_size: 8MB - framework: - type: esp-idf - version: recommended - components: - - espressif/esp-nn==1.1.2 # TODO remove this when MWW stops failing - sdkconfig_options: - CONFIG_ESP32S3_DATA_CACHE_64KB: "y" - CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" - CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y" - - # Moves instructions and read only data from flash into PSRAM on boot. - # Both enabled allows instructions to execute while a flash operation is in progress without needing to be placed in IRAM. - # Considerably speeds up mWW at the cost of using more PSRAM. - CONFIG_SPIRAM_RODATA: "y" - CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y" - - CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" - CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" - - CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y" - CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y" # TLS1.3 support isn't enabled by default in IDF 5.1.5 - -wifi: - id: wifi_id - fast_connect: ${hidden_ssid} - on_connect: - - lambda: id(improv_ble_in_progress) = false; - - script.execute: control_leds - on_disconnect: - - script.execute: control_leds - -network: - enable_ipv6: true - -logger: - level: debug - initial_level: debug - logs: - sensor: WARN # avoids logging debug sensor updates -# hardware_uart: uart0 #Uncomment to see serial logs via USB connection. Comment out after debufgging - this line introduces noise on speaker... - -api: - id: api_id - actions: - - action: start_va - then: - - voice_assistant.start - - action: stop_va - then: - - voice_assistant.stop - - action: set_time_zone - variables: - posix_time_zone: string - then: - - lambda: |- - setenv("TZ", posix_time_zone.c_str(), 1); - tzset(); - id(saved_time_zone) = posix_time_zone; - id(publish_current_time).execute(); - - on_client_connected: - - script.execute: control_leds - on_client_disconnected: - - script.execute: control_leds - encryption: - -ota: - - platform: esphome - id: ota_esphome - password: !secret ota_password - -i2c: - - id: internal_i2c - sda: GPIO5 - scl: GPIO6 - frequency: 400kHz - -psram: - mode: octal - speed: 80MHz - ignore_not_found: false # The VPE has PSRAM, so this is safe. Allows configuring WiFi driver to use more resources (done automatically by the speaker media player) - -globals: - - id: init_in_progress - type: bool - restore_value: no - initial_value: 'true' - # Global variable storing the state of ImprovBLE. Used to draw different LED animations - - id: improv_ble_in_progress - type: bool - restore_value: no - initial_value: 'false' - # Global variable tracking the phase of the voice assistant (defined above). Initialized to not_ready - - id: voice_assistant_phase - type: int - restore_value: no - initial_value: ${voice_assist_not_ready_phase_id} - - id: saved_time_zone - type: std::string - restore_value: yes - initial_value: '"UTC0"' - - id: saved_alarm_action - type: std::string - restore_value: yes - initial_value: '"Play sound"' - # Global variable storing the first active timer - - id: first_active_timer - type: voice_assistant::Timer - restore_value: no - # Global variable storing if a timer is active - - id: is_timer_active - type: bool - restore_value: no - # Global variable storing if a factory reset was requested. If it is set to true, the device will factory reset once the center button is released - - id: factory_reset_requested - type: bool - restore_value: no - initial_value: 'false' - -# Time sync from Home Assistant -time: - - platform: homeassistant - id: homeassistant_time - on_time: - # Every 1 minute - - seconds: 0 - then: - - script.execute: check_alarm - on_time_sync: - - script.execute: publish_current_time - -datetime: - # schedule the alarm time from Home Assistant - - platform: template - icon: mdi:bell-ring - name: "Alarm time" - id: alarm_time - type: time - initial_value: "00:00:00" - restore_value: true - optimistic: true - internal: false - set_action: - then: - - switch.turn_on: alarm_on - - -switch: - # Hardware speaker mute - - platform: template - id: speaker_mute_switch - name: Speaker mute - icon: mdi:volume-mute - internal: true - optimistic: true - turn_on_action: - - lambda: id(respeaker).mute_speaker(); - turn_off_action: - - lambda: id(respeaker).unmute_speaker(); - # stateless momentary mic mute switch - - platform: gpio - internal: true - pin: - number: GPIO4 # D3 - inverted: true - id: mute_toggle - on_turn_on: - - delay: 300ms - - switch.turn_off: mute_toggle - # stateful user facing mic mute switch - - platform: template - id: mic_mute_switch - name: Mic mute - icon: mdi:microphone-off - optimistic: false - lambda: |- - if (id(mute_state).state) { - return true; - } else { - return false; - } - on_turn_on: - - if: - condition: - and: - - lambda: return !id(init_in_progress); - - switch.is_on: mute_sound - then: - - script.execute: - id: play_sound - priority: false - sound_file: "mute_switch_on_sound" - on_turn_off: - - if: - condition: - and: - - lambda: return !id(init_in_progress); - - switch.is_on: mute_sound - then: - - script.execute: - id: play_sound - priority: false - sound_file: "mute_switch_off_sound" - turn_on_action: - - if: - condition: - lambda: return !id(mute_state).state; - then: - - switch.turn_on: mute_toggle - turn_off_action: - - if: - condition: - lambda: return id(mute_state).state; - then: - - switch.turn_on: mute_toggle - # Button click Sounds Switch. - - platform: template - id: button_sound - name: Button click sounds - icon: "mdi:bullhorn" - entity_category: config - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - # Mute Sound Switch. - - platform: template - id: mute_sound - name: Mute-unmute sound - icon: "mdi:bullhorn" - entity_category: config - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - # Wake Word Sound Switch. - - platform: template - id: wake_sound - name: Wake sound - icon: "mdi:bullhorn" - entity_category: config - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - # Internal switch to track when a timer is ringing on the device. - - platform: template - id: timer_ringing - optimistic: true - internal: true - restore_mode: ALWAYS_OFF - on_turn_off: - # Disable stop wake word - - micro_wake_word.disable_model: stop - - script.execute: disable_repeat - # Stop any current announcement (ie: stop the timer ring mid playback) - - if: - condition: - media_player.is_announcing: - id: external_media_player - then: - media_player.stop: - announcement: true - id: external_media_player - # Set back ducking ratio to zero - - mixer_speaker.apply_ducking: - id: media_mixing_input - decibel_reduction: 0 - duration: 1.0s - # Refresh the LED ring - - script.execute: control_leds - on_turn_on: - # Duck audio - - mixer_speaker.apply_ducking: - id: media_mixing_input - decibel_reduction: 20 - duration: 0.0s - # Enable stop wake word - - micro_wake_word.enable_model: stop - # Ring timer - - script.execute: ring_timer - # Refresh LED - - script.execute: control_leds - # If 15 minutes have passed and the timer is still ringing, stop it. - - delay: 15min - - switch.turn_off: timer_ringing - # Defines if alarm is active - - platform: template - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - id: alarm_on - icon: mdi:bell-badge - name: "Alarm on" - on_turn_on: - - script.execute: control_leds - on_turn_off: - - script.execute: control_leds - -binary_sensor: - # User Button. Used for many things (See on_multi_click) - - platform: gpio - id: user_button - pin: - number: GPIO3 # D2 - inverted: true - name: "User button" - on_press: - - script.execute: control_leds - on_release: - - script.execute: control_leds - # If a factory reset is requested, factory reset on release - - if: - condition: - lambda: return id(factory_reset_requested); - then: - - button.press: factory_reset_button - on_multi_click: - # Simple Click: - # - Abort "things" in order - # - Timer - # - Announcements - # - Voice Assistant Pipeline run - # - Music - # - Starts the voice assistant if it is not yet running and if the device is not muted. - - timing: - - ON for at most 1s - - OFF for at least 0.25s - then: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - if: - condition: - switch.is_on: timer_ringing - then: - - switch.turn_off: timer_ringing - else: - - if: - condition: - voice_assistant.is_running: - then: - - voice_assistant.stop: - else: - - if: - condition: - media_player.is_announcing: - id: external_media_player - then: - media_player.stop: - announcement: true - id: external_media_player - else: - - if: - condition: - media_player.is_playing: - id: external_media_player - then: - - media_player.pause: - id: external_media_player - else: - - if: - condition: - and: - - switch.is_off: mic_mute_switch - - not: - voice_assistant.is_running - then: - - if: - condition: - switch.is_on: button_sound - then: - - script.execute: - id: play_sound - priority: true - sound_file: "center_button_press_sound" - - delay: 300ms - - voice_assistant.start: - # Double Click - # . Exposed as an event entity. To be used in automations inside Home Assistant - - timing: - - ON for at most 1s - - OFF for at most 0.25s - - ON for at most 1s - - OFF for at least 0.25s - then: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - if: - condition: - switch.is_on: button_sound - then: - - script.execute: - id: play_sound - priority: false - sound_file: "center_button_double_press_sound" - - event.trigger: - id: button_press_event - event_type: "double_press" - # Triple Click - # . Exposed as an event entity. To be used in automations inside Home Assistant - - timing: - - ON for at most 1s - - OFF for at most 0.25s - - ON for at most 1s - - OFF for at most 0.25s - - ON for at most 1s - - OFF for at least 0.25s - then: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - if: - condition: - switch.is_on: button_sound - then: - - script.execute: - id: play_sound - priority: false - sound_file: "center_button_triple_press_sound" - - event.trigger: - id: button_press_event - event_type: "triple_press" - # Long Press - # . Exposed as an event entity. To be used in automations inside Home Assistant - - timing: - - ON for at least 1s - then: - - if: - condition: - lambda: return !id(init_in_progress); - then: - - if: - condition: - switch.is_on: button_sound - then: - - script.execute: - id: play_sound - priority: false - sound_file: "center_button_long_press_sound" - - light.turn_off: led_internal - - event.trigger: - id: button_press_event - event_type: "long_press" - # Factory Reset Warning - # . Audible and Visible warning. - - timing: - - ON for at least 10s - then: - - light.turn_on: - brightness: 100% - id: led_internal - effect: "Factory Reset Coming Up" - - script.execute: - id: play_sound - priority: true - sound_file: "factory_reset_initiated_sound" - - wait_until: - binary_sensor.is_off: user_button - - if: - condition: - lambda: return !id(factory_reset_requested); - then: - - light.turn_off: led_internal - - script.execute: - id: play_sound - priority: true - sound_file: "factory_reset_cancelled_sound" - # Factory Reset Confirmed. - # . Audible warning to prompt user to release the button - # . Set factory_reset_requested to true - - timing: - - ON for at least 22s - then: - - script.execute: - id: play_sound - priority: true - sound_file: "factory_reset_confirmed_sound" - - light.turn_on: - brightness: 100% - red: 100% - green: 0% - blue: 0% - id: led_internal - effect: "none" - - lambda: id(factory_reset_requested) = true; - -light: - - platform: esp32_rmt_led_strip - id: led_internal - internal: true - rgb_order: GRB - pin: GPIO1 - num_leds: 1 - rmt_symbols: 192 - chipset: ws2812 - default_transition_length: 0s - effects: - - addressable_lambda: - name: "Fast Pulse" - update_interval: 10ms - lambda: |- - static float fraction = 0.0; - static float step = 0.05; - static bool increasing = true; - - auto values = id(led_internal)->current_values; - Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255); - it[0].set_rgb(color.red * fraction, - color.green * fraction, - color.blue * fraction); - - fraction += (step * (increasing ? 1 : -1)); - if (fraction > 1.0) { - fraction = 1.0; - increasing = !increasing; - } else if (fraction < 0.0) { - fraction = 0.0; - increasing = !increasing; - } - - - addressable_lambda: - name: "Slow Pulse" - update_interval: 30ms - lambda: |- - static float fraction = 0.0; - static float step = 0.05; - static bool increasing = true; - - auto values = id(led_internal)->current_values; - Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255); - it[0].set_rgb(color.red * fraction, - color.green * fraction, - color.blue * fraction); - - fraction += (step * (increasing ? 1 : -1)); - if (fraction > 1.0) { - fraction = 1.0; - increasing = !increasing; - } else if (fraction < 0.0) { - fraction = 0.0; - increasing = !increasing; - } - - - addressable_lambda: - name: "Factory Reset Coming Up" - update_interval: 200ms - lambda: |- - static bool on = false; - on = !on; - it[0].set_rgb(on ? 255 : 0, 0, 0); - # User facing LED. - # Exposed to be used by the user. - - platform: partition - id: led_respeaker_onboard - name: LED Respeaker onboard - entity_category: config - icon: "mdi:circle-outline" - default_transition_length: 0ms - restore_mode: RESTORE_DEFAULT_OFF - on_turn_off: - - script.execute: control_leds - initial_state: - color_mode: rgb - brightness: 100% - red: 9.4% - green: 73.3% - blue: 94.9% - segments: - - id: led_internal - from: 0 - to: 0 - effects: - - addressable_lambda: - name: "Fast Pulse" - update_interval: 10ms - lambda: |- - static float fraction = 0.0; - static float step = 0.05; - static bool increasing = true; - - auto values = id(led_respeaker_onboard)->current_values; - Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255); - it[0].set_rgb(color.red * fraction, - color.green * fraction, - color.blue * fraction); - - fraction += (step * (increasing ? 1 : -1)); - if (fraction > 1.0) { - fraction = 1.0; - increasing = !increasing; - } else if (fraction < 0.0) { - fraction = 0.0; - increasing = !increasing; - } - - - addressable_lambda: - name: "Slow Pulse" - update_interval: 30ms - lambda: |- - static float fraction = 0.0; - static float step = 0.05; - static bool increasing = true; - - auto values = id(led_respeaker_onboard)->current_values; - Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255); - it[0].set_rgb(color.red * fraction, - color.green * fraction, - color.blue * fraction); - - fraction += (step * (increasing ? 1 : -1)); - if (fraction > 1.0) { - fraction = 1.0; - increasing = !increasing; - } else if (fraction < 0.0) { - fraction = 0.0; - increasing = !increasing; - } - -sensor: - - platform: template - id: next_timer - name: "Next timer" - update_interval: never - disabled_by_default: true - device_class: duration - unit_of_measurement: s - icon: "mdi:timer" - -text_sensor: - - platform: template - id: next_timer_name - name: "Next timer name" - icon: "mdi:timer" - disabled_by_default: true - - platform: template - name: "Current device time" - id: current_time - icon: mdi:clock - -event: - # Event entity exposed to the user to automate on complex center button presses. - # The simple press is not exposed as it is used to control the device itself. - - platform: template - id: button_press_event - name: "Button press" - icon: mdi:button-pointer - device_class: button - event_types: - - double_press - - triple_press - - long_press - -script: - # Master script controlling the LEDs, based on different conditions : initialization in progress, wifi and api connected and voice assistant phase. - # For the sake of simplicity and re-usability, the script calls child scripts defined below. - # This script will be called every time one of these conditions is changing. - - id: control_leds - then: - - lambda: | - id(check_if_timers_active).execute(); - if (id(is_timer_active)){ - id(fetch_first_active_timer).execute(); - } - if (id(improv_ble_in_progress)) { - id(control_leds_improv_ble_state).execute(); - } else if (id(init_in_progress)) { - id(control_leds_init_state).execute(); - } else if (!id(wifi_id).is_connected() || !id(api_id).is_connected()){ - id(control_leds_no_ha_connection_state).execute(); - } else if (id(user_button).state) { - id(control_leds_center_button_touched).execute(); - } else if (id(timer_ringing).state) { - id(control_leds_timer_ringing).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_waiting_for_command_phase_id}) { - id(control_leds_voice_assistant_waiting_for_command_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_listening_for_command_phase_id}) { - id(control_leds_voice_assistant_listening_for_command_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_thinking_phase_id}) { - id(control_leds_voice_assistant_thinking_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_replying_phase_id}) { - id(control_leds_voice_assistant_replying_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_error_phase_id}) { - id(control_leds_voice_assistant_error_phase).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_not_ready_phase_id}) { - id(control_leds_voice_assistant_not_ready_phase).execute(); - } else if (id(is_timer_active)) { - id(control_leds_timer_ticking).execute(); - } else if (id(alarm_on).state && !id(led_respeaker_onboard).remote_values.is_on()) { - id(control_leds_alarm_active).execute(); - } else if (id(voice_assistant_phase) == ${voice_assist_idle_phase_id}) { - id(control_leds_voice_assistant_idle_phase).execute(); - } - - # Script executed during Improv BLE - # Warm White slow pulse - - id: control_leds_improv_ble_state - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 100% - green: 89% - blue: 71% - id: led_internal - effect: "Slow Pulse" - - # Script executed during initialization - # Fast Blue pulse if Wifi is connected, Else slow blue pulse - - id: control_leds_init_state - then: - - if: - condition: - wifi.connected: - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 9% - green: 73% - blue: 95% - id: led_internal - effect: "Fast Pulse" - else: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 9% - green: 73% - blue: 95% - id: led_internal - effect: "Slow Pulse" - # Script executed when the device has no connection to Home Assistant - # Red slow pulse (This will be visible during HA updates for example) - - id: control_leds_no_ha_connection_state - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 1 - green: 0 - blue: 0 - id: led_internal - effect: "Slow Pulse" - - # Script executed when the voice assistant is idle (waiting for a wake word) - # Nothing - - id: control_leds_voice_assistant_idle_phase - then: - - light.turn_off: led_internal - - if: - condition: - light.is_on: led_respeaker_onboard - then: - light.turn_on: led_respeaker_onboard - - # Script executed when the voice assistant is waiting for a command (After the wake word) - # Slow purple pulse - - id: control_leds_voice_assistant_waiting_for_command_phase - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 1 - green: 0.2 - blue: 1 - id: led_internal - effect: "Slow Pulse" - - # Script executed when the voice assistant is listening to a command - # Slow purple pulse - - id: control_leds_voice_assistant_listening_for_command_phase - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 1 - green: 0.2 - blue: 1 - id: led_internal - effect: "Slow Pulse" - - # Script executed when the voice assistant is thinking to a command - # Fast purple pulse - - id: control_leds_voice_assistant_thinking_phase - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 1 - green: 0.2 - blue: 1 - id: led_internal - effect: "Fast Pulse" - - # Script executed when the voice assistant is replying to a command - # Slow cyan pulse - - id: control_leds_voice_assistant_replying_phase - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 0.2 - green: 1 - blue: 1 - id: led_internal - effect: "Slow Pulse" - - # Script executed when the voice assistant is in error - # Fast Red Pulse - - id: control_leds_voice_assistant_error_phase - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 1 - green: 0 - blue: 0 - id: led_internal - effect: "Fast Pulse" - - # Script executed when the voice assistant is not ready - - id: control_leds_voice_assistant_not_ready_phase - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 1 - green: 0 - blue: 0 - id: led_internal - effect: "Slow Pulse" - - # Script executed when the center button is touched - # The LED turns on blue - - id: control_leds_center_button_touched - then: - - light.turn_on: - brightness: !lambda return min ( max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); - red: 0 - green: 0 - blue: 1 - id: led_internal - effect: "None" - - # Script executed when the timer is ringing, to control the LEDs - # The LED blinks green. - - id: control_leds_timer_ringing - then: - - light.turn_on: - brightness: !lambda return min ( max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f ); - red: 0 - green: 1 - blue: 0 - id: led_internal - effect: "Fast Pulse" - - # Script executed when the timer is ticking, to control the LEDs - # Slow dim while pulse. - - id: control_leds_timer_ticking - then: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 0.3 - green: 0.3 - blue: 0.3 - id: led_internal - effect: "Slow Pulse" - - # Script executed when the alarm is active - # The LED turns on dim green - - id: control_leds_alarm_active - then: - - light.turn_on: - brightness: !lambda return 0.3f; - red: 0 - green: 1 - blue: 0 - id: led_internal - effect: "None" - - - # Script executed when the timer is ringing, to playback sounds. - - id: ring_timer - then: - - script.execute: enable_repeat_one - - script.execute: - id: play_sound - priority: true - sound_file: "timer_finished_sound" - - # Script executed when the timer is ringing, to repeat the timer finished sound. - - id: enable_repeat_one - then: - # Turn on the repeat mode and pause for 500 ms between playlist items/repeats - - media_player.repeat_one: - id: external_media_player - announcement: true - - speaker_source.set_playlist_delay: - id: external_media_player - pipeline: announcement - delay: 500ms - - # Script execute when the timer is done ringing, to disable repeat mode. - - id: disable_repeat - then: - # Turn off the repeat mode and pause for 0 ms between playlist items/repeats - - media_player.repeat_off: - id: external_media_player - announcement: true - - speaker_source.set_playlist_delay: - id: external_media_player - pipeline: announcement - delay: 0ms - - # Script executed when we want to play sounds on the device. - - id: play_sound - parameters: - priority: bool - # sound_file: "audio::AudioFile*" - sound_file: string - then: - - if: - condition: - lambda: return priority; - then: - - media_player.stop: - id: external_media_player - announcement: true - - lambda: |- - if ( (id(external_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) { - id(external_media_player) - ->make_call() - .set_media_url("audio-file://" + sound_file) - .set_announcement(true) - .perform(); - } - - # Script used to fetch the first active timer (Stored in global first_active_timer) - - id: fetch_first_active_timer - then: - - lambda: | - const auto &timers = id(va).get_timers(); - auto output_timer = *timers.begin(); - for (const auto &timer : timers) { - if (timer.is_active && timer.seconds_left <= output_timer.seconds_left) { - output_timer = timer; - } - } - id(first_active_timer) = output_timer; - - # Script used to check if a timer is active (Stored in global is_timer_active) - - id: check_if_timers_active - then: - - lambda: | - const auto &timers = id(va).get_timers(); - bool output = false; - for (const auto &timer : timers) { - if (timer.is_active) { - output = true; - } - } - id(is_timer_active) = output; - - # Script used activate the stop word if the TTS step is long. - # Why is this wrapped on a script? - # Becasue we want to stop the sequence if the TTS step is faster than that. - # This allows us to prevent having the deactivation of the stop word before its own activation. - - id: activate_stop_word_once - then: - - wait_until: - condition: - media_player.is_announcing: - id: external_media_player - - delay: 1s - # Enable stop wake word - - if: - condition: - switch.is_off: timer_ringing - then: - - micro_wake_word.enable_model: stop - - wait_until: - not: - media_player.is_announcing: - id: external_media_player - - if: - condition: - switch.is_off: timer_ringing - then: - - micro_wake_word.disable_model: stop - - - id: check_alarm - then: - - lambda: |- - id(publish_current_time).execute(); - // Check alarm - if (id(alarm_on).state) { - auto alarm_dt = id(alarm_time).state_as_esptime(); - auto time_now = id(homeassistant_time).now(); - - if (time_now.hour == alarm_dt.hour && time_now.minute == alarm_dt.minute) { - auto action = id(alarm_action).current_option(); - if (action == "Play sound") { - id(timer_ringing).turn_on(); - } else if (action == "Send event") { - id(send_alarm_event).execute(); - } else if (action == "Sound and event") { - id(timer_ringing).turn_on(); - id(send_alarm_event).execute(); - } - } - } - - - id: send_wake_word_event - parameters: - wake_word: string - then: - - homeassistant.event: - event: esphome.wake_word_detected - data: - wake_word: !lambda return wake_word; - - id: send_alarm_event - then: - - homeassistant.event: - event: esphome.alarm_ringing - - id: send_tts_uri_event - parameters: - tts_uri: string - then: - - homeassistant.event: - event: esphome.tts_uri - data: - uri: !lambda return tts_uri; - - id: send_stt_text_event - parameters: - stt_text: string - then: - - homeassistant.event: - event: esphome.stt_text - data: - text: !lambda return stt_text; - - id: publish_current_time - then: - - lambda: |- - // Publish current time - auto time_now = id(homeassistant_time).now(); - id(current_time).publish_state(time_now.strftime("%H:%M")); - -i2s_audio: - - id: i2s_output - i2s_lrclk_pin: - number: GPIO7 - allow_other_uses: true - i2s_bclk_pin: - number: GPIO8 - allow_other_uses: true - i2s_mclk_pin: - number: GPIO9 - allow_other_uses: true - - - id: i2s_input - i2s_lrclk_pin: - number: GPIO7 - allow_other_uses: true - i2s_bclk_pin: - number: GPIO8 - allow_other_uses: true - i2s_mclk_pin: - number: GPIO9 - allow_other_uses: true - -microphone: - - platform: i2s_audio - id: i2s_mics - i2s_din_pin: GPIO44 - adc_type: external - pdm: false - sample_rate: 48000 - bits_per_sample: 32bit - i2s_mode: secondary - i2s_audio_id: i2s_input - channel: stereo - -speaker: - # Hardware speaker output - - platform: i2s_audio - id: i2s_audio_speaker - sample_rate: 48000 - i2s_mode: secondary - i2s_dout_pin: GPIO43 - bits_per_sample: 32bit - i2s_audio_id: i2s_output - dac_type: external - channel: stereo - timeout: never - buffer_duration: 100ms - audio_dac: aic3204_dac - - # Virtual speakers to combine the announcement and media streams together into one output - - platform: mixer - id: mixing_speaker - output_speaker: i2s_audio_speaker - num_channels: 2 - task_stack_in_psram: true - source_speakers: - - id: announcement_mixing_input - timeout: never - - id: media_mixing_input - timeout: never - - # Virtual speakers to resample each pipelines' audio, if necessary, as the mixer speaker requires the same sample rate - - platform: resampler - id: announcement_resampling_speaker - output_speaker: announcement_mixing_input - sample_rate: 48000 - bits_per_sample: 16 - - platform: resampler - id: media_resampling_speaker - output_speaker: media_mixing_input - sample_rate: 48000 - bits_per_sample: 16 - -sendspin: - id: sendspin_hub - task_stack_in_psram: false - -audio_file: - - id: center_button_press_sound - file: ${center_button_press_sound_file} - - id: center_button_double_press_sound - file: ${center_button_double_press_sound_file} - - id: center_button_triple_press_sound - file: ${center_button_triple_press_sound_file} - - id: center_button_long_press_sound - file: ${center_button_long_press_sound_file} - - id: factory_reset_initiated_sound - file: ${factory_reset_initiated_sound_file} - - id: factory_reset_cancelled_sound - file: ${factory_reset_cancelled_sound_file} - - id: factory_reset_confirmed_sound - file: ${factory_reset_confirmed_sound_file} - - id: mute_switch_on_sound - file: ${mute_switch_on_sound_file} - - id: mute_switch_off_sound - file: ${mute_switch_off_sound_file} - - id: timer_finished_sound - file: ${timer_finished_sound_file} - - id: wake_word_triggered_sound - file: ${wake_word_triggered_sound_file} - - id: error_cloud_expired - file: ${error_cloud_expired_sound_file} - -media_source: - - platform: audio_file - id: audio_file_announcement_source - - platform: audio_http - id: http_announcement_source - buffer_size: 250000 - - platform: audio_http - id: http_media_source - buffer_size: 500000 - - platform: sendspin - id: sendspin_media_source - fixed_delay: 480 microseconds # The AIC3204 DAC used, as configured, on the VPE delays audio by 480 microseconds - -media_player: - - platform: sendspin - id: sendspin_group_media_player - - platform: speaker_source - id: external_media_player - name: Media Player - announcement_pipeline: - format: FLAC # FLAC is the least processor intensive codec - num_channels: 1 # Stereo audio is unnecessary for announcements - sample_rate: 48000 - speaker: announcement_resampling_speaker - sources: - - audio_file_announcement_source - - http_announcement_source - media_pipeline: - format: FLAC # FLAC is the least processor intensive codec - num_channels: 2 - sample_rate: 48000 - speaker: media_resampling_speaker - sources: - - http_media_source - - sendspin_media_source - volume_increment: 0.05 - volume_min: 0.4 - volume_max: 0.85 - on_announcement: - - mixer_speaker.apply_ducking: - id: media_mixing_input - decibel_reduction: 20 - duration: 0.0s - on_state: - if: - condition: - and: - - switch.is_off: timer_ringing - - not: - voice_assistant.is_running: - - not: - media_player.is_announcing: external_media_player - then: - - mixer_speaker.apply_ducking: - id: media_mixing_input - decibel_reduction: 0 - duration: 1.0s - -respeaker_lite: - id: respeaker - reset_pin: GPIO2 - mute_state: - internal: true - id: mute_state - firmware_version: - icon: mdi:application-cog - name: XMOS firmware version - internal: false - id: firmware_version - firmware: - url: https://github.com/formatBCE/Respeaker-Lite-ESPHome-integration/raw/refs/heads/main/respeaker_lite_i2s_dfu_firmware_48k_v1.1.0.bin - version: "1.1.0" - md5: 9297155d1bf3eb21a9d4db52a89ea0c6 - on_begin: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 50% - green: 50% - blue: 50% - id: led_internal - effect: "Slow Pulse" - on_end: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 0% - green: 100% - blue: 0% - id: led_internal - effect: "Fast Pulse" - - delay: 3s - - light.turn_off: - id: led_internal - on_error: - - light.turn_on: - brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ); - red: 100% - green: 0% - blue: 0% - id: led_internal - effect: "Fast Pulse" - - delay: 3s - - light.turn_off: - id: led_internal - -external_components: - - source: - type: git - url: https://github.com/formatBCE/esphome - ref: respeaker_microphone - components: - - i2s_audio - refresh: 0s - - source: - type: git - url: https://github.com/formatBCE/Respeaker-Lite-ESPHome-integration - ref: main - components: - - respeaker_lite - refresh: 0s - -audio_dac: - - platform: aic3204 - id: aic3204_dac - i2c_id: internal_i2c - -micro_wake_word: - id: mww - microphone: - microphone: i2s_mics - channels: 1 - gain_factor: 4 - stop_after_detection: false - models: - - model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json - # probability_cutoff: 0.8 - id: okay_nabu - - model: https://raw.githubusercontent.com/formatBCE/Respeaker-Lite-ESPHome-integration/refs/heads/main/microwakeword/models/v2/kenobi.json - id: kenobi - - model: hey_jarvis - id: hey_jarvis - - model: hey_mycroft - id: hey_mycroft - - model: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json - id: stop - internal: true - vad: - probability_cutoff: 0.05 - on_wake_word_detected: - # If the wake word is detected when the device is muted (Possible with the software mute switch): Do nothing - - if: - condition: - switch.is_off: mic_mute_switch - then: - - script.execute: - id: send_wake_word_event - wake_word: !lambda return wake_word; - # If a timer is ringing: Stop it, do not start the voice assistant (We can stop timer from voice!) - - if: - condition: - switch.is_on: timer_ringing - then: - - switch.turn_off: timer_ringing - # Stop voice assistant if running - else: - - if: - condition: - voice_assistant.is_running: - then: - voice_assistant.stop: - # Stop any other media player announcement - else: - - if: - condition: - media_player.is_announcing: - id: external_media_player - then: - - media_player.stop: - announcement: true - id: external_media_player - # Start the voice assistant and play the wake sound, if enabled - else: - - if: - condition: - switch.is_on: wake_sound - then: - - script.execute: - id: play_sound - priority: true - sound_file: "wake_word_triggered_sound" - - delay: 300ms - - voice_assistant.start: - wake_word: !lambda return wake_word; - -select: - - platform: template - name: "Wake word sensitivity" - id: wake_word_sensitivity - optimistic: true - initial_option: Slightly sensitive - restore_value: true - entity_category: config - options: - - Slightly sensitive - - Moderately sensitive - - Very sensitive - on_value: - # Sets specific wake word probabilities computed for each particular model - # Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff - # False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus. - # These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2 - lambda: |- - if (x == "Slightly sensitive") { - id(okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default) - id(hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default) - id(hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo - } else if (x == "Moderately sensitive") { - id(okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo - id(hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo - id(hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default) - } else if (x == "Very sensitive") { - id(okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo - id(hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo - id(hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo - } - - platform: logger - id: logger_select - name: Logger Level - disabled_by_default: true - - platform: template - optimistic: true - name: "Alarm action" - id: alarm_action - icon: mdi:bell-plus - options: - - "Play sound" - - "Send event" - - "Sound and event" - initial_option: "Play sound" - on_value: - then: - - lambda: |- - id(saved_alarm_action) = x; - -voice_assistant: - id: va - microphone: - microphone: i2s_mics - channels: 0 - media_player: external_media_player - micro_wake_word: mww - use_wake_word: false - noise_suppression_level: 0 - auto_gain: 0 dbfs - volume_multiplier: 1 - on_client_connected: - - if: - condition: - - lambda: return id(init_in_progress); - - switch.is_on: mic_mute_switch - then: - - switch.turn_off: mic_mute_switch - - lambda: id(init_in_progress) = false; - - micro_wake_word.start: - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - - script.execute: control_leds - on_client_disconnected: - - voice_assistant.stop: - - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - - script.execute: control_leds - on_error: - # Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized - # These two are ignored for a better user experience - - if: - condition: - and: - - lambda: return !id(init_in_progress); - - lambda: return code != "duplicate_wake_up_detected"; - - lambda: return code != "stt-no-text-recognized"; - then: - - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; - - script.execute: control_leds - # If the error code is cloud-auth-failed, serve a local audio file guiding the user. - - if: - condition: - - lambda: return code == "cloud-auth-failed"; - then: - - script.execute: - id: play_sound - priority: true - sound_file: "error_cloud_expired" - # When the voice assistant starts: Play a wake up sound, duck audio. - on_start: - - mixer_speaker.apply_ducking: - id: media_mixing_input - decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume - duration: 0.0s # The duration of the transition (default is no transition) - on_listening: - - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; - - script.execute: control_leds - on_stt_vad_start: - - lambda: id(voice_assistant_phase) = ${voice_assist_listening_for_command_phase_id}; - - script.execute: control_leds - on_stt_vad_end: - - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; - - script.execute: control_leds - on_intent_progress: - - if: - condition: - # A nonempty x variable means a streaming TTS url was sent to the media player - lambda: 'return !x.empty();' - then: - - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - - script.execute: control_leds - # Start a script that would potentially enable the stop word if the response is longer than a second - - script.execute: activate_stop_word_once - on_tts_start: - - if: - condition: - # The intent_progress trigger didn't start the TTS Reponse - lambda: 'return id(voice_assistant_phase) != ${voice_assist_replying_phase_id};' - then: - - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - - script.execute: control_leds - # Start a script that would potentially enable the stop word if the response is longer than a second - - script.execute: activate_stop_word_once - on_tts_end: - - script.execute: - id: send_tts_uri_event - tts_uri: !lambda 'return x;' - on_stt_end: - - script.execute: - id: send_stt_text_event - stt_text: !lambda 'return x;' - - # When the voice assistant ends ... - on_end: - - wait_until: - not: - voice_assistant.is_running: - # Stop ducking audio. - - mixer_speaker.apply_ducking: - id: media_mixing_input - decibel_reduction: 0 - duration: 1.0s - # If the end happened because of an error, let the error phase on for a second - - if: - condition: - lambda: return id(voice_assistant_phase) == ${voice_assist_error_phase_id}; - then: - - delay: 1s - # Reset the voice assistant phase id and reset the LED animations. - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - - script.execute: control_leds - on_timer_finished: - - switch.turn_on: timer_ringing - - lambda: | - id(next_timer).publish_state(-1); - id(next_timer_name).publish_state("-"); - on_timer_started: - - script.execute: control_leds - - lambda: | - id(next_timer).publish_state(id(first_active_timer).seconds_left); - id(next_timer_name).publish_state(id(first_active_timer).name); - on_timer_cancelled: - - script.execute: control_leds - - lambda: | - id(next_timer).publish_state(id(first_active_timer).seconds_left); - id(next_timer_name).publish_state(id(first_active_timer).name); - on_timer_updated: - - script.execute: control_leds - - lambda: | - id(next_timer).publish_state(id(first_active_timer).seconds_left); - id(next_timer_name).publish_state(id(first_active_timer).name); - on_timer_tick: - - script.execute: control_leds - - lambda: | - int seconds_left = id(first_active_timer).seconds_left; - if (std::abs(seconds_left) % 5 == 0) { - id(next_timer).publish_state(seconds_left); - } - -button: - - platform: factory_reset - id: factory_reset_button - name: "Factory Reset" - entity_category: diagnostic - internal: true - - platform: restart - id: restart_button - name: "Restart" - entity_category: config - disabled_by_default: true - icon: "mdi:restart" - -debug: - update_interval: 5s diff --git a/packages/chore_tracker_ha.yaml b/packages/chore_tracker_ha.yaml index e230ca1..93604b6 100644 --- a/packages/chore_tracker_ha.yaml +++ b/packages/chore_tracker_ha.yaml @@ -1,65 +1,65 @@ -################################################################################ -# chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml) -# Kids: Jordyn, Declan, Chloe -# -# BIDIRECTIONAL SYNC: -# Screen to HA: Each switch calls homeassistant.service on toggle -# HA to Screen: ESPHome native API handles this automatically -################################################################################ +# ################################################################################ +# # chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml) +# # Kids: Jordyn, Declan, Chloe +# # +# # BIDIRECTIONAL SYNC: +# # Screen to HA: Each switch calls homeassistant.service on toggle +# # HA to Screen: ESPHome native API handles this automatically +# ################################################################################ -input_button: - jordyn_reset_chores: - name: "Reset Jordyn's Chores" - icon: mdi:restart - declan_reset_chores: - name: "Reset Declan's Chores" - icon: mdi:restart - chloe_reset_chores: - name: "Reset Chloe's Chores" - icon: mdi:restart - reset_all_chores: - name: "Reset All Chores" - icon: mdi:restart-alert +# input_button: +# jordyn_reset_chores: +# name: "Reset Jordyn's Chores" +# icon: mdi:restart +# declan_reset_chores: +# name: "Reset Declan's Chores" +# icon: mdi:restart +# chloe_reset_chores: +# name: "Reset Chloe's Chores" +# icon: mdi:restart +# reset_all_chores: +# name: "Reset All Chores" +# icon: mdi:restart-alert -template: - - sensor: - - name: "Jordyn Chores Done Today" - unique_id: jordyn_chores_done_today - icon: mdi:check-circle - state: > - {% set chores = [states('switch.chore_tracker_jordyn_put_on_underwear'), states('switch.chore_tracker_jordyn_brush_teeth'), states('switch.chore_tracker_jordyn_fill_water_bowls'), states('switch.chore_tracker_jordyn_restock_cat_food_cans')] %} - {{ chores | select('equalto', 'on') | list | count }} +# template: +# - sensor: +# - name: "Jordyn Chores Done Today" +# unique_id: jordyn_chores_done_today +# icon: mdi:check-circle +# state: > +# {% set chores = [states('switch.chore_tracker_jordyn_put_on_underwear'), states('switch.chore_tracker_jordyn_brush_teeth'), states('switch.chore_tracker_jordyn_fill_water_bowls'), states('switch.chore_tracker_jordyn_restock_cat_food_cans')] %} +# {{ chores | select('equalto', 'on') | list | count }} - - name: "Jordyn All Chores Done" - unique_id: jordyn_all_chores_done - icon: mdi:check-all - state: > - {{ states('sensor.jordyn_chores_done_today') | int == 4 }} +# - name: "Jordyn All Chores Done" +# unique_id: jordyn_all_chores_done +# icon: mdi:check-all +# state: > +# {{ states('sensor.jordyn_chores_done_today') | int == 4 }} - - name: "Declan Chores Done Today" - unique_id: declan_chores_done_today - icon: mdi:check-circle - state: > - {% set chores = [states('switch.chore_tracker_declan_take_morning_pill'), states('switch.chore_tracker_declan_scoop_dog_poop')] %} - {{ chores | select('equalto', 'on') | list | count }} +# - name: "Declan Chores Done Today" +# unique_id: declan_chores_done_today +# icon: mdi:check-circle +# state: > +# {% set chores = [states('switch.chore_tracker_declan_take_morning_pill'), states('switch.chore_tracker_declan_scoop_dog_poop')] %} +# {{ chores | select('equalto', 'on') | list | count }} - - name: "Declan All Chores Done" - unique_id: declan_all_chores_done - icon: mdi:check-all - state: > - {{ states('sensor.declan_chores_done_today') | int == 2 }} +# - name: "Declan All Chores Done" +# unique_id: declan_all_chores_done +# icon: mdi:check-all +# state: > +# {{ states('sensor.declan_chores_done_today') | int == 2 }} - - name: "Chloe Chores Done Today" - unique_id: chloe_chores_done_today - icon: mdi:check-circle - state: > - {% set chores = [states('switch.chore_tracker_chloe_fill_kitty_feeders'), states('switch.chore_tracker_chloe_scoop_kitty_litter'), states('switch.chore_tracker_chloe_replace_kitty_litter_bags')] %} - {{ chores | select('equalto', 'on') | list | count }} +# - name: "Chloe Chores Done Today" +# unique_id: chloe_chores_done_today +# icon: mdi:check-circle +# state: > +# {% set chores = [states('switch.chore_tracker_chloe_fill_kitty_feeders'), states('switch.chore_tracker_chloe_scoop_kitty_litter'), states('switch.chore_tracker_chloe_replace_kitty_litter_bags')] %} +# {{ chores | select('equalto', 'on') | list | count }} - - name: "Chloe All Chores Done" - unique_id: chloe_all_chores_done - icon: mdi:check-all - state: > - {{ states('sensor.chloe_chores_done_today') | int == 3 }} +# - name: "Chloe All Chores Done" +# unique_id: chloe_all_chores_done +# icon: mdi:check-all +# state: > +# {{ states('sensor.chloe_chores_done_today') | int == 3 }} diff --git a/packages/integration_health_monitor.yaml b/packages/integration_health_monitor.yaml index 3925024..69f4a4d 100644 --- a/packages/integration_health_monitor.yaml +++ b/packages/integration_health_monitor.yaml @@ -1,33 +1,33 @@ -# Integration Health Monitor Package +# # Integration Health Monitor Package -automation: - # ---------------------------------------------------------- - # Kasa-Specific Recovery Attempt - # ---------------------------------------------------------- - - id: integration_health_kasa_recovery - alias: "🔄 Attempt Kasa Integration Reload on Failure" - description: "Automatically attempts to reload Kasa integration after errors" - mode: single - trigger: - - platform: event - event_type: system_log_event - event_data: - level: ERROR - condition: - - condition: template - value_template: > - {{ 'kasa' in (trigger.event.data.name | default('') | lower) - and '403' in (trigger.event.data.message | default('')) }} - action: - - delay: - minutes: 2 - - service: persistent_notification.create - data: - title: "🔄 Attempting Kasa Recovery" - message: "Detected Kasa authentication errors. Attempting integration reload..." - notification_id: "kasa_recovery" - - service: homeassistant.reload_config_entry - target: - entity_id: switch.studio_plug_1_2 - - delay: - hours: 1 +# automation: +# # ---------------------------------------------------------- +# # Kasa-Specific Recovery Attempt +# # ---------------------------------------------------------- +# - id: integration_health_kasa_recovery +# alias: "🔄 Attempt Kasa Integration Reload on Failure" +# description: "Automatically attempts to reload Kasa integration after errors" +# mode: single +# trigger: +# - platform: event +# event_type: system_log_event +# event_data: +# level: ERROR +# condition: +# - condition: template +# value_template: > +# {{ 'kasa' in (trigger.event.data.name | default('') | lower) +# and '403' in (trigger.event.data.message | default('')) }} +# action: +# - delay: +# minutes: 2 +# - service: persistent_notification.create +# data: +# title: "🔄 Attempting Kasa Recovery" +# message: "Detected Kasa authentication errors. Attempting integration reload..." +# notification_id: "kasa_recovery" +# - service: homeassistant.reload_config_entry +# target: +# entity_id: switch.studio_plug_1_2 +# - delay: +# hours: 1 diff --git a/packages/litter_box_monitor.yaml b/packages/litter_box_monitor.yaml deleted file mode 100644 index 5345109..0000000 --- a/packages/litter_box_monitor.yaml +++ /dev/null @@ -1,306 +0,0 @@ -# ============================================================================= -# LITTER BOX MONITORING - MQTT Sensors & Automations -# ============================================================================= -# Receives analysis results from litter_box_analyzer.py via MQTT and -# exposes them as Home Assistant entities with notifications. -# -# MQTT topics (published by the Python analyzer): -# litter_box/status/cat_present -> ON/OFF -# litter_box/status/motion_detected -> ON/OFF -# litter_box/status/motion_score -> float -# litter_box/status/box_state -> open/closed/unknown -# litter_box/status/box_diff_score -> float -# litter_box/status/litter_box_stuck -> ON/OFF -# litter_box/status/needs_scooping -> ON/OFF -# litter_box/status/cleanliness_score -> 0-100 -# litter_box/status/status_message -> text -# ============================================================================= - -# --------------------------------------------------------------------------- -# MQTT Binary Sensors -# --------------------------------------------------------------------------- -mqtt: - binary_sensor: - - name: "Litter Box - Cat Present" - unique_id: litter_box_cat_present - state_topic: "litter_box/status/cat_present" - payload_on: "ON" - payload_off: "OFF" - device_class: occupancy - icon: "mdi:cat" - - - name: "Litter Box - Motion Detected" - unique_id: litter_box_motion_detected - state_topic: "litter_box/status/motion_detected" - payload_on: "ON" - payload_off: "OFF" - device_class: motion - icon: "mdi:motion-sensor" - - - name: "Litter Box - Stuck Closed" - unique_id: litter_box_stuck_closed - state_topic: "litter_box/status/litter_box_stuck" - payload_on: "ON" - payload_off: "OFF" - device_class: problem - icon: "mdi:alert-circle" - - - name: "Litter Box - Needs Scooping" - unique_id: litter_box_needs_scooping - state_topic: "litter_box/status/needs_scooping" - payload_on: "ON" - payload_off: "OFF" - device_class: problem - icon: "mdi:shovel" - - # --------------------------------------------------------------------------- - # MQTT Numeric Sensors - # --------------------------------------------------------------------------- - sensor: - - name: "Litter Box - Motion Score" - unique_id: litter_box_motion_score - state_topic: "litter_box/status/motion_score" - unit_of_measurement: "%" - icon: "mdi:chart-bell-curve" - - - name: "Litter Box - Cleanliness Score" - unique_id: litter_box_cleanliness_score - state_topic: "litter_box/status/cleanliness_score" - unit_of_measurement: "%" - icon: "mdi:star" - - - name: "Litter Box - Box Diff Score" - unique_id: litter_box_box_diff_score - state_topic: "litter_box/status/box_diff_score" - icon: "mdi:compare" - - # MQTT text/state sensors - - name: "Litter Box - Box State" - unique_id: litter_box_box_state - state_topic: "litter_box/status/box_state" - icon: "mdi:state-machine" - - - name: "Litter Box - Status Message" - unique_id: litter_box_status_message - state_topic: "litter_box/status/status_message" - icon: "mdi:message-text" - -# --------------------------------------------------------------------------- -# Helper Toggles (for notification cooldown tracking) -# --------------------------------------------------------------------------- -input_boolean: - litter_box_stuck_notified: - name: "Litter Box Stuck - Notification Sent" - icon: "mdi:bell-off" - - litter_box_cat_notified: - name: "Litter Box Cat - Notification Cooldown" - icon: "mdi:cat" - - litter_box_scoop_notified: - name: "Litter Box Scoop - Notification Cooldown" - icon: "mdi:shovel" - -# --------------------------------------------------------------------------- -# Timers for cooldown resets -# --------------------------------------------------------------------------- -timer: - litter_box_cat_cooldown: - name: "Cat Visit Cooldown" - duration: "00:15:00" - restore: true - - litter_box_scoop_cooldown: - name: "Scoop Reminder Cooldown" - duration: "04:00:00" - restore: true - -# --------------------------------------------------------------------------- -# Automations -# --------------------------------------------------------------------------- -automation: - - # --- Keep IR on at night so the camera can see the litter box --- - - id: litter_box_ir_on_at_sunset - alias: "Litter Box Camera - IR On At Sunset" - triggers: - - trigger: sun - event: sunset - actions: - - action: light.turn_on - target: - entity_id: light.litter_box_camera_ir_night_vision - mode: single - - - id: litter_box_ir_off_at_sunrise - alias: "Litter Box Camera - IR Off At Sunrise" - triggers: - - trigger: sun - event: sunrise - actions: - - action: light.turn_off - target: - entity_id: light.litter_box_camera_ir_night_vision - mode: single - - # --- Initialize ESPHome analysis entities so alerts can trigger from known off/clean states --- - - id: litter_box_initialize_analysis_status - alias: "Litter Box Camera - Initialize Analysis Status" - triggers: - - trigger: homeassistant - event: start - - trigger: time_pattern - minutes: "/30" - conditions: - - condition: template - value_template: >- - {{ states('binary_sensor.litter_box_camera_cat_present') in ['unknown', 'unavailable'] - or states('binary_sensor.litter_box_camera_needs_scooping') in ['unknown', 'unavailable'] - or states('binary_sensor.litter_box_camera_litter_level_low') in ['unknown', 'unavailable'] - or states('sensor.litter_box_camera_cleanliness_score') in ['unknown', 'unavailable'] }} - actions: - - action: esphome.litter_box_cam_update_status - data: - cat_present: false - needs_scooping: false - litter_level_low: false - cleanliness_score: 100 - mode: single - - # --- Request a camera capture after IR turns on so analysis has a lit frame at night --- - - id: litter_box_capture_after_ir_on - alias: "Litter Box Camera - Capture After IR Turns On" - triggers: - - trigger: state - entity_id: light.litter_box_camera_ir_night_vision - to: "on" - actions: - - delay: "00:00:05" - - action: button.press - target: - entity_id: button.litter_box_camera_capture_now - mode: single - - # --- Cat Detected Notification --- - - id: litter_box_cat_detected - alias: "Litter Box - Cat Detected Alert" - triggers: - - trigger: state - entity_id: binary_sensor.litter_box_camera_cat_present - from: "off" - to: "on" - conditions: - - condition: state - entity_id: input_boolean.litter_box_cat_notified - state: "off" - actions: - - action: input_boolean.turn_on - target: - entity_id: input_boolean.litter_box_cat_notified - - action: timer.start - target: - entity_id: timer.litter_box_cat_cooldown - - action: notify.notify_events_cats - data: - message: "🐱 A cat is using the litter box right now!" - title: "Litter Box Activity" - mode: single - - # --- Reset cat notification cooldown --- - - id: litter_box_cat_cooldown_reset - alias: "Litter Box - Reset Cat Notification Cooldown" - triggers: - - trigger: event - event_type: timer.finished - event_data: - entity_id: timer.litter_box_cat_cooldown - actions: - - action: input_boolean.turn_off - target: - entity_id: input_boolean.litter_box_cat_notified - mode: single - - # --- Stuck Closed Alert (status text confirms rotated closed) --- - - id: litter_box_stuck_alert - alias: "Litter Box - Stuck Closed Alert" - initial_state: true - triggers: - - trigger: state - entity_id: sensor.litter_box_camera_status_message - conditions: - - condition: template - value_template: >- - {{ 'stuck' in states('sensor.litter_box_camera_status_message') | lower - or 'closed' in states('sensor.litter_box_camera_status_message') | lower }} - - condition: state - entity_id: input_boolean.litter_box_stuck_notified - state: "off" - actions: - - action: input_boolean.turn_on - target: - entity_id: input_boolean.litter_box_stuck_notified - - action: notify.notify_events_cats - data: - message: "⚠️ The litter box is STUCK ROTATED CLOSED! Visual state is 'closed' — check the mechanism." - title: "Litter Box STUCK CLOSED!" - mode: single - - # --- Reset stuck notification when status returns to normal/open --- - - id: litter_box_stuck_reset - alias: "Litter Box - Reset Stuck Notification on Open" - initial_state: true - triggers: - - trigger: state - entity_id: sensor.litter_box_camera_status_message - conditions: - - condition: template - value_template: >- - {{ states('sensor.litter_box_camera_status_message') | lower in ['ok', 'calibrated. monitoring started.', 'image sent for analysis'] - or 'open' in states('sensor.litter_box_camera_status_message') | lower }} - - condition: state - entity_id: input_boolean.litter_box_stuck_notified - state: "on" - actions: - - action: input_boolean.turn_off - target: - entity_id: input_boolean.litter_box_stuck_notified - mode: single - - # --- Needs Scooping Alert --- - - id: litter_box_needs_scooping_alert - alias: "Litter Box - Needs Scooping Alert" - triggers: - - trigger: state - entity_id: binary_sensor.litter_box_camera_needs_scooping - from: "off" - to: "on" - conditions: - - condition: state - entity_id: input_boolean.litter_box_scoop_notified - state: "off" - actions: - - action: input_boolean.turn_on - target: - entity_id: input_boolean.litter_box_scoop_notified - - action: timer.start - target: - entity_id: timer.litter_box_scoop_cooldown - - action: notify.notify_events_cats - data: - message: "🧹 The litter box could use a scoop! Cleanliness: {{ states('sensor.litter_box_camera_cleanliness_score') }}%" - title: "Litter Box Needs Scooping" - mode: single - - # --- Reset scoop notification cooldown --- - - id: litter_box_scoop_cooldown_reset - alias: "Litter Box - Reset Scoop Notification Cooldown" - triggers: - - trigger: event - event_type: timer.finished - event_data: - entity_id: timer.litter_box_scoop_cooldown - actions: - - action: input_boolean.turn_off - target: - entity_id: input_boolean.litter_box_scoop_notified - mode: single diff --git a/packages/lunch_menus.yaml b/packages/lunch_menus.yaml index eda6e37..2dc11d9 100644 --- a/packages/lunch_menus.yaml +++ b/packages/lunch_menus.yaml @@ -1,62 +1,62 @@ -################################################################################ -# lunch_menus.yaml -# -# Pulls Mon–Fri of the current school week from each lunch Google Calendar into -# template sensors. On Sat/Sun, advances to next week's Monday. The Lunch Menus -# dashboard renders those attributes — the stock calendar card has no school- -# week view. -################################################################################ +# ################################################################################ +# # lunch_menus.yaml +# # +# # Pulls Mon–Fri of the current school week from each lunch Google Calendar into +# # template sensors. On Sat/Sun, advances to next week's Monday. The Lunch Menus +# # dashboard renders those attributes — the stock calendar card has no school- +# # week view. +# ################################################################################ -template: - - trigger: - - platform: time_pattern - minutes: "/15" - - platform: homeassistant - event: start - - platform: event - event_type: event_template_reloaded - action: - - variables: - # weekday(): Mon=0 … Sun=6. Weekend → next Monday. - school_monday: >- - {% set wd = now().weekday() %} - {% if wd >= 5 %} - {{ (today_at() + timedelta(days=(7 - wd))).isoformat() }} - {% else %} - {{ (today_at() - timedelta(days=wd)).isoformat() }} - {% endif %} - - action: calendar.get_events - target: - entity_id: - - calendar.bhs_lunch_menu - - calendar.bis_school_lunches - - calendar.hms_lunch_menu - data: - start_date_time: "{{ school_monday }}" - duration: - days: 5 - response_variable: agenda - sensor: - - name: "BHS Lunch Menu Week" - unique_id: bhs_lunch_menu_week - icon: mdi:food-apple - state: "{{ agenda['calendar.bhs_lunch_menu'].events | count }}" - attributes: - events: "{{ agenda['calendar.bhs_lunch_menu'].events }}" - week_start: "{{ school_monday }}" +# template: +# - trigger: +# - platform: time_pattern +# minutes: "/15" +# - platform: homeassistant +# event: start +# - platform: event +# event_type: event_template_reloaded +# action: +# - variables: +# # weekday(): Mon=0 … Sun=6. Weekend → next Monday. +# school_monday: >- +# {% set wd = now().weekday() %} +# {% if wd >= 5 %} +# {{ (today_at() + timedelta(days=(7 - wd))).isoformat() }} +# {% else %} +# {{ (today_at() - timedelta(days=wd)).isoformat() }} +# {% endif %} +# - action: calendar.get_events +# target: +# entity_id: +# - calendar.bhs_lunch_menu +# - calendar.bis_school_lunches +# - calendar.hms_lunch_menu +# data: +# start_date_time: "{{ school_monday }}" +# duration: +# days: 5 +# response_variable: agenda +# sensor: +# - name: "BHS Lunch Menu Week" +# unique_id: bhs_lunch_menu_week +# icon: mdi:food-apple +# state: "{{ agenda['calendar.bhs_lunch_menu'].events | count }}" +# attributes: +# events: "{{ agenda['calendar.bhs_lunch_menu'].events }}" +# week_start: "{{ school_monday }}" - - name: "BIS Lunch Menu Week" - unique_id: bis_lunch_menu_week - icon: mdi:food-apple - state: "{{ agenda['calendar.bis_school_lunches'].events | count }}" - attributes: - events: "{{ agenda['calendar.bis_school_lunches'].events }}" - week_start: "{{ school_monday }}" +# - name: "BIS Lunch Menu Week" +# unique_id: bis_lunch_menu_week +# icon: mdi:food-apple +# state: "{{ agenda['calendar.bis_school_lunches'].events | count }}" +# attributes: +# events: "{{ agenda['calendar.bis_school_lunches'].events }}" +# week_start: "{{ school_monday }}" - - name: "HMS Lunch Menu Week" - unique_id: hms_lunch_menu_week - icon: mdi:food-apple - state: "{{ agenda['calendar.hms_lunch_menu'].events | count }}" - attributes: - events: "{{ agenda['calendar.hms_lunch_menu'].events }}" - week_start: "{{ school_monday }}" +# - name: "HMS Lunch Menu Week" +# unique_id: hms_lunch_menu_week +# icon: mdi:food-apple +# state: "{{ agenda['calendar.hms_lunch_menu'].events | count }}" +# attributes: +# events: "{{ agenda['calendar.hms_lunch_menu'].events }}" +# week_start: "{{ school_monday }}" diff --git a/packages/voice_satellite_alarms.yaml b/packages/voice_satellite_alarms.yaml deleted file mode 100644 index e23dff7..0000000 --- a/packages/voice_satellite_alarms.yaml +++ /dev/null @@ -1,125 +0,0 @@ -# Unified Home Assistant-managed alarms for ESPHome voice satellites. -# This keeps one consistent alarm UX across Jarvis and ReSpeaker devices. -# -# The assist_satellite entity IDs below are derived from the ESPHome device names: -# - office-jarvis-satellite -> assist_satellite.office_jarvis_satellite_assist_satellite -# - respeaker-satellite-fr -> assist_satellite.respeaker_satellite_fr_assist_satellite -# - respeaker-satellite-lr -> assist_satellite.respeaker_satellite_lr_assist_satellite -# - respeaker-satellite-mbr -> assist_satellite.respeaker_satellite_mbr_assist_satellite -# If Home Assistant generated different entity IDs in your instance, adjust them here -# and in dashboards/views/voice-alarms-dashboard.yaml. - -input_boolean: - voice_alarm_office_jarvis_enabled: - name: Office Jarvis Alarm Enabled - icon: mdi:alarm - - voice_alarm_respeaker_family_room_enabled: - name: ReSpeaker Family Room Alarm Enabled - icon: mdi:alarm - - voice_alarm_respeaker_living_room_enabled: - name: ReSpeaker Living Room Alarm Enabled - icon: mdi:alarm - - voice_alarm_respeaker_master_bedroom_enabled: - name: ReSpeaker Master Bedroom Alarm Enabled - icon: mdi:alarm - -input_datetime: - voice_alarm_office_jarvis_time: - name: Office Jarvis Alarm Time - has_date: false - has_time: true - - voice_alarm_respeaker_family_room_time: - name: ReSpeaker Family Room Alarm Time - has_date: false - has_time: true - - voice_alarm_respeaker_living_room_time: - name: ReSpeaker Living Room Alarm Time - has_date: false - has_time: true - - voice_alarm_respeaker_master_bedroom_time: - name: ReSpeaker Master Bedroom Alarm Time - has_date: false - has_time: true - -script: - voice_satellite_alarm_announce: - alias: Voice Satellite Alarm Announce - description: Repeat an alarm announcement on a specific Assist satellite. - mode: parallel - fields: - satellite_entity: - name: Assist satellite entity - required: true - selector: - entity: - filter: - domain: assist_satellite - device_name: - name: Device name - required: true - selector: - text: - message: - name: Message - required: false - selector: - text: - sequence: - - repeat: - count: 3 - sequence: - - action: assist_satellite.announce - target: - entity_id: "{{ satellite_entity }}" - data: - message: >- - {% set custom_message = message | default('', true) %} - {{ custom_message if custom_message else 'Alarm. Attention in ' ~ device_name ~ '.' }} - preannounce: true - - delay: "00:00:08" - - test_voice_alarm_office_jarvis: - alias: Test Voice Alarm - Office Jarvis - mode: single - sequence: - - action: script.voice_satellite_alarm_announce - data: - satellite_entity: assist_satellite.office_jarvis_satellite_assist_satellite - device_name: Office Jarvis - message: This is a test alarm for Office Jarvis. - - test_voice_alarm_respeaker_family_room: - alias: Test Voice Alarm - ReSpeaker Family Room - mode: single - sequence: - - action: script.voice_satellite_alarm_announce - data: - satellite_entity: assist_satellite.respeaker_satellite_fr_assist_satellite - device_name: ReSpeaker Family Room - message: This is a test alarm for the ReSpeaker in the family room. - - test_voice_alarm_respeaker_living_room: - alias: Test Voice Alarm - ReSpeaker Living Room - mode: single - sequence: - - action: script.voice_satellite_alarm_announce - data: - satellite_entity: assist_satellite.respeaker_satellite_lr_assist_satellite - device_name: ReSpeaker Living Room - message: This is a test alarm for the ReSpeaker in the living room. - - test_voice_alarm_respeaker_master_bedroom: - alias: Test Voice Alarm - ReSpeaker Master Bedroom - mode: single - sequence: - - action: script.voice_satellite_alarm_announce - data: - satellite_entity: assist_satellite.respeaker_satellite_mbr_assist_satellite - device_name: ReSpeaker Master Bedroom - message: This is a test alarm for the ReSpeaker in the master bedroom. diff --git a/scripts/litter_box_analyzer.py b/scripts/litter_box_analyzer.py deleted file mode 100755 index 8522ecd..0000000 --- a/scripts/litter_box_analyzer.py +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/env python3 -""" -Litter Box Camera Analyzer - MQTT-based computer vision service. - -Subscribes to base64-encoded JPEG images from the DFRobot DFR1154 ESP32-S3 -camera (published via ESPHome to MQTT) and runs analysis to detect: - - Cat presence (motion-based: sustained blob in frame) - - Box visual state (open vs. rotated/closed) via frame comparison - - Litter box stuck closed (prolonged non-open state) - - Cleanliness / needs scooping - -Publishes results back via MQTT for Home Assistant to consume. - -Dependencies: pip3 install opencv-python numpy paho-mqtt -""" - -import argparse -import base64 -import logging -import os -import signal -import sys -import time -from collections import deque -from datetime import datetime, timezone - -import cv2 -import numpy as np -import paho.mqtt.client as mqtt - -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- -CONFIG = { - "mqtt_broker": os.environ.get("MQTT_BROKER", "localhost"), - "mqtt_port": int(os.environ.get("MQTT_PORT", "1883")), - "mqtt_username": os.environ.get("MQTT_USERNAME", ""), - "mqtt_password": os.environ.get("MQTT_PASSWORD", ""), - "mqtt_topic_image": "litter_box/camera/image", - "mqtt_topic_prefix": "litter_box/status", - "motion_threshold": 15.0, # avg pixel delta for motion (0-255) - "cat_detection_frames": 4, # consecutive motion frames to confirm cat - "cat_absent_frames": 3, # consecutive no-motion to confirm cat left - "box_state_threshold": 30.0, # avg diff from open ref -> closed state - "stuck_closed_frames": 10, # consecutive "closed" frames before stuck alert - "analysis_cooldown": 2.0, # seconds between analyses - "health_check_interval": 300, # seconds between health checks when no motion - "calibration_frames": 10, # frames to average for open reference - "debug_dir": os.environ.get("DEBUG_DIR", ""), -} - -# --------------------------------------------------------------------------- -# Logging -# --------------------------------------------------------------------------- -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", -) -log = logging.getLogger("litter_box") - -# --------------------------------------------------------------------------- -# Globals -# --------------------------------------------------------------------------- -shutdown_flag = False -mqtt_client: mqtt.Client | None = None - -reference_open: np.ndarray | None = None # averaged "open/normal" frame -motion_streak: int = 0 -no_motion_streak: int = 0 -cat_present = False -cat_visit_completed = False - -box_closed_streak: int = 0 # consecutive frames in "closed" state -box_stuck_notified = False -box_state: str = "unknown" # "open", "closed", "unknown" - -cleanliness_score: int = 100 -needs_scooping = False - -calibration_frames: deque = deque(maxlen=CONFIG["calibration_frames"]) -calibrated = False - -last_analysis_time: float = 0.0 -last_mqtt_publish: dict = {} # avoid re-publishing same values - - -# --------------------------------------------------------------------------- -# Image decoding -# --------------------------------------------------------------------------- -def decode_image(payload: bytes) -> np.ndarray | None: - try: - raw = base64.b64decode(payload) - arr = np.frombuffer(raw, dtype=np.uint8) - img = cv2.imdecode(arr, cv2.IMREAD_COLOR) - if img is None: - log.warning("cv2.imdecode returned None (%d bytes)", len(raw)) - return img - except Exception as exc: - log.error("Failed to decode image: %s", exc) - return None - - -# --------------------------------------------------------------------------- -# Image comparison helpers -# --------------------------------------------------------------------------- -def _downsample_gray(img: np.ndarray, size=(50, 40)) -> np.ndarray: - small = cv2.resize(img, size, interpolation=cv2.INTER_AREA) - return cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) - - -def compute_diff_score(frame: np.ndarray, reference: np.ndarray) -> float: - """Return mean absolute pixel difference (0-255) between frame and reference.""" - f = _downsample_gray(frame) - r = _downsample_gray(reference) - return float(np.mean(np.abs(f.astype(float) - r.astype(float)))) - - -def compute_cleanliness(frame: np.ndarray, reference: np.ndarray) -> int: - """Return cleanliness % (100 = pristine, matches reference; 0 = very different).""" - diff = compute_diff_score(frame, reference) - raw = max(0.0, 100.0 - (diff * 2.5)) # scale: diff 40 -> 0%, diff 0 -> 100% - return int(min(100, raw)) - - -def build_average_frame(frames: list[np.ndarray]) -> np.ndarray: - stacked = np.stack([f.astype(np.float32) for f in frames], axis=0) - return np.mean(stacked, axis=0).astype(np.uint8) - - -# --------------------------------------------------------------------------- -# Core analysis -# --------------------------------------------------------------------------- -def analyze_frame(frame: np.ndarray) -> dict: - global reference_open, motion_streak, no_motion_streak - global cat_present, cat_visit_completed - global box_closed_streak, box_stuck_notified, box_state - global cleanliness_score, needs_scooping - global calibration_frames, calibrated - - results = { - "cat_present": False, - "cat_visit_complete": False, - "motion_detected": False, - "motion_score": 0.0, - "box_state": "unknown", - "litter_box_stuck": False, - "needs_scooping": False, - "cleanliness_score": 100, - "status_message": "OK", - } - - # --- Calibration phase: build "open/normal" reference --- - if not calibrated: - calibration_frames.append(frame.copy()) - results["status_message"] = f"Calibrating {len(calibration_frames)}/{CONFIG['calibration_frames']}..." - if len(calibration_frames) >= CONFIG["calibration_frames"]: - reference_open = build_average_frame(list(calibration_frames)) - calibrated = True - log.info("Calibration complete - open reference frame established") - results["status_message"] = "Calibrated. Monitoring started." - return results - - if reference_open is None: - results["status_message"] = "No reference frame - recalibrating..." - calibrated = False - return results - - # --- 1. Box visual state detection (open vs. closed) --- - diff_from_open = compute_diff_score(frame, reference_open) - box_is_closed = diff_from_open > CONFIG["box_state_threshold"] - - if box_is_closed: - box_closed_streak += 1 - results["box_state"] = "closed" - if box_closed_streak >= CONFIG["stuck_closed_frames"] and not box_stuck_notified: - results["litter_box_stuck"] = True - box_stuck_notified = True - results["status_message"] = ( - f"STUCK CLOSED - box closed for {box_closed_streak} frames" - ) - else: - box_closed_streak = 0 - box_stuck_notified = False - results["box_state"] = "open" - - box_state = results["box_state"] - results["box_diff_score"] = round(diff_from_open, 1) - - # --- 2. Cat presence via motion relative to the open reference --- - motion_score = diff_from_open # reuse - motion is deviation from empty/open state - results["motion_score"] = round(motion_score, 1) - - if motion_score > CONFIG["motion_threshold"]: - results["motion_detected"] = True - motion_streak += 1 - no_motion_streak = 0 - - if motion_streak >= CONFIG["cat_detection_frames"]: - results["cat_present"] = True - cat_present = True - else: - no_motion_streak += 1 - motion_streak = 0 - - if cat_present and no_motion_streak >= CONFIG["cat_absent_frames"]: - cat_present = False - results["cat_visit_complete"] = True - - results["cat_present"] = cat_present - - # --- 3. Cleanliness --- - cleanliness = compute_cleanliness(frame, reference_open) - results["cleanliness_score"] = cleanliness - if cleanliness < 45: - results["needs_scooping"] = True - results["status_message"] = results.get("status_message", "") + " Needs scooping." - - cleanliness_score = cleanliness - needs_scooping = results["needs_scooping"] - - # --- 4. Debug --- - if CONFIG["debug_dir"]: - _save_debug(frame, results) - - return results - - -# --------------------------------------------------------------------------- -# Debug frame saving -# --------------------------------------------------------------------------- -def _save_debug(frame: np.ndarray, results: dict) -> None: - ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") - path = os.path.join(CONFIG["debug_dir"], f"litter_{ts}.jpg") - annotated = frame.copy() - y = 30 - for label, color in [ - (f"Box: {results.get('box_state', '?')} Diff: {results.get('box_diff_score', 0):.1f}", (255, 255, 0)), - (f"Motion: {results.get('motion_score', 0):.1f} Clean: {results.get('cleanliness_score', 100)}%", (0, 255, 0)), - ]: - cv2.putText(annotated, label, (10, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) - y += 20 - if results.get("cat_present"): - cv2.putText(annotated, "CAT", (annotated.shape[1] - 100, 30), - cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) - if results.get("litter_box_stuck"): - cv2.putText(annotated, "STUCK!", (annotated.shape[1] - 140, 60), - cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 3) - cv2.imwrite(path, annotated) - - -# --------------------------------------------------------------------------- -# MQTT Callbacks -# --------------------------------------------------------------------------- -def on_connect(client, userdata, flags, reason_code, properties=None): - if reason_code == 0: - log.info("Connected to MQTT broker at %s:%d", CONFIG["mqtt_broker"], CONFIG["mqtt_port"]) - client.subscribe(CONFIG["mqtt_topic_image"]) - log.info("Subscribed to %s", CONFIG["mqtt_topic_image"]) - else: - log.error("MQTT connection failed: rc=%d", reason_code) - - -def on_disconnect(client, userdata, flags, reason_code, properties=None): - log.warning("MQTT disconnected (rc=%d), auto-reconnecting", reason_code) - - -def on_message(client, userdata, msg): - global last_analysis_time - - now = time.time() - if now - last_analysis_time < CONFIG["analysis_cooldown"]: - return - - img = decode_image(msg.payload) - if img is None: - return - - last_analysis_time = now - results = analyze_frame(img) - _publish_results(results) - - -def _should_publish(topic: str, value: str) -> bool: - """Skip re-publishing the same value to reduce MQTT traffic.""" - if last_mqtt_publish.get(topic) == value: - return False - last_mqtt_publish[topic] = value - return True - - -def _publish_results(results: dict) -> None: - prefix = CONFIG["mqtt_topic_prefix"] - publish = mqtt_client.publish - - cat_val = "ON" if results.get("cat_present") else "OFF" - motion_val = "ON" if results.get("motion_detected") else "OFF" - stuck_val = "ON" if results.get("litter_box_stuck") else "OFF" - scoop_val = "ON" if results.get("needs_scooping") else "OFF" - motion_score_val = str(results.get("motion_score", 0)) - clean_val = str(results.get("cleanliness_score", 100)) - box_state_val = results.get("box_state", "unknown") - msg_val = results.get("status_message", "OK") - - publish(f"{prefix}/cat_present", cat_val, retain=False) - publish(f"{prefix}/motion_detected", motion_val, retain=False) - publish(f"{prefix}/motion_score", motion_score_val, retain=False) - publish(f"{prefix}/litter_box_stuck", stuck_val, retain=False) - publish(f"{prefix}/needs_scooping", scoop_val, retain=False) - publish(f"{prefix}/cleanliness_score", clean_val, retain=False) - publish(f"{prefix}/box_state", box_state_val, retain=False) - publish(f"{prefix}/status_message", msg_val, retain=False) - - if results.get("cat_visit_complete"): - log.info("Cat visit completed!") - if results.get("litter_box_stuck"): - log.warning("LITTER BOX STUCK CLOSED!") - - -# --------------------------------------------------------------------------- -# Signal handling -# --------------------------------------------------------------------------- -def signal_handler(sig, frame): - global shutdown_flag - log.info("Received signal %d, shutting down...", sig) - shutdown_flag = True - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- -def main(): - global mqtt_client - - parser = argparse.ArgumentParser(description="Litter Box Camera Analyzer") - parser.add_argument("--broker", default=CONFIG["mqtt_broker"], help="MQTT broker host") - parser.add_argument("--port", type=int, default=CONFIG["mqtt_port"], help="MQTT broker port") - parser.add_argument("--username", default=CONFIG["mqtt_username"], help="MQTT username") - parser.add_argument("--password", default=CONFIG["mqtt_password"], help="MQTT password") - parser.add_argument("--debug-dir", default=CONFIG["debug_dir"], - help="Save annotated images to this directory") - parser.add_argument("--motion-threshold", type=float, default=CONFIG["motion_threshold"], - help="Avg pixel diff for motion (0-255, default 15)") - parser.add_argument("--box-state-threshold", type=float, default=CONFIG["box_state_threshold"], - help="Avg pixel diff from open ref to flag closed (default 30)") - parser.add_argument("--stuck-frames", type=int, default=CONFIG["stuck_closed_frames"], - help="Consecutive 'closed' frames before stuck alert") - parser.add_argument("--calibration-frames", type=int, default=CONFIG["calibration_frames"], - help="Frames to average for open reference") - args = parser.parse_args() - - CONFIG["mqtt_broker"] = args.broker - CONFIG["mqtt_port"] = args.port - CONFIG["mqtt_username"] = args.username - CONFIG["mqtt_password"] = args.password - CONFIG["motion_threshold"] = args.motion_threshold - CONFIG["box_state_threshold"] = args.box_state_threshold - CONFIG["stuck_closed_frames"] = args.stuck_frames - CONFIG["calibration_frames"] = args.calibration_frames - if args.debug_dir: - CONFIG["debug_dir"] = args.debug_dir - os.makedirs(args.debug_dir, exist_ok=True) - calibration_frames.clear() - for _ in range(CONFIG["calibration_frames"]): - calibration_frames.append(None) - calibration_frames.clear() - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="litter_box_analyzer") - mqtt_client.on_connect = on_connect - mqtt_client.on_disconnect = on_disconnect - mqtt_client.on_message = on_message - - if CONFIG["mqtt_username"]: - mqtt_client.username_pw_set(CONFIG["mqtt_username"], CONFIG["mqtt_password"]) - - mqtt_client.connect_async(CONFIG["mqtt_broker"], CONFIG["mqtt_port"], keepalive=60) - mqtt_client.loop_start() - - log.info("=== Litter Box Analyzer Started ===") - log.info(" MQTT: %s:%d", CONFIG["mqtt_broker"], CONFIG["mqtt_port"]) - log.info(" Image topic: %s", CONFIG["mqtt_topic_image"]) - log.info(" Status prefix: %s", CONFIG["mqtt_topic_prefix"]) - log.info(" Motion threshold: %.1f Box closed threshold: %.1f", - CONFIG["motion_threshold"], CONFIG["box_state_threshold"]) - log.info(" Stuck-closed frames: %d Calibration: %d frames", - CONFIG["stuck_closed_frames"], CONFIG["calibration_frames"]) - log.info(" Waiting for %d frames to calibrate open reference...", - CONFIG["calibration_frames"]) - - try: - while not shutdown_flag: - time.sleep(1) - finally: - mqtt_client.loop_stop() - mqtt_client.disconnect() - log.info("Litter Box Analyzer stopped") - - -if __name__ == "__main__": - main() diff --git a/scripts/litter_box_analyzer.sh b/scripts/litter_box_analyzer.sh deleted file mode 100755 index 664e07c..0000000 --- a/scripts/litter_box_analyzer.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -# Start / stop the Litter Box Camera Analyzer service. -# This script manages the Python analyzer as a background process. -# -# Usage: -# ./litter_box_analyzer.sh start -# ./litter_box_analyzer.sh stop -# ./litter_box_analyzer.sh status -# -# Configuration (edit or set env vars): -# MQTT_BROKER - MQTT broker hostname/IP (default: localhost) -# MQTT_PORT - MQTT port (default: 1883) -# MQTT_USERNAME - MQTT username (default: none) -# MQTT_PASSWORD - MQTT password (default: none) -# DEBUG_DIR - Save annotated debug images (default: none) - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -ANALYZER="${SCRIPT_DIR}/litter_box_analyzer.py" -PIDFILE="${SCRIPT_DIR}/.litter_box_analyzer.pid" -LOGFILE="${SCRIPT_DIR}/litter_box_analyzer.log" - -MQTT_BROKER="${MQTT_BROKER:-localhost}" -MQTT_PORT="${MQTT_PORT:-1883}" -MOTION_THRESHOLD="${MOTION_THRESHOLD:-15}" -BOX_STATE_THRESHOLD="${BOX_STATE_THRESHOLD:-30}" -STUCK_FRAMES="${STUCK_FRAMES:-10}" -CALIBRATION_FRAMES="${CALIBRATION_FRAMES:-10}" - -start() { - if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then - echo "Analyzer is already running (PID $(cat "$PIDFILE"))" - return 1 - fi - - local extra_args=() - [ -n "${DEBUG_DIR:-}" ] && extra_args+=(--debug-dir "$DEBUG_DIR") - - echo "Starting Litter Box Analyzer..." - python3 "$ANALYZER" \ - --broker "$MQTT_BROKER" \ - --port "$MQTT_PORT" \ - --motion-threshold "$MOTION_THRESHOLD" \ - --box-state-threshold "$BOX_STATE_THRESHOLD" \ - --stuck-frames "$STUCK_FRAMES" \ - --calibration-frames "$CALIBRATION_FRAMES" \ - "${extra_args[@]}" \ - >> "$LOGFILE" 2>&1 & - echo $! > "$PIDFILE" - echo "Started (PID $!). Log: $LOGFILE" -} - -stop() { - if [ ! -f "$PIDFILE" ]; then - echo "No PID file found. Analyzer may not be running." - return 1 - fi - PID=$(cat "$PIDFILE") - if kill "$PID" 2>/dev/null; then - echo "Stopped analyzer (PID $PID)" - rm -f "$PIDFILE" - else - echo "Process $PID not found. Removing stale PID file." - rm -f "$PIDFILE" - fi -} - -status() { - if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then - echo "Analyzer is running (PID $(cat "$PIDFILE"))" - else - echo "Analyzer is NOT running" - [ -f "$PIDFILE" ] && echo "(stale PID file exists)" && rm -f "$PIDFILE" - fi -} - -case "${1:-}" in - start) start ;; - stop) stop ;; - restart) stop; sleep 1; start ;; - status) status ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 1 - ;; -esac