Refactor integration health monitor automation, remove litter box monitor package, update lunch menus formatting, delete voice satellite alarms, and remove litter box analyzer scripts
This commit is contained in:
@@ -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
|
|
||||||
@@ -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
|
# 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
|
# (see homeassistant/components/lovelace: url_path validation). A hyphen-less
|
||||||
# key fails validation for the whole `lovelace:` block, not just its own entry,
|
# key fails validation for the whole `lovelace:` block, not just its own entry,
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mode: yaml
|
|
||||||
title: Voice Alarms
|
|
||||||
icon: mdi:alarm-panel
|
|
||||||
show_in_sidebar: false
|
|
||||||
filename: dashboards/views/voice-alarms-dashboard.yaml
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+1369
-1369
File diff suppressed because it is too large
Load Diff
@@ -1,115 +1,115 @@
|
|||||||
################################################################################
|
# ################################################################################
|
||||||
# chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
# # chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||||
# Kids: Jordyn, Declan, Chloe
|
# # Kids: Jordyn, Declan, Chloe
|
||||||
################################################################################
|
# ################################################################################
|
||||||
|
|
||||||
title: "Chore Tracker"
|
# title: "Chore Tracker"
|
||||||
views:
|
# views:
|
||||||
- title: Today
|
# - title: Today
|
||||||
icon: mdi:checkbox-marked-circle
|
# icon: mdi:checkbox-marked-circle
|
||||||
path: chores
|
# path: chores
|
||||||
cards:
|
# cards:
|
||||||
- type: horizontal-stack
|
# - type: horizontal-stack
|
||||||
cards:
|
# cards:
|
||||||
- type: markdown
|
# - type: markdown
|
||||||
content: >
|
# content: >
|
||||||
### Jordyn
|
# ### Jordyn
|
||||||
{{
|
# {{
|
||||||
'All done!' if states('sensor.jordyn_all_chores_done') == 'True'
|
# 'All done!' if states('sensor.jordyn_all_chores_done') == 'True'
|
||||||
else states('sensor.jordyn_chores_done_today') ~ '/4 chores done'
|
# else states('sensor.jordyn_chores_done_today') ~ '/4 chores done'
|
||||||
}}
|
# }}
|
||||||
|
|
||||||
- type: markdown
|
# - type: markdown
|
||||||
content: >
|
# content: >
|
||||||
### Declan
|
# ### Declan
|
||||||
{{
|
# {{
|
||||||
'All done!' if states('sensor.declan_all_chores_done') == 'True'
|
# 'All done!' if states('sensor.declan_all_chores_done') == 'True'
|
||||||
else states('sensor.declan_chores_done_today') ~ '/2 chores done'
|
# else states('sensor.declan_chores_done_today') ~ '/2 chores done'
|
||||||
}}
|
# }}
|
||||||
|
|
||||||
- type: markdown
|
# - type: markdown
|
||||||
content: >
|
# content: >
|
||||||
### Chloe
|
# ### Chloe
|
||||||
{{
|
# {{
|
||||||
'All done!' if states('sensor.chloe_all_chores_done') == 'True'
|
# 'All done!' if states('sensor.chloe_all_chores_done') == 'True'
|
||||||
else states('sensor.chloe_chores_done_today') ~ '/3 chores done'
|
# else states('sensor.chloe_chores_done_today') ~ '/3 chores done'
|
||||||
}}
|
# }}
|
||||||
|
|
||||||
- type: button
|
# - type: button
|
||||||
name: "Reset ALL Chores"
|
# name: "Reset ALL Chores"
|
||||||
icon: mdi:restart-alert
|
# icon: mdi:restart-alert
|
||||||
tap_action:
|
# tap_action:
|
||||||
action: call-service
|
# action: call-service
|
||||||
service: input_button.press
|
# service: input_button.press
|
||||||
target:
|
# target:
|
||||||
entity_id: input_button.reset_all_chores
|
# entity_id: input_button.reset_all_chores
|
||||||
- type: entities
|
# - type: entities
|
||||||
title: " Jordyn's Chores"
|
# title: " Jordyn's Chores"
|
||||||
entities:
|
# entities:
|
||||||
- entity: switch.chore_tracker_jordyn_put_on_underwear
|
# - entity: switch.chore_tracker_jordyn_put_on_underwear
|
||||||
name: " Put on underwear"
|
# name: " Put on underwear"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_jordyn_brush_teeth
|
# - entity: switch.chore_tracker_jordyn_brush_teeth
|
||||||
name: " Brush Teeth"
|
# name: " Brush Teeth"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_jordyn_fill_water_bowls
|
# - entity: switch.chore_tracker_jordyn_fill_water_bowls
|
||||||
name: " Fill water bowls"
|
# name: " Fill water bowls"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_jordyn_restock_cat_food_cans
|
# - entity: switch.chore_tracker_jordyn_restock_cat_food_cans
|
||||||
name: " Restock Cat Food Cans"
|
# name: " Restock Cat Food Cans"
|
||||||
- type: divider
|
# - type: divider
|
||||||
- entity: sensor.jordyn_chores_done_today
|
# - entity: sensor.jordyn_chores_done_today
|
||||||
name: "Chores done today"
|
# name: "Chores done today"
|
||||||
- entity: sensor.jordyn_all_chores_done
|
# - entity: sensor.jordyn_all_chores_done
|
||||||
name: "All Done?"
|
# name: "All Done?"
|
||||||
- type: button
|
# - type: button
|
||||||
name: "Reset Jordyn's Chores"
|
# name: "Reset Jordyn's Chores"
|
||||||
tap_action:
|
# tap_action:
|
||||||
action: call-service
|
# action: call-service
|
||||||
service: input_button.press
|
# service: input_button.press
|
||||||
target:
|
# target:
|
||||||
entity_id: input_button.jordyn_reset_chores
|
# entity_id: input_button.jordyn_reset_chores
|
||||||
- type: entities
|
# - type: entities
|
||||||
title: " Declan's Chores"
|
# title: " Declan's Chores"
|
||||||
entities:
|
# entities:
|
||||||
- entity: switch.chore_tracker_declan_take_morning_pill
|
# - entity: switch.chore_tracker_declan_take_morning_pill
|
||||||
name: " Take morning pill"
|
# name: " Take morning pill"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_declan_scoop_dog_poop
|
# - entity: switch.chore_tracker_declan_scoop_dog_poop
|
||||||
name: " Scoop Dog Poop"
|
# name: " Scoop Dog Poop"
|
||||||
- type: divider
|
# - type: divider
|
||||||
- entity: sensor.declan_chores_done_today
|
# - entity: sensor.declan_chores_done_today
|
||||||
name: "Chores done today"
|
# name: "Chores done today"
|
||||||
- entity: sensor.declan_all_chores_done
|
# - entity: sensor.declan_all_chores_done
|
||||||
name: "All Done?"
|
# name: "All Done?"
|
||||||
- type: button
|
# - type: button
|
||||||
name: "Reset Declan's Chores"
|
# name: "Reset Declan's Chores"
|
||||||
tap_action:
|
# tap_action:
|
||||||
action: call-service
|
# action: call-service
|
||||||
service: input_button.press
|
# service: input_button.press
|
||||||
target:
|
# target:
|
||||||
entity_id: input_button.declan_reset_chores
|
# entity_id: input_button.declan_reset_chores
|
||||||
- type: entities
|
# - type: entities
|
||||||
title: " Chloe's Chores"
|
# title: " Chloe's Chores"
|
||||||
entities:
|
# entities:
|
||||||
- entity: switch.chore_tracker_chloe_fill_kitty_feeders
|
# - entity: switch.chore_tracker_chloe_fill_kitty_feeders
|
||||||
name: " Fill kitty feeders"
|
# name: " Fill kitty feeders"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_chloe_scoop_kitty_litter
|
# - entity: switch.chore_tracker_chloe_scoop_kitty_litter
|
||||||
name: " Scoop Kitty Litter"
|
# name: " Scoop Kitty Litter"
|
||||||
|
|
||||||
- entity: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
# - entity: switch.chore_tracker_chloe_replace_kitty_litter_bags
|
||||||
name: " Replace Kitty Litter Bags"
|
# name: " Replace Kitty Litter Bags"
|
||||||
- type: divider
|
# - type: divider
|
||||||
- entity: sensor.chloe_chores_done_today
|
# - entity: sensor.chloe_chores_done_today
|
||||||
name: "Chores done today"
|
# name: "Chores done today"
|
||||||
- entity: sensor.chloe_all_chores_done
|
# - entity: sensor.chloe_all_chores_done
|
||||||
name: "All Done?"
|
# name: "All Done?"
|
||||||
- type: button
|
# - type: button
|
||||||
name: "Reset Chloe's Chores"
|
# name: "Reset Chloe's Chores"
|
||||||
tap_action:
|
# tap_action:
|
||||||
action: call-service
|
# action: call-service
|
||||||
service: input_button.press
|
# service: input_button.press
|
||||||
target:
|
# target:
|
||||||
entity_id: input_button.chloe_reset_chores
|
# entity_id: input_button.chloe_reset_chores
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +1,81 @@
|
|||||||
################################################################################
|
# ################################################################################
|
||||||
# chores_config.yaml — YOUR SINGLE SOURCE OF TRUTH
|
# # chores_config.yaml — YOUR SINGLE SOURCE OF TRUTH
|
||||||
#
|
# #
|
||||||
# Edit this file to change kids, chores, and settings.
|
# # Edit this file to change kids, chores, and settings.
|
||||||
# Then run: python3 generate.py
|
# # Then run: python3 generate.py
|
||||||
#
|
# #
|
||||||
# ICONS: Use MDI codepoints from https://pictogrammers.com/library/mdi/
|
# # ICONS: Use MDI codepoints from https://pictogrammers.com/library/mdi/
|
||||||
# Format: "\U000FXXXX" e.g. mdi:bed = "\U000F02E3"
|
# # Format: "\U000FXXXX" e.g. mdi:bed = "\U000F02E3"
|
||||||
# The kid avatar field also uses MDI codepoints.
|
# # The kid avatar field also uses MDI codepoints.
|
||||||
#
|
# #
|
||||||
# Common chore icons:
|
# # Common chore icons:
|
||||||
# Bed: "\U000F02E3" mdi:bed
|
# # Bed: "\U000F02E3" mdi:bed
|
||||||
# Brush teeth: "\U000F09A9" mdi:toothbrush
|
# # Brush teeth: "\U000F09A9" mdi:toothbrush
|
||||||
# Broom: "\U000F00A8" mdi:broom
|
# # Broom: "\U000F00A8" mdi:broom
|
||||||
# Book: "\U000F0219" mdi:book-open-variant
|
# # Book: "\U000F0219" mdi:book-open-variant
|
||||||
# Dog: "\U000F01F9" mdi:dog
|
# # Dog: "\U000F01F9" mdi:dog
|
||||||
# Paw: "\U000F0265" mdi:paw
|
# # Paw: "\U000F0265" mdi:paw
|
||||||
# Silverware: "\U000F03E7" mdi:silverware-fork-knife
|
# # Silverware: "\U000F03E7" mdi:silverware-fork-knife
|
||||||
# Trash: "\U000F05B8" mdi:trash-can
|
# # Trash: "\U000F05B8" mdi:trash-can
|
||||||
# Watering can: "\U000F1B25" mdi:watering-can
|
# # Watering can: "\U000F1B25" mdi:watering-can
|
||||||
# Piano: "\U000F0F9E" mdi:piano
|
# # Piano: "\U000F0F9E" mdi:piano
|
||||||
# Dumbbell: "\U000F0F1E" mdi:dumbbell
|
# # Dumbbell: "\U000F0F1E" mdi:dumbbell
|
||||||
# Shower: "\U000F0467" mdi:shower
|
# # Shower: "\U000F0467" mdi:shower
|
||||||
################################################################################
|
# ################################################################################
|
||||||
|
|
||||||
settings:
|
# settings:
|
||||||
device_name: chore-tracker
|
# device_name: chore-tracker
|
||||||
friendly_name: "Chore Tracker"
|
# friendly_name: "Chore Tracker"
|
||||||
wifi_ssid: !secret wifi_iot_ssid
|
# wifi_ssid: !secret wifi_iot_ssid
|
||||||
wifi_password: !secret wifi_password
|
# wifi_password: !secret wifi_password
|
||||||
api_key: !secret api_encryption_key
|
# api_key: !secret api_encryption_key
|
||||||
ota_password: !secret ota_password
|
# ota_password: !secret ota_password
|
||||||
reset_time: "00:00:00"
|
# reset_time: "00:00:00"
|
||||||
reminder_time: "18:00"
|
# reminder_time: "18:00"
|
||||||
notify_service: notify.notify
|
# notify_service: notify.notify
|
||||||
static_ip: 192.168.69.115
|
# static_ip: 192.168.69.115
|
||||||
gateway: 192.168.69.1
|
# gateway: 192.168.69.1
|
||||||
subnet: 255.255.255.0
|
# subnet: 255.255.255.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ── KIDS ──────────────────────────────────────────────────────────────────────
|
# # ── KIDS ──────────────────────────────────────────────────────────────────────
|
||||||
# avatar: MDI codepoint shown in the sidebar on the kid's chore page
|
# # avatar: MDI codepoint shown in the sidebar on the kid's chore page
|
||||||
# color / color_dark: hex colours for that kid's sidebar
|
# # color / color_dark: hex colours for that kid's sidebar
|
||||||
#
|
# #
|
||||||
kids:
|
# kids:
|
||||||
- name: Jordyn
|
# - name: Jordyn
|
||||||
avatar: "\U000F011B" # mdi:cat
|
# 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: "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.
|
# 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:
|
# chores:
|
||||||
- name: Put on underwear
|
# - name: Put on underwear
|
||||||
icon: "\U000F02E3" # mdi:bed
|
# icon: "\U000F02E3" # mdi:bed
|
||||||
- name: Brush Teeth
|
# - name: Brush Teeth
|
||||||
icon: "\U000F09A9" # mdi:toothbrush
|
# icon: "\U000F09A9" # mdi:toothbrush
|
||||||
- name: Fill water bowls
|
# - name: Fill water bowls
|
||||||
icon: "\U000F01AB" # mdi:cup-water
|
# icon: "\U000F01AB" # mdi:cup-water
|
||||||
- name: Restock Cat Food Cans
|
# - name: Restock Cat Food Cans
|
||||||
icon: "\U000F171C" # mdi:food-turkey
|
# icon: "\U000F171C" # mdi:food-turkey
|
||||||
|
|
||||||
- name: Declan
|
# - name: Declan
|
||||||
avatar: "\U000F1477" # mdi:wizard-hat
|
# avatar: "\U000F1477" # mdi:wizard-hat
|
||||||
color: "720026" # #720026 is a deep red, used for the kid's sidebar background.
|
# 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.
|
# color_dark: "4F000B" # #4F000B is a darker version of #720026, easier to read when used as a background colour for the sidebar.
|
||||||
chores:
|
# chores:
|
||||||
- name: Take morning pill
|
# - name: Take morning pill
|
||||||
icon: "\U000F0402" # mdi:pill
|
# icon: "\U000F0402" # mdi:pill
|
||||||
- name: Scoop Dog Poop
|
# - name: Scoop Dog Poop
|
||||||
icon: "\U000F01F7" # mdi:emoticon-poop
|
# icon: "\U000F01F7" # mdi:emoticon-poop
|
||||||
|
|
||||||
- name: Chloe
|
# - name: Chloe
|
||||||
avatar: "\U000F16A3" # mdi:robot-excited
|
# avatar: "\U000F16A3" # mdi:robot-excited
|
||||||
color: "73683B" # #73683B is a light brown, used for the kid's sidebar background.
|
# 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.
|
# color_dark: "583E23" # #583E23 is a darker version of #73683B, easier to read when used as a background colour for the sidebar.
|
||||||
chores:
|
# chores:
|
||||||
- name: Fill kitty feeders
|
# - name: Fill kitty feeders
|
||||||
icon: "\U000F02A9" # mdi:bowl-outline
|
# icon: "\U000F02A9" # mdi:bowl-outline
|
||||||
- name: Scoop Kitty Litter
|
# - name: Scoop Kitty Litter
|
||||||
icon: "\U000F01F7" # mdi:emoticon-poop
|
# icon: "\U000F01F7" # mdi:emoticon-poop
|
||||||
- name: Replace Kitty Litter Bags
|
# - name: Replace Kitty Litter Bags
|
||||||
icon: "\U000F01F7" # mdi:emoticon-poop
|
# icon: "\U000F01F7" # mdi:emoticon-poop
|
||||||
+989
-989
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
CODEOWNERS = ["@blacknell"]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
#pragma once
|
|
||||||
#include "esphome/core/automation.h"
|
|
||||||
#include "max17043.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace max17043 {
|
|
||||||
|
|
||||||
template<typename... Ts> class SleepAction : public Action<Ts...> {
|
|
||||||
public:
|
|
||||||
explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {}
|
|
||||||
|
|
||||||
void play(Ts... x) override { this->max17043_->sleep_mode(); }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
MAX17043Component *max17043_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace max17043
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -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<uint8_t *>(&config_reg), 2) != i2c::ERROR_OK) {
|
|
||||||
this->status_set_warning();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
config_reg = i2c::i2ctohs(config_reg) & MAX17043_CONFIG_SAFE_MASK;
|
|
||||||
ESP_LOGV(TAG, "MAX17043 CONFIG register reads 0x%X", config_reg);
|
|
||||||
|
|
||||||
if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) {
|
|
||||||
ESP_LOGW(TAG, "Non-standard CONFIG register 0x%X at 0x36; continuing in compatible mode", config_reg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to write back to config register to reset the sleep bit
|
|
||||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) {
|
|
||||||
this->status_set_error("sleep reset failed");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MAX17043Component::dump_config() {
|
|
||||||
ESP_LOGCONFIG(TAG, "MAX17043:");
|
|
||||||
LOG_I2C_DEVICE(this);
|
|
||||||
if (this->is_failed()) {
|
|
||||||
ESP_LOGE(TAG, "Communication with MAX17043 failed");
|
|
||||||
}
|
|
||||||
LOG_UPDATE_INTERVAL(this);
|
|
||||||
LOG_SENSOR(" ", "Battery Voltage", this->voltage_sensor_);
|
|
||||||
LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_);
|
|
||||||
}
|
|
||||||
|
|
||||||
float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; }
|
|
||||||
|
|
||||||
void MAX17043Component::sleep_mode() {
|
|
||||||
if (!this->is_failed()) {
|
|
||||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) {
|
|
||||||
ESP_LOGW(TAG, "Unable to write the sleep bit to config register");
|
|
||||||
this->status_set_warning();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace max17043
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -60,7 +60,10 @@ esp32_camera:
|
|||||||
name: "${friendly_name}"
|
name: "${friendly_name}"
|
||||||
external_clock:
|
external_clock:
|
||||||
pin: GPIO5
|
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:
|
i2c_pins:
|
||||||
sda: GPIO8
|
sda: GPIO8
|
||||||
scl: GPIO9
|
scl: GPIO9
|
||||||
@@ -87,14 +90,17 @@ esp32_camera:
|
|||||||
contrast: -2
|
contrast: -2
|
||||||
saturation: 0
|
saturation: 0
|
||||||
|
|
||||||
# Auto-Exposure & Gain tuning (AEC2 + AE Level lift shadows away from bright LEDs).
|
# Exposure & gain are MANUAL on purpose. The two LED strips in frame kept the
|
||||||
# The gain ceiling is the main lever for the tinted door: 8X was capping how
|
# auto-exposure meter satisfied, so AEC never lengthened the exposure and the
|
||||||
# far AGC could amplify, leaving everything outside the LED strips black.
|
# tinted-door scene stayed black regardless of gain ceiling or AE level.
|
||||||
aec_mode: auto
|
# 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
|
aec2: true
|
||||||
ae_level: 2
|
ae_level: 2 # only consulted if AEC is flipped back to auto
|
||||||
agc_mode: auto
|
agc_mode: manual
|
||||||
agc_gain_ceiling: 128X
|
agc_value: 30 # max sensor gain (0-30)
|
||||||
|
agc_gain_ceiling: 128X # only consulted if AGC is flipped back to auto
|
||||||
wb_mode: auto
|
wb_mode: auto
|
||||||
|
|
||||||
# Camera endpoints:
|
# Camera endpoints:
|
||||||
@@ -113,6 +119,25 @@ switch:
|
|||||||
pin: GPIO47
|
pin: GPIO47
|
||||||
restore_mode: ALWAYS_OFF
|
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:
|
sensor:
|
||||||
- platform: wifi_signal
|
- platform: wifi_signal
|
||||||
name: "${friendly_name} WiFi Signal"
|
name: "${friendly_name} WiFi Signal"
|
||||||
@@ -159,6 +184,31 @@ interval:
|
|||||||
|
|
||||||
# Dynamic controls exposed to Home Assistant for real-time camera tuning
|
# Dynamic controls exposed to Home Assistant for real-time camera tuning
|
||||||
number:
|
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
|
- platform: template
|
||||||
name: "${friendly_name} Brightness"
|
name: "${friendly_name} Brightness"
|
||||||
min_value: -2
|
min_value: -2
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_small_family_room_lamp
|
|
||||||
entity_id: light.small_family_room_lamp
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_small_family_room_lamp
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_small_family_room_lamp);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_big_family_room_lamp
|
|
||||||
entity_id: switch.big_family_room_lamp
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_big_family_room_lamp
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_big_family_room_lamp);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
CODEOWNERS = ["@blacknell"]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
#pragma once
|
|
||||||
#include "esphome/core/automation.h"
|
|
||||||
#include "max17043.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace max17043 {
|
|
||||||
|
|
||||||
template<typename... Ts> class SleepAction : public Action<Ts...> {
|
|
||||||
public:
|
|
||||||
explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {}
|
|
||||||
|
|
||||||
void play(Ts... x) override { this->max17043_->sleep_mode(); }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
MAX17043Component *max17043_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace max17043
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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: '%'
|
|
||||||
@@ -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_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_small_family_room_lamp
|
|
||||||
entity_id: light.small_family_room_lamp
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_small_family_room_lamp
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_small_family_room_lamp);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_living_room_lamp_1
|
|
||||||
entity_id: light.living_room_lamp_1
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_family_room_standing_lamp
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x3D0002) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_family_room_standing_lamp);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_living_room_light_2
|
|
||||||
entity_id: light.living_room_light_2
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_living_room_main_light
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_living_room_main_light);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_patio_light_1
|
|
||||||
entity_id: light.patio_light_1
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_living_room_patio_light
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_outside_patio_light
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t off_color = lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn1 = id(btn_living_room_patio_light);
|
|
||||||
auto *btn2 = id(btn_outside_patio_light);
|
|
||||||
if (x == "on") {
|
|
||||||
lv_obj_set_style_text_color(btn1, lv_color_hex(0x1A0C00), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), lv_color_hex(0x1A0C00), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
lv_obj_set_style_text_color(btn2, lv_color_hex(0x001A33), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), lv_color_hex(0x001A33), static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lv_obj_set_style_text_color(btn1, off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn1); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn1, i), off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
lv_obj_set_style_text_color(btn2, off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn2); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn2, i), off_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_kitchen_sink_light
|
|
||||||
entity_id: light.kitchen_sink_light
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_kitchen_sink_light
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_kitchen_sink_light);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_office_lamp_2
|
|
||||||
entity_id: light.office_lamp_2
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_office_lamp
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_office_lamp);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_office_led_strip
|
|
||||||
entity_id: light.led_strip_controller_led_strip_controller
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_office_led_strip
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_office_led_strip);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_garage_cam_light
|
|
||||||
entity_id: light.esp32_saturn4_cam_esp32_saturn4_cam_light
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_garage_cam_light
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_garage_cam_light);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_office_echo_plug
|
|
||||||
entity_id: switch.office_echo_plug
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_office_echo_plug
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_office_echo_plug);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_office_end_table_lamp_outlet
|
|
||||||
entity_id: switch.office_end_table_lamp_socket_1
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_office_end_table_lamp_outlet
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_office_end_table_lamp_outlet);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_office_wax_warmer_outlet
|
|
||||||
entity_id: switch.office_wax_warmer_socket_1
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_office_wax_warmer_outlet
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_office_wax_warmer_outlet);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_outside_lamppost_outlet_1
|
|
||||||
entity_id: switch.lamppost_outlets_socket_1
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_outside_lamppost_outlet_1
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_outside_lamppost_outlet_1);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_outside_lamppost_outlet_2
|
|
||||||
entity_id: switch.lamppost_outlets_socket_2
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_outside_lamppost_outlet_2
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_outside_lamppost_outlet_2);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_outside_porch_decor_outlet
|
|
||||||
entity_id: switch.washing_machine_socket_1
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_outside_porch_decor_outlet
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_outside_porch_decor_outlet);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_living_room_garland_switch
|
|
||||||
entity_id: switch.big_family_room_lamp
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_living_room_garland_switch
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001F15) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_living_room_garland_switch);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_living_room_calendar_switch
|
|
||||||
entity_id: switch.digital_calendar
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_living_room_calendar_switch
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1E0F3F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_living_room_calendar_switch);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_upstairs_airquality_oled
|
|
||||||
entity_id: switch.airqualitysensor_3_oled_power
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_upstairs_airquality_oled
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x001A33) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_upstairs_airquality_oled);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_upstairs_camera_motion
|
|
||||||
entity_id: switch.upstairs_camera_motion_detection
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_upstairs_camera_motion
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_upstairs_camera_motion);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_garage_fume_exhaust_fan
|
|
||||||
entity_id: switch.fume_exhaust_fan
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_garage_fume_exhaust_fan
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x22001F) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_garage_fume_exhaust_fan);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ts_garage_resin_printer_heater
|
|
||||||
entity_id: switch.resin_printer_heater
|
|
||||||
internal: true
|
|
||||||
on_value:
|
|
||||||
then:
|
|
||||||
- lvgl.widget.update:
|
|
||||||
id: btn_garage_resin_printer_heater
|
|
||||||
state:
|
|
||||||
checked: !lambda return x == "on";
|
|
||||||
- lambda: |-
|
|
||||||
const lv_color_t text_color = (x == "on") ? lv_color_hex(0x1A0C00) : lv_color_hex(0xE3E2E6);
|
|
||||||
auto *btn = id(btn_garage_resin_printer_heater);
|
|
||||||
lv_obj_set_style_text_color(btn, text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
for (uint32_t i = 0; i < lv_obj_get_child_cnt(btn); i++) {
|
|
||||||
lv_obj_set_style_text_color(lv_obj_get_child(btn, i), text_color, static_cast<lv_style_selector_t>(LV_PART_MAIN | LV_STATE_DEFAULT));
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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:
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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:
|
|
||||||
@@ -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();'
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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();'
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +1,65 @@
|
|||||||
################################################################################
|
# ################################################################################
|
||||||
# chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
# # chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml)
|
||||||
# Kids: Jordyn, Declan, Chloe
|
# # Kids: Jordyn, Declan, Chloe
|
||||||
#
|
# #
|
||||||
# BIDIRECTIONAL SYNC:
|
# # BIDIRECTIONAL SYNC:
|
||||||
# Screen to HA: Each switch calls homeassistant.service on toggle
|
# # Screen to HA: Each switch calls homeassistant.service on toggle
|
||||||
# HA to Screen: ESPHome native API handles this automatically
|
# # HA to Screen: ESPHome native API handles this automatically
|
||||||
################################################################################
|
# ################################################################################
|
||||||
|
|
||||||
input_button:
|
# input_button:
|
||||||
jordyn_reset_chores:
|
# jordyn_reset_chores:
|
||||||
name: "Reset Jordyn's Chores"
|
# name: "Reset Jordyn's Chores"
|
||||||
icon: mdi:restart
|
# icon: mdi:restart
|
||||||
declan_reset_chores:
|
# declan_reset_chores:
|
||||||
name: "Reset Declan's Chores"
|
# name: "Reset Declan's Chores"
|
||||||
icon: mdi:restart
|
# icon: mdi:restart
|
||||||
chloe_reset_chores:
|
# chloe_reset_chores:
|
||||||
name: "Reset Chloe's Chores"
|
# name: "Reset Chloe's Chores"
|
||||||
icon: mdi:restart
|
# icon: mdi:restart
|
||||||
reset_all_chores:
|
# reset_all_chores:
|
||||||
name: "Reset All Chores"
|
# name: "Reset All Chores"
|
||||||
icon: mdi:restart-alert
|
# icon: mdi:restart-alert
|
||||||
|
|
||||||
template:
|
# template:
|
||||||
- sensor:
|
# - sensor:
|
||||||
- name: "Jordyn Chores Done Today"
|
# - name: "Jordyn Chores Done Today"
|
||||||
unique_id: jordyn_chores_done_today
|
# unique_id: jordyn_chores_done_today
|
||||||
icon: mdi:check-circle
|
# icon: mdi:check-circle
|
||||||
state: >
|
# 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')] %}
|
# {% 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 }}
|
# {{ chores | select('equalto', 'on') | list | count }}
|
||||||
|
|
||||||
- name: "Jordyn All Chores Done"
|
# - name: "Jordyn All Chores Done"
|
||||||
unique_id: jordyn_all_chores_done
|
# unique_id: jordyn_all_chores_done
|
||||||
icon: mdi:check-all
|
# icon: mdi:check-all
|
||||||
state: >
|
# state: >
|
||||||
{{ states('sensor.jordyn_chores_done_today') | int == 4 }}
|
# {{ states('sensor.jordyn_chores_done_today') | int == 4 }}
|
||||||
|
|
||||||
- name: "Declan Chores Done Today"
|
# - name: "Declan Chores Done Today"
|
||||||
unique_id: declan_chores_done_today
|
# unique_id: declan_chores_done_today
|
||||||
icon: mdi:check-circle
|
# icon: mdi:check-circle
|
||||||
state: >
|
# state: >
|
||||||
{% set chores = [states('switch.chore_tracker_declan_take_morning_pill'), states('switch.chore_tracker_declan_scoop_dog_poop')] %}
|
# {% set chores = [states('switch.chore_tracker_declan_take_morning_pill'), states('switch.chore_tracker_declan_scoop_dog_poop')] %}
|
||||||
{{ chores | select('equalto', 'on') | list | count }}
|
# {{ chores | select('equalto', 'on') | list | count }}
|
||||||
|
|
||||||
- name: "Declan All Chores Done"
|
# - name: "Declan All Chores Done"
|
||||||
unique_id: declan_all_chores_done
|
# unique_id: declan_all_chores_done
|
||||||
icon: mdi:check-all
|
# icon: mdi:check-all
|
||||||
state: >
|
# state: >
|
||||||
{{ states('sensor.declan_chores_done_today') | int == 2 }}
|
# {{ states('sensor.declan_chores_done_today') | int == 2 }}
|
||||||
|
|
||||||
- name: "Chloe Chores Done Today"
|
# - name: "Chloe Chores Done Today"
|
||||||
unique_id: chloe_chores_done_today
|
# unique_id: chloe_chores_done_today
|
||||||
icon: mdi:check-circle
|
# icon: mdi:check-circle
|
||||||
state: >
|
# 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')] %}
|
# {% 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 }}
|
# {{ chores | select('equalto', 'on') | list | count }}
|
||||||
|
|
||||||
- name: "Chloe All Chores Done"
|
# - name: "Chloe All Chores Done"
|
||||||
unique_id: chloe_all_chores_done
|
# unique_id: chloe_all_chores_done
|
||||||
icon: mdi:check-all
|
# icon: mdi:check-all
|
||||||
state: >
|
# state: >
|
||||||
{{ states('sensor.chloe_chores_done_today') | int == 3 }}
|
# {{ states('sensor.chloe_chores_done_today') | int == 3 }}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
# Integration Health Monitor Package
|
# # Integration Health Monitor Package
|
||||||
|
|
||||||
automation:
|
# automation:
|
||||||
# ----------------------------------------------------------
|
# # ----------------------------------------------------------
|
||||||
# Kasa-Specific Recovery Attempt
|
# # Kasa-Specific Recovery Attempt
|
||||||
# ----------------------------------------------------------
|
# # ----------------------------------------------------------
|
||||||
- id: integration_health_kasa_recovery
|
# - id: integration_health_kasa_recovery
|
||||||
alias: "🔄 Attempt Kasa Integration Reload on Failure"
|
# alias: "🔄 Attempt Kasa Integration Reload on Failure"
|
||||||
description: "Automatically attempts to reload Kasa integration after errors"
|
# description: "Automatically attempts to reload Kasa integration after errors"
|
||||||
mode: single
|
# mode: single
|
||||||
trigger:
|
# trigger:
|
||||||
- platform: event
|
# - platform: event
|
||||||
event_type: system_log_event
|
# event_type: system_log_event
|
||||||
event_data:
|
# event_data:
|
||||||
level: ERROR
|
# level: ERROR
|
||||||
condition:
|
# condition:
|
||||||
- condition: template
|
# - condition: template
|
||||||
value_template: >
|
# value_template: >
|
||||||
{{ 'kasa' in (trigger.event.data.name | default('') | lower)
|
# {{ 'kasa' in (trigger.event.data.name | default('') | lower)
|
||||||
and '403' in (trigger.event.data.message | default('')) }}
|
# and '403' in (trigger.event.data.message | default('')) }}
|
||||||
action:
|
# action:
|
||||||
- delay:
|
# - delay:
|
||||||
minutes: 2
|
# minutes: 2
|
||||||
- service: persistent_notification.create
|
# - service: persistent_notification.create
|
||||||
data:
|
# data:
|
||||||
title: "🔄 Attempting Kasa Recovery"
|
# title: "🔄 Attempting Kasa Recovery"
|
||||||
message: "Detected Kasa authentication errors. Attempting integration reload..."
|
# message: "Detected Kasa authentication errors. Attempting integration reload..."
|
||||||
notification_id: "kasa_recovery"
|
# notification_id: "kasa_recovery"
|
||||||
- service: homeassistant.reload_config_entry
|
# - service: homeassistant.reload_config_entry
|
||||||
target:
|
# target:
|
||||||
entity_id: switch.studio_plug_1_2
|
# entity_id: switch.studio_plug_1_2
|
||||||
- delay:
|
# - delay:
|
||||||
hours: 1
|
# hours: 1
|
||||||
|
|||||||
@@ -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
|
|
||||||
+59
-59
@@ -1,62 +1,62 @@
|
|||||||
################################################################################
|
# ################################################################################
|
||||||
# lunch_menus.yaml
|
# # lunch_menus.yaml
|
||||||
#
|
# #
|
||||||
# Pulls Mon–Fri of the current school week from each lunch Google Calendar into
|
# # 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
|
# # 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-
|
# # dashboard renders those attributes — the stock calendar card has no school-
|
||||||
# week view.
|
# # week view.
|
||||||
################################################################################
|
# ################################################################################
|
||||||
|
|
||||||
template:
|
# template:
|
||||||
- trigger:
|
# - trigger:
|
||||||
- platform: time_pattern
|
# - platform: time_pattern
|
||||||
minutes: "/15"
|
# minutes: "/15"
|
||||||
- platform: homeassistant
|
# - platform: homeassistant
|
||||||
event: start
|
# event: start
|
||||||
- platform: event
|
# - platform: event
|
||||||
event_type: event_template_reloaded
|
# event_type: event_template_reloaded
|
||||||
action:
|
# action:
|
||||||
- variables:
|
# - variables:
|
||||||
# weekday(): Mon=0 … Sun=6. Weekend → next Monday.
|
# # weekday(): Mon=0 … Sun=6. Weekend → next Monday.
|
||||||
school_monday: >-
|
# school_monday: >-
|
||||||
{% set wd = now().weekday() %}
|
# {% set wd = now().weekday() %}
|
||||||
{% if wd >= 5 %}
|
# {% if wd >= 5 %}
|
||||||
{{ (today_at() + timedelta(days=(7 - wd))).isoformat() }}
|
# {{ (today_at() + timedelta(days=(7 - wd))).isoformat() }}
|
||||||
{% else %}
|
# {% else %}
|
||||||
{{ (today_at() - timedelta(days=wd)).isoformat() }}
|
# {{ (today_at() - timedelta(days=wd)).isoformat() }}
|
||||||
{% endif %}
|
# {% endif %}
|
||||||
- action: calendar.get_events
|
# - action: calendar.get_events
|
||||||
target:
|
# target:
|
||||||
entity_id:
|
# entity_id:
|
||||||
- calendar.bhs_lunch_menu
|
# - calendar.bhs_lunch_menu
|
||||||
- calendar.bis_school_lunches
|
# - calendar.bis_school_lunches
|
||||||
- calendar.hms_lunch_menu
|
# - calendar.hms_lunch_menu
|
||||||
data:
|
# data:
|
||||||
start_date_time: "{{ school_monday }}"
|
# start_date_time: "{{ school_monday }}"
|
||||||
duration:
|
# duration:
|
||||||
days: 5
|
# days: 5
|
||||||
response_variable: agenda
|
# response_variable: agenda
|
||||||
sensor:
|
# sensor:
|
||||||
- name: "BHS Lunch Menu Week"
|
# - name: "BHS Lunch Menu Week"
|
||||||
unique_id: bhs_lunch_menu_week
|
# unique_id: bhs_lunch_menu_week
|
||||||
icon: mdi:food-apple
|
# icon: mdi:food-apple
|
||||||
state: "{{ agenda['calendar.bhs_lunch_menu'].events | count }}"
|
# state: "{{ agenda['calendar.bhs_lunch_menu'].events | count }}"
|
||||||
attributes:
|
# attributes:
|
||||||
events: "{{ agenda['calendar.bhs_lunch_menu'].events }}"
|
# events: "{{ agenda['calendar.bhs_lunch_menu'].events }}"
|
||||||
week_start: "{{ school_monday }}"
|
# week_start: "{{ school_monday }}"
|
||||||
|
|
||||||
- name: "BIS Lunch Menu Week"
|
# - name: "BIS Lunch Menu Week"
|
||||||
unique_id: bis_lunch_menu_week
|
# unique_id: bis_lunch_menu_week
|
||||||
icon: mdi:food-apple
|
# icon: mdi:food-apple
|
||||||
state: "{{ agenda['calendar.bis_school_lunches'].events | count }}"
|
# state: "{{ agenda['calendar.bis_school_lunches'].events | count }}"
|
||||||
attributes:
|
# attributes:
|
||||||
events: "{{ agenda['calendar.bis_school_lunches'].events }}"
|
# events: "{{ agenda['calendar.bis_school_lunches'].events }}"
|
||||||
week_start: "{{ school_monday }}"
|
# week_start: "{{ school_monday }}"
|
||||||
|
|
||||||
- name: "HMS Lunch Menu Week"
|
# - name: "HMS Lunch Menu Week"
|
||||||
unique_id: hms_lunch_menu_week
|
# unique_id: hms_lunch_menu_week
|
||||||
icon: mdi:food-apple
|
# icon: mdi:food-apple
|
||||||
state: "{{ agenda['calendar.hms_lunch_menu'].events | count }}"
|
# state: "{{ agenda['calendar.hms_lunch_menu'].events | count }}"
|
||||||
attributes:
|
# attributes:
|
||||||
events: "{{ agenda['calendar.hms_lunch_menu'].events }}"
|
# events: "{{ agenda['calendar.hms_lunch_menu'].events }}"
|
||||||
week_start: "{{ school_monday }}"
|
# week_start: "{{ school_monday }}"
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user